@omkarux/vela 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +21 -0
  3. package/README.md +112 -0
  4. package/dist/components/Button/Button.d.ts +32 -0
  5. package/dist/components/Button/Button.js +51 -0
  6. package/dist/components/Button/index.d.ts +2 -0
  7. package/dist/components/ContextualAlert/ContextualAlert.d.ts +18 -0
  8. package/dist/components/ContextualAlert/ContextualAlert.js +49 -0
  9. package/dist/components/ContextualAlert/index.d.ts +2 -0
  10. package/dist/components/Input/Input.d.ts +12 -0
  11. package/dist/components/Input/Input.js +31 -0
  12. package/dist/components/Input/index.d.ts +2 -0
  13. package/dist/components/StatusIndicator/StatusIndicator.d.ts +14 -0
  14. package/dist/components/StatusIndicator/StatusIndicator.js +31 -0
  15. package/dist/components/StatusIndicator/index.d.ts +2 -0
  16. package/dist/components/Tabs/Tabs.d.ts +44 -0
  17. package/dist/components/Tabs/Tabs.js +94 -0
  18. package/dist/components/Tabs/index.d.ts +2 -0
  19. package/dist/components/Toggle/Toggle.d.ts +23 -0
  20. package/dist/components/Toggle/Toggle.js +55 -0
  21. package/dist/components/Toggle/index.d.ts +2 -0
  22. package/dist/index.d.ts +24 -0
  23. package/dist/index.js +16 -0
  24. package/dist/lib/cn.d.ts +5 -0
  25. package/dist/lib/cn.js +6 -0
  26. package/dist/lib/icons.d.ts +7 -0
  27. package/dist/lib/icons.js +32 -0
  28. package/dist/tokens.css +610 -0
  29. package/dist/vela.css +897 -0
  30. package/guidelines/components/button.md +104 -0
  31. package/guidelines/components/contextual-alert.md +59 -0
  32. package/guidelines/components/input.md +65 -0
  33. package/guidelines/components/status-indicator.md +48 -0
  34. package/guidelines/components/tabs.md +71 -0
  35. package/guidelines/components/toggle.md +53 -0
  36. package/guidelines/foundations/color.md +67 -0
  37. package/guidelines/foundations/radius.md +17 -0
  38. package/guidelines/foundations/sizing.md +35 -0
  39. package/guidelines/foundations/spacing.md +28 -0
  40. package/guidelines/foundations/typography.md +34 -0
  41. package/guidelines/llms.txt +33 -0
  42. package/guidelines/overview.md +48 -0
  43. package/guidelines/setup.md +64 -0
  44. package/guidelines/tokens.md +69 -0
  45. package/package.json +84 -0
@@ -0,0 +1,104 @@
1
+ # Button
2
+
3
+ Triggers an action — save, submit, delete, apply, cancel. Carries a text label and an
4
+ optional **left** icon. `variant` encodes the action's consequence; `appearance` encodes
5
+ visual weight.
6
+
7
+ ## When to use Button (vs. something else)
8
+
9
+ - Action that stays in the page flow → **Button**.
10
+ - Navigates to another page → a router `<Link>`, not a Button.
11
+ - Flips a binary setting that takes effect immediately → **Toggle**.
12
+ - Icon-only, no label → an Icon Button, which is **not in this kit**. Flag the gap.
13
+
14
+ ## Props
15
+
16
+ ```tsx
17
+ <Button variant="primary" appearance="filled" size="regular" icon={<PlusIcon />}>
18
+ Save Changes
19
+ </Button>
20
+ ```
21
+
22
+ | Prop | Type | Default | Notes |
23
+ |---|---|---|---|
24
+ | `variant` | `"primary" \| "standard" \| "destructive"` | `"standard"` | Consequence. **These three only.** |
25
+ | `appearance` | `"filled" \| "hollow" \| "text-link"` | `"filled"` | Weight. **These three only.** `text-link` is primary-only. |
26
+ | `size` | `"tiny" \| "regular" \| "large" \| "huge"` | `"regular"` | **These four only.** |
27
+ | `disabled` | `boolean` | `false` | A state, not a variant. Leaves the tab order. |
28
+ | `loading` | `boolean` | `false` | Shows an indicator, sets `aria-busy`, ignores clicks. |
29
+ | `icon` | `ReactNode` | — | Always **left** of the label. There is no trailing slot. |
30
+ | `type` | `"button" \| "submit"` | `"button"` | `"submit"` only for a form's commit button. |
31
+ | `children` | `ReactNode` | — | Label. Verb-first, Title Case. |
32
+
33
+ Every other native `<button>` attribute is forwarded, and the ref lands on the `<button>`.
34
+
35
+ ## Hard constraints (enforced by the type system)
36
+
37
+ These are not review conventions — the compiler rejects them. `ButtonProps` is a
38
+ discriminated union, so a violation is a build failure, not a bug report.
39
+
40
+ - **`appearance="text-link"` requires `variant="primary"`.** No standard or destructive
41
+ text-link exists.
42
+ - **`text-link` is `tiny` or `regular` only**, and **never carries an icon** (`icon?: never`).
43
+ - **`text-link` is never underlined.** Colour carries the affordance; hover shifts colour.
44
+ - **One `variant="primary" appearance="filled"` per page.** Pair it with hollow / text-link
45
+ for everything else. This one is a convention — the compiler cannot count buttons.
46
+ - **Icons sit left only.**
47
+ - **Width hugs content.** Never fix a button width.
48
+ - **`destructive` encodes consequence, not emphasis.** Delete/revoke/remove only. Never
49
+ for "Cancel", never just to stand out.
50
+
51
+ ## Token bindings
52
+
53
+ Container: `border-radius: var(--vela-radius-4)` — not a pill. Focus ring comes from
54
+ `.vela-root :focus-visible`; do not add a per-button outline.
55
+
56
+ | variant · appearance | Background | Text | Border |
57
+ |---|---|---|---|
58
+ | primary · filled | `--vela-btn-primary-bg` | `--vela-btn-primary-text` | same as bg |
59
+ | primary · hollow | transparent | `--vela-btn-hollow-primary-text` | `--vela-btn-hollow-primary-border` |
60
+ | primary · text-link | transparent | `--vela-text-link` | none |
61
+ | standard · filled | `--vela-btn-standard-bg` | `--vela-btn-standard-text` | `--vela-btn-standard-border` |
62
+ | standard · hollow | transparent | `--vela-btn-standard-text` | `--vela-btn-standard-border` |
63
+ | destructive · filled | `--vela-btn-destructive-bg` | `--vela-btn-destructive-text` | same as bg |
64
+ | destructive · hollow | transparent | `--vela-btn-destructive-hollow-text` | same as text |
65
+ | disabled (any) | `--vela-btn-disabled-bg` | `--vela-btn-disabled-text` | `--vela-border-inactive` |
66
+
67
+ `--vela-btn-destructive-bg` resolves to `red-700`, **not** `red-500`. See
68
+ `foundations/color.md` — red-500 fails AA behind a white label.
69
+
70
+ ## Sizes
71
+
72
+ | size | Height | H-padding | Icon | Font |
73
+ |---|---|---|---|---|
74
+ | tiny | `--vela-control-height-24` | `--vela-space-10` | `--vela-icon-12` | 12/18 |
75
+ | regular | `--vela-control-height-35` | `--vela-space-15` | `--vela-icon-16` | 14/21 |
76
+ | large | `--vela-control-height-40` | `--vela-space-20` | `--vela-icon-16` | 15/22 |
77
+ | huge | `--vela-control-height-60` | `--vela-space-30` | `--vela-icon-16` | 16/24 |
78
+
79
+ Label weight is **Regular 400 at every size**.
80
+
81
+ ## States
82
+
83
+ - **Hover** — filled variants darken (lighten in dark mode) via their `-hover-bg` token.
84
+ `text-link` shifts colour and stays un-underlined.
85
+ - **Focus** — the shared `:focus-visible` ring. Never `outline: none`.
86
+ - **Disabled** — native `disabled`. Not focusable. Explain why in adjacent helper text.
87
+ - **Loading** — the label **stays**, a spinner appears, `aria-busy="true"` is set, and the
88
+ button remains focusable while ignoring clicks. Swapping the label for a spinner would
89
+ strip the control's accessible name mid-request; disabling it would drop focus.
90
+
91
+ ## Anti-patterns
92
+
93
+ - ❌ Two primary filled buttons on one page.
94
+ - ❌ Pill-shaped buttons → `--vela-radius-4`.
95
+ - ❌ `--vela-control-height-48` for huge → huge is 60px.
96
+ - ❌ A 20px or 24px icon in a Button → 12 at tiny, 16 elsewhere.
97
+ - ❌ Fixed button widths.
98
+ - ❌ A trailing icon.
99
+ - ❌ A destructive or standard text-link → primary only (will not compile).
100
+ - ❌ Underlining a text-link.
101
+ - ❌ `destructive` for emphasis on a harmless action.
102
+ - ❌ Hardcoded colours → always `var(--vela-*)`.
103
+ - ❌ SemiBold or Bold labels → Regular 400.
104
+ - ❌ Non-verb labels ("Information", "Status") → if it is not an action, it is not a button.
@@ -0,0 +1,59 @@
1
+ # Contextual Alert
2
+
3
+ An inline banner carrying a severity. Lives in the page flow, next to the thing it is about
4
+ — it is not a toast and not a modal.
5
+
6
+ ## When to use Contextual Alert
7
+
8
+ - An event or condition the user must read, inline → **Contextual Alert**.
9
+ - An object's ongoing health, usually in a table → **Status Indicator**.
10
+ - Transient confirmation that floats and auto-dismisses → a Toast, **not in this kit**.
11
+ - A blocking decision → a Modal, **not in this kit**. Flag the gap.
12
+
13
+ ## Props
14
+
15
+ ```tsx
16
+ <ContextualAlert severity="major" title="Scan failed" onDismiss={close}>
17
+ Three assets could not be reached.
18
+ </ContextualAlert>
19
+ ```
20
+
21
+ | Prop | Type | Default | Notes |
22
+ |---|---|---|---|
23
+ | `severity` | `"success" \| "info" \| "warning" \| "minor" \| "major" \| "critical"` | — | **Required. These six only.** |
24
+ | `title` | `string` | — | Optional bold lead-in. |
25
+ | `children` | `ReactNode` | — | **Required.** The body. |
26
+ | `onDismiss` | `() => void` | — | Omit for alerts the user must not clear. |
27
+ | `dismissLabel` | `string` | `"Dismiss"` | Accessible name for the close control. |
28
+
29
+ ## Hard constraints
30
+
31
+ - **Six severities, no more.** There is no `"high"`, no `"error"`, no `"danger"`. Those
32
+ belong to the Risk taxonomy or to nothing. The compiler rejects them.
33
+ - **`major` and `critical` announce assertively** (`role="alert"`, `aria-live="assertive"`);
34
+ everything else is polite (`role="status"`). This is derived from `severity` and is not
35
+ configurable — an info banner that interrupts a screen reader is a defect.
36
+ - **No dismiss control unless `onDismiss` is passed.** A critical alert the user can clear
37
+ without acting is usually wrong.
38
+ - Icon is chosen by severity and is `aria-hidden` — the text carries the meaning.
39
+
40
+ ## Token bindings
41
+
42
+ Every level binds its own complete row: `--vela-bg-severity-*`,
43
+ `--vela-border-severity-*`, `--vela-icon-severity-*`, `--vela-text-severity-*`.
44
+ Never mix a severity background with a risk text colour.
45
+
46
+ ## Accessibility
47
+
48
+ - Colour is never the only signal: each severity has a distinct icon **and** its label text.
49
+ - Live-region politeness is derived from severity, as above.
50
+ - The dismiss control is a real button with an accessible name.
51
+
52
+ ## Anti-patterns
53
+
54
+ - ❌ `severity="high"` or `"error"` → not in the taxonomy (will not compile).
55
+ - ❌ Using a risk token for the background → severity and risk are separate.
56
+ - ❌ An info alert with `role="alert"` → interrupts for no reason.
57
+ - ❌ Dismissible critical alerts with no other resolution path.
58
+ - ❌ Conveying severity by colour alone.
59
+ - ❌ Using this as a toast → it is inline and does not float.
@@ -0,0 +1,65 @@
1
+ # Input
2
+
3
+ Single-line text entry with a label, optional hint, and an error state.
4
+
5
+ ## When to use Input
6
+
7
+ - Single-line text or numeric-as-text → **Input**.
8
+ - Multi-line → Text Area, **not in this kit**. Flag the gap.
9
+ - Numeric with a spinner → Number Stepper, **not in this kit**. Flag the gap.
10
+ - Choosing from a set → Radio or Dropdown, **not in this kit**.
11
+
12
+ ## Props
13
+
14
+ ```tsx
15
+ <Input label="Tenant name" hint="Lowercase letters only" error={err} size="regular" />
16
+ ```
17
+
18
+ | Prop | Type | Default | Notes |
19
+ |---|---|---|---|
20
+ | `label` | `string` | — | **Required.** |
21
+ | `hint` | `string` | — | Helper text. Wired via `aria-describedby`. |
22
+ | `error` | `string` | — | Presence sets the error state; the string is the message. |
23
+ | `size` | `"tiny" \| "regular" \| "large"` | `"regular"` | **These three only.** |
24
+ | `required` | `boolean` | `false` | Renders a `*` and sets the native attribute. |
25
+
26
+ Every other native `<input>` attribute is forwarded; the ref lands on the `<input>`.
27
+
28
+ ## Hard constraints
29
+
30
+ - **`label` is required.** An input with only a placeholder is not labelled.
31
+ - **`error` is a message, not a boolean.** An error state with no explanation is not an
32
+ error state.
33
+ - `aria-describedby` only ever references elements that are actually rendered — a dangling
34
+ reference is worse than none, and a test asserts this.
35
+
36
+ ## Token bindings
37
+
38
+ Background `--vela-control-bg`; border `--vela-control-border`; hover
39
+ `--vela-control-border-hover`; focus `--vela-control-border-active`; error
40
+ `--vela-control-border-error`; placeholder `--vela-control-placeholder`. Radius
41
+ `--vela-radius-4`.
42
+
43
+ ## Sizes
44
+
45
+ | size | Height | Padding |
46
+ |---|---|---|
47
+ | tiny | `--vela-control-height-25` | `--vela-space-5` |
48
+ | regular | `--vela-control-height-26` | `--vela-space-10` |
49
+ | large | `--vela-control-height-35` | `--vela-space-10` |
50
+
51
+ These are the **input family** heights and do not match Button's ramp. See
52
+ `foundations/sizing.md`.
53
+
54
+ ## Accessibility
55
+
56
+ - Label is a real `<label htmlFor>`.
57
+ - Error state sets `aria-invalid` and the message is announced via `role="alert"`.
58
+ - The `*` is `aria-hidden`; `required` carries the semantics.
59
+
60
+ ## Anti-patterns
61
+
62
+ - ❌ Placeholder as the label.
63
+ - ❌ `error={true}` with the message elsewhere → pass the message.
64
+ - ❌ Turning the border red without `aria-invalid`.
65
+ - ❌ Input `regular` (26px) next to Button `regular` (35px) without deliberate alignment.
@@ -0,0 +1,48 @@
1
+ # Status Indicator
2
+
3
+ A coloured dot representing one of exactly five health states, usually in a table cell or
4
+ next to an object name.
5
+
6
+ ## When to use Status Indicator
7
+
8
+ - An object's ongoing health → **Status Indicator**.
9
+ - An event the user must read → **Contextual Alert**.
10
+ - A risk score → the Risk taxonomy, on a Token Pill (**not in this kit**). Flag the gap.
11
+
12
+ ## Props
13
+
14
+ ```tsx
15
+ <StatusIndicator status="unhealthy" label="api-gateway-01" />
16
+ ```
17
+
18
+ | Prop | Type | Default | Notes |
19
+ |---|---|---|---|
20
+ | `status` | `"unknown" \| "healthy" \| "warning" \| "medium" \| "unhealthy"` | — | **Required. These five only.** |
21
+ | `label` | `string` | — | Visible text. **Strongly preferred.** |
22
+ | `size` | `"small" \| "regular"` | `"regular"` | **These two only.** |
23
+
24
+ ## Hard constraints
25
+
26
+ - **Five states.** Not severity's six, not risk's three. There is no `"critical"` status and
27
+ no `"high"` status — the compiler rejects both.
28
+ - **`medium` means degraded**, sitting between `warning` and `unhealthy`. It is not the
29
+ middle of a risk scale.
30
+ - **Colour is never the only signal.** With no `label`, the component sets an `aria-label`
31
+ from the status ("Unhealthy", "Degraded", …) so the dot is never silent. With a `label`,
32
+ it does not duplicate it — the visible text is the accessible name.
33
+
34
+ ## Token bindings
35
+
36
+ Fill is `--vela-signal-status-*` only. The dot uses `--vela-radius-full` — one of only two
37
+ components allowed to.
38
+
39
+ In light mode `warning` binds `yellow-700` and `medium` binds `orange-600` rather than the
40
+ 500s, so both clear 3:1 against the page. See `foundations/color.md`.
41
+
42
+ ## Anti-patterns
43
+
44
+ - ❌ `status="critical"` → that is severity (will not compile).
45
+ - ❌ Binding `--vela-signal-severity-*` to the dot → wrong taxonomy.
46
+ - ❌ A bare dot in a table with no label and no header context.
47
+ - ❌ Six or seven states → the set is closed at five.
48
+ - ❌ `--vela-radius-full` borrowed for other components.
@@ -0,0 +1,71 @@
1
+ # Tabs
2
+
3
+ Navigate between **different** content panels.
4
+
5
+ ## When to use Tabs (vs. something else)
6
+
7
+ - Different content sections (Alerts / Assets / Audit) → **Tabs**.
8
+ - The **same** dataset rendered differently (Grid / Dashboard / Chart) → **View Switcher**,
9
+ which is **not in this kit**. Flag the gap; do not substitute Tabs. If only the
10
+ *presentation* of one dataset changes, it is not Tabs.
11
+ - Sequential steps → a Wizard, not Tabs.
12
+ - More than ~7 panels → reconsider the information architecture.
13
+
14
+ ## Composition
15
+
16
+ ```tsx
17
+ <Tabs defaultValue="alerts" onValueChange={setView}>
18
+ <Tabs.List aria-label="Views">
19
+ <Tabs.Trigger value="alerts" count={12}>Alerts</Tabs.Trigger>
20
+ <Tabs.Trigger value="assets">Assets</Tabs.Trigger>
21
+ <Tabs.Trigger value="audit" disabled>Audit</Tabs.Trigger>
22
+ </Tabs.List>
23
+ <Tabs.Panel value="alerts">…</Tabs.Panel>
24
+ <Tabs.Panel value="assets">…</Tabs.Panel>
25
+ </Tabs>
26
+ ```
27
+
28
+ | Part | Prop | Type | Notes |
29
+ |---|---|---|---|
30
+ | `Tabs` | `defaultValue` | `string` | **Required.** |
31
+ | | `value` / `onValueChange` | `string` / `(v) => void` | Controlled mode. |
32
+ | | `size` | `"regular" \| "large"` | **These two only.** |
33
+ | `Tabs.List` | `aria-label` | `string` | **Required.** |
34
+ | `Tabs.Trigger` | `value` | `string` | Must match a Panel. |
35
+ | | `count` | `number` | Optional numeric badge. |
36
+ | | `disabled` | `boolean` | Skipped by arrow-key navigation. |
37
+ | `Tabs.Panel` | `value` | `string` | Must match a Trigger. |
38
+
39
+ ## Hard constraints
40
+
41
+ - **`Tabs.List` requires `aria-label`.** An unnamed tablist is unnavigable by screen reader,
42
+ so the type makes it impossible.
43
+ - **Parts must be used inside `<Tabs>`.** They throw a named error otherwise, rather than
44
+ rendering something silently broken.
45
+ - **Only the active panel renders.** Do not rely on hidden panels holding DOM state.
46
+ - Controlled and uncontrolled are exclusive.
47
+
48
+ ## Token bindings
49
+
50
+ Trigger rest `--vela-text-de-emphasized`; hover `--vela-text-default`; selected
51
+ `--vela-text-link` with a 2px `--vela-border-active` underline. List rule
52
+ `--vela-border-subtle`.
53
+
54
+ ## Accessibility
55
+
56
+ This is the reason Tabs is more than a styled list:
57
+
58
+ - Correct roles throughout: `tablist` / `tab` / `tabpanel`, with `aria-selected`,
59
+ `aria-controls` and `aria-labelledby` wired both ways.
60
+ - **Roving tabindex** — the whole tablist is one Tab stop; Left/Right move between tabs and
61
+ Home/End jump to the ends. Disabled tabs are skipped, and the list wraps.
62
+ - The panel is focusable so keyboard users land in the content after activating a tab.
63
+
64
+ ## Anti-patterns
65
+
66
+ - ❌ Tabs for the same dataset in a different view → View Switcher.
67
+ - ❌ `<Tabs.List>` without `aria-label` → will not compile.
68
+ - ❌ Buttons in a `<div>` with click handlers → no roles, no keyboard, no announcement.
69
+ - ❌ Every tab tabbable → use the roving tabindex the component already implements.
70
+ - ❌ Tabs as page navigation → use real links so URLs work.
71
+ - ❌ Keeping hidden panels mounted to preserve form state → lift the state instead.
@@ -0,0 +1,53 @@
1
+ # Toggle
2
+
3
+ A binary setting that takes effect **immediately**. No Save step.
4
+
5
+ ## When to use Toggle
6
+
7
+ - Notifications on/off, dark mode, "enable this rule" → **Toggle**.
8
+ - Records intent until a Save click (accept terms, pick scope in a form) → **Checkbox**,
9
+ which is not in this kit. Flag the gap rather than substituting a Toggle.
10
+ - More than two states → not a Toggle.
11
+
12
+ ## Props
13
+
14
+ ```tsx
15
+ <Toggle label="Email alerts" checked={on} onChange={setOn} />
16
+ ```
17
+
18
+ | Prop | Type | Default | Notes |
19
+ |---|---|---|---|
20
+ | `label` | `string` | — | **Required.** A switch with no name is unusable by screen reader. |
21
+ | `checked` | `boolean` | — | Controlled. Omit for uncontrolled. |
22
+ | `defaultChecked` | `boolean` | `false` | Uncontrolled initial state. |
23
+ | `onChange` | `(checked: boolean) => void` | — | Receives the **next** value. |
24
+ | `disabled` | `boolean` | `false` | |
25
+ | `labelPosition` | `"right" \| "left"` | `"right"` | |
26
+ | `size` | `"tiny" \| "regular"` | `"regular"` | **These two only.** |
27
+
28
+ ## Hard constraints
29
+
30
+ - **`label` is required and is a `string`.** Not optional, not `ReactNode`. The type system
31
+ makes an unlabelled switch impossible.
32
+ - Controlled and uncontrolled are exclusive: pass `checked` **or** `defaultChecked`.
33
+ - The change is immediate. Never pair a Toggle with a Save button that gates it.
34
+
35
+ ## Token bindings
36
+
37
+ Track off `--vela-toggle-off-bg`; on `--vela-toggle-on-bg`; knob `--vela-toggle-knob`;
38
+ disabled track `--vela-border-inactive`. Track radius is `--vela-radius-full` — one of only
39
+ two components allowed to use it.
40
+
41
+ ## Accessibility
42
+
43
+ - Renders a native `<button role="switch">` with `aria-checked`.
44
+ - Enter and Space activate it; it is in the tab order.
45
+ - The visible label is wired via `aria-labelledby` and is itself clickable.
46
+ - Motion respects `prefers-reduced-motion`.
47
+
48
+ ## Anti-patterns
49
+
50
+ - ❌ A Toggle behind a Save button → that is a Checkbox.
51
+ - ❌ `<Toggle />` with no label → will not compile.
52
+ - ❌ Toggles for more than two states.
53
+ - ❌ A custom `<div>` with a click handler → loses role, keyboard and announcement.
@@ -0,0 +1,67 @@
1
+ # Foundation: Colour
2
+
3
+ Two layers: **primitives** (raw scale) and **semantics** (role-named, resolving to
4
+ primitives). **Always bind semantics.** Primitives exist so semantics can be re-pointed for
5
+ theming; binding one directly defeats that and breaks dark mode.
6
+
7
+ - ✅ `color: var(--vela-text-default)` — survives re-skinning
8
+ - ⚠️ `color: var(--vela-grey-700)` — primitive; only when no semantic covers the role
9
+ - ❌ `color: #191d1f` — never raw hex
10
+
11
+ ## Primitives (reference only)
12
+
13
+ Families: `grey` (50–950), `primary` (300–600), `blue` (100–950), `green` (100–950),
14
+ `red` (100–950), `critical` (100–950), `yellow` (100–950), `orange` (100–950),
15
+ plus `--vela-black` (#191d1f, not pure black) and `--vela-white`.
16
+
17
+ ## Three separate status taxonomies — keep them apart
18
+
19
+ Vela deliberately separates **severity** (events), **risk** (scoring), and **status** (the
20
+ health dot). Do not interchange them. There is no "critical risk", no "high severity", and
21
+ the dot states are neither.
22
+
23
+ ### Severity — 6 levels
24
+
25
+ `success` · `info` · `warning` · `minor` · `major` · `critical`
26
+
27
+ Every level defines a complete row: `--vela-bg-severity-*`, `--vela-border-severity-*`,
28
+ `--vela-signal-severity-*`, `--vela-icon-severity-*`, `--vela-text-severity-*`,
29
+ `--vela-border-severity-*-indicator`. **No cell is missing.** If a token you expect does not
30
+ resolve, you have the wrong taxonomy, not a gap.
31
+
32
+ ### Risk — 3 levels
33
+
34
+ `low` · `medium` · `high` — same six roles per level, prefixed `--vela-*-risk-*`.
35
+
36
+ ### Status — 5 dot fills
37
+
38
+ `--vela-signal-status-` + `unknown` | `healthy` | `warning` | `medium` | `unhealthy`.
39
+ One row, five swatches, no other roles. Status Indicator only.
40
+
41
+ ## Contrast is a constraint, not an aspiration
42
+
43
+ Every foreground/background pair the kit ships is asserted against WCAG 2.1 AA in
44
+ `src/styles/contrast.test.ts` — 4.5:1 for text, 3:1 for UI boundaries and non-text signals,
45
+ **in both themes**. Run `npm run contrast` for the report.
46
+
47
+ Consequences you will notice, all deliberate:
48
+
49
+ - `--vela-btn-destructive-bg` is `red-700` (6.57:1), **not** `red-500`. Red-500 behind a white
50
+ label is 4.08:1 and fails AA. Do not "fix" it back to the signal colour.
51
+ - `--vela-text-de-emphasized` is `grey-600` (4.90:1), not `grey-500` (3.47:1).
52
+ - `--vela-text-severity-success` is `green-800` (6.68:1). `green-700` would technically pass at
53
+ 4.87:1; green-800 is kept for headroom, so a future palette tweak cannot silently cross the
54
+ line. That is a judgement call, not a compliance requirement — recorded here as one.
55
+ - Status dots use `yellow-700` (4.07:1) and `orange-600` (3.80:1), not the 500s, which sit at
56
+ 2.59:1 and 2.78:1 on white.
57
+ - Tightest shipped pair: the input border at 3.29:1 against a 3:1 minimum. Anything you add
58
+ below that is a regression.
59
+
60
+ If you add a pair, add it to `PAIRS` in `scripts/contrast-core.mjs`. An unasserted pair is
61
+ an unverified claim.
62
+
63
+ ## Both themes are real
64
+
65
+ Dark mode re-points semantics only — no primitive is redefined, and a test fails the build if
66
+ a raw hex appears in the dark block. Anything you write must therefore bind semantics, or it
67
+ will simply not respond to the theme.
@@ -0,0 +1,17 @@
1
+ # Foundation: Radius
2
+
3
+ Four tokens, each with a scope. Using one outside its scope is drift.
4
+
5
+ | Token | Value | Scope |
6
+ |---|---|---|
7
+ | `--vela-radius-3` | 3px | Small inner elements (dismiss buttons, inline chips) |
8
+ | `--vela-radius-4` | 4px | **The default for every component** |
9
+ | `--vela-radius-8` | 8px | Modal / dialog containers only |
10
+ | `--vela-radius-12` | 12px | Top-level side panel outer edge only |
11
+ | `--vela-radius-full` | 9999px | Status Indicator dots and the Toggle track only |
12
+
13
+ ## Anti-patterns
14
+
15
+ - ❌ Pill-shaped buttons (`border-radius: 999px`) → buttons are `--vela-radius-4`.
16
+ - ❌ `--vela-radius-full` on anything but a status dot or a toggle track.
17
+ - ❌ `--vela-radius-8` on a card → cards are `--vela-radius-4`; 8 is for modals.
@@ -0,0 +1,35 @@
1
+ # Foundation: Sizing
2
+
3
+ ## Control heights
4
+
5
+ | Token | Value | Used by |
6
+ |---|---|---|
7
+ | `--vela-control-height-24` | 24px | Button `tiny` |
8
+ | `--vela-control-height-25` | 25px | Input `tiny` |
9
+ | `--vela-control-height-26` | 26px | Input `regular` |
10
+ | `--vela-control-height-35` | 35px | Button `regular`, Input `large` |
11
+ | `--vela-control-height-40` | 40px | Button `large` |
12
+ | `--vela-control-height-48` | 48px | Reserved |
13
+ | `--vela-control-height-60` | 60px | Button `huge` |
14
+
15
+ **Button and the input family do not share a height ramp.** A `regular` Button is 35px; a
16
+ `regular` Input is 26px. Placing them on one row requires deliberate alignment — this is a
17
+ known property of the system, not a bug to normalise away.
18
+
19
+ ## Icon sizes
20
+
21
+ | Token | Value | Used by |
22
+ |---|---|---|
23
+ | `--vela-icon-12` | 12px | Button `tiny` |
24
+ | `--vela-icon-16` | 16px | Button `regular`/`large`/`huge`, alert icons |
25
+ | `--vela-icon-20` | 20px | Dismiss controls |
26
+ | `--vela-icon-24` | 24px | Reserved |
27
+
28
+ **Button icons do not scale past 16px.** 12 at tiny, 16 everywhere else. `huge` is 60px tall
29
+ with a 16px icon — that is correct.
30
+
31
+ ## Anti-patterns
32
+
33
+ - ❌ `--vela-control-height-48` for Button `huge` → huge is 60px.
34
+ - ❌ `--vela-icon-20` or `-24` inside a Button → 12 or 16 only.
35
+ - ❌ Fixed button widths → buttons hug their content.
@@ -0,0 +1,28 @@
1
+ # Foundation: Spacing
2
+
3
+ A 5px-based scale. **`25` exists; `50` does not** — this is intentional, not an omission.
4
+
5
+ | Token | Value | Typical use |
6
+ |---|---|---|
7
+ | `--vela-space-5` | 5px | Icon-to-label gap |
8
+ | `--vela-space-10` | 10px | Control padding, tight stacks |
9
+ | `--vela-space-15` | 15px | Default button padding, form gaps |
10
+ | `--vela-space-20` | 20px | Section padding, tab gaps |
11
+ | `--vela-space-25` | 25px | Wide inner padding |
12
+ | `--vela-space-30` | 30px | Large button padding |
13
+ | `--vela-space-40` | 40px | Section separation |
14
+ | `--vela-space-60` | 60px | Page-level rhythm |
15
+ | `--vela-space-80` | 80px | Page-level rhythm |
16
+
17
+ ## Rules
18
+
19
+ - Values already carry `px`. `padding: var(--vela-space-10)` — never `calc(... * 1px)`.
20
+ - **Do not interpolate.** There is no 35, no 50, no 70. If a layout seems to need one, you
21
+ are probably compensating for a wrong control height.
22
+ - Use `gap` on a flex/grid parent rather than margins on children.
23
+
24
+ ## Anti-patterns
25
+
26
+ - ❌ `padding: 12px` → use `--vela-space-10` or `--vela-space-15`.
27
+ - ❌ `--vela-space-50` → does not exist; the scale skips it.
28
+ - ❌ Margin-bottom stacks → use `gap`.
@@ -0,0 +1,34 @@
1
+ # Foundation: Typography
2
+
3
+ **Open Sans** for all UI text (Light 300, Regular 400, SemiBold 600).
4
+ **Oswald** for large stat/number displays only. Both have system fallbacks.
5
+
6
+ ## The scale
7
+
8
+ | Role | Size / Weight / Line-height | Class |
9
+ |---|---|---|
10
+ | H1 | 32 / 300 / 42 | `.vela-h1` |
11
+ | H2 | 24 / 300 / 33 | `.vela-h2` |
12
+ | H3 | 20 / 300 / 27 | `.vela-h3` |
13
+ | H4 | 16 / 400 / 24 | `.vela-h4` |
14
+ | H5 | 14 / 600 / 21 | `.vela-h5` |
15
+ | H6 | 12 / 600 / 18 | `.vela-h6` |
16
+ | Body | 14 / 400 / 21 | `.vela-body` |
17
+ | Meta | 12 / 400 / 18, +0.3 tracking | `.vela-meta` |
18
+
19
+ Note the weight inversion: H1–H3 are **Light 300** and get their prominence from size;
20
+ H5–H6 are **SemiBold 600** and get theirs from weight. Do not "correct" H1 to bold.
21
+
22
+ ## Rules
23
+
24
+ - Use the class that matches the **role**, not the one that matches the size you want.
25
+ - Button labels are **Regular 400 at every size** — never SemiBold, never Bold.
26
+ - Button labels are Title Case and verb-first: "Save Changes", not "Information".
27
+ - Each size is a triple. Never take a size without its line-height.
28
+
29
+ ## Anti-patterns
30
+
31
+ - ❌ `font-size: 15px` outside a Button `large` → not on the scale.
32
+ - ❌ A bold H1 → H1 is Light 300.
33
+ - ❌ SemiBold button labels → Regular 400 at every size.
34
+ - ❌ Setting `font-family`/`font-size`/`line-height` by hand → use the class or the tokens.
@@ -0,0 +1,33 @@
1
+ # Vela Design System
2
+
3
+ > A token-first React component kit. Two-layer colour architecture (primitives -> semantics),
4
+ > three deliberately separate status taxonomies, full light and dark theming, zero runtime
5
+ > dependencies. Every design constraint below is enforced by the TypeScript types, not by review.
6
+
7
+ Read `overview.md` first: it maps a user need to a component and lists the choices agents
8
+ most often get wrong. Then read the per-component file before generating any markup.
9
+
10
+ ## Hard rules (apply everywhere)
11
+ - Bind SEMANTIC tokens (`var(--vela-text-default)`). Never a primitive (`--vela-grey-700`), never raw hex.
12
+ - Numeric tokens already carry their unit. Use `var(--vela-space-10)`, never `calc(var(--vela-space-10) * 1px)`.
13
+ - Prop sets are CLOSED. If a value is not in the component's Props table, it does not exist.
14
+ - Never invent a component. If nothing fits, say so and name the closest match.
15
+ - Severity, Risk and Status are three different taxonomies. Never cross them.
16
+
17
+ ## Docs
18
+ - [Component catalogue and disambiguations](overview.md)
19
+ - [Install and theming](setup.md)
20
+ - [Token naming and lookup](tokens.md)
21
+ - [Colour](foundations/color.md)
22
+ - [Spacing](foundations/spacing.md)
23
+ - [Sizing](foundations/sizing.md)
24
+ - [Radius](foundations/radius.md)
25
+ - [Typography](foundations/typography.md)
26
+
27
+ ## Components
28
+ - [Button](components/button.md)
29
+ - [Toggle](components/toggle.md)
30
+ - [Input](components/input.md)
31
+ - [Contextual Alert](components/contextual-alert.md)
32
+ - [Status Indicator](components/status-indicator.md)
33
+ - [Tabs](components/tabs.md)