diff --git a/DESIGN.md b/DESIGN.md index a000c05..9181eb1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -315,10 +315,10 @@ shadow is reserved for things genuinely *above* the page. Nothing has a resting itself, and the window only exists under `?frame`. ### Shadow Vocabulary -- **Drawer / card** (`0 0 0 1px var(--line), 0 10px 26px -12px oklch(0.25 0.03 262 / 0.30)`): an open unit row and a compose card — the two things that lift out of a list in place. +- **Drawer / card** (`0 0 0 1px var(--line), 0 10px 26px -12px oklch(0.25 0.03 262 / 0.30)`): an open unit row and the *request* card — the two things that lift out of a list in place. The goal composer is not one of them: it belongs to no list, so it is a modal. - **Popover / menu** (`0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 18px 40px -10px oklch(0.25 0.03 262 / 0.34)`): the ⊕ menu and the account popover. - **Floating action** (`0 2px 4px oklch(0.25 0.03 262 / 0.20), 0 10px 22px -6px oklch(0.35 0.12 264 / 0.45)`): the ⊕, whose shadow carries the accent hue because the button is accent-filled. -- **Modal** (`0 2px 6px …, 0 24px 60px -14px oklch(0.25 0.03 262 / 0.4)`): the space picker over its scrim. +- **Modal** (`0 2px 6px …, 0 24px 60px -14px oklch(0.25 0.03 262 / 0.4)`): the space picker and the goal composer, each over its scrim. - **Window** (`0 1px 1px …, 0 18px 34px -14px …, 0 44px 90px -30px …`): the framed app and the sign-in card, on the desk. In dark mode every one of these gives way to a single deeper, softer shadow and the work is done by tone @@ -378,19 +378,30 @@ fill *is* the state (a pressed version button, an "on" auto-review switch). ### The ⊕ (signature component) A 46px accent disc pinned to the pane's bottom-right with an accent-tinted shadow; it lifts 1.5px on hover -and rotates its cross 135° into an ✕ when its menu is open. It makes an item of the list you are in — a -goal on a project's list, a request everywhere else — and on a smart list it is absent rather than inert. -Its menu is generated from the space's own registry records, so what it offers is data rather than markup. -`⌘N` is the same button. +and rotates its cross 135° into an ✕ when its menu is open. It is present on every view of an open space, +and its menu always opens with **New goal** — a goal is the one record a human writes from scratch, and +one that could only be written from a project's own goal list was one seven of the ten views had no way +to write at all. Below that, separated by a hairline, it still makes an item of the list you are in: on a +goal, a request of one of the registry's goal-scoped types; on System, a project-scoped one. That section +is generated from the space's own registry records, so what it offers is data rather than markup. `⌘N` is +the same button — and `n` is the goal item alone, from anywhere. + +The rule this replaces was "it makes an item of the list you are in, and on a smart list it is absent +rather than inert". It was coherent and it cost the product its one authored record: on `/me`, on the +Logbook, on `/space`, there was no ⊕ and so no way to write down the thing you had just thought of. The +goal is now the constant and the list's own item is the variable. ### The keyboard map -`/` puts the caret in quick find, `⌘N` is the ⊕ of the view being shown, Escape closes one layer per +`/` puts the caret in quick find, `n` writes a goal from anywhere, `⌘N` is the ⊕ of the view being shown, +Escape closes one layer per press — and `?` draws the map itself, over whatever is already open. It is the picker's paper card on the same scrim: a group heading per surface, a sans keycap per chord (`Esc` is a label printed on a key, not a machine identifier, so keycaps are never mono), and one plain sentence beside each. The pane bar carries a keyboard button next to the appearance toggle, titled with the key it stands for — a shortcut nobody can find is a shortcut nobody has. While the map is up it holds the keyboard: nothing behind it -answers a key, and Escape takes the map away before it takes anything else. +answers a key, and Escape takes the map away before it takes anything else. The same rule makes `n` inert +under the space picker, and inert while a card already open has something typed into it — a bare letter is +reachable by accident, and opening a second card is how the first one is abandoned. ### The URI chip Mono, 11.5px, sunk, 4px radius, `cursor: copy`, flashing sage on copy. It is how a record's identity stays diff --git a/packages/ui/src/app.css b/packages/ui/src/app.css index aef0221..93ef490 100644 --- a/packages/ui/src/app.css +++ b/packages/ui/src/app.css @@ -625,8 +625,10 @@ select.sl { resize: none; cursor: pointer; padding-right: 8px; } animation: pop 300ms var(--ease-out); } @keyframes pop { from { opacity: 0; transform: translateY(-6px) scale(0.995); } } -.newreq h3 { margin: 0 0 3px; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; } -.newreq .hint { margin: 0 0 11px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); } +/* The goal composer is a modal (`.scrim .newgoal`) rather than a card in the page, but it is the same + object saying the same things, so the heading and the hint under it are drawn once for both. */ +.newreq h3, .newgoal h3 { margin: 0 0 3px; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; } +.newreq .hint, .newgoal .hint { margin: 0 0 11px; font-size: 12.5px; line-height: 1.5; color: var(--ink-2); } /* Not scoped to the card: the space composer lives inside the picker, which is already a raised surface, so it borrows these fields and none of the card's chrome. */ .fields { display: grid; gap: 9px; } @@ -826,7 +828,11 @@ select.sl { resize: none; cursor: pointer; padding-right: 8px; } position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 24px; background: oklch(0.2 0.03 262 / 0.34); overflow-y: auto; } -.scrim .picker { +/* Two cards on the one scrim: the space picker, and the goal composer. The composer is here rather + than in a page because the ⊕ offers a goal on every view of a space — and because a card injected + into a goal list shoves every row under it down as it opens, which was the visible bug. Off the + flow, nothing behind it moves. */ +.scrim .picker, .scrim .newgoal { width: min(560px, 100%); max-width: none; background: var(--paper); border: 1px solid var(--line); border-radius: 13px; padding: 24px 26px 26px; box-shadow: 0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 24px 60px -14px oklch(0.25 0.03 262 / 0.4); diff --git a/packages/ui/src/lib/components/NewGoal.svelte b/packages/ui/src/lib/components/NewGoal.svelte index 1649099..0388f81 100644 --- a/packages/ui/src/lib/components/NewGoal.svelte +++ b/packages/ui/src/lib/components/NewGoal.svelte @@ -1,9 +1,15 @@ -
The fixture has no repo behind it. Open a live space to write one.
+ + ++ This space has no projects yet. An admin writes the first one, on + the space's own page. +
+ {:else} ++ Every project in this space is archived. + Add one on the space's own page. +
+ {/if} + + {:else if !writable} + + {#if space.fixture} +The fixture has no repo behind it. Open a live space to write one.
+ {:else} ++ + to write a goal. +
+ {/if} + {:else} -- - to write a goal. -
- {/if} - - {:else} -