@aria-framework/theme 0.3.0 → 0.4.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 CHANGED
@@ -1,113 +1,174 @@
1
- # field-ops-theme
2
-
3
- Shared **Field Ops Console** design system for apps on the Bootstrap 5.3 (web) +
4
- Expo / React Native (mobile) stack. One source of record for the brand tokens,
5
- the web `theme.css`, and the React Native palette / theme provider / UI primitives.
6
- Light + dark.
7
-
8
- > **`tokens.json` is the single source of record.** A tiny generator (`build.js`)
9
- > emits BOTH `web/theme.css` (CSS custom properties) and `mobile/tokens.ts` (the RN
10
- > palette) from it, so the two platforms can never drift. Edit `tokens.json`, run
11
- > `npm run build`, commit the regenerated outputs, and bump the package version
12
- > (semver). Consuming apps pin a version and `npm update` to adopt.
13
-
14
- ## Editing tokens (the generator)
15
-
16
- 1. Edit values in **`tokens.json`** (palettes.light / palettes.dark, plus status /
17
- priority / space). Hex is stored uppercase; `--brand-rgb` is derived automatically.
18
- 2. Run **`npm run build`** (`node build.js`). It regenerates:
19
- - `web/theme.css` — the `:root` + `[data-bs-theme="dark"]` token blocks (the rest
20
- of the file — Bootstrap mapping, edge-clarity, buttons, a11y — is static template).
21
- - `mobile/tokens.ts` `Palette`, `palettes`, `colors`, `status`, `priority`,
22
- `space`, `shadow`.
23
- 3. **Do not hand-edit** `web/theme.css` or `mobile/tokens.ts` (both carry an
24
- AUTO-GENERATED banner). The static CSS (fonts/mapping/buttons/a11y) lives in the
25
- template inside `build.js`; structural CSS changes go there.
26
- 4. Commit the regenerated files (consumers don't run the generator) and bump the version.
27
-
28
- > Note: the generator covers tokens. The RN `fonts.ts` / `ThemeContext.tsx` / `ui.tsx`
29
- > are hand-written (they're code, not values). Consuming the package's RAW `.tsx` on
30
- > mobile out-of-tree needs Metro `watchFolders` (and breaks `tsc` unless the package
31
- > ships compiled JS+`.d.ts`) see the Metro note below.
32
-
33
- ## Install
34
-
35
- Pin by git tag (recommended) or local path:
36
-
37
- ```jsonc
38
- // package.json of the consuming app
39
- "dependencies": {
40
- "field-ops-theme": "github:<you>/field-ops-theme#v0.1.0"
41
- // or, for local dev: "field-ops-theme": "file:../field-ops-theme"
42
- }
43
- ```
44
-
45
- ## Web (Bootstrap 5.3)
46
-
47
- Load order in `<head>` matters — Bootstrap first, then this theme (so its `--bs-*`
48
- overrides win), then your app-specific component CSS:
49
-
50
- ```html
51
- <link rel="stylesheet" href="/vendor/bootstrap.min.css">
52
- <link rel="stylesheet" href="/theme/theme.css"> <!-- field-ops-theme -->
53
- <link rel="stylesheet" href="/css/app.css"> <!-- your components -->
54
- ```
55
-
56
- **Fonts + CSP.** `theme.css` ships self-hosted woff2 in `web/fonts/` and references
57
- them with **relative** URLs (`./fonts/…`). Serve the package's `web/` folder
58
- **same-origin** so they satisfy a strict `Content-Security-Policy: font-src 'self'`
59
- (no CDN). With Express:
60
-
61
- ```js
62
- const themeDir = path.dirname(require.resolve('field-ops-theme/theme.css'));
63
- app.use('/theme', express.static(themeDir)); // → /theme/theme.css + /theme/fonts/*
64
- ```
65
-
66
- Dark mode is driven by a single attribute: `<html data-bs-theme="dark">`.
67
-
68
- ### What's in `theme.css` (generic only)
69
- Design tokens (Ops Light `:root` + Ops Dark `[data-bs-theme="dark"]`), the
70
- `--bs-*` mapping, `--shadow`/`--shadow-lg`, brand buttons (`.btn-primary`,
71
- `.btn-outline-primary`), edge-clarity (cards/inputs use `--line-strong` + shadow;
72
- tables keep the faint `--line` for row dividers), `.mono` helper, focus-visible
73
- and reduced-motion. **App-specific component styles do not belong here** — keep
74
- sidebars, dashboards, page layouts etc. in your app's own CSS.
75
-
76
- ## Mobile (Expo / React Native)
77
-
78
- ```tsx
79
- import { ThemeProvider, useTheme, Card, Btn, Field, palettes } from 'field-ops-theme/mobile';
80
-
81
- export default function App() {
82
- return (
83
- <ThemeProvider storageKey="myapp.theme">{/* namespace per app */}
84
- <Screens />
85
- </ThemeProvider>
86
- );
87
- }
88
-
89
- function Screen() {
90
- const { colors, scheme } = useTheme();
91
- return <Card><Text style={{ color: colors.text }}>Hello</Text></Card>;
92
- }
93
- ```
94
-
95
- Fonts: load `@expo-google-fonts/hanken-grotesk` + `@expo-google-fonts/ibm-plex-mono`
96
- in your app entry; the family names in `mobile/fonts.ts` match those packages.
97
-
98
- Peer deps (provided by the app): `react`, `react-native`,
99
- `@react-native-async-storage/async-storage`.
100
-
101
- > **Metro note:** consuming a `file:` dependency requires Metro to watch the
102
- > package folder. Add it to `watchFolders` in `metro.config.js`, or use a published
103
- > git version to avoid symlink/transpile quirks.
104
-
105
- ## Versioning
106
-
107
- Token change edit `tokens.json`, run `npm run build`, commit the regenerated
108
- `web/theme.css` + `mobile/tokens.ts`, then bump the `version` in `package.json`.
109
-
110
- - **v0.1.0** — initial extraction (refined light borders + defined elevation tokens),
111
- hand-maintained web/mobile.
112
- - **v0.2.0** — added `tokens.json` + `build.js` generator (single source of record;
113
- web + mobile generated from one file). Unified the drifted dark `--line-strong`.
1
+ # @aria-framework/theme
2
+
3
+ Shared **Field Ops Console** design system for apps on the Bootstrap 5.3 (web) +
4
+ Expo / React Native (mobile) stack. One source of record for the brand tokens,
5
+ the web `theme.css` + shell components (sidebar), and the React Native palette /
6
+ theme provider / UI primitives. Light + dark.
7
+
8
+ > **`tokens.json` is the single source of record.** A tiny generator (`build.js`)
9
+ > emits BOTH `web/theme.css` (CSS custom properties) and `mobile/tokens.ts` (the RN
10
+ > palette) from it, so the two platforms can never drift. Edit `tokens.json`, run
11
+ > `npm run build`, commit the regenerated outputs, and bump the package version
12
+ > (semver). Consuming apps pin a version and `npm update` to adopt.
13
+
14
+ ## Editing tokens (the generator)
15
+
16
+ 1. Edit values in **`tokens.json`** (palettes.light / palettes.dark, plus status /
17
+ priority / space). Hex is stored uppercase; `--brand-rgb` is derived automatically.
18
+ 2. Run **`npm run build`** (`node build.js`). It regenerates:
19
+ - `web/theme.css` — the `:root` + `[data-bs-theme="dark"]` token blocks (the rest
20
+ of the file — Bootstrap mapping, edge-clarity, buttons, a11y — is static
21
+ template), **plus every component partial in `src/components/*.css`** appended
22
+ in filename order.
23
+ - `mobile/tokens.ts` `Palette`, `palettes`, `colors`, `status`, `priority`,
24
+ `space`, `shadow`.
25
+ 3. **Do not hand-edit** `web/theme.css` or `mobile/tokens.ts` (both carry an
26
+ AUTO-GENERATED banner). The static base CSS (fonts/mapping/buttons/a11y) lives in
27
+ `src/base.css`; component CSS lives in `src/components/*.css` — build.js emits
28
+ generated tokens base components.
29
+ 4. Commit the regenerated files (consumers don't run the generator) and bump the version.
30
+
31
+ > Note: the generator covers tokens. The RN `fonts.ts` / `ThemeContext.tsx` / `ui.tsx`
32
+ > are hand-written (they're code, not values). Consuming the package's RAW `.tsx` on
33
+ > mobile out-of-tree needs Metro `watchFolders` (and breaks `tsc` unless the package
34
+ > ships compiled JS+`.d.ts`) — see the Metro note below.
35
+
36
+ ## Install
37
+
38
+ Published on the public npm registry:
39
+
40
+ ```bash
41
+ npm install @aria-framework/theme
42
+ ```
43
+
44
+ ## Web (Bootstrap 5.3)
45
+
46
+ Load order in `<head>` matters — Bootstrap first, then this theme (so its `--bs-*`
47
+ overrides win), then your app-specific component CSS:
48
+
49
+ ```html
50
+ <link rel="stylesheet" href="/vendor/bootstrap.min.css">
51
+ <link rel="stylesheet" href="/theme/theme.css"> <!-- @aria-framework/theme -->
52
+ <link rel="stylesheet" href="/css/app.css"> <!-- your components -->
53
+ ```
54
+
55
+ **Fonts + CSP.** `theme.css` ships self-hosted woff2 in `web/fonts/` and references
56
+ them with **relative** URLs (`./fonts/…`). Serve the package's `web/` folder
57
+ **same-origin** so they satisfy a strict `Content-Security-Policy: font-src 'self'`
58
+ (no CDN). With Express:
59
+
60
+ ```js
61
+ const themeDir = path.dirname(require.resolve('@aria-framework/theme/theme.css'));
62
+ app.use('/theme', express.static(themeDir)); // /theme/theme.css + /theme/fonts/*
63
+ ```
64
+
65
+ Dark mode is driven by a single attribute: `<html data-bs-theme="dark">`.
66
+
67
+ ### What's in `theme.css`
68
+ Design tokens (Ops Light `:root` + Ops Dark `[data-bs-theme="dark"]`), the
69
+ `--bs-*` mapping, `--shadow`/`--shadow-lg`, brand buttons (`.btn-primary`,
70
+ `.btn-outline-primary`), edge-clarity (cards/inputs use `--line-strong` + shadow;
71
+ tables keep the faint `--line` for row dividers), `.mono` helper, focus-visible
72
+ and reduced-motion plus the **shell components** below. **Domain-specific styles
73
+ do not belong here** — keep dashboards, page layouts etc. in your app's own CSS.
74
+ Rule of thumb: if a second app would want it unchanged except for content, it
75
+ belongs here; if it knows about your domain, it stays in the app.
76
+
77
+ ### Component: sidebar rail
78
+
79
+ A dark nav rail, identical in light + dark modes. Mobile-first: off-canvas
80
+ overlay under 768px (hamburger + backdrop + Esc/backdrop-tap close, focus
81
+ trapped via `inert`); an in-flow rail on desktop with a persisted collapse to a
82
+ 64px icon rail. The package ships the **CSS** (in `theme.css`) and the
83
+ **behavior** (`sidebar.js`); the app ships the **markup** against this contract:
84
+
85
+ ```html
86
+ <div id="wrapper">
87
+ <nav class="s-side">
88
+ <div class="s-brand">
89
+ <a class="s-brand-link" href="/">
90
+ <span class="s-mark">S</span>
91
+ <span><span class="s-name">MyApp</span><span class="s-ver">v1.0.0</span></span>
92
+ </a>
93
+ <button class="s-collapse" data-sidebar-toggle aria-label="Collapse sidebar"><i class="bi bi-chevron-double-left"></i></button>
94
+ </div>
95
+ <div class="s-nav">
96
+ <div class="s-grp">Section</div>
97
+ <a class="s-link active" href="/x"><i class="bi bi-..."></i><span>Item</span></a>
98
+ </div>
99
+ <div class="s-me">
100
+ <span class="s-av">AB</span>
101
+ <span class="s-me-who"><b>Ann Bee</b><span>ADMIN</span></span>
102
+ <form class="s-out-form" method="post" action="/logout"><button class="s-out" aria-label="Sign out"><i class="bi bi-box-arrow-right"></i></button></form>
103
+ </div>
104
+ </nav>
105
+ <div id="page-content-wrapper">
106
+ <!-- your topbar should include the mobile hamburger: -->
107
+ <button class="topbar-toggle" data-sidebar-toggle aria-label="Open menu"><i class="bi bi-list"></i></button>
108
+ ...
109
+ </div>
110
+ </div>
111
+ <script src="/theme/sidebar.js" data-storage-key="myapp-sidebar-collapsed"></script>
112
+ ```
113
+
114
+ Notes:
115
+ - `data-storage-key` namespaces the persisted collapsed state per app (default
116
+ `aria-sidebar-collapsed`). Keep an app's historical key across migrations.
117
+ - To avoid a flash of the expanded rail on load, pre-apply the class in an early
118
+ script: `if (localStorage.getItem('<key>') === '1') document.documentElement.classList.add('sidebar-collapsed')`.
119
+ - State classes live on `<html>`: `.sidebar-open` (mobile overlay),
120
+ `.sidebar-collapsed` (desktop icon rail).
121
+ - Sidebar tokens are themeable in `tokens.json` under `"sidebar"` (emitted as
122
+ `--side-*`). Icons in the examples are Bootstrap Icons, but any icon font works.
123
+
124
+ ## Mobile (Expo / React Native)
125
+
126
+ ```tsx
127
+ import { ThemeProvider, useTheme, Card, Btn, Field, palettes } from '@aria-framework/theme/mobile';
128
+
129
+ export default function App() {
130
+ return (
131
+ <ThemeProvider storageKey="myapp.theme">{/* namespace per app */}
132
+ <Screens />
133
+ </ThemeProvider>
134
+ );
135
+ }
136
+
137
+ function Screen() {
138
+ const { colors, scheme } = useTheme();
139
+ return <Card><Text style={{ color: colors.text }}>Hello</Text></Card>;
140
+ }
141
+ ```
142
+
143
+ Fonts: load `@expo-google-fonts/hanken-grotesk` + `@expo-google-fonts/ibm-plex-mono`
144
+ in your app entry; the family names in `mobile/fonts.ts` match those packages.
145
+
146
+ Peer deps (provided by the app): `react`, `react-native`,
147
+ `@react-native-async-storage/async-storage`.
148
+
149
+ > **Metro note:** consuming a `file:` dependency requires Metro to watch the
150
+ > package folder. Add it to `watchFolders` in `metro.config.js`, or use a published
151
+ > git version to avoid symlink/transpile quirks.
152
+
153
+ ## Versioning
154
+
155
+ Token change → edit `tokens.json`, run `npm run build`, commit the regenerated
156
+ `web/theme.css` + `mobile/tokens.ts`, then bump the `version` in `package.json`.
157
+
158
+ - **v0.1.0** — initial extraction (refined light borders + defined elevation tokens),
159
+ hand-maintained web/mobile.
160
+ - **v0.2.0** — added `tokens.json` + `build.js` generator (single source of record;
161
+ web + mobile generated from one file). Unified the drifted dark `--line-strong`.
162
+ - **v0.3.0** — renamed to `@aria-framework/theme` and published to npm. Mobile
163
+ sources now compile to `dist/` (.js + `.d.ts`, via a `prepare` script) so RN
164
+ apps can import the package out-of-tree; `exports` point at the built files.
165
+ - **v0.4.0** — first shell component: the **sidebar rail** (CSS + `--side-*`
166
+ tokens + `sidebar.js` behavior with a per-app `data-storage-key`; markup
167
+ contract above). Build now assembles generated tokens → `src/base.css` →
168
+ `src/components/*.css`. Sidebar carries its own focus-visible contrast rule.
169
+ - **v0.4.1** — `sidebar.js` double-include guard (a second script tag no longer
170
+ registers duplicate listeners); this changelog added.
171
+
172
+ > **0.x semver reminder:** `^0.4.0` matches `<0.5.0` only — consumers pick up
173
+ > patches via `npm update`, but every **minor** bump (0.4 → 0.5) requires
174
+ > editing the range in each consuming app.
package/build.js CHANGED
@@ -1,173 +1,133 @@
1
- #!/usr/bin/env node
2
- /* field-ops-theme generator. Reads tokens.json (single source of record) and emits
3
- * - web/theme.css (Bootstrap 5.3 token layer + static mapping/edge/buttons/a11y)
4
- * - mobile/tokens.ts (RN Palette + palettes/colors/status/priority/space/shadow)
5
- * Run: `node build.js`. The two outputs are committed artifacts (consumers don't
6
- * need to run this); regenerate whenever tokens.json changes, then bump the version. */
7
- const fs = require("fs");
8
- const path = require("path");
9
- const T = require("./tokens.json");
10
-
11
- const GEN_CSS = "/* AUTO-GENERATED from tokens.json by build.js — DO NOT EDIT. Change tokens.json, run `node build.js`. */";
12
- const GEN_TS = "// AUTO-GENERATED from tokens.json by build.js — DO NOT EDIT. Change tokens.json, run `node build.js`.";
13
- const FONT_SANS = `'Hanken Grotesk', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`;
14
- const FONT_MONO = `'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace`;
15
-
16
- const hexToRgb = (hex) => {
17
- const n = parseInt(hex.replace("#", ""), 16);
18
- return [(n >> 16) & 255, (n >> 8) & 255, n & 255].join(",");
19
- };
20
-
21
- /* ── web/theme.css ────────────────────────────────────────────────────────── */
22
- const cssTokens = (p) => ` --brand:${p.brand}; --brand-deep:${p.brandDeep}; --brand-bright:${p.brandBright}; --brand-rgb:${hexToRgb(p.brand)};
23
- --signal:${p.signal};
24
- --paper:${p.paper}; --surface:${p.surface}; --surface-2:${p.surface2}; --line:${p.line}; --line-strong:${p.lineStrong};
25
- --text:${p.text}; --text-soft:${p.textSoft}; --text-faint:${p.textFaint};
26
- --shadow:${p.shadowWeb};
27
- --shadow-lg:${p.shadowLgWeb};`;
28
-
29
- const themeCss = `${GEN_CSS}
30
- /* field-ops-theme — Field Ops Console design system (web).
31
- Drop-in token layer for any Bootstrap 5.3 app. Load order in <head>:
32
- bootstrap.min.css → field-ops-theme/theme.css → app.css
33
- Serve web/ same-origin (e.g. mount at /theme) so fonts satisfy CSP font-src 'self'. */
34
-
35
- /* ── Self-hosted fonts (relative URLs resolve under the mount, no CDN) ─────*/
36
- @font-face{font-family:'Hanken Grotesk';font-style:normal;font-weight:100 900;font-display:swap;
37
- src:url('./fonts/hanken-grotesk-wght.woff2') format('woff2')}
38
- @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
39
- src:url('./fonts/ibm-plex-mono-400.woff2') format('woff2')}
40
- @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
41
- src:url('./fonts/ibm-plex-mono-500.woff2') format('woff2')}
42
- @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:600;font-display:swap;
43
- src:url('./fonts/ibm-plex-mono-600.woff2') format('woff2')}
44
-
45
- /* ── Design tokens Ops Light in :root; Ops Dark keyed on [data-bs-theme] ───*/
46
- :root{
47
- --font-sans:${FONT_SANS};
48
- --font-mono:${FONT_MONO};
49
- ${cssTokens(T.palettes.light)}
50
- }
51
- [data-bs-theme="dark"]{
52
- ${cssTokens(T.palettes.dark)}
53
- }
54
-
55
- /* ── Map tokens onto Bootstrap (load AFTER bootstrap.min.css so these win) ───*/
56
- :root{
57
- --bs-body-font-family:var(--font-sans);
58
- --bs-primary:var(--brand); --bs-primary-rgb:var(--brand-rgb);
59
- --bs-link-color:var(--brand); --bs-link-color-rgb:var(--brand-rgb); --bs-link-hover-color:var(--brand-deep);
60
- --bs-body-bg:var(--paper); --bs-body-color:var(--text);
61
- --bs-border-color:var(--line);
62
- }
63
- [data-bs-theme="dark"]{
64
- --bs-primary:var(--brand); --bs-primary-rgb:var(--brand-rgb);
65
- --bs-link-color:var(--brand); --bs-link-color-rgb:var(--brand-rgb); --bs-link-hover-color:var(--brand-bright);
66
- --bs-body-bg:var(--paper); --bs-body-color:var(--text);
67
- --bs-border-color:var(--line);
68
- }
69
- body{font-family:var(--font-sans)}
70
- .mono,.tracking-id,code,kbd,.font-monospace{font-family:var(--font-mono)}
71
-
72
- /* ── Edge clarity — faint --line for internal dividers; --line-strong + shadow
73
- for container edges, so cards/inputs read as raised on white/paper. ────────*/
74
- .card{ border-color:var(--line-strong); box-shadow:var(--shadow); }
75
- .form-control,.form-select{ border-color:var(--line-strong); }
76
- .input-group-text{ border-color:var(--line-strong); }
77
- .table{ --bs-table-border-color:var(--line); }
78
- .list-group{ --bs-list-group-border-color:var(--line-strong); }
79
-
80
- /* ── Brand buttons (component vars compile into Bootstrap; set explicitly) ───*/
81
- .btn-primary{
82
- --bs-btn-bg:var(--brand); --bs-btn-border-color:var(--brand);
83
- --bs-btn-hover-bg:var(--brand-deep); --bs-btn-hover-border-color:var(--brand-deep);
84
- --bs-btn-active-bg:var(--brand-deep); --bs-btn-active-border-color:var(--brand-deep);
85
- --bs-btn-disabled-bg:var(--brand); --bs-btn-disabled-border-color:var(--brand);
86
- }
87
- .btn-outline-primary{
88
- --bs-btn-color:var(--brand); --bs-btn-border-color:var(--brand);
89
- --bs-btn-hover-bg:var(--brand); --bs-btn-hover-border-color:var(--brand);
90
- --bs-btn-active-bg:var(--brand); --bs-btn-active-border-color:var(--brand);
91
- }
92
-
93
- /* ── Accessibility ──────────────────────────────────────────────────────────*/
94
- a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
95
- textarea:focus-visible, [tabindex]:focus-visible {
96
- outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 6px;
97
- }
98
- @media (prefers-reduced-motion: reduce) {
99
- *, *::before, *::after {
100
- animation-duration: .001ms !important; animation-iteration-count: 1 !important;
101
- transition-duration: .001ms !important; scroll-behavior: auto !important;
102
- }
103
- }
104
- `;
105
-
106
- /* ── mobile/tokens.ts ─────────────────────────────────────────────────────── */
107
- const lc = (h) => h.toLowerCase();
108
- const rnPalette = (p) => `{
109
- bg: "${lc(p.paper)}",
110
- surface: "${lc(p.surface)}",
111
- surface2: "${lc(p.surface2)}",
112
- border: "${lc(p.line)}",
113
- lineStrong: "${lc(p.lineStrong)}",
114
- text: "${lc(p.text)}",
115
- muted: "${lc(p.textSoft)}",
116
- textFaint: "${lc(p.textFaint)}",
117
- primary: "${lc(p.brand)}",
118
- primaryText: "${lc(p.primaryText)}",
119
- brandDeep: "${lc(p.brandDeep)}",
120
- brandBright: "${lc(p.brandBright)}",
121
- danger: "${lc(p.danger)}",
122
- success: "${lc(p.success)}",
123
- signal: "${lc(p.signal)}",
124
- warnBg: "${lc(p.warnBg)}",
125
- warnText: "${lc(p.warnText)}",
126
- brandTint: "${p.brandTint}",
127
- signalTint: "${p.signalTint}",
128
- }`;
129
- const rnShadow = (s) => `{ shadowColor: "${lc(s.shadowColor)}", shadowOpacity: ${s.shadowOpacity}, shadowRadius: ${s.shadowRadius}, shadowOffset: { width: ${s.shadowOffset.width}, height: ${s.shadowOffset.height} }, elevation: ${s.elevation} }`;
130
- const rec = (o) => "{\n" + Object.entries(o).map(([k, v]) => ` ${k}: "${v}",`).join("\n") + "\n}";
131
-
132
- const tokensTs = `${GEN_TS}
133
- /**
134
- * Field Ops Console — shared visual tokens (React Native side).
135
- * \`palettes.light|dark\` are consumed by ThemeContext; \`colors\` is the light
136
- * default kept for back-compat with screens that import it directly.
137
- */
138
- export type Palette = {
139
- bg: string; surface: string; surface2: string; border: string; lineStrong: string;
140
- text: string; muted: string; textFaint: string;
141
- primary: string; primaryText: string; brandDeep: string; brandBright: string;
142
- danger: string; success: string; signal: string;
143
- warnBg: string; warnText: string;
144
- brandTint: string; signalTint: string;
145
- };
146
-
147
- const light: Palette = ${rnPalette(T.palettes.light)};
148
-
149
- const dark: Palette = ${rnPalette(T.palettes.dark)};
150
-
151
- export const palettes = { light, dark };
152
-
153
- /** Back-compat default (light). */
154
- export const colors = light;
155
-
156
- export const status: Record<string, string> = ${rec(T.status)};
157
-
158
- export const priority: Record<string, string> = ${rec(T.priority)};
159
-
160
- export const space = { ${Object.entries(T.space).map(([k, v]) => `${k}: ${v}`).join(", ")} };
161
-
162
- /** Subtle card elevation per scheme — RN has no box-shadow string, so this mirrors
163
- * the web \`--shadow\` token as style props (iOS shadow*; Android elevation). */
164
- export const shadow = {
165
- light: ${rnShadow(T.palettes.light.shadowRn)},
166
- dark: ${rnShadow(T.palettes.dark.shadowRn)},
167
- } as const;
168
- `;
169
-
170
- /* ── write ────────────────────────────────────────────────────────────────── */
171
- fs.writeFileSync(path.join(__dirname, "web", "theme.css"), themeCss);
172
- fs.writeFileSync(path.join(__dirname, "mobile", "tokens.ts"), tokensTs);
173
- console.log("field-ops-theme: generated web/theme.css + mobile/tokens.ts from tokens.json");
1
+ #!/usr/bin/env node
2
+ /* @aria-framework/theme generator. Reads tokens.json (single source of record) and emits
3
+ * - web/theme.css (Bootstrap 5.3 token layer + static mapping/edge/buttons/a11y)
4
+ * - mobile/tokens.ts (RN Palette + palettes/colors/status/priority/space/shadow)
5
+ * Run: `node build.js`. The two outputs are committed artifacts (consumers don't
6
+ * need to run this); regenerate whenever tokens.json changes, then bump the version. */
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const T = require("./tokens.json");
10
+
11
+ const GEN_CSS = "/* AUTO-GENERATED from tokens.json by build.js — DO NOT EDIT. Change tokens.json, run `node build.js`. */";
12
+ const GEN_TS = "// AUTO-GENERATED from tokens.json by build.js — DO NOT EDIT. Change tokens.json, run `node build.js`.";
13
+ const FONT_SANS = `'Hanken Grotesk', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`;
14
+ const FONT_MONO = `'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace`;
15
+
16
+ const hexToRgb = (hex) => {
17
+ const n = parseInt(hex.replace("#", ""), 16);
18
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255].join(",");
19
+ };
20
+
21
+ // Fail fast with a real message rather than a TypeError from inside a template.
22
+ for (const key of ["palettes", "sidebar", "status", "priority", "space"]) {
23
+ if (!T[key]) throw new Error(`tokens.json is missing the "${key}" section (required since theme 0.4.0)`);
24
+ }
25
+
26
+ /* ── web/theme.css ────────────────────────────────────────────────────────── */
27
+ const cssTokens = (p) => ` --brand:${p.brand}; --brand-deep:${p.brandDeep}; --brand-bright:${p.brandBright}; --brand-rgb:${hexToRgb(p.brand)};
28
+ --signal:${p.signal};
29
+ --paper:${p.paper}; --surface:${p.surface}; --surface-2:${p.surface2}; --line:${p.line}; --line-strong:${p.lineStrong};
30
+ --text:${p.text}; --text-soft:${p.textSoft}; --text-faint:${p.textFaint};
31
+ --shadow:${p.shadowWeb};
32
+ --shadow-lg:${p.shadowLgWeb};`;
33
+
34
+ const themeCss = `${GEN_CSS}
35
+ /* @aria-framework/theme Field Ops Console design system (web).
36
+ Drop-in token layer for any Bootstrap 5.3 app. Load order in <head>:
37
+ bootstrap.min.css → theme.css → app.css
38
+ Serve web/ same-origin (e.g. mount at /theme) so fonts satisfy CSP font-src 'self'. */
39
+
40
+ /* ── Design tokens — Ops Light in :root; Ops Dark keyed on [data-bs-theme] ───*/
41
+ :root{
42
+ --font-sans:${FONT_SANS};
43
+ --font-mono:${FONT_MONO};
44
+ ${cssTokens(T.palettes.light)}
45
+ /* Sidebar rail (scheme-independent: dark in both modes) */
46
+ --side-bg1:${T.sidebar.bg1}; --side-bg2:${T.sidebar.bg2}; --side-tx:${T.sidebar.tx}; --side-muted:${T.sidebar.muted};
47
+ --side-active:${T.sidebar.active}; --side-line:${T.sidebar.line}; --side-card:${T.sidebar.card};
48
+ }
49
+ [data-bs-theme="dark"]{
50
+ ${cssTokens(T.palettes.dark)}
51
+ }`;
52
+
53
+ /* ── mobile/tokens.ts ─────────────────────────────────────────────────────── */
54
+ const lc = (h) => h.toLowerCase();
55
+ const rnPalette = (p) => `{
56
+ bg: "${lc(p.paper)}",
57
+ surface: "${lc(p.surface)}",
58
+ surface2: "${lc(p.surface2)}",
59
+ border: "${lc(p.line)}",
60
+ lineStrong: "${lc(p.lineStrong)}",
61
+ text: "${lc(p.text)}",
62
+ muted: "${lc(p.textSoft)}",
63
+ textFaint: "${lc(p.textFaint)}",
64
+ primary: "${lc(p.brand)}",
65
+ primaryText: "${lc(p.primaryText)}",
66
+ brandDeep: "${lc(p.brandDeep)}",
67
+ brandBright: "${lc(p.brandBright)}",
68
+ danger: "${lc(p.danger)}",
69
+ success: "${lc(p.success)}",
70
+ signal: "${lc(p.signal)}",
71
+ warnBg: "${lc(p.warnBg)}",
72
+ warnText: "${lc(p.warnText)}",
73
+ brandTint: "${p.brandTint}",
74
+ signalTint: "${p.signalTint}",
75
+ }`;
76
+ const rnShadow = (s) => `{ shadowColor: "${lc(s.shadowColor)}", shadowOpacity: ${s.shadowOpacity}, shadowRadius: ${s.shadowRadius}, shadowOffset: { width: ${s.shadowOffset.width}, height: ${s.shadowOffset.height} }, elevation: ${s.elevation} }`;
77
+ const rec = (o) => "{\n" + Object.entries(o).map(([k, v]) => ` ${k}: "${v}",`).join("\n") + "\n}";
78
+
79
+ const tokensTs = `${GEN_TS}
80
+ /**
81
+ * Field Ops Console — shared visual tokens (React Native side).
82
+ * \`palettes.light|dark\` are consumed by ThemeContext; \`colors\` is the light
83
+ * default kept for back-compat with screens that import it directly.
84
+ */
85
+ export type Palette = {
86
+ bg: string; surface: string; surface2: string; border: string; lineStrong: string;
87
+ text: string; muted: string; textFaint: string;
88
+ primary: string; primaryText: string; brandDeep: string; brandBright: string;
89
+ danger: string; success: string; signal: string;
90
+ warnBg: string; warnText: string;
91
+ brandTint: string; signalTint: string;
92
+ };
93
+
94
+ const light: Palette = ${rnPalette(T.palettes.light)};
95
+
96
+ const dark: Palette = ${rnPalette(T.palettes.dark)};
97
+
98
+ export const palettes = { light, dark };
99
+
100
+ /** Back-compat default (light). */
101
+ export const colors = light;
102
+
103
+ export const status: Record<string, string> = ${rec(T.status)};
104
+
105
+ export const priority: Record<string, string> = ${rec(T.priority)};
106
+
107
+ export const space = { ${Object.entries(T.space).map(([k, v]) => `${k}: ${v}`).join(", ")} };
108
+
109
+ /** Subtle card elevation per scheme — RN has no box-shadow string, so this mirrors
110
+ * the web \`--shadow\` token as style props (iOS shadow*; Android elevation). */
111
+ export const shadow = {
112
+ light: ${rnShadow(T.palettes.light.shadowRn)},
113
+ dark: ${rnShadow(T.palettes.dark.shadowRn)},
114
+ } as const;
115
+ `;
116
+
117
+ /* ── assemble theme.css: generated tokens → src/base.css → src/components/* ──
118
+ base.css is the static layer (fonts, Bootstrap map, edge-clarity, buttons,
119
+ a11y). Each src/components/*.css is a self-contained component (markup
120
+ contract in the file header + README), appended in filename order so output
121
+ is deterministic. Everything references the generated tokens above. */
122
+ const baseCss = fs.readFileSync(path.join(__dirname, "src", "base.css"), "utf8").trimEnd();
123
+ const compDir = path.join(__dirname, "src", "components");
124
+ const components = fs.existsSync(compDir)
125
+ ? fs.readdirSync(compDir).filter((f) => f.endsWith(".css")).sort()
126
+ .map((f) => fs.readFileSync(path.join(compDir, f), "utf8").trimEnd())
127
+ : [];
128
+ const fullCss = [themeCss.trimEnd(), baseCss, ...components].join("\n\n") + "\n";
129
+
130
+ /* ── write ────────────────────────────────────────────────────────────────── */
131
+ fs.writeFileSync(path.join(__dirname, "web", "theme.css"), fullCss);
132
+ fs.writeFileSync(path.join(__dirname, "mobile", "tokens.ts"), tokensTs);
133
+ console.log(`@aria-framework/theme: generated web/theme.css (+${components.length} component(s)) + mobile/tokens.ts from tokens.json`);
@@ -16,7 +16,7 @@ const react_native_1 = require("react-native");
16
16
  const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
17
17
  const tokens_1 = require("./tokens");
18
18
  /** Default AsyncStorage key. Override per-app via <ThemeProvider storageKey="myapp.theme">
19
- * so multiple field-ops-theme apps on a device don't share one preference. */
19
+ * so multiple aria-framework apps on a device don't share one preference. */
20
20
  const DEFAULT_KEY = "field-ops.theme";
21
21
  const Ctx = (0, react_1.createContext)(undefined);
22
22
  function ThemeProvider({ children, storageKey = DEFAULT_KEY }) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** field-ops-theme (mobile) — barrel re-export for React Native consumers. */
1
+ /** @aria-framework/theme (mobile) — barrel re-export for React Native consumers. */
2
2
  export * from "./tokens";
3
3
  export * from "./fonts";
4
4
  export * from "./ThemeContext";