indira-workspace-ui 0.1.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.
- package/.claude/skills/indira-api/SKILL.md +148 -0
- package/.claude/skills/indira-api/reference/api-architecture.md +337 -0
- package/.claude/skills/indira-ui/SKILL.md +149 -0
- package/README.md +60 -0
- package/bin/cli.js +65 -0
- package/dist-lib/indira-workspace-ui.cjs +2 -0
- package/dist-lib/indira-workspace-ui.cjs.map +1 -0
- package/dist-lib/indira-workspace-ui.mjs +1522 -0
- package/dist-lib/indira-workspace-ui.mjs.map +1 -0
- package/dist-lib/style.css +725 -0
- package/docs/design-system.md +462 -0
- package/package.json +68 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# Indira Workspace UI — design system specification
|
|
2
|
+
|
|
3
|
+
A build-from-scratch specification for the UI language used by the BRD
|
|
4
|
+
Governance Tool. Written so an agent (or a person) can reproduce the library in
|
|
5
|
+
any stack without seeing the original code. Where a number appears, it is the
|
|
6
|
+
number — not a suggestion.
|
|
7
|
+
|
|
8
|
+
The reference implementation is React + Tailwind, but nothing here depends on
|
|
9
|
+
either. The system is: **a token set, a fixed set of scales, one shell, ~25
|
|
10
|
+
components, and a short list of laws.** Everything the product looks like falls
|
|
11
|
+
out of those.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 0. The three sentences
|
|
16
|
+
|
|
17
|
+
1. **Constraint and repetition read as "designed"; variety reads as "generated".**
|
|
18
|
+
One accent, one radius set, one shadow set, one icon family, one type scale.
|
|
19
|
+
2. **Status is data, the brand is chrome.** Six status hues carry meaning; the
|
|
20
|
+
brand colour carries *action*. They never trade places.
|
|
21
|
+
3. **A person who opens a screen must find its one primary action within two
|
|
22
|
+
seconds, and must never be shown data they did not come for.**
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. Tokens
|
|
27
|
+
|
|
28
|
+
Tokens are CSS custom properties holding **"R G B" triplets** (no `rgb()`), so
|
|
29
|
+
every colour can take an opacity modifier: `rgb(var(--c-primary) / 0.12)`.
|
|
30
|
+
Two token sets — light and dark — over one set of components. Dark is *re-toned*,
|
|
31
|
+
not inverted: it never bottoms out at pure black, and the brand is lifted so it
|
|
32
|
+
still clears 7:1.
|
|
33
|
+
|
|
34
|
+
### 1.1 Colour — light
|
|
35
|
+
|
|
36
|
+
| Token | Value | Role |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `--c-primary` | `182 30 66` (#B61E42) | The single accent. Primary buttons, active nav, focus rings, links. |
|
|
39
|
+
| `--c-on-primary` | `255 255 255` | Text/icon on primary. |
|
|
40
|
+
| `--c-primary-container` | `243 204 219` (#F3CCDB) | Tonal buttons, active chip fill, accent notices. Deliberately a saturated rose, not a pale tint: a pale pink is 12/255 from the "rejected" chip and reads as an error. |
|
|
41
|
+
| `--c-on-primary-container` | `122 18 48` | Text on the container. |
|
|
42
|
+
| `--c-canvas` | `247 249 251` | The page ground behind everything. |
|
|
43
|
+
| `--c-surface` | `255 255 255` | Cards, table bodies, dialogs. |
|
|
44
|
+
| `--c-surface-1` | `248 250 252` | Hover, table header rows, wells. |
|
|
45
|
+
| `--c-surface-2` | `241 245 249` | Controls at rest, tab tracks, count boxes, notices. |
|
|
46
|
+
| `--c-surface-3` | `232 238 245` | Pressed / selected. |
|
|
47
|
+
| `--c-sidebar` | `255 255 255` | Navigation column and header. Light, never a dark rail. |
|
|
48
|
+
| `--c-sidebar-line` | `233 237 243` | Hairline inside the sidebar. |
|
|
49
|
+
| `--c-on-surface` | `15 23 42` | Headings, primary text ("ink"). |
|
|
50
|
+
| `--c-on-surface-var` | `71 85 105` | Secondary text ("ink-muted"). |
|
|
51
|
+
| `--c-on-surface-faint` | `100 116 139` | Tertiary text, timestamps ("ink-faint"). Held at 4.8:1 on white — this is the floor for anything that is *text*. |
|
|
52
|
+
| `--c-outline-var` | `233 237 243` | The hairlines that separate ("line"). |
|
|
53
|
+
| `--c-outline` | `203 213 225` | Control borders ("line-strong"). |
|
|
54
|
+
|
|
55
|
+
Status — the **entire data palette**. Charts may draw from here and nowhere else.
|
|
56
|
+
|
|
57
|
+
| Token | Light | Dark | Meaning |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `--c-st-draft` | `148 163 184` | `148 163 184` | Not yet submitted |
|
|
60
|
+
| `--c-st-submitted` | `37 99 235` | `96 165 250` | Sent, level 1 not yet acted |
|
|
61
|
+
| `--c-st-review` | `2 132 199` | `56 189 248` | In review |
|
|
62
|
+
| `--c-st-approved` | `22 163 74` | `74 222 128` | Approved / published |
|
|
63
|
+
| `--c-st-rejected` | `220 38 38` | `248 113 113` | Rejected |
|
|
64
|
+
| `--c-st-sentback` | `217 119 6` | `251 191 36` | Sent back for changes; also "attention" |
|
|
65
|
+
|
|
66
|
+
Each status also has a chip triplet — pastel fill, saturated label, tinted
|
|
67
|
+
border — `--chip-{status}`, `--chip-{status}-fg`, `--chip-{status}-bd`:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
draft 241 245 249 / 71 85 105 / 226 232 240
|
|
71
|
+
submitted 239 246 255 / 29 78 216 / 219 234 254
|
|
72
|
+
review 224 242 254 / 3 105 161 / 186 230 253
|
|
73
|
+
approved 220 252 231 / 21 128 61 / 187 247 208
|
|
74
|
+
rejected 254 226 226 / 185 28 28 / 254 202 202
|
|
75
|
+
sentback 254 243 199 / 180 83 9 / 253 230 138
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Colour-vision rule: approved↔rejected (green↔red) and submitted↔review
|
|
79
|
+
(blue↔cyan) fail deuteranopia when the segments *touch*. So: no stacked bars
|
|
80
|
+
with adjacent statuses; status always pairs colour with a **shape** (icon or
|
|
81
|
+
label); KPI tiles instead of distribution bars; the only chart is single-series
|
|
82
|
+
in the accent.
|
|
83
|
+
|
|
84
|
+
### 1.2 Colour — dark
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
--c-primary 242 135 158 --c-on-primary 92 12 36
|
|
88
|
+
--c-primary-container 116 18 46 --c-on-primary-container 251 208 218
|
|
89
|
+
--c-canvas 15 18 24 --c-surface 22 26 33 --c-surface-1 28 33 41
|
|
90
|
+
--c-surface-2 36 42 52 --c-surface-3 45 53 64
|
|
91
|
+
--c-sidebar 22 26 33 --c-sidebar-line 42 49 60
|
|
92
|
+
--c-on-surface 226 232 240 --c-on-surface-var 160 172 187 --c-on-surface-faint 125 138 154
|
|
93
|
+
--c-outline-var 42 49 60 --c-outline 65 74 87
|
|
94
|
+
chips: draft 36 42 52/160 172 187/51 59 71 · submitted 23 41 77/147 197 253/30 58 107
|
|
95
|
+
review 12 53 80/125 211 252/18 73 110 · approved 16 55 31/134 239 172/23 81 45
|
|
96
|
+
rejected 69 22 26/252 165 165/97 33 38 · sentback 64 48 12/252 211 77/90 68 19
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Selected with `[data-theme='dark']` on the root; the default follows
|
|
100
|
+
`prefers-color-scheme`. Shadows disappear on dark surfaces, so every card gets
|
|
101
|
+
a 1px `--c-outline-var` border in dark mode only.
|
|
102
|
+
|
|
103
|
+
### 1.3 Elevation — three steps, nothing else
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
--elev-1: 0 1px 2px rgb(15 23 42 / 0.04), 0 1px 3px rgb(15 23 42 / 0.06) controls at rest, filled buttons
|
|
107
|
+
--elev-2: 0 2px 4px rgb(15 23 42 / 0.05), 0 8px 16px rgb(15 23 42 / 0.08) cards, menus, popovers
|
|
108
|
+
--elev-3: 0 4px 8px rgb(15 23 42 / 0.06), 0 16px 32px rgb(15 23 42 / 0.14) dialogs, contact cards
|
|
109
|
+
dark: same offsets with rgb(0 0 0 / 0.3–0.5)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Cards carry `--elev-2` and **no border** in light mode. Hairlines (`--c-outline-var`)
|
|
113
|
+
separate *regions inside* a card (header from body, table rows); shadows
|
|
114
|
+
separate the card from the canvas.
|
|
115
|
+
|
|
116
|
+
### 1.4 Radius — four values
|
|
117
|
+
|
|
118
|
+
| Name | px | Where |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| `xs` | 8 | Inputs, tags, chips' count boxes, alert boxes, small buttons, tab segments |
|
|
121
|
+
| `sm` | 12 | Nav items, menus, dialogs, inner panels, tab tracks, contact cards |
|
|
122
|
+
| `lg` | 16 | Cards — the dominant shape |
|
|
123
|
+
| `full` | 9999 | Buttons, chips, pills, avatars, badges |
|
|
124
|
+
|
|
125
|
+
24px appears exactly once: the rounded corner where the content panel meets
|
|
126
|
+
the shell (§3).
|
|
127
|
+
|
|
128
|
+
### 1.5 Type — one family, six sizes, four weights
|
|
129
|
+
|
|
130
|
+
Family: `Inter, "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif`.
|
|
131
|
+
Mono: `JetBrains Mono, Cascadia Mono, Consolas, ui-monospace`. Document
|
|
132
|
+
(rendered BRD only): `Georgia, Tinos, "Times New Roman", serif`.
|
|
133
|
+
|
|
134
|
+
| Token | size / line | Weight | Use |
|
|
135
|
+
| --- | --- | --- | --- |
|
|
136
|
+
| `xs` | 12 / 16 | 500–600 | Table headers (uppercase, tracking 0.06em), meta, captions, labels, chips |
|
|
137
|
+
| `sm` | 14 / 20 | 400–500 | Body, table cells, nav items, buttons (600) |
|
|
138
|
+
| `base` | 16 / 24 | 500–600 | Card titles, names in headers |
|
|
139
|
+
| `lg` | 20 / 28 | 600 | Detail-page titles |
|
|
140
|
+
| `xl` | 24 / 32 | 600 | KPI figures, compact page titles (when one is used at all) |
|
|
141
|
+
| `2xl` | 36 / 40 | 700 | Reserved. Not used on workspace screens. |
|
|
142
|
+
|
|
143
|
+
Two deliberate exceptions, set in component CSS rather than utilities so the
|
|
144
|
+
scale itself stays closed: card titles at **18/24 medium**, and contact-card
|
|
145
|
+
rows at **13px**.
|
|
146
|
+
|
|
147
|
+
Weights: 400 body · 500 labels/nav · 600 titles, buttons, figures · 700 never
|
|
148
|
+
in the workspace. Numbers are always `tabular-nums`.
|
|
149
|
+
|
|
150
|
+
Enforce the scale by **deleting** the framework's defaults, not extending them:
|
|
151
|
+
an off-scale utility should fail to compile.
|
|
152
|
+
|
|
153
|
+
### 1.6 Spacing — the 4/8 grid, and the numbers that recur
|
|
154
|
+
|
|
155
|
+
| Value | Where it recurs |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| 4 | Gap between icon-only buttons; tab-track padding; chip count-box inset |
|
|
158
|
+
| 8 | Gap between a label and its control; gap between meta rows; toolbar-button gap |
|
|
159
|
+
| 12 | Gap between chips; gap between avatar and text; contact-card row gap |
|
|
160
|
+
| 16 | Card inner padding (compact); table cell horizontal padding; gap between icon and title in a card |
|
|
161
|
+
| 20 | Card padding (BRD card); horizontal padding of notices |
|
|
162
|
+
| 24 | Grid column gap; dialog body padding; card body padding; page gutter (min) |
|
|
163
|
+
| 32 | Page gutter at ≥1280; header horizontal padding |
|
|
164
|
+
| 36 | BRD card side padding; status orb offset from the card edge |
|
|
165
|
+
| 48 | Grid row gap when cards carry an overhanging orb |
|
|
166
|
+
| 72 | Header height |
|
|
167
|
+
| 224 | Sidebar width (84 when collapsed to a rail) |
|
|
168
|
+
|
|
169
|
+
Density over air: 56px table rows, 48px nav items, 40px controls, 34px small
|
|
170
|
+
buttons, 32px chips.
|
|
171
|
+
|
|
172
|
+
### 1.7 Motion
|
|
173
|
+
|
|
174
|
+
One curve: `cubic-bezier(0.2, 0, 0, 1)`. Durations: 100ms (fast), 150ms
|
|
175
|
+
(default, colour/shadow/transform), 200ms (layout: sidebar width). Skeletons,
|
|
176
|
+
never spinners, for content that is loading; a spinner only inside a button
|
|
177
|
+
that is doing something. Nothing pulses at the reader. The one animation is
|
|
178
|
+
`fade-up` (150ms) for menus, cards and dialogs appearing.
|
|
179
|
+
|
|
180
|
+
### 1.8 Icons
|
|
181
|
+
|
|
182
|
+
One family (Lucide geometry), 24-unit grid, stroke 1.75, round joins. Sizes
|
|
183
|
+
14 / 16 / 18 / 20 / 24 only. Inline SVG, ~35 glyphs. **Never emoji.** Every
|
|
184
|
+
status has a glyph as well as a colour: draft `file`, submitted `send`, review
|
|
185
|
+
`clock`, approved `check-circle`, rejected `ban`, sent back `send-back`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 2. Roles, not colours, in components
|
|
190
|
+
|
|
191
|
+
Components reference **roles**, never raw tokens: `primary`, `primary-on`,
|
|
192
|
+
`primary-container`, `canvas`, `surface`, `surface-1..3`, `sidebar`, `ink`,
|
|
193
|
+
`ink-muted`, `ink-faint`, `line`, `line-strong`, `st-{status}`. Swapping the
|
|
194
|
+
brand means changing four numbers in §1.1; nothing else moves.
|
|
195
|
+
|
|
196
|
+
Hover and press are **state layers** — an inset wash of the on-colour over the
|
|
197
|
+
container (`inset 0 0 0 100px rgb(var(--c-on-primary) / 0.12)`) — never a
|
|
198
|
+
second hard-coded colour.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 3. The shell — how the sidebar and header make an "L"
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
┌──────────┬────────────────────────────────────────────────┐
|
|
206
|
+
│ brand │ greeting [ search ] 🔔 (avatar) ▾ │ ← header, 72px, white
|
|
207
|
+
│ ├──╮ │
|
|
208
|
+
│ ● Nav │ ╰─────────────────────────────────────────────┤ ← 24px radius corner
|
|
209
|
+
│ Nav │ │
|
|
210
|
+
│ Nav │ content panel (canvas grey) │
|
|
211
|
+
│ │ │
|
|
212
|
+
│ ⚙ ⏻ │ │
|
|
213
|
+
└──────────┴────────────────────────────────────────────────┘
|
|
214
|
+
224px
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- The **shell** itself is the white surface. The sidebar and the header are
|
|
218
|
+
regions of it, not boxes beside each other — so **neither carries a border**.
|
|
219
|
+
- The **content panel** is the only thing with an edge: canvas grey, with a
|
|
220
|
+
**24px top-left radius** curving into the corner where sidebar meets header.
|
|
221
|
+
That single curve is what makes the two read as one "L".
|
|
222
|
+
- Sidebar: brand mark at top (20px padding), nav items 48px tall, 14px/500,
|
|
223
|
+
icon 20, radius 12, **14px vertical pitch** between items, 16px side padding.
|
|
224
|
+
Active item: solid `primary` fill, `on-primary` text, 600 weight, soft
|
|
225
|
+
tinted shadow `0 8px 16px rgb(var(--c-primary) / 0.22)`. No section
|
|
226
|
+
captions — a 12px gap separates groups. Theme and sign-out pinned at the foot.
|
|
227
|
+
- A collapse control sits on the sidebar's outer edge at a fixed height
|
|
228
|
+
(`top: 26px`, half outside), so it is in the same place whether the sidebar
|
|
229
|
+
is 224px or the 84px rail. Below 1000px the rail is forced.
|
|
230
|
+
- Header: greeting (12px muted "Good morning," over 16px/600 name) on the left;
|
|
231
|
+
search 320px, 40px tall, `surface-1` fill, no border until focus; bell with a
|
|
232
|
+
count badge (16px, primary); avatar 40 + name 14/600 + role 12 muted + chevron
|
|
233
|
+
in a pill trigger. All on one 72px centre line. Header hides when the sidebar
|
|
234
|
+
is railed (focus mode) — the rail keeps theme and sign-out so nothing is lost.
|
|
235
|
+
- Double-tap of the space bar outside any field toggles browser full screen.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 4. Components
|
|
240
|
+
|
|
241
|
+
Each entry: anatomy · measurements · states · rules. All are single-purpose.
|
|
242
|
+
|
|
243
|
+
### 4.1 Button `.btn`
|
|
244
|
+
Pill (`full`), 40px, `px-5`, 14/600, gap 8, icon 18. Small: 34px, `px-4`,
|
|
245
|
+
12px, icon 16. Icon-only: 40×40 (34×34 small). Variants:
|
|
246
|
+
|
|
247
|
+
- **filled** — primary + `elev-1`. **At most one per view.**
|
|
248
|
+
- **tonal** — primary-container. Secondary.
|
|
249
|
+
- **outlined** — surface + `line-strong` border. Tertiary.
|
|
250
|
+
- **text** — transparent. Row actions, "Clear", "View all".
|
|
251
|
+
- **approve / sendback / reject** — status-tinted, used only for the three
|
|
252
|
+
decisions.
|
|
253
|
+
- **attention** modifier — the filled button ringed in sent-back amber
|
|
254
|
+
(`0 0 0 3px rgb(var(--c-st-sentback) / 0.35)`) while the form is incomplete:
|
|
255
|
+
still the thing to press, visibly "not ready yet".
|
|
256
|
+
|
|
257
|
+
Disabled: `opacity: 0.4`, `pointer-events: none`.
|
|
258
|
+
|
|
259
|
+
### 4.2 Card `.card`
|
|
260
|
+
`surface`, radius 16, `elev-2`, no border (hairline in dark). Header
|
|
261
|
+
`.card-h`: 24px horizontal / 20px vertical, hairline below, title 16/600.
|
|
262
|
+
Body `.card-b`: 24px. A card is a *region*, never decoration — do not nest cards.
|
|
263
|
+
|
|
264
|
+
### 4.3 Stat tile `.stat-tile`
|
|
265
|
+
A card with a 44px tinted status icon (radius 8) left, figure 24/600 above a
|
|
266
|
+
12/500 muted label right. Click filters the list it summarises. Active: 2px
|
|
267
|
+
primary ring, no border change.
|
|
268
|
+
|
|
269
|
+
### 4.4 Large-icon record card `.brd-card`
|
|
270
|
+
For lists where the *state* of a record matters more than its metadata.
|
|
271
|
+
Card 16px radius, `elev-2`, padding `48 36 24`, min-height 230, 4 columns from
|
|
272
|
+
1100px, gaps 24 across / 48 down. A **52px solid status orb** (white glyph,
|
|
273
|
+
`0 6px 14px rgb(15 23 42 / 0.14)`) is absolutely positioned at `top: -26px;
|
|
274
|
+
left: 36px` — centred on the card's top edge. Title 18/24/500, clamped to two
|
|
275
|
+
lines **and reserving two** so meta rows align across a row. Two meta rows at
|
|
276
|
+
28px pitch (12px, faint, 14px icon). Hairline. Footer pinned with `mt-auto`:
|
|
277
|
+
two 11/700 labels ("Approvers", "Progress"), a 24px avatar stack (-4px overlap,
|
|
278
|
+
2px surface ring, dashed "+N"), and a 14/700 tabular percentage.
|
|
279
|
+
The card shows **no id, no status text, no priority** — the orb *is* the status;
|
|
280
|
+
the id lives in `title=` and an `sr-only` span.
|
|
281
|
+
|
|
282
|
+
### 4.5 Table `.th` `.td` `.table-plan`
|
|
283
|
+
Header: 12px uppercase 600, tracking 0.06em, faint, `surface-1` row, hairline.
|
|
284
|
+
Cells: 14px, `px-4 py-3.5`, hairline, `overflow-wrap: anywhere` so a long
|
|
285
|
+
email never widens the table. Any table with more than five columns uses
|
|
286
|
+
`table-layout: fixed` with a `<colgroup>` **column plan** in percentages, and
|
|
287
|
+
every single-line cell `truncate`s with the full value in `title=`. Secondary
|
|
288
|
+
facts (email under name, designation under department) go on a second 12px
|
|
289
|
+
faint line inside the cell, not in their own column. Row actions are icon
|
|
290
|
+
buttons with tooltips. Clickable rows use `.row-link` (hover `surface-1`).
|
|
291
|
+
|
|
292
|
+
### 4.6 Filter chips `.fchip` `.fcount`
|
|
293
|
+
32px, radius 8, 13/500, `padding 0 4px 0 12px`, 12px gap between chips. The
|
|
294
|
+
count rides in a 26px box (radius 8, `surface-2`, 11/600 tabular). Active: ink
|
|
295
|
+
label; count box `surface` with a 1px primary border and primary text. Counts
|
|
296
|
+
come from the **full** set so a chip always says how many are behind it.
|
|
297
|
+
|
|
298
|
+
### 4.7 Tabs `.tabs` `.tab` — segmented control
|
|
299
|
+
Track: inline-flex, 4px padding, radius 12, `surface-2`. Segment: 34px,
|
|
300
|
+
`0 14px`, radius 8, 13/500, muted. Active: `surface` fill, `elev-1`, ink,
|
|
301
|
+
600. Badge: 20px, radius 6, `surface-3`; on the active segment it becomes
|
|
302
|
+
primary-container. An 8px amber dot (`.tab-alert`) marks a tab with empty
|
|
303
|
+
required fields — only after a submit attempt, and it does not pulse.
|
|
304
|
+
Tabs switch **views**; chips **filter**. They must not look alike.
|
|
305
|
+
|
|
306
|
+
### 4.8 Toolbar button `.tbtn`
|
|
307
|
+
32px, radius 8, `surface`, `elev-1`, 12/500, icon 16. Icon-only 32×32.
|
|
308
|
+
Active: primary-container. Used for grid/list toggles and "clear search".
|
|
309
|
+
|
|
310
|
+
### 4.9 Inputs `.inp` `.lbl` `.hint`
|
|
311
|
+
40px, radius 8, `line-strong` border, `surface`, 14px, `px-4`. Hover: faint
|
|
312
|
+
border. Focus: primary border + `0 0 0 3px rgb(var(--c-primary) / 0.12)`.
|
|
313
|
+
Label 12/600 muted above with 8px gap; hint 12 faint below. Required marked
|
|
314
|
+
with a primary asterisk. Search inputs carry a 16px glyph at `left: 12px`.
|
|
315
|
+
In dense rows (pipeline editor) controls drop to 36px.
|
|
316
|
+
|
|
317
|
+
### 4.10 Chip / pill / tag
|
|
318
|
+
- **Status pill** `.chip .chip--{status}`: pill, `px-3 py-1`, 12/600, pastel
|
|
319
|
+
fill + saturated text + tinted border, 6px dot in the saturated colour.
|
|
320
|
+
- **Tag** `.tag`: radius 8, `surface-1`, hairline, 12/500 muted. Roles, levels,
|
|
321
|
+
counts.
|
|
322
|
+
- **Version chip** `.vchip`: like tag but mono 12/600. Current version takes
|
|
323
|
+
the primary container.
|
|
324
|
+
|
|
325
|
+
### 4.11 Notice `.notice` (+ `-accent`, `-alert`)
|
|
326
|
+
Radius 8, **no border**, `surface-2` fill (accent: primary-container; alert:
|
|
327
|
+
rejected chip fill), `px-5 py-4`, 14px/1.5. The 20px icon sits on the first
|
|
328
|
+
line's centre (`mt-0.5`) and carries the tone colour. Text is `min-w-0 flex-1`;
|
|
329
|
+
an optional action is `shrink-0` on the right.
|
|
330
|
+
|
|
331
|
+
### 4.12 Avatar
|
|
332
|
+
Circle, initials 600 white on one of six tints derived from the name
|
|
333
|
+
(`#1D4ED8 #6D28D9 #15803D #B45309 #475569 #0F766E` — all outside the brand's
|
|
334
|
+
red family and outside the status palette, so an avatar can never be mistaken
|
|
335
|
+
for either). Renders a photo instead when the person has one. Sizes 20 / 24 /
|
|
336
|
+
28 / 32 / 40 / 44 / 48. Stacks overlap by 4px with a 2px surface ring.
|
|
337
|
+
|
|
338
|
+
### 4.13 Person + contact card `.person` `.person-card`
|
|
339
|
+
Any rendered person is a trigger: hover 350ms in / 180ms out, focus opens
|
|
340
|
+
immediately, Esc closes. The card (300px, radius 12, `elev-3`, hairline) is
|
|
341
|
+
portalled to `body` and positioned `fixed` below the trigger, flipping above
|
|
342
|
+
when it would not fit. Head: 48px avatar, name 16/600, designation 12 muted,
|
|
343
|
+
status tag if not active. Body rows (13px, 16px icon, 10px gap): email as a
|
|
344
|
+
mailto link, department, "Reports to …", role. Resolves from id, email, or
|
|
345
|
+
name against one cached directory request. Inside a `<button>` it is
|
|
346
|
+
hover-only (no second focusable).
|
|
347
|
+
|
|
348
|
+
### 4.14 Stepper (approval route)
|
|
349
|
+
Stamp: 40px circle, 2px border, 12/500. Four states, each a **shape as well as
|
|
350
|
+
a colour** so it survives a greyscale print: done = primary fill + check;
|
|
351
|
+
current = primary-container + primary ring 4px at 14%; rejected = rejected
|
|
352
|
+
chip + strike; waiting = surface + `line-strong`. Horizontal (strip, 168px
|
|
353
|
+
min per step, connector at `top: 20px`) or vertical (connector down the left
|
|
354
|
+
at `left: 20px`, 24px gap between steps, text left-aligned). No pulsing.
|
|
355
|
+
|
|
356
|
+
### 4.15 Dialog `.modal`
|
|
357
|
+
Scrim `black/30`, panel `surface` radius 12 `elev-3`, `max-h: calc(100vh - 48px)`,
|
|
358
|
+
**flex column: header and footer fixed, only the body scrolls** (24px padding).
|
|
359
|
+
Focus trapped, returned to the invoker on close, Esc closes. Sizes 480 / 640 /
|
|
360
|
+
960.
|
|
361
|
+
|
|
362
|
+
### 4.16 Full-screen document `.doc-full`
|
|
363
|
+
`fixed inset-0`, canvas fill, page 920px centred, one pill "Exit" control
|
|
364
|
+
fixed top-right (40px, `elev-2`). Esc closes. Body scroll locked beneath.
|
|
365
|
+
|
|
366
|
+
### 4.17 Skeleton `.skel`
|
|
367
|
+
Rounded bars in `surface-2`, in the **shape of the content** (a card skeleton
|
|
368
|
+
has an orb, two title lines, meta, a rule, a footer) so nothing reflows when
|
|
369
|
+
data lands.
|
|
370
|
+
|
|
371
|
+
### 4.18 Empty state
|
|
372
|
+
48px circular `surface-1` icon well, 14/500 title, 14 muted line (max 46ch),
|
|
373
|
+
one tonal small button. Never a paragraph.
|
|
374
|
+
|
|
375
|
+
### 4.19 Toast
|
|
376
|
+
Bottom-centre, `elev-2`, 14px, auto-dismiss; error variant uses the rejected
|
|
377
|
+
chip colours. One at a time.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 5. Laws
|
|
382
|
+
|
|
383
|
+
These are the rules the components exist to serve. A screen that follows every
|
|
384
|
+
component spec but breaks one of these is wrong.
|
|
385
|
+
|
|
386
|
+
1. **Two-second rule.** Every screen has one primary action, it is the only
|
|
387
|
+
filled button on the screen, and it sits top-right of the content panel.
|
|
388
|
+
A newcomer finds it in two seconds without reading.
|
|
389
|
+
2. **No page-name heading.** The active sidebar item already names the screen.
|
|
390
|
+
The top of the content panel is a *toolbar* — filters left, view controls
|
|
391
|
+
and the primary action right — not a title. (Detail screens name the
|
|
392
|
+
*record*, which is different: that title is content.)
|
|
393
|
+
3. **No over-data.** Show what the person came for and nothing they did not.
|
|
394
|
+
A list row carries at most: identity, one status signal, one "where it is",
|
|
395
|
+
one "who", one date. Everything else is one click away or in a tooltip.
|
|
396
|
+
4. **Tooltips, not descriptions.** Explanatory copy lives in `title=` and in
|
|
397
|
+
contact cards, never as a sentence under a control. Labels are two or three
|
|
398
|
+
words. A subtitle under a heading is a smell.
|
|
399
|
+
5. **Status is a shape and a colour.** Never colour alone (CVD, greyscale
|
|
400
|
+
print). Never a colour outside the six.
|
|
401
|
+
6. **Nothing overflows.** Single-line text truncates with an ellipsis and a
|
|
402
|
+
tooltip; multi-line clamps to two; long unbroken strings wrap inside their
|
|
403
|
+
cell; tables have a column plan. Audited at 1440 and 1180 — zero offenders.
|
|
404
|
+
7. **Rows align.** Titles reserve their lines; footers pin to the bottom; grids
|
|
405
|
+
use `items-start`. A ragged row reads as broken.
|
|
406
|
+
8. **One accent.** The brand colour appears on: the primary button, the active
|
|
407
|
+
nav item, the active chip's count box, focus rings, links. That is the list.
|
|
408
|
+
9. **Density over air.** 4/8 grid, 56px rows, 48px nav, 40px controls. White
|
|
409
|
+
space is spent on grouping, not on padding.
|
|
410
|
+
10. **Skeleton, not spinner.** A spinner only inside a control that is working.
|
|
411
|
+
11. **Motion is 150ms and purposeful.** Nothing pulses, nothing bounces,
|
|
412
|
+
nothing animates at the reader.
|
|
413
|
+
12. **People are hoverable.** Wherever a name or email appears, the contact
|
|
414
|
+
card is one hover away; pipelines show the email, not the name — it is
|
|
415
|
+
what people recognise from their mailbox.
|
|
416
|
+
13. **Dialogs fit the viewport.** Chrome fixed, body scrolls, a 4-level form
|
|
417
|
+
fits a laptop without scrolling.
|
|
418
|
+
14. **Destructive and decision actions are named** ("Reject", "Send back",
|
|
419
|
+
"Approve"), coloured by status, and on a detail screen gated behind having
|
|
420
|
+
read the thing being decided.
|
|
421
|
+
15. **Consistency beats local optimisation.** If a screen wants a component
|
|
422
|
+
the library does not have, the answer is usually an existing one used
|
|
423
|
+
plainly. Add to the library only when three screens need it.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 6. Fixes shipped, as rules (the audit trail behind the laws)
|
|
428
|
+
|
|
429
|
+
| Symptom | Rule that came out of it |
|
|
430
|
+
| --- | --- |
|
|
431
|
+
| Cards 3 wide at 1120px | 4 columns from 1100px; 24/48 gaps |
|
|
432
|
+
| Toolbar 9px above the first orb | 24px below toolbar + 28px grid top padding so an overhanging orb has 26px clear |
|
|
433
|
+
| Initials clipped in an avatar stack | 24px avatars, −4px overlap, 2px ring |
|
|
434
|
+
| Users table 480px wider than the screen | ≤6 columns, column plan, secondary facts on a second line, icon actions |
|
|
435
|
+
| "Awaiting approval" pill overflowing its column | Pill label "Pending"; the amber tone carries the meaning |
|
|
436
|
+
| BRD id truncated to "BRD-2026-…" | The column whose distinguishing digits are at the end never truncates |
|
|
437
|
+
| Chip tabs not recognisable as tabs | Segmented control for views; chips only for filters |
|
|
438
|
+
| Greeting removed from the header | Restored: 12 muted + 16/600 name, left of search |
|
|
439
|
+
| Sidebar/header looked like two boxes | One white shell; content panel with a 24px corner |
|
|
440
|
+
| Deep links bounced to /login in dev | Boot must ignore a duplicate-request rejection under StrictMode |
|
|
441
|
+
| Approve link acted on GET | Mutations happen on POST behind a confirm page (scanners prefetch GETs) |
|
|
442
|
+
| Long emails widening tables | `overflow-wrap: anywhere` on every cell |
|
|
443
|
+
| Pipeline editor needed scrolling | One line per level; labels once as column headings |
|
|
444
|
+
| Preview and route stacked | Route vertical in a 300px column; document takes the rest |
|
|
445
|
+
| Document cramped in a card | Click or "Full preview" opens it alone with an Exit control |
|
|
446
|
+
| SSO button with nothing happening on click | Spinner + "Redirecting to Microsoft…", reset on `pageshow` |
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## 7. Building it in another stack
|
|
451
|
+
|
|
452
|
+
1. Create the two token sets from §1 as CSS variables on `:root` and
|
|
453
|
+
`[data-theme='dark']`.
|
|
454
|
+
2. Configure the framework so **only** these scales exist: type 12/14/16/20/24/36,
|
|
455
|
+
weights 400/500/600/700, radii 8/12/16/full, shadows e1/e2/e3, the six
|
|
456
|
+
status roles and the surface/ink/line roles. Delete the defaults.
|
|
457
|
+
3. Build the shell (§3) first. Get the "L" right before any page.
|
|
458
|
+
4. Build components in this order: button, card, input, table, chips, tabs,
|
|
459
|
+
avatar, person card, notice, dialog, skeleton, empty state, stat tile, record
|
|
460
|
+
card, stepper. Each with its states from §4.
|
|
461
|
+
5. Only then compose screens, checking each against §5 and running an overflow
|
|
462
|
+
audit at 1440 and 1180.
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "indira-workspace-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"description": "Indira Workspace UI — the component library specified in docs/design-system.md, plus a CLI to install the indira-ui and indira-api Claude Code skills",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"component-library",
|
|
10
|
+
"design-system",
|
|
11
|
+
"tailwindcss",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"claude-skill"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"dist-lib",
|
|
17
|
+
"bin",
|
|
18
|
+
".claude/skills/indira-ui",
|
|
19
|
+
".claude/skills/indira-api",
|
|
20
|
+
"docs/design-system.md"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist-lib/indira-workspace-ui.cjs",
|
|
23
|
+
"module": "./dist-lib/indira-workspace-ui.mjs",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist-lib/indira-workspace-ui.mjs",
|
|
27
|
+
"require": "./dist-lib/indira-workspace-ui.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./style.css": "./dist-lib/style.css"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"indira-workspace-ui": "bin/cli.js"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"dev": "vite",
|
|
39
|
+
"build": "vite build",
|
|
40
|
+
"build:lib": "vite build --config vite.lib.config.js && node scripts/copy-style.mjs",
|
|
41
|
+
"prepublishOnly": "npm run build:lib",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"preview": "vite preview"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
47
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"lucide-react": "^1.38.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^10.0.1",
|
|
54
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
55
|
+
"@types/react": "^19.2.18",
|
|
56
|
+
"@types/react-dom": "^19.2.4",
|
|
57
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
58
|
+
"autoprefixer": "^10.5.4",
|
|
59
|
+
"eslint": "^10.9.0",
|
|
60
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
61
|
+
"eslint-plugin-react-refresh": "^0.5.4",
|
|
62
|
+
"globals": "^17.11.0",
|
|
63
|
+
"postcss": "^8.5.26",
|
|
64
|
+
"react": "^19.2.8",
|
|
65
|
+
"react-dom": "^19.2.8",
|
|
66
|
+
"vite": "^8.2.2"
|
|
67
|
+
}
|
|
68
|
+
}
|