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.
- package/.claude/skills/indira-api/SKILL.md +14 -4
- package/.claude/skills/indira-api/reference/api-architecture.md +71 -1
- package/.claude/skills/indira-ui/SKILL.md +72 -14
- package/README.md +29 -0
- package/dist-lib/vault.cjs +2 -0
- package/dist-lib/vault.cjs.map +1 -0
- package/dist-lib/vault.css +159 -0
- package/dist-lib/vault.mjs +279 -0
- package/dist-lib/vault.mjs.map +1 -0
- package/docs/design-system-vault.md +127 -0
- package/package.json +10 -4
|
@@ -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,
|
|
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` (§
|
|
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
|
-
|
|
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 (§
|
|
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.
|
|
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
|
|
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.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
number and rule you need is in
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
147
|
-
decision, not a
|
|
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.
|
|
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"
|