@aria-framework/theme 0.3.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 ADDED
@@ -0,0 +1,113 @@
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`.
package/build.js ADDED
@@ -0,0 +1,173 @@
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");
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Theme provider — resolves the active palette (Ops Light / Ops Dark) from the
3
+ * user's preference (light | dark | system) and the OS appearance, and persists
4
+ * the choice in AsyncStorage. Screens read `const { colors } = useTheme()`.
5
+ */
6
+ import React from "react";
7
+ import { type Palette } from "./tokens";
8
+ export type ThemePref = "light" | "dark" | "system";
9
+ type Scheme = "light" | "dark";
10
+ interface ThemeState {
11
+ colors: Palette;
12
+ scheme: Scheme;
13
+ pref: ThemePref;
14
+ setPref: (p: ThemePref) => void;
15
+ }
16
+ export declare function ThemeProvider({ children, storageKey }: {
17
+ children: React.ReactNode;
18
+ storageKey?: string;
19
+ }): React.JSX.Element;
20
+ export declare function useTheme(): ThemeState;
21
+ export {};
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ThemeProvider = ThemeProvider;
7
+ exports.useTheme = useTheme;
8
+ const jsx_runtime_1 = require("react/jsx-runtime");
9
+ /**
10
+ * Theme provider — resolves the active palette (Ops Light / Ops Dark) from the
11
+ * user's preference (light | dark | system) and the OS appearance, and persists
12
+ * the choice in AsyncStorage. Screens read `const { colors } = useTheme()`.
13
+ */
14
+ const react_1 = require("react");
15
+ const react_native_1 = require("react-native");
16
+ const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
17
+ const tokens_1 = require("./tokens");
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. */
20
+ const DEFAULT_KEY = "field-ops.theme";
21
+ const Ctx = (0, react_1.createContext)(undefined);
22
+ function ThemeProvider({ children, storageKey = DEFAULT_KEY }) {
23
+ const [pref, setPrefState] = (0, react_1.useState)("system");
24
+ const [sysScheme, setSysScheme] = (0, react_1.useState)(react_native_1.Appearance.getColorScheme() === "dark" ? "dark" : "light");
25
+ // OS appearance listener — independent of storageKey, so subscribe exactly once.
26
+ (0, react_1.useEffect)(() => {
27
+ const sub = react_native_1.Appearance.addChangeListener(({ colorScheme }) => setSysScheme(colorScheme === "dark" ? "dark" : "light"));
28
+ return () => sub.remove();
29
+ }, []);
30
+ // Load the persisted preference (re-reads only if the storage key changes).
31
+ (0, react_1.useEffect)(() => {
32
+ async_storage_1.default.getItem(storageKey).then((v) => {
33
+ if (v === "light" || v === "dark" || v === "system")
34
+ setPrefState(v);
35
+ }).catch(() => { });
36
+ }, [storageKey]);
37
+ const setPref = (0, react_1.useCallback)((p) => {
38
+ setPrefState(p);
39
+ async_storage_1.default.setItem(storageKey, p).catch(() => { });
40
+ }, [storageKey]);
41
+ const scheme = pref === "system" ? sysScheme : pref;
42
+ const value = (0, react_1.useMemo)(() => ({ colors: tokens_1.palettes[scheme], scheme, pref, setPref }), [scheme, pref, setPref]);
43
+ return (0, jsx_runtime_1.jsx)(Ctx.Provider, { value: value, children: children });
44
+ }
45
+ function useTheme() {
46
+ const v = (0, react_1.useContext)(Ctx);
47
+ if (!v)
48
+ throw new Error("useTheme must be used within ThemeProvider");
49
+ return v;
50
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Font families — "Field Ops Console" uses Hanken Grotesk (UI) + IBM Plex Mono
3
+ * (data). @expo-google-fonts ships each weight as its own family name, so weighted
4
+ * text must pick the matching family via hanken() rather than relying on fontWeight.
5
+ */
6
+ export declare const FONT: {
7
+ r: string;
8
+ m: string;
9
+ sb: string;
10
+ b: string;
11
+ xb: string;
12
+ monoR: string;
13
+ mono: string;
14
+ };
15
+ export declare function hanken(weight?: string | number): string;
package/dist/fonts.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FONT = void 0;
4
+ exports.hanken = hanken;
5
+ /**
6
+ * Font families — "Field Ops Console" uses Hanken Grotesk (UI) + IBM Plex Mono
7
+ * (data). @expo-google-fonts ships each weight as its own family name, so weighted
8
+ * text must pick the matching family via hanken() rather than relying on fontWeight.
9
+ */
10
+ exports.FONT = {
11
+ r: "HankenGrotesk_400Regular",
12
+ m: "HankenGrotesk_500Medium",
13
+ sb: "HankenGrotesk_600SemiBold",
14
+ b: "HankenGrotesk_700Bold",
15
+ xb: "HankenGrotesk_800ExtraBold",
16
+ monoR: "IBMPlexMono_400Regular",
17
+ mono: "IBMPlexMono_500Medium",
18
+ };
19
+ function hanken(weight) {
20
+ const w = String(weight !== null && weight !== void 0 ? weight : "400");
21
+ if (w === "800")
22
+ return exports.FONT.xb;
23
+ if (w === "700")
24
+ return exports.FONT.b;
25
+ if (w === "600")
26
+ return exports.FONT.sb;
27
+ if (w === "500")
28
+ return exports.FONT.m;
29
+ return exports.FONT.r;
30
+ }
@@ -0,0 +1,5 @@
1
+ /** field-ops-theme (mobile) — barrel re-export for React Native consumers. */
2
+ export * from "./tokens";
3
+ export * from "./fonts";
4
+ export * from "./ThemeContext";
5
+ export * from "./ui";
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /** field-ops-theme (mobile) — barrel re-export for React Native consumers. */
18
+ __exportStar(require("./tokens"), exports);
19
+ __exportStar(require("./fonts"), exports);
20
+ __exportStar(require("./ThemeContext"), exports);
21
+ __exportStar(require("./ui"), exports);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Field Ops Console — shared visual tokens (React Native side).
3
+ * `palettes.light|dark` are consumed by ThemeContext; `colors` is the light
4
+ * default kept for back-compat with screens that import it directly.
5
+ */
6
+ export type Palette = {
7
+ bg: string;
8
+ surface: string;
9
+ surface2: string;
10
+ border: string;
11
+ lineStrong: string;
12
+ text: string;
13
+ muted: string;
14
+ textFaint: string;
15
+ primary: string;
16
+ primaryText: string;
17
+ brandDeep: string;
18
+ brandBright: string;
19
+ danger: string;
20
+ success: string;
21
+ signal: string;
22
+ warnBg: string;
23
+ warnText: string;
24
+ brandTint: string;
25
+ signalTint: string;
26
+ };
27
+ export declare const palettes: {
28
+ light: Palette;
29
+ dark: Palette;
30
+ };
31
+ /** Back-compat default (light). */
32
+ export declare const colors: Palette;
33
+ export declare const status: Record<string, string>;
34
+ export declare const priority: Record<string, string>;
35
+ export declare const space: {
36
+ xs: number;
37
+ sm: number;
38
+ md: number;
39
+ lg: number;
40
+ xl: number;
41
+ };
42
+ /** Subtle card elevation per scheme — RN has no box-shadow string, so this mirrors
43
+ * the web `--shadow` token as style props (iOS shadow*; Android elevation). */
44
+ export declare const shadow: {
45
+ readonly light: {
46
+ readonly shadowColor: "#101820";
47
+ readonly shadowOpacity: 0.08;
48
+ readonly shadowRadius: 8;
49
+ readonly shadowOffset: {
50
+ readonly width: 0;
51
+ readonly height: 2;
52
+ };
53
+ readonly elevation: 2;
54
+ };
55
+ readonly dark: {
56
+ readonly shadowColor: "#000000";
57
+ readonly shadowOpacity: 0.45;
58
+ readonly shadowRadius: 10;
59
+ readonly shadowOffset: {
60
+ readonly width: 0;
61
+ readonly height: 3;
62
+ };
63
+ readonly elevation: 3;
64
+ };
65
+ };
package/dist/tokens.js ADDED
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shadow = exports.space = exports.priority = exports.status = exports.colors = exports.palettes = void 0;
4
+ const light = {
5
+ bg: "#f4f5f2",
6
+ surface: "#ffffff",
7
+ surface2: "#fafbf9",
8
+ border: "#dcdad2",
9
+ lineStrong: "#cbc8bc",
10
+ text: "#18222a",
11
+ muted: "#5c6770",
12
+ textFaint: "#929ba1",
13
+ primary: "#0e7a6e",
14
+ primaryText: "#ffffff",
15
+ brandDeep: "#0a5c53",
16
+ brandBright: "#15b8a6",
17
+ danger: "#dc2626",
18
+ success: "#0e9e6e",
19
+ signal: "#e2710b",
20
+ warnBg: "#fcefd9",
21
+ warnText: "#8a4b08",
22
+ brandTint: "rgba(14,122,110,0.10)",
23
+ signalTint: "rgba(226,113,11,0.12)",
24
+ };
25
+ const dark = {
26
+ bg: "#0c1215",
27
+ surface: "#141c21",
28
+ surface2: "#192329",
29
+ border: "#243038",
30
+ lineStrong: "#3a4854",
31
+ text: "#e7edef",
32
+ muted: "#9ba7ae",
33
+ textFaint: "#69767d",
34
+ primary: "#16b5a3",
35
+ primaryText: "#08110f",
36
+ brandDeep: "#0e7a6e",
37
+ brandBright: "#2fe0cc",
38
+ danger: "#f05252",
39
+ success: "#22b583",
40
+ signal: "#f2871f",
41
+ warnBg: "#2a2114",
42
+ warnText: "#f2c879",
43
+ brandTint: "rgba(22,181,163,0.14)",
44
+ signalTint: "rgba(242,135,31,0.16)",
45
+ };
46
+ exports.palettes = { light, dark };
47
+ /** Back-compat default (light). */
48
+ exports.colors = light;
49
+ exports.status = {
50
+ new: "#2563eb",
51
+ open: "#0e8fa8",
52
+ pending: "#d9870b",
53
+ resolved: "#0e9e6e",
54
+ closed: "#7a848b",
55
+ };
56
+ exports.priority = {
57
+ low: "#9aa6ae",
58
+ medium: "#2563eb",
59
+ high: "#ea580c",
60
+ critical: "#dc2626",
61
+ };
62
+ exports.space = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 };
63
+ /** Subtle card elevation per scheme — RN has no box-shadow string, so this mirrors
64
+ * the web `--shadow` token as style props (iOS shadow*; Android elevation). */
65
+ exports.shadow = {
66
+ light: { shadowColor: "#101820", shadowOpacity: 0.08, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 2 },
67
+ dark: { shadowColor: "#000000", shadowOpacity: 0.45, shadowRadius: 10, shadowOffset: { width: 0, height: 3 }, elevation: 3 },
68
+ };
package/dist/ui.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ /** Small shared UI primitives (kept in one file to limit screen boilerplate).
2
+ * All are theme-aware via useTheme()/useStyles(). */
3
+ import React from "react";
4
+ import { ViewStyle, TextStyle, LayoutChangeEvent } from "react-native";
5
+ import { type Palette } from "./tokens";
6
+ /** Approx height of the floating bottom tab bar (excludes the device inset, which
7
+ * the bar adds on top via useSafeAreaInsets). */
8
+ export declare const TAB_BAR_HEIGHT = 60;
9
+ /** Bottom padding for a scroll container so its last content clears the gesture
10
+ * bar — and, on the four tab screens, the floating tab bar too. Pass onTab=true
11
+ * for Home/Tickets/Visits/Customers. */
12
+ export declare function useBottomPad(onTab?: boolean): number;
13
+ /** IBM Plex Mono for data (IDs, time, distances, GPS) — loaded in App.tsx. */
14
+ export declare const MONO: string;
15
+ declare function makeStyles(c: Palette): any;
16
+ export type Styles = ReturnType<typeof makeStyles>;
17
+ /** Theme-aware shared stylesheet — `const styles = useStyles()`. */
18
+ export declare function useStyles(): Styles;
19
+ export declare function Card({ children, style, onLayout }: {
20
+ children: React.ReactNode;
21
+ style?: ViewStyle;
22
+ onLayout?: (e: LayoutChangeEvent) => void;
23
+ }): React.JSX.Element;
24
+ export declare function Btn({ title, onPress, loading, disabled, variant, }: {
25
+ title: string;
26
+ onPress: () => void;
27
+ loading?: boolean;
28
+ disabled?: boolean;
29
+ variant?: "primary" | "outline" | "danger" | "signal";
30
+ }): React.JSX.Element;
31
+ export declare function Field({ label, value, onChangeText, placeholder, secureTextEntry, keyboardType, multiline, autoCapitalize, }: {
32
+ label?: string;
33
+ value: string;
34
+ onChangeText: (t: string) => void;
35
+ placeholder?: string;
36
+ secureTextEntry?: boolean;
37
+ keyboardType?: "default" | "numeric" | "email-address";
38
+ multiline?: boolean;
39
+ autoCapitalize?: "none" | "sentences";
40
+ }): React.JSX.Element;
41
+ export declare function Badge({ label, color, filled }: {
42
+ label: string;
43
+ color: string;
44
+ filled?: boolean;
45
+ }): React.JSX.Element;
46
+ export declare function Row({ children, style }: {
47
+ children: React.ReactNode;
48
+ style?: ViewStyle;
49
+ }): React.JSX.Element;
50
+ /** Compact in-screen segmented tab control (not a navigator) — pass the active
51
+ * key and the options; renders an evenly-split pill row. */
52
+ export declare function Segmented<T extends string>({ options, value, onChange }: {
53
+ options: {
54
+ key: T;
55
+ label: string;
56
+ }[];
57
+ value: T;
58
+ onChange: (k: T) => void;
59
+ }): React.JSX.Element;
60
+ export declare function Muted({ children, style }: {
61
+ children: React.ReactNode;
62
+ style?: TextStyle;
63
+ }): React.JSX.Element;
64
+ /** Monospace data span (tracking IDs, time, distances, coordinates). */
65
+ export declare function Mono({ children, style }: {
66
+ children: React.ReactNode;
67
+ style?: TextStyle;
68
+ }): React.JSX.Element;
69
+ /** Shared checkbox — replaces the hand-rolled 26px boxes duplicated across screens. */
70
+ export declare function Checkbox({ checked, label, sublabel, onToggle }: {
71
+ checked: boolean;
72
+ label: string;
73
+ sublabel?: string;
74
+ onToggle: () => void;
75
+ }): React.JSX.Element;
76
+ /** Pulsing placeholder block for loading states (replaces blank spinners). */
77
+ export declare function Skeleton({ height, width, style }: {
78
+ height?: number;
79
+ width?: ViewStyle["width"];
80
+ style?: ViewStyle;
81
+ }): React.JSX.Element;
82
+ /** iOS-style switch — for settings toggles (on/off state, not a form choice). */
83
+ export declare function Switch({ value, onToggle }: {
84
+ value: boolean;
85
+ onToggle: () => void;
86
+ }): React.JSX.Element;
87
+ export {};