From f1d539f8547b9a51f83f8f61d23de03682e8dc5e Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 1 Sep 2026 06:26:18 +0100 Subject: [PATCH 01/11] feat(design): Carry the primitives the dashboard had to keep for itself The dashboard maintained its own copies of the shared components and its own fork of the theme, so the two surfaces drifted apart and a fix in one never reached the other. The pieces it had that this package did not are the reason it could not simply drop them. Adds a failed read with a retry, a list placeholder, a dismissible action error, page navigation, themed line art, and a theme switch. The two loading treatments are now one component: the richer spinner, with the source of the work named in the line beneath it. The error and the theme switch take what they show rather than reading it from shared state, so an application keeps whatever it already uses to hold one and nothing here depends on a particular framework. --- package.json | 2 +- src/assets/illustrations/all-the-data.svg | 48 +++++ src/assets/illustrations/bug-fixing.svg | 175 ++++++++++++++++++ src/assets/illustrations/code-inspection.svg | 34 ++++ src/assets/illustrations/code-review.svg | 34 ++++ src/assets/illustrations/code-thinking.svg | 35 ++++ src/assets/illustrations/code-typing.svg | 38 ++++ src/assets/illustrations/conceptual-idea.svg | 36 ++++ src/assets/illustrations/connecting-teams.svg | 47 +++++ src/assets/illustrations/control-panel.svg | 70 +++++++ src/assets/illustrations/data-points.svg | 53 ++++++ .../illustrations/developer-activity.svg | 80 ++++++++ src/assets/illustrations/engineering-team.svg | 48 +++++ src/assets/illustrations/file-searching.svg | 48 +++++ src/assets/illustrations/firmware.svg | 133 +++++++++++++ src/assets/illustrations/for-review.svg | 17 ++ src/assets/illustrations/good-team.svg | 71 +++++++ src/assets/illustrations/mind-map.svg | 48 +++++ src/assets/illustrations/programming.svg | 106 +++++++++++ src/assets/illustrations/secure-server.svg | 47 +++++ src/assets/illustrations/server-status.svg | 36 ++++ src/assets/illustrations/version-control.svg | 41 ++++ src/components/ActionError.vue | 45 +++++ src/components/Illustration.vue | 68 +++++++ src/components/LoadFailure.vue | 50 +++++ src/components/Loading.vue | 72 +++++-- src/components/Pagination.vue | 74 ++++++++ src/components/SkeletonList.vue | 50 +++++ src/components/ThemeToggle.vue | 27 +++ src/composables/useTheme.ts | 43 +++++ src/index.js | 15 ++ src/lib/pagination.ts | 67 +++++++ 32 files changed, 1740 insertions(+), 18 deletions(-) create mode 100644 src/assets/illustrations/all-the-data.svg create mode 100644 src/assets/illustrations/bug-fixing.svg create mode 100644 src/assets/illustrations/code-inspection.svg create mode 100644 src/assets/illustrations/code-review.svg create mode 100644 src/assets/illustrations/code-thinking.svg create mode 100644 src/assets/illustrations/code-typing.svg create mode 100644 src/assets/illustrations/conceptual-idea.svg create mode 100644 src/assets/illustrations/connecting-teams.svg create mode 100644 src/assets/illustrations/control-panel.svg create mode 100644 src/assets/illustrations/data-points.svg create mode 100644 src/assets/illustrations/developer-activity.svg create mode 100644 src/assets/illustrations/engineering-team.svg create mode 100644 src/assets/illustrations/file-searching.svg create mode 100644 src/assets/illustrations/firmware.svg create mode 100644 src/assets/illustrations/for-review.svg create mode 100644 src/assets/illustrations/good-team.svg create mode 100644 src/assets/illustrations/mind-map.svg create mode 100644 src/assets/illustrations/programming.svg create mode 100644 src/assets/illustrations/secure-server.svg create mode 100644 src/assets/illustrations/server-status.svg create mode 100644 src/assets/illustrations/version-control.svg create mode 100644 src/components/ActionError.vue create mode 100644 src/components/Illustration.vue create mode 100644 src/components/LoadFailure.vue create mode 100644 src/components/Pagination.vue create mode 100644 src/components/SkeletonList.vue create mode 100644 src/components/ThemeToggle.vue create mode 100644 src/composables/useTheme.ts create mode 100644 src/lib/pagination.ts 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/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/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/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..df4d142 100644 --- a/src/index.js +++ b/src/index.js @@ -28,9 +28,24 @@ 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}` +} From 00a50fa516eb3b2ae2ceab65811fc17767166e24 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 1 Sep 2026 06:26:31 +0100 Subject: [PATCH 02/11] docs(design): Record what 0.2.0 adds --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd12831..a91bf0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ 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 + +- 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 + +### Changed + +- `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 From d31d2078c80f4bb61f498ec813ef8e31cbbb2dd4 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 1 Sep 2026 06:28:33 +0100 Subject: [PATCH 03/11] feat(design): Carry the severity and figure colours as shared tokens The dashboard defined its own scale for how bad a finding is and its own ordered palette for a row of figures, which is why it could not take the preset as it stood. Both are now part of the shared vocabulary, so a severity means the same colour wherever it is shown. --- src/styles/tokens.css | 17 +++++++++++++++++ tailwind-preset.js | 14 ++++++++++++++ 2 files changed, 31 insertions(+) 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)', From d9d683fac5f057ece885e2e2274ce226500e6ece Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 1 Sep 2026 06:28:33 +0100 Subject: [PATCH 04/11] docs(design): Record the tokens 0.2.0 adds --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91bf0e..31bd554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ set. Everything from 0.1.0 keeps working unchanged. 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 From a30416a09b9fa0e8fa436c8aabfbcad8eab44ffe Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 8 Sep 2026 00:48:28 +0100 Subject: [PATCH 05/11] feat(design): Let a tab carry an icon and be turned off A switcher whose options are not all available has to say so on the option rather than hiding it, and a reader scanning a row of five words finds the one they want by its mark before its label. Without either, an application that needs them writes the strip out by hand, which is how the same control came to exist in two designs across six screens. Also takes a tighter size, for a strip inside a panel header rather than one standing beside a page title. --- CHANGELOG.md | 3 +++ src/components/Tabs.vue | 27 +++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31bd554..1db2e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ set. Everything from 0.1.0 keeps working unchanged. ### Changed +- `Tabs` carries an icon and can turn an option off, 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 diff --git a/src/components/Tabs.vue b/src/components/Tabs.vue index de2fda8..5142c1d 100644 --- a/src/components/Tabs.vue +++ b/src/components/Tabs.vue @@ -1,15 +1,27 @@ From 2c30a08a1d78533c86cfbd8ce769bff6cb2f54a8 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 8 Sep 2026 01:07:35 +0100 Subject: [PATCH 08/11] feat(design): Carry the kinds a person can pick and what each is for --- src/composables/useContextKinds.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/composables/useContextKinds.ts b/src/composables/useContextKinds.ts index 207e3c2..a1109b3 100644 --- a/src/composables/useContextKinds.ts +++ b/src/composables/useContextKinds.ts @@ -53,6 +53,23 @@ const INK: Record = { review: '#db2777', plan: '#65a30d', } +const OPTIONS = [ + '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 = { + 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 +91,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 } } From af63f2f5760dc571d91be50a14c71d40740fbb0d Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 8 Sep 2026 02:58:17 +0100 Subject: [PATCH 09/11] feat(design): Add a drawer, for work rather than for a question A modal is right for a question with an answer. It is wrong for writing something: it floats in the middle of the window, it is only as tall as what is in it, and it covers the thing being written about. This comes in from either side at the full height of the window, takes a share of the width rather than a fixed one, and holds the page still behind it. Below the small breakpoint it takes the whole width, because two fifths of a phone is a column too narrow to write in. Five panels across the applications were each drawing their own version of this, which is why they open from different sides at different widths. --- CHANGELOG.md | 4 ++ src/components/Drawer.vue | 125 ++++++++++++++++++++++++++++++++++++++ src/index.js | 1 + 3 files changed, 130 insertions(+) create mode 100644 src/components/Drawer.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 189a5d7..9ad331d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ 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 diff --git a/src/components/Drawer.vue b/src/components/Drawer.vue new file mode 100644 index 0000000..d9a767c --- /dev/null +++ b/src/components/Drawer.vue @@ -0,0 +1,125 @@ + + + diff --git a/src/index.js b/src/index.js index df4d142..e3df1c8 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ 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' From 02335bbb11f96d7464ac770d12e6dc039ba3b653 Mon Sep 17 00:00:00 2001 From: nfebe Date: Wed, 9 Sep 2026 21:53:41 +0100 Subject: [PATCH 10/11] feat(design): Give intent a colour of its own Knowledge can be filed as intent: what a project exists to do, who it is for, and what it deliberately is not. It carries its own colour, picker entry and description wherever kinds are shown. The palette is spaced by hue rather than picked by eye. Intent took the gap near 298 degrees. The other wide gap falls in the green that collapses onto pink for a red-green colourblind reader. --- src/composables/useContextKinds.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/composables/useContextKinds.ts b/src/composables/useContextKinds.ts index a1109b3..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,26 +42,31 @@ 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 = [ - 'Decision', 'Constraint', 'Workaround', 'Convention', + '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', From 3fa357d3c9bc681018ccea0164ca993a2f771dc4 Mon Sep 17 00:00:00 2001 From: nfebe Date: Wed, 9 Sep 2026 21:55:05 +0100 Subject: [PATCH 11/11] docs(design): Say what the drawer is, not what it replaced --- src/components/Drawer.vue | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/Drawer.vue b/src/components/Drawer.vue index d9a767c..26908d6 100644 --- a/src/components/Drawer.vue +++ b/src/components/Drawer.vue @@ -5,13 +5,8 @@ import { cn } from '../lib/utils' /** A panel that comes in from the side, over what you were looking at. * - * A modal is right for a question with an answer. It is wrong for work: it - * floats in the middle, it is as tall as its contents, and it covers the thing - * being worked on. Writing something long against a list you are still reading - * wants the full height of the window and a side of it, not the centre. - * - * Five panels in the applications were each drawing their own version of this, - * which is why they open from different sides at different widths. + * Takes the full height of the window, so a long piece of work stays readable + * against a list that is still on screen behind it. */ interface Props {