@lcars-gui/federation 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,506 @@
1
+ "use client";
2
+
3
+ // src/react/Button.tsx
4
+ import { forwardRef } from "react";
5
+
6
+ // src/shared.ts
7
+ function colorAttribute(color) {
8
+ return color ? { "data-color": String(color) } : {};
9
+ }
10
+
11
+ // src/button.ts
12
+ var defaultButtonProps = {
13
+ color: 1,
14
+ size: "m",
15
+ shape: "capsule",
16
+ align: "center",
17
+ disabled: false
18
+ };
19
+ var buttonClassName = "lcar-btn-std";
20
+ function normalizeButtonProps(props) {
21
+ return {
22
+ ...defaultButtonProps,
23
+ ...props
24
+ };
25
+ }
26
+ function buttonDataAttributes(props) {
27
+ const { color, size, length, shape, align } = normalizeButtonProps(props);
28
+ return {
29
+ "data-color": String(color),
30
+ "data-size": size,
31
+ ...length && { "data-length": length },
32
+ ...shape !== "capsule" && { "data-shape": shape },
33
+ ...align === "bottom-right" && { "data-align": "br" }
34
+ };
35
+ }
36
+
37
+ // src/frame.ts
38
+ var frameClassNames = {
39
+ elbow: "lcar-elbow",
40
+ bar: "lcar-bar",
41
+ block: "lcar-block",
42
+ title: "lcar-title",
43
+ stack: "lcar-stack",
44
+ frame: "lcar-frame",
45
+ frameTop: "lcar-frame-top",
46
+ frameBottom: "lcar-frame-bottom",
47
+ frameContent: "lcar-frame-content",
48
+ frameHeader: "lcar-frame-header"
49
+ };
50
+ function elbowDataAttributes({ corner = "tl", color }) {
51
+ return { "data-corner": corner, ...colorAttribute(color) };
52
+ }
53
+ function barDataAttributes({ color, cap, length }) {
54
+ return {
55
+ ...colorAttribute(color),
56
+ ...cap && { "data-cap": cap },
57
+ ...length && { "data-length": length }
58
+ };
59
+ }
60
+ function blockDataAttributes({ color, fill }) {
61
+ return {
62
+ ...colorAttribute(color),
63
+ ...fill && { "data-fill": "" }
64
+ };
65
+ }
66
+
67
+ // src/display.ts
68
+ import { seededRandom } from "@lcars-gui/core";
69
+ var displayClassNames = {
70
+ cascade: "lcar-cascade",
71
+ cascadeCol: "lcar-cascade-col",
72
+ readout: "lcar-readout",
73
+ number: "lcar-number",
74
+ divider: "lcar-divider",
75
+ grid: "lcar-grid",
76
+ level: "lcar-level",
77
+ gauge: "lcar-gauge",
78
+ sensor: "lcar-sensor",
79
+ blip: "lcar-blip",
80
+ scan: "lcar-scan",
81
+ status: "lcar-status",
82
+ alert: "lcar-alert",
83
+ alertTitle: "lcar-alert-title",
84
+ alertText: "lcar-alert-text"
85
+ };
86
+ var controlClassNames = {
87
+ tabs: "lcar-tabs",
88
+ tablist: "lcar-tablist",
89
+ tab: "lcar-tab",
90
+ tabpanel: "lcar-tabpanel",
91
+ field: "lcar-field",
92
+ fieldLabel: "lcar-field-label",
93
+ input: "lcar-input",
94
+ range: "lcar-range",
95
+ keypad: "lcar-keypad",
96
+ keypadDisplay: "lcar-keypad-display"
97
+ };
98
+ function cascadeNumbers(columns, rows, seed = 47) {
99
+ const random = seededRandom(seed);
100
+ return Array.from({ length: columns }, (_, col) => {
101
+ const digits = 2 + col * 3 % 5;
102
+ return Array.from(
103
+ { length: rows },
104
+ () => String(Math.floor(random() * 10 ** digits)).padStart(digits, "0")
105
+ );
106
+ });
107
+ }
108
+ function levelSegments(value, segments = 10, max = 100) {
109
+ const lit = Math.round(Math.min(Math.max(value, 0), max) / max * segments);
110
+ return Array.from({ length: segments }, (_, i) => i < lit);
111
+ }
112
+ function toPercent(value, max = 100) {
113
+ return Math.min(Math.max(value / max * 100, 0), 100);
114
+ }
115
+
116
+ // src/react/Button.tsx
117
+ import { cx } from "@lcars-gui/core";
118
+ import { useLcarsSound } from "@lcars-gui/core/react";
119
+ import { jsx } from "react/jsx-runtime";
120
+ var Button = forwardRef(
121
+ function Button2({ color, size, length, shape, align, sound = "beep", className, type = "button", onClick, ...rest }, ref) {
122
+ const play = useLcarsSound();
123
+ return /* @__PURE__ */ jsx(
124
+ "button",
125
+ {
126
+ ref,
127
+ type,
128
+ className: cx(buttonClassName, className),
129
+ ...buttonDataAttributes({ color, size, length, shape, align }),
130
+ onClick: (e) => {
131
+ if (sound) play(sound);
132
+ onClick?.(e);
133
+ },
134
+ ...rest
135
+ }
136
+ );
137
+ }
138
+ );
139
+
140
+ // src/react/Frame.tsx
141
+ import { cx as cx2 } from "@lcars-gui/core";
142
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
143
+ function Elbow({ corner, color, className, ...rest }) {
144
+ return /* @__PURE__ */ jsx2(
145
+ "div",
146
+ {
147
+ className: cx2(frameClassNames.elbow, className),
148
+ ...elbowDataAttributes({ corner, color }),
149
+ ...rest
150
+ }
151
+ );
152
+ }
153
+ function Bar({ color, cap, length, className, ...rest }) {
154
+ return /* @__PURE__ */ jsx2(
155
+ "div",
156
+ {
157
+ className: cx2(frameClassNames.bar, className),
158
+ ...barDataAttributes({ color, cap, length }),
159
+ ...rest
160
+ }
161
+ );
162
+ }
163
+ function Block({ color, fill, className, ...rest }) {
164
+ return /* @__PURE__ */ jsx2(
165
+ "div",
166
+ {
167
+ className: cx2(frameClassNames.block, className),
168
+ ...blockDataAttributes({ color, fill }),
169
+ ...rest
170
+ }
171
+ );
172
+ }
173
+ function Title({ color, className, ...rest }) {
174
+ return /* @__PURE__ */ jsx2(
175
+ "h1",
176
+ {
177
+ className: cx2(frameClassNames.title, className),
178
+ ...colorAttribute(color),
179
+ ...rest
180
+ }
181
+ );
182
+ }
183
+ function Stack({ className, ...rest }) {
184
+ return /* @__PURE__ */ jsx2("div", { className: cx2(frameClassNames.stack, className), ...rest });
185
+ }
186
+ function Frame({
187
+ color,
188
+ title,
189
+ topLabel,
190
+ bottomLabel,
191
+ sidebar,
192
+ top,
193
+ bottom,
194
+ className,
195
+ children,
196
+ ...rest
197
+ }) {
198
+ return /* @__PURE__ */ jsxs("div", { className: cx2(frameClassNames.frame, className), ...rest, children: [
199
+ /* @__PURE__ */ jsxs("div", { className: frameClassNames.frameTop, children: [
200
+ /* @__PURE__ */ jsx2(Elbow, { corner: "tl", color, children: topLabel }),
201
+ top ?? /* @__PURE__ */ jsx2(Bar, { color }),
202
+ title != null && /* @__PURE__ */ jsxs(Fragment, { children: [
203
+ /* @__PURE__ */ jsx2(Title, { color, children: title }),
204
+ /* @__PURE__ */ jsx2(Bar, { color, cap: "right", length: "xs" })
205
+ ] })
206
+ ] }),
207
+ /* @__PURE__ */ jsxs(Stack, { children: [
208
+ sidebar,
209
+ /* @__PURE__ */ jsx2(Block, { color, fill: true })
210
+ ] }),
211
+ /* @__PURE__ */ jsx2("div", { className: frameClassNames.frameContent, children }),
212
+ /* @__PURE__ */ jsxs("div", { className: frameClassNames.frameBottom, children: [
213
+ /* @__PURE__ */ jsx2(Elbow, { corner: "bl", color, children: bottomLabel }),
214
+ bottom ?? /* @__PURE__ */ jsx2(Bar, { color, cap: "right" })
215
+ ] })
216
+ ] });
217
+ }
218
+ function SplitFrame({
219
+ color,
220
+ title,
221
+ header,
222
+ headerSidebar,
223
+ sidebar,
224
+ upperLabel,
225
+ lowerLabel,
226
+ upperBar,
227
+ lowerBar,
228
+ className,
229
+ children,
230
+ ...rest
231
+ }) {
232
+ return /* @__PURE__ */ jsxs("div", { className: cx2(frameClassNames.frame, className), "data-layout": "split", ...rest, children: [
233
+ /* @__PURE__ */ jsxs(Stack, { children: [
234
+ headerSidebar,
235
+ /* @__PURE__ */ jsx2(Block, { color, fill: true })
236
+ ] }),
237
+ /* @__PURE__ */ jsxs("div", { className: cx2(frameClassNames.frameContent, frameClassNames.frameHeader), children: [
238
+ header,
239
+ title != null && /* @__PURE__ */ jsx2(Title, { color, children: title })
240
+ ] }),
241
+ /* @__PURE__ */ jsxs("div", { className: frameClassNames.frameBottom, children: [
242
+ /* @__PURE__ */ jsx2(Elbow, { corner: "bl", color, children: upperLabel }),
243
+ upperBar ?? /* @__PURE__ */ jsx2(Bar, { color, cap: "right" })
244
+ ] }),
245
+ /* @__PURE__ */ jsxs("div", { className: frameClassNames.frameTop, children: [
246
+ /* @__PURE__ */ jsx2(Elbow, { corner: "tl", color, children: lowerLabel }),
247
+ lowerBar ?? /* @__PURE__ */ jsx2(Bar, { color, cap: "right" })
248
+ ] }),
249
+ /* @__PURE__ */ jsxs(Stack, { children: [
250
+ sidebar,
251
+ /* @__PURE__ */ jsx2(Block, { color, fill: true })
252
+ ] }),
253
+ /* @__PURE__ */ jsx2("div", { className: frameClassNames.frameContent, children })
254
+ ] });
255
+ }
256
+
257
+ // src/react/Controls.tsx
258
+ import {
259
+ forwardRef as forwardRef2,
260
+ useId,
261
+ useState
262
+ } from "react";
263
+ import { useLcarsSound as useLcarsSound2 } from "@lcars-gui/core/react";
264
+ import { cx as cx3 } from "@lcars-gui/core";
265
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
266
+ function Toggle({ pressed, defaultPressed = false, onPressedChange, onClick, ...rest }) {
267
+ const [inner, setInner] = useState(defaultPressed);
268
+ const isOn = pressed ?? inner;
269
+ return /* @__PURE__ */ jsx3(
270
+ Button,
271
+ {
272
+ "aria-pressed": isOn,
273
+ onClick: (e) => {
274
+ setInner(!isOn);
275
+ onPressedChange?.(!isOn);
276
+ onClick?.(e);
277
+ },
278
+ ...rest
279
+ }
280
+ );
281
+ }
282
+ var Slider = forwardRef2(function Slider2({ color, className, ...rest }, ref) {
283
+ return /* @__PURE__ */ jsx3(
284
+ "input",
285
+ {
286
+ ref,
287
+ type: "range",
288
+ className: cx3(controlClassNames.range, className),
289
+ ...colorAttribute(color),
290
+ ...rest
291
+ }
292
+ );
293
+ });
294
+ var Input = forwardRef2(function Input2({ color, label, className, ...rest }, ref) {
295
+ return /* @__PURE__ */ jsxs2("label", { className: controlClassNames.field, ...colorAttribute(color), children: [
296
+ label != null && /* @__PURE__ */ jsx3("span", { className: controlClassNames.fieldLabel, children: label }),
297
+ /* @__PURE__ */ jsx3("input", { ref, className: cx3(controlClassNames.input, className), ...rest })
298
+ ] });
299
+ });
300
+ function Tabs({ items, value, defaultValue, onValueChange, color, className, ...rest }) {
301
+ const base = useId();
302
+ const play = useLcarsSound2();
303
+ const [inner, setInner] = useState(defaultValue ?? items[0]?.id);
304
+ const active = value ?? inner;
305
+ const select = (id) => {
306
+ setInner(id);
307
+ onValueChange?.(id);
308
+ play("beep");
309
+ };
310
+ const onKeyDown = (e) => {
311
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
312
+ if (!step) return;
313
+ const index = items.findIndex((item) => item.id === active);
314
+ const next = items[(index + step + items.length) % items.length];
315
+ select(next.id);
316
+ document.getElementById(`${base}-tab-${next.id}`)?.focus();
317
+ };
318
+ return /* @__PURE__ */ jsxs2("div", { className: cx3(controlClassNames.tabs, className), ...colorAttribute(color), ...rest, children: [
319
+ /* @__PURE__ */ jsx3("div", { role: "tablist", className: controlClassNames.tablist, onKeyDown, children: items.map((item) => {
320
+ const selected = item.id === active;
321
+ return /* @__PURE__ */ jsx3(
322
+ "button",
323
+ {
324
+ type: "button",
325
+ role: "tab",
326
+ id: `${base}-tab-${item.id}`,
327
+ "aria-selected": selected,
328
+ "aria-controls": `${base}-panel-${item.id}`,
329
+ tabIndex: selected ? 0 : -1,
330
+ className: controlClassNames.tab,
331
+ ...colorAttribute(item.color),
332
+ onClick: () => select(item.id),
333
+ children: item.label
334
+ },
335
+ item.id
336
+ );
337
+ }) }),
338
+ /* @__PURE__ */ jsx3(Bar, { cap: "right", color }),
339
+ items.map((item) => /* @__PURE__ */ jsx3(
340
+ "div",
341
+ {
342
+ role: "tabpanel",
343
+ id: `${base}-panel-${item.id}`,
344
+ "aria-labelledby": `${base}-tab-${item.id}`,
345
+ hidden: item.id !== active,
346
+ className: controlClassNames.tabpanel,
347
+ children: item.content
348
+ },
349
+ item.id
350
+ ))
351
+ ] });
352
+ }
353
+ var keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "CLR", "0", "ENT"];
354
+ function Keypad({ color = 4, length = 6, mask, onSubmit, className, ...rest }) {
355
+ const play = useLcarsSound2();
356
+ const [code, setCode] = useState("");
357
+ const [state, setState] = useState();
358
+ const press = (key) => {
359
+ setState(void 0);
360
+ if (key === "CLR") return setCode("");
361
+ if (key === "ENT") {
362
+ const accepted = onSubmit?.(code);
363
+ if (accepted === void 0) return play("confirm");
364
+ setState(accepted ? "ok" : "error");
365
+ play(accepted ? "confirm" : "deny");
366
+ return setCode("");
367
+ }
368
+ if (code.length < length) setCode(code + key);
369
+ };
370
+ const shown = state === "ok" ? "OK" : state === "error" ? "DENIED" : mask ? "\u2022".repeat(code.length) : code;
371
+ return /* @__PURE__ */ jsxs2("div", { className: cx3(controlClassNames.keypad, className), ...colorAttribute(color), ...rest, children: [
372
+ /* @__PURE__ */ jsx3("output", { className: controlClassNames.keypadDisplay, "data-state": state, children: shown || "\u2013".repeat(length) }),
373
+ keys.map((key) => /* @__PURE__ */ jsx3(
374
+ Button,
375
+ {
376
+ shape: "rect",
377
+ align: "bottom-right",
378
+ color: key === "CLR" ? 2 : key === "ENT" ? 6 : color,
379
+ sound: key === "ENT" ? false : "beep",
380
+ onClick: () => press(key),
381
+ children: key
382
+ },
383
+ key
384
+ ))
385
+ ] });
386
+ }
387
+
388
+ // src/react/Display.tsx
389
+ import { useMemo } from "react";
390
+ import { cx as cx4 } from "@lcars-gui/core";
391
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
392
+ function DataCascade({ columns = 8, rows = 6, seed = 47, className, ...rest }) {
393
+ const data = useMemo(() => cascadeNumbers(columns, rows, seed), [columns, rows, seed]);
394
+ 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((n, r) => /* @__PURE__ */ jsx4("span", { style: { "--i": r * 2 + c }, children: n }, r)) }, c)) });
395
+ }
396
+ function Readout({ items, color, className, ...rest }) {
397
+ return /* @__PURE__ */ jsx4("dl", { className: cx4(displayClassNames.readout, className), ...colorAttribute(color), ...rest, children: items.map((item, i) => /* @__PURE__ */ jsxs3("div", { style: { display: "contents" }, children: [
398
+ /* @__PURE__ */ jsx4("dt", { ...colorAttribute(item.color), children: item.label }),
399
+ /* @__PURE__ */ jsx4("dd", { children: item.value })
400
+ ] }, i)) });
401
+ }
402
+ function NumberCode({ color, className, ...rest }) {
403
+ return /* @__PURE__ */ jsx4("span", { className: cx4(displayClassNames.number, className), ...colorAttribute(color), ...rest });
404
+ }
405
+ function Divider({ color, className, ...rest }) {
406
+ return /* @__PURE__ */ jsx4("div", { role: "separator", className: cx4(displayClassNames.divider, className), ...colorAttribute(color), ...rest });
407
+ }
408
+ function ButtonGrid({ columns = 4, className, style, ...rest }) {
409
+ return /* @__PURE__ */ jsx4("div", { className: cx4(displayClassNames.grid, className), style: { "--cols": columns, ...style }, ...rest });
410
+ }
411
+ function LevelBar({ value, max = 100, segments = 10, orientation, color, className, ...rest }) {
412
+ return /* @__PURE__ */ jsx4(
413
+ "div",
414
+ {
415
+ role: "meter",
416
+ "aria-valuenow": value,
417
+ "aria-valuemin": 0,
418
+ "aria-valuemax": max,
419
+ className: cx4(displayClassNames.level, className),
420
+ "data-orientation": orientation,
421
+ ...colorAttribute(color),
422
+ ...rest,
423
+ children: levelSegments(value, segments, max).map((on, i) => /* @__PURE__ */ jsx4("span", { "data-on": on ? "" : void 0 }, i))
424
+ }
425
+ );
426
+ }
427
+ function Gauge({ value, max = 100, segments = 12, label, color, className, style, ...rest }) {
428
+ return /* @__PURE__ */ jsxs3(
429
+ "div",
430
+ {
431
+ role: "meter",
432
+ "aria-valuenow": value,
433
+ "aria-valuemin": 0,
434
+ "aria-valuemax": max,
435
+ className: cx4(displayClassNames.gauge, className),
436
+ style: { "--value": toPercent(value, max), "--segments": segments, ...style },
437
+ ...colorAttribute(color),
438
+ ...rest,
439
+ children: [
440
+ /* @__PURE__ */ jsx4("span", { children: Math.round(value) }),
441
+ label != null && /* @__PURE__ */ jsx4("small", { children: label })
442
+ ]
443
+ }
444
+ );
445
+ }
446
+ function Sensor({ blips = [], scan, color, className, children, ...rest }) {
447
+ return /* @__PURE__ */ jsxs3("div", { className: cx4(cx4(displayClassNames.sensor, scan ? displayClassNames.scan : void 0), className), ...colorAttribute(color), ...rest, children: [
448
+ blips.map((b, i) => /* @__PURE__ */ jsx4("span", { className: displayClassNames.blip, style: { "--x": b.x, "--y": b.y }, ...colorAttribute(b.color) }, i)),
449
+ children
450
+ ] });
451
+ }
452
+ function StatusIndicator({ status = "ok", blink, className, ...rest }) {
453
+ return /* @__PURE__ */ jsx4(
454
+ "span",
455
+ {
456
+ role: "status",
457
+ className: cx4(displayClassNames.status, className),
458
+ "data-status": status,
459
+ "data-blink": blink ? "" : void 0,
460
+ ...rest
461
+ }
462
+ );
463
+ }
464
+ function AlertBanner({ level = "red", title = "Alert", children, className, ...rest }) {
465
+ return /* @__PURE__ */ jsxs3("div", { role: "alert", className: cx4(displayClassNames.alert, className), "data-level": level, ...rest, children: [
466
+ /* @__PURE__ */ jsx4("span", { className: displayClassNames.alertTitle, children: title }),
467
+ /* @__PURE__ */ jsx4("span", { className: displayClassNames.alertText, children: children ?? `Condition: ${level}` })
468
+ ] });
469
+ }
470
+
471
+ // src/react/Root.tsx
472
+ import { jsx as jsx5 } from "react/jsx-runtime";
473
+ function LcarsRoot({ theme, alert, ...rest }) {
474
+ return /* @__PURE__ */ jsx5("div", { "data-theme": theme, "data-alert": alert ?? void 0, ...rest });
475
+ }
476
+
477
+ // src/react/index.ts
478
+ import { SoundProvider, useLcarsSound as useLcarsSound3 } from "@lcars-gui/core/react";
479
+ export {
480
+ AlertBanner,
481
+ Bar,
482
+ Block,
483
+ Button,
484
+ ButtonGrid,
485
+ DataCascade,
486
+ Divider,
487
+ Elbow,
488
+ Frame,
489
+ Gauge,
490
+ Input,
491
+ Keypad,
492
+ LcarsRoot,
493
+ LevelBar,
494
+ NumberCode,
495
+ Readout,
496
+ Sensor,
497
+ Slider,
498
+ SoundProvider,
499
+ SplitFrame,
500
+ Stack,
501
+ StatusIndicator,
502
+ Tabs,
503
+ Title,
504
+ Toggle,
505
+ useLcarsSound3 as useLcarsSound
506
+ };
@@ -0,0 +1,5 @@
1
+ /* Theme extras: DS9 – Deep Space Nine station
2
+ Published as @lcars-gui/federation/themes/ds9.css.
3
+ Add animations/effects here that only fit this theme.
4
+ Scope all rules under [data-theme="ds9"] so the file can safely be
5
+ loaded while other themes are active. */
@@ -0,0 +1,5 @@
1
+ /* Theme extras: Picard – 25th century
2
+ Published as @lcars-gui/federation/themes/picard.css.
3
+ Add animations/effects here that only fit this theme.
4
+ Scope all rules under [data-theme="picard"] so the file can safely be
5
+ loaded while other themes are active. */
@@ -0,0 +1,5 @@
1
+ /* Theme extras: TNG – Enterprise-D
2
+ Published as @lcars-gui/federation/themes/tng.css.
3
+ Add animations/effects here that only fit this theme.
4
+ Scope all rules under [data-theme="tng"] so the file can safely be
5
+ loaded while other themes are active. */