Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 69 additions & 30 deletions app/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useRecall } from '@/lib/recall/use-recall';
import Marginalia from '@/components/progression/Marginalia';
import { useProgression } from '@/lib/progression/use-progression';
import ConfirmSheet from '@/components/ConfirmSheet';
import FirstNote from '@/components/FirstNote';
import DatePicker from '@/components/DatePicker';
import DueDateBadge from '@/components/DueDateBadge';
import LoadingIndicator from '@/components/LoadingIndicator';
Expand Down Expand Up @@ -68,7 +69,7 @@ import {
* It deliberately does not become a second Tasks screen. The task list here
* is this course's list and nothing else, with no filters and no sort toggle:
* it reads in the order the student dragged it into (what matters, until they
* do). The cross-course list, with its filters, its editing and its reading
* do), each open task numbered in the margin, and the number is the handle. The cross-course list, with its filters, its editing and its reading
* view, is still Tasks, and there is a link through to it at the foot of the
* section.
*/
Expand Down Expand Up @@ -143,6 +144,15 @@ export default function CoursePage() {
};
}, [courseTasks]);

// The open list is numbered, and the numbers are what carry a task, as soon
// as there are two to put in order.
const numbered = open.length > 1;
// Placed by hand (or by Claude's reorder_tasks) at least once.
const arranged = open.some((task) => typeof task.position === 'number');
// Rows outside the numbered list step in to its lane on a phone, where the
// numbers cannot hang out in the margin.
const lane = numbered ? 'pl-[22px] md:pl-0' : '';

function openRow(task: Task, course: Course) {
return (
<TaskRow
Expand Down Expand Up @@ -463,11 +473,23 @@ export default function CoursePage() {
drop the course column they would otherwise all repeat. */}
<section>
<div className="flex items-baseline justify-between gap-3 pb-2">
<p className="eyebrow m-0">
Tasks
<span className="ml-1.5 font-mono tracking-normal text-ink-soft">
{open.length}
<p className="m-0 flex items-baseline gap-2.5">
<span className="eyebrow">
Tasks
<span className="ml-1.5 font-mono tracking-normal text-ink-soft">
{open.length}
</span>
</span>
{/* Which order the numbers are, in the margin: the student's
own once anything has been placed, "what matters" until. */}
{numbered && (
<span
className="font-hand text-[15px] leading-none text-muted"
style={{ transform: 'rotate(-2deg)' }}
>
{arranged ? 'in your order' : 'most pressing first'}
</span>
)}
</p>
{done.length > 0 && (
<button
Expand All @@ -481,40 +503,57 @@ export default function CoursePage() {
)}
</div>

{/* Gone for good the moment anything has been placed, read off
the tasks themselves rather than a dismissed flag. */}
{numbered && !arranged && (
<FirstNote className="mb-4 mt-2">
These read most pressing first: high priority, then soonest due. Drag
a number to put them in the order you mean to do them. The order
stays, here and for Claude.
</FirstNote>
)}

{/* The rows are written on the page; their wash reaches a little
past the column on either side, the way a highlighter does. */}
<div className="-mx-[15px]">
{/* Carried from the grip in the margin: every other part of a
row already does something when pressed. */}
{open.length > 1 ? (
<ReorderList
items={open}
getId={(task) => task.id}
getLabel={(task) => task.title}
label={`${course.code} tasks, in the order you arranged them`}
shape="row"
carry="grip"
className=""
onReorder={moveTask}
renderItem={(task) => openRow(task, course)}
/>
{/* Carried from the number in the margin: every other part of a
row already does something when pressed. From `md` the
numbers hang out in the page margin so the rows keep the
column; on a phone there is no margin to spare, so they take
a lane and the rows below step in to match. */}
{numbered ? (
<div className="md:-ml-[22px]">
<ReorderList
items={open}
getId={(task) => task.id}
getLabel={(task) => task.title}
label={`${course.code} tasks, in the order you arranged them`}
shape="row"
carry="grip"
numbered
className=""
onReorder={moveTask}
renderItem={(task) => openRow(task, course)}
/>
</div>
) : (
open.map((task) => <div key={task.id}>{openRow(task, course)}</div>)
)}

{showDone &&
done.map((task) => (
<TaskRow
key={task.id}
task={task}
course={course}
hideCourse
onToggle={toggleTask}
onStartTimer={(t, el) => setStartTarget({ task: t, course, anchor: el })}
onOpen={openTask}
onDelete={(t) => removeTask(t.id)}
ground="page"
/>
<div key={task.id} className={lane}>
<TaskRow
task={task}
course={course}
hideCourse
onToggle={toggleTask}
onStartTimer={(t, el) => setStartTarget({ task: t, course, anchor: el })}
onOpen={openTask}
onDelete={(t) => removeTask(t.id)}
ground="page"
/>
</div>
))}

{open.length === 0 && !showDone && !adding && (
Expand Down
74 changes: 71 additions & 3 deletions components/ReorderList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ interface Props<T> {
label: string;
shape?: Shape;
carry?: CarryStart;
/**
* Rows only: the grip is the item's place in the list, written in the
* margin in mono, and the strokes show under a cursor on the number itself.
* The number takes a 22px lane of its own in the flow, so it reaches a
* phone too, where a grip hung out past the row would be off the glass.
* While an item is carried every number reads where it would land.
*/
numbered?: boolean;
/** Spacing between items, as the caller's own list would set it. */
className?: string;
}
Expand Down Expand Up @@ -160,6 +168,7 @@ export default function ReorderList<T>({
label,
shape = 'sheet',
carry: carryStart = 'press',
numbered = false,
className = 'gap-3',
}: Props<T>) {
const reducedMotion = usePrefersReducedMotion();
Expand Down Expand Up @@ -481,6 +490,14 @@ export default function ReorderList<T>({
return 0;
}

/** The place an item reads as, counting from 1, where it would be if let go now. */
function placeFor(index: number): number {
if (!carry || !carry.slots[carry.from] || index >= carry.slots.length) return index + 1;
if (index === carry.from) return carry.to + 1;
const shift = shiftFor(index);
return shift < 0 ? index : shift > 0 ? index + 2 : index + 1;
}

const landingTop = (() => {
if (!carry) return 0;
const { from, to, slots, listTop } = carry;
Expand All @@ -492,6 +509,7 @@ export default function ReorderList<T>({

const ease = 'cubic-bezier(0.2, 0.7, 0.2, 1)';
const sheet = shape === 'sheet';
const lane = numbered && !sheet;

return (
<div className="relative">
Expand Down Expand Up @@ -530,7 +548,7 @@ export default function ReorderList<T>({
ref={(node) => {
itemRefs.current[index] = node;
}}
className="group/reorder relative"
className={`group/reorder relative ${lane ? 'flex items-stretch' : ''}`}
onPointerDown={(event) => handlePointerDown(event, index)}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
Expand Down Expand Up @@ -567,7 +585,56 @@ export default function ReorderList<T>({
event until then, so on a touch screen, where nothing ever
hovers, a tap in that spot still does what it always did and
a finger rearranges by pressing and holding. Under `grip`
there is nothing else to press, so they stay faintly there. */}
there is nothing else to press, so they stay faintly there.
A numbered row writes its place there instead, and the
number is what is taken hold of. */}
{lane ? (
<button
type="button"
data-reorder-grip
aria-label={`Reorder ${getLabel(item)}, ${index + 1} of ${ordered.length}`}
aria-keyshortcuts="ArrowUp ArrowDown"
title="Drag, or press the up and down arrow keys"
onKeyDown={(event) => handleGripKey(event, index)}
className="group/grip relative z-20 w-[22px] shrink-0 bg-transparent p-0"
style={{ cursor: carried ? 'grabbing' : 'grab', touchAction: 'none' }}
>
<span
aria-hidden
className={`absolute inset-0 flex items-center justify-center font-mono text-[11.5px] tabular-nums transition-[opacity,color] duration-150 ${
carried
? 'font-semibold text-ink'
: `${carry ? '' : '[@media(hover:hover)]:group-hover/grip:opacity-0 group-focus-visible/grip:opacity-0'} ${
placeFor(index) === 1 ? 'text-ink-soft' : 'text-muted'
}`
}`}
>
{placeFor(index)}
</span>
{/* The carried row keeps its number, the place it would land. */}
<span
aria-hidden
className={`absolute inset-0 flex items-center justify-center text-muted opacity-0 transition-opacity duration-150 ${
carry ? '' : '[@media(hover:hover)]:group-hover/grip:opacity-100 group-focus-visible/grip:opacity-100'
}`}
>
<svg width="9" height="18" viewBox="0 0 9 18" fill="none" aria-hidden>
<path
d="M3.1 2.5 Q2.2 6.5 3 10 T2.7 15.5"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
<path
d="M6.4 2.8 Q5.6 7 6.3 10.5 T6 15.2"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
</svg>
</span>
</button>
) : (
<button
type="button"
data-reorder-grip
Expand Down Expand Up @@ -618,7 +685,8 @@ export default function ReorderList<T>({
</svg>
)}
</button>
{renderItem(item, index)}
)}
{lane ? <div className="min-w-0 flex-1">{renderItem(item, index)}</div> : renderItem(item, index)}
</li>
);
})}
Expand Down
22 changes: 22 additions & 0 deletions readmedesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -2422,6 +2422,20 @@ Both paths have a keyboard twin: the grip is a real button, and up and down
arrows move the item one place, with every move said out loud through a
polite live region.

A row list can be **numbered** (`numbered`), and the course page's open tasks
are. The grip is then the item's place, written in mono (digits, so mono is
right) in a 22px lane of its own: `1` in `ink-soft`, the rest `muted`. Under
a cursor on the number it gives way to the two pencil strokes; on a touch
screen nothing hovers, so the number simply stays and is what a finger takes
hold of. The lane is in the flow rather than hung off the row, because a grip
hung 18px past a row that is already pulled into the gutter sat off the glass
on a phone, which left a course's tasks with no way to be reordered there.
From `md` the list is pulled back by the lane so the numbers sit in the page
margin and the rows keep the column; below `md` they take the lane, and the
finished rows under them step in to match. While a row is carried every
number reads the place it would land in, the carried one in `ink` at 600,
so the new order is visible before letting go.

### Study: the reader

`/notes` is Markd folded into the app, a reader for long study notes written
Expand Down Expand Up @@ -2703,6 +2717,14 @@ vocabulary, which used to open the page), then **Weak points** once Claude
has recorded any. The right column carries **Practice** (once there is a
paper) and **Sessions** with Log time; below `xl` they follow in that order.

**Tasks** is numbered once it holds two open tasks, and the numbers carry
them (see Rearranging by hand). Beside the `.eyebrow` and its count, a few
words in Caveat say which order the numbers are: "most pressing first" until
anything has been placed, "in your order" after, read off whether any open
task has a position (a drag here, or Claude's `reorder_tasks`). Until then a
`FirstNote` under the header says what the order is and that dragging a
number changes it, and it goes for good with the first placement.

**Where the grade stands**, on a course with no scheme, says "Akada does not
know how ECON 100 is marked yet." over two equal buttons. **Type it in**
opens the scheme editor already holding the usual pieces (Quizzes,
Expand Down
Loading