From 123b3c9d9252ea73c3eec7baacaabffc08e47362 Mon Sep 17 00:00:00 2001 From: nfebe Date: Sat, 29 Aug 2026 01:06:05 +0100 Subject: [PATCH 01/12] feat: The pieces every surface is built from Tokens, primitives, and the graph renderer, in one place because the alternative was copying. A design copied by hand drifts the moment either copy changes, and the drift stays invisible until somebody notices the products no longer look like one. Nothing here knows what a workspace, a review or a proposal is. Those belong to whichever application has them, and a package that grew to hold them would be that application wearing a different name. Source rather than a build, since every consumer already compiles Vue, and no releases: it is pinned by commit, which costs nothing and is honest about moving with the products that use it. --- .gitignore | 1 + README.md | 71 ++++++ package.json | 31 +++ src/components/Avatar.vue | 51 ++++ src/components/Badge.vue | 41 ++++ src/components/Button.vue | 58 +++++ src/components/Card.vue | 28 +++ src/components/CodeGraph.vue | 359 +++++++++++++++++++++++++++++ src/components/Logo.vue | 49 ++++ src/components/Modal.vue | 73 ++++++ src/composables/useContextKinds.ts | 78 +++++++ src/index.js | 17 ++ src/lib/utils.ts | 6 + src/styles/tokens.css | 163 +++++++++++++ src/types.ts | 69 ++++++ tailwind-preset.js | 123 ++++++++++ 16 files changed, 1218 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 src/components/Avatar.vue create mode 100755 src/components/Badge.vue create mode 100755 src/components/Button.vue create mode 100644 src/components/Card.vue create mode 100644 src/components/CodeGraph.vue create mode 100755 src/components/Logo.vue create mode 100755 src/components/Modal.vue create mode 100644 src/composables/useContextKinds.ts create mode 100644 src/index.js create mode 100755 src/lib/utils.ts create mode 100644 src/styles/tokens.css create mode 100644 src/types.ts create mode 100644 tailwind-preset.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a07aa80 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# SourceAnt design + +The pieces every SourceAnt surface is built from: the colour tokens, the +primitives, and the graph renderer. + +It exists because the alternative was copying. A design copied by hand drifts +the moment either copy changes, and the drift is invisible until somebody +notices the two products no longer look like one. + +## What is in it, and what is not + +Tokens, `Avatar`, `Badge`, `Button`, `Card`, `Logo`, `Modal`, and `CodeGraph`. + +Nothing that knows what a workspace, a review or a proposal is. Those belong to +whichever application has them, and a package that grew to hold them would be +that application wearing a different name. + +## Using it + +Pin it by commit. There are no releases: this moves with the products that use +it, and a version number would be a promise nobody is keeping yet. A pinned +commit costs nothing and stops a change here breaking a build there. + +```json +{ + "dependencies": { + "@sourceant/design": "github:sourceant/design#" + } +} +``` + +Take the Tailwind preset, and say which of your own files to scan: + +```js +import design from '@sourceant/design/tailwind' + +export default { + presets: [design], + content: ['./index.html', './src/**/*.{js,ts,vue}'], +} +``` + +Import the tokens once, before anything else: + +```js +import '@sourceant/design/tokens.css' +``` + +Then use what you need: + +```js +import { Button, Card, CodeGraph } from '@sourceant/design' +``` + +Source is shipped rather than a build, because every consumer already compiles +Vue. Vite needs to be told not to pre-bundle it: + +```js +export default defineConfig({ + optimizeDeps: { exclude: ['@sourceant/design'] }, +}) +``` + +## Themes + +Dark is the default. A `.light` class on the root element switches it; the +tokens are defined for both and nothing else needs to know which is on. + +## Licence + +MIT. diff --git a/package.json b/package.json new file mode 100644 index 0000000..b580332 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "@sourceant/design", + "version": "0.1.0", + "description": "SourceAnt design system: tokens, primitives, and the graph renderer", + "license": "MIT", + "type": "module", + "sideEffects": [ + "*.css" + ], + "exports": { + ".": "./src/index.js", + "./tokens.css": "./src/styles/tokens.css", + "./tailwind": "./tailwind-preset.js", + "./*": "./src/*" + }, + "files": [ + "src", + "tailwind-preset.js" + ], + "peerDependencies": { + "3d-force-graph": "^1.80.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "d3-force-3d": "^3.0.6", + "force-graph": "^1.51.4", + "lucide-vue-next": "^0.563.0", + "tailwind-merge": "^3.4.0", + "three-spritetext": "^1.10.0", + "vue": "^3.5.0" + } +} diff --git a/src/components/Avatar.vue b/src/components/Avatar.vue new file mode 100644 index 0000000..f57c797 --- /dev/null +++ b/src/components/Avatar.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/Badge.vue b/src/components/Badge.vue new file mode 100755 index 0000000..d397e92 --- /dev/null +++ b/src/components/Badge.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/components/Button.vue b/src/components/Button.vue new file mode 100755 index 0000000..351239a --- /dev/null +++ b/src/components/Button.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/Card.vue b/src/components/Card.vue new file mode 100644 index 0000000..eaa6c6b --- /dev/null +++ b/src/components/Card.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/CodeGraph.vue b/src/components/CodeGraph.vue new file mode 100644 index 0000000..7cb513d --- /dev/null +++ b/src/components/CodeGraph.vue @@ -0,0 +1,359 @@ + + + diff --git a/src/components/Logo.vue b/src/components/Logo.vue new file mode 100755 index 0000000..832ecc6 --- /dev/null +++ b/src/components/Logo.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/Modal.vue b/src/components/Modal.vue new file mode 100755 index 0000000..20abe26 --- /dev/null +++ b/src/components/Modal.vue @@ -0,0 +1,73 @@ + + + diff --git a/src/composables/useContextKinds.ts b/src/composables/useContextKinds.ts new file mode 100644 index 0000000..207e3c2 --- /dev/null +++ b/src/composables/useContextKinds.ts @@ -0,0 +1,78 @@ +/** + * What each kind of knowledge looks like, in one place. + * + * Six screens carried their own copy of this map, identical to the character, + * so a colour could only be changed by finding all six. Lookups are + * case-insensitive because the API answers lowercase and some screens title-case + * it for display. + * + * Tailwind only ever sees literal class names, so these are spelled out in full + * rather than assembled. + */ +const TONE: Record = { + 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', + convention: 'bg-violet-600/10 text-violet-600 border-violet-600/20', + pattern: 'bg-emerald-600/10 text-emerald-600 border-emerald-600/20', + discussion: 'bg-cyan-600/10 text-cyan-600 border-cyan-600/20', + review: 'bg-pink-600/10 text-pink-600 border-pink-600/20', + plan: 'bg-lime-600/10 text-lime-600 border-lime-600/20', +} + +const FILL: Record = { + decision: 'bg-blue-600', + constraint: 'bg-amber-600', + workaround: 'bg-red-600', + convention: 'bg-violet-600', + pattern: 'bg-emerald-600', + discussion: 'bg-cyan-600', + review: 'bg-pink-600', + plan: 'bg-lime-600', +} + +/** + * Concrete hex, for the graph: it draws to canvas, where a CSS variable is + * nothing. + * + * Measured rather than chosen. The set these replace had two pairs a reader + * could not separate: part against system was indistinguishable to a red-green + * 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. + */ +const INK: Record = { + system: '#7c3aed', part: '#d97706', contract: '#0891b2', + decision: '#2563eb', constraint: '#d97706', workaround: '#dc2626', + convention: '#7c3aed', pattern: '#059669', discussion: '#0891b2', + review: '#db2777', plan: '#65a30d', +} + +export function useContextKinds() { + const NEUTRAL = 'bg-muted text-muted-foreground border-border' + + function tone(kind?: string | null) { + return TONE[(kind ?? '').toLowerCase()] ?? NEUTRAL + } + + function fill(kind?: string | null) { + return FILL[(kind ?? '').toLowerCase()] ?? 'bg-muted-foreground' + } + + function ink(kind?: string | null) { + return INK[(kind ?? '').toLowerCase()] ?? '#a1a1aa' + } + + const legend = Object.keys(INK).map(kind => ({ + kind, + label: kind.charAt(0).toUpperCase() + kind.slice(1), + color: INK[kind]!, + })) + + return { tone, fill, ink, legend, kinds: Object.keys(TONE) } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b5895ce --- /dev/null +++ b/src/index.js @@ -0,0 +1,17 @@ +/* Everything this package offers, in one place. + * + * Source is shipped rather than a build. Consumers already compile Vue, and a + * build step here would be one more thing to be stale: a package pinned by + * commit should be exactly what that commit says. + */ + +export { default as Avatar } from './components/Avatar.vue' +export { default as Badge } from './components/Badge.vue' +export { default as Button } from './components/Button.vue' +export { default as Card } from './components/Card.vue' +export { default as CodeGraph } from './components/CodeGraph.vue' +export { default as Logo } from './components/Logo.vue' +export { default as Modal } from './components/Modal.vue' + +export { useContextKinds } from './composables/useContextKinds' +export { cn } from './lib/utils' diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100755 index 0000000..d32b0fe --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..5a7314e --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,163 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root, + .dark { + color-scheme: dark; + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 9% 7%; + --card-foreground: 0 0% 98%; + --popover: 240 9% 7%; + --popover-foreground: 0 0% 98%; + --primary: 357 89% 47%; + --primary-foreground: 0 0% 100%; + --secondary: 240 4% 16%; + --secondary-foreground: 0 0% 98%; + --muted: 240 5% 14%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 4% 16%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 50.6%; + --destructive-foreground: 0 0% 98%; + --success: 142 71% 45%; + --success-foreground: 0 0% 98%; + --warning: 38 92% 50%; + --warning-foreground: 0 0% 98%; + --border: 240 6% 16%; + --input: 240 6% 16%; + --ring: 357 89% 47%; + --radius: 0.25rem; + --brand: 357 89% 47%; + /* Capability pillar accents, shared with the marketing site */ + --pillar-memory: 217 91% 60%; + --pillar-graph: 262 83% 66%; + --pillar-review: 142 71% 45%; + --pillar-tokens: 38 92% 50%; + } + + .light { + color-scheme: light; + --background: 240 20% 98%; + --foreground: 240 10% 10%; + --card: 0 0% 100%; + --card-foreground: 240 10% 10%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 10%; + --primary: 357 89% 47%; + --primary-foreground: 0 0% 100%; + --secondary: 240 10% 94%; + --secondary-foreground: 240 10% 20%; + --muted: 240 10% 94%; + --muted-foreground: 240 5% 45%; + --accent: 357 60% 96%; + --accent-foreground: 357 70% 40%; + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 100%; + --success: 142 70% 35%; + --success-foreground: 0 0% 100%; + --warning: 38 92% 45%; + --warning-foreground: 0 0% 100%; + --border: 240 10% 90%; + --input: 240 10% 90%; + --ring: 357 89% 47%; + --brand: 357 89% 47%; + --pillar-memory: 217 91% 55%; + --pillar-graph: 262 83% 58%; + --pillar-review: 142 71% 40%; + --pillar-tokens: 38 92% 45%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground font-sans antialiased; + } + + html { + scroll-behavior: smooth; + } +} + +@layer components { + .gradient-text { + @apply bg-gradient-to-r from-primary via-purple-400 to-pink-400 bg-clip-text text-transparent; + } + + .light .gradient-text { + @apply from-purple-600 via-violet-600 to-pink-600; + } + + .gradient-border { + @apply relative; + } + + .gradient-border::before { + content: ''; + @apply absolute -inset-[1px] rounded-lg bg-gradient-to-r from-primary via-purple-500 to-pink-500 opacity-20; + } + + .glass { + @apply bg-background/80 backdrop-blur-xl; + } + + .light .glass { + @apply bg-background/90 backdrop-blur-xl border-b border-border/50; + } + + .glow { + box-shadow: 0 0 60px -15px hsl(var(--primary) / 0.3); + } + + .light .glow { + box-shadow: 0 0 60px -15px hsl(var(--primary) / 0.15); + } + + .glow-sm { + box-shadow: 0 0 30px -10px hsl(var(--primary) / 0.2); + } + + .light .glow-sm { + box-shadow: 0 0 30px -10px hsl(var(--primary) / 0.1); + } + + .text-brand { + color: hsl(var(--brand)); + } + + .bg-brand { + background-color: hsl(var(--brand)); + } + + .border-brand { + border-color: hsl(var(--brand)); + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } + + .animate-delay-100 { + animation-delay: 100ms; + } + + .animate-delay-200 { + animation-delay: 200ms; + } + + .animate-delay-300 { + animation-delay: 300ms; + } + + .animate-delay-500 { + animation-delay: 500ms; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..401b5e9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,69 @@ +/* What the agent answers with when asked for a graph. + * + * One definition, shared by the renderer and everything feeding it. Two of them + * is how a drawing and its data drift apart. + */ + +export interface GraphNode { + id: string + name: string + /** + * Knowledge: system | part | contract | decision | constraint | convention | + * pattern | workaround | discussion | review | file | code. + * Code: class | function | method | module | route | variable | symbol. + */ + kind: string + status?: string + /** Where it lives, when the graph is of code. */ + path?: string | null + /** What it is written in, when it is a file. */ + language?: string + /** What the index filed it under, when the local index answered. */ + labels?: string[] + /** How many lines meet here. The canvas sizes a node by this. */ + degree?: number + /** Which part of the code it belongs to, or null if that part is not coloured. */ + community?: number | null +} + +/** One part of a code graph: a set of symbols more connected to each other + * than to the rest, named after where it lives or what it is built around. */ +export interface GraphCommunity { + id: number + name: string + size: number +} + +/** A typed edge between two graph nodes. */ +export interface GraphLink { + source: string + target: string + type?: string +} + +/** Nodes and edges served for the graph view. */ +export interface KnowledgeGraphData { + nodes: GraphNode[] + links: GraphLink[] + /** The parts worth colouring, biggest first. Code graphs only. */ + communities?: GraphCommunity[] + /** True when the answer was cut to a size a person can read. */ + truncated?: boolean + /** The node it was walked out from, or null if it drew everything. */ + focus?: string | null +} + +/** One repository indexed on this machine. */ +export interface Repository { + name: string + path: string +} + +/** One thing recorded about a repository. */ +export interface Knowledge { + id: string + kind: string + status: string + summary: string + properties: Record +} diff --git a/tailwind-preset.js b/tailwind-preset.js new file mode 100644 index 0000000..919fa99 --- /dev/null +++ b/tailwind-preset.js @@ -0,0 +1,123 @@ +/* The design, as a Tailwind preset. + * + * Content globs are deliberately absent: which files to scan is the consuming + * application's question and differs in every one of them. + */ + +export default { + darkMode: 'class', + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + extend: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + mono: ['JetBrains Mono', 'monospace'], + }, + colors: { + brand: { + DEFAULT: 'hsl(var(--brand))', + red: '#E20C18', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + success: { + DEFAULT: 'hsl(var(--success))', + foreground: 'hsl(var(--success-foreground))', + }, + warning: { + DEFAULT: 'hsl(var(--warning))', + foreground: 'hsl(var(--warning-foreground))', + }, + pillar: { + memory: 'hsl(var(--pillar-memory) / )', + graph: 'hsl(var(--pillar-graph) / )', + review: 'hsl(var(--pillar-review) / )', + tokens: 'hsl(var(--pillar-tokens) / )', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + xl: 'calc(var(--radius) + 2px)', + '2xl': 'calc(var(--radius) + 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + 'fade-in': { + from: { opacity: '0' }, + to: { opacity: '1' }, + }, + 'fade-up': { + from: { opacity: '0', transform: 'translateY(10px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, + 'slide-in-right': { + from: { transform: 'translateX(100%)' }, + to: { transform: 'translateX(0)' }, + }, + shimmer: { + '100%': { transform: 'translateX(100%)' }, + }, + pulse: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0.5' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'fade-in': 'fade-in 0.3s ease-out', + 'fade-up': 'fade-up 0.4s ease-out', + 'slide-in-right': 'slide-in-right 0.3s ease-out', + shimmer: 'shimmer 2s infinite', + pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + }, + }, + plugins: [], +} From 57529a66ed1b1821dfacd69c65160887c989f2da Mon Sep 17 00:00:00 2001 From: nfebe Date: Sat, 29 Aug 2026 02:25:38 +0100 Subject: [PATCH 02/12] feat: Add the form, tab and list-item primitives The apps consuming this package had each rebuilt a labelled field, a text box, a picker, a segmented switcher and a row in a list of things, by hand and with slightly different spacing every time. All five now come from here, so a change to how a form or a list looks is one change rather than one per app. --- README.md | 4 ++- src/components/Field.vue | 36 +++++++++++++++++++++ src/components/Input.vue | 28 +++++++++++++++++ src/components/ItemCard.vue | 63 +++++++++++++++++++++++++++++++++++++ src/components/PageHead.vue | 42 +++++++++++++++++++++++++ src/components/Select.vue | 27 ++++++++++++++++ src/components/Tabs.vue | 49 +++++++++++++++++++++++++++++ src/components/Textarea.vue | 30 ++++++++++++++++++ src/index.js | 7 +++++ 9 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 src/components/Field.vue create mode 100644 src/components/Input.vue create mode 100644 src/components/ItemCard.vue create mode 100644 src/components/PageHead.vue create mode 100644 src/components/Select.vue create mode 100644 src/components/Tabs.vue create mode 100644 src/components/Textarea.vue diff --git a/README.md b/README.md index a07aa80..76e967c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ notices the two products no longer look like one. ## What is in it, and what is not -Tokens, `Avatar`, `Badge`, `Button`, `Card`, `Logo`, `Modal`, and `CodeGraph`. +Tokens, and the pieces a screen is assembled from: `Avatar`, `Badge`, `Button`, +`Card`, `Field`, `Input`, `ItemCard`, `Logo`, `Modal`, `PageHead`, `Select`, +`Tabs`, `Textarea`, and `CodeGraph`. Nothing that knows what a workspace, a review or a proposal is. Those belong to whichever application has them, and a package that grew to hold them would be diff --git a/src/components/Field.vue b/src/components/Field.vue new file mode 100644 index 0000000..3c67b09 --- /dev/null +++ b/src/components/Field.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/Input.vue b/src/components/Input.vue new file mode 100644 index 0000000..02bdc4c --- /dev/null +++ b/src/components/Input.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/ItemCard.vue b/src/components/ItemCard.vue new file mode 100644 index 0000000..56296df --- /dev/null +++ b/src/components/ItemCard.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/components/PageHead.vue b/src/components/PageHead.vue new file mode 100644 index 0000000..98e5de2 --- /dev/null +++ b/src/components/PageHead.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/Select.vue b/src/components/Select.vue new file mode 100644 index 0000000..1e209a4 --- /dev/null +++ b/src/components/Select.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/Tabs.vue b/src/components/Tabs.vue new file mode 100644 index 0000000..de2fda8 --- /dev/null +++ b/src/components/Tabs.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/Textarea.vue b/src/components/Textarea.vue new file mode 100644 index 0000000..4cabf80 --- /dev/null +++ b/src/components/Textarea.vue @@ -0,0 +1,30 @@ + + +