@lcars-gui/romulus 0.0.0-stage → 0.1.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.
@@ -0,0 +1,221 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, HTMLAttributes, ReactNode, InputHTMLAttributes } from 'react';
3
+ import { LcarsSoundKind } from '@lcars-gui/core';
4
+ export { LcarsSoundKind } from '@lcars-gui/core';
5
+ import { SoundProviderProps } from '@lcars-gui/core/react';
6
+ export { SoundProviderProps, useLcarsSound } from '@lcars-gui/core/react';
7
+
8
+ type RomulusColor = 1 | 2 | 3 | 4 | 5 | 6;
9
+ type RomulusLength = "xs" | "s" | "m" | "l" | "xl";
10
+ type RomulusTheme = "warbird" | "tal-shiar" | "free-state";
11
+ type AlertLevel = "red" | "yellow";
12
+ type StatusKind = "ok" | "warn" | "error" | "off";
13
+ type Orientation = "horizontal" | "vertical";
14
+
15
+ type ButtonSize = "xs" | "s" | "m" | "l" | "xl";
16
+ type ButtonShape = "chamfer" | "rect" | "point-left" | "point-right";
17
+ type ButtonVariant = "outline" | "solid";
18
+ type ButtonAlign = "center" | "start";
19
+ /**
20
+ * Platform-neutral button props (without events/children,
21
+ * those come from the respective renderer).
22
+ */
23
+ interface ButtonProps$1 {
24
+ color?: RomulusColor;
25
+ size?: ButtonSize;
26
+ length?: RomulusLength;
27
+ shape?: ButtonShape;
28
+ variant?: ButtonVariant;
29
+ align?: ButtonAlign;
30
+ disabled?: boolean;
31
+ }
32
+
33
+ type BarCap = "left" | "right" | "both";
34
+ interface BarProps$1 {
35
+ color?: RomulusColor;
36
+ cap?: BarCap;
37
+ length?: RomulusLength;
38
+ segmented?: boolean;
39
+ }
40
+ interface BlockProps$1 {
41
+ color?: RomulusColor;
42
+ fill?: boolean;
43
+ }
44
+
45
+ type ButtonProps = ButtonProps$1 & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color"> & {
46
+ /** Sound on click (only inside a SoundProvider); false = silent */
47
+ sound?: LcarsSoundKind | false;
48
+ };
49
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps$1 & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color"> & {
50
+ /** Sound on click (only inside a SoundProvider); false = silent */
51
+ sound?: LcarsSoundKind | false;
52
+ } & react.RefAttributes<HTMLButtonElement>>;
53
+
54
+ type DivProps$1 = Omit<HTMLAttributes<HTMLDivElement>, "color">;
55
+ type BarProps = BarProps$1 & DivProps$1;
56
+ /** Glowing line, optionally with pointed ends or dashed. */
57
+ declare function Bar({ color, cap, length, segmented, className, ...rest }: BarProps): react.JSX.Element;
58
+ type BlockProps = BlockProps$1 & DivProps$1;
59
+ /** Filled segment with chamfered corners; the label sits bottom left. */
60
+ declare function Block({ color, fill, className, ...rest }: BlockProps): react.JSX.Element;
61
+ type TitleProps = {
62
+ color?: RomulusColor;
63
+ } & Omit<HTMLAttributes<HTMLHeadingElement>, "color">;
64
+ /** Letter-spaced title in the header. */
65
+ declare function Title({ color, className, ...rest }: TitleProps): react.JSX.Element;
66
+ /** Vertical stack of buttons/blocks with even gaps. */
67
+ declare function Stack({ className, ...rest }: HTMLAttributes<HTMLDivElement>): react.JSX.Element;
68
+ interface PanelProps extends Omit<DivProps$1, "title"> {
69
+ color?: RomulusColor;
70
+ title?: ReactNode;
71
+ /** ID code in the filled trapezoid at the start of the header, e.g. "R-7734" */
72
+ code?: ReactNode;
73
+ /** Sidebar content (buttons/blocks); filled up to the bottom */
74
+ sidebar?: ReactNode;
75
+ /** Replaces the default line in the header or footer; footer={null} hides the footer */
76
+ header?: ReactNode;
77
+ footer?: ReactNode;
78
+ }
79
+ /** Romulan console: chamfered corners, talon corners, header with ID code, optional sidebar. */
80
+ declare function Panel({ color, title, code, sidebar, header, footer, className, children, ...rest }: PanelProps): react.JSX.Element;
81
+
82
+ interface ToggleProps extends ButtonProps {
83
+ pressed?: boolean;
84
+ defaultPressed?: boolean;
85
+ onPressedChange?: (pressed: boolean) => void;
86
+ }
87
+ /** Button with an on/off state (aria-pressed). */
88
+ declare function Toggle({ pressed, defaultPressed, onPressedChange, onClick, ...rest }: ToggleProps): react.JSX.Element;
89
+ type SliderProps = {
90
+ color?: RomulusColor;
91
+ } & Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "color">;
92
+ /** Slider (native input type="range") with a diamond thumb. */
93
+ declare const Slider: react.ForwardRefExoticComponent<{
94
+ color?: RomulusColor;
95
+ } & Omit<InputHTMLAttributes<HTMLInputElement>, "color" | "type"> & react.RefAttributes<HTMLInputElement>>;
96
+ type InputProps = {
97
+ color?: RomulusColor;
98
+ label?: ReactNode;
99
+ } & Omit<InputHTMLAttributes<HTMLInputElement>, "color">;
100
+ /** Text field with a filled trapezoid label. */
101
+ declare const Input: react.ForwardRefExoticComponent<{
102
+ color?: RomulusColor;
103
+ label?: ReactNode;
104
+ } & Omit<InputHTMLAttributes<HTMLInputElement>, "color"> & react.RefAttributes<HTMLInputElement>>;
105
+ interface TabItem {
106
+ id: string;
107
+ label: ReactNode;
108
+ content: ReactNode;
109
+ color?: RomulusColor;
110
+ }
111
+ interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "color"> {
112
+ items: TabItem[];
113
+ value?: string;
114
+ defaultValue?: string;
115
+ onValueChange?: (id: string) => void;
116
+ color?: RomulusColor;
117
+ }
118
+ /** Tabs as parallelograms above a glowing line. */
119
+ declare function Tabs({ items, value, defaultValue, onValueChange, color, className, ...rest }: TabsProps): react.JSX.Element;
120
+ interface KeypadProps extends Omit<HTMLAttributes<HTMLDivElement>, "color" | "onSubmit"> {
121
+ color?: RomulusColor;
122
+ /** Maximum code length */
123
+ length?: number;
124
+ /** Show the code as dots */
125
+ mask?: boolean;
126
+ /** Called with the code; true = accepted, false = denied */
127
+ onSubmit?: (code: string) => boolean | void;
128
+ }
129
+ /** Number pad with display, e.g. for access codes. */
130
+ declare function Keypad({ color, length, mask, onSubmit, className, ...rest }: KeypadProps): react.JSX.Element;
131
+
132
+ type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "color">;
133
+ interface DataCascadeProps extends DivProps {
134
+ columns?: number;
135
+ rows?: number;
136
+ seed?: number;
137
+ }
138
+ /** Columns of hex groups that flicker one by one. */
139
+ declare function DataCascade({ columns, rows, seed, className, ...rest }: DataCascadeProps): react.JSX.Element;
140
+ interface ReadoutItem {
141
+ label: ReactNode;
142
+ value: ReactNode;
143
+ color?: RomulusColor;
144
+ }
145
+ interface ReadoutProps extends Omit<HTMLAttributes<HTMLDListElement>, "color"> {
146
+ items: ReadoutItem[];
147
+ color?: RomulusColor;
148
+ }
149
+ /** Value table: label, dotted leader line, value. */
150
+ declare function Readout({ items, color, className, ...rest }: ReadoutProps): react.JSX.Element;
151
+ type NumberCodeProps = {
152
+ color?: RomulusColor;
153
+ } & Omit<HTMLAttributes<HTMLSpanElement>, "color">;
154
+ /** ID code like "R-7734". */
155
+ declare function NumberCode({ color, className, ...rest }: NumberCodeProps): react.JSX.Element;
156
+ /** Line with tick marks and a diamond in the middle. */
157
+ declare function Divider({ color, className, ...rest }: {
158
+ color?: RomulusColor;
159
+ } & DivProps): react.JSX.Element;
160
+ interface ButtonGridProps extends HTMLAttributes<HTMLDivElement> {
161
+ columns?: number;
162
+ }
163
+ /** Grid of buttons. */
164
+ declare function ButtonGrid({ columns, className, style, ...rest }: ButtonGridProps): react.JSX.Element;
165
+ interface LevelBarProps extends DivProps {
166
+ value: number;
167
+ max?: number;
168
+ segments?: number;
169
+ orientation?: Orientation;
170
+ color?: RomulusColor;
171
+ }
172
+ /** Level bar of slanted segments (vertical: chevrons). */
173
+ declare function LevelBar({ value, max, segments, orientation, color, className, ...rest }: LevelBarProps): react.JSX.Element;
174
+ interface GaugeProps extends DivProps {
175
+ value: number;
176
+ max?: number;
177
+ segments?: number;
178
+ label?: ReactNode;
179
+ color?: RomulusColor;
180
+ }
181
+ /** Ring gauge made of segments, value in the center. */
182
+ declare function Gauge({ value, max, segments, label, color, className, style, ...rest }: GaugeProps): react.JSX.Element;
183
+ interface Blip {
184
+ x: number;
185
+ y: number;
186
+ color?: RomulusColor;
187
+ }
188
+ interface SensorProps extends DivProps {
189
+ blips?: Blip[];
190
+ /** Radar sweep around the center */
191
+ scan?: boolean;
192
+ color?: RomulusColor;
193
+ }
194
+ /** Sensor display with a triangular grid, crosshair and diamond contacts. */
195
+ declare function Sensor({ blips, scan, color, className, children, ...rest }: SensorProps): react.JSX.Element;
196
+ interface StatusIndicatorProps extends HTMLAttributes<HTMLSpanElement> {
197
+ status?: StatusKind;
198
+ blink?: boolean;
199
+ }
200
+ /** Diamond indicator light with label. */
201
+ declare function StatusIndicator({ status, blink, className, ...rest }: StatusIndicatorProps): react.JSX.Element;
202
+ interface AlertBannerProps extends Omit<DivProps, "title"> {
203
+ level?: AlertLevel;
204
+ title?: ReactNode;
205
+ }
206
+ /** Hexagonal, blinking alert banner. */
207
+ declare function AlertBanner({ level, title, children, className, ...rest }: AlertBannerProps): react.JSX.Element;
208
+
209
+ interface RomulusRootProps extends HTMLAttributes<HTMLDivElement> {
210
+ theme?: RomulusTheme;
211
+ /** Turns everything below red/yellow and makes the lines pulse */
212
+ alert?: AlertLevel | null;
213
+ }
214
+ /** Sets the color theme and alert state for all components below. */
215
+ declare function RomulusRoot({ theme, alert, ...rest }: RomulusRootProps): react.JSX.Element;
216
+
217
+ /** Enables interface sounds (Romulan patterns) for all components below. */
218
+ declare function SoundProvider(props: SoundProviderProps): react.JSX.Element;
219
+
220
+ export { AlertBanner, Bar, Block, Button, ButtonGrid, DataCascade, Divider, Gauge, Input, Keypad, LevelBar, NumberCode, Panel, Readout, RomulusRoot, Sensor, Slider, SoundProvider, Stack, StatusIndicator, Tabs, Title, Toggle };
221
+ export type { AlertBannerProps, AlertLevel, BarCap, BarProps, Blip, BlockProps, ButtonAlign, ButtonGridProps, ButtonProps, ButtonShape, ButtonSize, ButtonVariant, DataCascadeProps, GaugeProps, InputProps, KeypadProps, LevelBarProps, NumberCodeProps, Orientation, PanelProps, ReadoutItem, ReadoutProps, RomulusColor, RomulusLength, RomulusRootProps, RomulusTheme, SensorProps, SliderProps, StatusIndicatorProps, StatusKind, TabItem, TabsProps, TitleProps, ToggleProps };
@@ -0,0 +1,452 @@
1
+ "use client";
2
+
3
+ // src/react/Button.tsx
4
+ import { forwardRef } from "react";
5
+ import { cx } from "@lcars-gui/core";
6
+ import { useLcarsSound } from "@lcars-gui/core/react";
7
+
8
+ // src/shared.ts
9
+ function colorAttribute(color) {
10
+ return color ? { "data-color": String(color) } : {};
11
+ }
12
+
13
+ // src/button.ts
14
+ var buttonClassName = "rom-btn";
15
+ function buttonDataAttributes({ color, size = "m", length, shape = "chamfer", variant = "outline", align = "center" }) {
16
+ return {
17
+ ...colorAttribute(color),
18
+ "data-size": size,
19
+ ...length && { "data-length": length },
20
+ ...shape !== "chamfer" && { "data-shape": shape },
21
+ ...variant !== "outline" && { "data-variant": variant },
22
+ ...align !== "center" && { "data-align": align }
23
+ };
24
+ }
25
+
26
+ // src/panel.ts
27
+ var panelClassNames = {
28
+ panel: "rom-panel",
29
+ header: "rom-panel-header",
30
+ code: "rom-panel-code",
31
+ sidebar: "rom-panel-sidebar",
32
+ content: "rom-panel-content",
33
+ footer: "rom-panel-footer",
34
+ bar: "rom-bar",
35
+ block: "rom-block",
36
+ title: "rom-title",
37
+ stack: "rom-stack"
38
+ };
39
+ function barDataAttributes({ color, cap, length, segmented }) {
40
+ return {
41
+ ...colorAttribute(color),
42
+ ...cap && { "data-cap": cap },
43
+ ...length && { "data-length": length },
44
+ ...segmented && { "data-segmented": "" }
45
+ };
46
+ }
47
+ function blockDataAttributes({ color, fill }) {
48
+ return {
49
+ ...colorAttribute(color),
50
+ ...fill && { "data-fill": "" }
51
+ };
52
+ }
53
+
54
+ // src/display.ts
55
+ import { seededRandom } from "@lcars-gui/core";
56
+ var displayClassNames = {
57
+ cascade: "rom-cascade",
58
+ cascadeCol: "rom-cascade-col",
59
+ readout: "rom-readout",
60
+ number: "rom-number",
61
+ divider: "rom-divider",
62
+ grid: "rom-grid",
63
+ level: "rom-level",
64
+ gauge: "rom-gauge",
65
+ sensor: "rom-sensor",
66
+ blip: "rom-blip",
67
+ scan: "rom-scan",
68
+ status: "rom-status",
69
+ alert: "rom-alert",
70
+ alertTitle: "rom-alert-title",
71
+ alertText: "rom-alert-text"
72
+ };
73
+ var controlClassNames = {
74
+ tabs: "rom-tabs",
75
+ tablist: "rom-tablist",
76
+ tab: "rom-tab",
77
+ tabpanel: "rom-tabpanel",
78
+ field: "rom-field",
79
+ fieldLabel: "rom-field-label",
80
+ input: "rom-input",
81
+ range: "rom-range",
82
+ keypad: "rom-keypad",
83
+ keypadDisplay: "rom-keypad-display"
84
+ };
85
+ function cascadeGlyphs(columns, rows, seed = 1701) {
86
+ const random = seededRandom(seed);
87
+ const hex = () => Math.floor(random() * 256).toString(16).toUpperCase().padStart(2, "0");
88
+ return Array.from({ length: columns }, (_, col) => {
89
+ const groups = 1 + col % 3;
90
+ return Array.from({ length: rows }, () => Array.from({ length: groups }, hex).join("\xB7"));
91
+ });
92
+ }
93
+ function levelSegments(value, segments = 12, max = 100) {
94
+ const lit = Math.round(Math.min(Math.max(value, 0), max) / max * segments);
95
+ return Array.from({ length: segments }, (_, i) => i < lit);
96
+ }
97
+ function toPercent(value, max = 100) {
98
+ return Math.min(Math.max(value / max * 100, 0), 100);
99
+ }
100
+
101
+ // src/sound.ts
102
+ var romulanSound = {
103
+ wave: "triangle",
104
+ patterns: {
105
+ beep: [[523, 55]],
106
+ confirm: [[392, 60], [523, 60], [784, 100]],
107
+ deny: [[311, 140], [233, 220]],
108
+ alert: [[466, 180], [349, 180], [466, 180], [349, 180]]
109
+ }
110
+ };
111
+
112
+ // src/react/Button.tsx
113
+ import { jsx } from "react/jsx-runtime";
114
+ var Button = forwardRef(
115
+ function Button2({ color, size, length, shape, variant, align, sound = "beep", className, type = "button", onClick, ...rest }, ref) {
116
+ const play = useLcarsSound();
117
+ return /* @__PURE__ */ jsx(
118
+ "button",
119
+ {
120
+ ref,
121
+ type,
122
+ className: cx(buttonClassName, className),
123
+ ...buttonDataAttributes({ color, size, length, shape, variant, align }),
124
+ onClick: (e) => {
125
+ if (sound) play(sound);
126
+ onClick?.(e);
127
+ },
128
+ ...rest
129
+ }
130
+ );
131
+ }
132
+ );
133
+
134
+ // src/react/Panel.tsx
135
+ import { cx as cx2 } from "@lcars-gui/core";
136
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
137
+ function Bar({ color, cap, length, segmented, className, ...rest }) {
138
+ return /* @__PURE__ */ jsx2(
139
+ "div",
140
+ {
141
+ className: cx2(panelClassNames.bar, className),
142
+ ...barDataAttributes({ color, cap, length, segmented }),
143
+ ...rest
144
+ }
145
+ );
146
+ }
147
+ function Block({ color, fill, className, ...rest }) {
148
+ return /* @__PURE__ */ jsx2(
149
+ "div",
150
+ {
151
+ className: cx2(panelClassNames.block, className),
152
+ ...blockDataAttributes({ color, fill }),
153
+ ...rest
154
+ }
155
+ );
156
+ }
157
+ function Title({ color, className, ...rest }) {
158
+ return /* @__PURE__ */ jsx2(
159
+ "h1",
160
+ {
161
+ className: cx2(panelClassNames.title, className),
162
+ ...colorAttribute(color),
163
+ ...rest
164
+ }
165
+ );
166
+ }
167
+ function Stack({ className, ...rest }) {
168
+ return /* @__PURE__ */ jsx2("div", { className: cx2(panelClassNames.stack, className), ...rest });
169
+ }
170
+ function Panel({
171
+ color,
172
+ title,
173
+ code,
174
+ sidebar,
175
+ header,
176
+ footer,
177
+ className,
178
+ children,
179
+ ...rest
180
+ }) {
181
+ return /* @__PURE__ */ jsxs("section", { className: cx2(panelClassNames.panel, className), ...colorAttribute(color), ...rest, children: [
182
+ /* @__PURE__ */ jsxs("div", { className: panelClassNames.header, children: [
183
+ code != null && /* @__PURE__ */ jsx2("span", { className: panelClassNames.code, children: code }),
184
+ title != null && /* @__PURE__ */ jsx2(Title, { children: title }),
185
+ header ?? /* @__PURE__ */ jsx2(Bar, { cap: "right" })
186
+ ] }),
187
+ sidebar != null && /* @__PURE__ */ jsxs(Stack, { className: panelClassNames.sidebar, children: [
188
+ sidebar,
189
+ /* @__PURE__ */ jsx2(Block, { fill: true })
190
+ ] }),
191
+ /* @__PURE__ */ jsx2("div", { className: panelClassNames.content, children }),
192
+ footer !== null && /* @__PURE__ */ jsx2("div", { className: panelClassNames.footer, children: footer ?? /* @__PURE__ */ jsxs(Fragment, { children: [
193
+ /* @__PURE__ */ jsx2(Bar, { segmented: true }),
194
+ /* @__PURE__ */ jsx2(Bar, { length: "s", cap: "right" })
195
+ ] }) })
196
+ ] });
197
+ }
198
+
199
+ // src/react/Controls.tsx
200
+ import {
201
+ forwardRef as forwardRef2,
202
+ useId,
203
+ useState
204
+ } from "react";
205
+ import { cx as cx3 } from "@lcars-gui/core";
206
+ import { useLcarsSound as useLcarsSound2 } from "@lcars-gui/core/react";
207
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
208
+ function Toggle({ pressed, defaultPressed = false, onPressedChange, onClick, ...rest }) {
209
+ const [inner, setInner] = useState(defaultPressed);
210
+ const isOn = pressed ?? inner;
211
+ return /* @__PURE__ */ jsx3(
212
+ Button,
213
+ {
214
+ "aria-pressed": isOn,
215
+ onClick: (e) => {
216
+ setInner(!isOn);
217
+ onPressedChange?.(!isOn);
218
+ onClick?.(e);
219
+ },
220
+ ...rest
221
+ }
222
+ );
223
+ }
224
+ var Slider = forwardRef2(function Slider2({ color, className, ...rest }, ref) {
225
+ return /* @__PURE__ */ jsx3(
226
+ "input",
227
+ {
228
+ ref,
229
+ type: "range",
230
+ className: cx3(controlClassNames.range, className),
231
+ ...colorAttribute(color),
232
+ ...rest
233
+ }
234
+ );
235
+ });
236
+ var Input = forwardRef2(function Input2({ color, label, className, ...rest }, ref) {
237
+ return /* @__PURE__ */ jsxs2("label", { className: controlClassNames.field, ...colorAttribute(color), children: [
238
+ label != null && /* @__PURE__ */ jsx3("span", { className: controlClassNames.fieldLabel, children: label }),
239
+ /* @__PURE__ */ jsx3("input", { ref, className: cx3(controlClassNames.input, className), ...rest })
240
+ ] });
241
+ });
242
+ function Tabs({ items, value, defaultValue, onValueChange, color, className, ...rest }) {
243
+ const base = useId();
244
+ const play = useLcarsSound2();
245
+ const [inner, setInner] = useState(defaultValue ?? items[0]?.id);
246
+ const active = value ?? inner;
247
+ const select = (id) => {
248
+ setInner(id);
249
+ onValueChange?.(id);
250
+ play("beep");
251
+ };
252
+ const onKeyDown = (e) => {
253
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
254
+ if (!step) return;
255
+ const index = items.findIndex((item) => item.id === active);
256
+ const next = items[(index + step + items.length) % items.length];
257
+ select(next.id);
258
+ document.getElementById(`${base}-tab-${next.id}`)?.focus();
259
+ };
260
+ return /* @__PURE__ */ jsxs2("div", { className: cx3(controlClassNames.tabs, className), ...colorAttribute(color), ...rest, children: [
261
+ /* @__PURE__ */ jsx3("div", { role: "tablist", className: controlClassNames.tablist, onKeyDown, children: items.map((item) => {
262
+ const selected = item.id === active;
263
+ return /* @__PURE__ */ jsx3(
264
+ "button",
265
+ {
266
+ type: "button",
267
+ role: "tab",
268
+ id: `${base}-tab-${item.id}`,
269
+ "aria-selected": selected,
270
+ "aria-controls": `${base}-panel-${item.id}`,
271
+ tabIndex: selected ? 0 : -1,
272
+ className: controlClassNames.tab,
273
+ ...colorAttribute(item.color),
274
+ onClick: () => select(item.id),
275
+ children: item.label
276
+ },
277
+ item.id
278
+ );
279
+ }) }),
280
+ /* @__PURE__ */ jsx3(Bar, { cap: "right", color }),
281
+ items.map((item) => /* @__PURE__ */ jsx3(
282
+ "div",
283
+ {
284
+ role: "tabpanel",
285
+ id: `${base}-panel-${item.id}`,
286
+ "aria-labelledby": `${base}-tab-${item.id}`,
287
+ hidden: item.id !== active,
288
+ className: controlClassNames.tabpanel,
289
+ children: item.content
290
+ },
291
+ item.id
292
+ ))
293
+ ] });
294
+ }
295
+ var keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "CLR", "0", "ENT"];
296
+ function Keypad({ color = 1, length = 6, mask, onSubmit, className, ...rest }) {
297
+ const play = useLcarsSound2();
298
+ const [code, setCode] = useState("");
299
+ const [state, setState] = useState();
300
+ const press = (key) => {
301
+ setState(void 0);
302
+ if (key === "CLR") return setCode("");
303
+ if (key === "ENT") {
304
+ const accepted = onSubmit?.(code);
305
+ if (accepted === void 0) return play("confirm");
306
+ setState(accepted ? "ok" : "error");
307
+ play(accepted ? "confirm" : "deny");
308
+ return setCode("");
309
+ }
310
+ if (code.length < length) setCode(code + key);
311
+ };
312
+ const shown = state === "ok" ? "OK" : state === "error" ? "DENIED" : mask ? "\u2022".repeat(code.length) : code;
313
+ return /* @__PURE__ */ jsxs2("div", { className: cx3(controlClassNames.keypad, className), ...colorAttribute(color), ...rest, children: [
314
+ /* @__PURE__ */ jsx3("output", { className: controlClassNames.keypadDisplay, "data-state": state, children: shown || "\u2013".repeat(length) }),
315
+ keys.map((key) => /* @__PURE__ */ jsx3(
316
+ Button,
317
+ {
318
+ color: key === "CLR" ? 2 : key === "ENT" ? 4 : color,
319
+ variant: key === "ENT" ? "solid" : "outline",
320
+ sound: key === "ENT" ? false : "beep",
321
+ onClick: () => press(key),
322
+ children: key
323
+ },
324
+ key
325
+ ))
326
+ ] });
327
+ }
328
+
329
+ // src/react/Display.tsx
330
+ import { useMemo } from "react";
331
+ import { cx as cx4 } from "@lcars-gui/core";
332
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
333
+ function DataCascade({ columns = 6, rows = 6, seed = 1701, className, ...rest }) {
334
+ const data = useMemo(() => cascadeGlyphs(columns, rows, seed), [columns, rows, seed]);
335
+ return /* @__PURE__ */ jsx4("div", { className: cx4(displayClassNames.cascade, className), "aria-hidden": true, ...rest, children: data.map((col, c) => /* @__PURE__ */ jsx4("div", { className: displayClassNames.cascadeCol, children: col.map((g, r) => /* @__PURE__ */ jsx4("span", { style: { "--i": r * 3 + c }, children: g }, r)) }, c)) });
336
+ }
337
+ function Readout({ items, color, className, ...rest }) {
338
+ return /* @__PURE__ */ jsx4("dl", { className: cx4(displayClassNames.readout, className), ...colorAttribute(color), ...rest, children: items.map((item, i) => /* @__PURE__ */ jsxs3("div", { children: [
339
+ /* @__PURE__ */ jsx4("dt", { ...colorAttribute(item.color), children: item.label }),
340
+ /* @__PURE__ */ jsx4("dd", { children: item.value })
341
+ ] }, i)) });
342
+ }
343
+ function NumberCode({ color, className, ...rest }) {
344
+ return /* @__PURE__ */ jsx4("span", { className: cx4(displayClassNames.number, className), ...colorAttribute(color), ...rest });
345
+ }
346
+ function Divider({ color, className, ...rest }) {
347
+ return /* @__PURE__ */ jsx4("div", { role: "separator", className: cx4(displayClassNames.divider, className), ...colorAttribute(color), ...rest });
348
+ }
349
+ function ButtonGrid({ columns = 4, className, style, ...rest }) {
350
+ return /* @__PURE__ */ jsx4("div", { className: cx4(displayClassNames.grid, className), style: { "--cols": columns, ...style }, ...rest });
351
+ }
352
+ function LevelBar({ value, max = 100, segments = 12, orientation, color, className, ...rest }) {
353
+ return /* @__PURE__ */ jsx4(
354
+ "div",
355
+ {
356
+ role: "meter",
357
+ "aria-valuenow": value,
358
+ "aria-valuemin": 0,
359
+ "aria-valuemax": max,
360
+ className: cx4(displayClassNames.level, className),
361
+ "data-orientation": orientation,
362
+ ...colorAttribute(color),
363
+ ...rest,
364
+ children: levelSegments(value, segments, max).map((on, i) => /* @__PURE__ */ jsx4("span", { "data-on": on ? "" : void 0 }, i))
365
+ }
366
+ );
367
+ }
368
+ function Gauge({ value, max = 100, segments = 24, label, color, className, style, ...rest }) {
369
+ return /* @__PURE__ */ jsxs3(
370
+ "div",
371
+ {
372
+ role: "meter",
373
+ "aria-valuenow": value,
374
+ "aria-valuemin": 0,
375
+ "aria-valuemax": max,
376
+ className: cx4(displayClassNames.gauge, className),
377
+ style: { "--value": toPercent(value, max), "--segments": segments, ...style },
378
+ ...colorAttribute(color),
379
+ ...rest,
380
+ children: [
381
+ /* @__PURE__ */ jsx4("span", { children: Math.round(value) }),
382
+ label != null && /* @__PURE__ */ jsx4("small", { children: label })
383
+ ]
384
+ }
385
+ );
386
+ }
387
+ function Sensor({ blips = [], scan, color, className, children, ...rest }) {
388
+ return /* @__PURE__ */ jsxs3("div", { className: cx4(cx4(displayClassNames.sensor, scan ? displayClassNames.scan : void 0), className), ...colorAttribute(color), ...rest, children: [
389
+ blips.map((b, i) => /* @__PURE__ */ jsx4("span", { className: displayClassNames.blip, style: { "--x": b.x, "--y": b.y }, ...colorAttribute(b.color) }, i)),
390
+ children
391
+ ] });
392
+ }
393
+ function StatusIndicator({ status = "ok", blink, className, ...rest }) {
394
+ return /* @__PURE__ */ jsx4(
395
+ "span",
396
+ {
397
+ role: "status",
398
+ className: cx4(displayClassNames.status, className),
399
+ "data-status": status,
400
+ "data-blink": blink ? "" : void 0,
401
+ ...rest
402
+ }
403
+ );
404
+ }
405
+ function AlertBanner({ level = "red", title = "Alert", children, className, ...rest }) {
406
+ return /* @__PURE__ */ jsxs3("div", { role: "alert", className: cx4(displayClassNames.alert, className), "data-level": level, ...rest, children: [
407
+ /* @__PURE__ */ jsx4("span", { className: displayClassNames.alertTitle, children: title }),
408
+ /* @__PURE__ */ jsx4("span", { className: displayClassNames.alertText, children: children ?? `Condition: ${level}` })
409
+ ] });
410
+ }
411
+
412
+ // src/react/Root.tsx
413
+ import { jsx as jsx5 } from "react/jsx-runtime";
414
+ function RomulusRoot({ theme, alert, ...rest }) {
415
+ return /* @__PURE__ */ jsx5("div", { "data-theme": theme, "data-alert": alert ?? void 0, ...rest });
416
+ }
417
+
418
+ // src/react/Sound.tsx
419
+ import {
420
+ SoundProvider as CoreSoundProvider
421
+ } from "@lcars-gui/core/react";
422
+ import { useLcarsSound as useLcarsSound3 } from "@lcars-gui/core/react";
423
+ import { jsx as jsx6 } from "react/jsx-runtime";
424
+ function SoundProvider(props) {
425
+ return /* @__PURE__ */ jsx6(CoreSoundProvider, { ...romulanSound, ...props });
426
+ }
427
+ export {
428
+ AlertBanner,
429
+ Bar,
430
+ Block,
431
+ Button,
432
+ ButtonGrid,
433
+ DataCascade,
434
+ Divider,
435
+ Gauge,
436
+ Input,
437
+ Keypad,
438
+ LevelBar,
439
+ NumberCode,
440
+ Panel,
441
+ Readout,
442
+ RomulusRoot,
443
+ Sensor,
444
+ Slider,
445
+ SoundProvider,
446
+ Stack,
447
+ StatusIndicator,
448
+ Tabs,
449
+ Title,
450
+ Toggle,
451
+ useLcarsSound3 as useLcarsSound
452
+ };
@@ -0,0 +1,11 @@
1
+ /* Theme extras: Romulan Free State (Picard era)
2
+ Published as @lcars-gui/romulus/themes/free-state.css.
3
+ Soft glow on text and contacts. */
4
+
5
+ [data-theme="free-state"] :is(.rom-btn, .rom-title, .rom-tab, .rom-number) {
6
+ text-shadow: 0 0 0.6em color-mix(in srgb, currentColor 70%, transparent);
7
+ }
8
+
9
+ [data-theme="free-state"] .rom-blip {
10
+ box-shadow: 0 0 1.2rem 0.2rem var(--rom-bg);
11
+ }
@@ -0,0 +1,11 @@
1
+ /* Theme extras: Tal Shiar – intelligence service
2
+ Published as @lcars-gui/romulus/themes/tal-shiar.css.
3
+ Titles and ID codes flicker irregularly, as if the signal were jammed. */
4
+
5
+ [data-theme="tal-shiar"] :is(.rom-title, .rom-panel-code) {
6
+ animation: rom-flicker 7s infinite;
7
+ }
8
+
9
+ [data-theme="tal-shiar"] .rom-panel-code {
10
+ animation-delay: 2.3s;
11
+ }