@lcars-gui/romulus 0.0.0-stage → 0.1.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.
@@ -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
+ * Plattformneutrale Button-Props (ohne Events/Children,
21
+ * die liefert der jeweilige 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
+ /** Ton beim Klicken (nur innerhalb eines SoundProvider); false = stumm */
47
+ sound?: LcarsSoundKind | false;
48
+ };
49
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps$1 & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color"> & {
50
+ /** Ton beim Klicken (nur innerhalb eines SoundProvider); false = stumm */
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
+ /** Leuchtlinie, optional mit spitzen Enden oder gestrichelt. */
57
+ declare function Bar({ color, cap, length, segmented, className, ...rest }: BarProps): react.JSX.Element;
58
+ type BlockProps = BlockProps$1 & DivProps$1;
59
+ /** Gefülltes Segment mit abgeschrägten Ecken; Label sitzt unten links. */
60
+ declare function Block({ color, fill, className, ...rest }: BlockProps): react.JSX.Element;
61
+ type TitleProps = {
62
+ color?: RomulusColor;
63
+ } & Omit<HTMLAttributes<HTMLHeadingElement>, "color">;
64
+ /** Gesperrter Titel in der Kopfzeile. */
65
+ declare function Title({ color, className, ...rest }: TitleProps): react.JSX.Element;
66
+ /** Senkrechter Stapel aus Buttons/Blocks mit gleichmäßigen Lücken. */
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
+ /** Kennung im gefüllten Trapez vorn im Kopf, z. B. "R-7734" */
72
+ code?: ReactNode;
73
+ /** Inhalt der Seitenleiste (Buttons/Blocks); wird nach unten aufgefüllt */
74
+ sidebar?: ReactNode;
75
+ /** Ersetzt die Standardlinie im Kopf bzw. Fuß; footer={null} blendet den Fuß aus */
76
+ header?: ReactNode;
77
+ footer?: ReactNode;
78
+ }
79
+ /** Romulanische Konsole: abgeschrägte Ecken, Krallen-Ecken, Kopf mit Kennung, optionale Seitenleiste. */
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 mit Zustand an/aus (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
+ /** Schieberegler (natives input type="range") mit Rautengriff. */
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
+ /** Textfeld mit gefülltem Trapez als Beschriftung. */
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
+ /** Registerkarten als Parallelogramme über einer Leuchtlinie. */
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
+ /** Maximale Länge des Codes */
123
+ length?: number;
124
+ /** Code als Punkte anzeigen */
125
+ mask?: boolean;
126
+ /** Wird mit dem Code aufgerufen; true = Code akzeptiert, false = abgelehnt */
127
+ onSubmit?: (code: string) => boolean | void;
128
+ }
129
+ /** Ziffernfeld mit Anzeige, z. B. für Zugangscodes. */
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
+ /** Spalten aus Hex-Gruppen, die einzeln aufflackern. */
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
+ /** Wertetabelle: Label, gepunktete Führungslinie, Wert. */
150
+ declare function Readout({ items, color, className, ...rest }: ReadoutProps): react.JSX.Element;
151
+ type NumberCodeProps = {
152
+ color?: RomulusColor;
153
+ } & Omit<HTMLAttributes<HTMLSpanElement>, "color">;
154
+ /** Kennung wie „R-7734“. */
155
+ declare function NumberCode({ color, className, ...rest }: NumberCodeProps): react.JSX.Element;
156
+ /** Linie mit Skalenstrichen und Raute in der Mitte. */
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
+ /** Raster aus 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
+ /** Pegelanzeige aus schrägen Segmenten (senkrecht: Winkel). */
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
+ /** Ringanzeige aus Segmenten, Wert in der Mitte. */
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 um die Mitte */
191
+ scan?: boolean;
192
+ color?: RomulusColor;
193
+ }
194
+ /** Sensoranzeige mit Dreiecksraster, Fadenkreuz und Kontakten als Rauten. */
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
+ /** Rautenlampe mit Beschriftung. */
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
+ /** Sechseckiges, blinkendes Alarm-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
+ /** Färbt alles darunter rot/gelb und lässt Linien pulsieren */
212
+ alert?: AlertLevel | null;
213
+ }
214
+ /** Setzt Farbthema und Alarmzustand für alle Komponenten darunter. */
215
+ declare function RomulusRoot({ theme, alert, ...rest }: RomulusRootProps): react.JSX.Element;
216
+
217
+ /** Aktiviert die Bedien-Töne (romulanische Tonfolgen) für alle Komponenten darunter. */
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-Ära)
2
+ Wird als @lcars-gui/romulus/themes/free-state.css veröffentlicht.
3
+ Weiches Leuchten auf Schrift und Kontakten. */
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
+ }