Refine header emphasis, field-on-layer treatment, and narrow-pane control collapse

This commit is contained in:
2026-06-10 14:14:02 +03:00
parent 2410c6e965
commit 68a044752f
18 changed files with 478 additions and 88 deletions
+30
View File
@@ -29,6 +29,10 @@ export type IconName =
| 'add' // add / create-new — Carbon Add
| 'search' // live library search — Carbon Search
| 'settings' // per-pane settings disclosure (gear) — Carbon Settings
| 'import' // import a workspace file — Carbon Upload (a file goes in)
| 'export' // export the workspace to a file — Carbon Download (a file comes out)
| 'info' // about / information — Carbon Information (outline)
| 'revert' // revert draft to last published — Carbon Reset
// Pane-toggle sub-family (spec §01A): a panel frame with one region filled, so the
// glyph shows *which* pane it controls by position (left / centre / right).
| 'pane-library' // toggle the library pane (left)
@@ -69,6 +73,32 @@ const GLYPHS: Record<IconName, ReactNode> = {
<rect x="12" y="2" width="8" height="2" />
</>
),
// Carbon Upload — open tray with an up arrow (a file is brought into the app).
import: (
<>
<path d="M26,24v4H6V24H4v4a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V24Z" />
<path d="M6,12l1.41,1.41L15,5.83V24h2V5.83l7.59,7.59L26,12,16,2Z" />
</>
),
// Carbon Download — open tray with a down arrow (a file is written out).
export: (
<>
<path d="M26,24v4H6V24H4v4a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V24Z" />
<path d="M26,14l-1.41-1.41L17,20.17V2H15V20.17l-7.59-7.58L6,14l10,10Z" />
</>
),
// Carbon Information (outline) — ring + "i".
info: (
<>
<path d="M17,22V14H13v2h2v6H12v2h8V22Z" />
<path d="M16,8a1.5,1.5,0,1,0,1.5,1.5A1.5,1.5,0,0,0,16,8Z" />
<path d="M16,30A14,14,0,1,1,30,16,14,14,0,0,1,16,30ZM16,4A12,12,0,1,0,28,16,12,12,0,0,0,16,4Z" />
</>
),
// Carbon Reset — a circular arrow, "return to the prior state".
revert: (
<path d="M18,28A12,12,0,1,0,6,16v6.2L2.41,18.59,1,20l6,6,6-6L11.59,18.59,8,22.2V16A10,10,0,1,1,18,26Z" />
),
dataset: (
<>
<rect x="8" y="18" width="4" height="2" />
+10 -1
View File
@@ -10,13 +10,22 @@
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: flex-end;
/* Fit control hugs the left, settings gear trails right (mirrors the editor
toolbar). It used to be flex-end, which made any overflow trim the *left*
item — clipping "Original". Left-aligned, the gear is the trimmable end, and
the preview's min-width is sized so it never actually overflows. */
justify-content: flex-start;
gap: var(--space-3);
height: 40px;
padding: 0 var(--space-4);
border-bottom: var(--border-width) solid var(--border);
}
/* Push the settings gear (the last child) to the far right. */
.header > :last-child {
margin-left: auto;
}
.body {
position: relative; /* establishes stacking context for the busy overlay */
flex: 1 1 auto;
+34 -9
View File
@@ -2,6 +2,10 @@
display: flex;
flex-direction: column;
height: 100%;
/* Size-query container so the Create CTA can respond to the pane's own width.
`inline-size` (not `size`) leaves block-size uncontained, so the metadata
panel's percentage max-height and the list's flex height still resolve. */
container-type: inline-size;
}
/* Library toolbar — heading + date-format settings gear (spec §02 / §07). */
@@ -39,6 +43,9 @@
color: var(--accent-contrast);
font: inherit;
font-weight: 600;
/* The label never wraps: above the collapse width the pane is wide enough for one
line; below it, only the icon shows. */
white-space: nowrap;
cursor: pointer;
transition: background var(--dur-fast) var(--ease);
}
@@ -47,6 +54,16 @@
background: var(--accent-hover);
}
/* Narrow pane → the CTA sheds its label and centres the "+" icon (the button stays
full-width, so it keeps a large target). The threshold sits comfortably above the
216px pane minimum, so the full label is shown across the normal width range and
only collapses when the pane is dragged tight. */
@container (max-width: 250px) {
.createNewLabel {
display: none;
}
}
/* Search + Sort row, pinned above the list (spec §02). */
.controls {
flex: 0 0 auto;
@@ -74,6 +91,10 @@
.searchInput {
width: 100%;
/* Let the input shrink with its flex slot: an <input>'s default min-width is its
intrinsic content width (~20ch), which otherwise overflowed the slot and slid
over the Sort trigger at the pane minimum. */
min-width: 0;
height: 32px;
/* Room for the leading magnifier and the trailing clear button. */
padding: 0 32px 0 32px;
@@ -344,10 +365,14 @@
.metaComment {
width: 100%;
padding: var(--space-2) var(--space-3);
border: var(--border-width) solid var(--border);
/* A field is a layer *on top of* the panel it sits on (Carbon layering,
arch 09 §3.2): on the gray `--layer-01` panel the field goes white with a
strong border — matching the search input above — rather than a darker gray,
which stacked three muddy grays. */
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--layer-02, var(--background));
color: var(--text-primary);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
@@ -390,7 +415,7 @@
.metaTimes dd {
margin: 0;
color: var(--text-primary);
color: var(--text);
white-space: nowrap;
}
@@ -434,10 +459,11 @@
flex: 1 1 auto;
height: 32px;
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border);
/* White on the gray panel, same field-on-layer treatment as the inputs above. */
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--background);
color: var(--text-primary);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 12px;
font-weight: 500;
@@ -449,8 +475,7 @@
}
.metaAction:hover {
background: var(--layer-02, var(--layer-01));
border-color: var(--border-strong, var(--border));
background: var(--layer-02);
}
.metaDanger:hover {
+11 -2
View File
@@ -254,8 +254,17 @@ export function SnippetLibrary() {
{/* Create raises no toast: the new snippet opens in the editor, so the
result is already on-screen (spec §02; docs/architecture/10 → Toast
copy). Delete/duplicate toast because the outcome isn't visible. */}
<button className={styles.createNew} onClick={() => createSnippet()}>
<Icon name="add" /> Create New Snippet
{/* Primary CTA. When the library pane is dragged narrow the label collapses
to just the "+" icon (a @container query) so it never wraps to two lines;
`aria-label` keeps the accessible name when the text is hidden. */}
<button
className={styles.createNew}
onClick={() => createSnippet()}
aria-label="Create New Snippet"
title="Create New Snippet"
>
<Icon name="add" />
<span className={styles.createNewLabel}>Create New Snippet</span>
</button>
{/* Search + Sort controls, pinned above the list (spec §02; council
+8 -2
View File
@@ -2,15 +2,21 @@
.wrap {
position: relative;
/* Never shrink: the search box (flex: 1) absorbs the slack, so the sort trigger
keeps its size instead of being squeezed into the search input (which used to
overlap it at the pane minimum). */
flex: 0 0 auto;
display: inline-flex;
}
/* Trigger — shows the current field + direction for recognition (council SORT). */
/* Trigger — shows the current field + direction for recognition (council SORT).
32px tall to match the search input it sits beside (they shared a row at
mismatched 28/32px heights before). */
.trigger {
display: inline-flex;
align-items: center;
gap: var(--space-1);
height: 28px;
height: 32px;
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
+13 -5
View File
@@ -4,9 +4,11 @@
* A disclosure + non-modal popover, the same APG primitive as SettingsPopover
* (docs/architecture/10 → "settings are … a disclosure popover") — deliberately
* NOT an ARIA menu. It differs from SettingsPopover only in its **trigger**: per
* NN/g recognition-over-recall the trigger shows the current state ("Sort:
* Modified ↓") instead of a bare gear, so the user reads the order without
* opening the popover. The body is a labelled `group` of field buttons (radio
* NN/g recognition-over-recall the trigger shows the current state ("Modified ↓")
* instead of a bare gear, so the user reads the order without opening the popover.
* The visible text drops the "Sort:" verb prefix to stay compact in the narrow
* library rail; the full name rides in `aria-label` (arch 10 §8). The body is a
* labelled `group` of field buttons (radio
* group), each showing a direction arrow on the active field.
*
* Selection model (spec §02): re-selecting the ACTIVE field flips direction;
@@ -121,11 +123,17 @@ export function SortControl() {
className={styles.trigger}
aria-expanded={open}
aria-controls={ID}
// Trigger shows current state for recognition (council SORT, NN/g #6).
// Trigger shows current field + direction for recognition (council SORT,
// NN/g #6) — but without a "Sort:" prefix, so it stays compact beside the
// search box in the narrow library pane (the prefix wasted ~36px and
// crowded search at the pane minimum). The accessible name keeps the verb
// ("Sort by Modified, descending") via aria-label, since the trimmed
// visible text ("Modified ↓") would otherwise read as a bare field name.
aria-label={`Sort by ${labelFor(sortBy)}, ${sortOrder === 'desc' ? 'descending' : 'ascending'}`}
title={`Sort by ${labelFor(sortBy)}, ${sortOrder === 'desc' ? 'descending' : 'ascending'}`}
onClick={() => toggle(ID)}
>
Sort: {labelFor(sortBy)} <span aria-hidden="true">{arrow}</span>
{labelFor(sortBy)} <span aria-hidden="true">{arrow}</span>
</button>
{open &&
createPortal(
+40
View File
@@ -13,6 +13,10 @@
height: 40px;
padding: 0 var(--space-4);
border-bottom: var(--border-width) solid var(--border);
/* Size-query container so the actions below respond to the *pane's* width (the
toolbar spans it), not the viewport's. Scoped to the toolbar — not the pane —
to keep size containment well clear of the Monaco editor's own layout. */
container-type: inline-size;
}
.spacer {
@@ -47,6 +51,42 @@
cursor: default;
}
/* Collapsible secondary actions (Extract, Revert): label by default, icon-only
when the pane is narrow. Both children live in the DOM always; the container
query swaps which one shows, so there's no reflow cost beyond display. */
.collapsible {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
}
.actionIcon {
display: none;
}
.actionLabel {
display: inline;
white-space: nowrap;
}
/* Below this pane width the labels would crowd Publish / wrap, so the secondary
actions shed their text and become square icon buttons (the accessible name +
title keep them identifiable). 480px is comfortably above the 384px pane
minimum, so the collapse is in effect across the whole narrow range. */
@container (max-width: 480px) {
.collapsible {
width: 28px;
padding: 0;
}
.collapsible .actionLabel {
display: none;
}
.collapsible .actionIcon {
display: inline-flex;
}
}
.publish {
border-color: transparent;
background: var(--accent);
+15 -4
View File
@@ -33,6 +33,7 @@ import { notify } from '../stores/NotificationStore';
import { usePreviewStore } from '../stores/PreviewStore';
import { selectActiveSnippet, selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { useUserSettingsStore } from '../stores/UserSettingsStore';
import { Icon } from './Icon';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import {
NumberControl,
@@ -189,23 +190,33 @@ function EditorToolbar() {
<span className={styles.spacer} />
{/* Secondary actions collapse to icon-only when the editor pane is narrow
(a @container query in the CSS), so "Extract to Dataset" no longer wraps
to two lines and Revert/Publish stay on one row. The label rides in
`title` (and the accessible name) when only the icon shows. Publish — the
one primary action — keeps its label at every width. */}
{canExtract && (
<button
type="button"
className={styles.action}
className={`${styles.action} ${styles.collapsible}`}
onClick={() => openModal('extract')}
title="Extract inline data into a reusable dataset"
aria-label="Extract to Dataset"
>
Extract to Dataset
<Icon name="dataset" className={styles.actionIcon} />
<span className={styles.actionLabel}>Extract to Dataset</span>
</button>
)}
<button
type="button"
className={styles.action}
className={`${styles.action} ${styles.collapsible}`}
onClick={() => void handleRevert()}
disabled={activeId === null || !dirty}
title="Revert draft to the last published version"
aria-label="Revert"
>
Revert
<Icon name="revert" className={styles.actionIcon} />
<span className={styles.actionLabel}>Revert</span>
</button>
<button
type="button"