@warmio/ui 1.3.0 → 1.4.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@warmio/ui`
2
2
 
3
- Portable React components shared by Warm products. Install `@warmio/ui@1.3.0` with the exact
4
- `@warmio/design@1.5.0` CSS/primitive contract, import `@warmio/design/globals.css` once, and add
3
+ Portable React components shared by Warm products. Install `@warmio/ui@1.4.0` with the exact
4
+ `@warmio/design@1.6.0` CSS/primitive contract, import `@warmio/design/globals.css` once, and add
5
5
  the package's compiled files to Tailwind's source detection:
6
6
 
7
7
  ## Why these files are separate
@@ -19,9 +19,9 @@ deployment permission to publish packages.
19
19
  @source '../node_modules/@warmio/ui/dist';
20
20
  ```
21
21
 
22
- The `1.3.0` public UI surface contains the Warm badge, Text and `StackedDisplay`, DropdownMenu,
22
+ The `1.4.0` public UI surface contains the Warm badge, Text and `StackedDisplay`, DropdownMenu,
23
23
  Sheet, the complete responsive Sidebar family, and the `ProductHeader` composition. `Button`,
24
- `ButtonLink`, `Logo`, and `WarmLogoIcon` are directly owned by `@warmio/design@1.5.0` and are
24
+ `ButtonLink`, `Logo`, and `WarmLogoIcon` are directly owned by `@warmio/design@1.6.0` and are
25
25
  re-exported through UI where applicable. `ProductHeader` owns the responsive frame,
26
26
  breadcrumb/title semantics, leading slot, action slot, and optional portal targets used by Warm
27
27
  product shells. Styling is sealed: use documented semantic properties and layout wrappers, never
@@ -6,16 +6,18 @@ import {
6
6
  import * as React from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  var textClassNameByVariant = {
9
- display: "block text-[1.35rem] font-normal leading-snug tracking-normal [font-family:var(--font-display)] @3xl/main:text-[1.875rem]",
9
+ display: "block text-3xl font-normal leading-snug tracking-normal [font-family:var(--font-display)]",
10
10
  displayCompact: "block text-xl font-normal leading-snug tracking-normal [font-family:var(--font-display)] @3xl/main:text-2xl",
11
11
  displayHero: "block text-3xl font-bold leading-tight tracking-normal text-foreground sm:text-4xl lg:text-5xl",
12
12
  metricValue: "block text-2xl",
13
13
  metricValueLead: "block text-3xl",
14
14
  metricDisplay: "block text-5xl font-bold leading-none tracking-tight",
15
+ metricDisplayCompact: "block text-4xl font-bold leading-none tracking-tight",
15
16
  overline: "block font-mono text-xs font-bold uppercase tracking-[0.18em]",
16
17
  micro: "text-xs font-normal leading-snug",
17
18
  microStrong: "text-xs font-bold leading-snug",
18
19
  body: "text-sm",
20
+ lede: "text-[1.35rem] leading-snug",
19
21
  caption: "text-xs",
20
22
  helper: "text-xs",
21
23
  sectionTitle: "text-base font-bold leading-normal"
@@ -27,10 +29,12 @@ var defaultTextElementByVariant = {
27
29
  metricValue: "span",
28
30
  metricValueLead: "span",
29
31
  metricDisplay: "span",
32
+ metricDisplayCompact: "span",
30
33
  overline: "span",
31
34
  micro: "span",
32
35
  microStrong: "span",
33
36
  body: "p",
37
+ lede: "p",
34
38
  caption: "span",
35
39
  helper: "small",
36
40
  sectionTitle: "h3"
@@ -53,7 +57,7 @@ var textClassNameByAlign = {
53
57
  };
54
58
  function Text({
55
59
  as,
56
- variant = "micro",
60
+ variant = "caption",
57
61
  tone = "default",
58
62
  align,
59
63
  numeric = false,
@@ -86,8 +90,8 @@ function StackedDisplay({
86
90
  presentation = "dashboard",
87
91
  secondLine
88
92
  }) {
89
- const firstLineClassName = presentation === "welcome" ? "block text-muted-foreground font-editorial" : "block text-foreground";
90
- const secondLineClassName = presentation === "welcome" ? "block text-foreground" : "block text-muted-foreground font-editorial";
93
+ const firstLineClassName = presentation === "welcome" ? "block text-muted-foreground" : "block text-foreground";
94
+ const secondLineClassName = presentation === "welcome" ? "block text-foreground" : "block text-muted-foreground";
91
95
  return /* @__PURE__ */ jsxs(Text, { as: "span", variant: "displayCompact", align, children: [
92
96
  /* @__PURE__ */ jsx("span", { className: firstLineClassName, children: firstLine }),
93
97
  /* @__PURE__ */ jsx("span", { className: secondLineClassName, children: secondLine })
@@ -114,8 +114,8 @@ import { MenuIcon, PanelLeftIcon } from "lucide-react";
114
114
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
115
115
  var SIDEBAR_COOKIE_NAME = "sidebar_state";
116
116
  var SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
117
- function readSidebarCookie() {
118
- const match = document.cookie.match(/(?:^|; )sidebar_state=(true|false)(?:;|$)/);
117
+ function readSidebarCookie(cookie) {
118
+ const match = cookie.match(/(?:^|; )sidebar_state=(true|false)(?:;|$)/);
119
119
  if (!match) return null;
120
120
  return match[1] === "true";
121
121
  }
@@ -130,24 +130,21 @@ function useSidebar() {
130
130
  }
131
131
  return context;
132
132
  }
133
+ function subscribeToForceIconSidebar(onStoreChange) {
134
+ const mql = window.matchMedia(TABLET_ICON_SIDEBAR_QUERY);
135
+ mql.addEventListener("change", onStoreChange);
136
+ return () => mql.removeEventListener("change", onStoreChange);
137
+ }
138
+ function getForceIconSidebarSnapshot() {
139
+ return window.matchMedia(TABLET_ICON_SIDEBAR_QUERY).matches;
140
+ }
133
141
  function useForceIconSidebar(forceIconAtTablet) {
134
- return React4.useSyncExternalStore(
135
- React4.useCallback(
136
- (onStoreChange) => {
137
- if (!forceIconAtTablet) return () => {
138
- };
139
- const mql = window.matchMedia(TABLET_ICON_SIDEBAR_QUERY);
140
- mql.addEventListener("change", onStoreChange);
141
- return () => mql.removeEventListener("change", onStoreChange);
142
- },
143
- [forceIconAtTablet]
144
- ),
145
- React4.useCallback(() => {
146
- if (!forceIconAtTablet) return false;
147
- return window.matchMedia(TABLET_ICON_SIDEBAR_QUERY).matches;
148
- }, [forceIconAtTablet]),
142
+ const isTablet = React4.useSyncExternalStore(
143
+ subscribeToForceIconSidebar,
144
+ getForceIconSidebarSnapshot,
149
145
  getServerForceIconSidebarSnapshot
150
146
  );
147
+ return forceIconAtTablet && isTablet;
151
148
  }
152
149
  function SidebarProvider({
153
150
  defaultOpen = true,
@@ -168,18 +165,19 @@ function SidebarProvider({
168
165
  (_, nextOpen) => nextOpen,
169
166
  defaultOpen
170
167
  );
171
- React4.useLayoutEffect(() => {
172
- const stored = readSidebarCookie();
173
- if (stored === null) return;
174
- _setOpen(stored);
175
- }, []);
168
+ const cookieHydratedRef = React4.useRef(false);
169
+ const hydrateSidebarCookie = (node) => {
170
+ if (!node || cookieHydratedRef.current) return;
171
+ cookieHydratedRef.current = true;
172
+ const stored = readSidebarCookie(document.cookie);
173
+ if (stored !== null) _setOpen(stored);
174
+ };
176
175
  const open = openProp ?? _open;
177
176
  const setOpen = React4.useCallback(
178
- (value) => {
177
+ (openState) => {
179
178
  if (!allowUserCollapse) {
180
179
  return;
181
180
  }
182
- const openState = typeof value === "function" ? value(open) : value;
183
181
  if (setOpenProp) {
184
182
  setOpenProp(openState);
185
183
  } else {
@@ -187,29 +185,16 @@ function SidebarProvider({
187
185
  }
188
186
  document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
189
187
  },
190
- [allowUserCollapse, setOpenProp, open]
188
+ [allowUserCollapse, setOpenProp]
191
189
  );
192
190
  const toggleSidebar = React4.useCallback(() => {
193
191
  if (!allowUserCollapse) {
194
192
  return;
195
193
  }
196
194
  setSidebarMotionEnabled(true);
197
- return isMobile ? setOpenMobile((open2) => !open2) : setOpen((open2) => !open2);
198
- }, [allowUserCollapse, isMobile, setOpen, setOpenMobile]);
195
+ return isMobile ? setOpenMobile((open2) => !open2) : setOpen(!open);
196
+ }, [allowUserCollapse, isMobile, open, setOpen]);
199
197
  const toggleSidebarEvent = React4.useEffectEvent(toggleSidebar);
200
- React4.useEffect(() => {
201
- if (!allowUserCollapse) {
202
- return;
203
- }
204
- const handleKeyDown = (event) => {
205
- if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
206
- event.preventDefault();
207
- toggleSidebarEvent();
208
- }
209
- };
210
- window.addEventListener("keydown", handleKeyDown);
211
- return () => window.removeEventListener("keydown", handleKeyDown);
212
- }, [allowUserCollapse]);
213
198
  const state = forceIcon && !isMobile ? "collapsed" : open ? "expanded" : "collapsed";
214
199
  const contextValue = React4.useMemo(
215
200
  () => ({
@@ -224,9 +209,23 @@ function SidebarProvider({
224
209
  }),
225
210
  [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, sidebarMotionEnabled]
226
211
  );
212
+ React4.useEffect(() => {
213
+ if (!allowUserCollapse) {
214
+ return;
215
+ }
216
+ const handleKeyDown = (event) => {
217
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
218
+ event.preventDefault();
219
+ toggleSidebarEvent();
220
+ }
221
+ };
222
+ window.addEventListener("keydown", handleKeyDown);
223
+ return () => window.removeEventListener("keydown", handleKeyDown);
224
+ }, [allowUserCollapse]);
227
225
  return /* @__PURE__ */ jsx2(SidebarContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx2(
228
226
  "div",
229
227
  {
228
+ ref: hydrateSidebarCookie,
230
229
  "data-slot": "sidebar-wrapper",
231
230
  style,
232
231
  className: cn(
@@ -363,7 +362,7 @@ function SidebarInset({ className, ...props }) {
363
362
  {
364
363
  "data-slot": "sidebar-inset",
365
364
  className: cn(
366
- "relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
365
+ "relative flex min-h-0 w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
367
366
  className
368
367
  ),
369
368
  ...props
@@ -0,0 +1,469 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-VIB7OX7D.js";
4
+
5
+ // ../ui/src/warmio/flame-expression-video.tsx
6
+ import { useRef } from "react";
7
+
8
+ // ../ui/src/warmio/flame-expression-videos.ts
9
+ var FLAME_EXPRESSION_VIDEO_PACK = "v4";
10
+ var FLAME_EXPRESSION_VIDEO_IDS = ["NORMAL", "LUL", "POG", "CRY", "LOVE", "HYPE"];
11
+ var LEGACY_MASCOT_MOOD_TO_EXPRESSION = {
12
+ calm: "NORMAL",
13
+ happy: "LUL",
14
+ focused: "NORMAL",
15
+ relieved: "LOVE",
16
+ /** @deprecated Use peekEyes="closed" on auth peek instead. */
17
+ "password-retry": "NORMAL",
18
+ frustrated: "CRY"
19
+ };
20
+ function isLegacyMascotMood(expression) {
21
+ return Object.hasOwn(LEGACY_MASCOT_MOOD_TO_EXPRESSION, expression);
22
+ }
23
+ function isFlameExpressionVideoId(expression) {
24
+ for (const id of FLAME_EXPRESSION_VIDEO_IDS) {
25
+ if (id === expression) return true;
26
+ }
27
+ return false;
28
+ }
29
+ function getFlameExpressionVideoSrc(expression, theme) {
30
+ return `/flame-expressions/${FLAME_EXPRESSION_VIDEO_PACK}/${expression}/open_${theme}.mp4`;
31
+ }
32
+ function getFlameMascotPeekVideoSrc(eyes, theme) {
33
+ return `/mascot/${eyes}_${theme}.mp4`;
34
+ }
35
+ function resolveFlameExpressionVideoId(expression) {
36
+ if (!expression) return "NORMAL";
37
+ if (isLegacyMascotMood(expression)) {
38
+ return LEGACY_MASCOT_MOOD_TO_EXPRESSION[expression];
39
+ }
40
+ if (isFlameExpressionVideoId(expression)) {
41
+ return expression;
42
+ }
43
+ return "NORMAL";
44
+ }
45
+
46
+ // ../ui/src/warmio/flame-expression-video.tsx
47
+ import { jsx, jsxs } from "react/jsx-runtime";
48
+ var LIGHT_KEY = 247;
49
+ var DARK_KEY = 17;
50
+ var HARD_KEY = 16;
51
+ var SOFT_KEY = 42;
52
+ function prefersReducedMotion() {
53
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
54
+ }
55
+ function isDarkDocument() {
56
+ return document.documentElement.classList.contains("dark");
57
+ }
58
+ function pixelChannel(pixels, index) {
59
+ const value = pixels[index];
60
+ if (value === void 0) throw new Error("Canvas pixel data is unexpectedly sparse");
61
+ return value;
62
+ }
63
+ function isDarkForRoot(root) {
64
+ const theme = root.dataset["flameTheme"];
65
+ if (theme === "dark") return true;
66
+ if (theme === "light") return false;
67
+ return isDarkDocument();
68
+ }
69
+ function punchKeyedBackground(context, width, height, dark) {
70
+ const frame = context.getImageData(0, 0, width, height);
71
+ const pixels = frame.data;
72
+ const key = dark ? DARK_KEY : LIGHT_KEY;
73
+ for (let index = 0; index < pixels.length; index += 4) {
74
+ const red = pixelChannel(pixels, index);
75
+ const green = pixelChannel(pixels, index + 1);
76
+ const blue = pixelChannel(pixels, index + 2);
77
+ if (!dark) {
78
+ const minChannel = Math.min(red, green, blue);
79
+ if (minChannel > LIGHT_KEY) {
80
+ pixels[index] = 255;
81
+ pixels[index + 1] = 255;
82
+ pixels[index + 2] = 255;
83
+ pixels[index + 3] = 255;
84
+ continue;
85
+ }
86
+ }
87
+ const distance = Math.max(Math.abs(red - key), Math.abs(green - key), Math.abs(blue - key));
88
+ if (distance <= HARD_KEY) {
89
+ pixels[index + 3] = 0;
90
+ continue;
91
+ }
92
+ if (distance < SOFT_KEY) {
93
+ const alpha = pixelChannel(pixels, index + 3);
94
+ pixels[index + 3] = Math.round(alpha * ((distance - HARD_KEY) / (SOFT_KEY - HARD_KEY)));
95
+ }
96
+ }
97
+ context.putImageData(frame, 0, 0);
98
+ }
99
+ function syncCanvasSize(canvas, context) {
100
+ const cssSize = canvas.clientWidth;
101
+ if (cssSize === 0) return false;
102
+ const pixelRatio = Math.min(Math.max(window.devicePixelRatio || 1, 2), 3);
103
+ const nextSize = Math.max(1, Math.round(cssSize * pixelRatio));
104
+ if (canvas.width !== nextSize || canvas.height !== nextSize) {
105
+ canvas.width = nextSize;
106
+ canvas.height = nextSize;
107
+ }
108
+ context.imageSmoothingEnabled = true;
109
+ context.imageSmoothingQuality = "high";
110
+ return true;
111
+ }
112
+ function drawKeyedFrame(video, canvas, context, dark) {
113
+ if (!syncCanvasSize(canvas, context) || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
114
+ return;
115
+ }
116
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
117
+ punchKeyedBackground(context, canvas.width, canvas.height, dark);
118
+ }
119
+ function sourceForTheme(video, dark) {
120
+ return dark ? video.dataset["darkSrc"] : video.dataset["lightSrc"];
121
+ }
122
+ function bindKeyedExpressionVideo(root) {
123
+ if (root.dataset["bound"] === "true") return () => void 0;
124
+ root.dataset["bound"] = "true";
125
+ const video = root.querySelector("[data-flame-expression-video-source]");
126
+ const canvas = root.querySelector("[data-flame-expression-video-frame]");
127
+ if (!video || !canvas) {
128
+ delete root.dataset["bound"];
129
+ return () => void 0;
130
+ }
131
+ const context = canvas.getContext("2d", { willReadFrequently: true });
132
+ if (!context) {
133
+ root.hidden = true;
134
+ delete root.dataset["bound"];
135
+ return () => void 0;
136
+ }
137
+ let dark = isDarkForRoot(root);
138
+ let playing = false;
139
+ let visible = true;
140
+ let frameHandle = 0;
141
+ let lastVideoTime = -1;
142
+ const reducedMotion = prefersReducedMotion();
143
+ let loadedDataListener = null;
144
+ const drawFrame = () => {
145
+ const nextDark = isDarkForRoot(root);
146
+ if (nextDark !== dark) {
147
+ applyTheme(nextDark);
148
+ return;
149
+ }
150
+ drawKeyedFrame(video, canvas, context, dark);
151
+ };
152
+ const stopLoop = () => {
153
+ cancelAnimationFrame(frameHandle);
154
+ frameHandle = 0;
155
+ lastVideoTime = -1;
156
+ };
157
+ const loop = () => {
158
+ if (!playing || reducedMotion) return;
159
+ if (video.currentTime !== lastVideoTime) {
160
+ lastVideoTime = video.currentTime;
161
+ drawFrame();
162
+ }
163
+ frameHandle = requestAnimationFrame(loop);
164
+ };
165
+ const startLoop = () => {
166
+ if (playing || reducedMotion) {
167
+ drawFrame();
168
+ return;
169
+ }
170
+ playing = true;
171
+ void video.play().catch(() => void 0);
172
+ loop();
173
+ };
174
+ const pauseLoop = () => {
175
+ playing = false;
176
+ video.pause();
177
+ stopLoop();
178
+ };
179
+ const playWhenSized = () => {
180
+ if (root.clientWidth === 0) return;
181
+ drawFrame();
182
+ if (!visible || reducedMotion || playing) return;
183
+ startLoop();
184
+ };
185
+ const continueFrom = (resumeAt) => {
186
+ if (Number.isFinite(resumeAt) && resumeAt > 0) {
187
+ try {
188
+ video.currentTime = resumeAt;
189
+ } catch {
190
+ }
191
+ }
192
+ drawFrame();
193
+ if (visible && !reducedMotion) {
194
+ startLoop();
195
+ }
196
+ };
197
+ const waitForData = (resumeAt) => {
198
+ if (loadedDataListener) {
199
+ video.removeEventListener("loadeddata", loadedDataListener);
200
+ }
201
+ loadedDataListener = () => continueFrom(resumeAt);
202
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
203
+ loadedDataListener();
204
+ loadedDataListener = null;
205
+ return;
206
+ }
207
+ video.addEventListener("loadeddata", loadedDataListener, { once: true });
208
+ void video.play().catch(() => void 0);
209
+ };
210
+ const applyTheme = (nextDark) => {
211
+ const nextSrc = sourceForTheme(video, nextDark);
212
+ if (!nextSrc) return;
213
+ const resumeAt = video.currentTime;
214
+ dark = nextDark;
215
+ const assignedSrc = video.getAttribute("src");
216
+ const alreadyOnSource = video.networkState !== HTMLMediaElement.NETWORK_EMPTY && (video.dataset["activeSrc"] === nextSrc || assignedSrc === nextSrc);
217
+ if (alreadyOnSource) {
218
+ video.dataset["activeSrc"] = nextSrc;
219
+ waitForData(resumeAt);
220
+ return;
221
+ }
222
+ pauseLoop();
223
+ video.dataset["activeSrc"] = nextSrc;
224
+ video.src = nextSrc;
225
+ waitForData(resumeAt);
226
+ };
227
+ syncCanvasSize(canvas, context);
228
+ applyTheme(dark);
229
+ const visibility = new IntersectionObserver(
230
+ (entries) => {
231
+ visible = entries.some((entry) => entry.isIntersecting);
232
+ if (!visible) {
233
+ pauseLoop();
234
+ return;
235
+ }
236
+ if (reducedMotion) {
237
+ drawFrame();
238
+ return;
239
+ }
240
+ startLoop();
241
+ },
242
+ { threshold: 0 }
243
+ );
244
+ visibility.observe(root);
245
+ const resize = new ResizeObserver(playWhenSized);
246
+ resize.observe(root);
247
+ const themeObserver = new MutationObserver(() => {
248
+ const nextDark = isDarkForRoot(root);
249
+ if (nextDark === dark) return;
250
+ applyTheme(nextDark);
251
+ });
252
+ themeObserver.observe(document.documentElement, {
253
+ attributes: true,
254
+ attributeFilter: ["class"]
255
+ });
256
+ const themeOverrideObserver = new MutationObserver(() => {
257
+ const nextDark = isDarkForRoot(root);
258
+ if (nextDark === dark) return;
259
+ applyTheme(nextDark);
260
+ });
261
+ themeOverrideObserver.observe(root, {
262
+ attributes: true,
263
+ attributeFilter: ["data-flame-theme"]
264
+ });
265
+ return () => {
266
+ visibility.disconnect();
267
+ resize.disconnect();
268
+ themeObserver.disconnect();
269
+ themeOverrideObserver.disconnect();
270
+ pauseLoop();
271
+ if (loadedDataListener) {
272
+ video.removeEventListener("loadeddata", loadedDataListener);
273
+ }
274
+ video.removeAttribute("src");
275
+ video.load();
276
+ delete video.dataset["activeSrc"];
277
+ delete root.dataset["bound"];
278
+ };
279
+ }
280
+ function readDomVideoTheme() {
281
+ if (typeof document === "undefined") return "light";
282
+ const mode = document.documentElement.getAttribute("data-theme-mode");
283
+ if (mode === "dark") return "dark";
284
+ if (mode === "light") return "light";
285
+ if (globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark";
286
+ return "light";
287
+ }
288
+ function WarmFlameExpressionVideo({
289
+ expression: expressionProp = "NORMAL",
290
+ size = "default",
291
+ label = null,
292
+ theme: themeProp,
293
+ peekEyes
294
+ }) {
295
+ const cleanupRef = useRef(void 0);
296
+ const expression = resolveFlameExpressionVideoId(expressionProp);
297
+ const videoTheme = themeProp ?? readDomVideoTheme();
298
+ const lightSrc = peekEyes ? getFlameMascotPeekVideoSrc(peekEyes, "light") : getFlameExpressionVideoSrc(expression, "light");
299
+ const darkSrc = peekEyes ? getFlameMascotPeekVideoSrc(peekEyes, "dark") : getFlameExpressionVideoSrc(expression, "dark");
300
+ const source = videoTheme === "dark" ? darkSrc : lightSrc;
301
+ const setRootRef = useRef(null);
302
+ if (setRootRef.current === null) {
303
+ setRootRef.current = (node) => {
304
+ cleanupRef.current?.();
305
+ cleanupRef.current = void 0;
306
+ if (!node) return;
307
+ cleanupRef.current = bindKeyedExpressionVideo(node);
308
+ };
309
+ }
310
+ return /* @__PURE__ */ jsxs(
311
+ "div",
312
+ {
313
+ ref: setRootRef.current,
314
+ className: cn(
315
+ "warm-flame-expression-video",
316
+ "pointer-events-none inline-flex shrink-0 select-none"
317
+ ),
318
+ "data-slot": "warm-flame-expression-video",
319
+ "data-size": size,
320
+ "data-expression": expression,
321
+ "data-flame-peek-eyes": peekEyes,
322
+ "data-flame-theme": videoTheme,
323
+ role: label ? "img" : void 0,
324
+ "aria-label": label ?? void 0,
325
+ "aria-hidden": label ? void 0 : true,
326
+ children: [
327
+ /* @__PURE__ */ jsx(
328
+ "video",
329
+ {
330
+ className: "warm-flame-expression-video__source",
331
+ "data-flame-expression-video-source": "",
332
+ "data-light-src": lightSrc,
333
+ "data-dark-src": darkSrc,
334
+ src: source,
335
+ muted: true,
336
+ loop: true,
337
+ playsInline: true,
338
+ preload: "auto",
339
+ disablePictureInPicture: true,
340
+ "aria-hidden": "true"
341
+ }
342
+ ),
343
+ /* @__PURE__ */ jsx("canvas", { className: "warm-flame-expression-video__frame", "data-flame-expression-video-frame": "" })
344
+ ]
345
+ }
346
+ );
347
+ }
348
+
349
+ // ../ui/src/warmio/logo.tsx
350
+ import {
351
+ Logo,
352
+ WarmLogoIcon
353
+ } from "@warmio/design/logo";
354
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
355
+ var brandedLogoShellClassNameBySize = {
356
+ sm: "size-8",
357
+ md: "size-10",
358
+ lg: "size-14"
359
+ };
360
+ var brandedLogoIconClassNameBySize = {
361
+ sm: "size-5",
362
+ md: "size-6",
363
+ lg: "size-8"
364
+ };
365
+ var _WarmLogoGlyph = /* @__PURE__ */ jsxs2(Fragment, { children: [
366
+ /* @__PURE__ */ jsx2(
367
+ "path",
368
+ {
369
+ d: "M 33.500 40.660 C 20.206 42.744, 0 48.560, -0 50.302 C -0 51.759, 123.955 348.889, 127.738 356.500 C 142.182 385.561, 169.450 410.275, 198.946 421.040 C 218.922 428.330, 243.683 430.825, 264.526 427.648 C 277.151 425.724, 294.628 420.011, 294.194 417.952 C 293.790 416.040, 172.732 124.599, 168.522 115.405 C 161.779 100.679, 149.168 84.171, 135.093 71.646 C 119.358 57.644, 98.880 47.362, 77 42.478 C 69.645 40.836, 40.220 39.606, 33.500 40.660",
370
+ fill: "currentColor"
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsx2(
374
+ "path",
375
+ {
376
+ d: "M 229.651 41.597 C 217.916 43.718, 202.443 49.144, 202.744 51.035 C 203.418 55.268, 329.689 356.584, 333.687 363.500 C 340.525 375.327, 345.293 381.483, 355.408 391.538 C 374.603 410.619, 398.878 423.014, 425.907 427.533 C 439.649 429.831, 463.594 429.132, 476 426.072 C 489.892 422.645, 497.188 419.815, 496.703 418.043 C 495.416 413.331, 371.058 116.094, 367.406 109 C 349.467 74.149, 313.008 47.875, 273.752 41.509 C 262.281 39.648, 240.195 39.693, 229.651 41.597",
377
+ fill: "currentColor"
378
+ }
379
+ ),
380
+ /* @__PURE__ */ jsx2(
381
+ "path",
382
+ {
383
+ d: "M 463 40.593 C 451.670 42.545, 447.288 43.569, 439 46.203 C 433.775 47.864, 429.338 49.372, 429.140 49.555 C 428.942 49.737, 434.670 64.021, 441.868 81.297 C 455.574 114.190, 462.215 131.660, 468.447 151.208 C 475.360 172.892, 486.124 186.213, 503.834 194.997 C 530.956 208.450, 564.982 201.679, 583.654 179.112 C 604.399 154.041, 605.407 121.944, 586.291 95.155 C 567.827 69.280, 539.107 50.114, 508 42.907 C 500.680 41.212, 494.713 40.644, 482 40.434 C 472.925 40.284, 464.375 40.356, 463 40.593",
384
+ fill: "currentColor"
385
+ }
386
+ )
387
+ ] });
388
+ function WarmBrandedLogoMark({
389
+ size = "md",
390
+ className,
391
+ logoClassName
392
+ }) {
393
+ return /* @__PURE__ */ jsxs2(
394
+ "span",
395
+ {
396
+ className: cn(
397
+ "relative inline-flex shrink-0 items-center justify-center",
398
+ brandedLogoShellClassNameBySize[size],
399
+ className
400
+ ),
401
+ "aria-hidden": "true",
402
+ children: [
403
+ /* @__PURE__ */ jsx2("span", { className: "pointer-events-none absolute inset-0 scale-[1.35] opacity-90", children: /* @__PURE__ */ jsx2(WarmFlameExpressionVideo, { expression: "calm", label: null }) }),
404
+ /* @__PURE__ */ jsx2(
405
+ "svg",
406
+ {
407
+ xmlns: "http://www.w3.org/2000/svg",
408
+ viewBox: "0 0 600 600",
409
+ className: cn(
410
+ "relative z-10 drop-shadow-sm",
411
+ brandedLogoIconClassNameBySize[size],
412
+ logoClassName
413
+ ),
414
+ "aria-hidden": "true",
415
+ children: /* @__PURE__ */ jsx2("g", { transform: "translate(0 108)", children: _WarmLogoGlyph })
416
+ }
417
+ )
418
+ ]
419
+ }
420
+ );
421
+ }
422
+ function WarmBrandedLogoLink({
423
+ href,
424
+ ariaLabel = "Warm home",
425
+ size = "md",
426
+ className,
427
+ logoClassName
428
+ }) {
429
+ return /* @__PURE__ */ jsx2("a", { href, "aria-label": ariaLabel, className: "inline-flex shrink-0 items-center", children: /* @__PURE__ */ jsx2(
430
+ WarmBrandedLogoMark,
431
+ {
432
+ size,
433
+ ...className !== void 0 ? { className } : {},
434
+ ...logoClassName !== void 0 ? { logoClassName } : {}
435
+ }
436
+ ) });
437
+ }
438
+ function WarmBrandedLogoMarkStatic({
439
+ className,
440
+ logoClassName
441
+ }) {
442
+ return /* @__PURE__ */ jsxs2("span", { className: cn("relative inline-flex size-8 items-center justify-center", className), children: [
443
+ /* @__PURE__ */ jsx2(
444
+ "span",
445
+ {
446
+ "aria-hidden": "true",
447
+ className: "absolute inset-0 rounded-full bg-gradient-to-br from-primary/90 via-warning/80 to-primary/70"
448
+ }
449
+ ),
450
+ /* @__PURE__ */ jsx2(
451
+ "svg",
452
+ {
453
+ xmlns: "http://www.w3.org/2000/svg",
454
+ viewBox: "0 0 600 600",
455
+ className: cn("relative z-10 size-5 drop-shadow-sm", logoClassName),
456
+ "aria-hidden": "true",
457
+ children: /* @__PURE__ */ jsx2("g", { transform: "translate(0 108)", children: _WarmLogoGlyph })
458
+ }
459
+ )
460
+ ] });
461
+ }
462
+
463
+ export {
464
+ WarmBrandedLogoMark,
465
+ WarmBrandedLogoLink,
466
+ WarmBrandedLogoMarkStatic,
467
+ Logo,
468
+ WarmLogoIcon
469
+ };