mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Show size and a draft indicator in snippet-library rows
This commit is contained in:
@@ -81,14 +81,9 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nameRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
@@ -96,14 +91,29 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Unpublished-draft indicator (spec §03D / §02 status). Floated into the row's
|
||||
left padding gutter and positioned out of flow so the title's start never
|
||||
shifts — dirty and clean rows stay aligned with each other and with the date. */
|
||||
/* Secondary metadata line (spec §02): status indicator · relative date · size. */
|
||||
.sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* Fixed-width leading slot for the status indicator. Always present (even when a
|
||||
snippet is published and shows no dot) so the date never shifts and rows stay
|
||||
aligned with one another. */
|
||||
.indicator {
|
||||
flex: 0 0 auto;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
/* Unpublished-draft indicator (spec §02 status; arch/10 §6 + council). The accent
|
||||
hue — not a warning colour, since unpublished work is a normal state — and the
|
||||
meaning rides on presence/absence + the accessible label, never colour alone. */
|
||||
.draftDot {
|
||||
position: absolute;
|
||||
left: calc(-1 * (var(--space-3) + var(--space-1)));
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* display:block so the 6px box applies — an inline span with no text content
|
||||
would ignore width/height and collapse to nothing. */
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
@@ -111,8 +121,12 @@
|
||||
}
|
||||
|
||||
.date {
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.delete {
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
* Snippet Library — the left pane (spec §02).
|
||||
*
|
||||
* M1 scope: the always-visible list with a pinned "Create New Snippet" item,
|
||||
* selection/highlight, and delete. Search, sort controls, the metadata panel,
|
||||
* status/dataset indicators, and the storage monitor arrive in later milestones.
|
||||
* selection/highlight, and delete. Each row carries a secondary metadata line —
|
||||
* draft status indicator, relative date, and size (spec §02). Search, sort
|
||||
* controls, the metadata panel, the dataset icon, and the storage monitor arrive
|
||||
* in later milestones.
|
||||
*/
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { hasUnpublishedChanges } from '@core/snippet';
|
||||
import { formatSnippetSize, hasUnpublishedChanges, snippetSizeBytes } from '@core/snippet';
|
||||
import { confirm } from '../stores/ConfirmStore';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import styles from './SnippetLibrary.module.css';
|
||||
@@ -63,40 +65,51 @@ export function SnippetLibrary() {
|
||||
No snippets yet — create your first one with the button above.
|
||||
</li>
|
||||
)}
|
||||
{ordered.map((s) => (
|
||||
<li key={s.id} className={`${styles.item} ${s.id === activeId ? styles.active : ''}`}>
|
||||
{/* The row's selectable area is a real <button> so it's keyboard
|
||||
operable (Enter/Space) and focusable — not a click-only <li>.
|
||||
Not a listbox option: APG forbids interactive children, and each
|
||||
row carries a delete button. */}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.itemMain}
|
||||
aria-current={s.id === activeId ? 'true' : undefined}
|
||||
onClick={() => selectSnippet(s.id)}
|
||||
>
|
||||
<span className={styles.nameRow}>
|
||||
{hasUnpublishedChanges(s) && (
|
||||
<span
|
||||
className={styles.draftDot}
|
||||
title="Has unpublished draft changes"
|
||||
aria-label="Has unpublished draft changes"
|
||||
/>
|
||||
)}
|
||||
{ordered.map((s) => {
|
||||
// Size is omitted under ~1 KB per spec §02; null collapses the suffix.
|
||||
const size = formatSnippetSize(snippetSizeBytes(s));
|
||||
const date = relativeDate(s.modified);
|
||||
return (
|
||||
<li key={s.id} className={`${styles.item} ${s.id === activeId ? styles.active : ''}`}>
|
||||
{/* The row's selectable area is a real <button> so it's keyboard
|
||||
operable (Enter/Space) and focusable — not a click-only <li>.
|
||||
Not a listbox option: APG forbids interactive children, and each
|
||||
row carries a delete button. */}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.itemMain}
|
||||
aria-current={s.id === activeId ? 'true' : undefined}
|
||||
onClick={() => selectSnippet(s.id)}
|
||||
>
|
||||
<span className={styles.name}>{s.name}</span>
|
||||
</span>
|
||||
<span className={styles.date}>{relativeDate(s.modified)}</span>
|
||||
</button>
|
||||
<button
|
||||
className={styles.delete}
|
||||
aria-label={`Delete ${s.name}`}
|
||||
title="Delete snippet"
|
||||
onClick={() => void handleDelete(s.id, s.name)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{/* Secondary metadata line (spec §02): status · date · size. The
|
||||
indicator sits in a fixed-width leading slot so the date stays
|
||||
aligned across rows whether or not a snippet is dirty. Draft is
|
||||
flagged by presence + label, never colour alone (arch/10 §6). */}
|
||||
<span className={styles.sub}>
|
||||
<span className={styles.indicator}>
|
||||
{hasUnpublishedChanges(s) && (
|
||||
<span
|
||||
className={styles.draftDot}
|
||||
title="Has unpublished draft changes"
|
||||
aria-label="Has unpublished draft changes"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.date}>{size ? `${date} · ${size}` : date}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={styles.delete}
|
||||
aria-label={`Delete ${s.name}`}
|
||||
title="Delete snippet"
|
||||
onClick={() => void handleDelete(s.id, s.name)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user