diff --git a/CHANGELOG.md b/CHANGELOG.md index fd12831..9ad331d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-09-01 + +The components the dashboard kept for itself, so both surfaces can share one +set. Everything from 0.1.0 keeps working unchanged. + +### Added + +- `Drawer`, a panel that comes in from either side at the full height of the + window. A modal is right for a question with an answer and wrong for work: it + floats in the middle, it is as tall as its contents, and it covers the thing + being worked on +- A failed read with the reason and a retry, a list placeholder that holds the + shape of what is coming, a dismissible action error, page navigation with the + helpers that decide which numbers to offer, themed line art, and a theme + switch +- `useTheme`, which reads the theme off the root element so a toggle renders the + right icon on first paint +- The severity scale and the ordered figure palette, so a severity means the + same colour wherever it is shown + +### Changed + +- `Tabs` carries an icon and a count, can turn an option off, and comes in three sizes, which is what kept + applications spelling the strip out by hand: a switcher whose options are not + all available has to say so on the option itself +- `Loading` takes the richer spinner and can name where the work is going, which + makes the second loading treatment unnecessary + ## [0.1.0] - 2026-08-30 First release. Versioned on its own, because nothing about the components is diff --git a/package.json b/package.json index 456736e..6f6d932 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sourceant/design", - "version": "0.1.0", + "version": "0.2.0", "description": "SourceAnt design system: tokens, primitives, and the graph renderer", "license": "MIT", "type": "module", diff --git a/src/assets/illustrations/all-the-data.svg b/src/assets/illustrations/all-the-data.svg new file mode 100644 index 0000000..a86be03 --- /dev/null +++ b/src/assets/illustrations/all-the-data.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/bug-fixing.svg b/src/assets/illustrations/bug-fixing.svg new file mode 100644 index 0000000..5299cdc --- /dev/null +++ b/src/assets/illustrations/bug-fixing.svg @@ -0,0 +1,175 @@ + + + + + + + + + + bug fixing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/code-inspection.svg b/src/assets/illustrations/code-inspection.svg new file mode 100644 index 0000000..4b1558f --- /dev/null +++ b/src/assets/illustrations/code-inspection.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/code-review.svg b/src/assets/illustrations/code-review.svg new file mode 100644 index 0000000..31c17db --- /dev/null +++ b/src/assets/illustrations/code-review.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/code-thinking.svg b/src/assets/illustrations/code-thinking.svg new file mode 100644 index 0000000..673c0fc --- /dev/null +++ b/src/assets/illustrations/code-thinking.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/code-typing.svg b/src/assets/illustrations/code-typing.svg new file mode 100644 index 0000000..d39160a --- /dev/null +++ b/src/assets/illustrations/code-typing.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/conceptual-idea.svg b/src/assets/illustrations/conceptual-idea.svg new file mode 100644 index 0000000..eda8b41 --- /dev/null +++ b/src/assets/illustrations/conceptual-idea.svg @@ -0,0 +1,36 @@ + + + conceptual_idea + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/connecting-teams.svg b/src/assets/illustrations/connecting-teams.svg new file mode 100644 index 0000000..57d9222 --- /dev/null +++ b/src/assets/illustrations/connecting-teams.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/control-panel.svg b/src/assets/illustrations/control-panel.svg new file mode 100644 index 0000000..7371cac --- /dev/null +++ b/src/assets/illustrations/control-panel.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/data-points.svg b/src/assets/illustrations/data-points.svg new file mode 100644 index 0000000..f9bd153 --- /dev/null +++ b/src/assets/illustrations/data-points.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/developer-activity.svg b/src/assets/illustrations/developer-activity.svg new file mode 100644 index 0000000..baeac27 --- /dev/null +++ b/src/assets/illustrations/developer-activity.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/engineering-team.svg b/src/assets/illustrations/engineering-team.svg new file mode 100644 index 0000000..204b894 --- /dev/null +++ b/src/assets/illustrations/engineering-team.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/file-searching.svg b/src/assets/illustrations/file-searching.svg new file mode 100644 index 0000000..92e45e6 --- /dev/null +++ b/src/assets/illustrations/file-searching.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/firmware.svg b/src/assets/illustrations/firmware.svg new file mode 100644 index 0000000..74449db --- /dev/null +++ b/src/assets/illustrations/firmware.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/for-review.svg b/src/assets/illustrations/for-review.svg new file mode 100644 index 0000000..ad23aa5 --- /dev/null +++ b/src/assets/illustrations/for-review.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/good-team.svg b/src/assets/illustrations/good-team.svg new file mode 100644 index 0000000..6fc03f5 --- /dev/null +++ b/src/assets/illustrations/good-team.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/mind-map.svg b/src/assets/illustrations/mind-map.svg new file mode 100644 index 0000000..5dee1a5 --- /dev/null +++ b/src/assets/illustrations/mind-map.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/programming.svg b/src/assets/illustrations/programming.svg new file mode 100644 index 0000000..05c41eb --- /dev/null +++ b/src/assets/illustrations/programming.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/secure-server.svg b/src/assets/illustrations/secure-server.svg new file mode 100644 index 0000000..2c66485 --- /dev/null +++ b/src/assets/illustrations/secure-server.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/server-status.svg b/src/assets/illustrations/server-status.svg new file mode 100644 index 0000000..431e4c5 --- /dev/null +++ b/src/assets/illustrations/server-status.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/version-control.svg b/src/assets/illustrations/version-control.svg new file mode 100644 index 0000000..350d544 --- /dev/null +++ b/src/assets/illustrations/version-control.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/ActionError.vue b/src/components/ActionError.vue new file mode 100644 index 0000000..6aa3583 --- /dev/null +++ b/src/components/ActionError.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/Drawer.vue b/src/components/Drawer.vue new file mode 100644 index 0000000..26908d6 --- /dev/null +++ b/src/components/Drawer.vue @@ -0,0 +1,120 @@ + + + diff --git a/src/components/Illustration.vue b/src/components/Illustration.vue new file mode 100644 index 0000000..13204ab --- /dev/null +++ b/src/components/Illustration.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/components/LoadFailure.vue b/src/components/LoadFailure.vue new file mode 100644 index 0000000..3758169 --- /dev/null +++ b/src/components/LoadFailure.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/Loading.vue b/src/components/Loading.vue index e2995a7..2e1615b 100644 --- a/src/components/Loading.vue +++ b/src/components/Loading.vue @@ -1,6 +1,5 @@ diff --git a/src/components/Pagination.vue b/src/components/Pagination.vue new file mode 100644 index 0000000..86fc602 --- /dev/null +++ b/src/components/Pagination.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/components/SkeletonList.vue b/src/components/SkeletonList.vue new file mode 100644 index 0000000..9e779d4 --- /dev/null +++ b/src/components/SkeletonList.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/Tabs.vue b/src/components/Tabs.vue index de2fda8..3f73ef8 100644 --- a/src/components/Tabs.vue +++ b/src/components/Tabs.vue @@ -1,15 +1,29 @@ diff --git a/src/components/ThemeToggle.vue b/src/components/ThemeToggle.vue new file mode 100644 index 0000000..6f355aa --- /dev/null +++ b/src/components/ThemeToggle.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/composables/useContextKinds.ts b/src/composables/useContextKinds.ts index 207e3c2..73d826e 100644 --- a/src/composables/useContextKinds.ts +++ b/src/composables/useContextKinds.ts @@ -10,6 +10,7 @@ * rather than assembled. */ const TONE: Record = { + intent: 'bg-fuchsia-600/10 text-fuchsia-600 border-fuchsia-600/20', decision: 'bg-blue-600/10 text-blue-600 border-blue-600/20', constraint: 'bg-amber-600/10 text-amber-600 border-amber-600/20', workaround: 'bg-red-600/10 text-red-600 border-red-600/20', @@ -21,6 +22,7 @@ const TONE: Record = { } const FILL: Record = { + intent: 'bg-fuchsia-600', decision: 'bg-blue-600', constraint: 'bg-amber-600', workaround: 'bg-red-600', @@ -40,19 +42,41 @@ const FILL: Record = { * colourblind reader, and review against discussion was hard to tell apart with * full colour vision. * - * Eight hues cannot all be separated from one another, which is a fact about - * eight rather than about these eight: red-green colourblindness collapses pink - * onto green whatever shades are picked. These pass every check except one pair - * of neighbours, and a drawing that needs more than colour to be read needs a - * second channel rather than better colours. + * Nine hues cannot all be separated from one another, which is a fact about + * nine rather than about these nine: red-green colourblindness collapses pink + * onto green whatever shades are picked. A drawing that needs more than colour + * to be read needs a second channel rather than better colours. + * + * They are spaced by hue rather than picked by eye. The two widest gaps left + * were near 123 and 298 degrees; intent took 298 because the other falls in the + * green that collapses onto pink. */ const INK: Record = { system: '#7c3aed', part: '#d97706', contract: '#0891b2', + intent: '#c026d3', decision: '#2563eb', constraint: '#d97706', workaround: '#dc2626', convention: '#7c3aed', pattern: '#059669', discussion: '#0891b2', review: '#db2777', plan: '#65a30d', } +const OPTIONS = [ + 'Intent', 'Decision', 'Constraint', 'Workaround', 'Convention', + 'Pattern', 'Discussion', 'Review', 'Plan', +] as const + +/** What each kind is for, in the words a person picking one needs. */ +const DESCRIBED: Record = { + Intent: 'What this exists to do, who for, and what it deliberately is not', + Decision: 'A deliberate choice made about architecture, tooling, or approach', + Constraint: 'A hard limitation imposed by external factors or requirements', + Workaround: 'A temporary fix for a known issue or limitation', + Convention: 'An established principle or standard the team follows', + Pattern: 'An observed model, recurring choice, or emergent practice', + Discussion: 'Key points from a team discussion or debate', + Review: 'Insights surfaced during code review', + Plan: 'A future intention or roadmap item', +} + export function useContextKinds() { const NEUTRAL = 'bg-muted text-muted-foreground border-border' @@ -74,5 +98,5 @@ export function useContextKinds() { color: INK[kind]!, })) - return { tone, fill, ink, legend, kinds: Object.keys(TONE) } + return { tone, fill, ink, legend, kinds: Object.keys(TONE), kindOptions: OPTIONS, kindDescriptions: DESCRIBED } } diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts new file mode 100644 index 0000000..3dc6670 --- /dev/null +++ b/src/composables/useTheme.ts @@ -0,0 +1,43 @@ +import { onMounted, ref } from 'vue' + +/** + * The light/dark preference, shared by everything that can change it. + * + * The class on the root element is the source of truth rather than the ref: an + * application sets it before Vue mounts to avoid a flash of the wrong theme, so + * reading it back on mount is what keeps a toggle from rendering the wrong icon + * on first paint. + * + * The ref lives at module scope so every caller shares one, without depending + * on a framework's state API. Under server rendering that means it is shared + * across requests, which is harmless only because its value before mount is a + * default nobody renders a decision from. + */ +const isDark = ref(true) + +const STORAGE_KEY = 'theme' + +export function useTheme() { + function readFromDocument() { + isDark.value = !document.documentElement.classList.contains('light') + } + + function setTheme(dark: boolean) { + isDark.value = dark + document.documentElement.classList.toggle('dark', dark) + document.documentElement.classList.toggle('light', !dark) + try { + localStorage.setItem(STORAGE_KEY, dark ? 'dark' : 'light') + } catch { + // A browser refusing storage still gets the theme it asked for. + } + } + + function toggleTheme() { + setTheme(!isDark.value) + } + + onMounted(readFromDocument) + + return { isDark, toggleTheme, setTheme } +} diff --git a/src/index.js b/src/index.js index 77fba35..e3df1c8 100644 --- a/src/index.js +++ b/src/index.js @@ -22,15 +22,31 @@ export { default as Chip } from './components/Chip.vue' export { default as Diff } from './components/Diff.vue' export { default as Logo } from './components/Logo.vue' export { default as Markdown } from './components/Markdown.vue' +export { default as Drawer } from './components/Drawer.vue' export { default as Modal } from './components/Modal.vue' export { default as Notice } from './components/Notice.vue' export { default as Status } from './components/Status.vue' export { default as Empty } from './components/Empty.vue' export { default as DotIndicator } from './components/DotIndicator.vue' export { default as Loading } from './components/Loading.vue' +export { default as LoadFailure } from './components/LoadFailure.vue' +export { default as SkeletonList } from './components/SkeletonList.vue' +export { default as ActionError } from './components/ActionError.vue' +export { default as Pagination } from './components/Pagination.vue' +export { default as Illustration } from './components/Illustration.vue' +export { default as ThemeToggle } from './components/ThemeToggle.vue' export { default as Origin } from './components/Origin.vue' export { default as Section } from './components/Section.vue' export { useContextKinds } from './composables/useContextKinds' +export { useTheme } from './composables/useTheme' export { controlVariants, roomyVariants } from './lib/control' export { cn } from './lib/utils' +export { + DEFAULT_PAGE_SIZE, + MAX_PAGE_SIZE, + emptyPage, + pageKey, + pageSummary, + pageWindow, +} from './lib/pagination' diff --git a/src/lib/pagination.ts b/src/lib/pagination.ts new file mode 100644 index 0000000..0601bfc --- /dev/null +++ b/src/lib/pagination.ts @@ -0,0 +1,67 @@ +/** One page of a list, as every list endpoint answers. */ +export interface Paged { + items: T[] + total: number + page: number + size: number + pages: number +} + +export interface PageRequest { + page?: number + size?: number +} + +/** Enough to fill a list pane without asking the provider for more than it will answer. */ +export const DEFAULT_PAGE_SIZE = 20 + +/** What the API says a page is at most, so a request never comes back a 422. */ +export const MAX_PAGE_SIZE = 100 + +export function emptyPage(page: PageRequest = {}): Paged { + return { + items: [], + total: 0, + page: page.page ?? 1, + size: page.size ?? DEFAULT_PAGE_SIZE, + pages: 0, + } +} + +/** Identifies a page of a scope, so two scopes never share a cached answer. */ +export function pageKey(scope: string[], page: PageRequest): string { + return [ + [...scope].sort().join(','), + page.page ?? 1, + page.size ?? DEFAULT_PAGE_SIZE, + ].join('|') +} + +/** + * The page numbers to offer, with gaps where there are too many to show. + * A null stands for a run of pages that is not worth a button. + */ +export function pageWindow(current: number, total: number, span = 1): (number | null)[] { + if (total <= 1) return total === 1 ? [1] : [] + + const shown = new Set([1, total]) + for (let n = current - span; n <= current + span; n++) { + if (n >= 1 && n <= total) shown.add(n) + } + + const ordered = [...shown].sort((a, b) => a - b) + const withGaps: (number | null)[] = [] + ordered.forEach((n, index) => { + if (index > 0 && n - ordered[index - 1]! > 1) withGaps.push(null) + withGaps.push(n) + }) + return withGaps +} + +/** What a reader is looking at, phrased the way they would say it. */ +export function pageSummary(page: Paged): string { + if (!page.total) return 'Nothing here' + const first = (page.page - 1) * page.size + 1 + const last = Math.min(page.page * page.size, page.total) + return `${first}–${last} of ${page.total}` +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 5a7314e..2849fdf 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -36,6 +36,23 @@ --pillar-graph: 262 83% 66%; --pillar-review: 142 71% 45%; --pillar-tokens: 38 92% 50%; + + /* How bad a finding is. Reserved: never reused to tell two series apart. */ + --severity-critical: 0 72% 51%; + --severity-high: 25 90% 48%; + --severity-moderate: 38 92% 50%; + --severity-low: 217 91% 60%; + + /* One per figure in a row, in a fixed order so a card keeps its colour as + the row grows. Measured, not chosen: the pair a colourblind reader would + struggle with is amber against green, which every card's own words carry + it past. */ + --figure-1: 217 91% 55%; + --figure-2: 330 81% 50%; + --figure-3: 32 95% 44%; + --figure-4: 160 94% 30%; + --figure-5: 262 83% 58%; + --figure-6: 192 91% 36%; } .light { diff --git a/tailwind-preset.js b/tailwind-preset.js index 5cd8407..80d28db 100644 --- a/tailwind-preset.js +++ b/tailwind-preset.js @@ -73,6 +73,20 @@ export default { review: 'hsl(var(--pillar-review) / )', tokens: 'hsl(var(--pillar-tokens) / )', }, + severity: { + critical: 'hsl(var(--severity-critical) / )', + high: 'hsl(var(--severity-high) / )', + moderate: 'hsl(var(--severity-moderate) / )', + low: 'hsl(var(--severity-low) / )', + }, + figure: { + 1: 'hsl(var(--figure-1) / )', + 2: 'hsl(var(--figure-2) / )', + 3: 'hsl(var(--figure-3) / )', + 4: 'hsl(var(--figure-4) / )', + 5: 'hsl(var(--figure-5) / )', + 6: 'hsl(var(--figure-6) / )', + }, }, borderRadius: { lg: 'var(--radius)',