@apliteni/apliteni-ui 0.7.2 β 0.8.1
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 +94 -20
- package/package.json +20 -11
- package/react/README.md +58 -0
- package/react/dist/index.css +76 -0
- package/react/dist/index.d.ts +58 -0
- package/react/dist/index.js +254 -0
- package/src/assets/icons.js +95 -72
- package/src/components/feedback.js +9 -7
- package/src/components/index.js +90 -15
- package/src/components/tabs.js +6 -2
- package/src/components/topbar.js +39 -5
- package/src/styles/badge.css +11 -13
- package/src/styles/callout.css +5 -5
- package/src/styles/card.css +1 -1
- package/src/styles/drawer.css +2 -4
- package/src/styles/dropdown.css +1 -1
- package/src/styles/empty.css +1 -1
- package/src/styles/feedback.css +12 -6
- package/src/styles/input.css +4 -1
- package/src/styles/layout.css +1 -1
- package/src/styles/motion.css +1 -1
- package/src/styles/nav.css +3 -1
- package/src/styles/segmented.css +3 -0
- package/src/styles/table.css +13 -0
- package/src/styles/tabs.css +3 -0
- package/src/styles/topbar.css +3 -3
- package/src/tokens/accents.css +6 -2
- package/src/tokens/brand.generated.css +1 -1
- package/src/tokens/tokens.css +54 -0
package/README.md
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
# @apliteni/apliteni-ui
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@apliteni/apliteni-ui)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://ui.apli.tech)
|
|
6
|
+
|
|
3
7
|
The Apliteni design system & UI kit β one source of UI for every product surface
|
|
4
8
|
(the strategy deck, the text portal, `/account`, the operating model, and whatever
|
|
5
9
|
ships next).
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
published on **ui.apli.tech**.
|
|
11
|
+
Two layers over one set of design tokens: framework-agnostic **HTML + CSS**, and
|
|
12
|
+
**React components** for stateful surfaces. Themeable dark and light with **accent
|
|
13
|
+
sub-themes**. Showcased and reviewed in **Storybook**, and published on **ui.apli.tech**.
|
|
10
14
|
|
|
11
15
|
- π¨ **Live site + Storybook** β [ui.apli.tech](https://ui.apli.tech)
|
|
12
16
|
- π¦ **Package** β [`@apliteni/apliteni-ui`](https://www.npmjs.com/package/@apliteni/apliteni-ui) (public npm)
|
|
17
|
+
- βοΈ **React components** β `@apliteni/apliteni-ui/react` β a subpath of the same package, source in [`react/`](./react)
|
|
18
|
+
|
|
19
|
+
## HTML + CSS *and* React
|
|
20
|
+
|
|
21
|
+
The core is HTML + CSS because the strategy portal (`apliteni/strategy`, `viz/`)
|
|
22
|
+
server-renders HTML strings (`.mjs` modules), not a component framework. So the kit
|
|
23
|
+
ships the same shape: token CSS + component CSS + tiny HTML-string factories. That
|
|
24
|
+
makes it a *true* single source of truth β the portal imports it with no rewrite and
|
|
25
|
+
no framework drift. Storybook (`@storybook/html-vite`) renders exactly what ships.
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
React sits on top for surfaces that hold real client state β dashboards, tables,
|
|
28
|
+
filters, forms. Those components render the same `.ui-*` classes and the same tokens
|
|
29
|
+
as the vanilla kit, so the two layers can't drift.
|
|
15
30
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
CSS + component CSS + tiny HTML-string factories. That makes it a *true* single source
|
|
19
|
-
of truth β the portal imports it with no rewrite and no framework drift. Storybook
|
|
20
|
-
(`@storybook/html-vite`) renders exactly what ships.
|
|
31
|
+
**Which one:** does the surface hold meaningful client state? No β the HTML-string
|
|
32
|
+
factories below. Yes β the [React components](#react-components-stateful-surfaces).
|
|
21
33
|
|
|
22
34
|
## Install
|
|
23
35
|
|
|
@@ -67,6 +79,39 @@ import { tokensCss, topbarCss, cssText } from '@apliteni/apliteni-ui/inline';
|
|
|
67
79
|
// β¦inline tokensCss + topbarCss into the <style> you serve.
|
|
68
80
|
```
|
|
69
81
|
|
|
82
|
+
## React components (stateful surfaces)
|
|
83
|
+
|
|
84
|
+
`DataTable`, `Modal`, `Button`, `Badge`, `Card` and `Icon` β same `.ui-*` classes,
|
|
85
|
+
same tokens, TypeScript types included. They ship as a **subpath of this package**,
|
|
86
|
+
not as a package of their own: one install, one version, one pin.
|
|
87
|
+
|
|
88
|
+
**You install React yourself.** The kit declares no dependency on it β not a
|
|
89
|
+
dependency, not a peer β so npm will not warn you and will not pull it in. Bring your
|
|
90
|
+
own `react` and `react-dom`, version 18 or newer:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install @apliteni/apliteni-ui react react-dom
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Miss them and the import of `@apliteni/apliteni-ui/react` fails at build or at
|
|
97
|
+
runtime with a module-not-found error for `react`. Everything else in the kit is
|
|
98
|
+
unaffected, so a plain HTML consumer installs the kit alone and never gets React in
|
|
99
|
+
its tree.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import '@apliteni/apliteni-ui/css'; // kit tokens + .ui-* classes
|
|
103
|
+
import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, pager)
|
|
104
|
+
import { DataTable, Modal } from '@apliteni/apliteni-ui/react';
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The source lives in [`react/`](./react) β a private workspace with its own build
|
|
108
|
+
(tsup) and Storybook on port 6007. When 6007 is taken Storybook moves to the next
|
|
109
|
+
free port, so the root Storybook does not trust the number: it probes the range
|
|
110
|
+
6007 can drift into and composes the first port that proves it is this workspace's
|
|
111
|
+
Storybook. A stranger on the port is never composed β the "React components"
|
|
112
|
+
section is absent instead, and the terminal says why. Details in
|
|
113
|
+
[`react/README.md`](./react/README.md).
|
|
114
|
+
|
|
70
115
|
## Theming
|
|
71
116
|
|
|
72
117
|
Theme is a `data-theme="dark|light"` attribute on `<html>`; accent is an orthogonal
|
|
@@ -98,21 +143,32 @@ src/
|
|
|
98
143
|
assets/ # brand mark (seedling) + line-icon set
|
|
99
144
|
components/ # HTML-string factories: button(), card(), badge(), topbar()β¦
|
|
100
145
|
stories/ # Storybook: Foundations, Components, Apps
|
|
101
|
-
site/ # ui.apli.tech landing page
|
|
146
|
+
site/ # ui.apli.tech landing page (static site build)
|
|
147
|
+
react/ # React components β private workspace, built to react/dist/
|
|
148
|
+
dist/ # tsup output; shipped as β¦/react and β¦/react/css
|
|
102
149
|
```
|
|
103
150
|
|
|
104
151
|
## Develop
|
|
105
152
|
|
|
106
153
|
```bash
|
|
107
154
|
npm install
|
|
108
|
-
npm run storybook # http://localhost:6006
|
|
155
|
+
npm run storybook # http://localhost:6006, or the next free port
|
|
109
156
|
npm run build-storybook # -> storybook-static/
|
|
110
|
-
node site/build.mjs # -> site/public/ (landing + kit.css)
|
|
157
|
+
node site/build.mjs # -> site/public/ (landing + kit.css + /storybook)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The React components build and test through the workspace (`npm install` at the repo
|
|
161
|
+
root covers them β there is no second install):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm run storybook -w react # http://localhost:6007, or the next free port
|
|
165
|
+
npm test -w react # vitest
|
|
166
|
+
npm run build # tsup -> react/dist/ (also runs on prepare)
|
|
111
167
|
```
|
|
112
168
|
|
|
113
169
|
## Publish (public npm)
|
|
114
170
|
|
|
115
|
-
Versioned publish runs from CI on a GitHub Release
|
|
171
|
+
Versioned publish runs from CI on a GitHub Release:
|
|
116
172
|
|
|
117
173
|
```bash
|
|
118
174
|
npm version patch # or minor / major β bumps package.json + tags
|
|
@@ -120,17 +176,34 @@ git push --follow-tags
|
|
|
120
176
|
gh release create v$(node -p "require('./package.json').version") --generate-notes
|
|
121
177
|
```
|
|
122
178
|
|
|
123
|
-
The **Release** workflow (`.github/workflows/release.yml`) then publishes
|
|
124
|
-
|
|
179
|
+
The **Release** workflow (`.github/workflows/release.yml`) then publishes to the public
|
|
180
|
+
npm registry over npm Trusted Publishing (OIDC) β there is no long-lived token. No
|
|
181
|
+
manual `npm publish` needed. It runs in two jobs: `build` installs and runs `npm pack`,
|
|
182
|
+
whose `prepare` rebuilds `react/dist` from the tagged commit, and `publish` β the only
|
|
183
|
+
job that can mint an OIDC credential β just publishes that tarball, so no dependency
|
|
184
|
+
or build script ever runs beside the credential. The packaging guard
|
|
185
|
+
(`scripts/packaging.test.js`) fails CI if the React subpath isn't in the tarball, and
|
|
186
|
+
the release itself re-checks the tarball before publishing it.
|
|
125
187
|
|
|
126
188
|
## Deploy (ui.apli.tech)
|
|
127
189
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
190
|
+
The site is **100% static** (landing + hosted Storybook) β no container, no registry.
|
|
191
|
+
It's served by **Lessly static hosting**, built straight from this repo. The `site`
|
|
192
|
+
service builds from `main` with:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
npm ci && npm run build-storybook && node site/build.mjs
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
and serves `site/public/`, which includes the landing page, `/changelog`, `kit.css`,
|
|
199
|
+
and the Storybook folded in at `/storybook`. Push to `main` and redeploy the `site`
|
|
200
|
+
service to roll it out.
|
|
201
|
+
|
|
202
|
+
To reproduce the exact static bundle locally:
|
|
131
203
|
|
|
132
204
|
```bash
|
|
133
|
-
|
|
205
|
+
npm ci && npm run build-storybook && node site/build.mjs
|
|
206
|
+
# -> site/public/ (landing + /changelog + /storybook + kit.css)
|
|
134
207
|
```
|
|
135
208
|
|
|
136
209
|
## Adopting into the strategy portal
|
|
@@ -143,4 +216,5 @@ stays self-contained for the claude.ai Artifact CSP, baking tokens in via its bu
|
|
|
143
216
|
|
|
144
217
|
## License
|
|
145
218
|
|
|
146
|
-
[MIT](./LICENSE) Β© Apliteni
|
|
219
|
+
[MIT](./LICENSE) Β© Apliteni β for the **code**. The Apliteni name, logos, and brand
|
|
220
|
+
marks are trademarks and are **not** covered by the MIT license; see [TRADEMARK.md](./TRADEMARK.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apliteni/apliteni-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"workspaces": [
|
|
5
|
+
"react"
|
|
6
|
+
],
|
|
4
7
|
"description": "Apliteni shared design system & UI kit β tokens, components and the deck theme, framework-agnostic (HTML + CSS).",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"license": "MIT",
|
|
@@ -21,11 +24,18 @@
|
|
|
21
24
|
"./css": "./src/index.css",
|
|
22
25
|
"./tokens": "./src/tokens/tokens.css",
|
|
23
26
|
"./accents": "./src/tokens/accents.css",
|
|
24
|
-
"./brand": "./src/tokens/brand.generated.css"
|
|
27
|
+
"./brand": "./src/tokens/brand.generated.css",
|
|
28
|
+
"./react": {
|
|
29
|
+
"types": "./react/dist/index.d.ts",
|
|
30
|
+
"import": "./react/dist/index.js",
|
|
31
|
+
"default": "./react/dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./react/css": "./react/dist/index.css"
|
|
25
34
|
},
|
|
26
35
|
"files": [
|
|
27
36
|
"src",
|
|
28
37
|
"!src/**/*.test.js",
|
|
38
|
+
"react/dist",
|
|
29
39
|
"README.md"
|
|
30
40
|
],
|
|
31
41
|
"sideEffects": [
|
|
@@ -34,7 +44,9 @@
|
|
|
34
44
|
"scripts": {
|
|
35
45
|
"storybook": "storybook dev -p 6006 --no-open",
|
|
36
46
|
"build-storybook": "storybook build -o storybook-static",
|
|
37
|
-
"
|
|
47
|
+
"build": "npm run build --workspace react",
|
|
48
|
+
"prepare": "npm run build",
|
|
49
|
+
"test": "for d in src stories site scripts; do [ -d \"$d\" ] || { echo \"npm test: no such directory: $d\" >&2; exit 1; }; done; node --test 'src/**/*.test.js' 'stories/**/*.test.js' 'site/**/*.test.js' 'scripts/**/*.test.js'",
|
|
38
50
|
"tokens:sync": "node scripts/sync-brand-tokens.mjs",
|
|
39
51
|
"tokens:check": "node scripts/sync-brand-tokens.mjs --check",
|
|
40
52
|
"tokens:drift": "node scripts/brand-drift.mjs"
|
|
@@ -43,14 +55,11 @@
|
|
|
43
55
|
"node": ">=20"
|
|
44
56
|
},
|
|
45
57
|
"devDependencies": {
|
|
46
|
-
"@storybook/addon-a11y": "^
|
|
47
|
-
"@storybook/
|
|
48
|
-
"@storybook/blocks": "^8.6.14",
|
|
49
|
-
"@storybook/html": "^8.6.14",
|
|
50
|
-
"@storybook/html-vite": "^8.6.14",
|
|
58
|
+
"@storybook/addon-a11y": "^10.5.5",
|
|
59
|
+
"@storybook/html-vite": "^10.5.5",
|
|
51
60
|
"axe-core": "^4.12.1",
|
|
52
|
-
"jsdom": "^
|
|
53
|
-
"storybook": "^
|
|
54
|
-
"vite": "^
|
|
61
|
+
"jsdom": "^30.0.1",
|
|
62
|
+
"storybook": "^10.5.5",
|
|
63
|
+
"vite": "^8.2.0"
|
|
55
64
|
}
|
|
56
65
|
}
|
package/react/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# React components
|
|
2
|
+
|
|
3
|
+
Source for the kit's React layer β for surfaces that hold real client state
|
|
4
|
+
(dashboards, tables, filters, forms). They render the same `.ui-*` classes and
|
|
5
|
+
tokens as the vanilla kit, so the two layers can't drift.
|
|
6
|
+
|
|
7
|
+
This directory is a **private workspace**, not a package. It builds to `react/dist/`
|
|
8
|
+
and ships as the `@apliteni/apliteni-ui/react` subpath of the kit β one package, one
|
|
9
|
+
version, one pin. There is no `@apliteni/apliteni-ui-react` on npm.
|
|
10
|
+
|
|
11
|
+
**Decision rule:** does this surface hold meaningful client state?
|
|
12
|
+
No β use the vanilla factories from `@apliteni/apliteni-ui`.
|
|
13
|
+
Yes β use these React components.
|
|
14
|
+
|
|
15
|
+
## Install (as a consumer)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @apliteni/apliteni-ui react react-dom
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The kit declares no dependency on `react` or `react-dom`, so install them yourself
|
|
22
|
+
(18 or newer). Miss them and the import fails with a module-not-found error.
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import '@apliteni/apliteni-ui/css'; // kit tokens + .ui-* classes
|
|
28
|
+
import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (modal, pager)
|
|
29
|
+
import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Components: `DataTable`, `Modal`, `Button`, `Badge`, `Card`, `Icon`.
|
|
33
|
+
|
|
34
|
+
## Work on them
|
|
35
|
+
|
|
36
|
+
From the repo root β one `npm install` covers the workspace:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm test -w react # vitest
|
|
40
|
+
npm run storybook -w react # http://localhost:6007
|
|
41
|
+
npm run build # tsup -> react/dist/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
6007 is a request, not a promise: if something else already holds it, Storybook
|
|
45
|
+
walks upward to the next free port. The root Storybook composes these components
|
|
46
|
+
by following that drift β it probes 6007 through 6016 (the window Storybook's own
|
|
47
|
+
port-finder searches) and takes the first one whose `index.json` lists every story
|
|
48
|
+
file this workspace has on disk. A stranger on the port fails that check and is
|
|
49
|
+
never shown under "React components"; the section is simply absent, and the root
|
|
50
|
+
Storybook's terminal says which ports it tried and what it found.
|
|
51
|
+
|
|
52
|
+
The probe runs once, while the root Storybook boots. Start this one afterwards and
|
|
53
|
+
you have to restart the root Storybook before it appears.
|
|
54
|
+
|
|
55
|
+
The bare `@apliteni/apliteni-ui` specifier in this source resolves to the kit itself
|
|
56
|
+
once installed. In the repo there is no copy to resolve to, so `kit-alias.ts` points
|
|
57
|
+
vitest and Storybook straight at `../src/` β which is why the class-name parity tests
|
|
58
|
+
now compare against the working tree rather than the last published release.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* src/Modal.css */
|
|
2
|
+
.rx-scrim {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
background: rgba(0, 0, 0, .55);
|
|
6
|
+
backdrop-filter: blur(2px);
|
|
7
|
+
display: grid;
|
|
8
|
+
place-items: center;
|
|
9
|
+
z-index: 50;
|
|
10
|
+
}
|
|
11
|
+
.rx-modal {
|
|
12
|
+
width: min(480px, 92vw);
|
|
13
|
+
background: var(--surface);
|
|
14
|
+
border: 1px solid var(--border);
|
|
15
|
+
border-radius: var(--radius-xl);
|
|
16
|
+
box-shadow: var(--shadow-lg);
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
.rx-modal__head {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
padding: 16px 20px;
|
|
24
|
+
border-bottom: 1px solid var(--border);
|
|
25
|
+
}
|
|
26
|
+
.rx-modal__title {
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
font-size: var(--text-lg);
|
|
29
|
+
}
|
|
30
|
+
.rx-modal__body {
|
|
31
|
+
padding: 20px;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 16px;
|
|
35
|
+
}
|
|
36
|
+
.rx-modal__foot {
|
|
37
|
+
display: flex;
|
|
38
|
+
justify-content: flex-end;
|
|
39
|
+
gap: 12px;
|
|
40
|
+
padding: 16px 20px;
|
|
41
|
+
border-top: 1px solid var(--border);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* src/DataTable.css */
|
|
45
|
+
.rx-sortable {
|
|
46
|
+
user-select: none;
|
|
47
|
+
}
|
|
48
|
+
.rx-sort {
|
|
49
|
+
all: unset;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
font: inherit;
|
|
54
|
+
color: inherit;
|
|
55
|
+
}
|
|
56
|
+
.rx-sort:focus-visible {
|
|
57
|
+
outline: 2px solid var(--accent);
|
|
58
|
+
outline-offset: 2px;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
}
|
|
61
|
+
.rx-caret {
|
|
62
|
+
opacity: .5;
|
|
63
|
+
margin-left: 2px;
|
|
64
|
+
}
|
|
65
|
+
.rx-pager {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
justify-content: flex-end;
|
|
70
|
+
margin-top: 16px;
|
|
71
|
+
}
|
|
72
|
+
.rx-pager__info {
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
font-size: var(--text-sm);
|
|
75
|
+
margin-right: auto;
|
|
76
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ButtonHTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
declare function Icon({ name }: {
|
|
5
|
+
name: string;
|
|
6
|
+
}): react.JSX.Element;
|
|
7
|
+
|
|
8
|
+
type ButtonProps = {
|
|
9
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
icon?: string;
|
|
12
|
+
iconRight?: string;
|
|
13
|
+
iconOnly?: boolean;
|
|
14
|
+
block?: boolean;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
|
17
|
+
declare function Button({ variant, size, icon, iconRight, iconOnly, block, children, type, ...rest }: ButtonProps): react.JSX.Element;
|
|
18
|
+
|
|
19
|
+
declare function Badge({ variant, children }: {
|
|
20
|
+
variant?: string;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}): react.JSX.Element;
|
|
23
|
+
|
|
24
|
+
declare function Card({ title, sub, children }: {
|
|
25
|
+
title?: ReactNode;
|
|
26
|
+
sub?: ReactNode;
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
type ModalProps = {
|
|
31
|
+
open: boolean;
|
|
32
|
+
title: string;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
footer?: ReactNode;
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
declare function Modal({ open, title, onClose, footer, children }: ModalProps): react.ReactPortal | null;
|
|
38
|
+
|
|
39
|
+
type Column<T> = {
|
|
40
|
+
key: keyof T & string;
|
|
41
|
+
label: string;
|
|
42
|
+
num?: boolean;
|
|
43
|
+
sortable?: boolean;
|
|
44
|
+
render?: (row: T) => ReactNode;
|
|
45
|
+
};
|
|
46
|
+
type DataTableProps<T> = {
|
|
47
|
+
columns: Column<T>[];
|
|
48
|
+
rows: T[];
|
|
49
|
+
pageSize?: number;
|
|
50
|
+
selected: Set<string>;
|
|
51
|
+
onToggle: (name: string) => void;
|
|
52
|
+
onTogglePage: (names: string[]) => void;
|
|
53
|
+
};
|
|
54
|
+
declare function DataTable<T extends {
|
|
55
|
+
name: string;
|
|
56
|
+
}>({ columns, rows, pageSize, selected, onToggle, onTogglePage, }: DataTableProps<T>): react.JSX.Element;
|
|
57
|
+
|
|
58
|
+
export { Badge, Button, type ButtonProps, Card, type Column, DataTable, type DataTableProps, Icon, Modal, type ModalProps };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/primitives/Icon.tsx
|
|
4
|
+
import { icon } from "@apliteni/apliteni-ui";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
function Icon({ name }) {
|
|
7
|
+
return /* @__PURE__ */ jsx(
|
|
8
|
+
"span",
|
|
9
|
+
{
|
|
10
|
+
"aria-hidden": "true",
|
|
11
|
+
style: { display: "inline-flex" },
|
|
12
|
+
dangerouslySetInnerHTML: { __html: icon(name) }
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/primitives/Button.tsx
|
|
18
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
19
|
+
var cx = (...a) => a.filter(Boolean).join(" ");
|
|
20
|
+
function Button({
|
|
21
|
+
variant = "secondary",
|
|
22
|
+
size = "md",
|
|
23
|
+
icon: icon2,
|
|
24
|
+
iconRight,
|
|
25
|
+
iconOnly,
|
|
26
|
+
block,
|
|
27
|
+
children,
|
|
28
|
+
type = "button",
|
|
29
|
+
...rest
|
|
30
|
+
}) {
|
|
31
|
+
const cls = cx(
|
|
32
|
+
"ui-btn",
|
|
33
|
+
variant && `ui-btn--${variant}`,
|
|
34
|
+
size !== "md" && `ui-btn--${size}`,
|
|
35
|
+
block && "ui-btn--block",
|
|
36
|
+
iconOnly && "ui-btn--icon"
|
|
37
|
+
);
|
|
38
|
+
const labelled = rest["aria-label"] != null || rest["aria-labelledby"] != null;
|
|
39
|
+
const fallback = typeof children === "string" && children.trim() ? children.trim() : icon2;
|
|
40
|
+
const named = iconOnly && !labelled && fallback ? { "aria-label": fallback, title: rest.title ?? fallback } : {};
|
|
41
|
+
return /* @__PURE__ */ jsxs("button", { type, className: cls, ...rest, ...named, children: [
|
|
42
|
+
icon2 && /* @__PURE__ */ jsx2(Icon, { name: icon2 }),
|
|
43
|
+
!iconOnly && children != null && /* @__PURE__ */ jsx2("span", { children }),
|
|
44
|
+
iconRight && /* @__PURE__ */ jsx2(Icon, { name: iconRight })
|
|
45
|
+
] });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/primitives/Badge.tsx
|
|
49
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
50
|
+
function Badge({ variant = "neutral", children }) {
|
|
51
|
+
const cls = ["ui-badge", variant !== "neutral" && `ui-badge--${variant}`].filter(Boolean).join(" ");
|
|
52
|
+
return /* @__PURE__ */ jsx3("span", { className: cls, children });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/primitives/Card.tsx
|
|
56
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
57
|
+
function Card({ title, sub, children }) {
|
|
58
|
+
return /* @__PURE__ */ jsxs2("div", { className: "ui-card", children: [
|
|
59
|
+
title != null && /* @__PURE__ */ jsx4("div", { className: "ui-card__title", children: title }),
|
|
60
|
+
sub != null && /* @__PURE__ */ jsx4("div", { className: "ui-card__sub", children: sub }),
|
|
61
|
+
children
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/Modal.tsx
|
|
66
|
+
import { useEffect, useRef } from "react";
|
|
67
|
+
import { createPortal } from "react-dom";
|
|
68
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
69
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
70
|
+
function Modal({ open, title, onClose, footer, children }) {
|
|
71
|
+
const panel = useRef(null);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!open) return;
|
|
74
|
+
const onKey = (e) => {
|
|
75
|
+
if (e.key === "Escape") {
|
|
76
|
+
onClose();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (e.key !== "Tab" || !panel.current) return;
|
|
80
|
+
const items = Array.from(panel.current.querySelectorAll(FOCUSABLE));
|
|
81
|
+
if (items.length === 0) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
panel.current.focus();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const first = items[0];
|
|
87
|
+
const last = items[items.length - 1];
|
|
88
|
+
const active = document.activeElement;
|
|
89
|
+
if (!panel.current.contains(active)) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
first.focus();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!e.shiftKey && active === last) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
first.focus();
|
|
97
|
+
} else if (e.shiftKey && active === first) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
last.focus();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
document.addEventListener("keydown", onKey);
|
|
103
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
104
|
+
}, [open, onClose]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!open) return;
|
|
107
|
+
const opener = document.activeElement;
|
|
108
|
+
const portalRoot = panel.current?.closest(".rx-scrim");
|
|
109
|
+
const muted = Array.from(document.body.children).filter((el) => el !== portalRoot && !el.hasAttribute("inert"));
|
|
110
|
+
muted.forEach((el) => el.setAttribute("inert", ""));
|
|
111
|
+
return () => {
|
|
112
|
+
muted.forEach((el) => el.removeAttribute("inert"));
|
|
113
|
+
if (opener && document.contains(opener)) opener.focus();
|
|
114
|
+
};
|
|
115
|
+
}, [open]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!open) return;
|
|
118
|
+
const target = panel.current?.querySelector(
|
|
119
|
+
".rx-modal__body input, .rx-modal__body select, .rx-modal__body textarea, .rx-modal__body button"
|
|
120
|
+
) || panel.current;
|
|
121
|
+
target?.focus();
|
|
122
|
+
}, [open]);
|
|
123
|
+
if (!open) return null;
|
|
124
|
+
return createPortal(
|
|
125
|
+
/* @__PURE__ */ jsx5("div", { className: "rx-scrim", onMouseDown: (e) => {
|
|
126
|
+
if (e.target === e.currentTarget) onClose();
|
|
127
|
+
}, children: /* @__PURE__ */ jsxs3("div", { className: "rx-modal", role: "dialog", "aria-modal": "true", "aria-label": title, tabIndex: -1, ref: panel, children: [
|
|
128
|
+
/* @__PURE__ */ jsxs3("div", { className: "rx-modal__head", children: [
|
|
129
|
+
/* @__PURE__ */ jsx5("div", { className: "rx-modal__title", children: title }),
|
|
130
|
+
/* @__PURE__ */ jsx5(Button, { variant: "ghost", size: "sm", iconOnly: true, icon: "x", "aria-label": "Close", onClick: onClose })
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsx5("div", { className: "rx-modal__body", children }),
|
|
133
|
+
footer && /* @__PURE__ */ jsx5("div", { className: "rx-modal__foot", children: footer })
|
|
134
|
+
] }) }),
|
|
135
|
+
document.body
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/DataTable.tsx
|
|
140
|
+
import { useMemo, useState } from "react";
|
|
141
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
142
|
+
function DataTable({
|
|
143
|
+
columns,
|
|
144
|
+
rows,
|
|
145
|
+
pageSize = 4,
|
|
146
|
+
selected,
|
|
147
|
+
onToggle,
|
|
148
|
+
onTogglePage
|
|
149
|
+
}) {
|
|
150
|
+
const [sort, setSort] = useState(
|
|
151
|
+
{ key: columns.find((c) => c.sortable)?.key, dir: -1 }
|
|
152
|
+
);
|
|
153
|
+
const [page, setPage] = useState(0);
|
|
154
|
+
const sorted = useMemo(() => {
|
|
155
|
+
if (!sort.key) return rows;
|
|
156
|
+
const k = sort.key;
|
|
157
|
+
return [...rows].sort((a, b) => (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0) * sort.dir);
|
|
158
|
+
}, [rows, sort]);
|
|
159
|
+
const pages = Math.max(1, Math.ceil(sorted.length / pageSize));
|
|
160
|
+
const safePage = Math.min(page, pages - 1);
|
|
161
|
+
const slice = sorted.slice(safePage * pageSize, safePage * pageSize + pageSize);
|
|
162
|
+
const onSort = (k) => {
|
|
163
|
+
setSort((s) => s.key === k ? { key: k, dir: s.dir === 1 ? -1 : 1 } : { key: k, dir: -1 });
|
|
164
|
+
setPage(0);
|
|
165
|
+
};
|
|
166
|
+
const caret = (k) => sort.key === k ? sort.dir === 1 ? " \u25B2" : " \u25BC" : " \u2195";
|
|
167
|
+
const pageAllOn = slice.length > 0 && slice.every((r) => selected.has(r.name));
|
|
168
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
169
|
+
/* @__PURE__ */ jsxs4("table", { className: "ui-table ui-table--hover ui-table--zebra", children: [
|
|
170
|
+
/* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
|
|
171
|
+
/* @__PURE__ */ jsx6("th", { scope: "col", children: /* @__PURE__ */ jsx6(
|
|
172
|
+
"input",
|
|
173
|
+
{
|
|
174
|
+
type: "checkbox",
|
|
175
|
+
checked: pageAllOn,
|
|
176
|
+
"aria-label": "Select all rows on this page",
|
|
177
|
+
onChange: () => onTogglePage(slice.map((r) => r.name))
|
|
178
|
+
}
|
|
179
|
+
) }),
|
|
180
|
+
columns.map((c) => (
|
|
181
|
+
// The sort control is a real <button> inside the header cell. It used to be
|
|
182
|
+
// role="button" ON the <th>, which threw away the columnheader role and put
|
|
183
|
+
// aria-sort on a role that forbids it.
|
|
184
|
+
/* @__PURE__ */ jsx6(
|
|
185
|
+
"th",
|
|
186
|
+
{
|
|
187
|
+
scope: "col",
|
|
188
|
+
className: [c.num && "ui-table__num", c.sortable && "rx-sortable"].filter(Boolean).join(" "),
|
|
189
|
+
"aria-sort": c.sortable ? sort.key === c.key ? sort.dir === 1 ? "ascending" : "descending" : "none" : void 0,
|
|
190
|
+
children: c.sortable ? /* @__PURE__ */ jsxs4("button", { type: "button", className: "rx-sort", onClick: () => onSort(c.key), children: [
|
|
191
|
+
c.label,
|
|
192
|
+
/* @__PURE__ */ jsx6("span", { className: "rx-caret", "aria-hidden": "true", children: caret(c.key) })
|
|
193
|
+
] }) : c.label
|
|
194
|
+
},
|
|
195
|
+
c.key
|
|
196
|
+
)
|
|
197
|
+
))
|
|
198
|
+
] }) }),
|
|
199
|
+
/* @__PURE__ */ jsx6("tbody", { children: slice.map((r) => /* @__PURE__ */ jsxs4("tr", { children: [
|
|
200
|
+
/* @__PURE__ */ jsx6("td", { children: /* @__PURE__ */ jsx6(
|
|
201
|
+
"input",
|
|
202
|
+
{
|
|
203
|
+
type: "checkbox",
|
|
204
|
+
checked: selected.has(r.name),
|
|
205
|
+
"aria-label": `Select ${r.name}`,
|
|
206
|
+
onChange: () => onToggle(r.name)
|
|
207
|
+
}
|
|
208
|
+
) }),
|
|
209
|
+
columns.map((c) => /* @__PURE__ */ jsx6("td", { className: c.num ? "ui-table__num" : void 0, children: c.render ? c.render(r) : String(r[c.key]) }, c.key))
|
|
210
|
+
] }, r.name)) })
|
|
211
|
+
] }),
|
|
212
|
+
/* @__PURE__ */ jsxs4("div", { className: "rx-pager", children: [
|
|
213
|
+
/* @__PURE__ */ jsxs4("span", { className: "rx-pager__info", children: [
|
|
214
|
+
"Page ",
|
|
215
|
+
safePage + 1,
|
|
216
|
+
" of ",
|
|
217
|
+
pages,
|
|
218
|
+
" \xB7 ",
|
|
219
|
+
sorted.length,
|
|
220
|
+
" rows"
|
|
221
|
+
] }),
|
|
222
|
+
/* @__PURE__ */ jsx6(
|
|
223
|
+
Button,
|
|
224
|
+
{
|
|
225
|
+
variant: "ghost",
|
|
226
|
+
size: "sm",
|
|
227
|
+
icon: "chevronLeft",
|
|
228
|
+
disabled: safePage === 0,
|
|
229
|
+
onClick: () => setPage(safePage - 1),
|
|
230
|
+
children: "Prev"
|
|
231
|
+
}
|
|
232
|
+
),
|
|
233
|
+
/* @__PURE__ */ jsx6(
|
|
234
|
+
Button,
|
|
235
|
+
{
|
|
236
|
+
variant: "ghost",
|
|
237
|
+
size: "sm",
|
|
238
|
+
iconRight: "chevronRight",
|
|
239
|
+
disabled: safePage >= pages - 1,
|
|
240
|
+
onClick: () => setPage(safePage + 1),
|
|
241
|
+
children: "Next"
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
] })
|
|
245
|
+
] });
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
Badge,
|
|
249
|
+
Button,
|
|
250
|
+
Card,
|
|
251
|
+
DataTable,
|
|
252
|
+
Icon,
|
|
253
|
+
Modal
|
|
254
|
+
};
|