indira-workspace-ui 0.1.0 → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: indira-api
3
- description: Scaffold and extend Node/Express backend services in the Indira API architecture — routes/controller/service/repository/model layering, validated per-environment config, PM2 process definitions, and Pino structured HTTP logging with an audit trail on every mutation. Ensures every backend app shares the same folder structure and constraints. Use for any new Express service, or any new module (patient, auth, appointment, audit, billing, ...) added to an existing one.
3
+ description: Scaffold and extend Node/Express backend services in the Indira API architecture — routes/controller/service/repository/model layering, validated per-environment config, PM2 process definitions, and Pino structured HTTP logging with an audit trail on every mutation. Includes a ready-made SSO-with-approval-gate auth pattern (env-configured super-admin email allowlist, new users pending until approved). Ensures every backend app shares the same folder structure and constraints. Use for any new Express service, any new module (patient, auth, appointment, audit, billing, ...) added to an existing one, or the backend half of a new full-stack app (pair with /indira-ui for the frontend).
4
4
  ---
5
5
 
6
6
  # indira-api
@@ -10,6 +10,12 @@ The full spec lives at `docs/api-architecture.md` in the target repository —
10
10
  read it completely before doing anything else; every path, filename and rule
11
11
  you need is in it, and this skill does not repeat them.
12
12
 
13
+ **Building a brand-new full-stack app?** Scaffold the backend first —
14
+ `auth` and `audit` before any domain module (§13 of the spec) — then hand
15
+ off to `/indira-ui` for the frontend. That skill designs the screens; it
16
+ doesn't invent the data they show, so the API should exist (even just
17
+ scaffolded and booting) before it starts.
18
+
13
19
  Your job has four parts, in order. Do not skip Part 0 or Part 1.
14
20
 
15
21
  ## Part 0 — Establish the spec (new repo only)
@@ -39,11 +45,15 @@ Do not scaffold from a one-line request. For a **new service**, confirm:
39
45
  specific frontend, another internal service, a cron, a webhook)?
40
46
  - **Database** — Postgres, MySQL, MongoDB, something else? Driver or ORM
41
47
  preference, or should it match another Indira service for consistency?
42
- - **First modules** — beyond the mandatory `auth` and `audit` (§12 of the
48
+ - **First modules** — beyond the mandatory `auth` and `audit` (§13 of the
43
49
  spec builds these first), which domain modules does the initial version
44
50
  need? Get the entity name and its core fields for each.
45
51
  - **Auth model** — session, JWT, SSO against an existing identity provider?
46
- Anything from an existing Indira service to reuse rather than reinvent?
52
+ If it's SSO with an approval gate (a new user is `pending` until a super
53
+ admin approves them; an env-configured email allowlist grants super-admin
54
+ on first login) that exact pattern is §12 of the reference spec — read it
55
+ before scaffolding `auth`, don't design it from scratch. Anything from an
56
+ existing Indira service to reuse rather than reinvent?
47
57
  - **Environments** — which of local/uat/production does this need on day
48
58
  one, and are there secrets beyond DB + mail that the env schema must
49
59
  declare?
@@ -119,7 +129,7 @@ If any answer is no, fix the plan before writing code.
119
129
  key the config schema declares has a placeholder.
120
130
  8. Write one integration test per new route under `tests/`, and unit tests
121
131
  for the service's business rules beside the module.
122
- 9. Verify (§12 of the spec): the server boots, `GET /api/health` returns
132
+ 9. Verify (§13 of the spec): the server boots, `GET /api/health` returns
123
133
  200, one request produces one structured Pino log line carrying a
124
134
  request id, and a deliberately-triggered error returns the `{ error:
125
135
  { code, message, requestId } }` shape with that same id.
@@ -311,10 +311,80 @@ gitignored `.env.*` files and in the box's process environment — never in
311
311
  the spec doesn't have, the answer is usually the existing pattern used
312
312
  plainly. Extend this document (and every app's copy of it) before
313
313
  inventing a one-off.
314
+ 9. **The super-admin allowlist lives in env, not the database.**
315
+ `SUPER_ADMIN_EMAILS` (§12.1) is read at boot and re-checked on every SSO
316
+ login — never exposed as an editable field, never granted through an API
317
+ call.
314
318
 
315
319
  ---
316
320
 
317
- ## 12. Building it for a new app
321
+ ## 12. SSO authentication with an approval gate
322
+
323
+ A pattern for services that authenticate against a corporate identity
324
+ provider (reference IdP: Microsoft Entra ID / Azure AD, via OIDC) instead of
325
+ issuing their own passwords, and that gate a brand-new user behind a human
326
+ approval before they can do anything.
327
+
328
+ ### 12.1 Config
329
+
330
+ `config/index.js`'s schema declares, alongside the existing keys:
331
+
332
+ ```
333
+ SSO_TENANT_ID required
334
+ SSO_CLIENT_ID required
335
+ SSO_CLIENT_SECRET required
336
+ SSO_REDIRECT_URI required
337
+ SUPER_ADMIN_EMAILS required, comma-separated, lower-cased and trimmed on read
338
+ ```
339
+
340
+ `SUPER_ADMIN_EMAILS` is the one allowlist that grants the `super_admin`
341
+ role. It is never stored in the database and never editable from the API —
342
+ the only way to change who is a super admin is to change this env var and
343
+ redeploy. That is deliberate: the people who can approve new accounts must
344
+ themselves be provisioned outside the thing they administer.
345
+
346
+ ### 12.2 Flow
347
+
348
+ 1. `GET /api/auth/sso/login` redirects to the IdP's OIDC authorize endpoint
349
+ (state + PKCE verifier stored server-side, keyed by a short-lived cookie).
350
+ 2. `GET /api/auth/sso/callback` exchanges the code, validates the id_token
351
+ (issuer, audience, signature, nonce), and extracts the verified email.
352
+ 3. Look the email up in `users`:
353
+ - **Not found, and email ∈ `SUPER_ADMIN_EMAILS`** — create the user with
354
+ `role: "super_admin"`, `status: "active"`, issue the session/JWT +
355
+ refresh token immediately.
356
+ - **Not found, and email ∉ `SUPER_ADMIN_EMAILS`** — create the user with
357
+ `status: "pending"`, `role: "member"`. Do **not** issue a session.
358
+ Respond `202 { data: { status: "pending" } }` — the client shows a
359
+ "waiting for approval" screen, not an error.
360
+ - **Found, `status: "active"`** — issue session/JWT + refresh token as
361
+ normal. If email ∈ `SUPER_ADMIN_EMAILS` and the stored role isn't
362
+ `super_admin` yet, elevate it on this login too — the allowlist is
363
+ always the source of truth for who is a super admin, even for rows
364
+ created before an email was added to it.
365
+ - **Found, `status: "pending"`** — respond `202` again, same as a brand
366
+ new pending user. No session.
367
+ - **Found, `status: "rejected"`** — `403`. A rejected user is not
368
+ silently re-queued by logging in again.
369
+ 4. A `super_admin` calls `GET /api/users?status=pending` to see the queue,
370
+ and `POST /api/users/:id/approve` or `POST /api/users/:id/reject` to
371
+ resolve one. Approve flips `status` to `active` (the user's *next* SSO
372
+ login then issues a session); reject flips it to `rejected`. Both routes
373
+ sit behind `auth("user:approve")`, restricted to `super_admin`, and both
374
+ write to the audit trail (actor, the user approved/rejected, before/after
375
+ status) — this is a mutation like any other, law 5 applies.
376
+
377
+ ### 12.3 What this buys, and what it deliberately doesn't
378
+
379
+ This is a gate, not a role system — every approved user is a plain member
380
+ until something else grants them more. Fine-grained permissions beyond
381
+ "super admin vs. everyone else" are a separate module's concern (or a later
382
+ addendum to this one), not something to bolt onto `auth` because it's
383
+ already open.
384
+
385
+ ---
386
+
387
+ ## 13. Building it for a new app
318
388
 
319
389
  1. Scaffold the tree in §1 exactly. Nothing above `modules/*` beyond what's
320
390
  listed.
@@ -1,16 +1,46 @@
1
1
  ---
2
2
  name: indira-ui
3
- description: Design and build screens in the Indira Workspace UI language. Interviews the developer or business team first (who uses it, what they come to do, what must be found in two seconds), turns the answers into a component hierarchy that shows no more data than the task needs, then builds it from the design-system spec. Use for any new page, dashboard, form, list, or internal tool that should look and behave like the BRD Governance Tool.
3
+ description: Design and build screens from this repo's design-system spec(s) — Indira Workspace UI (light, the default) and, where the repo has it, Vault UI (dark, for custody/treasury/compliance-grade data) — asking which composition to use when more than one is available. Interviews the developer or business team first (who uses it, what they come to do, what must be found in two seconds), turns the answers into a component hierarchy that shows no more data than the task needs, then builds it from the chosen spec. Use for any new page, dashboard, form, list, or internal tool; migrating an existing screen's UI to this design language; or the frontend half of a new full-stack app (pair with /indira-api for the backend).
4
4
  ---
5
5
 
6
6
  # indira-ui
7
7
 
8
- You are a senior product designer who builds. You work in the **Indira
9
- Workspace UI** language, specified in `docs/design-system.md` in this
10
- repository. Read that file completely before doing anything else; every
11
- number and rule you need is in it, and this skill does not repeat them.
12
-
13
- Your job has three parts, in order, and you do not skip the first one.
8
+ You are a senior product designer who builds. A repo can define more than
9
+ one design composition which one you build in is decided in Part 0, below.
10
+ Whichever one is chosen, read its spec file completely before doing anything
11
+ else; every number and rule you need is in that one file, and this skill
12
+ does not repeat them.
13
+
14
+ Your job has four parts, in order, and you do not skip Part 0 or Part 1.
15
+
16
+ ## Part 0 — Pick the design composition
17
+
18
+ Check the target repo for `docs/design-system*.md`.
19
+
20
+ - **Exactly one file** — that is the composition. Read it completely and go
21
+ to Part 1; don't make the developer confirm a choice they don't have.
22
+ - **More than one file** — read enough of each (title + opening paragraph)
23
+ to describe it in a sentence, then ask the developer which this screen
24
+ should use before doing anything else. As of this writing, a repo may
25
+ offer:
26
+ - **Indira Workspace UI** (`docs/design-system.md`) — light, one rose
27
+ accent, soft rounded shapes. The default: governance/workflow tools,
28
+ anything that should look like the BRD Governance Tool.
29
+ - **Vault UI** (`docs/design-system-vault.md`) — dark by default, one
30
+ desaturated accent, dense tabular layout. Custody/treasury/compliance-
31
+ grade screens where the data itself carries the seriousness and a
32
+ light, friendly surface would undersell that.
33
+
34
+ List whatever compositions the repo actually has, not just these two —
35
+ more may exist by the time you read this. Wait for an explicit answer.
36
+ If the developer has no preference, recommend Indira Workspace UI and say
37
+ why (it's this product's established look); switch only on request.
38
+ - **No file** — fresh project. Fall back to building Indira Workspace UI
39
+ from this skill's own copy of the spec (§7), same as if this part didn't
40
+ exist.
41
+
42
+ Never mix components from two compositions on one screen — each spec's own
43
+ closing section says so, and it's as true across systems as within one.
14
44
 
15
45
  ## Part 1 — Grill before you draw
16
46
 
@@ -20,6 +50,29 @@ not one at a time, and stop as soon as the answers are clear — this is a
20
50
  discovery, not a form. Use the question tool when one is available; otherwise
21
51
  ask in prose and wait.
22
52
 
53
+ **Migrating an existing screen, not building a new one?** Same three parts,
54
+ but the brief opens differently — add these before "Who":
55
+
56
+ - **What's there today** — describe or screenshot the current screen. What
57
+ do people actually use vs. ignore? (Ask directly — the ignored parts are
58
+ what you leave out, exactly as with a fresh build.)
59
+ - **What must not change** — which behaviours, data, or integrations are
60
+ load-bearing and must survive untouched? A migration rebuilds the visual
61
+ and structural layer; it is not licence to rewrite what the screen does.
62
+ - **What's the current stack** — plain CSS, Bootstrap, MUI, styled-
63
+ components? None of its styling survives the swap; what you're checking
64
+ for is which existing data-fetching/behaviour logic can stay as-is
65
+ underneath the new markup.
66
+
67
+ Migrate one screen at a time. "Migrate the whole app" is always several
68
+ screens — say so, and ask which one first. An old screen and a newly
69
+ migrated one looking different in the same nav is normal mid-migration, not
70
+ a bug to fix immediately.
71
+
72
+ **Building a brand-new full-stack app?** If there's no backend yet, run
73
+ `/indira-api` first (it scaffolds `auth` and `audit` before any domain
74
+ module) — this skill designs screens, it doesn't invent the data they show.
75
+
23
76
  **Who**
24
77
  - Who opens this screen, by role? How often — several times a day, once a
25
78
  week, once a quarter?
@@ -130,20 +183,25 @@ Build from the spec, not from memory of other design systems.
130
183
 
131
184
  ## What "done" looks like
132
185
 
133
- Give back, in this order: the confirmed brief, the hierarchy tree, the
134
- screens built, the audit result, and a two-line note of any place the request
135
- was pushed back on and what was decided. If a requested element was left out
136
- for a law, name the law the requester should be able to overrule it
137
- knowingly, not discover it missing.
186
+ Give back, in this order: which design composition was used (and, if more
187
+ than one was on offer, that the developer chose it), the confirmed brief,
188
+ the hierarchy tree, the screens built, the audit result, and a two-line note
189
+ of any place the request was pushed back on and what was decided. If a
190
+ requested element was left out for a law, name the law — the requester
191
+ should be able to overrule it knowingly, not discover it missing.
138
192
 
139
193
  ## Things you never do
140
194
 
195
+ - Assume the design composition when the repo defines more than one — Part
196
+ 0 is not optional just because you're confident which one fits.
141
197
  - Add a page-name heading, a hero, a gradient, an emoji icon, or a second
142
198
  filled button.
143
199
  - Write a sentence of help text under a control.
144
200
  - Show an id, a status word and a status colour for the same record in the
145
201
  same place.
146
- - Invent a colour for a new status. Six statuses; a seventh is a product
147
- decision, not a design one.
202
+ - Invent a colour for a new status beyond what the chosen composition's own
203
+ spec defines — a new one is a product decision, not a per-screen one.
148
204
  - Use a spinner for page content, or animate anything on a loop.
149
205
  - Design a screen for a user you have not asked about.
206
+ - Reach for a second composition's component inside a screen built in the
207
+ first one.
package/README.md CHANGED
@@ -4,6 +4,16 @@ The component library specified in [docs/design-system.md](docs/design-system.md
4
4
  React 19 + Tailwind CSS 4 + Lucide. Published to npm as `indira-workspace-ui`, and it doubles
5
5
  as an installer for the `indira-ui` and `indira-api` Claude Code skills.
6
6
 
7
+ This repo also holds a second, deliberately separate composition — **Vault UI**
8
+ ([docs/design-system-vault.md](docs/design-system-vault.md), components in
9
+ [src/vault/](src/vault/)) — dark, one accent, dense tabular layout, for
10
+ custody/treasury/compliance-grade screens where a light, friendly surface
11
+ would undersell the data. See it live at the "Vault UI" nav item in the demo
12
+ workspace. The `/indira-ui` skill now asks which composition to use before
13
+ building a screen whenever a repo has more than one. Vault UI ships from its
14
+ own subpath — `indira-workspace-ui/vault` — so installing the main package
15
+ never pulls its dark theme in by accident.
16
+
7
17
  ## Install in a consuming project
8
18
 
9
19
  ```
@@ -35,6 +45,24 @@ Add a `@source` pointing at the installed package next to your own `@import "tai
35
45
  - One filled button per view, top-right (law 1). Status is always a glyph and a colour (law 5).
36
46
  - Theme: `useTheme()` sets `data-theme` on the root; the default follows `prefers-color-scheme`.
37
47
 
48
+ ### Vault UI
49
+
50
+ ```jsx
51
+ import { VaultShell, VaultButton, VaultTable, VaultStatusPill } from "indira-workspace-ui/vault";
52
+ import "indira-workspace-ui/vault/style.css";
53
+ ```
54
+
55
+ Same `@source` requirement as the main system, pointed at the same `dist-lib`:
56
+
57
+ ```css
58
+ @import "indira-workspace-ui/vault/style.css";
59
+ @source "../node_modules/indira-workspace-ui/dist-lib";
60
+ ```
61
+
62
+ Never import both `style.css` and `vault/style.css` into the same screen's markup — see
63
+ [docs/design-system-vault.md](docs/design-system-vault.md) §6. A page is one composition or the
64
+ other.
65
+
38
66
  ## Scales
39
67
 
40
68
  The only values that compile: text `xs sm base lg xl 2xl`, weights `normal medium semibold bold`,
@@ -55,6 +83,7 @@ npm run build:lib # builds the publishable package into dist-lib/
55
83
  | `src/components/` | One file per component; `index.js` is the barrel and the library's entry point. |
56
84
  | `src/screens/` | The demo workspace: Dashboard, BRD list, BRD detail, a library gallery per component group, and the dev-skills docs screen. |
57
85
  | `src/data/` | Demo directory and BRD records (demo app only — not part of the published package). |
86
+ | `src/vault/` | Vault UI — the second design composition. Its own tokens/components, scoped under `.vault`, never mixed with `src/components/`. Ships from its own `dist-lib/vault.{mjs,cjs}` + `dist-lib/vault.css`, exported as the package's `./vault` subpath. |
58
87
  | `.claude/skills/indira-ui`, `.claude/skills/indira-api` | The two Claude Code skills this package installs into consumer projects. |
59
88
  | `bin/cli.js` | The `indira-workspace-ui add-skills` CLI. |
60
89
  | `vite.lib.config.js` | Library build config — components only, externalizes `react`/`react-dom`/`lucide-react`. |
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react/jsx-runtime"),t=require("lucide-react");function n({name:t=``,photo:n,size:r=32,className:i=``}){let a=t.trim().split(/\s+/).slice(0,2).map(e=>e[0]).join(``).toUpperCase();return(0,e.jsx)(`span`,{className:`vault-avatar ${i}`,style:{width:r,height:r,fontSize:Math.round(r*.4)},children:n?(0,e.jsx)(`img`,{src:n,alt:``}):a})}function r({brand:r,nav:a=[],activeId:o,onNavigate:s,footNav:c=[],user:l,className:u=``,children:d}){return(0,e.jsxs)(`div`,{className:`vault-shell ${u}`,children:[(0,e.jsxs)(`aside`,{className:`vault-shell__sidebar`,children:[(0,e.jsxs)(`div`,{className:`vault-shell__brand`,children:[(0,e.jsx)(`span`,{className:`vault-shell__brand-mark`,children:r?.mark??`V`}),(0,e.jsx)(`span`,{className:`vault-shell__brand-name`,children:r?.name})]}),(0,e.jsx)(`nav`,{className:`vault-shell__nav`,children:a.map(t=>(0,e.jsx)(i,{item:t,active:t.id===o,onClick:()=>s?.(t.id)},t.id))}),c.length>0&&(0,e.jsxs)(`div`,{className:`vault-shell__foot`,children:[(0,e.jsx)(`div`,{className:`vault-shell__foot-label`,children:`Support`}),c.map(t=>(0,e.jsx)(i,{item:t,active:!1,onClick:()=>s?.(t.id)},t.id))]})]}),(0,e.jsxs)(`div`,{className:`vault-shell__main`,children:[(0,e.jsxs)(`header`,{className:`vault-shell__topbar`,children:[(0,e.jsx)(`button`,{type:`button`,className:`vault-topbar__icon`,title:`Notifications`,children:(0,e.jsx)(t.Bell,{size:17,"aria-hidden":`true`})}),l&&(0,e.jsxs)(`button`,{type:`button`,className:`vault-btn vault-user`,style:{padding:0,background:`none`},title:l.email,children:[(0,e.jsx)(n,{name:l.name,photo:l.photo}),(0,e.jsxs)(`span`,{className:`min-w-0`,style:{textAlign:`left`},children:[(0,e.jsx)(`span`,{className:`vault-user__name block`,children:l.name}),(0,e.jsx)(`span`,{className:`vault-user__role block`,children:l.role})]}),(0,e.jsx)(t.ChevronDown,{size:14,"aria-hidden":`true`,style:{color:`rgb(var(--v-ink-faint))`}})]})]}),(0,e.jsx)(`div`,{className:`vault-shell__content`,children:d})]})]})}function i({item:t,active:n,onClick:r}){let i=t.icon;return(0,e.jsxs)(`button`,{type:`button`,className:`vault-nav-item`,"aria-current":n?`page`:void 0,onClick:r,title:t.label,children:[i&&(0,e.jsx)(i,{size:17,"aria-hidden":`true`}),(0,e.jsx)(`span`,{className:`truncate`,children:t.label})]})}function a({tabs:t=[],value:n,onChange:r,className:i=``}){return(0,e.jsx)(`div`,{className:`vault-tabs ${i}`,role:`tablist`,children:t.map(t=>(0,e.jsx)(`button`,{type:`button`,role:`tab`,className:`vault-tab`,"aria-selected":t.value===n,onClick:()=>r?.(t.value),children:t.label},t.value))})}function o({onBack:n,title:r,tabs:i,value:o,onTabChange:s,figure:c,figureSub:l,pill:u}){return(0,e.jsxs)(e.Fragment,{children:[n&&(0,e.jsxs)(`button`,{type:`button`,className:`vault-back`,onClick:n,children:[(0,e.jsx)(t.ChevronLeft,{size:14,"aria-hidden":`true`}),`Back`]}),(0,e.jsxs)(`div`,{className:`vault-header`,children:[(0,e.jsxs)(`div`,{children:[(0,e.jsx)(`h1`,{className:`vault-header__title`,children:r}),i&&(0,e.jsx)(a,{tabs:i,value:o,onChange:s})]}),(c||u)&&(0,e.jsxs)(`div`,{children:[c&&(0,e.jsx)(`div`,{className:`vault-header__figure`,children:c}),l&&(0,e.jsx)(`div`,{className:`vault-header__sub`,children:l}),u&&(0,e.jsx)(`div`,{className:`vault-header__pill-row`,children:u})]})]})]})}function s({tone:t=`accent`,children:n,className:r=``}){return(0,e.jsx)(`span`,{className:`vault-pill vault-pill--${t} ${r}`,children:n})}function c({figure:t,label:n,tone:r,className:i=``}){let a=r===`accent`?`rgb(var(--v-accent))`:r===`warn`?`rgb(var(--v-warn))`:r===`danger`?`rgb(var(--v-danger))`:void 0;return(0,e.jsxs)(`div`,{className:`vault-stat ${i}`,style:a?{"--tone":a}:void 0,children:[(0,e.jsx)(`span`,{className:`vault-stat__figure`,children:t}),(0,e.jsx)(`span`,{className:`vault-stat__label`,children:n})]})}function l({variant:t=`primary`,icon:n,className:r=``,children:i,...a}){return(0,e.jsxs)(`button`,{type:`button`,className:`vault-btn vault-btn--${t} ${r}`,...a,children:[n&&(0,e.jsx)(n,{size:t===`icon`?18:15,"aria-hidden":`true`}),t!==`icon`&&i]})}function u({checked:n=!1,onChange:r,label:i,className:a=``}){return(0,e.jsxs)(`label`,{className:`vault-check-row ${a}`,style:{cursor:`pointer`},children:[(0,e.jsx)(`button`,{type:`button`,role:`checkbox`,"aria-checked":n,className:`vault-checkbox`,onClick:()=>r?.(!n),children:n&&(0,e.jsx)(t.Check,{size:12,"aria-hidden":`true`})}),i&&(0,e.jsx)(`span`,{className:`vault-check-row__label`,children:i})]})}function d({clearable:n=!1,onClear:r,className:i=``,...a}){return n?(0,e.jsxs)(`span`,{className:`vault-inp-wrap`,children:[(0,e.jsx)(`input`,{className:`vault-inp ${i}`,...a}),(0,e.jsx)(`button`,{type:`button`,onClick:r,title:`Clear`,"aria-label":`Clear`,children:(0,e.jsx)(t.X,{size:14,"aria-hidden":`true`})})]}):(0,e.jsx)(`input`,{className:`vault-inp ${i}`,...a})}function f({className:t=``,children:n,...r}){return(0,e.jsx)(`select`,{className:`vault-select ${t}`,...r,children:n})}function p({columns:t=[],rows:n=[],getRowId:r=(e,t)=>e.id??t,className:i=``}){return(0,e.jsx)(`div`,{className:`vault-table-wrap`,children:(0,e.jsxs)(`table`,{className:`vault-table ${i}`,children:[(0,e.jsx)(`thead`,{children:(0,e.jsx)(`tr`,{children:t.map(t=>(0,e.jsx)(`th`,{style:t.align===`right`?{textAlign:`right`}:void 0,children:t.label},t.key))})}),(0,e.jsx)(`tbody`,{children:n.map((n,i)=>(0,e.jsx)(`tr`,{children:t.map(t=>(0,e.jsx)(`td`,{style:t.align===`right`?{textAlign:`right`}:void 0,children:t.render?t.render(n):n[t.key]},t.key))},r(n,i)))})]})})}exports.VaultAvatar=n,exports.VaultButton=l,exports.VaultCheckbox=u,exports.VaultInput=d,exports.VaultPageHeader=o,exports.VaultSelect=f,exports.VaultShell=r,exports.VaultStatTile=c,exports.VaultStatusPill=s,exports.VaultTable=p,exports.VaultTabs=a;
2
+ //# sourceMappingURL=vault.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.cjs","names":[],"sources":["../src/vault/VaultAvatar.jsx","../src/vault/VaultShell.jsx","../src/vault/VaultPageHeader.jsx","../src/vault/VaultStatusPill.jsx","../src/vault/VaultStatTile.jsx","../src/vault/VaultControls.jsx","../src/vault/VaultTable.jsx"],"sourcesContent":["/** Circular avatar restyled for a dark ground — see VaultShell for usage. */\nexport default function VaultAvatar({ name = \"\", photo, size = 32, className = \"\" }) {\n const initials = name\n .trim()\n .split(/\\s+/)\n .slice(0, 2)\n .map((p) => p[0])\n .join(\"\")\n .toUpperCase();\n return (\n <span className={`vault-avatar ${className}`} style={{ width: size, height: size, fontSize: Math.round(size * 0.4) }}>\n {photo ? <img src={photo} alt=\"\" /> : initials}\n </span>\n );\n}\n","import { Bell, ChevronDown } from \"lucide-react\";\nimport VaultAvatar from \"./VaultAvatar\";\n\n/**\n * VaultShell — the sidebar + top bar chrome for Vault UI\n * (docs/design-system-vault.md §3). Dark flows continuously: sidebar,\n * top bar and content are all --v-canvas, no lighter sidebar surface.\n *\n * <VaultShell nav={NAV} activeId={id} onNavigate={setId} brand={{...}} user={{...}}>\n * {content}\n * </VaultShell>\n */\nexport default function VaultShell({ brand, nav = [], activeId, onNavigate, footNav = [], user, className = \"\", children }) {\n return (\n <div className={`vault-shell ${className}`}>\n <aside className=\"vault-shell__sidebar\">\n <div className=\"vault-shell__brand\">\n <span className=\"vault-shell__brand-mark\">{brand?.mark ?? \"V\"}</span>\n <span className=\"vault-shell__brand-name\">{brand?.name}</span>\n </div>\n <nav className=\"vault-shell__nav\">\n {nav.map((item) => (\n <VaultNavItem key={item.id} item={item} active={item.id === activeId} onClick={() => onNavigate?.(item.id)} />\n ))}\n </nav>\n {footNav.length > 0 && (\n <div className=\"vault-shell__foot\">\n <div className=\"vault-shell__foot-label\">Support</div>\n {footNav.map((item) => (\n <VaultNavItem key={item.id} item={item} active={false} onClick={() => onNavigate?.(item.id)} />\n ))}\n </div>\n )}\n </aside>\n <div className=\"vault-shell__main\">\n <header className=\"vault-shell__topbar\">\n <button type=\"button\" className=\"vault-topbar__icon\" title=\"Notifications\">\n <Bell size={17} aria-hidden=\"true\" />\n </button>\n {user && (\n <button type=\"button\" className=\"vault-btn vault-user\" style={{ padding: 0, background: \"none\" }} title={user.email}>\n <VaultAvatar name={user.name} photo={user.photo} />\n <span className=\"min-w-0\" style={{ textAlign: \"left\" }}>\n <span className=\"vault-user__name block\">{user.name}</span>\n <span className=\"vault-user__role block\">{user.role}</span>\n </span>\n <ChevronDown size={14} aria-hidden=\"true\" style={{ color: \"rgb(var(--v-ink-faint))\" }} />\n </button>\n )}\n </header>\n <div className=\"vault-shell__content\">{children}</div>\n </div>\n </div>\n );\n}\n\nfunction VaultNavItem({ item, active, onClick }) {\n const Icon = item.icon;\n return (\n <button type=\"button\" className=\"vault-nav-item\" aria-current={active ? \"page\" : undefined} onClick={onClick} title={item.label}>\n {Icon && <Icon size={17} aria-hidden=\"true\" />}\n <span className=\"truncate\">{item.label}</span>\n </button>\n );\n}\n","import { ChevronLeft } from \"lucide-react\";\n\n/** Underline tabs (§4) — a different composition earns a different tab shape than the main system's segmented track. */\nexport function VaultTabs({ tabs = [], value, onChange, className = \"\" }) {\n return (\n <div className={`vault-tabs ${className}`} role=\"tablist\">\n {tabs.map((t) => (\n <button\n key={t.value}\n type=\"button\"\n role=\"tab\"\n className=\"vault-tab\"\n aria-selected={t.value === value}\n onClick={() => onChange?.(t.value)}\n >\n {t.label}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * VaultPageHeader — back control, bold title, underline tabs, and a\n * right-hand slot for the one figure that matters most on this page plus\n * its status pill (docs/design-system-vault.md §3).\n *\n * <VaultPageHeader onBack={...} title=\"Marketing\" tabs={...} value={tab} onTabChange={setTab}\n * figure=\"2,345.6789 BTC\" figureSub=\"≈59,339,969.79 CNY\" pill={<VaultStatusPill tone=\"warn\">Effective after 23:59:59</VaultStatusPill>} />\n */\nexport default function VaultPageHeader({ onBack, title, tabs, value, onTabChange, figure, figureSub, pill }) {\n return (\n <>\n {onBack && (\n <button type=\"button\" className=\"vault-back\" onClick={onBack}>\n <ChevronLeft size={14} aria-hidden=\"true\" />\n Back\n </button>\n )}\n <div className=\"vault-header\">\n <div>\n <h1 className=\"vault-header__title\">{title}</h1>\n {tabs && <VaultTabs tabs={tabs} value={value} onChange={onTabChange} />}\n </div>\n {(figure || pill) && (\n <div>\n {figure && <div className=\"vault-header__figure\">{figure}</div>}\n {figureSub && <div className=\"vault-header__sub\">{figureSub}</div>}\n {pill && <div className=\"vault-header__pill-row\">{pill}</div>}\n </div>\n )}\n </div>\n </>\n );\n}\n","/**\n * VaultStatusPill — solid fill, one of the three semantic colours\n * (§1, §4). Sparingly: the record's one current state, never decoration.\n *\n * <VaultStatusPill tone=\"warn\">Effective after 23:59:59</VaultStatusPill>\n */\nexport default function VaultStatusPill({ tone = \"accent\", children, className = \"\" }) {\n return <span className={`vault-pill vault-pill--${tone} ${className}`}>{children}</span>;\n}\n","/**\n * VaultStatTile — a dense KPI figure. Colour only on the left edge, and\n * only when `tone` is set — most tiles stay neutral ink (§5 law 1: colour\n * reserved for state that actually matters, never decoration).\n *\n * <VaultStatTile figure={3} label=\"Approved\" tone=\"accent\" />\n */\nexport default function VaultStatTile({ figure, label, tone, className = \"\" }) {\n const toneVar = tone === \"accent\" ? \"rgb(var(--v-accent))\" : tone === \"warn\" ? \"rgb(var(--v-warn))\" : tone === \"danger\" ? \"rgb(var(--v-danger))\" : undefined;\n return (\n <div className={`vault-stat ${className}`} style={toneVar ? { \"--tone\": toneVar } : undefined}>\n <span className=\"vault-stat__figure\">{figure}</span>\n <span className=\"vault-stat__label\">{label}</span>\n </div>\n );\n}\n","import { Check, X } from \"lucide-react\";\n\n/**\n * Vault UI controls (§4): Button, Checkbox, Input, Select. Dark fields,\n * 8px radius, accent border + ring on focus.\n */\nexport function VaultButton({ variant = \"primary\", icon: Icon, className = \"\", children, ...rest }) {\n return (\n <button type=\"button\" className={`vault-btn vault-btn--${variant} ${className}`} {...rest}>\n {Icon && <Icon size={variant === \"icon\" ? 18 : 15} aria-hidden=\"true\" />}\n {variant !== \"icon\" && children}\n </button>\n );\n}\n\n/** `checked` + `onChange` — a plain toggle, not the main system's native checkbox skin. */\nexport function VaultCheckbox({ checked = false, onChange, label, className = \"\" }) {\n return (\n <label className={`vault-check-row ${className}`} style={{ cursor: \"pointer\" }}>\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={checked}\n className=\"vault-checkbox\"\n onClick={() => onChange?.(!checked)}\n >\n {checked && <Check size={12} aria-hidden=\"true\" />}\n </button>\n {label && <span className=\"vault-check-row__label\">{label}</span>}\n </label>\n );\n}\n\nexport function VaultInput({ clearable = false, onClear, className = \"\", ...rest }) {\n if (!clearable) return <input className={`vault-inp ${className}`} {...rest} />;\n return (\n <span className=\"vault-inp-wrap\">\n <input className={`vault-inp ${className}`} {...rest} />\n <button type=\"button\" onClick={onClear} title=\"Clear\" aria-label=\"Clear\">\n <X size={14} aria-hidden=\"true\" />\n </button>\n </span>\n );\n}\n\nexport function VaultSelect({ className = \"\", children, ...rest }) {\n return (\n <select className={`vault-select ${className}`} {...rest}>\n {children}\n </select>\n );\n}\n","/**\n * VaultTable (§4) — dense, muted sentence-case headers (not the main\n * system's uppercase-tracked style), hairline row dividers, no zebra.\n *\n * Column: { key, label, render?(row), align? }\n */\nexport default function VaultTable({ columns = [], rows = [], getRowId = (row, i) => row.id ?? i, className = \"\" }) {\n return (\n <div className=\"vault-table-wrap\">\n <table className={`vault-table ${className}`}>\n <thead>\n <tr>\n {columns.map((c) => (\n <th key={c.key} style={c.align === \"right\" ? { textAlign: \"right\" } : undefined}>\n {c.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, i) => (\n <tr key={getRowId(row, i)}>\n {columns.map((c) => (\n <td key={c.key} style={c.align === \"right\" ? { textAlign: \"right\" } : undefined}>\n {c.render ? c.render(row) : row[c.key]}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ /*
2
+ * Vault UI — a second design composition (docs/design-system-vault.md).
3
+ * Everything scoped under .vault so it never reaches an Indira Workspace UI
4
+ * screen sharing the same page, and never needs edits to src/index.css.
5
+ */
6
+
7
+ .vault {
8
+ --v-canvas: 13 16 22;
9
+ --v-surface: 20 24 32;
10
+ --v-surface-2: 27 32 42;
11
+ --v-line: 38 44 56;
12
+ --v-ink: 237 239 243;
13
+ --v-ink-muted: 138 145 158;
14
+ --v-ink-faint: 86 93 108;
15
+ --v-accent: 52 201 142;
16
+ --v-accent-on: 8 20 16;
17
+ --v-warn: 224 138 62;
18
+ --v-danger: 224 96 122;
19
+ --v-dur: 150ms;
20
+
21
+ background: rgb(var(--v-canvas));
22
+ color: rgb(var(--v-ink));
23
+ font-variant-numeric: tabular-nums;
24
+ border-radius: 20px;
25
+ overflow: hidden;
26
+ }
27
+
28
+ /* ---- presentation frame for the catalog screen only: a floating window
29
+ on a near-black backdrop, echoing how this composition is usually shown ---- */
30
+ .vault-frame-backdrop { background: #05070c; border-radius: 24px; padding: 32px; }
31
+ .vault-frame { max-width: 1040px; margin: 0 auto; box-shadow: 0 30px 60px rgb(0 0 0 / 0.45); }
32
+
33
+ /* ---- shell: sidebar + top bar ---- */
34
+ .vault-shell { display: flex; min-height: 100%; }
35
+ .vault-shell__sidebar { width: 220px; flex-shrink: 0; display: flex; flex-direction: column; padding: 20px 12px; }
36
+ .vault-shell__brand { display: flex; align-items: center; gap: 10px; padding: 0 8px; margin-bottom: 28px; }
37
+ .vault-shell__brand-mark { width: 28px; height: 28px; border-radius: 8px; background: rgb(var(--v-accent) / 0.16); color: rgb(var(--v-accent)); display: grid; place-items: center; font-weight: 700; font-size: 13px; }
38
+ .vault-shell__brand-name { font-size: 13px; font-weight: 700; letter-spacing: 0.02em; }
39
+ .vault-shell__nav { display: flex; flex-direction: column; gap: 2px; }
40
+ .vault-nav-item {
41
+ display: flex; align-items: center; gap: 10px; height: 38px; padding: 0 10px; border-radius: 8px;
42
+ font-size: 13px; font-weight: 500; color: rgb(var(--v-ink-muted)); background: transparent; border: 0; text-align: left;
43
+ transition: background-color var(--v-dur) ease, color var(--v-dur) ease;
44
+ }
45
+ .vault-nav-item:hover { background: rgb(var(--v-surface)); color: rgb(var(--v-ink)); }
46
+ .vault-nav-item[aria-current="page"] { background: rgb(var(--v-surface)); color: rgb(var(--v-ink)); }
47
+ .vault-nav-item[aria-current="page"] svg { color: rgb(var(--v-accent)); }
48
+ .vault-shell__foot { margin-top: auto; display: flex; flex-direction: column; gap: 2px; padding-top: 12px; }
49
+ .vault-shell__foot-label { font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: rgb(var(--v-ink-faint)); padding: 0 10px; margin-bottom: 6px; }
50
+
51
+ .vault-shell__main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
52
+ .vault-shell__topbar { height: 56px; flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; gap: 16px; padding: 0 24px; }
53
+ .vault-topbar__icon { width: 32px; height: 32px; border-radius: 9999px; display: grid; place-items: center; color: rgb(var(--v-ink-muted)); background: transparent; border: 0; }
54
+ .vault-topbar__icon:hover { background: rgb(var(--v-surface)); color: rgb(var(--v-ink)); }
55
+ .vault-user { display: flex; align-items: center; gap: 10px; }
56
+ .vault-user__name { font-size: 13px; font-weight: 600; line-height: 16px; }
57
+ .vault-user__role { font-size: 11px; color: rgb(var(--v-ink-muted)); line-height: 14px; }
58
+
59
+ .vault-shell__content { flex: 1; min-width: 0; padding: 8px 28px 28px; overflow-y: auto; }
60
+
61
+ /* ---- avatar ---- */
62
+ .vault-avatar { width: 32px; height: 32px; border-radius: 9999px; display: grid; place-items: center; overflow: hidden; flex-shrink: 0; font-size: 12px; font-weight: 700; color: rgb(var(--v-ink)); background: rgb(var(--v-surface-2)); }
63
+ .vault-avatar > img { width: 100%; height: 100%; object-fit: cover; }
64
+
65
+ /* ---- page header ---- */
66
+ .vault-back {
67
+ display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; margin-bottom: 20px;
68
+ border-radius: 9999px; border: 1px solid rgb(var(--v-line)); background: transparent; color: rgb(var(--v-ink-muted));
69
+ font-size: 12px; font-weight: 500; transition: border-color var(--v-dur) ease, color var(--v-dur) ease;
70
+ }
71
+ .vault-back:hover { color: rgb(var(--v-ink)); border-color: rgb(var(--v-ink-faint)); }
72
+ .vault-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; flex-wrap: wrap; margin-bottom: 24px; }
73
+ .vault-header__title { font-size: 20px; font-weight: 700; margin: 0 0 16px; }
74
+ .vault-header__figure { font-size: 20px; font-weight: 700; text-align: right; }
75
+ .vault-header__sub { font-size: 12px; color: rgb(var(--v-warn)); text-align: right; margin-top: 2px; }
76
+ .vault-header__pill-row { display: flex; justify-content: flex-end; margin-top: 8px; }
77
+
78
+ /* ---- tabs: underline, not the main system's segmented track ---- */
79
+ .vault-tabs { display: flex; align-items: center; gap: 24px; }
80
+ .vault-tab {
81
+ position: relative; background: none; border: 0; padding: 0 0 12px; font-size: 14px; font-weight: 500;
82
+ color: rgb(var(--v-ink-muted)); transition: color var(--v-dur) ease;
83
+ }
84
+ .vault-tab:hover { color: rgb(var(--v-ink)); }
85
+ .vault-tab[aria-selected="true"] { color: rgb(var(--v-ink)); font-weight: 700; }
86
+ .vault-tab[aria-selected="true"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgb(var(--v-accent)); border-radius: 2px; }
87
+
88
+ /* ---- section ---- */
89
+ .vault-section { margin-bottom: 28px; }
90
+ .vault-section__head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
91
+ .vault-section__title { font-size: 15px; font-weight: 700; margin: 0; }
92
+ .vault-section__hint { font-size: 12px; color: rgb(var(--v-ink-muted)); margin: 4px 0 0; }
93
+ .vault-section__hint a { color: rgb(var(--v-accent)); text-decoration: underline; text-underline-offset: 2px; }
94
+
95
+ /* ---- checkbox row ---- */
96
+ .vault-check-row { display: flex; align-items: center; gap: 10px; }
97
+ .vault-checkbox {
98
+ width: 18px; height: 18px; border-radius: 5px; border: 1px solid rgb(var(--v-line)); background: rgb(var(--v-surface));
99
+ display: inline-grid; place-items: center; flex-shrink: 0; padding: 0; color: rgb(var(--v-accent-on));
100
+ transition: background-color var(--v-dur) ease, border-color var(--v-dur) ease;
101
+ }
102
+ .vault-checkbox[aria-checked="true"] { background: rgb(var(--v-accent)); border-color: rgb(var(--v-accent)); }
103
+ .vault-check-row__label { font-size: 13px; color: rgb(var(--v-ink-muted)); }
104
+
105
+ /* ---- form grid ---- */
106
+ .vault-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; align-items: end; }
107
+ .vault-field { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
108
+ .vault-label { font-size: 12px; font-weight: 500; color: rgb(var(--v-ink-muted)); }
109
+ .vault-inp, .vault-select {
110
+ height: 38px; padding: 0 12px; border-radius: 8px; border: 1px solid rgb(var(--v-line)); background: rgb(var(--v-surface));
111
+ color: rgb(var(--v-ink)); font-size: 13px; font-family: inherit; outline: none; width: 100%;
112
+ transition: border-color var(--v-dur) ease, box-shadow var(--v-dur) ease;
113
+ }
114
+ .vault-inp::placeholder { color: rgb(var(--v-ink-faint)); }
115
+ .vault-inp:focus, .vault-select:focus { border-color: rgb(var(--v-accent)); box-shadow: 0 0 0 3px rgb(var(--v-accent) / 0.16); }
116
+ .vault-inp--clear { display: flex; align-items: center; }
117
+ .vault-inp-wrap { position: relative; }
118
+ .vault-inp-wrap > .vault-inp { padding-right: 32px; }
119
+ .vault-inp-wrap > button { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: rgb(var(--v-ink-faint)); background: none; border: 0; display: grid; place-items: center; width: 20px; height: 20px; }
120
+ .vault-inp-wrap > button:hover { color: rgb(var(--v-ink)); }
121
+
122
+ /* ---- buttons ---- */
123
+ .vault-btn { display: inline-flex; align-items: center; gap: 8px; border: 0; cursor: pointer; font-family: inherit; transition: background-color var(--v-dur) ease, color var(--v-dur) ease, opacity var(--v-dur) ease; }
124
+ .vault-btn--primary { height: 38px; padding: 0 18px; border-radius: 9999px; background: rgb(var(--v-accent)); color: rgb(var(--v-accent-on)); font-size: 13px; font-weight: 700; }
125
+ .vault-btn--primary:hover { background: rgb(var(--v-accent) / 0.88); }
126
+ .vault-btn--icon { width: 38px; height: 38px; border-radius: 9999px; background: rgb(var(--v-accent)); color: rgb(var(--v-accent-on)); justify-content: center; flex-shrink: 0; }
127
+ .vault-btn--icon:hover { background: rgb(var(--v-accent) / 0.88); }
128
+ .vault-btn--text { height: 28px; padding: 0 4px; background: none; color: rgb(var(--v-ink-muted)); font-size: 12px; font-weight: 500; }
129
+ .vault-btn--text:hover { color: rgb(var(--v-ink)); }
130
+ .vault-btn--danger-text { height: 28px; padding: 0; background: none; color: rgb(var(--v-danger)); font-size: 12px; font-weight: 600; }
131
+ .vault-btn--danger-text:hover { opacity: 0.8; }
132
+
133
+ /* ---- status pill ---- */
134
+ .vault-pill { display: inline-flex; align-items: center; height: 24px; padding: 0 12px; border-radius: 9999px; font-size: 11px; font-weight: 700; white-space: nowrap; }
135
+ .vault-pill--accent { background: rgb(var(--v-accent)); color: rgb(var(--v-accent-on)); }
136
+ .vault-pill--warn { background: rgb(var(--v-warn)); color: rgb(15 12 8); }
137
+ .vault-pill--danger { background: rgb(var(--v-danger) / 0.16); color: rgb(var(--v-danger)); }
138
+
139
+ /* ---- stat tile: dense KPI, colour only when the state actually matters ---- */
140
+ .vault-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 24px; }
141
+ @media (min-width: 900px) { .vault-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
142
+ .vault-stat {
143
+ background: rgb(var(--v-surface)); border-radius: 8px; padding: 14px 16px; text-align: left; border: 0;
144
+ border-left: 3px solid var(--tone, transparent); cursor: default;
145
+ }
146
+ .vault-stat__figure { display: block; font-size: 22px; font-weight: 700; line-height: 1.2; color: rgb(var(--v-ink)); }
147
+ .vault-stat__label { display: block; margin-top: 4px; font-size: 12px; color: rgb(var(--v-ink-muted)); }
148
+
149
+ /* ---- table ---- */
150
+ .vault-table-wrap { background: rgb(var(--v-surface)); border-radius: 12px; overflow: hidden; }
151
+ .vault-table { width: 100%; border-collapse: collapse; }
152
+ .vault-table th {
153
+ text-align: left; font-size: 12px; font-weight: 500; color: rgb(var(--v-ink-muted)); padding: 12px 16px;
154
+ border-bottom: 1px solid rgb(var(--v-line));
155
+ }
156
+ .vault-table td { padding: 12px 16px; font-size: 13px; color: rgb(var(--v-ink)); border-bottom: 1px solid rgb(var(--v-line)); vertical-align: middle; }
157
+ .vault-table tr:last-child td { border-bottom: 0; }
158
+ .vault-table tbody tr { transition: background-color var(--v-dur) ease; }
159
+ .vault-table tbody tr:hover td { background: rgb(var(--v-surface-2)); }
@@ -0,0 +1,279 @@
1
+ import { Fragment as e, jsx as t, jsxs as n } from "react/jsx-runtime";
2
+ import { Bell as r, Check as i, ChevronDown as a, ChevronLeft as o, X as s } from "lucide-react";
3
+ //#region src/vault/VaultAvatar.jsx
4
+ function c({ name: e = "", photo: n, size: r = 32, className: i = "" }) {
5
+ let a = e.trim().split(/\s+/).slice(0, 2).map((e) => e[0]).join("").toUpperCase();
6
+ return /* @__PURE__ */ t("span", {
7
+ className: `vault-avatar ${i}`,
8
+ style: {
9
+ width: r,
10
+ height: r,
11
+ fontSize: Math.round(r * .4)
12
+ },
13
+ children: n ? /* @__PURE__ */ t("img", {
14
+ src: n,
15
+ alt: ""
16
+ }) : a
17
+ });
18
+ }
19
+ //#endregion
20
+ //#region src/vault/VaultShell.jsx
21
+ function l({ brand: e, nav: i = [], activeId: o, onNavigate: s, footNav: l = [], user: d, className: f = "", children: p }) {
22
+ return /* @__PURE__ */ n("div", {
23
+ className: `vault-shell ${f}`,
24
+ children: [/* @__PURE__ */ n("aside", {
25
+ className: "vault-shell__sidebar",
26
+ children: [
27
+ /* @__PURE__ */ n("div", {
28
+ className: "vault-shell__brand",
29
+ children: [/* @__PURE__ */ t("span", {
30
+ className: "vault-shell__brand-mark",
31
+ children: e?.mark ?? "V"
32
+ }), /* @__PURE__ */ t("span", {
33
+ className: "vault-shell__brand-name",
34
+ children: e?.name
35
+ })]
36
+ }),
37
+ /* @__PURE__ */ t("nav", {
38
+ className: "vault-shell__nav",
39
+ children: i.map((e) => /* @__PURE__ */ t(u, {
40
+ item: e,
41
+ active: e.id === o,
42
+ onClick: () => s?.(e.id)
43
+ }, e.id))
44
+ }),
45
+ l.length > 0 && /* @__PURE__ */ n("div", {
46
+ className: "vault-shell__foot",
47
+ children: [/* @__PURE__ */ t("div", {
48
+ className: "vault-shell__foot-label",
49
+ children: "Support"
50
+ }), l.map((e) => /* @__PURE__ */ t(u, {
51
+ item: e,
52
+ active: !1,
53
+ onClick: () => s?.(e.id)
54
+ }, e.id))]
55
+ })
56
+ ]
57
+ }), /* @__PURE__ */ n("div", {
58
+ className: "vault-shell__main",
59
+ children: [/* @__PURE__ */ n("header", {
60
+ className: "vault-shell__topbar",
61
+ children: [/* @__PURE__ */ t("button", {
62
+ type: "button",
63
+ className: "vault-topbar__icon",
64
+ title: "Notifications",
65
+ children: /* @__PURE__ */ t(r, {
66
+ size: 17,
67
+ "aria-hidden": "true"
68
+ })
69
+ }), d && /* @__PURE__ */ n("button", {
70
+ type: "button",
71
+ className: "vault-btn vault-user",
72
+ style: {
73
+ padding: 0,
74
+ background: "none"
75
+ },
76
+ title: d.email,
77
+ children: [
78
+ /* @__PURE__ */ t(c, {
79
+ name: d.name,
80
+ photo: d.photo
81
+ }),
82
+ /* @__PURE__ */ n("span", {
83
+ className: "min-w-0",
84
+ style: { textAlign: "left" },
85
+ children: [/* @__PURE__ */ t("span", {
86
+ className: "vault-user__name block",
87
+ children: d.name
88
+ }), /* @__PURE__ */ t("span", {
89
+ className: "vault-user__role block",
90
+ children: d.role
91
+ })]
92
+ }),
93
+ /* @__PURE__ */ t(a, {
94
+ size: 14,
95
+ "aria-hidden": "true",
96
+ style: { color: "rgb(var(--v-ink-faint))" }
97
+ })
98
+ ]
99
+ })]
100
+ }), /* @__PURE__ */ t("div", {
101
+ className: "vault-shell__content",
102
+ children: p
103
+ })]
104
+ })]
105
+ });
106
+ }
107
+ function u({ item: e, active: r, onClick: i }) {
108
+ let a = e.icon;
109
+ return /* @__PURE__ */ n("button", {
110
+ type: "button",
111
+ className: "vault-nav-item",
112
+ "aria-current": r ? "page" : void 0,
113
+ onClick: i,
114
+ title: e.label,
115
+ children: [a && /* @__PURE__ */ t(a, {
116
+ size: 17,
117
+ "aria-hidden": "true"
118
+ }), /* @__PURE__ */ t("span", {
119
+ className: "truncate",
120
+ children: e.label
121
+ })]
122
+ });
123
+ }
124
+ //#endregion
125
+ //#region src/vault/VaultPageHeader.jsx
126
+ function d({ tabs: e = [], value: n, onChange: r, className: i = "" }) {
127
+ return /* @__PURE__ */ t("div", {
128
+ className: `vault-tabs ${i}`,
129
+ role: "tablist",
130
+ children: e.map((e) => /* @__PURE__ */ t("button", {
131
+ type: "button",
132
+ role: "tab",
133
+ className: "vault-tab",
134
+ "aria-selected": e.value === n,
135
+ onClick: () => r?.(e.value),
136
+ children: e.label
137
+ }, e.value))
138
+ });
139
+ }
140
+ function f({ onBack: r, title: i, tabs: a, value: s, onTabChange: c, figure: l, figureSub: u, pill: f }) {
141
+ return /* @__PURE__ */ n(e, { children: [r && /* @__PURE__ */ n("button", {
142
+ type: "button",
143
+ className: "vault-back",
144
+ onClick: r,
145
+ children: [/* @__PURE__ */ t(o, {
146
+ size: 14,
147
+ "aria-hidden": "true"
148
+ }), "Back"]
149
+ }), /* @__PURE__ */ n("div", {
150
+ className: "vault-header",
151
+ children: [/* @__PURE__ */ n("div", { children: [/* @__PURE__ */ t("h1", {
152
+ className: "vault-header__title",
153
+ children: i
154
+ }), a && /* @__PURE__ */ t(d, {
155
+ tabs: a,
156
+ value: s,
157
+ onChange: c
158
+ })] }), (l || f) && /* @__PURE__ */ n("div", { children: [
159
+ l && /* @__PURE__ */ t("div", {
160
+ className: "vault-header__figure",
161
+ children: l
162
+ }),
163
+ u && /* @__PURE__ */ t("div", {
164
+ className: "vault-header__sub",
165
+ children: u
166
+ }),
167
+ f && /* @__PURE__ */ t("div", {
168
+ className: "vault-header__pill-row",
169
+ children: f
170
+ })
171
+ ] })]
172
+ })] });
173
+ }
174
+ //#endregion
175
+ //#region src/vault/VaultStatusPill.jsx
176
+ function p({ tone: e = "accent", children: n, className: r = "" }) {
177
+ return /* @__PURE__ */ t("span", {
178
+ className: `vault-pill vault-pill--${e} ${r}`,
179
+ children: n
180
+ });
181
+ }
182
+ //#endregion
183
+ //#region src/vault/VaultStatTile.jsx
184
+ function m({ figure: e, label: r, tone: i, className: a = "" }) {
185
+ let o = i === "accent" ? "rgb(var(--v-accent))" : i === "warn" ? "rgb(var(--v-warn))" : i === "danger" ? "rgb(var(--v-danger))" : void 0;
186
+ return /* @__PURE__ */ n("div", {
187
+ className: `vault-stat ${a}`,
188
+ style: o ? { "--tone": o } : void 0,
189
+ children: [/* @__PURE__ */ t("span", {
190
+ className: "vault-stat__figure",
191
+ children: e
192
+ }), /* @__PURE__ */ t("span", {
193
+ className: "vault-stat__label",
194
+ children: r
195
+ })]
196
+ });
197
+ }
198
+ //#endregion
199
+ //#region src/vault/VaultControls.jsx
200
+ function h({ variant: e = "primary", icon: r, className: i = "", children: a, ...o }) {
201
+ return /* @__PURE__ */ n("button", {
202
+ type: "button",
203
+ className: `vault-btn vault-btn--${e} ${i}`,
204
+ ...o,
205
+ children: [r && /* @__PURE__ */ t(r, {
206
+ size: e === "icon" ? 18 : 15,
207
+ "aria-hidden": "true"
208
+ }), e !== "icon" && a]
209
+ });
210
+ }
211
+ function g({ checked: e = !1, onChange: r, label: a, className: o = "" }) {
212
+ return /* @__PURE__ */ n("label", {
213
+ className: `vault-check-row ${o}`,
214
+ style: { cursor: "pointer" },
215
+ children: [/* @__PURE__ */ t("button", {
216
+ type: "button",
217
+ role: "checkbox",
218
+ "aria-checked": e,
219
+ className: "vault-checkbox",
220
+ onClick: () => r?.(!e),
221
+ children: e && /* @__PURE__ */ t(i, {
222
+ size: 12,
223
+ "aria-hidden": "true"
224
+ })
225
+ }), a && /* @__PURE__ */ t("span", {
226
+ className: "vault-check-row__label",
227
+ children: a
228
+ })]
229
+ });
230
+ }
231
+ function _({ clearable: e = !1, onClear: r, className: i = "", ...a }) {
232
+ return e ? /* @__PURE__ */ n("span", {
233
+ className: "vault-inp-wrap",
234
+ children: [/* @__PURE__ */ t("input", {
235
+ className: `vault-inp ${i}`,
236
+ ...a
237
+ }), /* @__PURE__ */ t("button", {
238
+ type: "button",
239
+ onClick: r,
240
+ title: "Clear",
241
+ "aria-label": "Clear",
242
+ children: /* @__PURE__ */ t(s, {
243
+ size: 14,
244
+ "aria-hidden": "true"
245
+ })
246
+ })]
247
+ }) : /* @__PURE__ */ t("input", {
248
+ className: `vault-inp ${i}`,
249
+ ...a
250
+ });
251
+ }
252
+ function v({ className: e = "", children: n, ...r }) {
253
+ return /* @__PURE__ */ t("select", {
254
+ className: `vault-select ${e}`,
255
+ ...r,
256
+ children: n
257
+ });
258
+ }
259
+ //#endregion
260
+ //#region src/vault/VaultTable.jsx
261
+ function y({ columns: e = [], rows: r = [], getRowId: i = (e, t) => e.id ?? t, className: a = "" }) {
262
+ return /* @__PURE__ */ t("div", {
263
+ className: "vault-table-wrap",
264
+ children: /* @__PURE__ */ n("table", {
265
+ className: `vault-table ${a}`,
266
+ children: [/* @__PURE__ */ t("thead", { children: /* @__PURE__ */ t("tr", { children: e.map((e) => /* @__PURE__ */ t("th", {
267
+ style: e.align === "right" ? { textAlign: "right" } : void 0,
268
+ children: e.label
269
+ }, e.key)) }) }), /* @__PURE__ */ t("tbody", { children: r.map((n, r) => /* @__PURE__ */ t("tr", { children: e.map((e) => /* @__PURE__ */ t("td", {
270
+ style: e.align === "right" ? { textAlign: "right" } : void 0,
271
+ children: e.render ? e.render(n) : n[e.key]
272
+ }, e.key)) }, i(n, r))) })]
273
+ })
274
+ });
275
+ }
276
+ //#endregion
277
+ export { c as VaultAvatar, h as VaultButton, g as VaultCheckbox, _ as VaultInput, f as VaultPageHeader, v as VaultSelect, l as VaultShell, m as VaultStatTile, p as VaultStatusPill, y as VaultTable, d as VaultTabs };
278
+
279
+ //# sourceMappingURL=vault.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.mjs","names":[],"sources":["../src/vault/VaultAvatar.jsx","../src/vault/VaultShell.jsx","../src/vault/VaultPageHeader.jsx","../src/vault/VaultStatusPill.jsx","../src/vault/VaultStatTile.jsx","../src/vault/VaultControls.jsx","../src/vault/VaultTable.jsx"],"sourcesContent":["/** Circular avatar restyled for a dark ground — see VaultShell for usage. */\nexport default function VaultAvatar({ name = \"\", photo, size = 32, className = \"\" }) {\n const initials = name\n .trim()\n .split(/\\s+/)\n .slice(0, 2)\n .map((p) => p[0])\n .join(\"\")\n .toUpperCase();\n return (\n <span className={`vault-avatar ${className}`} style={{ width: size, height: size, fontSize: Math.round(size * 0.4) }}>\n {photo ? <img src={photo} alt=\"\" /> : initials}\n </span>\n );\n}\n","import { Bell, ChevronDown } from \"lucide-react\";\nimport VaultAvatar from \"./VaultAvatar\";\n\n/**\n * VaultShell — the sidebar + top bar chrome for Vault UI\n * (docs/design-system-vault.md §3). Dark flows continuously: sidebar,\n * top bar and content are all --v-canvas, no lighter sidebar surface.\n *\n * <VaultShell nav={NAV} activeId={id} onNavigate={setId} brand={{...}} user={{...}}>\n * {content}\n * </VaultShell>\n */\nexport default function VaultShell({ brand, nav = [], activeId, onNavigate, footNav = [], user, className = \"\", children }) {\n return (\n <div className={`vault-shell ${className}`}>\n <aside className=\"vault-shell__sidebar\">\n <div className=\"vault-shell__brand\">\n <span className=\"vault-shell__brand-mark\">{brand?.mark ?? \"V\"}</span>\n <span className=\"vault-shell__brand-name\">{brand?.name}</span>\n </div>\n <nav className=\"vault-shell__nav\">\n {nav.map((item) => (\n <VaultNavItem key={item.id} item={item} active={item.id === activeId} onClick={() => onNavigate?.(item.id)} />\n ))}\n </nav>\n {footNav.length > 0 && (\n <div className=\"vault-shell__foot\">\n <div className=\"vault-shell__foot-label\">Support</div>\n {footNav.map((item) => (\n <VaultNavItem key={item.id} item={item} active={false} onClick={() => onNavigate?.(item.id)} />\n ))}\n </div>\n )}\n </aside>\n <div className=\"vault-shell__main\">\n <header className=\"vault-shell__topbar\">\n <button type=\"button\" className=\"vault-topbar__icon\" title=\"Notifications\">\n <Bell size={17} aria-hidden=\"true\" />\n </button>\n {user && (\n <button type=\"button\" className=\"vault-btn vault-user\" style={{ padding: 0, background: \"none\" }} title={user.email}>\n <VaultAvatar name={user.name} photo={user.photo} />\n <span className=\"min-w-0\" style={{ textAlign: \"left\" }}>\n <span className=\"vault-user__name block\">{user.name}</span>\n <span className=\"vault-user__role block\">{user.role}</span>\n </span>\n <ChevronDown size={14} aria-hidden=\"true\" style={{ color: \"rgb(var(--v-ink-faint))\" }} />\n </button>\n )}\n </header>\n <div className=\"vault-shell__content\">{children}</div>\n </div>\n </div>\n );\n}\n\nfunction VaultNavItem({ item, active, onClick }) {\n const Icon = item.icon;\n return (\n <button type=\"button\" className=\"vault-nav-item\" aria-current={active ? \"page\" : undefined} onClick={onClick} title={item.label}>\n {Icon && <Icon size={17} aria-hidden=\"true\" />}\n <span className=\"truncate\">{item.label}</span>\n </button>\n );\n}\n","import { ChevronLeft } from \"lucide-react\";\n\n/** Underline tabs (§4) — a different composition earns a different tab shape than the main system's segmented track. */\nexport function VaultTabs({ tabs = [], value, onChange, className = \"\" }) {\n return (\n <div className={`vault-tabs ${className}`} role=\"tablist\">\n {tabs.map((t) => (\n <button\n key={t.value}\n type=\"button\"\n role=\"tab\"\n className=\"vault-tab\"\n aria-selected={t.value === value}\n onClick={() => onChange?.(t.value)}\n >\n {t.label}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * VaultPageHeader — back control, bold title, underline tabs, and a\n * right-hand slot for the one figure that matters most on this page plus\n * its status pill (docs/design-system-vault.md §3).\n *\n * <VaultPageHeader onBack={...} title=\"Marketing\" tabs={...} value={tab} onTabChange={setTab}\n * figure=\"2,345.6789 BTC\" figureSub=\"≈59,339,969.79 CNY\" pill={<VaultStatusPill tone=\"warn\">Effective after 23:59:59</VaultStatusPill>} />\n */\nexport default function VaultPageHeader({ onBack, title, tabs, value, onTabChange, figure, figureSub, pill }) {\n return (\n <>\n {onBack && (\n <button type=\"button\" className=\"vault-back\" onClick={onBack}>\n <ChevronLeft size={14} aria-hidden=\"true\" />\n Back\n </button>\n )}\n <div className=\"vault-header\">\n <div>\n <h1 className=\"vault-header__title\">{title}</h1>\n {tabs && <VaultTabs tabs={tabs} value={value} onChange={onTabChange} />}\n </div>\n {(figure || pill) && (\n <div>\n {figure && <div className=\"vault-header__figure\">{figure}</div>}\n {figureSub && <div className=\"vault-header__sub\">{figureSub}</div>}\n {pill && <div className=\"vault-header__pill-row\">{pill}</div>}\n </div>\n )}\n </div>\n </>\n );\n}\n","/**\n * VaultStatusPill — solid fill, one of the three semantic colours\n * (§1, §4). Sparingly: the record's one current state, never decoration.\n *\n * <VaultStatusPill tone=\"warn\">Effective after 23:59:59</VaultStatusPill>\n */\nexport default function VaultStatusPill({ tone = \"accent\", children, className = \"\" }) {\n return <span className={`vault-pill vault-pill--${tone} ${className}`}>{children}</span>;\n}\n","/**\n * VaultStatTile — a dense KPI figure. Colour only on the left edge, and\n * only when `tone` is set — most tiles stay neutral ink (§5 law 1: colour\n * reserved for state that actually matters, never decoration).\n *\n * <VaultStatTile figure={3} label=\"Approved\" tone=\"accent\" />\n */\nexport default function VaultStatTile({ figure, label, tone, className = \"\" }) {\n const toneVar = tone === \"accent\" ? \"rgb(var(--v-accent))\" : tone === \"warn\" ? \"rgb(var(--v-warn))\" : tone === \"danger\" ? \"rgb(var(--v-danger))\" : undefined;\n return (\n <div className={`vault-stat ${className}`} style={toneVar ? { \"--tone\": toneVar } : undefined}>\n <span className=\"vault-stat__figure\">{figure}</span>\n <span className=\"vault-stat__label\">{label}</span>\n </div>\n );\n}\n","import { Check, X } from \"lucide-react\";\n\n/**\n * Vault UI controls (§4): Button, Checkbox, Input, Select. Dark fields,\n * 8px radius, accent border + ring on focus.\n */\nexport function VaultButton({ variant = \"primary\", icon: Icon, className = \"\", children, ...rest }) {\n return (\n <button type=\"button\" className={`vault-btn vault-btn--${variant} ${className}`} {...rest}>\n {Icon && <Icon size={variant === \"icon\" ? 18 : 15} aria-hidden=\"true\" />}\n {variant !== \"icon\" && children}\n </button>\n );\n}\n\n/** `checked` + `onChange` — a plain toggle, not the main system's native checkbox skin. */\nexport function VaultCheckbox({ checked = false, onChange, label, className = \"\" }) {\n return (\n <label className={`vault-check-row ${className}`} style={{ cursor: \"pointer\" }}>\n <button\n type=\"button\"\n role=\"checkbox\"\n aria-checked={checked}\n className=\"vault-checkbox\"\n onClick={() => onChange?.(!checked)}\n >\n {checked && <Check size={12} aria-hidden=\"true\" />}\n </button>\n {label && <span className=\"vault-check-row__label\">{label}</span>}\n </label>\n );\n}\n\nexport function VaultInput({ clearable = false, onClear, className = \"\", ...rest }) {\n if (!clearable) return <input className={`vault-inp ${className}`} {...rest} />;\n return (\n <span className=\"vault-inp-wrap\">\n <input className={`vault-inp ${className}`} {...rest} />\n <button type=\"button\" onClick={onClear} title=\"Clear\" aria-label=\"Clear\">\n <X size={14} aria-hidden=\"true\" />\n </button>\n </span>\n );\n}\n\nexport function VaultSelect({ className = \"\", children, ...rest }) {\n return (\n <select className={`vault-select ${className}`} {...rest}>\n {children}\n </select>\n );\n}\n","/**\n * VaultTable (§4) — dense, muted sentence-case headers (not the main\n * system's uppercase-tracked style), hairline row dividers, no zebra.\n *\n * Column: { key, label, render?(row), align? }\n */\nexport default function VaultTable({ columns = [], rows = [], getRowId = (row, i) => row.id ?? i, className = \"\" }) {\n return (\n <div className=\"vault-table-wrap\">\n <table className={`vault-table ${className}`}>\n <thead>\n <tr>\n {columns.map((c) => (\n <th key={c.key} style={c.align === \"right\" ? { textAlign: \"right\" } : undefined}>\n {c.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, i) => (\n <tr key={getRowId(row, i)}>\n {columns.map((c) => (\n <td key={c.key} style={c.align === \"right\" ? { textAlign: \"right\" } : undefined}>\n {c.render ? c.render(row) : row[c.key]}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ # Vault UI — a second design composition, for serious data
2
+
3
+ Indira Workspace UI (`docs/design-system.md`) is the default: light, one rose
4
+ accent, soft rounded shapes. It is right for governance/workflow tools where
5
+ the *product* should feel approachable.
6
+
7
+ Vault UI is the other option. It exists for screens where the *data itself*
8
+ carries the seriousness — balances, spend limits, custody/treasury,
9
+ compliance ledgers — and a light, friendly surface would undersell that.
10
+ Dark by default (it does not ship a light mode; committing to dark is part
11
+ of the seriousness), one desaturated accent, moderate rounding, dense
12
+ tabular layout. Inspired by custody/treasury dashboards (Fireblocks, BitGo,
13
+ Coinbase Custody-style admin tools) — not a literal clone of any one of
14
+ them.
15
+
16
+ Same method as the main system: a token set, a fixed set of scales, one
17
+ shell, a small set of components, a short list of laws. Component code
18
+ lives in `src/vault/`, styles in `src/vault/vault.css`, everything scoped
19
+ under one root class (`.vault`) so it never bleeds into an Indira Workspace
20
+ UI screen sharing the same page.
21
+
22
+ ---
23
+
24
+ ## 1. Tokens
25
+
26
+ RGB triplets, same convention as the main system, so any colour can take an
27
+ opacity modifier.
28
+
29
+ | Token | Value | Role |
30
+ | --- | --- | --- |
31
+ | `--v-canvas` | `13 16 22` | Page ground. Sidebar, header and content panel are all this — dark flows continuously, no lighter sidebar. |
32
+ | `--v-surface` | `20 24 32` | Raised regions: the table block, inputs, the active-nav pill. |
33
+ | `--v-surface-2` | `27 32 42` | Hover / pressed state on a surface. |
34
+ | `--v-line` | `38 44 56` | Hairlines: table row dividers, input borders at rest. |
35
+ | `--v-ink` | `237 239 243` | Primary text. |
36
+ | `--v-ink-muted` | `138 145 158` | Secondary text, column headers, placeholders. |
37
+ | `--v-ink-faint` | `86 93 108` | Tertiary — disabled, least important. |
38
+ | `--v-accent` | `52 201 142` | The one accent: primary button, focus ring, active tab underline, positive status, checkbox check. |
39
+ | `--v-accent-on` | `8 20 16` | Text/icon on the accent fill. |
40
+ | `--v-warn` | `224 138 62` | Pending / "effective after" / needs-attention status. Used on a pill only, never decoratively. |
41
+ | `--v-danger` | `224 96 122` | Destructive text action (e.g. "Remove"). Text colour only — no filled danger buttons in this set; a destructive action here is a plain link-weight word, confirmed before it fires. |
42
+
43
+ Exactly three semantic colours (`accent`, `warn`, `danger`) — the same
44
+ discipline as the main system's six statuses, just a smaller palette
45
+ because Vault screens carry fewer distinct states. A fourth is a product
46
+ decision, not a per-screen one.
47
+
48
+ ## 2. Scales
49
+
50
+ - **Radius**: `8px` controls (inputs, buttons, table block), `9999px` pills
51
+ and circular icon buttons and avatars. Two values, not four — this system
52
+ doesn't need the main system's card/nav-item middle step.
53
+ - **Type**: same family as the main system (Inter / system sans). Sizes:
54
+ `11px` column headers and micro-labels, `13px` body/table cells, `14px`
55
+ controls, `16px` section titles, `20px` page title, `24px` the balance
56
+ figure. Numbers are always `tabular-nums`.
57
+ - **Spacing**: 4/8 grid, same as the main system. Sidebar `220px`, header
58
+ row `64px`, panel padding `24px`, table cell padding `12px 16px`.
59
+ - **Motion**: `150ms` ease, background/border/colour transitions only. No
60
+ spring, no bounce — same law as the main system.
61
+
62
+ ## 3. Shell
63
+
64
+ Sidebar (dark, `220px`, icon + label nav, active item gets a `--v-surface`
65
+ pill behind it and an accent-tinted icon) and a slim top bar (icon actions,
66
+ bell, avatar + name + role + chevron, right-aligned) — no search field by
67
+ default; Vault screens are narrower in scope than a full workspace. A page
68
+ inside it gets its own header block: a "Back" control, a bold title, an
69
+ underline tab row for switching views, and — right-aligned — the figure
70
+ that matters most on this page (a balance, a total, a count) plus a status
71
+ pill for the record's current state.
72
+
73
+ ## 4. Components
74
+
75
+ - **`VaultShell`** — the sidebar + top bar chrome described above.
76
+ - **`VaultPageHeader`** — back control, title, `VaultTabs`, and a right-hand
77
+ slot for a figure + `VaultStatusPill`.
78
+ - **`VaultTabs`** — underline style: active tab is ink text over a
79
+ `2px` accent underline; inactive tabs are muted text, no track/pill
80
+ (this is *not* the main system's segmented-track tabs — a different
81
+ composition earns a different tab treatment).
82
+ - **Controls** (`VaultButton`, `VaultCheckbox`, `VaultInput`, `VaultSelect`)
83
+ — dark fields, `8px` radius, accent border + ring on focus. `VaultButton`
84
+ variants: `primary` (solid accent), `icon` (circular, for the one
85
+ add/create action per section), `text` (row actions), `danger-text` (the
86
+ one destructive action, coloured, never filled).
87
+ - **`VaultStatusPill`** — pill, solid fill in one of the three semantic
88
+ colours, white/on-colour text. Sparingly: the record's one current state,
89
+ never decoration.
90
+ - **`VaultTable`** — dense, header row in muted sentence-case (not the main
91
+ system's uppercase-tracked style — Vault's own voice), hairline row
92
+ dividers, no zebra striping, a `text`/`danger-text` button for row
93
+ actions.
94
+ - **`VaultAvatar`** — circular, photo or initials, same idea as the main
95
+ system's Avatar, restyled for a dark ground.
96
+
97
+ ## 5. Laws
98
+
99
+ 1. **One accent, spent twice at most per view**: the primary button and the
100
+ one positive/active status. Warn and danger are for state, never chrome.
101
+ 2. **Dark, not dimmed** — this is not the main system's palette with the
102
+ lights off; every token above is authored for a dark ground, so contrast
103
+ and the accent's saturation are tuned for it directly.
104
+ 3. **Density is the point.** A Vault screen assumes the reader wants to
105
+ scan a lot of figures fast — tabular numbers, tight rows, minimal
106
+ decoration. If a control isn't load-bearing, it isn't on the screen.
107
+ 4. **Read-only is visible.** A view a person can't act on says so (a
108
+ labelled state, not just the absence of a button) — the main system's
109
+ law 3 (no over-data) and law 5 (status is a shape and a colour) both
110
+ still apply here; this system just draws them in a different hand.
111
+ 5. **No destructive action fires unconfirmed.** `danger-text` is a trigger,
112
+ not the action itself — same rule as the main system's law 14, restated
113
+ because this system's whole reason to exist is data where that matters.
114
+
115
+ ---
116
+
117
+ ## 6. Building it
118
+
119
+ 1. Tokens + component CSS live in `src/vault/vault.css`, scoped under one
120
+ `.vault` root class — never edit `src/index.css` or the main system's
121
+ component classes to make room for this one.
122
+ 2. Build `VaultShell` first, then controls, then `VaultTable` and
123
+ `VaultPageHeader`.
124
+ 3. A screen either lives entirely in Indira Workspace UI or entirely in
125
+ Vault UI — the two are never mixed on one screen (that's exactly the
126
+ "variety reads as generated" failure the main system's law 1 warns
127
+ about, just as true across two systems as within one).
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "indira-workspace-ui",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
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",
6
+ "description": "Indira Workspace UI — the component library specified in docs/design-system.md, plus Vault UI (docs/design-system-vault.md, a second dark/serious-data composition) and a CLI to install the indira-ui and indira-api Claude Code skills",
7
7
  "keywords": [
8
8
  "react",
9
9
  "component-library",
@@ -17,7 +17,8 @@
17
17
  "bin",
18
18
  ".claude/skills/indira-ui",
19
19
  ".claude/skills/indira-api",
20
- "docs/design-system.md"
20
+ "docs/design-system.md",
21
+ "docs/design-system-vault.md"
21
22
  ],
22
23
  "main": "./dist-lib/indira-workspace-ui.cjs",
23
24
  "module": "./dist-lib/indira-workspace-ui.mjs",
@@ -26,7 +27,12 @@
26
27
  "import": "./dist-lib/indira-workspace-ui.mjs",
27
28
  "require": "./dist-lib/indira-workspace-ui.cjs"
28
29
  },
29
- "./style.css": "./dist-lib/style.css"
30
+ "./style.css": "./dist-lib/style.css",
31
+ "./vault": {
32
+ "import": "./dist-lib/vault.mjs",
33
+ "require": "./dist-lib/vault.cjs"
34
+ },
35
+ "./vault/style.css": "./dist-lib/vault.css"
30
36
  },
31
37
  "bin": {
32
38
  "indira-workspace-ui": "bin/cli.js"