mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Refine header emphasis, field-on-layer treatment, and narrow-pane control collapse
This commit is contained in:
@@ -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,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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user