@apliteni/apliteni-ui 0.75.1 → 0.76.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/README.md CHANGED
@@ -4,288 +4,161 @@
4
4
  [![license: MIT](https://img.shields.io/npm/l/@apliteni/apliteni-ui?color=3b9dff)](./LICENSE)
5
5
  [![live: ui.apli.tech](https://img.shields.io/badge/live-ui.apli.tech-9b5dff)](https://ui.apli.tech)
6
6
 
7
- The Apliteni design system and UI kit supplies shared UI for the strategy deck,
8
- the text portal, `/account`, the operating model, and future product surfaces.
7
+ The Apliteni design system and UI kit provides shared design tokens, components, themes and
8
+ UI rules for the strategy deck, the text portal, `/account` and future product surfaces.
9
9
 
10
- It provides framework-agnostic **HTML + CSS** and **React components** for stateful
11
- surfaces, with shared tokens, dark and light themes, and **accent sub-themes**.
12
- Review components in **Storybook** at **ui.apli.tech**.
10
+ One package carries two layers. **HTML + CSS** includes token CSS, component CSS and
11
+ HTML-string factories. Use this layer for server-rendered pages that do not use a component
12
+ framework. **React components** support surfaces with client-side state, such as tables,
13
+ filters, forms and modals. Both layers use the same `.ui-*` classes and tokens, so a page
14
+ can use both. Choose React when the surface needs state.
13
15
 
14
- - **Live site + Storybook** → [ui.apli.tech](https://ui.apli.tech)
15
- - **Package** → [`@apliteni/apliteni-ui`](https://www.npmjs.com/package/@apliteni/apliteni-ui) (public npm)
16
- - **React components** → `@apliteni/apliteni-ui/react` — a subpath of the same package, source in [`react/`](./react)
17
-
18
- ## HTML + CSS *and* React
19
-
20
- The strategy portal (`apliteni/strategy`, `viz/`) server-renders HTML strings from
21
- `.mjs` modules without a component framework. The kit supplies token CSS, component
22
- CSS, and HTML-string factories that the portal can import without a rewrite.
23
- Storybook (`@storybook/html-vite`) renders those same strings.
24
-
25
- React components handle client state in dashboards, tables, filters, and forms.
26
- They use the vanilla kit's `.ui-*` classes and tokens.
27
-
28
- **Which one:** does the surface hold meaningful client state? No → the HTML-string
29
- factories below. Yes → the [React components](#react-components-stateful-surfaces).
30
-
31
- Either layer follows the same UI rules — what one page may hold, which component to reach
32
- for, the states it owes, how colour and wording work. They live in the **Guidelines** section
33
- of Storybook, which opens on
34
- [links to every guideline page](https://ui.apli.tech/storybook/?path=/story/guidelines-overview--overview).
35
- The same guidance ships as plain Markdown in
36
- `node_modules/@apliteni/apliteni-ui/guidelines/`. Start with
37
- [guidelines/overview.md](guidelines/overview.md); Storybook reads these documents too.
38
-
39
- [The page](https://ui.apli.tech/storybook/?path=/story/guidelines-the-page--the-page) is the one
40
- to read before you design a screen: the limits one page keeps, whatever it is about.
16
+ Every component is in Storybook at **[ui.apli.tech](https://ui.apli.tech)**.
41
17
 
42
18
  ## Install
43
19
 
44
- Published on the **public npm registry** — no scope config, no token:
20
+ The package is published on the public npm registry. You do not need scope configuration or
21
+ an npm token:
45
22
 
46
23
  ```bash
47
- npm install @apliteni/apliteni-ui
48
- ```
49
-
50
- ## Use it
51
-
52
- ```js
53
- import '@apliteni/apliteni-ui/css'; // once, at app root (load the two fonts too — see below)
54
- import { button, card, topbar, wireTopbar } from '@apliteni/apliteni-ui';
55
-
56
- el.innerHTML = topbar({ word: 'Strategy', account: { name, email } })
57
- + card({ title: 'Appearance', body: button({ label: 'Save', variant: 'primary' }) });
58
- wireTopbar(document); // theme toggle, menus, segmented, copy buttons
59
- ```
60
-
61
- ### The two fonts
62
-
63
- The kit names two families and bundles neither, so the host page loads them. Poppins is
64
- `--font-display` — headings, brand marks, large readouts. IBM Plex Sans is `--font-sans` —
65
- tables, fields, paragraphs, chat, which is most of an application. Weights 300-700 in both:
66
-
67
- ```html
68
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
69
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap">
70
- ```
71
-
72
- If either font is missing, that role falls back to its system stack, and nothing says so.
73
- For the old single-family appearance, set both roles to the same family in your stylesheet,
74
- after the kit's:
75
-
76
- ```css
77
- :root { --font-sans: var(--font-display); }
24
+ npm install @apliteni/apliteni-ui react react-dom
78
25
  ```
79
26
 
80
- ### Reuse the account page
27
+ Install React and React DOM yourself, using version 18 or newer. The kit does not declare
28
+ either package as a regular or peer dependency. npm therefore does not install them or warn
29
+ when they are missing. If they are missing, importing `@apliteni/apliteni-ui/react` fails
30
+ at build time or runtime with a module-not-found error. A plain HTML consumer installs only
31
+ the kit and is unaffected because React is not added to its dependency tree.
81
32
 
82
- The whole `/account` layout (topbar + sticky sidebar + page body) ships as one
83
- factory, so every product renders the same account shell instead of re-building it:
33
+ The React components are a subpath of this package, not a separate package. You get one
34
+ install, one version and one version pin.
84
35
 
85
- ```js
86
- import { accountShell, card, switchToggle, wireTopbar, wireShell } from '@apliteni/apliteni-ui';
87
-
88
- el.innerHTML = accountShell({
89
- word: 'Strategy', // the product word in the topbar
90
- account: { name, email }, // signed-in user (drives the avatar menu)
91
- active: 'prefs', // which sidebar item is current
92
- title: 'Preferences',
93
- sub: 'How the portal looks and speaks to you.',
94
- body: card({ title: 'Appearance', body: switchToggle({ label: 'Reduce motion' }) }),
95
- });
96
- wireTopbar(el); // menus, theme toggle, segmented controls
97
- wireShell(el); // the toggle that folds the rail, the reader's menu, the nav's groups
98
-
99
- // Custom sidebar nav? pass `nav: [['prefs','gear','Preferences'], ['billing','wallet','Billing']]`
100
- // A page that will never call wireShell()? pass `collapsible: false` and no toggle is drawn
101
- ```
36
+ | Import | What it is |
37
+ | --- | --- |
38
+ | `@apliteni/apliteni-ui/css` | Tokens and every component stylesheet. Import once at the app root. |
39
+ | `@apliteni/apliteni-ui` | The HTML-string factories: `button()`, `card()`, `topbar()`, `accountShell()` and the rest. |
40
+ | `@apliteni/apliteni-ui/react` | The React components, TypeScript types included. |
41
+ | `@apliteni/apliteni-ui/react/css` | Their shell styles. |
42
+ | `@apliteni/apliteni-ui/inline` | The same CSS as strings, for a server that inlines it. |
43
+ | `@apliteni/apliteni-ui/guidelines/*` | The UI rules as Markdown. |
102
44
 
103
- Server-rendered apps that inline CSS (like the strategy portal) import the stylesheet
104
- as **strings** instead:
45
+ ## Use it
105
46
 
106
47
  ```js
107
- import { tokensCss, topbarCss, cssText } from '@apliteni/apliteni-ui/inline';
108
- // …inline tokensCss + topbarCss into the <style> you serve.
109
- ```
110
-
111
- `cssText` includes every sheet. When selecting individual sheets from `styles`,
112
- `successPanel()` needs both `styles.callout` (panel layout) and `styles.success`
113
- (shared glowing check and reduced-motion styles), after `tokensCss` and `baseCss`.
114
-
115
- ## React components (stateful surfaces)
116
-
117
- `DataTable`, `Pagination`, `Modal`, `Button`, `Badge`, `Card` and `Icon` — same `.ui-*` classes,
118
- same tokens, TypeScript types included. They ship as a **subpath of this package**,
119
- not as a package of their own: one install, one version, one pin.
48
+ import '@apliteni/apliteni-ui/css';
49
+ import { topbar, card, button, wireTopbar } from '@apliteni/apliteni-ui';
120
50
 
121
- Install `react` and `react-dom`, version 18 or newer, yourself. The kit declares
122
- neither a regular nor a peer dependency on React, so npm neither installs it nor
123
- warns when it is missing:
124
-
125
- ```bash
126
- npm install @apliteni/apliteni-ui react react-dom
51
+ el.innerHTML = topbar({ word: 'Strategy', account: { name, email } })
52
+ + card({ title: 'Appearance', body: button({ label: 'Save', variant: 'primary' }) });
53
+ wireTopbar(document); // theme toggle, menus, segmented controls, copy buttons
127
54
  ```
128
55
 
129
- Without them, importing `@apliteni/apliteni-ui/react` fails at build or runtime with
130
- a module-not-found error for `react`. Plain HTML consumers can install the kit alone;
131
- the other entry points are unaffected and do not bring React into the dependency tree.
132
-
133
56
  ```tsx
134
- import '@apliteni/apliteni-ui/css'; // kit tokens + .ui-* classes
135
- import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, sort control)
57
+ import '@apliteni/apliteni-ui/css';
58
+ import '@apliteni/apliteni-ui/react/css';
136
59
  import { DataTable, Modal } from '@apliteni/apliteni-ui/react';
137
60
  ```
138
61
 
139
- The source lives in [`react/`](./react) — a private workspace with its own build
140
- (tsup) and Storybook on port 6007. When 6007 is taken Storybook moves to the next
141
- free port, so the root Storybook does not trust the number: it probes the range
142
- 6007 can drift into and composes the first port that proves it is this workspace's
143
- Storybook. A stranger on the port is never composed — the "React components"
144
- section is absent instead, and the terminal says why. Details in
145
- [`react/README.md`](./react/README.md).
62
+ The `accountShell()` factory provides the complete `/account` layout: the topbar, sticky
63
+ sidebar and page body, so no product rebuilds it. The
64
+ [component catalog](docs/library.md#component-catalog) lists this factory and every other one.
146
65
 
147
- ### Updating a busy button
66
+ ### Fonts
148
67
 
149
- Keep the factory-created element in place to animate its label:
68
+ The kit defines two font families but does not bundle them, so the host page must load them.
69
+ Poppins is `--font-display`, used for headings, brand marks and large readouts. IBM Plex Sans
70
+ is `--font-sans`, used for tables, fields, paragraphs, chat and most application text. Both
71
+ families support weights 300–700.
150
72
 
151
- ```js
152
- import { button, setButtonBusy } from '@apliteni/apliteni-ui';
153
- host.innerHTML = button({ label: 'Save changes', variant: 'primary' });
154
- const control = host.querySelector('button');
155
- setButtonBusy(control, { busy: true });
156
- // When the request completes:
157
- setButtonBusy(control, { busy: false });
73
+ ```html
74
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
75
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap">
158
76
  ```
159
77
 
160
- The helper keeps focus on the button, uses `aria-disabled` and blocks clicks,
161
- Enter and Space while busy. Its sibling polite live region announces progress and completion.
162
- Explicit `disabled: true` stays natively disabled. Static `button({ busy: true })`
163
- markup uses native disabled until the helper wires it; call `setButtonBusy` to
164
- switch to the focus-preserving behavior. React `<Button busy={saving}>Save changes</Button>` uses the same treatment.
165
- The action label slides down, three dots take its place, and the label returns from below
166
- when complete. Its hidden label preserves the width and accessible name. React retains
167
- the last ready children while busy; changed children appear on completion. Reduced
168
- motion switches immediately to static dots.
169
- See Storybook's **Button / Busy Transition** for the live vanilla example.
170
-
171
- ## Theming
172
-
173
- React forms can use [Checkbox, radio and Switch](react/README.md#checkbox-and-switch)
174
- with native labels, keyboard behavior and controlled or uncontrolled state.
78
+ If a family does not load, the browser uses its system fallback stack. The kit does not
79
+ display a message about this fallback.
175
80
 
176
- React pages can use [ThemeToggle and its pre-paint script](react/README.md#themetoggle)
177
- to save dark, light or auto choices. Auto resolves to dark or light on the root.
81
+ ## Theme and accent
178
82
 
179
- Theme is a `data-theme="dark|light"` attribute on `<html>`; accent is an orthogonal
180
- `data-accent` on top:
83
+ Set the theme on `<html>` with `data-theme="dark|light"`. You can set an accent separately
84
+ with `data-accent`:
181
85
 
182
86
  ```html
183
87
  <html data-theme="dark" data-accent="phoenix">
184
88
  ```
185
89
 
186
- Each accent re-points only the accent family (`--accent`, `--purple*`, `--glow-purple`,
187
- `--ring`, `--grad-*`). Surfaces, text and signal colours (green = live, pink = danger)
188
- stay put — so **every accent works in both themes** and every component follows with no
189
- component-level change.
190
-
191
- Focus uses a surface-coloured gap, a solid band and a decorative glow. Tune
192
- `--ring-width`, `--ring-color`, `--ring-gap-width` and `--ring-gap` at the root;
193
- `--ring` remains the composed shadow. Kit surfaces recompose it to match their
194
- background, so an ancestor's custom `--ring` must also be applied on those surfaces.
195
- The page shell inherits root overrides; it does not introduce another composition.
196
- See [the focus-ring contract](docs/specification.md#the-focus-ring).
197
-
198
- Both attributes are overrides, not requirements: with neither present the kit paints dark
199
- Nebula, and `data-accent` alone paints that accent on the dark theme. An absent `data-theme`
200
- is *not* "follow the system" — the kit ships no `prefers-color-scheme` rule, so a host that
201
- wants the OS preference resolves it in JS and stamps the attribute. See
202
- [`docs/library.md`](./docs/library.md#an-absent-attribute-means-dark).
203
-
204
- Shipped accents: **Nebula** (purple, default), **Phoenix** (ember), **Ocean** (azure),
205
- **Emerald** (jade). Runtime helpers: `applyTheme('light')` / `applyAccent('phoenix')`
206
- (both persist to `localStorage`); or the `accentPicker()` component wired by `wireTopbar()`.
207
-
208
- ## Layout
209
-
210
- ```
211
- src/
212
- tokens/tokens.css # colours, type, spacing, radius, elevation, motion — dark + light
213
- tokens/accents.css # accent sub-themes (data-accent) for both themes
214
- styles/*.css # one file per component (button, card, badge, segmented, input,
215
- # table, callout, code, topbar, layout)
216
- index.css # bundler entry — import '@apliteni/apliteni-ui/css'
217
- inline.js # CSS as strings for server-render consumers (…/inline)
218
- assets/ # brand mark (seedling) + line-icon set
219
- components/ # HTML-string factories: button(), card(), badge(), topbar()…
220
- stories/ # Storybook: Foundations, Components, Showcases
221
- site/ # ui.apli.tech landing page (static site build)
222
- react/ # React components — private workspace, built to react/dist/
223
- dist/ # tsup output; shipped as …/react and …/react/css
224
- ```
225
-
226
- `SidebarNav` renders flat items or captioned sections with one level of nested
227
- disclosure, counts, disabled and danger rows, an artwork slot, and a footer slot. Its
228
- `renderLink(item, linkProps)` hook supports router links; spread the supplied props
229
- to retain names, children and current-page semantics. `collapsed` changes only
230
- presentation, keeping nested links reachable and named.
90
+ An accent changes only the accent family: `--accent`, `--purple*`, `--glow-purple`, `--ring`
91
+ and `--grad-*`. Surfaces, text and signal colours remain unchanged. Green remains the live
92
+ colour and pink remains the danger colour. As a result, every accent works in both themes
93
+ without requiring component changes. The shipped accents are **Nebula** (purple and the
94
+ default), **Phoenix** (ember), **Ocean** (azure) and **Emerald** (jade).
95
+
96
+ Both attributes override the defaults. If neither is present, the kit uses dark Nebula. If
97
+ only `data-accent` is present, the kit uses that accent with the dark theme. An absent
98
+ `data-theme` does *not* mean “follow the system”: the kit has no `prefers-color-scheme`
99
+ rule. If the host wants to follow the operating system preference, its JavaScript must
100
+ choose the theme and add the attribute. See
101
+ [an absent attribute means dark](docs/library.md#an-absent-attribute-means-dark).
102
+
103
+ You can change the settings at runtime with `applyTheme('light')` and
104
+ `applyAccent('phoenix')`. These functions save the choices to `localStorage`. You can also
105
+ use `accentPicker()`, which `wireTopbar()` connects.
106
+
107
+ ## The rules
108
+
109
+ The **Guidelines** explain what a page may contain, which component to choose, which states
110
+ it must support, and how to handle colour and wording. Both kit layers follow these rules.
111
+ The guidelines ship as Markdown in the package and are also available as a Storybook
112
+ section.
113
+
114
+ - [guidelines/overview.md](guidelines/overview.md) — the index, and the rules the kit has yet to meet.
115
+ - [guidelines/the-page.md](guidelines/the-page.md) — read this one before designing a screen.
116
+ - [Guidelines in Storybook](https://ui.apli.tech/storybook/?path=/story/guidelines-overview--overview) — the same pages with live specimens.
117
+
118
+ ## Documentation
119
+
120
+ | Page | What it answers |
121
+ | --- | --- |
122
+ | [specification.md](docs/specification.md) | What the kit guarantees and what it refuses to do. A gate on `npm test` holds every statement. |
123
+ | [library.md](docs/library.md) | Architecture, the `src/` layout, tokens and theming, the component catalog. |
124
+ | [react/README.md](react/README.md) | The React components, their props, and the Storybook on port 6007. |
125
+ | [storybook.md](docs/storybook.md) | The workbench: config, theming toolbar, story conventions. |
126
+ | [landing-page.md](docs/landing-page.md) | ui.apli.tech: the chrome, the static build, the hosting. |
127
+ | [changelog.md](docs/changelog.md) | The changelog's data model and its Storybook deeplinks. |
231
128
 
232
129
  ## Contribute
233
130
 
234
- Use Node 20 or newer. Install [jq](https://jqlang.github.io/jq/), then run `npm ci` at
235
- the repository root. Start Storybook with `npm run storybook`. For React, use `npm run
236
- storybook -w react`. Before opening a PR, run `npm test`, `npm test -w react`, `npm run
237
- build-storybook`, and `node site/build.mjs`.
238
-
239
- Add a general, composable kit component only when all three checks are true:
240
- (1) the issue names at least two products that use it or have requested it;
241
- (2) existing components cannot provide it without copying their markup or logic;
242
- (3) it contains no domain-specific data or rules. Otherwise, build the UI by
243
- composition and add a Storybook showcase. Keep the vanilla kit’s existing look.
244
-
245
- To add a component, put its token-based CSS in `src/styles/`. Include it in
246
- `src/index.css` and in both the `styles` map and `cssText` in `src/inline.js`. Add the
247
- HTML factory under `src/components/`, export it from `src/index.js`, and add a
248
- playground and state examples under `stories/components/`. Put React components in
249
- `react/src/`, with a test and story that use the shared classes and tokens. Read the
250
- [guidelines](guidelines/overview.md) for the design rules.
251
-
252
- Open an issue, branch from `main`, and link the issue in your PR. Changes to published
253
- files, including this README, require a version bump in `package.json` and
254
- `package-lock.json`, plus an entry in `site/changelog.mjs`. Merging the bump starts the
255
- release workflow. Do not push a tag or publish by hand.
256
-
257
- [Agent rules](AGENTS.md) cover review, data handling, and release checks.
131
+ Use Node 20 or newer. Install [jq](https://jqlang.github.io/jq/), then run:
258
132
 
259
- ## Deploy (ui.apli.tech)
260
-
261
- The site is **100% static** (landing + hosted Storybook) — no container, no registry.
262
- It's served by **Lessly static hosting**, built straight from this repo. The `site`
263
- service builds from `main` with:
264
-
265
- ```
266
- npm ci && npm run build-storybook && node site/build.mjs
133
+ ```bash
134
+ npm ci
135
+ npm run storybook # the kit; add -w react for the React workspace
136
+ npm test && npm test -w react && npm run build-storybook && node site/build.mjs
267
137
  ```
268
138
 
269
- and serves `site/public/`, which includes the landing page, `/changelog`, `kit.css`,
270
- and the Storybook folded in at `/storybook`. Push to `main` and redeploy the `site`
271
- service to roll it out.
139
+ Run the final command before opening a PR.
272
140
 
273
- To reproduce the exact static bundle locally:
274
-
275
- ```bash
276
- npm ci && npm run build-storybook && node site/build.mjs
277
- # -> site/public/ (landing + /changelog + /storybook + kit.css)
278
- ```
141
+ Add a general, composable kit component only when all three conditions are true: the issue
142
+ names at least two products that use it or have requested it; existing components cannot
143
+ provide it without copying markup or logic; and it contains no domain-specific data or
144
+ rules. If any condition is missing, compose the UI from existing components and add a
145
+ Storybook showcase. Preserve the vanilla kit's existing look.
279
146
 
280
- ## Adopting into the strategy portal
147
+ To add a component, put its token-based CSS in `src/styles/`. Include that CSS in
148
+ `src/index.css` and in both the `styles` map and `cssText` in `src/inline.js`. Put the HTML
149
+ factory in `src/components/` and export it from `src/index.js`. Add a playground and state
150
+ examples in `stories/components/`. Put React components in `react/src/`; each one needs a
151
+ test and a story that use the shared classes and tokens. The
152
+ [guidelines](guidelines/overview.md) define the design rules.
281
153
 
282
- The topbar CSS keeps the **same class names** the portal already uses (`.topbar`,
283
- `.brand`, `.dtsw`, `.toggle`, `.acct`, `.amenu`), and the token names match `viz/`
284
- verbatim — so migration is subtractive: swap the inlined token/topbar CSS for the
285
- package's `tokensCss` / `topbarCss` and delete the duplication. The deck (`index.html`)
286
- stays self-contained for the claude.ai Artifact CSP, baking tokens in via its build step.
154
+ Open an issue, create a branch from `main`, and link the issue in your PR. If you change a
155
+ published file, including this README, bump the version in `package.json` and
156
+ `package-lock.json`, and add an entry to `site/changelog.mjs`. Merging that version bump
157
+ starts the release workflow. Do not push a tag or publish manually.
158
+ [AGENTS.md](AGENTS.md) describes the review, data-handling and release checks.
287
159
 
288
160
  ## License
289
161
 
290
- [MIT](./LICENSE) © Apliteni — for the **code**. The Apliteni name, logos, and brand
291
- marks are trademarks and are **not** covered by the MIT license; see [TRADEMARK.md](./TRADEMARK.md).
162
+ [MIT](./LICENSE) © Apliteni, for the **code**. The Apliteni name, logos and brand marks
163
+ are trademarks and are **not** covered by the MIT license — see
164
+ [TRADEMARK.md](./TRADEMARK.md).
@@ -37,3 +37,17 @@
37
37
  **Don't:** Make both actions primary or colour every label and value with the accent.
38
38
 
39
39
  **Except:** Keep [Badge success, pending and danger](https://ui.apli.tech/storybook/?path=/story/components-badge-status--badges) for their named statuses; do not replace those signals with the accent.
40
+
41
+ ## Follow the consequence
42
+
43
+ <!-- rule: follow-the-consequence -->
44
+
45
+ **Rule:** Where a screen shows a source beside the values that will be saved, give the saved values the leading position, the wider column, the heavier weight and whatever accent the pair carries; leave the source with none of the four.
46
+
47
+ **Why:** Emphasis tells the reader what the screen is for. A preview that outweighs the data being committed makes a commitment look like a reading task, and an accent on the reference says the reference is the point. The accent goes on the name because a line drawn strongly enough to carry the signal is an outlined box, which the kit does not draw.
48
+
49
+ **Do:** Lead with the extracted fields, in the wider column, with each value heavier than its label, and put the accent on the pane's own name.
50
+
51
+ **Don't:** Give the source document the leading, wider pane, or let a control inside it hold the only accent on the screen while the saved values hold none.
52
+
53
+ **Except:** Fade nothing to make a block quieter; a quieter block is smaller, later or uncoloured, never muted. See [Limit muted ink](https://ui.apli.tech/storybook/?path=/story/guidelines-labels-and-titles--page).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.75.1",
3
+ "version": "0.76.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -29,6 +29,9 @@ import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (mo
29
29
  import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
30
30
  ```
31
31
 
32
+ Both stylesheets. The React one adds what React's own components need — it carries no tokens, no
33
+ reset and none of the kit's control CSS, so it cannot stand in for the first import.
34
+
32
35
  Components: `Success`, `SuccessPanel`, `SuccessCheck`, `Checkbox`, `Switch`, `SearchField`, `EmptyState`, `DataTable`, `Pagination`, `StatBand`, `Modal`, `Confirm`, `Drawer`, `CommandPalette`, `Dropdown`, `BackLink`, `Tooltip`, `Snippet`, `Tabs`, `Button`, `Badge`, `Pill`, `StatusDot`, `Card`, `Callout`, `Icon`.
33
36
 
34
37
  `Pill` renders metadata with the existing pill spacing. Omit `variant` for neutral
@@ -109,260 +109,6 @@
109
109
  }
110
110
  }
111
111
 
112
- /* ../src/styles/input.css */
113
- .ui-field {
114
- display: block;
115
- }
116
- .ui-field__label {
117
- display: block;
118
- color: var(--strong);
119
- font-size: var(--text-base);
120
- font-weight: var(--weight-medium);
121
- margin-bottom: 8px;
122
- }
123
- .ui-field__req {
124
- color: var(--pink);
125
- margin-left: 4px;
126
- }
127
- .ui-field__hint {
128
- color: var(--text);
129
- font-size: var(--text-sm);
130
- margin-top: 7px;
131
- line-height: 1.5;
132
- }
133
- .ui-field__error {
134
- color: var(--pink);
135
- font-size: var(--text-sm);
136
- margin-top: 7px;
137
- display: flex;
138
- align-items: center;
139
- gap: 6px;
140
- }
141
- .ui-field__error svg {
142
- width: 16px;
143
- height: 16px;
144
- flex: none;
145
- stroke-width: 2.4;
146
- }
147
- .ui-input,
148
- .ui-textarea,
149
- .ui-select {
150
- font-family: var(--font-sans);
151
- font-size: var(--text-base);
152
- color: var(--strong);
153
- background: var(--field-bg);
154
- border: 1px solid var(--field-edge);
155
- border-radius: var(--radius-md);
156
- padding: 12px 15px;
157
- width: 100%;
158
- transition: border-color var(--dur-med) var(--ease), box-shadow var(--dur-med) var(--ease);
159
- }
160
- .ui-textarea {
161
- resize: vertical;
162
- min-height: 96px;
163
- line-height: 1.55;
164
- }
165
- .ui-input:hover,
166
- .ui-textarea:hover,
167
- .ui-select:hover {
168
- border-color: var(--field-edge-hover);
169
- }
170
- .ui-input:focus-visible,
171
- .ui-textarea:focus-visible,
172
- .ui-select:focus-visible {
173
- outline: 2px solid transparent;
174
- border-color: var(--accent);
175
- box-shadow: var(--ring);
176
- }
177
- .ui-input::placeholder,
178
- .ui-textarea::placeholder {
179
- color: var(--muted);
180
- opacity: 0.9;
181
- }
182
- .ui-input:disabled,
183
- .ui-select:disabled,
184
- .ui-textarea:disabled {
185
- background-color: var(--disabled-surface);
186
- border-color: var(--disabled-border);
187
- color: var(--disabled-ink);
188
- cursor: not-allowed;
189
- }
190
- .ui-input.is-invalid,
191
- .ui-select.is-invalid,
192
- .ui-textarea.is-invalid {
193
- border-color: var(--pink);
194
- }
195
- .ui-input[type=search]::-webkit-search-cancel-button {
196
- appearance: none;
197
- display: none;
198
- }
199
- .ui-input-group {
200
- position: relative;
201
- display: flex;
202
- align-items: center;
203
- }
204
- .ui-input-group .ui-input {
205
- padding-left: 42px;
206
- }
207
- .ui-input-group__icon {
208
- position: absolute;
209
- left: 14px;
210
- width: 17px;
211
- height: 17px;
212
- color: var(--muted);
213
- pointer-events: none;
214
- }
215
- .ui-input-group__icon svg {
216
- width: 100%;
217
- height: 100%;
218
- stroke: currentColor;
219
- fill: none;
220
- stroke-width: 2.2;
221
- }
222
- .ui-select {
223
- appearance: none;
224
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a29db6' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
225
- background-repeat: no-repeat;
226
- background-position: right 15px center;
227
- padding-right: 40px;
228
- cursor: pointer;
229
- }
230
- :root[data-theme=light] .ui-select {
231
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235c6270' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
232
- }
233
- .ui-check {
234
- display: inline-flex;
235
- align-items: flex-start;
236
- gap: 11px;
237
- cursor: pointer;
238
- font-size: var(--text-base);
239
- color: var(--text);
240
- line-height: 1.5;
241
- }
242
- .ui-check input {
243
- appearance: none;
244
- width: 19px;
245
- height: 19px;
246
- flex: none;
247
- margin-top: 1px;
248
- border: 1.5px solid var(--border-strong);
249
- border-radius: var(--radius-xs);
250
- background: var(--surface-2);
251
- cursor: pointer;
252
- transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
253
- position: relative;
254
- }
255
- .ui-check input::before {
256
- content: "";
257
- position: absolute;
258
- left: 50%;
259
- top: 50%;
260
- width: 24px;
261
- height: 24px;
262
- transform: translate(-50%, -50%);
263
- }
264
- .ui-check input[type=radio] {
265
- border-radius: 50%;
266
- }
267
- .ui-check input:enabled:hover {
268
- border-color: var(--accent);
269
- }
270
- .ui-check input:checked {
271
- background: var(--accent);
272
- border-color: var(--accent);
273
- }
274
- .ui-check input[type=checkbox]:checked::after {
275
- content: "";
276
- position: absolute;
277
- left: 5px;
278
- top: 1.5px;
279
- width: 5px;
280
- height: 10px;
281
- border: solid var(--accent-contrast);
282
- border-width: 0 2px 2px 0;
283
- transform: rotate(45deg);
284
- }
285
- .ui-check input[type=radio]:checked::after {
286
- content: "";
287
- position: absolute;
288
- inset: 4px;
289
- border-radius: 50%;
290
- background: var(--accent-contrast);
291
- }
292
- .ui-check input:focus-visible {
293
- outline: 2px solid transparent;
294
- box-shadow: var(--ring);
295
- }
296
- .ui-check input:disabled {
297
- background: var(--disabled-surface);
298
- border-color: var(--disabled-border);
299
- cursor: not-allowed;
300
- }
301
- .ui-check input:disabled:checked {
302
- background: var(--disabled-ink);
303
- border-color: var(--disabled-ink);
304
- }
305
- .ui-check input[type=checkbox]:disabled:checked::after {
306
- border-color: var(--disabled-surface);
307
- }
308
- .ui-check input[type=radio]:disabled:checked::after {
309
- background: var(--disabled-surface);
310
- }
311
- .ui-check:has(input:disabled) {
312
- color: var(--disabled-ink);
313
- cursor: not-allowed;
314
- }
315
- .ui-switch {
316
- --sw-w: 44px;
317
- --sw-h: 26px;
318
- position: relative;
319
- display: inline-block;
320
- width: var(--sw-w);
321
- height: var(--sw-h);
322
- flex: none;
323
- }
324
- .ui-switch input {
325
- position: absolute;
326
- opacity: 0;
327
- width: 0;
328
- height: 0;
329
- }
330
- .ui-switch__track {
331
- position: absolute;
332
- inset: 0;
333
- background: var(--surface-3);
334
- border-radius: var(--radius-pill);
335
- cursor: pointer;
336
- transition: background var(--dur-med) var(--ease);
337
- }
338
- .ui-switch__track::before {
339
- content: "";
340
- position: absolute;
341
- top: 3px;
342
- left: 3px;
343
- width: calc(var(--sw-h) - 6px);
344
- height: calc(var(--sw-h) - 6px);
345
- background: var(--control-knob);
346
- border: 1px solid var(--border-strong);
347
- box-sizing: border-box;
348
- border-radius: 50%;
349
- transition: transform var(--dur-med) var(--ease);
350
- }
351
- .ui-switch input:checked + .ui-switch__track {
352
- background: var(--accent);
353
- }
354
- .ui-switch input:checked + .ui-switch__track::before {
355
- transform: translateX(calc(var(--sw-w) - var(--sw-h)));
356
- }
357
- .ui-switch input:focus-visible + .ui-switch__track {
358
- outline: 2px solid transparent;
359
- box-shadow: var(--ring);
360
- }
361
- .ui-switch input:disabled + .ui-switch__track {
362
- opacity: 0.5;
363
- cursor: not-allowed;
364
- }
365
-
366
112
  /* ../src/styles/tooltip.css */
367
113
  .ui-tip-host {
368
114
  position: relative;
@@ -72,10 +72,11 @@ declare const StatusDot: react.ForwardRefExoticComponent<Omit<HTMLAttributes<HTM
72
72
  } & react.RefAttributes<HTMLSpanElement>>;
73
73
 
74
74
  type Level = 2 | 3 | 4 | 5 | 6;
75
- declare function Card({ title, sub, level, children }: {
75
+ declare function Card({ title, sub, level, className, children }: {
76
76
  title?: ReactNode;
77
77
  sub?: ReactNode;
78
78
  level?: Level;
79
+ className?: string;
79
80
  children?: ReactNode;
80
81
  }): react.JSX.Element;
81
82
 
@@ -244,11 +244,11 @@ var StatusDot = forwardRef5(function StatusDot2({
244
244
 
245
245
  // src/primitives/Card.tsx
246
246
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
247
- function Card({ title, sub, level = 2, children }) {
247
+ function Card({ title, sub, level = 2, className, children }) {
248
248
  const n = Number(level);
249
249
  const Heading = `h${[2, 3, 4, 5, 6].includes(n) ? n : 2}`;
250
250
  const titled = title != null && title !== false && title !== "";
251
- return /* @__PURE__ */ jsxs4("div", { className: "ui-card", children: [
251
+ return /* @__PURE__ */ jsxs4("div", { className: ["ui-card", className].filter(Boolean).join(" "), children: [
252
252
  titled && /* @__PURE__ */ jsx8(Heading, { className: "ui-card__title", children: title }),
253
253
  sub != null && /* @__PURE__ */ jsx8("div", { className: "ui-card__sub", children: sub }),
254
254
  children
@@ -137,11 +137,16 @@ svg:where(:not([width]):not([height])) {
137
137
  }
138
138
 
139
139
  /* Focus-visible ring, shared by all interactive controls */
140
+ /* `.toggle` is the theme switch — a real button that wears neither .ui-btn nor
141
+ .ui-focusable, in the vanilla topbar, React ThemeToggle and the React shell's bar
142
+ alike, so keyboard focus on it fell back to the browser's own outline in all three.
143
+ It has no class of its own to opt in with; it joins the list instead. Found on #385. */
140
144
  .ui-focusable:focus-visible,
141
145
  .ui-snippet :focus-visible,
142
146
  .ui-btn:focus-visible,
143
147
  .ui-input:focus-visible,
144
148
  .ui-seg button:focus-visible,
149
+ .toggle:focus-visible,
145
150
  .ui-switch:focus-visible {
146
151
  outline: 2px solid transparent;
147
152
  box-shadow: var(--ring);
@@ -17,14 +17,32 @@
17
17
  padding: 0 var(--space-4) var(--space-3) 0;
18
18
  border-bottom: 1px solid var(--border);
19
19
  }
20
- .ui-table td {
20
+ /* A row header is a body cell that happens to name its row, so it takes the body cell's
21
+ * padding. `.ui-table th` pads for the head, where the label sits above the rule and
22
+ * wants no space over it; inside the body or the footer that left the name sitting low
23
+ * against the values beside it. `:is()` here is only for specificity — two type
24
+ * selectors outrank `.ui-table th`, which states the head's padding after this. */
25
+ .ui-table td,
26
+ .ui-table :is(tbody, tfoot) th {
21
27
  padding: var(--space-4) var(--space-4) var(--space-4) 0;
22
28
  border-bottom: 1px solid var(--border);
23
29
  color: var(--text);
24
30
  vertical-align: middle;
25
31
  white-space: nowrap;
26
32
  }
27
- .ui-table tr:last-child td { border-bottom: 0; }
33
+ /* The last row of the last section, so a final body row keeps its separator when a
34
+ * footer follows it. `tr:last-child td` closed the wrong edge: it stripped the border
35
+ * from the last row of every section, leaving items and totals with nothing between
36
+ * them. why: docs/specification.md#spacing-and-rhythm */
37
+ .ui-table > :last-child > tr:last-child > :is(th, td) { border-bottom: 0; }
38
+ /* Totals open with the 2px rule the zebra header already uses, so a reader can see
39
+ * where items end and totals begin; five identical hairlines told them nothing.
40
+ * Collapsed borders paint one edge, so this meets the last body row's hairline and the
41
+ * wider of the two wins — no doubled line.
42
+ * A footer label is a row header for the figure beside it, so it sits against that
43
+ * figure rather than against the first column it spans. */
44
+ .ui-table tfoot tr:first-child > :is(th, td) { border-top: 2px solid var(--border-strong); }
45
+ .ui-table tfoot th { text-align: right; }
28
46
  .ui-table td.ui-table__title { color: var(--strong); font-weight: var(--weight-medium); width: 99%; white-space: normal; }
29
47
  .ui-table th:last-child { padding-right: 0; }
30
48
  .ui-table td.ui-table__act { padding-right: 0; text-align: right; }
@@ -62,14 +80,16 @@
62
80
  * why: docs/specification.md#spacing-and-rhythm
63
81
  * held by: stories/table-rhythm.test.js */
64
82
  .ui-table--dense th { padding: 0 var(--space-3) var(--space-2); }
65
- .ui-table--dense td { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); white-space: nowrap; }
83
+ .ui-table--dense td,
84
+ .ui-table--dense :is(tbody, tfoot) th { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); white-space: nowrap; }
66
85
  .ui-table--dense th:first-child,
67
86
  .ui-table--dense td:first-child { padding-left: var(--space-3); }
68
87
  .ui-table--dense th:last-child,
69
88
  .ui-table--dense td:last-child { padding-right: var(--space-3); }
70
89
 
71
- .ui-table--zebra th { border-bottom-width: 2px; }
72
- .ui-table--zebra td { border-bottom: 0; }
90
+ .ui-table--zebra thead th { border-bottom-width: 2px; }
91
+ .ui-table--zebra td,
92
+ .ui-table--zebra :is(tbody, tfoot) th { border-bottom: 0; }
73
93
  .ui-table--zebra tbody tr:nth-child(even) td { background: var(--surface-2); --ring-gap: var(--surface-2); }
74
94
  /* The stripe stays full-bleed (the #71 inset above is hover only), but it only reads right
75
95
  * if the end cells are inset from the row's own edge, and `.ui-table td` sets
@@ -116,8 +136,15 @@
116
136
  }
117
137
 
118
138
  .ui-table__num { text-align: right; font-variant-numeric: tabular-nums; }
119
- .ui-table th.ui-table__num { text-align: right; }
139
+ /* A numeric column is sized to its content while .ui-table__title claims the rest,
140
+ * so a two-word numeric header wraps where none of its values ever do. */
141
+ .ui-table th.ui-table__num { text-align: right; white-space: nowrap; }
120
142
  .ui-table__num--strong { color: var(--strong); font-weight: var(--weight-semibold); }
143
+ /* `.ui-table th` and `.ui-table td` each state their own ink, and `th` its weight too,
144
+ * so both outranked the helper: a totals row marked strong came out in body ink, and
145
+ * bold only in the figure. Scoped to the table, the helper wins on either cell and the
146
+ * row reads as one row. */
147
+ .ui-table :is(th, td).ui-table__num--strong { color: var(--strong); font-weight: var(--weight-semibold); }
121
148
  .ui-table__code { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
122
149
 
123
150
  /* The empty state (.ui-empty) — shown when a table/list has no rows — now
@@ -155,7 +182,8 @@
155
182
  padding-inline: 0;
156
183
  }
157
184
  .ui-table td.ui-table__num { text-align: right; }
158
- .ui-table--compact td { padding: var(--space-1) var(--space-3); font-size: var(--text-sm); line-height: var(--leading-snug); height: 33px; }
185
+ .ui-table--compact td,
186
+ .ui-table--compact :is(tbody, tfoot) th { padding: var(--space-1) var(--space-3); font-size: var(--text-sm); line-height: var(--leading-snug); height: 33px; }
159
187
  .ui-table--compact th { padding: var(--space-2) var(--space-3); }
160
188
  .ui-table--compact th:last-child,
161
189
  .ui-table--compact td:last-child { padding-right: var(--space-3); }