@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.
- package/LICENSE +21 -0
- package/README.md +681 -0
- package/dist/components/admin-shell.d.ts +121 -0
- package/dist/components/app-shell.d.ts +23 -0
- package/dist/components/auth-screen.d.ts +33 -0
- package/dist/components/avatar.d.ts +10 -0
- package/dist/components/badge.d.ts +14 -0
- package/dist/components/banner.d.ts +11 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/button.d.ts +21 -0
- package/dist/components/card.d.ts +15 -0
- package/dist/components/chat-composer.d.ts +52 -0
- package/dist/components/chat.d.ts +77 -0
- package/dist/components/checkout.d.ts +79 -0
- package/dist/components/choice.d.ts +15 -0
- package/dist/components/combobox.d.ts +31 -0
- package/dist/components/date-field.d.ts +58 -0
- package/dist/components/dialog.d.ts +20 -0
- package/dist/components/empty-state.d.ts +9 -0
- package/dist/components/feedback.d.ts +18 -0
- package/dist/components/field.d.ts +47 -0
- package/dist/components/icon-button.d.ts +12 -0
- package/dist/components/icon.d.ts +72 -0
- package/dist/components/layout.d.ts +13 -0
- package/dist/components/list.d.ts +16 -0
- package/dist/components/menu.d.ts +56 -0
- package/dist/components/onboarding.d.ts +50 -0
- package/dist/components/otp-input.d.ts +29 -0
- package/dist/components/pagination.d.ts +12 -0
- package/dist/components/password-field.d.ts +33 -0
- package/dist/components/phone-field.d.ts +44 -0
- package/dist/components/plan-card.d.ts +45 -0
- package/dist/components/plan-picker.d.ts +52 -0
- package/dist/components/profile-header.d.ts +47 -0
- package/dist/components/profile-screen.d.ts +44 -0
- package/dist/components/resource-meter.d.ts +22 -0
- package/dist/components/section-header.d.ts +8 -0
- package/dist/components/segmented.d.ts +18 -0
- package/dist/components/sheet.d.ts +18 -0
- package/dist/components/sign-in-flow.d.ts +60 -0
- package/dist/components/slider.d.ts +10 -0
- package/dist/components/stat.d.ts +10 -0
- package/dist/components/stepper.d.ts +17 -0
- package/dist/components/tab-bar.d.ts +40 -0
- package/dist/components/table.d.ts +33 -0
- package/dist/components/tabs.d.ts +23 -0
- package/dist/components/task-progress.d.ts +38 -0
- package/dist/components/theme.d.ts +49 -0
- package/dist/components/toast.d.ts +17 -0
- package/dist/components/tooltip.d.ts +18 -0
- package/dist/components/typography.d.ts +18 -0
- package/dist/components/wizard.d.ts +70 -0
- package/dist/index.d.ts +104 -0
- package/dist/lib/calendars.d.ts +81 -0
- package/dist/lib/cn.d.ts +7 -0
- package/dist/lib/countries.d.ts +33 -0
- package/dist/lib/use-anchor.d.ts +22 -0
- package/dist/nim.css +1 -0
- package/dist/nim.js +3266 -0
- package/package.json +83 -0
- package/src/components/admin-shell.tsx +338 -0
- package/src/components/app-shell.tsx +36 -0
- package/src/components/auth-screen.tsx +71 -0
- package/src/components/avatar.tsx +32 -0
- package/src/components/badge.tsx +44 -0
- package/src/components/banner.tsx +47 -0
- package/src/components/breadcrumb.tsx +45 -0
- package/src/components/button.tsx +64 -0
- package/src/components/card.tsx +46 -0
- package/src/components/chat-composer.tsx +331 -0
- package/src/components/chat.tsx +352 -0
- package/src/components/checkout.tsx +173 -0
- package/src/components/choice.tsx +46 -0
- package/src/components/combobox.tsx +157 -0
- package/src/components/date-field.tsx +394 -0
- package/src/components/dialog.tsx +77 -0
- package/src/components/empty-state.tsx +23 -0
- package/src/components/feedback.tsx +61 -0
- package/src/components/field.tsx +177 -0
- package/src/components/icon-button.tsx +35 -0
- package/src/components/icon.tsx +167 -0
- package/src/components/layout.tsx +38 -0
- package/src/components/list.tsx +78 -0
- package/src/components/menu.tsx +172 -0
- package/src/components/onboarding.tsx +157 -0
- package/src/components/otp-input.tsx +144 -0
- package/src/components/pagination.tsx +86 -0
- package/src/components/password-field.tsx +112 -0
- package/src/components/phone-field.tsx +242 -0
- package/src/components/plan-card.tsx +124 -0
- package/src/components/plan-picker.tsx +144 -0
- package/src/components/profile-header.tsx +134 -0
- package/src/components/profile-screen.tsx +104 -0
- package/src/components/resource-meter.tsx +60 -0
- package/src/components/section-header.tsx +31 -0
- package/src/components/segmented.tsx +53 -0
- package/src/components/sheet.tsx +74 -0
- package/src/components/sign-in-flow.tsx +322 -0
- package/src/components/slider.tsx +50 -0
- package/src/components/stat.tsx +30 -0
- package/src/components/stepper.tsx +77 -0
- package/src/components/tab-bar.tsx +82 -0
- package/src/components/table.tsx +81 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/task-progress.tsx +111 -0
- package/src/components/theme.tsx +112 -0
- package/src/components/toast.tsx +87 -0
- package/src/components/tooltip.tsx +30 -0
- package/src/components/typography.tsx +65 -0
- package/src/components/wizard.tsx +192 -0
- package/src/index.ts +216 -0
- package/src/lib/calendars.ts +227 -0
- package/src/lib/cn.ts +8 -0
- package/src/lib/countries.ts +110 -0
- package/src/lib/use-anchor.ts +92 -0
- package/src/theme/colorways/coral.css +62 -0
- package/src/theme/colorways/oxblood.css +23 -0
- package/src/theme/colorways/paper.css +68 -0
- package/src/theme/colorways/teal.css +56 -0
- package/src/theme/colorways/vermilion.css +21 -0
- package/src/theme/components.css +4606 -0
- package/src/theme/contract.css +204 -0
- package/src/theme/fonts.css +67 -0
- package/src/theme/index.css +33 -0
- package/src/theme/persian.css +47 -0
- package/src/theme/reset.css +118 -0
- package/src/theme/styles/ledger.css +106 -0
- package/src/theme/styles/vlora.css +91 -0
- 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.
|