@hyperframes/studio 0.8.101 → 0.8.103

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 (57) hide show
  1. package/dist/assets/{hyperframes-player-CFJLrHRF.js → hyperframes-player-DG795Hwu.js} +1 -1
  2. package/dist/assets/{index-BBNVnnyH.js → index-BPJRf2X9.js} +119 -119
  3. package/dist/assets/{index-CJOzbslX.js → index-BiEtDkae.js} +1 -1
  4. package/dist/assets/{index-xB5O0hPw.css → index-CQN3M56N.css} +1 -1
  5. package/dist/assets/{index-C33EDBL2.js → index-DuCZ3pQw.js} +1 -1
  6. package/dist/{chunk-MTAVNAGW.js → chunk-46XP7J3B.js} +2 -2
  7. package/dist/{chunk-S65AQH6M.js → chunk-DI4LCJHL.js} +3 -2
  8. package/dist/chunk-DI4LCJHL.js.map +1 -0
  9. package/dist/{chunk-7X4XRP3L.js → chunk-HIGHVWYD.js} +2 -2
  10. package/dist/{domEditingLayers-NU6Z5QWA.js → domEditingLayers-AQBDFNUC.js} +3 -3
  11. package/dist/index.d.ts +13 -3
  12. package/dist/index.html +2 -2
  13. package/dist/index.js +932 -850
  14. package/dist/index.js.map +1 -1
  15. package/dist/player.js +2 -2
  16. package/dist/styles.css +1 -1
  17. package/package.json +8 -8
  18. package/src/components/editor/CanvasContextMenu.test.tsx +23 -2
  19. package/src/components/editor/CanvasContextMenu.tsx +7 -10
  20. package/src/components/editor/ConnectedDomEditOverlay.tsx +3 -128
  21. package/src/components/editor/DomEditOverlay.tsx +2 -1
  22. package/src/components/editor/PropertyPanel.tsx +2 -2
  23. package/src/components/editor/canvasContextMenuZOrder.ts +13 -0
  24. package/src/components/editor/domEditOverlayGestures.ts +1 -1
  25. package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
  26. package/src/components/editor/gsapAnimatesProperty.ts +0 -29
  27. package/src/components/editor/gsapLivePreview.test.ts +46 -1
  28. package/src/components/editor/manualOffsetDrag.test.ts +16 -0
  29. package/src/components/editor/manualOffsetDrag.ts +5 -5
  30. package/src/components/editor/plainTranslate.test.ts +38 -22
  31. package/src/components/editor/plainTranslate.ts +66 -36
  32. package/src/components/editor/previewReadOnly.overlay.test.tsx +30 -0
  33. package/src/components/editor/previewReadOnlyContext.tsx +3 -1
  34. package/src/components/editor/propertyPanelHelpers.ts +3 -0
  35. package/src/components/editor/propertyPanelTransformCommit.test.ts +51 -0
  36. package/src/components/editor/propertyPanelTransformCommit.ts +3 -1
  37. package/src/components/editor/useColorGradingController.test.ts +36 -1
  38. package/src/components/editor/useColorGradingController.ts +2 -2
  39. package/src/components/editor/useDomEditNudge.test.tsx +1 -0
  40. package/src/components/editor/useDomEditNudge.ts +6 -3
  41. package/src/components/editor/useDomEditZOrder.ts +185 -0
  42. package/src/components/nle/PreviewOverlaysTestMocks.ts +4 -0
  43. package/src/domEditExports.test.tsx +92 -0
  44. package/src/hooks/gsapRuntimeKeyframes.ts +6 -0
  45. package/src/hooks/useDomEditCommits.test.tsx +94 -90
  46. package/src/hooks/usePreviewInteraction.test.ts +32 -0
  47. package/src/hooks/usePreviewInteraction.ts +3 -2
  48. package/src/index.ts +3 -0
  49. package/src/player/components/TimelineClipFades.test.tsx +103 -3
  50. package/src/player/components/TimelineClipFades.tsx +53 -50
  51. package/src/styles/theme.css +1 -1
  52. package/src/utils/projectRouting.test.ts +9 -0
  53. package/src/utils/projectRouting.ts +3 -1
  54. package/dist/chunk-S65AQH6M.js.map +0 -1
  55. /package/dist/{chunk-MTAVNAGW.js.map → chunk-46XP7J3B.js.map} +0 -0
  56. /package/dist/{chunk-7X4XRP3L.js.map → chunk-HIGHVWYD.js.map} +0 -0
  57. /package/dist/{domEditingLayers-NU6Z5QWA.js.map → domEditingLayers-AQBDFNUC.js.map} +0 -0
@@ -1,11 +1,4 @@
1
- import {
2
- useEffect,
3
- useRef,
4
- useState,
5
- type CSSProperties,
6
- type KeyboardEvent,
7
- type PointerEvent,
8
- } from "react";
1
+ import { useEffect, useRef, useState, type CSSProperties, type PointerEvent } from "react";
9
2
  import {
10
3
  HF_AUDIO_FADE_IN_ATTR,
11
4
  HF_AUDIO_FADE_OUT_ATTR,
@@ -13,13 +6,16 @@ import {
13
6
  formatFadeSeconds,
14
7
  } from "@hyperframes/core/audio-fade";
15
8
  import type { TimelineElement } from "../store/playerStore";
9
+ import { Tooltip } from "../../components/ui";
16
10
  import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
17
11
  import { releasedOutsideWindow } from "./timelinePointerRelease";
18
12
 
19
13
  type FadeEdge = "in" | "out";
20
14
 
21
15
  const HANDLE_SIZE = 10;
22
- const HANDLE_HIT = 16;
16
+ const HANDLE_HIT = 24;
17
+ const HANDLE_Z_ABOVE_CLIP_CONTENT = 30;
18
+ const SUPPRESS_CLIP_NATIVE_TITLE = "";
23
19
  /** Pixels of pointer travel before a press on the handle counts as a drag. */
24
20
  const DRAG_THRESHOLD_PX = 2;
25
21
 
@@ -147,31 +143,38 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
147
143
  );
148
144
  };
149
145
 
150
- const onHandleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
151
- const g = gesture.current;
152
- if (e.key !== "Escape" || !g) return;
153
- e.preventDefault();
154
- gesture.current = null;
155
- setDragging(null);
156
- revertGesture(g);
157
- };
146
+ useEffect(() => {
147
+ if (dragging === null) return;
148
+ const cancelOnWindowEscape = (e: KeyboardEvent) => {
149
+ const g = gesture.current;
150
+ if (e.key !== "Escape" || !g) return;
151
+ e.preventDefault();
152
+ e.stopPropagation();
153
+ gesture.current = null;
154
+ setDragging(null);
155
+ revertGesture(g);
156
+ };
157
+ window.addEventListener("keydown", cancelOnWindowEscape, { capture: true });
158
+ return () => window.removeEventListener("keydown", cancelOnWindowEscape, { capture: true });
159
+ });
158
160
 
159
161
  const showIn = fades.fadeIn > 0;
160
162
  const showOut = fades.fadeOut > 0;
161
163
  const handlesVisible = showHandles || dragging !== null;
162
164
  if (!showIn && !showOut && !handlesVisible) return null;
163
165
 
164
- const handleStyle = (leftPx: number): CSSProperties => ({
166
+ const hitWidth = Math.min(HANDLE_HIT, widthPx / 2);
167
+ const handleStyle = (edge: FadeEdge): CSSProperties => ({
165
168
  position: "absolute",
166
169
  top: -(HANDLE_HIT - HANDLE_SIZE) / 2 + 1,
167
- left: leftPx - HANDLE_HIT / 2,
168
- width: HANDLE_HIT,
170
+ left: edge === "in" ? 0 : widthPx - hitWidth,
171
+ width: hitWidth,
169
172
  height: HANDLE_HIT,
170
173
  display: "flex",
171
174
  alignItems: "center",
172
175
  justifyContent: "center",
173
176
  cursor: "ew-resize",
174
- zIndex: 6,
177
+ zIndex: HANDLE_Z_ABOVE_CLIP_CONTENT,
175
178
  opacity: handlesVisible ? 1 : 0,
176
179
  pointerEvents: handlesVisible && canEdit ? "auto" : "none",
177
180
  touchAction: "none",
@@ -191,7 +194,8 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
191
194
  height: "100%",
192
195
  pointerEvents: "none",
193
196
  zIndex: 5,
194
- overflow: "visible",
197
+ overflow: "hidden",
198
+ borderRadius: "inherit",
195
199
  }}
196
200
  viewBox={`0 0 ${Math.max(widthPx, 1)} 100`}
197
201
  preserveAspectRatio="none"
@@ -245,25 +249,23 @@ export function TimelineClipFades({ el, pps, widthPx, showHandles }: TimelineCli
245
249
  direction="in"
246
250
  value={fades.fadeIn}
247
251
  max={Math.max(0, el.duration - fades.fadeOut)}
248
- style={handleStyle(inPx)}
252
+ style={handleStyle("in")}
249
253
  dragging={dragging === "in"}
250
254
  onPointerDown={onHandlePointerDown("in")}
251
255
  onPointerMove={onHandlePointerMove}
252
256
  onPointerUp={(e) => finish(e, false)}
253
257
  onPointerCancel={(e) => finish(e, true)}
254
- onKeyDown={onHandleKeyDown}
255
258
  />
256
259
  <FadeHandle
257
260
  direction="out"
258
261
  value={fades.fadeOut}
259
262
  max={Math.max(0, el.duration - fades.fadeIn)}
260
- style={handleStyle(widthPx - outPx)}
263
+ style={handleStyle("out")}
261
264
  dragging={dragging === "out"}
262
265
  onPointerDown={onHandlePointerDown("out")}
263
266
  onPointerMove={onHandlePointerMove}
264
267
  onPointerUp={(e) => finish(e, false)}
265
268
  onPointerCancel={(e) => finish(e, true)}
266
- onKeyDown={onHandleKeyDown}
267
269
  />
268
270
  </>
269
271
  )}
@@ -281,7 +283,6 @@ function FadeHandle({
281
283
  onPointerMove,
282
284
  onPointerUp,
283
285
  onPointerCancel,
284
- onKeyDown,
285
286
  }: {
286
287
  direction: "in" | "out";
287
288
  value: number;
@@ -292,31 +293,31 @@ function FadeHandle({
292
293
  onPointerMove: (event: PointerEvent<HTMLDivElement>) => void;
293
294
  onPointerUp: (event: PointerEvent<HTMLDivElement>) => void;
294
295
  onPointerCancel: (event: PointerEvent<HTMLDivElement>) => void;
295
- onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
296
296
  }) {
297
297
  const label = direction === "in" ? "Fade in" : "Fade out";
298
298
  return (
299
- <div
300
- role="slider"
301
- tabIndex={-1}
302
- aria-label={label}
303
- aria-valuemin={0}
304
- aria-valuemax={max}
305
- aria-valuenow={value}
306
- aria-valuetext={`${formatFadeSeconds(value)}s`}
307
- data-testid={`clip-fade-handle-${direction}`}
308
- title={`${label}: ${formatFadeSeconds(value)}s — drag to change`}
309
- style={style}
310
- onPointerDown={onPointerDown}
311
- onPointerMove={onPointerMove}
312
- onPointerUp={onPointerUp}
313
- onPointerCancel={onPointerCancel}
314
- onKeyDown={onKeyDown}
315
- onClick={(e) => e.stopPropagation()}
316
- onDoubleClick={(e) => e.stopPropagation()}
317
- >
318
- <FadeDot active={dragging} />
319
- </div>
299
+ <Tooltip label={`${label} ${formatFadeSeconds(value)} s`}>
300
+ <div
301
+ role="slider"
302
+ tabIndex={-1}
303
+ aria-label={label}
304
+ aria-valuemin={0}
305
+ aria-valuemax={max}
306
+ aria-valuenow={value}
307
+ aria-valuetext={`${formatFadeSeconds(value)}s`}
308
+ data-testid={`clip-fade-handle-${direction}`}
309
+ title={SUPPRESS_CLIP_NATIVE_TITLE}
310
+ style={style}
311
+ onPointerDown={onPointerDown}
312
+ onPointerMove={onPointerMove}
313
+ onPointerUp={onPointerUp}
314
+ onPointerCancel={onPointerCancel}
315
+ onClick={(e) => e.stopPropagation()}
316
+ onDoubleClick={(e) => e.stopPropagation()}
317
+ >
318
+ <FadeDot active={dragging} />
319
+ </div>
320
+ </Tooltip>
320
321
  );
321
322
  }
322
323
 
@@ -326,10 +327,12 @@ function FadeDot({ active }: { active: boolean }) {
326
327
  aria-hidden="true"
327
328
  style={{
328
329
  display: "block",
330
+ flexShrink: 0,
331
+ pointerEvents: "none",
329
332
  width: HANDLE_SIZE,
330
333
  height: HANDLE_SIZE,
331
334
  borderRadius: "50%",
332
- background: active ? "var(--timeline-fade-dot-active)" : "var(--clip-handle)",
335
+ background: "var(--timeline-fade-dot)",
333
336
  boxShadow: "0 0 0 1.5px var(--timeline-fade-dot-ring)",
334
337
  transform: active ? "scale(1.15)" : undefined,
335
338
  transition: "transform 80ms ease-out",
@@ -114,7 +114,7 @@
114
114
  --timeline-handle: rgba(255, 255, 255, 0.55);
115
115
  /* Audio clip fades: the shaded wedge over the faded gain, and the corner dots. */
116
116
  --timeline-fade-shade: rgba(0, 0, 0, 0.38);
117
- --timeline-fade-dot-active: rgba(255, 255, 255, 1);
117
+ --timeline-fade-dot: #fff;
118
118
  --timeline-fade-dot-ring: rgba(0, 0, 0, 0.55);
119
119
  --timeline-resize-seam: rgba(255, 255, 255, 0.12);
120
120
  --timeline-resize-active: rgba(255, 255, 255, 0.24);
@@ -18,6 +18,15 @@ describe("project routing utilities", () => {
18
18
  },
19
19
  );
20
20
 
21
+ it("opens a folder whose name has a colon, encoded once both ways", () => {
22
+ const id = "Customer story: Northwind";
23
+ expect(encodeProjectId(id)).toBe("Customer%20story%3A%20Northwind");
24
+ expect(buildProjectApiPath(id, "/preview")).toBe(
25
+ "/api/projects/Customer%20story%3A%20Northwind/preview",
26
+ );
27
+ expect(parseProjectIdFromHash(buildProjectHash(id))).toBe(id);
28
+ });
29
+
21
30
  it("decodes project ids from hash routes before building capture URLs", () => {
22
31
  vi.useFakeTimers();
23
32
  vi.setSystemTime(new Date("2026-05-01T12:00:00Z"));
@@ -5,13 +5,15 @@ export interface ProjectHashRoute {
5
5
  params: URLSearchParams;
6
6
  }
7
7
 
8
+ const WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;
9
+
8
10
  /** Project names are single path segments, including when received from a hash or server. */
9
11
  export function isValidProjectId(value: string): boolean {
10
12
  return (
11
13
  value.length > 0 &&
12
14
  value !== "." &&
13
15
  value !== ".." &&
14
- !value.includes(":") &&
16
+ !WINDOWS_DRIVE_PREFIX.test(value) &&
15
17
  !value.includes("/") &&
16
18
  !value.includes("\\") &&
17
19
  !Array.from(value).some((char) => char.charCodeAt(0) < 32)
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/sourceScopedSelectorIndex.ts","../src/utils/projectRouting.ts"],"sourcesContent":["/**\n * Occurrence index for a selector within its source document.\n *\n * The preview flattens multiple composition files into one DOM, so a raw\n * `querySelectorAll` index is not a stable source-file identity. Callers supply\n * their existing source resolver; this helper alone owns occurrence scoping.\n */\n\n/** Every element matching one selector, mapped to the source file it belongs to\n * and its occurrence index within that file. One document query fills it. */\ntype SelectorOccurrences = Map<Element, { scope: string; index: number }>;\n\ninterface SelectorIndexPass {\n doc: Document;\n bySelector: Map<string, SelectorOccurrences>;\n}\n\nlet activePass: SelectorIndexPass | null = null;\n\n/**\n * Share one occurrence index per selector across every\n * `getSourceScopedSelectorIndex` call made synchronously inside `run`.\n *\n * Unshared, each call runs its own whole-document `querySelectorAll(selector)`\n * and resolves the source file of every match. A rebuild that walks n elements\n * sharing a selector therefore does n whole-document queries and n² resolver\n * calls — a composition of repeated cards or tiles is exactly that shape. One\n * index per selector makes the same pass linear.\n *\n * Correctness rests on the pass being ONE synchronous walk of a document that\n * does not change under it: nothing invalidates the index, so `run` must not\n * mutate `doc`, and every call inside it must resolve source files the same way\n * (the resolver may be a fresh closure per call, as long as it answers the\n * same). Outside a pass the index is per-call, exactly as before.\n */\nexport function withSelectorIndexPass<T>(doc: Document, run: () => T): T {\n const previous = activePass;\n activePass = { doc, bySelector: new Map() };\n try {\n return run();\n } finally {\n activePass = previous;\n }\n}\n\nfunction buildOccurrences(\n doc: Document,\n selector: string,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): SelectorOccurrences {\n const occurrences: SelectorOccurrences = new Map();\n const counts = new Map<string, number>();\n for (const candidate of doc.querySelectorAll(selector)) {\n const scope = resolveSourceFile(candidate) ?? \"index.html\";\n const index = counts.get(scope) ?? 0;\n counts.set(scope, index + 1);\n occurrences.set(candidate, { scope, index });\n }\n return occurrences;\n}\n\nexport function getSourceScopedSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n sourceFile: string | undefined,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): number | undefined {\n if (!selector || selector.startsWith(\"#\") || selector.startsWith(\"[data-composition-id=\")) {\n return undefined;\n }\n\n try {\n const pass = activePass?.doc === doc ? activePass : null;\n let occurrences = pass?.bySelector.get(selector);\n if (!occurrences) {\n occurrences = buildOccurrences(doc, selector, resolveSourceFile);\n pass?.bySelector.set(selector, occurrences);\n }\n // An element outside the requested scope is not in that scope's occurrence\n // run at all, which is the `indexOf` miss the filtered form returned before.\n const hit = occurrences.get(el);\n return hit && hit.scope === (sourceFile ?? \"index.html\") ? hit.index : undefined;\n } catch {\n return undefined;\n }\n}\n","const PROJECT_HASH_PREFIX = \"#project/\";\n\nexport interface ProjectHashRoute {\n projectId: string;\n params: URLSearchParams;\n}\n\n/** Project names are single path segments, including when received from a hash or server. */\nexport function isValidProjectId(value: string): boolean {\n return (\n value.length > 0 &&\n value !== \".\" &&\n value !== \"..\" &&\n !value.includes(\":\") &&\n !value.includes(\"/\") &&\n !value.includes(\"\\\\\") &&\n !Array.from(value).some((char) => char.charCodeAt(0) < 32)\n );\n}\n\nfunction decodeHashProjectId(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeHashParams(\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): URLSearchParams {\n if (!params) return new URLSearchParams();\n if (params instanceof URLSearchParams) return params;\n\n const next = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (!key || value == null || value === \"\") continue;\n next.set(key, value);\n }\n return next;\n}\n\nexport function encodeProjectId(projectId: string): string {\n if (!isValidProjectId(projectId)) throw new Error(\"Invalid project ID\");\n return encodeURIComponent(projectId);\n}\n\nexport function buildProjectHash(\n projectId: string,\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): string {\n const search = normalizeHashParams(params).toString();\n return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : \"\"}`;\n}\n\nexport function parseProjectHashRoute(hash: string): ProjectHashRoute | null {\n if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;\n\n const route = hash.slice(PROJECT_HASH_PREFIX.length);\n const queryIndex = route.indexOf(\"?\");\n const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;\n if (!encodedProjectId || encodedProjectId.includes(\"/\")) return null;\n\n const projectId = decodeHashProjectId(encodedProjectId);\n if (!isValidProjectId(projectId)) return null;\n\n const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : \"\";\n return {\n projectId,\n params: new URLSearchParams(rawParams),\n };\n}\n\nexport function parseProjectIdFromHash(hash: string): string | null {\n return parseProjectHashRoute(hash)?.projectId ?? null;\n}\n\nexport function buildProjectApiPath(projectId: string, suffix = \"\"): string {\n const normalizedSuffix = suffix && !suffix.startsWith(\"/\") ? `/${suffix}` : suffix;\n return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;\n}\n"],"mappings":";AAiBA,IAAI,aAAuC;AAkBpC,SAAS,sBAAyB,KAAe,KAAiB;AACvE,QAAM,WAAW;AACjB,eAAa,EAAE,KAAK,YAAY,oBAAI,IAAI,EAAE;AAC1C,MAAI;AACF,WAAO,IAAI;AAAA,EACb,UAAE;AACA,iBAAa;AAAA,EACf;AACF;AAEA,SAAS,iBACP,KACA,UACA,mBACqB;AACrB,QAAM,cAAmC,oBAAI,IAAI;AACjD,QAAM,SAAS,oBAAI,IAAoB;AACvC,aAAW,aAAa,IAAI,iBAAiB,QAAQ,GAAG;AACtD,UAAM,QAAQ,kBAAkB,SAAS,KAAK;AAC9C,UAAM,QAAQ,OAAO,IAAI,KAAK,KAAK;AACnC,WAAO,IAAI,OAAO,QAAQ,CAAC;AAC3B,gBAAY,IAAI,WAAW,EAAE,OAAO,MAAM,CAAC;AAAA,EAC7C;AACA,SAAO;AACT;AAEO,SAAS,6BACd,KACA,IACA,UACA,YACA,mBACoB;AACpB,MAAI,CAAC,YAAY,SAAS,WAAW,GAAG,KAAK,SAAS,WAAW,uBAAuB,GAAG;AACzF,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,OAAO,YAAY,QAAQ,MAAM,aAAa;AACpD,QAAI,cAAc,MAAM,WAAW,IAAI,QAAQ;AAC/C,QAAI,CAAC,aAAa;AAChB,oBAAc,iBAAiB,KAAK,UAAU,iBAAiB;AAC/D,YAAM,WAAW,IAAI,UAAU,WAAW;AAAA,IAC5C;AAGA,UAAM,MAAM,YAAY,IAAI,EAAE;AAC9B,WAAO,OAAO,IAAI,WAAW,cAAc,gBAAgB,IAAI,QAAQ;AAAA,EACzE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACtFA,IAAM,sBAAsB;AAQrB,SAAS,iBAAiB,OAAwB;AACvD,SACE,MAAM,SAAS,KACf,UAAU,OACV,UAAU,QACV,CAAC,MAAM,SAAS,GAAG,KACnB,CAAC,MAAM,SAAS,GAAG,KACnB,CAAC,MAAM,SAAS,IAAI,KACpB,CAAC,MAAM,KAAK,KAAK,EAAE,KAAK,CAAC,SAAS,KAAK,WAAW,CAAC,IAAI,EAAE;AAE7D;AAEA,SAAS,oBAAoB,OAAuB;AAClD,MAAI;AACF,WAAO,mBAAmB,KAAK;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,oBACP,QACiB;AACjB,MAAI,CAAC,OAAQ,QAAO,IAAI,gBAAgB;AACxC,MAAI,kBAAkB,gBAAiB,QAAO;AAE9C,QAAM,OAAO,IAAI,gBAAgB;AACjC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,CAAC,OAAO,SAAS,QAAQ,UAAU,GAAI;AAC3C,SAAK,IAAI,KAAK,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAEO,SAAS,gBAAgB,WAA2B;AACzD,MAAI,CAAC,iBAAiB,SAAS,EAAG,OAAM,IAAI,MAAM,oBAAoB;AACtE,SAAO,mBAAmB,SAAS;AACrC;AAEO,SAAS,iBACd,WACA,QACQ;AACR,QAAM,SAAS,oBAAoB,MAAM,EAAE,SAAS;AACpD,SAAO,GAAG,mBAAmB,GAAG,gBAAgB,SAAS,CAAC,GAAG,SAAS,IAAI,MAAM,KAAK,EAAE;AACzF;AAEO,SAAS,sBAAsB,MAAuC;AAC3E,MAAI,CAAC,KAAK,WAAW,mBAAmB,EAAG,QAAO;AAElD,QAAM,QAAQ,KAAK,MAAM,oBAAoB,MAAM;AACnD,QAAM,aAAa,MAAM,QAAQ,GAAG;AACpC,QAAM,mBAAmB,cAAc,IAAI,MAAM,MAAM,GAAG,UAAU,IAAI;AACxE,MAAI,CAAC,oBAAoB,iBAAiB,SAAS,GAAG,EAAG,QAAO;AAEhE,QAAM,YAAY,oBAAoB,gBAAgB;AACtD,MAAI,CAAC,iBAAiB,SAAS,EAAG,QAAO;AAEzC,QAAM,YAAY,cAAc,IAAI,MAAM,MAAM,aAAa,CAAC,IAAI;AAClE,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,IAAI,gBAAgB,SAAS;AAAA,EACvC;AACF;AAEO,SAAS,uBAAuB,MAA6B;AAClE,SAAO,sBAAsB,IAAI,GAAG,aAAa;AACnD;AAEO,SAAS,oBAAoB,WAAmB,SAAS,IAAY;AAC1E,QAAM,mBAAmB,UAAU,CAAC,OAAO,WAAW,GAAG,IAAI,IAAI,MAAM,KAAK;AAC5E,SAAO,iBAAiB,gBAAgB,SAAS,CAAC,GAAG,gBAAgB;AACvE;","names":[]}