@nim.zone/ui 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +681 -0
  3. package/dist/components/admin-shell.d.ts +121 -0
  4. package/dist/components/app-shell.d.ts +23 -0
  5. package/dist/components/auth-screen.d.ts +33 -0
  6. package/dist/components/avatar.d.ts +10 -0
  7. package/dist/components/badge.d.ts +14 -0
  8. package/dist/components/banner.d.ts +11 -0
  9. package/dist/components/breadcrumb.d.ts +12 -0
  10. package/dist/components/button.d.ts +21 -0
  11. package/dist/components/card.d.ts +15 -0
  12. package/dist/components/chat-composer.d.ts +52 -0
  13. package/dist/components/chat.d.ts +77 -0
  14. package/dist/components/checkout.d.ts +79 -0
  15. package/dist/components/choice.d.ts +15 -0
  16. package/dist/components/combobox.d.ts +31 -0
  17. package/dist/components/date-field.d.ts +58 -0
  18. package/dist/components/dialog.d.ts +20 -0
  19. package/dist/components/empty-state.d.ts +9 -0
  20. package/dist/components/feedback.d.ts +18 -0
  21. package/dist/components/field.d.ts +47 -0
  22. package/dist/components/icon-button.d.ts +12 -0
  23. package/dist/components/icon.d.ts +72 -0
  24. package/dist/components/layout.d.ts +13 -0
  25. package/dist/components/list.d.ts +16 -0
  26. package/dist/components/menu.d.ts +56 -0
  27. package/dist/components/onboarding.d.ts +50 -0
  28. package/dist/components/otp-input.d.ts +29 -0
  29. package/dist/components/pagination.d.ts +12 -0
  30. package/dist/components/password-field.d.ts +33 -0
  31. package/dist/components/phone-field.d.ts +44 -0
  32. package/dist/components/plan-card.d.ts +45 -0
  33. package/dist/components/plan-picker.d.ts +52 -0
  34. package/dist/components/profile-header.d.ts +47 -0
  35. package/dist/components/profile-screen.d.ts +44 -0
  36. package/dist/components/resource-meter.d.ts +22 -0
  37. package/dist/components/section-header.d.ts +8 -0
  38. package/dist/components/segmented.d.ts +18 -0
  39. package/dist/components/sheet.d.ts +18 -0
  40. package/dist/components/sign-in-flow.d.ts +60 -0
  41. package/dist/components/slider.d.ts +10 -0
  42. package/dist/components/stat.d.ts +10 -0
  43. package/dist/components/stepper.d.ts +17 -0
  44. package/dist/components/tab-bar.d.ts +40 -0
  45. package/dist/components/table.d.ts +33 -0
  46. package/dist/components/tabs.d.ts +23 -0
  47. package/dist/components/task-progress.d.ts +38 -0
  48. package/dist/components/theme.d.ts +49 -0
  49. package/dist/components/toast.d.ts +17 -0
  50. package/dist/components/tooltip.d.ts +18 -0
  51. package/dist/components/typography.d.ts +18 -0
  52. package/dist/components/wizard.d.ts +70 -0
  53. package/dist/index.d.ts +104 -0
  54. package/dist/lib/calendars.d.ts +81 -0
  55. package/dist/lib/cn.d.ts +7 -0
  56. package/dist/lib/countries.d.ts +33 -0
  57. package/dist/lib/use-anchor.d.ts +22 -0
  58. package/dist/nim.css +1 -0
  59. package/dist/nim.js +3266 -0
  60. package/package.json +83 -0
  61. package/src/components/admin-shell.tsx +338 -0
  62. package/src/components/app-shell.tsx +36 -0
  63. package/src/components/auth-screen.tsx +71 -0
  64. package/src/components/avatar.tsx +32 -0
  65. package/src/components/badge.tsx +44 -0
  66. package/src/components/banner.tsx +47 -0
  67. package/src/components/breadcrumb.tsx +45 -0
  68. package/src/components/button.tsx +64 -0
  69. package/src/components/card.tsx +46 -0
  70. package/src/components/chat-composer.tsx +331 -0
  71. package/src/components/chat.tsx +352 -0
  72. package/src/components/checkout.tsx +173 -0
  73. package/src/components/choice.tsx +46 -0
  74. package/src/components/combobox.tsx +157 -0
  75. package/src/components/date-field.tsx +394 -0
  76. package/src/components/dialog.tsx +77 -0
  77. package/src/components/empty-state.tsx +23 -0
  78. package/src/components/feedback.tsx +61 -0
  79. package/src/components/field.tsx +177 -0
  80. package/src/components/icon-button.tsx +35 -0
  81. package/src/components/icon.tsx +167 -0
  82. package/src/components/layout.tsx +38 -0
  83. package/src/components/list.tsx +78 -0
  84. package/src/components/menu.tsx +172 -0
  85. package/src/components/onboarding.tsx +157 -0
  86. package/src/components/otp-input.tsx +144 -0
  87. package/src/components/pagination.tsx +86 -0
  88. package/src/components/password-field.tsx +112 -0
  89. package/src/components/phone-field.tsx +242 -0
  90. package/src/components/plan-card.tsx +124 -0
  91. package/src/components/plan-picker.tsx +144 -0
  92. package/src/components/profile-header.tsx +134 -0
  93. package/src/components/profile-screen.tsx +104 -0
  94. package/src/components/resource-meter.tsx +60 -0
  95. package/src/components/section-header.tsx +31 -0
  96. package/src/components/segmented.tsx +53 -0
  97. package/src/components/sheet.tsx +74 -0
  98. package/src/components/sign-in-flow.tsx +322 -0
  99. package/src/components/slider.tsx +50 -0
  100. package/src/components/stat.tsx +30 -0
  101. package/src/components/stepper.tsx +77 -0
  102. package/src/components/tab-bar.tsx +82 -0
  103. package/src/components/table.tsx +81 -0
  104. package/src/components/tabs.tsx +72 -0
  105. package/src/components/task-progress.tsx +111 -0
  106. package/src/components/theme.tsx +112 -0
  107. package/src/components/toast.tsx +87 -0
  108. package/src/components/tooltip.tsx +30 -0
  109. package/src/components/typography.tsx +65 -0
  110. package/src/components/wizard.tsx +192 -0
  111. package/src/index.ts +216 -0
  112. package/src/lib/calendars.ts +227 -0
  113. package/src/lib/cn.ts +8 -0
  114. package/src/lib/countries.ts +110 -0
  115. package/src/lib/use-anchor.ts +92 -0
  116. package/src/theme/colorways/coral.css +62 -0
  117. package/src/theme/colorways/oxblood.css +23 -0
  118. package/src/theme/colorways/paper.css +68 -0
  119. package/src/theme/colorways/teal.css +56 -0
  120. package/src/theme/colorways/vermilion.css +21 -0
  121. package/src/theme/components.css +4606 -0
  122. package/src/theme/contract.css +204 -0
  123. package/src/theme/fonts.css +67 -0
  124. package/src/theme/index.css +33 -0
  125. package/src/theme/persian.css +47 -0
  126. package/src/theme/reset.css +118 -0
  127. package/src/theme/styles/ledger.css +106 -0
  128. package/src/theme/styles/vlora.css +91 -0
  129. package/src/vite-env.d.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,681 @@
1
+ # nim
2
+
3
+ The front-end design system and UI kit shared across nim products.
4
+
5
+ nim is two things in one package: a **token contract** that defines the vocabulary
6
+ a product interface is allowed to speak, and a **React kit** that speaks only that
7
+ vocabulary. Swapping the theme changes every screen at once, because nothing
8
+ downstream of the contract holds a literal value.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @nim.zone/ui react react-dom
14
+ ```
15
+
16
+ ```tsx
17
+ import { NimProvider, Button } from '@nim.zone/ui'
18
+ ```
19
+
20
+ The stylesheet ships with the import, so nothing else is required. Consumers that
21
+ need the raw token contract without the React kit can import
22
+ `@nim.zone/ui/styles.css`, `@nim.zone/ui/src/theme/index.css` or
23
+ `@nim.zone/ui/fonts.css` directly. `react` / `react-dom` >= 18 are peers.
24
+
25
+ Published from `nim-ui/` in this repo (`npm publish`, which runs `npm run build`
26
+ first). Repo-local development uses the scripts below.
27
+
28
+ ---
29
+
30
+ Reference implementation: `../vlora-app` — its architecture (flat CSS-variable
31
+ tokens, thin components that compose semantic class names, all styling in
32
+ `@layer components`, RTL- and mobile-first) is the shape nim generalises.
33
+
34
+ ```bash
35
+ npm install
36
+ npm run dev # the docs gallery — every token, component, variant, state
37
+ npm run build # the distributable kit → dist/nim.js + dist/nim.css
38
+ npm run build:docs # the gallery, published to the site → nim.zone/uikit
39
+ npm run typecheck
40
+ ```
41
+
42
+ The gallery covers three pages: **Foundations** documents colour,
43
+ type, space and fixed sizes, shape, elevation, focus, density and motion — the
44
+ motion section runs the three easing curves side by side and reports whether
45
+ your own OS is asking for reduced motion — and **Components** shows every
46
+ variant, size and state; **Flows** runs the ten screens a product is judged on
47
+ before it is used — the intro carousel, the sign-in, a wizard, a conversation
48
+ with voice, video and file messages, a checkout, a long-running job, the plan
49
+ picker, the profile, the app shell and an operator console — each mounted and
50
+ working, the phone ones in a 390pt frame. The language switch puts the whole thing into Farsi and RTL rather
51
+ than mirroring English.
52
+
53
+ The gallery is published as part of the personal site: `build:docs` emits into
54
+ `../apps/nim/public/uikit`, which the site serves at `/uikit/` and
55
+ ships in its Docker image. That output is committed, so a site deploy needs no
56
+ knowledge of this package; rerun `npm run uikit` from the site (or `build:docs`
57
+ here) whenever the kit changes.
58
+
59
+ ---
60
+
61
+ ## What changed in 0.6
62
+
63
+ 0.6 is a second sweep through the reference apps — `vlora-app` for the phone
64
+ flows, `vlora-admin` for the console — pulling out the screens the family kept
65
+ rebuilding. Nothing was removed and no token changed.
66
+
67
+ **From the app**
68
+
69
+ - `Wizard` + `ChoiceGrid` — the one-question-per-screen flow behind Vlora's
70
+ daily reflection: step dots, a back control, a close control that is always
71
+ present, and a CTA gated on the step's own `canContinue`. The step index is
72
+ the wizard's; the answers stay the caller's, because every product's are
73
+ shaped differently and a shell that owned them would have to know. The grid
74
+ states "pick one" or "pick any" in ARIA rather than implying it, and a
75
+ capped multi-select disables the rest instead of hiding them, so the grid
76
+ does not reflow under a finger.
77
+ - `OrderSummary`, `OptionCard`, `ActionBar` — the checkout, in three parts.
78
+ Every figure is a `ReactNode` the caller already formatted: money is the last
79
+ thing a UI kit should be rounding, and a component taking numbers would have
80
+ to guess a currency, a tax rule and a digit shape. `OptionCard` keeps a real
81
+ radio inside the plate, so a set of payment methods or saved addresses is a
82
+ real radio group with arrow-key movement and a name that submits.
83
+ - `TaskProgress` — a long job with named stages, from the scan pipeline. The
84
+ stages are the point: a percentage tells someone how long to wait, a named
85
+ stage tells them which part failed, which is the difference between "try
86
+ again" and "try again in daylight". Failure is a state of a step, not a
87
+ replacement for the list.
88
+
89
+ **From the admin**
90
+
91
+ - `AdminShell` — grouped sidebar, topbar, one scrolling workspace. The
92
+ counterpart to `AppShell` rather than a variant of it: a console is
93
+ desktop-first, two-column and deeply nested; a phone app is one column with
94
+ five destinations, and sharing a component would make every screen carry the
95
+ other's assumptions. Below 60rem the same sidebar becomes a drawer — the same
96
+ markup, so the two cannot drift. The breakpoint is a **container** query, so
97
+ a console embedded in a panel answers its own width rather than the window's.
98
+ - `DetailHeader` — where a record sits, what it is, and what can be done to it.
99
+ The actions are at the top, because an operator working a queue acts without
100
+ reading the whole record and a button under a thousand rows is a button
101
+ nobody finds. The status badge sits beside the heading, never inside it: an
102
+ `<h1>` that swallows a badge is a heading whose name is "Payment #48210
103
+ Awaiting review".
104
+ - `FilterChips` — the filters narrowing a table, each removable, each naming
105
+ what it removes. It renders nothing when there are none rather than reserving
106
+ an empty strip.
107
+ - `ActivityFeed` — who did what, with absolute timestamps. An audit trail is
108
+ read to reconstruct a sequence, and a relative time that keeps moving is
109
+ exactly what you cannot compare two of.
110
+
111
+ ---
112
+
113
+ ## What changed in 0.5
114
+
115
+ 0.5 does two things: it makes the flows **mountable** rather than composable-in-
116
+ principle, and it adds the one surface the kit had no answer for at all — a
117
+ conversation.
118
+
119
+ **Flows you can mount**
120
+
121
+ 0.3 shipped the parts of a sign-in; a product still had to write the step
122
+ machine, the countdown and the error states itself, which is exactly the code
123
+ that gets written differently in every app and wrong in most of them. 0.5 ships
124
+ the assembled screens, each holding its own state:
125
+
126
+ - `SignInFlow` — phone → code, or email → password, with the resend countdown,
127
+ the loading and error states and the step machine already wired. Hand it three
128
+ async functions; `onVerifyCode` resolving *is* success, and routing stays the
129
+ app's, made in one place instead of at five exits.
130
+ - `PlanPicker` — billing period, the tiers, one action. Keeps the cycle and the
131
+ prices in step and hands `onSubmit` the pair a checkout needs. It takes no
132
+ payment handler: a plan picker that also knows how to charge is two screens
133
+ welded together, and only one of them is the same across products.
134
+ - `ProfileScreen` — the identity plate plus grouped rows declared as *data*: a
135
+ label, an icon, and either somewhere to go or something to toggle.
136
+ - `AppShell` — sticky header, one scroll region, the tab bar, and content that
137
+ reserves the room the floating bar covers.
138
+
139
+ The parts they are built from (`AuthScreen`, `PhoneField`, `OtpInput`,
140
+ `PasswordField`, `PlanCard`, `ProfileHeader`, `TabBar`) are unchanged and still
141
+ exported: use them directly when a product's flow differs — an invite-code step,
142
+ a captcha, a tenant picker. The assembled component is the common shape, not the
143
+ only one.
144
+
145
+ **Chat**
146
+
147
+ `Chat` + `ChatComposer` carry text, voice, video, images and files.
148
+
149
+ - Media plays in the platform's own elements. `<audio>` gives a voice message a
150
+ decoder, the OS media keys and playback that survives a backgrounded tab;
151
+ `<video controls>` brings picture-in-picture, captions and AirPlay. Only the
152
+ transport around them is drawn — the waveform is a scrub bar over a real
153
+ control, not a replacement for one.
154
+ - Voice is recorded in place with `MediaRecorder` over `getUserMedia`. Where
155
+ either is missing — an old browser, an insecure origin — the button is not
156
+ rendered rather than offered and then failing, and the stream's tracks are
157
+ stopped on every exit path including unmount, so the microphone indicator
158
+ never outlives the recording.
159
+ - The transcript follows the newest message *only when the viewer is already at
160
+ the bottom*. Yanking someone back down while they read history is the single
161
+ most common chat bug, and it is a scroll check rather than a scroll call.
162
+ - Nothing here uploads, transcodes, or holds a socket. `onSend` gets the draft
163
+ and `onFiles` gets the original `File`s, because an object URL is for showing
164
+ and a `File` is for uploading and the caller needs both.
165
+
166
+ ---
167
+
168
+ ## What changed in 0.4
169
+
170
+ 0.4 answers the one limitation 0.2 and 0.3 both shipped with: the calendar was
171
+ Gregorian, and an Iranian product had to build its own. `Calendar`, `DateField`
172
+ and the new `DatePicker` now draw the **Jalali** calendar as readily as the
173
+ Gregorian one, following the locale unless told otherwise.
174
+
175
+ - `lib/calendars.ts` — calendar arithmetic for both systems, with no table and
176
+ no leap rule: `Intl` is the source of truth and the inverse is corrected
177
+ against it. See [the Jalali calendar](#the-jalali-calendar) for why, and for
178
+ the range it was verified over.
179
+ - `Calendar` and `DateField` take `system="persian" | "gregory"`. The formatter
180
+ no longer pins `gregory` — it could not before, because the grid was Gregorian
181
+ and an `fa` label would have contradicted it. Now they agree either way.
182
+ - `DatePicker` — the compact form generalised from `iranianlawclub-web`'s Jalali
183
+ picker: one field, the month behind a button, a clear control, and the other
184
+ calendar's reading under it. Use it in a form; `DateField` is for the screen
185
+ whose subject is the date.
186
+ - Typed entry stays platform-first where the platform has something to offer,
187
+ and is a validated text field where it does not.
188
+
189
+ No token changed and nothing was removed, so 0.3 → 0.4 is a version bump. The
190
+ kit still has one runtime dependency: `react-aria-components` and
191
+ `@internationalized/date`, which the source picker used, are not part of it.
192
+
193
+ ---
194
+
195
+ ## What changed in 0.3
196
+
197
+ 0.3 adds the **flows** layer: the screens every product in the family rebuilds
198
+ by hand on day one, generalised out of `vlora-app` and put behind the same
199
+ contract as everything else. No token changed, nothing was removed, and no
200
+ runtime dependency was added — upgrading from 0.2 is a version bump.
201
+
202
+ **Sign-in**
203
+
204
+ - `PhoneField` — a country picker welded to a number input, covering every ISO
205
+ 3166-1 country and territory. The table carries only the ISO code and the
206
+ dialling code; the name comes from `Intl.DisplayNames` in the viewer's locale
207
+ (so a Persian page lists «آلمان»), and the flag is derived from the code's
208
+ regional indicators rather than shipped as 250 images. Country and national
209
+ digits are separate props: a field owning one E.164 string has to re-parse it
210
+ on every keystroke to know which flag to draw. `toE164(country, national)`
211
+ does the joining.
212
+ - `OtpInput` — the boxed code. One `<input>` per digit but a single string in
213
+ the caller's state, so a keystroke, a paste and an SMS autofill take the same
214
+ path and cannot disagree. Pinned `dir="ltr"` even in a Persian page, and
215
+ Persian and Arabic-Indic digits are normalised to ASCII on the way in.
216
+ - `PasswordField` — reveal toggle and an optional strength meter. Revealing is a
217
+ real `type` swap, so a password manager still sees a password field. Scoring
218
+ stays the caller's: a meter that disagrees with the server's policy is worse
219
+ than none. `scorePassword` is the default for products without one.
220
+ - `AuthScreen` — the frame all three steps share, which is what makes them read
221
+ as one screen changing rather than three screens, and puts the CTA in the
222
+ place a thumb has already learned.
223
+
224
+ **The rest of the first session**
225
+
226
+ - `Onboarding` — the three-screen intro: art, a promise chip, a title that
227
+ breaks where the copy says it does, dots that are also controls, and one CTA
228
+ that advances. `onDone` fires from finish and from skip, so the caller routes
229
+ in one place.
230
+ - `TabBar` — the floating bottom navigation, with an optional lifted centre
231
+ action. It renders real links or buttons with `aria-current`; routing stays
232
+ outside via `renderItem`, which is why the kit still ships no router.
233
+ - `PlanCard` — one subscription tier as the control itself, with included,
234
+ pending and excluded features all shown. Prices are `ReactNode`: currency and
235
+ digit shaping are the product's locale decision, and a kit that formatted them
236
+ would be wrong in Persian first.
237
+ - `AvatarRing` and `ProfileHeader` — an avatar wearing a progress ring, and the
238
+ identity plate above a profile's sections.
239
+
240
+ ---
241
+
242
+ ## What changed in 0.2
243
+
244
+ 0.2 is a finish pass, not a new architecture: the token contract, the thin
245
+ components and the platform-first rule are unchanged. What it fixes is the
246
+ craft, plus the components a product runs out of on its first screen.
247
+
248
+ **Accessibility**
249
+
250
+ - Keyboard focus is now visible on every focusable surface. `--nim-shadow-focus`
251
+ was defined by every theme and consumed by exactly one rule, so tabbing
252
+ through a nim screen showed nothing.
253
+ - `IconButton` at 36px keeps a 44px target. The contract already said 44px is
254
+ "never reduced, only visually inset"; the small variant did not honour it.
255
+ - `prefers-reduced-motion` is honoured — see the accessibility floor above for
256
+ why three animations deliberately survive it.
257
+
258
+ **Contract**
259
+
260
+ - Nine sizing literals (`6px` dots, avatar sizes, the progress track, the sheet
261
+ handle, spinner sizes, a `2px` subtitle margin) moved out of `components.css`
262
+ and into `--nim-size-*`. A literal there is a decision a theme cannot answer.
263
+ - `--nim-accent-hover` and `--nim-danger-hover` are new rungs, so every emphasis
264
+ hovers along its own tone ramp. `filter: brightness(0.92)` — the one hover no
265
+ theme could answer, and which inverted in dark — is gone.
266
+ - `--nim-type-control-*` splits control text off the label role. A ledger button
267
+ set in 12px tracked mono read as a caption; labels keep that voice, controls
268
+ no longer borrow it.
269
+ - `--nim-leading-base` is new, and the leading rungs are now ordered in every
270
+ preset. `vlora`'s `tight` (1.62) used to be looser than `ledger`'s `relaxed`
271
+ (1.66), so a component asking for tight leading got opposite intent depending
272
+ on the active theme.
273
+
274
+ **Craft**
275
+
276
+ - Press is one composite applied identically to buttons, icon buttons, rows and
277
+ cards. Interactive cards previously applied `scale` only, which is `1` on the
278
+ ledger presets — the largest tap target in the kit answered a press with
279
+ nothing.
280
+ - Primary hovers to `--nim-ink-secondary` instead of jumping to the accent,
281
+ which changed hue under the pointer and made primary and accent identical at
282
+ the moment of choosing between them.
283
+ - The selected segment takes a border on four sides. `--nim-shadow-sm` is a
284
+ bottom hairline on the ledger presets, so the selection read as an underline.
285
+ - Dark is rebuilt around visibility rather than symmetry with light: the line
286
+ ramp lifts (this is a hairline theme — rules are load-bearing), offset shadows
287
+ are drawn in true black rather than in the line colour, and
288
+ `--nim-surface-muted` no longer equals `--nim-surface`, which had made a
289
+ hovered row invisible. The root dark fallback also stopped relying on a
290
+ hand-maintained theme exclusion list that had already fallen a theme behind.
291
+ - `Stat` and table figures set in tabular numerals.
292
+
293
+ **Two axes instead of four themes**
294
+
295
+ `data-nim-theme` is gone, replaced by `data-nim-style` (`ledger`, `vlora`) and
296
+ `data-nim-colorway` (`vermilion`, `oxblood`, `coral`, `teal`) — see the section
297
+ above for why. `NimProvider` takes `defaultStyle` and `defaultColorway`;
298
+ `useNim()` returns `style` / `colorway` / `setStyle` / `setColorway`. Every
299
+ palette value is unchanged; `oxblood` went from 220 lines to 6, and the
300
+ stylesheet lost 15% of its weight to the dark blocks that no longer need
301
+ duplicating.
302
+
303
+ **New**
304
+
305
+ Eleven components: `Dialog`, `Menu`, `Popover`, `Tooltip`, `Tabs`, `Table`,
306
+ `Combobox`, `DateField` / `Calendar`, `Stepper`, `Pagination`, `Breadcrumb`.
307
+ Plus `--nim-density`, and `forwardRef` on `Button` and `IconButton` — overlays
308
+ anchor to their trigger, and no component forwarded a ref before.
309
+
310
+ ### Upgrading
311
+
312
+ One breaking change, mechanical:
313
+
314
+ ```diff
315
+ - <NimProvider defaultTheme="vlora" defaultScheme="dark">
316
+ + <NimProvider defaultStyle="vlora" defaultColorway="coral" defaultScheme="dark">
317
+ ```
318
+
319
+ `ledger` → style `ledger` + colourway `vermilion`; `oxblood` → `ledger` +
320
+ `oxblood`; `vlora` → `vlora` + `coral`; `fatemifar` → `vlora` + `teal` plus the
321
+ font override shown above. Any markup setting `data-nim-theme` by hand sets the
322
+ two attributes instead. `useNim().theme` / `setTheme` become `style` /
323
+ `colorway` and their setters.
324
+
325
+ Nothing else was removed, so the rest is a visual review. Look at: buttons and segmented options (larger, set in
326
+ the sans rather than the mono on the ledger presets), anything relying on
327
+ primary's hover turning accent, and any app that set `--nim-leading-tight`
328
+ expecting `vlora`'s old 1.62.
329
+
330
+ A deliberate omission: the command palette shown in the 0.2 design review is not
331
+ in this release. It is app-shaped — it has to know the whole product's actions —
332
+ and composes from `Dialog` and `Combobox` in the meantime.
333
+
334
+ ---
335
+
336
+ ## Principles
337
+
338
+ 1. **Tokens are the system.** A colour, radius, shadow, or type value may appear
339
+ in exactly one place: a theme file. A literal in `components.css` is a bug —
340
+ it is a decision that escaped the contract.
341
+ 2. **Semantic names only.** `--nim-accent`, never `--nim-orange`. `--nim-surface`,
342
+ never `--nim-gray-100`. Names describe the role, so a theme can answer them
343
+ however it wants.
344
+ 3. **Thin components.** A component maps props to class names and renders the
345
+ right element. It does not hold styles, and it does not hold layout opinions
346
+ about the page around it.
347
+ 4. **The platform first.** Checkboxes are `<input>`, tabs are `role="tablist"`,
348
+ a row that does something is a `<button>` or an `<a>`. Behaviour that the
349
+ browser already gets right is never re-implemented.
350
+ 5. **Logical properties only.** No `left`/`right`. RTL therefore needs no mirror
351
+ stylesheet — direction is a single `dir` attribute.
352
+ 6. **Spacing belongs to the page.** No component sets outer margin. `Stack` and
353
+ `Inline` express rhythm at the call site.
354
+
355
+ ---
356
+
357
+ ## Architecture
358
+
359
+ ```
360
+ src/
361
+ theme/
362
+ contract.css the vocabulary — invariants, the scheme switch, and the
363
+ two checklists a style and a colourway must answer
364
+ styles/ledger.css style · square, hairline, hard offset, mono labels
365
+ styles/vlora.css style · rounded, soft elevation, sentence-case labels
366
+ colorways/paper.css neutrals shared by vermilion + oxblood
367
+ colorways/vermilion.css print vermilion (default)
368
+ colorways/oxblood.css wax-seal red (6 declarations)
369
+ colorways/coral.css warm cream + coral
370
+ colorways/teal.css clinical teal
371
+ reset.css scoped to .nim-root, never global
372
+ components.css the only file that draws anything
373
+ index.css import entry (order is load-bearing)
374
+ components/ one file per component, thin by construction
375
+ lib/ cn() and useAnchor(), the kit's only helpers
376
+ index.ts the public surface
377
+ docs/ the gallery — the kit's first consumer
378
+ ```
379
+
380
+ Import order in `index.css` matters: contract → styles → colourways → reset →
381
+ components. The reset is applied inside `.nim-root` so nim can live beside
382
+ another design system.
383
+
384
+ ---
385
+
386
+ ## Two axes: style and colourway
387
+
388
+ nim separates **how an interface is shaped** from **how it is coloured**, and
389
+ they are set independently.
390
+
391
+ A **style** owns shape, elevation geometry, type voice and press. A
392
+ **colourway** owns surfaces, ink, lines, accent, status, and the tint the
393
+ style's shadows are drawn in. Neither knows anything about the other: a style
394
+ names no colour, and a colourway names no radius.
395
+
396
+ | Styles | `ledger` (default) | `vlora` |
397
+ |---|---|---|
398
+ | Shape | `0` — square | `6–24px` — rounded |
399
+ | Elevation | hard offset register mark | soft ambient shadow |
400
+ | Labels | mono, uppercase, wide-tracked | text face, sentence case |
401
+ | Leading | tight (1.45 base) | loose (1.84 base) — Persian needs the room |
402
+ | Press | shifts into its shadow | compresses |
403
+ | Default face | Geist / Geist Mono | Vazirmatn |
404
+
405
+ | Colourways | `vermilion` (default) | `oxblood` | `coral` | `teal` |
406
+ |---|---|---|---|---|
407
+ | Voice | print & record | law & institution | warm consumer product | clinical care |
408
+ | Canvas | warm paper `#f7f4ee` | warm paper `#f7f4ee` | warm cream `#faf9f6` | cool mist `#f6faf9` |
409
+ | Ink | near-black `#17150f` | near-black `#17150f` | slate `#131314` | near-black `#1d1d1f` |
410
+ | Accent | vermilion `#b82f18` | seal red `#6b1f2a` | coral `#d97757` | teal `#00baba` |
411
+
412
+ ```tsx
413
+ import { NimProvider } from '@nim.zone/ui' // the stylesheet comes with the import
414
+
415
+ <NimProvider defaultStyle="ledger" defaultColorway="oxblood">
416
+ <App />
417
+ </NimProvider>
418
+ ```
419
+
420
+ `NimProvider` writes `data-nim-style` / `data-nim-colorway` / `data-nim-scheme`
421
+ / `dir` onto both its own wrapper and `<html>`, so portalled surfaces — sheets,
422
+ dialogs, menus, toasts — inherit them from outside the React tree.
423
+
424
+ The pairings that carry a product's identity are `ledger` + `vermilion` (nim
425
+ itself), `ledger` + `oxblood` (legal), `vlora` + `coral` (Vlora), and `vlora` +
426
+ `teal` (Fatemifar) — but the axes are genuinely orthogonal, so `ledger` + `teal`
427
+ is a legal thing to try rather than a mistake.
428
+
429
+ ### Why two axes rather than more presets
430
+
431
+ Before 0.2 these were four self-contained themes. `oxblood` was 220 lines that
432
+ duplicated **98 identical tokens in order to change 6** — its accent family —
433
+ and `fatemifar` was mostly `vlora` with the neutrals rotated toward its accent.
434
+ Every new palette meant a new copy of the whole contract, and every structural
435
+ fix had to be applied four times or silently skip a preset. Splitting the axes
436
+ made `oxblood` six declarations.
437
+
438
+ ### Schemes
439
+
440
+ Every colour in a colourway is a `light-dark()` pair, and `color-scheme` picks
441
+ a side. So a colourway is one block: no duplicated dark rule, no
442
+ `prefers-color-scheme` query per palette, and no hand-maintained exclusion list
443
+ to fall behind — which is exactly how a preset ended up inheriting another's
444
+ dark palette before 0.2.
445
+
446
+ `defaultScheme` takes `light`, `dark`, or `system`. `system` sets no attribute
447
+ at all and lets the OS decide.
448
+
449
+ ### Fonts
450
+
451
+ The typeface belongs to the style, and an app with its own brand face overrides
452
+ it on the provider — the font file is a product asset the app already ships, so
453
+ nim owns the vocabulary rather than the face:
454
+
455
+ ```tsx
456
+ <NimProvider defaultStyle="vlora" defaultColorway="teal"
457
+ style={{ '--nim-font-sans': "'YekanBakh', 'Vazirmatn', system-ui, sans-serif" }}>
458
+ ```
459
+
460
+ **Vazirmatn** — the Persian face this repo's Farsi products already use — ships
461
+ as an optional stylesheet, because a stylesheet that requests font files the
462
+ host does not serve produces 404s and a flash of fallback:
463
+
464
+ ```tsx
465
+ import 'nim/fonts.css' // then serve the three subsets at /fonts/
466
+ ```
467
+
468
+ It declares one variable file per subset (arabic, latin-ext, latin) at weight
469
+ 100–900, the same three files `vlora-app`, `vlora-web`, `vlora-admin` and
470
+ `iranianlawclub-web` already serve from `public/fonts/`. Both styles already
471
+ name `Vazirmatn` in their stack — `ledger` after Geist, `vlora` first — so
472
+ Persian text falls through to it as soon as it loads, and Latin text does not
473
+ move.
474
+
475
+ ---
476
+
477
+ ## RTL and Persian
478
+
479
+ Direction and language are separate settings, and nim treats them that way:
480
+ `dir` says which way the line runs, `lang` says which script is being set. Only
481
+ the second implies typographic corrections, because an RTL page of Latin text
482
+ wants none of them.
483
+
484
+ ```tsx
485
+ <NimProvider direction="rtl" locale="fa-IR">
486
+ ```
487
+
488
+ **Layout** needs nothing: the whole component layer is written in logical
489
+ properties, so there is no mirror stylesheet.
490
+
491
+ **Directional icons** mirror; the rest do not. A "forward" arrow points left in
492
+ Persian, but a checkmark and a plus mean the same thing in both directions.
493
+ Which glyphs are directional is decided once, in `components/icon.tsx`, rather
494
+ than per component — mirroring whole SVG subtrees per component is how RTL
495
+ interfaces end up with backwards checkmarks.
496
+
497
+ **Script corrections** live in `theme/persian.css` and key off `lang`, not
498
+ `dir`. Two of nim's type tokens are actively harmful to a joined script and
499
+ neither is a style's fault — they are correct for Latin:
500
+
501
+ - `--nim-label-tracking` is 0.12em on the ledger style. Tracking a Persian word
502
+ does not space it out, it breaks the joins.
503
+ - negative tracking on display and title sizes does the same thing more subtly.
504
+
505
+ So under `lang="fa"` the tracking tokens go to zero, `text-transform` is
506
+ dropped, `font-feature-settings: 'calt' 1, 'kern' 1, 'ss01' 1` is turned on, and
507
+ the ledger style's Latin-tuned leading is loosened to the room Persian needs.
508
+ The `vlora` style already builds all of this in.
509
+
510
+ **Formatting.** `locale` reaches components through `useNim()`, so `Calendar`
511
+ takes its month names, weekday names, week start (Saturday for `fa`) and digits
512
+ from it rather than hardcoding English and `0–9`.
513
+
514
+ ### The Jalali calendar
515
+
516
+ `Calendar`, `DateField` and `DatePicker` draw either calendar. Unset, the system
517
+ follows the locale — an `fa` interface gets Jalali months, Persian digits and a
518
+ week that starts on Saturday; everything else gets Gregorian — and `system` pins
519
+ it explicitly:
520
+
521
+ ```tsx
522
+ <DatePicker label="تاریخ جلسه" value={hearing} onChange={setHearing} /> // Jalali under fa
523
+ <DatePicker label="Hearing" system="gregory" value={h} onChange={setH} /> // pinned
524
+ ```
525
+
526
+ **The value never changes.** An `IsoDate` is the Gregorian `YYYY-MM-DD` in both
527
+ systems: the calendar is what the viewer reads, not what the API receives. A
528
+ picker on the Jalali calendar shows the Gregorian equivalent under the field —
529
+ the reconciliation an Iranian office does by hand all day — and it is the same
530
+ date, not a second value.
531
+
532
+ `lib/calendars.ts` holds no conversion table and no leap-year rule, because the
533
+ platform already ships one: `Intl` with `-u-ca-persian` is ICU's Persian
534
+ calendar. The hard direction (Gregorian to Jalali) is asked of `Intl`; the easy
535
+ one is a mean-year estimate corrected against that same answer until it
536
+ round-trips. Month lengths are *measured* — the distance to the first of the
537
+ next month — so an Esfand of 30 days needs no special case, and nothing here
538
+ goes stale in 1408. Every day from 1900 to 2100 round-trips exactly, and the
539
+ leap years it produces are the known Jalali set.
540
+
541
+ Typed entry differs by system on purpose. Gregorian is `<input type="date">`:
542
+ the mobile date keyboard, the locale's field order and form validation come
543
+ from the platform. No browser ships a Jalali date input, so that side is a text
544
+ field reading `۱۴۰۴/۰۶/۰۱` which accepts Persian digits and commits only what
545
+ round-trips through ICU.
546
+
547
+ ### Adding a style or a colourway
548
+
549
+ Copy the nearest neighbour and answer its half of the checklist at the bottom of
550
+ `contract.css` — the list is split into "required of a style" and "required of a
551
+ colourway". Add the id to `NimStyle` or `NimColorway`. Nothing else changes: no
552
+ component, no class name, no markup. A colourway sharing an existing neutral set
553
+ adds itself to the grouped selector in `colorways/paper.css` and states only its
554
+ accent, which is all `oxblood` is.
555
+
556
+ ---
557
+
558
+ ## Components
559
+
560
+ | Group | Exports |
561
+ |---|---|
562
+ | Actions | `Button` · `IconButton` |
563
+ | Content | `Card` · `Badge` · `Stat` · `ResourceMeter` (measured or capacity-only) · `Avatar` · `SectionHeader` |
564
+ | Forms | `Input` · `Textarea` · `Select` · `Checkbox` · `Switch` · `Slider` · `Segmented` · `Combobox` · `DateField` / `DatePicker` / `Calendar` · `Stepper` |
565
+ | Collections | `List` · `ListRow` · `Table` |
566
+ | Navigation | `Tabs` · `Breadcrumb` · `Pagination` · `TabBar` |
567
+ | Overlays | `Sheet` · `Dialog` · `Menu` · `Popover` · `Tooltip` |
568
+ | Feedback | `Banner` · `EmptyState` · `Spinner` · `Progress` · `Skeleton` · `ToastProvider` / `useToast` |
569
+ | Type | `Display` · `Title` · `Body` · `Label` · `Caption` · `Rule` |
570
+ | Layout | `AppFrame` · `Stack` · `Inline` · `AdminShell` · `DetailHeader` · `FilterChips` · `ActivityFeed` |
571
+ | Flows | `Onboarding` · `SignInFlow` · `Wizard` · `PlanPicker` · `ProfileScreen` · `AppShell` · `TaskProgress` |
572
+ | Flow parts | `AuthScreen` · `PhoneField` · `OtpInput` · `PasswordField` · `PlanCard` · `ProfileHeader` · `AvatarRing` · `ChoiceGrid` · `OptionCard` |
573
+ | Commerce | `OrderSummary` · `ActionBar` |
574
+ | Console | `AdminShell` · `DetailHeader` · `FilterChips` · `ActivityFeed` |
575
+ | Chat | `Chat` · `ChatComposer` |
576
+ | System | `NimProvider` · `useNim` · `useSchemeToggle` · `Icon` / `iconNames` · `cn` · `COUNTRIES` / `countryByIso2` / `countryByDial` / `countryNamer` / `toAsciiDigits` · `toE164` · `scorePassword` |
577
+
578
+ Picking between the near-neighbours:
579
+
580
+ - **`Tabs` vs `Segmented`** — tabs switch a *region* of the page; a segmented
581
+ control sets a *value*. They look alike and mean different things.
582
+ - **`Menu` vs `Popover`** — a menu holds actions and closes when one is chosen;
583
+ a popover holds a form and does not close on a click inside it.
584
+ - **`AdminShell` vs `AppShell`** — a console and a phone app, not two sizes of
585
+ one thing: two columns and a deep hierarchy against one column and five
586
+ destinations.
587
+ - **`Wizard` vs `Onboarding`** — the wizard collects answers and gates its CTA
588
+ on them; onboarding shows three slides and asks for nothing.
589
+ - **`OptionCard` vs `PlanCard`** — a row-shaped choice among several (payment
590
+ method, address) against a tier with a price and a feature list.
591
+ - **`SignInFlow` vs `AuthScreen`** — the flow is the screen, mounted and
592
+ stateful; `AuthScreen` is the frame one step is drawn in, for a product whose
593
+ sign-in has more steps than these.
594
+ - **`DateField` vs `DatePicker`** — the field keeps the month open and belongs
595
+ on a screen whose subject is the date; the picker hides it behind a button and
596
+ belongs in a form where three other fields need the space.
597
+ - **`TabBar` vs `Tabs`** — the tab bar is the app's destinations and lives at
598
+ the bottom of the frame; `Tabs` switches a region inside one screen.
599
+ - **`Dialog` vs `Sheet`** — the sheet is the mobile-first modal surface; the
600
+ dialog is the centred one, and renders a real `<dialog>` so the top layer,
601
+ the focus trap and Escape come from the platform.
602
+
603
+ Icons are addressed by **role**, not by vendor name (`<Icon name="trash" />`).
604
+ The registry in `components/icon.tsx` is the whole point: it keeps the set
605
+ finite and reviewable, stops two screens meaning "delete" with two glyphs, and
606
+ makes swapping icon libraries a one-file change.
607
+
608
+ ### Accessibility floor
609
+
610
+ Every interactive element ships a hover, a press, a focus ring drawn outside its
611
+ box, a disabled state, and a 44px minimum target — including `IconButton` at its
612
+ 36px size, which keeps the box and restores the target with a transparent
613
+ `::after`. `IconButton` requires a `label`. Overlays share one dismissal
614
+ contract: Escape closes, an outside pointer closes, and focus returns to
615
+ whatever opened them. Form controls wire label/hint/error ids to the control
616
+ automatically, and an invalid field focuses in danger so the ring never
617
+ contradicts the message under it.
618
+
619
+ `prefers-reduced-motion` is honoured, but not by stopping everything: the
620
+ spinner keeps turning more slowly and the indeterminate progress bar fills
621
+ instead of sliding, because those two are the only signal that work is
622
+ happening. Reduced motion is a vestibular accommodation, not a request for less
623
+ information.
624
+
625
+ ### Density
626
+
627
+ `--nim-density` is one multiplier over the control scale and the block padding
628
+ of anything row-shaped, so a data-dense screen and a mobile flow stay the same
629
+ system:
630
+
631
+ ```tsx
632
+ <div style={{ '--nim-density': 0.82 }}>…</div> // compact · 36px controls
633
+ <div>…</div> // default · 44px
634
+ <div style={{ '--nim-density': 1.18 }}>…</div> // roomy · 52px
635
+ ```
636
+
637
+ It never scales type, and it never crosses `--nim-touch-min`. The multiplication
638
+ is applied where each height is *used* rather than folded into
639
+ `--nim-control-md`: a custom property that references another is substituted
640
+ where it is declared, so baking density into the token would freeze it at the
641
+ root and make a subtree override do nothing.
642
+
643
+ ---
644
+
645
+ ## Using it in an app
646
+
647
+ ```tsx
648
+ import { Button, Card, Stack, Stat, Title, NimProvider, ToastProvider } from '@nim.zone/ui'
649
+
650
+ export function Screen() {
651
+ return (
652
+ <NimProvider defaultStyle="ledger" defaultColorway="vermilion">
653
+ <ToastProvider>
654
+ <Stack gap="loose">
655
+ <Title>Today</Title>
656
+ <Card variant="raised">
657
+ <Stat value="18M" unit="/min" label="Events" delta="+12%" />
658
+ </Card>
659
+ <Button iconEnd="arrow-forward">Continue</Button>
660
+ </Stack>
661
+ </ToastProvider>
662
+ </NimProvider>
663
+ )
664
+ }
665
+ ```
666
+
667
+ ### Adopting nim in `vlora-app`
668
+
669
+ The `vlora` style and `coral` colourway carry that app's exact palette, radii,
670
+ shadows, and type voice, so adoption is mechanical rather than a restyle:
671
+
672
+ 1. Wrap the tree in `<NimProvider defaultStyle="vlora" defaultColorway="coral"
673
+ direction="rtl">` — the stylesheet arrives with the first `nim` import.
674
+ 2. Repoint `src/components/ui/index.ts` at `nim` re-exports, one component at a
675
+ time — the prop APIs were modelled on Vlora's own.
676
+ 3. Delete the corresponding blocks from `src/theme/tailwind.css` as each
677
+ component moves over.
678
+ 4. Keep app-specific surfaces (scanner, mascot, reflect flow) in the app. nim
679
+ owns the shared vocabulary, not the product's own domain UI.
680
+
681
+ Nothing in `vlora-app` has been modified by this package.