@arcanejs/toolkit-frontend 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.
@@ -0,0 +1,1100 @@
1
+ import {
2
+ Icon,
3
+ TRANSPARENCY_SVG_URI,
4
+ core_exports
5
+ } from "../chunk-UJVH3PQF.mjs";
6
+ import {
7
+ THEME,
8
+ buttonPressed,
9
+ buttonStateNormal,
10
+ buttonStateNormalActive,
11
+ buttonStateNormalHover,
12
+ rectButton,
13
+ touchIndicatorNormal,
14
+ touchIndicatorTouching
15
+ } from "../chunk-6Z2ACPNM.mjs";
16
+ import {
17
+ calculateClass,
18
+ trackTouch,
19
+ usePressable
20
+ } from "../chunk-35U577MM.mjs";
21
+ import "../chunk-7P6ASYW6.mjs";
22
+
23
+ // src/components/index.tsx
24
+ import { isCoreComponent } from "@arcanejs/protocol/core";
25
+
26
+ // src/components/button.tsx
27
+ import React from "react";
28
+ import { styled } from "styled-components";
29
+
30
+ // src/components/context.ts
31
+ import { createContext } from "react";
32
+ var StageContext = createContext({
33
+ sendMessage: null,
34
+ renderComponent: () => {
35
+ throw new Error(`missing root context`);
36
+ }
37
+ });
38
+
39
+ // src/components/button.tsx
40
+ import { jsx, jsxs } from "react/jsx-runtime";
41
+ var TOUCH_INDICATOR_CLASS = "touch-indicator";
42
+ var TOUCHING_CLASS = "touching";
43
+ var ERROR_CLASS = "error";
44
+ var ButtonContents = styled.div`
45
+ padding: 6px 4px;
46
+ display: flex;
47
+ justify-content: center;
48
+ align-items: center;
49
+
50
+ > * {
51
+ padding: 0;
52
+ }
53
+ `;
54
+ var ButtonLabel = styled.span`
55
+ padding: 0 4px;
56
+ `;
57
+ var Button = (props) => {
58
+ const { sendMessage } = React.useContext(StageContext);
59
+ const { touching, handlers } = usePressable(
60
+ () => sendMessage?.({
61
+ type: "component-message",
62
+ namespace: "core",
63
+ componentKey: props.info.key,
64
+ component: "button"
65
+ })
66
+ );
67
+ const { state } = props.info;
68
+ return /* @__PURE__ */ jsxs(
69
+ "div",
70
+ {
71
+ className: calculateClass(
72
+ props.className,
73
+ touching || state.state === "pressed" ? TOUCHING_CLASS : null,
74
+ state.state === "error" && ERROR_CLASS
75
+ ),
76
+ ...handlers,
77
+ title: state.state === "error" ? state.error : void 0,
78
+ children: [
79
+ /* @__PURE__ */ jsx("div", { className: TOUCH_INDICATOR_CLASS }),
80
+ /* @__PURE__ */ jsxs(ButtonContents, { children: [
81
+ props.info.icon && /* @__PURE__ */ jsx(Icon, { icon: props.info.icon }),
82
+ props.info.text && /* @__PURE__ */ jsx(ButtonLabel, { children: props.info.text })
83
+ ] })
84
+ ]
85
+ }
86
+ );
87
+ };
88
+ var StyledButton = styled(Button)`
89
+ ${rectButton}
90
+ outline: none;
91
+ height: 30px;
92
+ position: relative;
93
+ overflow: visible;
94
+
95
+ .${TOUCH_INDICATOR_CLASS} {
96
+ ${touchIndicatorNormal}
97
+ }
98
+
99
+ &.${ERROR_CLASS} {
100
+ color: ${THEME.colorRed};
101
+ border-color: ${THEME.colorRed};
102
+ }
103
+
104
+ &.${TOUCHING_CLASS} {
105
+ ${buttonStateNormalActive}
106
+
107
+ .${TOUCH_INDICATOR_CLASS} {
108
+ ${touchIndicatorTouching}
109
+ }
110
+ }
111
+ `;
112
+
113
+ // src/components/group.tsx
114
+ import React3, {
115
+ useContext,
116
+ useState
117
+ } from "react";
118
+ import { styled as styled3 } from "styled-components";
119
+
120
+ // src/components/nesting.tsx
121
+ import React2 from "react";
122
+ import { styled as styled2 } from "styled-components";
123
+ import { jsx as jsx2 } from "react/jsx-runtime";
124
+ function nextColor(currentColor) {
125
+ switch (currentColor) {
126
+ case "dark":
127
+ return "lighter";
128
+ case "lighter":
129
+ return "dark";
130
+ case "lighterer":
131
+ return "dark";
132
+ }
133
+ }
134
+ var LastNestedColor = React2.createContext("dark");
135
+ var NestedContent = ({ className, children }) => {
136
+ const color = React2.useContext(LastNestedColor);
137
+ return /* @__PURE__ */ jsx2("div", { className: calculateClass(className, `color-${color}`), children: /* @__PURE__ */ jsx2(LastNestedColor.Provider, { value: nextColor(color), children }) });
138
+ };
139
+ NestedContent.displayName = "NestedContent";
140
+ var StyledNestedContent = styled2(NestedContent)`
141
+ padding: ${THEME.sizingPx.spacing / 2}px;
142
+ box-shadow: inset 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
143
+
144
+ &.color-dark {
145
+ background: ${THEME.bgDark1};
146
+ }
147
+
148
+ &.color-lighter {
149
+ background: ${THEME.bg};
150
+ }
151
+
152
+ &.color-lighterer {
153
+ background: ${THEME.bgLight1};
154
+ }
155
+ `;
156
+
157
+ // src/components/group.tsx
158
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
159
+ var CollapseIcon = styled3(Icon)({
160
+ cursor: "pointer"
161
+ });
162
+ var Header = styled3.div`
163
+ display: flex;
164
+ align-items: center;
165
+ padding: 5px 2px;
166
+ background: ${THEME.borderDark};
167
+ border-bottom: 1px solid ${THEME.borderDark};
168
+
169
+ &.touching {
170
+ background: ${THEME.bgDark1};
171
+ }
172
+
173
+ &.collapsed {
174
+ border-bottom: none;
175
+ }
176
+
177
+ > * {
178
+ margin: 0 3px;
179
+ }
180
+ `;
181
+ var Label = styled3.span`
182
+ display: inline-block;
183
+ border-radius: 3px;
184
+ background: ${THEME.bg};
185
+ border: 1px solid ${THEME.bgLight1};
186
+ padding: 3px 4px;
187
+ `;
188
+ var Grow = styled3.span({
189
+ flexGrow: "1"
190
+ });
191
+ var CollapseBar = styled3.span({
192
+ flexGrow: "1",
193
+ cursor: "pointer",
194
+ height: "30px"
195
+ });
196
+ var GroupChildren = styled3.div`
197
+ display: flex;
198
+ flex-direction: ${(p) => p.info.direction === "vertical" ? "column" : "row"};
199
+ flex-wrap: ${(p) => p.info.wrap ? "wrap" : "nowrap"};
200
+ ${(p) => p.info.direction === "vertical" ? "" : "align-items: center;"}
201
+
202
+ > * {
203
+ margin: ${THEME.sizingPx.spacing / 2}px;
204
+ }
205
+ `;
206
+ var EditableTitle = styled3.span`
207
+ display: flex;
208
+ align-items: center;
209
+ border-radius: 3px;
210
+ cursor: pointer;
211
+ padding: 3px 2px;
212
+
213
+ > * {
214
+ margin: 0 2px;
215
+ }
216
+
217
+ > .icon {
218
+ color: ${THEME.bg};
219
+ }
220
+
221
+ &:hover {
222
+ background: ${THEME.bg};
223
+
224
+ > .icon {
225
+ color: ${THEME.hint};
226
+ }
227
+ }
228
+ `;
229
+ var TitleInput = styled3.input`
230
+ background: none;
231
+ border: none;
232
+ outline: none;
233
+ color: ${THEME.textNormal};
234
+ `;
235
+ var GroupStateContext = React3.createContext({
236
+ isCollapsed: () => {
237
+ throw new Error("missing GroupStateContext");
238
+ },
239
+ toggleCollapsed: () => {
240
+ throw new Error("missing GroupStateContext");
241
+ }
242
+ });
243
+ var GroupStateWrapper = ({ openByDefault, children }) => {
244
+ const [state, setState] = useState({});
245
+ const isCollapsed = (key, defaultState) => {
246
+ let match = state[key];
247
+ if (!match) {
248
+ match = defaultState === "auto" ? openByDefault ? "open" : "closed" : defaultState;
249
+ setState((current) => ({
250
+ ...current,
251
+ [key]: match
252
+ }));
253
+ }
254
+ return match === "closed";
255
+ };
256
+ const toggleCollapsed = (key) => {
257
+ setState((current) => ({
258
+ ...current,
259
+ [key]: current[key] === "closed" ? "open" : "closed"
260
+ }));
261
+ };
262
+ return /* @__PURE__ */ jsx3(GroupStateContext.Provider, { value: { isCollapsed, toggleCollapsed }, children });
263
+ };
264
+ var Group = ({ className, info }) => {
265
+ const groupState = useContext(GroupStateContext);
266
+ const { renderComponent, sendMessage } = useContext(StageContext);
267
+ const [editingTitle, setEditingTitle] = useState(false);
268
+ const children = /* @__PURE__ */ jsx3(GroupChildren, { info, children: info.children.map(renderComponent) });
269
+ const collapsible = !!info.defaultCollapsibleState;
270
+ const collapsed = info.defaultCollapsibleState ? groupState.isCollapsed(info.key, info.defaultCollapsibleState) : false;
271
+ const collapsePressable = usePressable(
272
+ () => groupState.toggleCollapsed(info.key)
273
+ );
274
+ const showTitle = info.title || info.editableTitle;
275
+ const displayHeader = [
276
+ showTitle,
277
+ info.labels?.length,
278
+ info.headers?.length,
279
+ collapsible
280
+ ].some((v) => v);
281
+ const updateTitle = (e) => {
282
+ sendMessage?.({
283
+ type: "component-message",
284
+ namespace: "core",
285
+ componentKey: info.key,
286
+ component: "group",
287
+ title: e.currentTarget.value
288
+ });
289
+ setEditingTitle(false);
290
+ };
291
+ const keyDown = (e) => {
292
+ if (e.key == "Enter") {
293
+ updateTitle(e);
294
+ }
295
+ };
296
+ const hasBorder = info.border || displayHeader;
297
+ const childrenElements = hasBorder ? /* @__PURE__ */ jsx3(StyledNestedContent, { children }) : children;
298
+ return /* @__PURE__ */ jsxs2("div", { className: calculateClass(className, !hasBorder && "no-border"), children: [
299
+ displayHeader ? /* @__PURE__ */ jsxs2(
300
+ Header,
301
+ {
302
+ className: calculateClass(
303
+ collapsePressable.touching && "touching",
304
+ collapsible && collapsed && "collapsed"
305
+ ),
306
+ children: [
307
+ collapsible && /* @__PURE__ */ jsx3(
308
+ CollapseIcon,
309
+ {
310
+ icon: collapsed ? "arrow_right" : "arrow_drop_down",
311
+ ...collapsePressable.handlers
312
+ }
313
+ ),
314
+ info.labels?.map((l) => /* @__PURE__ */ jsx3(Label, { children: l.text })),
315
+ showTitle && (info.editableTitle ? editingTitle ? /* @__PURE__ */ jsx3(
316
+ TitleInput,
317
+ {
318
+ ref: (input) => input?.focus(),
319
+ onBlur: updateTitle,
320
+ onKeyDown: keyDown,
321
+ defaultValue: info.title
322
+ }
323
+ ) : /* @__PURE__ */ jsxs2(EditableTitle, { onClick: () => setEditingTitle(true), children: [
324
+ /* @__PURE__ */ jsx3("span", { children: info.title }),
325
+ /* @__PURE__ */ jsx3(Icon, { className: "icon", icon: "edit" })
326
+ ] }) : /* @__PURE__ */ jsx3("span", { children: info.title })),
327
+ collapsible ? /* @__PURE__ */ jsx3(CollapseBar, { ...collapsePressable.handlers }) : /* @__PURE__ */ jsx3(Grow, {}),
328
+ info.headers?.map((h) => h.children.map((c) => renderComponent(c)))
329
+ ]
330
+ }
331
+ ) : null,
332
+ collapsible && collapsed ? null : childrenElements
333
+ ] });
334
+ };
335
+ Group.displayName = "Group";
336
+ var StyledGroup = styled3(Group)`
337
+ border: 1px solid ${THEME.borderDark};
338
+
339
+ > .title {
340
+ padding: 5px;
341
+ background: ${THEME.borderDark};
342
+ border-bottom: 1px solid ${THEME.borderDark};
343
+ }
344
+
345
+ &.no-border {
346
+ border: none;
347
+ margin: 0 !important;
348
+ }
349
+ `;
350
+
351
+ // src/components/label.tsx
352
+ import { styled as styled4 } from "styled-components";
353
+ import { jsx as jsx4 } from "react/jsx-runtime";
354
+ var Label2 = ({ className, info }) => /* @__PURE__ */ jsx4("div", { className, children: info.text });
355
+ var StyledLabel = styled4(Label2)`
356
+ font-weight: ${(p) => p.info.bold ? "bold" : "normal"};
357
+ white-space: nowrap;
358
+ `;
359
+
360
+ // src/components/rect.tsx
361
+ import { styled as styled5 } from "styled-components";
362
+ import { jsx as jsx5 } from "react/jsx-runtime";
363
+ var CLS_GROW = "grow";
364
+ var Wrapper = styled5.div`
365
+ min-width: 30px;
366
+ height: 30px;
367
+ border-radius: 3px;
368
+ overflow: hidden;
369
+ background: url('${TRANSPARENCY_SVG_URI}');
370
+ background-repeat: repeat;
371
+ background-size: 10px;
372
+ border: 1px solid ${THEME.borderDark};
373
+
374
+ &.${CLS_GROW} {
375
+ flex-grow: 1;
376
+ }
377
+ `;
378
+ var Inner = styled5.div`
379
+ width: 100%;
380
+ height: 100%;
381
+ `;
382
+ var Rect = ({ className, info }) => /* @__PURE__ */ jsx5(Wrapper, { className: calculateClass(className, info.grow && CLS_GROW), children: /* @__PURE__ */ jsx5(Inner, { style: { backgroundColor: info.color } }) });
383
+
384
+ // src/components/slider-button.tsx
385
+ import React4, { useCallback } from "react";
386
+ import { styled as styled6 } from "styled-components";
387
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
388
+ var CLASS_SLIDER_DISPLAY = "slider-display";
389
+ var CLASS_SLIDER_VALUE = "slider-value";
390
+ var CLASS_GRADIENT = "gradient";
391
+ var CLASS_GROW = "grow";
392
+ var OPEN_SLIDER_WIDTH = 400;
393
+ var SLIDER_PADDING = 15;
394
+ var SLIDER_VALUE_WIDTH = 60;
395
+ var OPEN_SLIDER_INNER_WIDTH = OPEN_SLIDER_WIDTH - SLIDER_PADDING * 4 - SLIDER_VALUE_WIDTH * 2;
396
+ var KEYS = {
397
+ ArrowUp: "ArrowUp",
398
+ ArrowDown: "ArrowDown",
399
+ Enter: "Enter",
400
+ Escape: "Escape"
401
+ };
402
+ var NUMBER_FORMATTER = new Intl.NumberFormat(void 0, {
403
+ // A large enough value for most usecases,
404
+ // but to avoid inaccuracies from floating-point maths
405
+ maximumFractionDigits: 10
406
+ });
407
+ var getRelativeCursorPosition = (elem, pageX) => {
408
+ const rect = elem.getBoundingClientRect();
409
+ return pageX - rect.left;
410
+ };
411
+ var SliderButton = ({ info, className }) => {
412
+ const { sendMessage } = React4.useContext(StageContext);
413
+ const [state, setState] = React4.useState({ state: "closed" });
414
+ const input = React4.useRef(null);
415
+ const displayValue = (value2) => {
416
+ if (info.max === 1 && info.min === 0) {
417
+ return `${Math.round(value2 * 100)}%`;
418
+ }
419
+ return NUMBER_FORMATTER.format(value2);
420
+ };
421
+ const sendValue = useCallback(
422
+ (value2) => sendMessage?.({
423
+ type: "component-message",
424
+ namespace: "core",
425
+ componentKey: info.key,
426
+ component: "slider_button",
427
+ value: value2
428
+ }),
429
+ [sendMessage, info.key]
430
+ );
431
+ const sanitizeValue = useCallback(
432
+ (value2) => {
433
+ const i = Math.round((value2 - info.min) / info.step);
434
+ const v = i * info.step + info.min;
435
+ return Math.max(info.min, Math.min(info.max, v));
436
+ },
437
+ [info.min, info.max, info.step]
438
+ );
439
+ const getNewValue = useCallback(
440
+ (startValue, diff) => {
441
+ return sanitizeValue((startValue || 0) + diff);
442
+ },
443
+ [sanitizeValue]
444
+ );
445
+ const getCurrentInputValue = useCallback(
446
+ (e) => {
447
+ const float = parseFloat(e.currentTarget.value);
448
+ return sanitizeValue(isNaN(float) ? info.value || 0 : float);
449
+ },
450
+ [info.value, sanitizeValue]
451
+ );
452
+ const onKeyDown = (e) => {
453
+ if (e.key === KEYS.ArrowDown || e.key === KEYS.ArrowUp) {
454
+ const currentValue = getCurrentInputValue(e);
455
+ const diff = e.key === KEYS.ArrowUp ? info.step : -info.step;
456
+ const newValue = sanitizeValue(currentValue + diff);
457
+ e.currentTarget.value = NUMBER_FORMATTER.format(newValue);
458
+ sendValue(newValue);
459
+ } else if (e.key === KEYS.Enter) {
460
+ const sanitizedValue = getCurrentInputValue(e);
461
+ sendValue(sanitizedValue);
462
+ e.currentTarget.value = NUMBER_FORMATTER.format(sanitizedValue);
463
+ } else if (e.key === KEYS.Escape) {
464
+ input.current?.blur();
465
+ }
466
+ };
467
+ const onFocus = (e) => {
468
+ setState({ state: "focused" });
469
+ e.currentTarget.value = `${info.value || 0}`;
470
+ };
471
+ const onBlur = () => {
472
+ setState({ state: "closed" });
473
+ };
474
+ const onTouchStart = (e) => {
475
+ for (const touch of Array.from(e.changedTouches)) {
476
+ const originalPageX = touch.pageX;
477
+ const cursorPosition = getRelativeCursorPosition(
478
+ e.currentTarget,
479
+ touch.pageX
480
+ );
481
+ const start = onDown(cursorPosition);
482
+ trackTouch(
483
+ touch,
484
+ (p) => {
485
+ const amntDiff = (p.pageX - originalPageX) / OPEN_SLIDER_INNER_WIDTH;
486
+ const newValueDiff = (info.max - info.min) * amntDiff;
487
+ sendValue(getNewValue(start.startValue, newValueDiff));
488
+ setState({ ...start, diff: newValueDiff });
489
+ },
490
+ (p) => {
491
+ const amntDiff = (p.pageX - originalPageX) / OPEN_SLIDER_INNER_WIDTH;
492
+ const newValueDiff = (info.max - info.min) * amntDiff;
493
+ sendValue(getNewValue(start.startValue, newValueDiff));
494
+ setState({ state: "closed" });
495
+ }
496
+ );
497
+ return;
498
+ }
499
+ };
500
+ const onDown = (cursorStartPosition) => {
501
+ const value2 = info.value === null ? 0 : info.value;
502
+ const amnt = (value2 - info.min) / (info.max - info.min);
503
+ const innerLeft = cursorStartPosition - amnt * OPEN_SLIDER_INNER_WIDTH - SLIDER_PADDING * 2 - SLIDER_VALUE_WIDTH + "px";
504
+ const start = {
505
+ state: "touching",
506
+ startValue: info.value,
507
+ startX: cursorStartPosition,
508
+ innerLeft,
509
+ diff: 0
510
+ };
511
+ setState(start);
512
+ return start;
513
+ };
514
+ const value = state.state === "touching" ? getNewValue(state.startValue, state.diff) : info.value;
515
+ const valueDisplay = value !== null ? displayValue(value) : "";
516
+ const valueCSSPercent = value ? (value - info.min) / (info.max - info.min) * 100 + "%" : "0";
517
+ const gradientStops = info.gradient && info.gradient.map((g) => `${g.color} ${g.position * 100}%`);
518
+ const sliderGradient = gradientStops ? {
519
+ background: `linear-gradient(90deg, ${gradientStops.join(", ")}), url(${TRANSPARENCY_SVG_URI})`
520
+ } : void 0;
521
+ return /* @__PURE__ */ jsx6(
522
+ "div",
523
+ {
524
+ className: calculateClass(
525
+ className,
526
+ `state-${state.state}`,
527
+ info.grow && CLASS_GROW
528
+ ),
529
+ children: /* @__PURE__ */ jsxs3(
530
+ "div",
531
+ {
532
+ className: "inner",
533
+ onMouseDown: () => setState({ state: "mouse-down" }),
534
+ onMouseUp: () => input.current?.focus(),
535
+ onTouchStart,
536
+ style: state.state === "touching" ? { left: state.innerLeft } : {},
537
+ children: [
538
+ /* @__PURE__ */ jsx6(
539
+ "input",
540
+ {
541
+ type: "text",
542
+ ref: input,
543
+ onFocus,
544
+ onBlur,
545
+ onKeyDown
546
+ }
547
+ ),
548
+ /* @__PURE__ */ jsx6("div", { className: CLASS_SLIDER_VALUE, children: valueDisplay }),
549
+ /* @__PURE__ */ jsx6(
550
+ "div",
551
+ {
552
+ className: calculateClass(
553
+ CLASS_SLIDER_DISPLAY,
554
+ sliderGradient && CLASS_GRADIENT
555
+ ),
556
+ style: sliderGradient,
557
+ children: /* @__PURE__ */ jsx6("div", { className: "inner", style: { width: valueCSSPercent } })
558
+ }
559
+ ),
560
+ /* @__PURE__ */ jsx6("div", { className: CLASS_SLIDER_VALUE, children: valueDisplay })
561
+ ]
562
+ }
563
+ )
564
+ }
565
+ );
566
+ };
567
+ var StyledSliderButton = styled6(SliderButton)`
568
+ position: relative;
569
+ min-width: 100px;
570
+ min-height: 30px;
571
+
572
+ &.${CLASS_GROW} {
573
+ flex-grow: 1;
574
+ }
575
+
576
+ > .inner {
577
+ position: absolute;
578
+ display: flex;
579
+ align-items: center;
580
+ padding: 0 ${SLIDER_PADDING / 2}px;
581
+ left: 0;
582
+ top: 0;
583
+ bottom: 0;
584
+ width: 100%;
585
+ cursor: pointer;
586
+ transition: all 200ms;
587
+ border-radius: 3px;
588
+ border: 1px solid ${THEME.borderDark};
589
+ ${buttonStateNormal}
590
+
591
+ > input {
592
+ color: ${THEME.textNormal};
593
+ opacity: 0;
594
+ margin: 0 -9px;
595
+ padding: 6px 8px;
596
+ width: 0;
597
+ pointer-events: none;
598
+ transition: all 200ms;
599
+ border-radius: 3px;
600
+ background: ${THEME.bgDark1};
601
+ border: 1px solid ${THEME.borderDark};
602
+ overflow: hidden;
603
+ box-shadow: inset 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
604
+ }
605
+
606
+ > .${CLASS_SLIDER_DISPLAY} {
607
+ flex-grow: 1;
608
+ margin: 0 ${SLIDER_PADDING / 2}px;
609
+ height: 4px;
610
+ background: ${THEME.bgDark1};
611
+ border: 1px solid ${THEME.borderDark};
612
+
613
+ > .inner {
614
+ height: 100%;
615
+ background: ${THEME.hint};
616
+ }
617
+
618
+ &.${CLASS_GRADIENT} {
619
+ height: 10px;
620
+
621
+ > .inner {
622
+ position: relative;
623
+ background: none;
624
+ border-right: 2px solid ${THEME.borderDark};
625
+
626
+ &::before {
627
+ content: '';
628
+ position: absolute;
629
+ width: 4px;
630
+ top: -5px;
631
+ bottom: -5px;
632
+ right: -3px;
633
+ background: ${THEME.borderDark};
634
+ }
635
+
636
+ &::after {
637
+ content: '';
638
+ position: absolute;
639
+ width: 2px;
640
+ top: -4px;
641
+ bottom: -4px;
642
+ right: -2px;
643
+ background: ${THEME.textNormal};
644
+ }
645
+ }
646
+ }
647
+ }
648
+
649
+ > .${CLASS_SLIDER_VALUE} {
650
+ width: ${SLIDER_VALUE_WIDTH}px;
651
+ margin: 0 -${SLIDER_VALUE_WIDTH / 2}px;
652
+ line-height: 30px;
653
+ text-align: center;
654
+ opacity: 0;
655
+ }
656
+
657
+ &:hover {
658
+ ${buttonStateNormalHover}
659
+ }
660
+ }
661
+
662
+ &.state-mouse-down {
663
+ > .inner {
664
+ ${buttonPressed}
665
+ }
666
+ }
667
+
668
+ &.state-focused {
669
+ > .inner {
670
+ > input {
671
+ opacity: 1;
672
+ width: 60%;
673
+ padding: 0 5px;
674
+ margin: 0;
675
+ }
676
+ }
677
+ }
678
+
679
+ &.state-touching {
680
+ z-index: 100;
681
+
682
+ .inner {
683
+ background: ${THEME.bgDark1};
684
+ width: ${OPEN_SLIDER_WIDTH}px;
685
+
686
+ > .${CLASS_SLIDER_VALUE} {
687
+ opacity: 1;
688
+ margin: 0 ${SLIDER_PADDING / 2}px;
689
+ }
690
+ }
691
+ }
692
+ `;
693
+
694
+ // src/components/switch.tsx
695
+ import React5, { useMemo, useState as useState2 } from "react";
696
+ import { styled as styled7 } from "styled-components";
697
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
698
+ var CLASS_TOUCHING = "touching";
699
+ var TOUCH_INDICATOR_CLASS2 = "touch-indicator";
700
+ var Switch = ({ className, info }) => {
701
+ const { sendMessage } = React5.useContext(StageContext);
702
+ const [touching, setTouching] = useState2(false);
703
+ const onClick = useMemo(
704
+ () => () => {
705
+ sendMessage?.({
706
+ type: "component-message",
707
+ namespace: "core",
708
+ componentKey: info.key,
709
+ component: "switch"
710
+ });
711
+ },
712
+ [sendMessage, info.key]
713
+ );
714
+ const onTouchStart = useMemo(
715
+ () => (event) => {
716
+ event.preventDefault();
717
+ setTouching(true);
718
+ },
719
+ []
720
+ );
721
+ const onTouchEnd = useMemo(
722
+ () => (event) => {
723
+ event.preventDefault();
724
+ setTouching(false);
725
+ onClick();
726
+ },
727
+ []
728
+ );
729
+ return /* @__PURE__ */ jsxs4(
730
+ "div",
731
+ {
732
+ className: calculateClass(className, touching && CLASS_TOUCHING),
733
+ onClick,
734
+ onTouchStart,
735
+ onTouchEnd,
736
+ children: [
737
+ /* @__PURE__ */ jsx7("div", { className: TOUCH_INDICATOR_CLASS2 }),
738
+ /* @__PURE__ */ jsx7("div", { className: "inner", children: /* @__PURE__ */ jsxs4("div", { className: "slider" + (info.state === "on" ? " on" : ""), children: [
739
+ /* @__PURE__ */ jsx7("div", { className: "on-text", children: "ON" }),
740
+ /* @__PURE__ */ jsx7("div", { className: "off-text", children: "OFF" }),
741
+ /* @__PURE__ */ jsx7("div", { className: "button" })
742
+ ] }) })
743
+ ]
744
+ }
745
+ );
746
+ };
747
+ var SWITCH_HEIGHT = 30;
748
+ var BUTTON_WIDTH = 30;
749
+ var TEXT_WIDTH = 40;
750
+ var StyledSwitch = styled7(Switch)`
751
+ position: relative;
752
+
753
+ .inner {
754
+ display: block;
755
+ position: relative;
756
+ overflow: hidden;
757
+ width: ${BUTTON_WIDTH + TEXT_WIDTH}px;
758
+ min-width: ${BUTTON_WIDTH + TEXT_WIDTH}px;
759
+ height: ${SWITCH_HEIGHT}px;
760
+ border-radius: 3px;
761
+ border: 1px solid ${THEME.borderDark};
762
+
763
+ > .slider {
764
+ position: absolute;
765
+ top: 0;
766
+ left: 0;
767
+ cursor: pointer;
768
+ transition: left 300ms;
769
+
770
+ > .on-text,
771
+ .off-text,
772
+ .button {
773
+ position: absolute;
774
+ height: ${SWITCH_HEIGHT}px;
775
+ }
776
+
777
+ > .on-text,
778
+ .off-text {
779
+ width: ${TEXT_WIDTH}px;
780
+ text-align: center;
781
+ top: 0;
782
+ line-height: ${SWITCH_HEIGHT - 2}px;
783
+ text-shadow: 0 -1px rgba(0, 0, 0, 0.4);
784
+ box-shadow:
785
+ inset 0 1px 2px rgba(0, 0, 0, 0.2),
786
+ 0 1px 0 0 rgba(255, 255, 255, 0.15);
787
+ }
788
+
789
+ > .on-text {
790
+ left: -40px;
791
+ background: linear-gradient(
792
+ to bottom,
793
+ ${THEME.hintDark1},
794
+ ${THEME.hint}
795
+ );
796
+ }
797
+
798
+ > .button {
799
+ top: -1px;
800
+ left: -1px;
801
+ width: ${BUTTON_WIDTH}px;
802
+ background: linear-gradient(to bottom, #4f5053, #343436);
803
+ text-shadow: 0 -1px rgba(0, 0, 0, 0.7);
804
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
805
+ border-radius: 3px;
806
+ border: 1px solid ${THEME.borderDark};
807
+ }
808
+
809
+ > .off-text {
810
+ background: linear-gradient(to bottom, #242525, #37383a);
811
+ left: ${BUTTON_WIDTH - 2}px;
812
+ }
813
+
814
+ &.on {
815
+ left: 40px;
816
+ }
817
+
818
+ &:hover > .button {
819
+ background: linear-gradient(to bottom, #5e6064, #393a3b);
820
+ }
821
+ }
822
+ }
823
+
824
+ .${TOUCH_INDICATOR_CLASS2} {
825
+ ${touchIndicatorNormal}
826
+ }
827
+
828
+ &.${CLASS_TOUCHING} {
829
+ .${TOUCH_INDICATOR_CLASS2} {
830
+ ${touchIndicatorTouching}
831
+ }
832
+ }
833
+ `;
834
+
835
+ // src/components/tabs.tsx
836
+ import React6 from "react";
837
+ import { styled as styled8 } from "styled-components";
838
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
839
+ var Wrapper2 = styled8.div`
840
+ display: flex;
841
+ flex-direction: column;
842
+ background: ${THEME.borderDark};
843
+ border: 1px solid ${THEME.borderDark};
844
+ `;
845
+ var TabList = styled8.div`
846
+ display: flex;
847
+ flex-direction: row;
848
+ border-bottom: 1px solid ${THEME.borderDark};
849
+ `;
850
+ var TabItem = styled8.div`
851
+ height: ${THEME.sizingPx.spacing * 3}px;
852
+ display: flex;
853
+ flex-direction: column;
854
+ justify-content: center;
855
+ padding: 0 ${THEME.sizingPx.spacing}px;
856
+ cursor: pointer;
857
+ background: ${THEME.bgDark1};
858
+ margin-right: 1px;
859
+
860
+ &:hover,
861
+ &.touching {
862
+ background: ${THEME.bgLight1};
863
+ }
864
+
865
+ &.current {
866
+ color: ${THEME.hint};
867
+
868
+ &::after {
869
+ content: '';
870
+ border-bottom: 2px solid ${THEME.hint};
871
+ display: block;
872
+ margin-top: ${THEME.sizingPx.spacing / 2}px;
873
+ }
874
+ }
875
+ `;
876
+ var Tabs = (props) => {
877
+ const { renderComponent } = React6.useContext(StageContext);
878
+ const [touching, setTouching] = React6.useState(null);
879
+ const [currentTab, setCurrentTab] = React6.useState(0);
880
+ const tab = props.info.tabs[currentTab];
881
+ return /* @__PURE__ */ jsxs5(Wrapper2, { children: [
882
+ /* @__PURE__ */ jsx8(TabList, { children: props.info.tabs.map((tab2, i) => /* @__PURE__ */ jsx8(
883
+ TabItem,
884
+ {
885
+ className: calculateClass(
886
+ touching === i && "touching",
887
+ currentTab === i && "current"
888
+ ),
889
+ onClick: () => setCurrentTab(i),
890
+ onTouchStart: (event) => {
891
+ event.preventDefault();
892
+ setTouching(i);
893
+ },
894
+ onTouchEnd: (event) => {
895
+ event.preventDefault();
896
+ setTouching(null);
897
+ setCurrentTab(i);
898
+ },
899
+ children: tab2.name
900
+ },
901
+ i
902
+ )) }),
903
+ /* @__PURE__ */ jsx8(StyledNestedContent, { children: tab?.child && renderComponent(tab.child) })
904
+ ] });
905
+ };
906
+
907
+ // src/components/text-input.tsx
908
+ import React7, { useEffect } from "react";
909
+ import { styled as styled9 } from "styled-components";
910
+ import { jsx as jsx9 } from "react/jsx-runtime";
911
+ var TextInput = ({ className, info }) => {
912
+ const { sendMessage } = React7.useContext(StageContext);
913
+ const ref = React7.useRef(null);
914
+ useEffect(() => {
915
+ if (ref.current && ref.current.value !== info.value) {
916
+ ref.current.value = info.value;
917
+ }
918
+ }, [info.value]);
919
+ return /* @__PURE__ */ jsx9(
920
+ "input",
921
+ {
922
+ className,
923
+ defaultValue: info.value,
924
+ ref,
925
+ onChange: (ev) => sendMessage?.({
926
+ type: "component-message",
927
+ namespace: "core",
928
+ componentKey: info.key,
929
+ component: "text-input",
930
+ value: ev.target.value
931
+ })
932
+ }
933
+ );
934
+ };
935
+ var StyledTextInput = styled9(TextInput)`
936
+ position: relative;
937
+ box-sizing: border-box;
938
+ padding: 6px 8px;
939
+ border-radius: 3px;
940
+ background: ${THEME.bgDark1};
941
+ border: 1px solid ${THEME.borderDark};
942
+ overflow: hidden;
943
+ box-shadow: inset 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
944
+ color: ${THEME.textNormal};
945
+ text-shadow: 0 -1px rgba(0, 0, 0, 0.7);
946
+
947
+ @media (max-width: 500px) {
948
+ flex-basis: 100%;
949
+ }
950
+ `;
951
+
952
+ // src/components/timeline.tsx
953
+ import React8, { useEffect as useEffect2, useState as useState3 } from "react";
954
+ import { styled as styled10 } from "styled-components";
955
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
956
+ var Wrapper3 = styled10.div`
957
+ flex-grow: 1;
958
+ `;
959
+ var Data = styled10.div`
960
+ display: flex;
961
+ `;
962
+ var Metadata = styled10.div`
963
+ flex-grow: 1;
964
+ `;
965
+ var SourceData = styled10.div`
966
+ display: flex;
967
+ flex-direction: column;
968
+ align-items: end;
969
+ justify-content: center;
970
+ `;
971
+ var IndicatorIcon = styled10(Icon)`
972
+ font-size: 40px;
973
+ `;
974
+ var Bar = styled10.div`
975
+ width: 100%;
976
+ height: 10px;
977
+ border: 1px solid ${THEME.borderDark};
978
+ background: ${THEME.borderDark};
979
+ `;
980
+ var Fill = styled10.div`
981
+ height: 100%;
982
+ background: ${THEME.hint};
983
+ `;
984
+ var Title = styled10.div`
985
+ font-size: 1.5em;
986
+ font-weight: bold;
987
+ margin-bottom: 0.5em;
988
+ `;
989
+ var Subtitle = styled10.div`
990
+ font-size: 1em;
991
+ font-weight: bold;
992
+ margin-bottom: 0.5em;
993
+ `;
994
+ var Timeline = (props) => {
995
+ const { className, info } = props;
996
+ const frameState = React8.useRef({
997
+ animationFrame: -1,
998
+ state: null
999
+ });
1000
+ const [currentTimeMillis, setCurrentTimeMillis] = useState3(0);
1001
+ useEffect2(() => {
1002
+ frameState.current.state = info.state;
1003
+ const recalculateCurrentTimeMillis = () => {
1004
+ if (frameState.current.state !== info.state) {
1005
+ return;
1006
+ }
1007
+ if (info.state.state === "playing") {
1008
+ setCurrentTimeMillis(
1009
+ (Date.now() - info.state.effectiveStartTime) * info.state.speed
1010
+ );
1011
+ frameState.current.animationFrame = window.requestAnimationFrame(
1012
+ recalculateCurrentTimeMillis
1013
+ );
1014
+ } else {
1015
+ setCurrentTimeMillis(info.state.currentTimeMillis);
1016
+ }
1017
+ };
1018
+ recalculateCurrentTimeMillis();
1019
+ return () => {
1020
+ window.cancelAnimationFrame(frameState.current.animationFrame);
1021
+ };
1022
+ }, [frameState, info.state]);
1023
+ return /* @__PURE__ */ jsxs6(Wrapper3, { className, children: [
1024
+ /* @__PURE__ */ jsxs6(Data, { children: [
1025
+ /* @__PURE__ */ jsxs6(Metadata, { children: [
1026
+ info.title && /* @__PURE__ */ jsx10(Title, { children: info.title }),
1027
+ info.subtitles?.map((subtitle, k) => /* @__PURE__ */ jsx10(Subtitle, { children: subtitle }, k))
1028
+ ] }),
1029
+ /* @__PURE__ */ jsxs6(SourceData, { children: [
1030
+ info.source?.name,
1031
+ /* @__PURE__ */ jsx10(
1032
+ IndicatorIcon,
1033
+ {
1034
+ icon: info.state.state === "playing" ? "play_arrow" : "pause"
1035
+ }
1036
+ )
1037
+ ] })
1038
+ ] }),
1039
+ /* @__PURE__ */ jsx10(Bar, { children: /* @__PURE__ */ jsx10(
1040
+ Fill,
1041
+ {
1042
+ style: {
1043
+ width: `${Math.min(100, 100 * currentTimeMillis / info.state.totalTimeMillis)}%`
1044
+ }
1045
+ }
1046
+ ) })
1047
+ ] });
1048
+ };
1049
+
1050
+ // src/components/index.tsx
1051
+ import { jsx as jsx11 } from "react/jsx-runtime";
1052
+ var CORE_FRONTEND_COMPONENT_RENDERER = {
1053
+ namespace: "core",
1054
+ render: (info) => {
1055
+ if (!isCoreComponent(info)) {
1056
+ throw new Error(`Cannot render non-core component ${info.namespace}`);
1057
+ }
1058
+ switch (info.component) {
1059
+ case "button":
1060
+ return /* @__PURE__ */ jsx11(StyledButton, { info }, info.key);
1061
+ case "group":
1062
+ return /* @__PURE__ */ jsx11(StyledGroup, { info }, info.key);
1063
+ case "label":
1064
+ return /* @__PURE__ */ jsx11(StyledLabel, { info }, info.key);
1065
+ case "rect":
1066
+ return /* @__PURE__ */ jsx11(Rect, { info }, info.key);
1067
+ case "slider_button":
1068
+ return /* @__PURE__ */ jsx11(StyledSliderButton, { info }, info.key);
1069
+ case "switch":
1070
+ return /* @__PURE__ */ jsx11(StyledSwitch, { info }, info.key);
1071
+ case "tabs":
1072
+ return /* @__PURE__ */ jsx11(Tabs, { info }, info.key);
1073
+ case "text-input":
1074
+ return /* @__PURE__ */ jsx11(StyledTextInput, { info }, info.key);
1075
+ case "timeline":
1076
+ return /* @__PURE__ */ jsx11(Timeline, { info }, info.key);
1077
+ case "group-header":
1078
+ case "tab":
1079
+ throw new Error(
1080
+ `Cannot render ${info.component} outside of expected parents`
1081
+ );
1082
+ }
1083
+ }
1084
+ };
1085
+ export {
1086
+ StyledButton as Button,
1087
+ CORE_FRONTEND_COMPONENT_RENDERER,
1088
+ StyledGroup as Group,
1089
+ GroupStateWrapper,
1090
+ StyledLabel as Label,
1091
+ StyledNestedContent as NestedContent,
1092
+ Rect,
1093
+ StyledSliderButton as SliderButton,
1094
+ StageContext,
1095
+ StyledSwitch as Switch,
1096
+ Tabs,
1097
+ StyledTextInput as TextInput,
1098
+ Timeline,
1099
+ core_exports as code
1100
+ };