@apliteni/apliteni-ui 0.7.2 β 0.8.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 +88 -19
- package/package.json +20 -11
- package/react/README.md +47 -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/segmented.css +3 -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,34 @@ 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. Details in [`react/README.md`](./react/README.md).
|
|
109
|
+
|
|
70
110
|
## Theming
|
|
71
111
|
|
|
72
112
|
Theme is a `data-theme="dark|light"` attribute on `<html>`; accent is an orthogonal
|
|
@@ -98,7 +138,9 @@ src/
|
|
|
98
138
|
assets/ # brand mark (seedling) + line-icon set
|
|
99
139
|
components/ # HTML-string factories: button(), card(), badge(), topbar()β¦
|
|
100
140
|
stories/ # Storybook: Foundations, Components, Apps
|
|
101
|
-
site/ # ui.apli.tech landing page
|
|
141
|
+
site/ # ui.apli.tech landing page (static site build)
|
|
142
|
+
react/ # React components β private workspace, built to react/dist/
|
|
143
|
+
dist/ # tsup output; shipped as β¦/react and β¦/react/css
|
|
102
144
|
```
|
|
103
145
|
|
|
104
146
|
## Develop
|
|
@@ -107,12 +149,21 @@ site/ # ui.apli.tech landing page + static server
|
|
|
107
149
|
npm install
|
|
108
150
|
npm run storybook # http://localhost:6006
|
|
109
151
|
npm run build-storybook # -> storybook-static/
|
|
110
|
-
node site/build.mjs # -> site/public/ (landing + kit.css)
|
|
152
|
+
node site/build.mjs # -> site/public/ (landing + kit.css + /storybook)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The React components build and test through the workspace (`npm install` at the repo
|
|
156
|
+
root covers them β there is no second install):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm run storybook -w react # http://localhost:6007
|
|
160
|
+
npm test -w react # vitest
|
|
161
|
+
npm run build # tsup -> react/dist/ (also runs on prepare)
|
|
111
162
|
```
|
|
112
163
|
|
|
113
164
|
## Publish (public npm)
|
|
114
165
|
|
|
115
|
-
Versioned publish runs from CI on a GitHub Release
|
|
166
|
+
Versioned publish runs from CI on a GitHub Release:
|
|
116
167
|
|
|
117
168
|
```bash
|
|
118
169
|
npm version patch # or minor / major β bumps package.json + tags
|
|
@@ -120,17 +171,34 @@ git push --follow-tags
|
|
|
120
171
|
gh release create v$(node -p "require('./package.json').version") --generate-notes
|
|
121
172
|
```
|
|
122
173
|
|
|
123
|
-
The **Release** workflow (`.github/workflows/release.yml`) then publishes
|
|
124
|
-
|
|
174
|
+
The **Release** workflow (`.github/workflows/release.yml`) then publishes to the public
|
|
175
|
+
npm registry over npm Trusted Publishing (OIDC) β there is no long-lived token. No
|
|
176
|
+
manual `npm publish` needed. It runs in two jobs: `build` installs and runs `npm pack`,
|
|
177
|
+
whose `prepare` rebuilds `react/dist` from the tagged commit, and `publish` β the only
|
|
178
|
+
job that can mint an OIDC credential β just publishes that tarball, so no dependency
|
|
179
|
+
or build script ever runs beside the credential. The packaging guard
|
|
180
|
+
(`scripts/packaging.test.js`) fails CI if the React subpath isn't in the tarball, and
|
|
181
|
+
the release itself re-checks the tarball before publishing it.
|
|
125
182
|
|
|
126
183
|
## Deploy (ui.apli.tech)
|
|
127
184
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
185
|
+
The site is **100% static** (landing + hosted Storybook) β no container, no registry.
|
|
186
|
+
It's served by **Lessly static hosting**, built straight from this repo. The `site`
|
|
187
|
+
service builds from `main` with:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
npm ci && npm run build-storybook && node site/build.mjs
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
and serves `site/public/`, which includes the landing page, `/changelog`, `kit.css`,
|
|
194
|
+
and the Storybook folded in at `/storybook`. Push to `main` and redeploy the `site`
|
|
195
|
+
service to roll it out.
|
|
196
|
+
|
|
197
|
+
To reproduce the exact static bundle locally:
|
|
131
198
|
|
|
132
199
|
```bash
|
|
133
|
-
|
|
200
|
+
npm ci && npm run build-storybook && node site/build.mjs
|
|
201
|
+
# -> site/public/ (landing + /changelog + /storybook + kit.css)
|
|
134
202
|
```
|
|
135
203
|
|
|
136
204
|
## Adopting into the strategy portal
|
|
@@ -143,4 +211,5 @@ stays self-contained for the claude.ai Artifact CSP, baking tokens in via its bu
|
|
|
143
211
|
|
|
144
212
|
## License
|
|
145
213
|
|
|
146
|
-
[MIT](./LICENSE) Β© Apliteni
|
|
214
|
+
[MIT](./LICENSE) Β© Apliteni β for the **code**. The Apliteni name, logos, and brand
|
|
215
|
+
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.0",
|
|
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,47 @@
|
|
|
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
|
+
The bare `@apliteni/apliteni-ui` specifier in this source resolves to the kit itself
|
|
45
|
+
once installed. In the repo there is no copy to resolve to, so `kit-alias.ts` points
|
|
46
|
+
vitest and Storybook straight at `../src/` β which is why the class-name parity tests
|
|
47
|
+
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
|
+
};
|