@unityevolv/ofiskit-ui-builder 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.
Files changed (79) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +30 -0
  3. package/dist/BuilderSteps.d.ts +35 -0
  4. package/dist/BuilderSteps.d.ts.map +1 -0
  5. package/dist/BuilderSteps.js +104 -0
  6. package/dist/BuilderSteps.js.map +1 -0
  7. package/dist/OfficeBuilder.d.ts +30 -0
  8. package/dist/OfficeBuilder.d.ts.map +1 -0
  9. package/dist/OfficeBuilder.js +55 -0
  10. package/dist/OfficeBuilder.js.map +1 -0
  11. package/dist/background/BackgroundStep.d.ts +35 -0
  12. package/dist/background/BackgroundStep.d.ts.map +1 -0
  13. package/dist/background/BackgroundStep.js +104 -0
  14. package/dist/background/BackgroundStep.js.map +1 -0
  15. package/dist/background/darkVersion.d.ts +25 -0
  16. package/dist/background/darkVersion.d.ts.map +1 -0
  17. package/dist/background/darkVersion.js +124 -0
  18. package/dist/background/darkVersion.js.map +1 -0
  19. package/dist/canvas/BuilderCanvas.d.ts +32 -0
  20. package/dist/canvas/BuilderCanvas.d.ts.map +1 -0
  21. package/dist/canvas/BuilderCanvas.js +40 -0
  22. package/dist/canvas/BuilderCanvas.js.map +1 -0
  23. package/dist/canvas/RoomShape.d.ts +27 -0
  24. package/dist/canvas/RoomShape.d.ts.map +1 -0
  25. package/dist/canvas/RoomShape.js +45 -0
  26. package/dist/canvas/RoomShape.js.map +1 -0
  27. package/dist/canvas/geometry.d.ts +115 -0
  28. package/dist/canvas/geometry.d.ts.map +1 -0
  29. package/dist/canvas/geometry.js +222 -0
  30. package/dist/canvas/geometry.js.map +1 -0
  31. package/dist/canvas/types.d.ts +47 -0
  32. package/dist/canvas/types.d.ts.map +1 -0
  33. package/dist/canvas/types.js +2 -0
  34. package/dist/canvas/types.js.map +1 -0
  35. package/dist/canvas/useCanvasDrag.d.ts +35 -0
  36. package/dist/canvas/useCanvasDrag.d.ts.map +1 -0
  37. package/dist/canvas/useCanvasDrag.js +138 -0
  38. package/dist/canvas/useCanvasDrag.js.map +1 -0
  39. package/dist/canvas/useCanvasKeys.d.ts +11 -0
  40. package/dist/canvas/useCanvasKeys.d.ts.map +1 -0
  41. package/dist/canvas/useCanvasKeys.js +43 -0
  42. package/dist/canvas/useCanvasKeys.js.map +1 -0
  43. package/dist/history.d.ts +31 -0
  44. package/dist/history.d.ts.map +1 -0
  45. package/dist/history.js +49 -0
  46. package/dist/history.js.map +1 -0
  47. package/dist/index.d.ts +21 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +20 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/panel/AreaInspector.d.ts +10 -0
  52. package/dist/panel/AreaInspector.d.ts.map +1 -0
  53. package/dist/panel/AreaInspector.js +30 -0
  54. package/dist/panel/AreaInspector.js.map +1 -0
  55. package/dist/panel/RoomInspector.d.ts +16 -0
  56. package/dist/panel/RoomInspector.d.ts.map +1 -0
  57. package/dist/panel/RoomInspector.js +26 -0
  58. package/dist/panel/RoomInspector.js.map +1 -0
  59. package/dist/panel/SaveBar.d.ts +7 -0
  60. package/dist/panel/SaveBar.d.ts.map +1 -0
  61. package/dist/panel/SaveBar.js +16 -0
  62. package/dist/panel/SaveBar.js.map +1 -0
  63. package/dist/panel/TemplateDetails.d.ts +16 -0
  64. package/dist/panel/TemplateDetails.d.ts.map +1 -0
  65. package/dist/panel/TemplateDetails.js +16 -0
  66. package/dist/panel/TemplateDetails.js.map +1 -0
  67. package/dist/panel/ToolBar.d.ts +34 -0
  68. package/dist/panel/ToolBar.d.ts.map +1 -0
  69. package/dist/panel/ToolBar.js +8 -0
  70. package/dist/panel/ToolBar.js.map +1 -0
  71. package/dist/prompt/PromptStep.d.ts +23 -0
  72. package/dist/prompt/PromptStep.d.ts.map +1 -0
  73. package/dist/prompt/PromptStep.js +24 -0
  74. package/dist/prompt/PromptStep.js.map +1 -0
  75. package/dist/prompt/prompt.d.ts +59 -0
  76. package/dist/prompt/prompt.d.ts.map +1 -0
  77. package/dist/prompt/prompt.js +40 -0
  78. package/dist/prompt/prompt.js.map +1 -0
  79. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @unityevolv/ofiskit-ui-builder
2
+
3
+ The office builder: three steps from nothing to a layout — a prompt for
4
+ generating the background picture, uploading it, and drawing the rooms on it.
5
+
6
+ ```tsx
7
+ import { BuilderSteps } from '@unityevolv/ofiskit-ui-builder'
8
+
9
+ ;<BuilderSteps document={promptDocument} saveLabel="Download template.json" onSave={save} />
10
+ ```
11
+
12
+ It takes a layout and emits a layout, and does not know what the host will do
13
+ with it: the free app downloads `template.json`, and a product saves it to a
14
+ database. Rooms and user areas snap to a grid, stay inside their room, and are
15
+ validated as they are drawn, so a layout is caught while it is being made rather
16
+ than when somebody walks into a wall.
17
+
18
+ `darkVersion` makes the dark theme's picture from an SVG background, so two
19
+ pictures of one office cannot drift apart.
20
+
21
+ Peer dependencies: `react`, `react-dom` and `@unityevolv/unitykit`.
22
+
23
+ ## Licence
24
+
25
+ AGPL-3.0-only. The interface packages (`ofiskit-template`,
26
+ `ofiskit-presence-store` and `ofiskit-adapters`) are Apache-2.0, so building
27
+ against the interfaces is not a licensing decision even though extending this is.
28
+
29
+ Part of [ofiskit](https://github.com/UnityEvolv/ofis-kit): a virtual office you
30
+ can run yourself. [Try the demo](https://unityevolv.com/ofis-kit/).
@@ -0,0 +1,35 @@
1
+ import { type CanvasShape, type Template } from '@unityevolv/ofiskit-template';
2
+ import { type ReactNode } from 'react';
3
+ import { type BackgroundImages } from './background/BackgroundStep.js';
4
+ import type { PromptDocument } from './prompt/prompt.js';
5
+ /**
6
+ * The whole builder, as three steps: the prompt, the background, the rooms.
7
+ *
8
+ * Each step is one job. Writing a prompt for a picture, loading the picture, and
9
+ * drawing rooms on it are different kinds of work, and on one screen the upload
10
+ * sat under a long prompt and the drawing began without warning.
11
+ *
12
+ * Every step stays mounted and only the current one is shown, so moving back and
13
+ * forth loses nothing: the prompt's fields, the images, and the rooms with their
14
+ * undo history are all where they were left. The one thing that does start
15
+ * again is the layout, and only when the shape changes, because every room is
16
+ * placed relative to it.
17
+ */
18
+ export interface BuilderStepsProps {
19
+ document: PromptDocument;
20
+ /** What the host's save button says. Download here; Save elsewhere. */
21
+ saveLabel: string;
22
+ /**
23
+ * The finished template, with `images` naming the files as they are now, and the
24
+ * images themselves as object URLs for a host that stores them.
25
+ */
26
+ onSave(template: Template, images: BackgroundImages): void;
27
+ /** Shown above the rooms: where the host's saved files go. */
28
+ saveHint?(files: {
29
+ light: string;
30
+ dark?: string;
31
+ }): ReactNode;
32
+ initialShape?: CanvasShape;
33
+ }
34
+ export declare function BuilderSteps(props: BuilderStepsProps): import("react").JSX.Element;
35
+ //# sourceMappingURL=BuilderSteps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BuilderSteps.d.ts","sourceRoot":"","sources":["../src/BuilderSteps.tsx"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AAC9F,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGnE,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAA;IACxB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC1D,8DAA8D;IAC9D,QAAQ,CAAC,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAC7D,YAAY,CAAC,EAAE,WAAW,CAAA;CAC3B;AAuBD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BA2LpD"}
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert, Button, Icon, Stepper } from '@unityevolv/unitykit';
3
+ import { createTemplate } from '@unityevolv/ofiskit-template';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { OfficeBuilder } from './OfficeBuilder.js';
6
+ import { BackgroundStep } from './background/BackgroundStep.js';
7
+ import { PromptStep } from './prompt/PromptStep.js';
8
+ /*
9
+ * Horizontal at every width, with the descriptions dropped on a phone: stacked
10
+ * vertically, three steps took a quarter of the screen before any of the step
11
+ * itself, and the one-word labels say enough on their own.
12
+ */
13
+ const onWide = (text) => _jsx("span", { className: "max-sm:hidden", children: text });
14
+ const STEPS = [
15
+ { key: 'prompt', label: 'Prompt', description: onWide('Describe the picture') },
16
+ {
17
+ key: 'background',
18
+ label: 'Background',
19
+ description: onWide('Upload it, and a dark version'),
20
+ },
21
+ { key: 'rooms', label: 'Rooms', description: onWide('Place rooms and areas') },
22
+ ];
23
+ const PROMPT = 0;
24
+ const BACKGROUND = 1;
25
+ const ROOMS = 2;
26
+ export function BuilderSteps(props) {
27
+ const [step, setStep] = useState(PROMPT);
28
+ /** The furthest step reached, which the indicator lets the author jump back to. */
29
+ const [reached, setReached] = useState(PROMPT);
30
+ const [shape, setShape] = useState(props.initialShape ?? 'landscape');
31
+ const [images, setImages] = useState({ light: null, dark: null });
32
+ // What the files are called beside template.json, keeping the type each was
33
+ // uploaded as: an SVG background is office-light.svg, not a .webp name.
34
+ const [files, setFiles] = useState({ light: 'office-light.webp', dark: 'office-dark.webp' });
35
+ // Images are checked against the shape, so a new shape needs them again. The
36
+ // key remounts the step, which clears what it was holding for the old ones.
37
+ const [backgroundKey, setBackgroundKey] = useState(0);
38
+ /** The layout the rooms step opened with; the builder keeps its own edits. */
39
+ const [layout, setLayout] = useState(null);
40
+ /** The file names for the template: a dark one only when there is a dark image. */
41
+ const namesFor = (current) => ({
42
+ light: current.light,
43
+ ...(images.dark ? { dark: current.dark } : {}),
44
+ });
45
+ const canEnter = (index) => index !== ROOMS || images.light !== null;
46
+ function go(index) {
47
+ if (index < PROMPT || index > ROOMS || !canEnter(index))
48
+ return;
49
+ if (index === ROOMS && (!layout || layout.template.canvas !== shape)) {
50
+ setLayout({
51
+ template: createTemplate({ name: 'My office', canvas: shape, images: namesFor(files) }),
52
+ key: (layout?.key ?? 0) + 1,
53
+ });
54
+ }
55
+ setStep(index);
56
+ setReached((current) => Math.max(current, index));
57
+ }
58
+ function changeShape(next) {
59
+ if (next === shape)
60
+ return;
61
+ setShape(next);
62
+ if (images.light || images.dark) {
63
+ setImages({ light: null, dark: null });
64
+ setBackgroundKey((key) => key + 1);
65
+ setReached((current) => Math.min(current, BACKGROUND));
66
+ }
67
+ }
68
+ /*
69
+ * Where focus goes when the step changes: to the step itself, so a screen
70
+ * reader lands at the top of the new content instead of on a Next button that
71
+ * now belongs to a different step. Not on the first render, which is the page
72
+ * opening and should leave focus where the browser put it.
73
+ */
74
+ const panels = useRef([]);
75
+ const first = useRef(true);
76
+ useEffect(() => {
77
+ if (first.current) {
78
+ first.current = false;
79
+ return;
80
+ }
81
+ panels.current[step]?.focus();
82
+ }, [step]);
83
+ const warning = [
84
+ layout && 'You have already placed rooms. Choosing a different shape starts the layout again.',
85
+ (images.light || images.dark) &&
86
+ 'Your images were checked against this shape, so a different one needs them uploaded again.',
87
+ ]
88
+ .filter(Boolean)
89
+ .join(' ');
90
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "border-b border-base-300 bg-base-100 px-4 py-3", children: _jsx("div", { className: "mx-auto w-full max-w-3xl", children: _jsx(Stepper, { steps: STEPS, current: step, label: "Office builder steps", orientation: "horizontal", onStepClick: go, allowUpcoming: reached > step }) }) }), _jsxs("div", { className: "min-h-0 flex-1 overflow-auto", children: [_jsx("section", { ref: (node) => {
91
+ panels.current[PROMPT] = node;
92
+ }, tabIndex: -1, hidden: step !== PROMPT, "aria-label": "Prompt", className: "outline-none", children: _jsx(PromptStep, { document: props.document, shape: shape, onShapeChange: changeShape, ...(warning ? { shapeChangeWarning: warning } : {}) }) }), _jsx("section", { ref: (node) => {
93
+ panels.current[BACKGROUND] = node;
94
+ }, tabIndex: -1, hidden: step !== BACKGROUND, "aria-label": "Background", className: "outline-none", children: _jsx(BackgroundStep, { document: props.document, shape: shape, images: images, onImage: (slot, url, fileName) => {
95
+ setImages((current) => ({ ...current, [slot]: url }));
96
+ setFiles((current) => ({ ...current, [slot]: fileName }));
97
+ } }, backgroundKey) }), _jsx("section", { ref: (node) => {
98
+ panels.current[ROOMS] = node;
99
+ }, tabIndex: -1, hidden: step !== ROOMS, "aria-label": "Rooms", className: "outline-none", children: layout && (_jsxs(_Fragment, { children: [props.saveHint && (_jsx(Alert, { variant: "info", className: "mx-4 mt-4", children: props.saveHint(namesFor(files)) })), _jsx(OfficeBuilder, { template: layout.template, imageUrl: images.light, darkImageUrl: images.dark, saveLabel: props.saveLabel,
100
+ // The names as they are now, not as they were when the layout
101
+ // opened: the images may have been replaced since.
102
+ onSave: (finished) => props.onSave({ ...finished, images: namesFor(files) }, images), onPickImage: () => go(BACKGROUND) }, layout.key)] })) })] }), _jsxs("nav", { "aria-label": "Builder steps", className: "flex items-center gap-2 border-t border-base-300 bg-base-100 px-4 py-2", children: [step > PROMPT && (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => go(step - 1), children: [_jsx(Icon, { name: "chevron-left", size: "sm" }), " Back"] })), step === BACKGROUND && !images.light && (_jsx("span", { className: "ml-auto text-xs text-base-content/70", children: "Add a background image to continue." })), step < ROOMS && (_jsxs(Button, { size: "sm", className: step === BACKGROUND && !images.light ? '' : 'ml-auto', disabled: !canEnter(step + 1), onClick: () => go(step + 1), children: ["Next: ", STEPS[step + 1]?.label, _jsx(Icon, { name: "chevron-right", size: "sm" })] }))] })] }));
103
+ }
104
+ //# sourceMappingURL=BuilderSteps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BuilderSteps.js","sourceRoot":"","sources":["../src/BuilderSteps.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { type Template } from '@unityevolv/ofiskit-template';
2
+ /**
3
+ * The office builder.
4
+ *
5
+ * It takes a layout and emits a layout. It does not know whether the host will
6
+ * write a file, save a platform template or save an org template — the free app
7
+ * downloads `template.json`, the wrapper apps save to a database, and the
8
+ * component is the same in all three.
9
+ *
10
+ * The panel on the right is always visible and never collapses into a menu. Every
11
+ * control is one click away, which matters because drawing a layout is a loop of
12
+ * draw, look, adjust, and a menu in the middle of that loop is felt on every
13
+ * iteration.
14
+ *
15
+ * What is left in this file is the assembly: the canvas, the panel, the selection
16
+ * they share and the history they both write to. Each part of the panel is its own
17
+ * file, because they are unrelated jobs that happen to sit in one column.
18
+ */
19
+ export interface OfficeBuilderProps {
20
+ template: Template;
21
+ imageUrl: string | null;
22
+ darkImageUrl?: string | null;
23
+ /** What the host's save button says and does. Download here; Save elsewhere. */
24
+ saveLabel: string;
25
+ onSave(template: Template): void;
26
+ /** Lets the host swap the images; the builder does not store files. */
27
+ onPickImage?(which: 'light' | 'dark'): void;
28
+ }
29
+ export declare function OfficeBuilder(props: OfficeBuilderProps): import("react").JSX.Element;
30
+ //# sourceMappingURL=OfficeBuilder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfficeBuilder.d.ts","sourceRoot":"","sources":["../src/OfficeBuilder.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,QAAQ,EAEd,MAAM,8BAA8B,CAAA;AAYrC;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IAChC,uEAAuE;IACvE,WAAW,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;CAC5C;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BAsHtD"}
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { validateTemplate, withAvatarSize, } from '@unityevolv/ofiskit-template';
3
+ import { useMemo, useState } from 'react';
4
+ import { BuilderCanvas } from './canvas/BuilderCanvas.js';
5
+ import { useHistory } from './history.js';
6
+ import { AreaInspector } from './panel/AreaInspector.js';
7
+ import { RoomInspector } from './panel/RoomInspector.js';
8
+ import { SaveBar } from './panel/SaveBar.js';
9
+ import { TemplateDetails } from './panel/TemplateDetails.js';
10
+ import { ToolBar } from './panel/ToolBar.js';
11
+ export function OfficeBuilder(props) {
12
+ const history = useHistory(props.template);
13
+ const template = history.value;
14
+ const [tool, setTool] = useState('select');
15
+ const [selection, setSelection] = useState({ kind: 'none' });
16
+ const [showGhosts, setShowGhosts] = useState(true);
17
+ const [showDark, setShowDark] = useState(false);
18
+ const [unfitted, setUnfitted] = useState([]);
19
+ const validation = useMemo(() => validateTemplate(template), [template]);
20
+ const issues = validation.ok ? [] : validation.issues;
21
+ const room = selection.kind !== 'none' && 'roomId' in selection
22
+ ? template.rooms.find((candidate) => candidate.id === selection.roomId)
23
+ : undefined;
24
+ const area = selection.kind === 'area'
25
+ ? room?.areas.find((candidate) => candidate.id === selection.areaId)
26
+ : undefined;
27
+ /**
28
+ * One way in for every change, so undo behaves the same wherever it came from.
29
+ *
30
+ * A drag reports continuously and must not leave a hundred steps behind it: the
31
+ * gesture says what the template was before it started, and everything until the
32
+ * pointer comes up replaces that one step.
33
+ */
34
+ const update = (next, options) => {
35
+ if (options?.before)
36
+ history.commit(options.before);
37
+ if (options?.transient || options?.before)
38
+ history.replace(next);
39
+ else
40
+ history.set(next);
41
+ };
42
+ const changeAvatarSize = (size) => {
43
+ const result = withAvatarSize(template, size);
44
+ history.set(result.template);
45
+ // Rather than silently shrinking somebody's 3x2 huddle, say which areas no
46
+ // longer fit and let the author decide what to do about it.
47
+ setUnfitted(result.unfitted);
48
+ };
49
+ return (_jsxs("div", { className: "grid h-full gap-4 p-4 lg:grid-cols-[1fr_22rem]", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(BuilderCanvas, { template: template, imageUrl: showDark ? (props.darkImageUrl ?? props.imageUrl) : props.imageUrl, tool: tool, selection: selection, onSelect: setSelection, onChange: update, onToolDone: () => setTool('select'), showGhosts: showGhosts }), _jsx("p", { className: "mt-2 text-xs text-base-content/70", children: tool === 'room'
50
+ ? 'Drag on the canvas to draw a room.'
51
+ : tool === 'area'
52
+ ? 'Drag inside a room to draw a user area. It snaps to whole avatar cells.'
53
+ : 'Click to select. Drag a room to move it, or its bottom-right corner to resize.' })] }), _jsxs("aside", { className: "flex min-w-0 flex-col gap-4 overflow-y-auto", "aria-label": "Builder controls", children: [_jsx(ToolBar, { template: template, tool: tool, onTool: setTool, onUndo: history.undo, onRedo: history.redo, canUndo: history.canUndo, canRedo: history.canRedo, onAvatarSize: changeAvatarSize, unfitted: unfitted, onSelect: setSelection, showGhosts: showGhosts, onShowGhosts: setShowGhosts, ...(props.darkImageUrl ? { showDark, onShowDark: setShowDark } : {}), ...(props.onPickImage ? { onPickImage: props.onPickImage } : {}) }), _jsxs("section", { className: "border-t border-base-300 pt-3", children: [room && selection.kind === 'room' && (_jsx(RoomInspector, { template: template, room: room, onChange: history.set, onSelect: setSelection })), room && area && selection.kind === 'area' && (_jsx(AreaInspector, { template: template, room: room, area: area, onChange: history.set, onSelect: setSelection })), selection.kind === 'none' && (_jsx(TemplateDetails, { template: template, onChange: history.set, onSelect: setSelection }))] }), _jsx(SaveBar, { issues: issues, saveLabel: props.saveLabel, onSave: () => props.onSave(template) })] })] }));
54
+ }
55
+ //# sourceMappingURL=OfficeBuilder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfficeBuilder.js","sourceRoot":"","sources":["../src/OfficeBuilder.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { CanvasShape } from '@unityevolv/ofiskit-template';
2
+ import type { PromptDocument } from '../prompt/prompt.js';
3
+ /**
4
+ * Step two: the picture the office is drawn on.
5
+ *
6
+ * The light image is required and the dark one is optional. When the light image
7
+ * is an SVG, the dark one can be generated from it here instead of being made
8
+ * separately, which is also the one way to be sure the two pictures show the same
9
+ * office.
10
+ *
11
+ * Each image is checked on the way in, against the shape chosen in step one: a
12
+ * picture of the wrong ratio puts every room slightly wrong against it, and
13
+ * finding that out here costs one regeneration rather than a whole layout.
14
+ */
15
+ export interface BackgroundImages {
16
+ /** Object URLs of what is loaded so far. */
17
+ light: string | null;
18
+ dark: string | null;
19
+ }
20
+ export interface BackgroundStepProps {
21
+ document: PromptDocument;
22
+ shape: CanvasShape;
23
+ /** What is loaded now, to preview. */
24
+ images: BackgroundImages;
25
+ /**
26
+ * An image came in, with the name it should have in the host's config folder.
27
+ * The name keeps the type it was uploaded as: `office-light.svg` for an SVG,
28
+ * not a `.webp` name for a file that is not one.
29
+ */
30
+ onImage(slot: 'light' | 'dark', url: string, fileName: string): void;
31
+ }
32
+ /** The name a background should have beside template.json, keeping its type. */
33
+ export declare function backgroundFileName(file: File, slot: 'light' | 'dark'): string;
34
+ export declare function BackgroundStep(props: BackgroundStepProps): import("react").JSX.Element;
35
+ //# sourceMappingURL=BackgroundStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundStep.d.ts","sourceRoot":"","sources":["../../src/background/BackgroundStep.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAG/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAGzD;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAA;IACxB,KAAK,EAAE,WAAW,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,CAAA;IACxB;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACrE;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAG7E;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BA8LxD"}
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Alert, Button, Field } from '@unityevolv/unitykit';
3
+ import { useEffect, useState } from 'react';
4
+ import { darkVersion } from './darkVersion.js';
5
+ /** The name a background should have beside template.json, keeping its type. */
6
+ export function backgroundFileName(file, slot) {
7
+ const extension = isSvg(file) ? 'svg' : (/\.([a-z0-9]+)$/i.exec(file.name)?.[1] ?? 'png');
8
+ return `office-${slot}.${extension.toLowerCase()}`;
9
+ }
10
+ function isSvg(file) {
11
+ return file.type === 'image/svg+xml' || /\.svg$/i.test(file.name);
12
+ }
13
+ export function BackgroundStep(props) {
14
+ const { document: prompt, shape } = props;
15
+ const canvas = prompt.canvases[shape];
16
+ const [problem, setProblem] = useState(null);
17
+ /** The light image, when it is an SVG: the only kind a dark version can be made from. */
18
+ const [lightSvg, setLightSvg] = useState(null);
19
+ /** A dark version made here, which the author needs as a file; they never had one. */
20
+ const [generated, setGenerated] = useState(null);
21
+ const [generating, setGenerating] = useState(false);
22
+ // The download link's URL is this step's own; the one handed to the host is not.
23
+ useEffect(() => () => {
24
+ if (generated)
25
+ URL.revokeObjectURL(generated.url);
26
+ }, [generated]);
27
+ /** Check an image against the canvas, and hand it to the host if it fits. */
28
+ async function accept(file, slot) {
29
+ const url = URL.createObjectURL(file);
30
+ const image = new Image();
31
+ const ok = await new Promise((resolve) => {
32
+ image.onload = () => resolve(true);
33
+ image.onerror = () => resolve(false);
34
+ image.src = url;
35
+ });
36
+ if (!ok) {
37
+ setProblem('That file could not be read as an image.');
38
+ URL.revokeObjectURL(url);
39
+ return false;
40
+ }
41
+ const wanted = canvas.ratio.split(':').map(Number);
42
+ const target = (wanted[0] ?? 1) / (wanted[1] ?? 1);
43
+ const actual = image.width / image.height;
44
+ if (Math.abs(actual - target) > 0.03) {
45
+ setProblem(`That image is ${image.width}x${image.height}, which is not ${canvas.ratio}. ` +
46
+ `Rooms would sit slightly wrong against it. Generate it at ${canvas.resolution}.`);
47
+ URL.revokeObjectURL(url);
48
+ return false;
49
+ }
50
+ setProblem(image.width < 1200
51
+ ? `That image is only ${image.width} pixels wide. It will look soft on a large screen.`
52
+ : null);
53
+ if (slot === 'light') {
54
+ // A new light picture makes any dark version made from the old one stale.
55
+ setLightSvg(isSvg(file) ? file : null);
56
+ }
57
+ // Replaced by whatever came in now; generating sets it again straight after.
58
+ setGenerated(null);
59
+ props.onImage(slot, url, backgroundFileName(file, slot));
60
+ return true;
61
+ }
62
+ /**
63
+ * Make the dark version from the light SVG, and put it in the dark slot.
64
+ *
65
+ * Through the same checks as an uploaded file, so a generated picture is held to
66
+ * the same shape as one somebody made by hand.
67
+ */
68
+ async function generateDark() {
69
+ if (!lightSvg)
70
+ return;
71
+ setGenerating(true);
72
+ try {
73
+ const svg = darkVersion(await lightSvg.text());
74
+ const name = backgroundFileName(lightSvg, 'dark');
75
+ const file = new File([svg], name, { type: 'image/svg+xml' });
76
+ if (await accept(file, 'dark')) {
77
+ setGenerated({ url: URL.createObjectURL(file), name });
78
+ }
79
+ }
80
+ catch (cause) {
81
+ setProblem(`The dark version could not be made: ${cause instanceof Error ? cause.message : 'unknown'}.`);
82
+ }
83
+ finally {
84
+ setGenerating(false);
85
+ }
86
+ }
87
+ return (_jsxs("div", { className: "mx-auto grid max-w-5xl gap-6 p-4 lg:grid-cols-2", children: [_jsxs("section", { className: "space-y-3", "aria-labelledby": "background-light", children: [_jsx("h2", { id: "background-light", className: "text-lg font-semibold", children: "Background image" }), _jsxs("p", { className: "text-sm text-base-content/75", children: ["Required. The office is drawn on this. It should be ", canvas.ratio, ", ideally", ' ', canvas.resolution, "."] }), _jsx(Field, { label: "Light image", help: "PNG, JPEG, WebP, AVIF or SVG.", children: (control) => (_jsx("input", { ...control, type: "file", accept: "image/*", className: "file-input file-input-bordered file-input-sm w-full", onChange: (event) => {
88
+ const file = event.target.files?.[0];
89
+ if (file)
90
+ void accept(file, 'light');
91
+ } })) }), _jsx(Preview, { url: props.images.light, label: "The light image" })] }), _jsxs("section", { className: "space-y-3", "aria-labelledby": "background-dark", children: [_jsx("h2", { id: "background-dark", className: "text-lg font-semibold", children: "Dark version" }), _jsx("p", { className: "text-sm text-base-content/75", children: "Optional, for the dark theme. The same scene recoloured, never a different layout." }), _jsx(Field, { label: "Dark image", help: "Upload one, or generate it from an SVG.", children: (control) => (_jsx("input", { ...control, type: "file", accept: "image/*", className: "file-input file-input-bordered file-input-sm w-full", onChange: (event) => {
92
+ const file = event.target.files?.[0];
93
+ if (file)
94
+ void accept(file, 'dark');
95
+ } })) }), lightSvg && (_jsxs("div", { className: "space-y-1", children: [_jsx(Button, { size: "sm", variant: "secondary", disabled: generating, onClick: () => void generateDark(), children: "Generate dark version" }), _jsx("p", { className: "text-xs text-base-content/70", children: "Made from your SVG: walls and floors darken, lamps stay lit. Nothing is uploaded." })] })), _jsx("p", { role: "status", className: "text-xs", children: generated && (_jsxs(_Fragment, { children: ["Dark version ready.", ' ', _jsxs("a", { href: generated.url, download: generated.name, className: "link", children: ["Download ", generated.name] }), ' ', "and put it beside template.json."] })) }), _jsx(Preview, { url: props.images.dark, label: "The dark image" })] }), problem && (_jsx(Alert, { variant: "warn", className: "lg:col-span-2", children: problem }))] }));
96
+ }
97
+ /** What is loaded, so the author can see it is the right picture before drawing on it. */
98
+ function Preview({ url, label }) {
99
+ if (!url) {
100
+ return (_jsx("div", { className: "flex aspect-video items-center justify-center rounded-lg border border-dashed border-base-300 text-xs text-base-content/60", children: "Nothing yet" }));
101
+ }
102
+ return (_jsx("img", { src: url, alt: label, className: "aspect-video w-full rounded-lg border border-base-300 object-contain" }));
103
+ }
104
+ //# sourceMappingURL=BackgroundStep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BackgroundStep.js","sourceRoot":"","sources":["../../src/background/BackgroundStep.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The dark version of an SVG background: the same office, after hours.
3
+ *
4
+ * Made from the light picture rather than drawn separately, because two drawings
5
+ * of one office drift apart, and the symptom is a desk that exists in one theme
6
+ * and not the other. Only an SVG can be recoloured this way; its colours are
7
+ * text, so rewriting them rewrites the picture and nothing else. A raster image
8
+ * would need a filter, and a filter darkens the lamps along with the walls.
9
+ *
10
+ * The rules are general, not tuned to any one drawing:
11
+ *
12
+ * - Pale colours (walls, floors, paper, sky) become dark, muted surfaces. They are
13
+ * most of the picture, so they are what makes it read as dark.
14
+ * - Mid and deep colours (furniture, books, plants) are only dimmed, so the rooms
15
+ * keep their character and stay a step above the walls.
16
+ * - Bright, warm, saturated colours are lights (lamps, bulbs, signs) and keep
17
+ * their colour, since that is what a lit office looks like at night.
18
+ * - A white sheen laid over something stays white but quieter, and a glow fades
19
+ * to under half. Shadows get stronger, because a shadow at the old strength
20
+ * vanishes on a dark wall.
21
+ *
22
+ * Pure string work with no DOM, so it can be tested directly.
23
+ */
24
+ export declare function darkVersion(svg: string): string;
25
+ //# sourceMappingURL=darkVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"darkVersion.d.ts","sourceRoot":"","sources":["../../src/background/darkVersion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The dark version of an SVG background: the same office, after hours.
3
+ *
4
+ * Made from the light picture rather than drawn separately, because two drawings
5
+ * of one office drift apart, and the symptom is a desk that exists in one theme
6
+ * and not the other. Only an SVG can be recoloured this way; its colours are
7
+ * text, so rewriting them rewrites the picture and nothing else. A raster image
8
+ * would need a filter, and a filter darkens the lamps along with the walls.
9
+ *
10
+ * The rules are general, not tuned to any one drawing:
11
+ *
12
+ * - Pale colours (walls, floors, paper, sky) become dark, muted surfaces. They are
13
+ * most of the picture, so they are what makes it read as dark.
14
+ * - Mid and deep colours (furniture, books, plants) are only dimmed, so the rooms
15
+ * keep their character and stay a step above the walls.
16
+ * - Bright, warm, saturated colours are lights (lamps, bulbs, signs) and keep
17
+ * their colour, since that is what a lit office looks like at night.
18
+ * - A white sheen laid over something stays white but quieter, and a glow fades
19
+ * to under half. Shadows get stronger, because a shadow at the old strength
20
+ * vanishes on a dark wall.
21
+ *
22
+ * Pure string work with no DOM, so it can be tested directly.
23
+ */
24
+ /**
25
+ * A hex colour, but not an id reference: `href="#bed"` and `url(#bed)` name an
26
+ * element, and recolouring one would break the drawing.
27
+ */
28
+ const COLOUR = /(?<!href=")(?<!url\()#([0-9a-f]{6}|[0-9a-f]{3})\b/gi;
29
+ export function darkVersion(svg) {
30
+ return adjustOverlays(svg).replace(COLOUR, (match) => night(expand(match.toLowerCase())));
31
+ }
32
+ /**
33
+ * Highlights and shadows, which are about opacity more than colour.
34
+ *
35
+ * Handled before the colour pass, which would otherwise darken a white sheen into
36
+ * a grey smear. The colours are written as markers the colour pass turns back
37
+ * into pure white and black; pure white or black anywhere else (paper, ink) is
38
+ * ordinary colour and goes through the curve.
39
+ */
40
+ function adjustOverlays(svg) {
41
+ return (svg
42
+ // A white sheen: stays white, at about a third of its strength.
43
+ .replace(/fill="#fff(?:fff)?"(\s+)opacity="([\d.]+)"/gi, (_, space, opacity) => `fill="${KEEP_WHITE}"${space}opacity="${round(Number(opacity) * 0.35)}"`)
44
+ // A black overlay: a shadow, which needs to be stronger to show on a dark wall.
45
+ .replace(/fill="#000(?:000)?"(\s+)opacity="([\d.]+)"/gi, (_, space, opacity) => `fill="${KEEP_BLACK}"${space}opacity="${round(Math.min(0.4, Number(opacity) * 3))}"`)
46
+ // A glow is a gradient fading to nothing. At full strength on a dark wall it
47
+ // reads as fog rather than light, so it is kept, at under half.
48
+ .replace(/stop-opacity="([\d.]+)"/gi, (_, opacity) => `stop-opacity="${round(Number(opacity) * 0.45)}"`)
49
+ // Drop shadows: black, and stronger.
50
+ .replace(/flood-color="[^"]*"/gi, `flood-color="${KEEP_BLACK}"`)
51
+ .replace(/flood-opacity="([\d.]+)"/gi, (_, opacity) => `flood-opacity="${round(Math.min(0.6, Number(opacity) * 2))}"`));
52
+ }
53
+ /** Markers for overlays: one step off pure white and black, which no drawing will miss. */
54
+ const KEEP_WHITE = '#fffffe';
55
+ const KEEP_BLACK = '#000001';
56
+ /**
57
+ * One colour at night.
58
+ *
59
+ * Lightness at or below 0.6 is scaled to 80%. Above it, lightness falls from
60
+ * 0.48 to 0.17 for pure white, which puts walls and paper at a dark theme's
61
+ * surface level while furniture stays a step above them. Pale colours also lose
62
+ * half their saturation: a wall tinted enough to tell rooms apart by day is loud
63
+ * at night.
64
+ */
65
+ function night(hex) {
66
+ if (hex === KEEP_WHITE)
67
+ return '#ffffff';
68
+ if (hex === KEEP_BLACK)
69
+ return '#000000';
70
+ const [h, s, l] = toHsl(hex);
71
+ if (isLight(h, s, l))
72
+ return hex;
73
+ const dark = l <= 0.6 ? l * 0.8 : 0.48 - ((l - 0.6) / 0.4) * 0.31;
74
+ return toHex(h, s * (l <= 0.6 ? 0.85 : 0.5), dark);
75
+ }
76
+ /**
77
+ * A light source: a warm (orange to yellow), strongly saturated, bright colour.
78
+ *
79
+ * Narrow on purpose. Keeping a colour that should have darkened leaves a bright
80
+ * patch on a dark wall, which is worse than a lamp that dims a little.
81
+ */
82
+ function isLight(h, s, l) {
83
+ const degrees = h * 360;
84
+ return degrees >= 38 && degrees <= 60 && s >= 0.75 && l >= 0.55 && l <= 0.85;
85
+ }
86
+ function expand(hex) {
87
+ if (hex.length !== 4)
88
+ return hex;
89
+ const [, r = '0', g = '0', b = '0'] = hex;
90
+ return `#${r}${r}${g}${g}${b}${b}`;
91
+ }
92
+ function toHsl(hex) {
93
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
94
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
95
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
96
+ const max = Math.max(r, g, b);
97
+ const min = Math.min(r, g, b);
98
+ const l = (max + min) / 2;
99
+ if (max === min)
100
+ return [0, 0, l];
101
+ const d = max - min;
102
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
103
+ const h = max === r
104
+ ? ((g - b) / d + (g < b ? 6 : 0)) / 6
105
+ : max === g
106
+ ? ((b - r) / d + 2) / 6
107
+ : ((r - g) / d + 4) / 6;
108
+ return [h, s, l];
109
+ }
110
+ function toHex(h, s, l) {
111
+ const channel = (n) => {
112
+ const k = (n + h * 12) % 12;
113
+ const a = s * Math.min(l, 1 - l);
114
+ const value = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
115
+ return Math.round(value * 255)
116
+ .toString(16)
117
+ .padStart(2, '0');
118
+ };
119
+ return `#${channel(0)}${channel(8)}${channel(4)}`;
120
+ }
121
+ function round(value) {
122
+ return String(Math.round(value * 1000) / 1000);
123
+ }
124
+ //# sourceMappingURL=darkVersion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"darkVersion.js","sourceRoot":"","sources":["../../src/background/darkVersion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;GAGG;AACH,MAAM,MAAM,GAAG,qDAAqD,CAAA;AAEpE,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,GAAW;IACjC,OAAO,CACL,GAAG;QACD,gEAAgE;SAC/D,OAAO,CACN,8CAA8C,EAC9C,CAAC,CAAC,EAAE,KAAa,EAAE,OAAe,EAAE,EAAE,CACpC,SAAS,UAAU,IAAI,KAAK,YAAY,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,CAC3E;QACD,gFAAgF;SAC/E,OAAO,CACN,8CAA8C,EAC9C,CAAC,CAAC,EAAE,KAAa,EAAE,OAAe,EAAE,EAAE,CACpC,SAAS,UAAU,IAAI,KAAK,YAAY,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CACvF;QACD,6EAA6E;QAC7E,gEAAgE;SAC/D,OAAO,CACN,2BAA2B,EAC3B,CAAC,CAAC,EAAE,OAAe,EAAE,EAAE,CAAC,iBAAiB,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,CAC1E;QACD,qCAAqC;SACpC,OAAO,CAAC,uBAAuB,EAAE,gBAAgB,UAAU,GAAG,CAAC;SAC/D,OAAO,CACN,4BAA4B,EAC5B,CAAC,CAAC,EAAE,OAAe,EAAE,EAAE,CAAC,kBAAkB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CACvF,CACJ,CAAA;AACH,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,GAAG,SAAS,CAAA;AAC5B,MAAM,UAAU,GAAG,SAAS,CAAA;AAE5B;;;;;;;;GAQG;AACH,SAAS,KAAK,CAAC,GAAW;IACxB,IAAI,GAAG,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IACxC,IAAI,GAAG,KAAK,UAAU;QAAE,OAAO,SAAS,CAAA;IAExC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAAE,OAAO,GAAG,CAAA;IAEhC,MAAM,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAA;IACjE,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAA;AACpD,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS;IAC9C,MAAM,OAAO,GAAG,CAAC,GAAG,GAAG,CAAA;IACvB,OAAO,OAAO,IAAI,EAAE,IAAI,OAAO,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,CAAA;AAC9E,CAAC;AAED,SAAS,MAAM,CAAC,GAAW;IACzB,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAA;IAChC,MAAM,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;IACzC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAA;AACpC,CAAC;AAED,SAAS,KAAK,CAAC,GAAW;IACxB,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAA;IAC7C,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAA;IAC7C,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAA;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC7B,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACzB,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAEjC,MAAM,CAAC,GAAG,GAAG,GAAG,GAAG,CAAA;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAA;IACzD,MAAM,CAAC,GACL,GAAG,KAAK,CAAC;QACP,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACrC,CAAC,CAAC,GAAG,KAAK,CAAC;YACT,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;YACvB,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;AAClB,CAAC;AAED,SAAS,KAAK,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS;IAC5C,MAAM,OAAO,GAAG,CAAC,CAAS,EAAE,EAAE;QAC5B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAA;QAC3B,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;QAChC,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAC7D,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC;aAC3B,QAAQ,CAAC,EAAE,CAAC;aACZ,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IACrB,CAAC,CAAA;IACD,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;AACnD,CAAC;AAED,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAA;AAChD,CAAC"}