love-ui 1.2.15 → 1.2.17

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 (54) hide show
  1. package/README.md +14 -6
  2. package/dist/index.js +149 -0
  3. package/dist/mcp-server.js +2 -0
  4. package/package.json +28 -4
  5. package/registry/default/examples/code-block-shared.tsx +8 -1
  6. package/registry/default/ui/form.tsx +6 -1
  7. package/registry/default/ui/toast-gooey-icons.tsx +68 -0
  8. package/registry/default/ui/toast-gooey-renderer.tsx +614 -0
  9. package/registry/default/ui/toast-gooey-types.ts +45 -0
  10. package/registry/default/ui/toast-gooey.css +511 -0
  11. package/registry/default/ui/toast-gooey.tsx +445 -0
  12. package/registry/default/ui/toast.tsx +1 -1
  13. package/skills/loveui-skills/SKILL.md +170 -0
  14. package/skills/loveui-skills/references/accessibility-baseline.md +32 -0
  15. package/skills/loveui-skills/references/component-api-and-naming.md +30 -0
  16. package/skills/loveui-skills/references/content-ux-writing.md +33 -0
  17. package/skills/loveui-skills/references/design-directions.md +60 -0
  18. package/skills/loveui-skills/references/forms-and-validation.md +30 -0
  19. package/skills/loveui-skills/references/frontend-architecture.md +30 -0
  20. package/skills/loveui-skills/references/interaction-heuristics.md +45 -0
  21. package/skills/loveui-skills/references/mcp-catalog-workflow.md +68 -0
  22. package/skills/loveui-skills/references/motion-and-feedback.md +31 -0
  23. package/skills/loveui-skills/references/navigation-and-information-architecture.md +30 -0
  24. package/skills/loveui-skills/references/page-blueprints.md +76 -0
  25. package/skills/loveui-skills/references/quality-gates.md +51 -0
  26. package/skills/loveui-skills/references/screenshot-translation-protocol.md +52 -0
  27. package/skills/loveui-skills/references/structural-cleanliness.md +37 -0
  28. package/skills/loveui-skills/references/testing-and-quality-strategy.md +33 -0
  29. package/skills/loveui-skills/references/visual-primitives.md +42 -0
  30. package/skills/loveui-skills/skills/adapt/SKILL.md +199 -0
  31. package/skills/loveui-skills/skills/animate/SKILL.md +190 -0
  32. package/skills/loveui-skills/skills/audit/SKILL.md +127 -0
  33. package/skills/loveui-skills/skills/bolder/SKILL.md +132 -0
  34. package/skills/loveui-skills/skills/clarify/SKILL.md +180 -0
  35. package/skills/loveui-skills/skills/colorize/SKILL.md +158 -0
  36. package/skills/loveui-skills/skills/critique/SKILL.md +118 -0
  37. package/skills/loveui-skills/skills/delight/SKILL.md +317 -0
  38. package/skills/loveui-skills/skills/distill/SKILL.md +137 -0
  39. package/skills/loveui-skills/skills/extract/SKILL.md +95 -0
  40. package/skills/loveui-skills/skills/frontend-design/SKILL.md +127 -0
  41. package/skills/loveui-skills/skills/frontend-design/reference/color-and-contrast.md +132 -0
  42. package/skills/loveui-skills/skills/frontend-design/reference/interaction-design.md +123 -0
  43. package/skills/loveui-skills/skills/frontend-design/reference/motion-design.md +99 -0
  44. package/skills/loveui-skills/skills/frontend-design/reference/responsive-design.md +114 -0
  45. package/skills/loveui-skills/skills/frontend-design/reference/spatial-design.md +100 -0
  46. package/skills/loveui-skills/skills/frontend-design/reference/typography.md +131 -0
  47. package/skills/loveui-skills/skills/frontend-design/reference/ux-writing.md +107 -0
  48. package/skills/loveui-skills/skills/harden/SKILL.md +358 -0
  49. package/skills/loveui-skills/skills/normalize/SKILL.md +67 -0
  50. package/skills/loveui-skills/skills/onboard/SKILL.md +243 -0
  51. package/skills/loveui-skills/skills/optimize/SKILL.md +269 -0
  52. package/skills/loveui-skills/skills/polish/SKILL.md +202 -0
  53. package/skills/loveui-skills/skills/quieter/SKILL.md +118 -0
  54. package/skills/loveui-skills/skills/teach-loveui/SKILL.md +69 -0
@@ -0,0 +1,614 @@
1
+ import {
2
+ type CSSProperties,
3
+ type MouseEventHandler,
4
+ memo,
5
+ type ReactNode,
6
+ type TransitionEventHandler,
7
+ useCallback,
8
+ useEffect,
9
+ useLayoutEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+ import type { GooeyButton, GooeyState, GooeyStyles } from "./toast-gooey-types";
15
+ import "./toast-gooey.css";
16
+ import {
17
+ ArrowRight,
18
+ Check,
19
+ CircleAlert,
20
+ LifeBuoy,
21
+ LoaderCircle,
22
+ X,
23
+ } from "./toast-gooey-icons";
24
+
25
+ /* --------------------------------- Config --------------------------------- */
26
+
27
+ const HEIGHT = 40;
28
+ const WIDTH = 350;
29
+ const DEFAULT_ROUNDNESS = 18;
30
+ const BLUR_RATIO = 0.5;
31
+ const PILL_PADDING = 10;
32
+ const MIN_EXPAND_RATIO = 2.25;
33
+ const SWAP_COLLAPSE_MS = 200;
34
+ const HEADER_EXIT_MS = 150;
35
+
36
+ type State = GooeyState;
37
+
38
+ interface View {
39
+ title?: string;
40
+ description?: ReactNode | string;
41
+ state: State;
42
+ icon?: ReactNode | null;
43
+ styles?: GooeyStyles;
44
+ button?: GooeyButton;
45
+ fill?: string;
46
+ }
47
+
48
+ interface GooeyProps {
49
+ id: string;
50
+ fill?: string;
51
+ state?: State;
52
+ title?: string;
53
+ description?: ReactNode | string;
54
+ position?: "left" | "center" | "right";
55
+ expand?: "top" | "bottom";
56
+ className?: string;
57
+ icon?: ReactNode | null;
58
+ styles?: GooeyStyles;
59
+ button?: GooeyButton;
60
+ roundness?: number;
61
+ exiting?: boolean;
62
+ autoExpandDelayMs?: number;
63
+ autoCollapseDelayMs?: number;
64
+ canExpand?: boolean;
65
+ interruptKey?: string;
66
+ refreshKey?: string;
67
+ onMouseEnter?: MouseEventHandler<HTMLButtonElement>;
68
+ onMouseLeave?: MouseEventHandler<HTMLButtonElement>;
69
+ onDismiss?: () => void;
70
+ }
71
+
72
+ /* ---------------------------------- Icons --------------------------------- */
73
+
74
+ const STATE_ICON: Record<State, ReactNode> = {
75
+ success: <Check />,
76
+ loading: <LoaderCircle data-gooey-icon="spin" aria-hidden="true" />,
77
+ error: <X />,
78
+ warning: <CircleAlert />,
79
+ info: <LifeBuoy />,
80
+ action: <ArrowRight />,
81
+ };
82
+
83
+ /* ----------------------------- Memoised Defs ------------------------------ */
84
+ const GooeyDefs = memo(function GooeyDefs({
85
+ filterId,
86
+ blur,
87
+ }: {
88
+ filterId: string;
89
+ blur: number;
90
+ }) {
91
+ return (
92
+ <defs>
93
+ <filter
94
+ id={filterId}
95
+ x="-20%"
96
+ y="-20%"
97
+ width="140%"
98
+ height="140%"
99
+ colorInterpolationFilters="sRGB"
100
+ >
101
+ <feGaussianBlur in="SourceGraphic" stdDeviation={blur} result="blur" />
102
+ <feColorMatrix
103
+ in="blur"
104
+ mode="matrix"
105
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -10"
106
+ result="goo"
107
+ />
108
+ <feComposite in="SourceGraphic" in2="goo" operator="atop" />
109
+ </filter>
110
+ </defs>
111
+ );
112
+ });
113
+
114
+ /* ------------------------------- Component -------------------------------- */
115
+
116
+ export const Gooey = memo(function Gooey({
117
+ id,
118
+ fill,
119
+ state = "success",
120
+ title = state,
121
+ description,
122
+ position = "left",
123
+ expand = "bottom",
124
+ className,
125
+ icon,
126
+ styles,
127
+ button,
128
+ roundness,
129
+ exiting = false,
130
+ autoExpandDelayMs,
131
+ autoCollapseDelayMs,
132
+ canExpand,
133
+ interruptKey,
134
+ refreshKey,
135
+ onMouseEnter,
136
+ onMouseLeave,
137
+ onDismiss,
138
+ }: GooeyProps) {
139
+ const next: View = useMemo(
140
+ () => ({ title, description, state, icon, styles, button, fill }),
141
+ [title, description, state, icon, styles, button, fill],
142
+ );
143
+
144
+ const [view, setView] = useState<View>(next);
145
+ const [applied, setApplied] = useState(refreshKey);
146
+ const [isExpanded, setIsExpanded] = useState(false);
147
+ const [ready, setReady] = useState(false);
148
+ const [pillWidth, setPillWidth] = useState(0);
149
+ const [contentHeight, setContentHeight] = useState(0);
150
+ const hasDesc = Boolean(view.description) || Boolean(view.button);
151
+ const isLoading = view.state === "loading";
152
+ const open = hasDesc && isExpanded && !isLoading;
153
+ const allowExpand = isLoading
154
+ ? false
155
+ : (canExpand ?? (!interruptKey || interruptKey === id));
156
+
157
+ const headerKey = `${view.state}-${view.title}`;
158
+ const filterId = `gooey-gooey-${id}`;
159
+ const resolvedRoundness = Math.max(0, roundness ?? DEFAULT_ROUNDNESS);
160
+ const blur = resolvedRoundness * BLUR_RATIO;
161
+
162
+ const headerRef = useRef<HTMLDivElement>(null);
163
+ const contentRef = useRef<HTMLDivElement>(null);
164
+ const headerExitRef = useRef<number | null>(null);
165
+ const autoExpandRef = useRef<number | null>(null);
166
+ const autoCollapseRef = useRef<number | null>(null);
167
+ const swapTimerRef = useRef<number | null>(null);
168
+ const lastRefreshKeyRef = useRef(refreshKey);
169
+ const pendingRef = useRef<{ key?: string; payload: View } | null>(null);
170
+ const [headerLayer, setHeaderLayer] = useState<{
171
+ current: { key: string; view: View };
172
+ prev: { key: string; view: View } | null;
173
+ }>({ current: { key: headerKey, view }, prev: null });
174
+
175
+ /* ------------------------------ Measurements ------------------------------ */
176
+
177
+ const innerRef = useRef<HTMLDivElement>(null);
178
+
179
+ const headerPadRef = useRef<number | null>(null);
180
+
181
+ // biome-ignore lint/correctness/useExhaustiveDependencies: headerLayer.current.key is used to force a re-render
182
+ useLayoutEffect(() => {
183
+ const el = innerRef.current;
184
+ const header = headerRef.current;
185
+ if (!el || !header) return;
186
+ if (headerPadRef.current === null) {
187
+ const cs = getComputedStyle(header);
188
+ headerPadRef.current =
189
+ parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
190
+ }
191
+ const px = headerPadRef.current;
192
+ const measure = () => {
193
+ const w = el.scrollWidth + px + PILL_PADDING;
194
+ if (w > PILL_PADDING) {
195
+ setPillWidth((prev) => (prev === w ? prev : w));
196
+ }
197
+ };
198
+ measure();
199
+ let rafId = 0;
200
+ const ro = new ResizeObserver(() => {
201
+ cancelAnimationFrame(rafId);
202
+ rafId = requestAnimationFrame(measure);
203
+ });
204
+ ro.observe(el);
205
+ return () => {
206
+ cancelAnimationFrame(rafId);
207
+ ro.disconnect();
208
+ };
209
+ }, [headerLayer.current.key]);
210
+
211
+ useLayoutEffect(() => {
212
+ if (!hasDesc) {
213
+ setContentHeight(0);
214
+ return;
215
+ }
216
+ const el = contentRef.current;
217
+ if (!el) return;
218
+ const measure = () => {
219
+ const h = el.scrollHeight;
220
+ setContentHeight((prev) => (prev === h ? prev : h));
221
+ };
222
+ measure();
223
+ let rafId = 0;
224
+ const ro = new ResizeObserver(() => {
225
+ cancelAnimationFrame(rafId);
226
+ rafId = requestAnimationFrame(measure);
227
+ });
228
+ ro.observe(el);
229
+ return () => {
230
+ cancelAnimationFrame(rafId);
231
+ ro.disconnect();
232
+ };
233
+ }, [hasDesc]);
234
+
235
+ useEffect(() => {
236
+ const raf = requestAnimationFrame(() => setReady(true));
237
+ return () => cancelAnimationFrame(raf);
238
+ }, []);
239
+
240
+ useLayoutEffect(() => {
241
+ setHeaderLayer((state) => {
242
+ if (state.current.key === headerKey) {
243
+ if (state.current.view === view) return state;
244
+ return { ...state, current: { key: headerKey, view } };
245
+ }
246
+ return {
247
+ prev: state.current,
248
+ current: { key: headerKey, view },
249
+ };
250
+ });
251
+ }, [headerKey, view]);
252
+
253
+ useEffect(() => {
254
+ if (!headerLayer.prev) return;
255
+ if (headerExitRef.current) {
256
+ clearTimeout(headerExitRef.current);
257
+ }
258
+ headerExitRef.current = window.setTimeout(() => {
259
+ headerExitRef.current = null;
260
+ setHeaderLayer((state) => ({ ...state, prev: null }));
261
+ }, HEADER_EXIT_MS);
262
+ return () => {
263
+ if (headerExitRef.current) {
264
+ clearTimeout(headerExitRef.current);
265
+ headerExitRef.current = null;
266
+ }
267
+ };
268
+ }, [headerLayer.prev]);
269
+
270
+ /* ----------------------------- Refresh logic ------------------------------ */
271
+
272
+ useEffect(() => {
273
+ if (refreshKey === undefined) {
274
+ setView(next);
275
+ setApplied(undefined);
276
+ pendingRef.current = null;
277
+ lastRefreshKeyRef.current = refreshKey;
278
+ return;
279
+ }
280
+
281
+ if (lastRefreshKeyRef.current === refreshKey) return;
282
+ lastRefreshKeyRef.current = refreshKey;
283
+
284
+ if (swapTimerRef.current) {
285
+ clearTimeout(swapTimerRef.current);
286
+ swapTimerRef.current = null;
287
+ }
288
+
289
+ if (open) {
290
+ pendingRef.current = { key: refreshKey, payload: next };
291
+ setIsExpanded(false);
292
+ swapTimerRef.current = window.setTimeout(() => {
293
+ swapTimerRef.current = null;
294
+ const pending = pendingRef.current;
295
+ if (!pending) return;
296
+ setView(pending.payload);
297
+ setApplied(pending.key);
298
+ pendingRef.current = null;
299
+ }, SWAP_COLLAPSE_MS);
300
+ } else {
301
+ pendingRef.current = null;
302
+ setView(next);
303
+ setApplied(refreshKey);
304
+ }
305
+ }, [open, refreshKey, next]);
306
+
307
+ /* ----------------------------- Auto expand/collapse ----------------------- */
308
+
309
+ // biome-ignore lint/correctness/useExhaustiveDependencies: applied is used to force a re-render
310
+ useEffect(() => {
311
+ if (!hasDesc) return;
312
+
313
+ if (autoExpandRef.current) clearTimeout(autoExpandRef.current);
314
+ if (autoCollapseRef.current) clearTimeout(autoCollapseRef.current);
315
+
316
+ if (exiting || !allowExpand) {
317
+ setIsExpanded(false);
318
+ return;
319
+ }
320
+
321
+ if (autoExpandDelayMs == null && autoCollapseDelayMs == null) return;
322
+
323
+ const expandDelay = autoExpandDelayMs ?? 0;
324
+ const collapseDelay = autoCollapseDelayMs ?? 0;
325
+
326
+ if (expandDelay > 0) {
327
+ autoExpandRef.current = window.setTimeout(
328
+ () => setIsExpanded(true),
329
+ expandDelay,
330
+ );
331
+ } else {
332
+ setIsExpanded(true);
333
+ }
334
+
335
+ if (collapseDelay > 0) {
336
+ autoCollapseRef.current = window.setTimeout(
337
+ () => setIsExpanded(false),
338
+ collapseDelay,
339
+ );
340
+ }
341
+
342
+ return () => {
343
+ if (autoExpandRef.current) clearTimeout(autoExpandRef.current);
344
+ if (autoCollapseRef.current) clearTimeout(autoCollapseRef.current);
345
+ };
346
+ }, [
347
+ autoCollapseDelayMs,
348
+ autoExpandDelayMs,
349
+ hasDesc,
350
+ allowExpand,
351
+ exiting,
352
+ applied,
353
+ ]);
354
+
355
+ /* ------------------------------ Derived values ---------------------------- */
356
+
357
+ const minExpanded = HEIGHT * MIN_EXPAND_RATIO;
358
+ const rawExpanded = hasDesc
359
+ ? Math.max(minExpanded, HEIGHT + contentHeight)
360
+ : minExpanded;
361
+
362
+ const frozenExpandedRef = useRef(rawExpanded);
363
+ if (open) {
364
+ frozenExpandedRef.current = rawExpanded;
365
+ }
366
+
367
+ const expanded = open ? rawExpanded : frozenExpandedRef.current;
368
+ const svgHeight = hasDesc ? Math.max(expanded, minExpanded) : HEIGHT;
369
+ const expandedContent = Math.max(0, expanded - HEIGHT);
370
+ const resolvedPillWidth = Math.max(pillWidth || HEIGHT, HEIGHT);
371
+ const pillHeight = HEIGHT + blur * 3;
372
+
373
+ const pillX =
374
+ position === "right"
375
+ ? WIDTH - resolvedPillWidth
376
+ : position === "center"
377
+ ? (WIDTH - resolvedPillWidth) / 2
378
+ : 0;
379
+
380
+ /* ------------------------------- Inline styles ---------------------------- */
381
+
382
+ const rootStyle = useMemo<CSSProperties & Record<string, string>>(
383
+ () => ({
384
+ "--_h": `${open ? expanded : HEIGHT}px`,
385
+ "--_pw": `${resolvedPillWidth}px`,
386
+ "--_px": `${pillX}px`,
387
+ "--_sy": `${open ? 1 : HEIGHT / pillHeight}`,
388
+ "--_ph": `${pillHeight}px`,
389
+ "--_by": `${open ? 1 : 0}`,
390
+ "--_ht": `translateY(${open ? (expand === "bottom" ? 3 : -3) : 0}px) scale(${open ? 0.9 : 1})`,
391
+ "--_co": `${open ? 1 : 0}`,
392
+ }),
393
+ [open, expanded, resolvedPillWidth, pillX, expand, pillHeight],
394
+ );
395
+
396
+ /* -------------------------------- Handlers -------------------------------- */
397
+
398
+ const handleEnter: MouseEventHandler<HTMLButtonElement> = useCallback(
399
+ (e) => {
400
+ onMouseEnter?.(e);
401
+ if (hasDesc) setIsExpanded(true);
402
+ },
403
+ [hasDesc, onMouseEnter],
404
+ );
405
+
406
+ const handleLeave: MouseEventHandler<HTMLButtonElement> = useCallback(
407
+ (e) => {
408
+ onMouseLeave?.(e);
409
+ setIsExpanded(false);
410
+ },
411
+ [onMouseLeave],
412
+ );
413
+
414
+ const handleTransitionEnd: TransitionEventHandler<HTMLButtonElement> =
415
+ useCallback(
416
+ (e) => {
417
+ if (e.propertyName !== "height" && e.propertyName !== "transform")
418
+ return;
419
+ if (open) return;
420
+ const pending = pendingRef.current;
421
+ if (!pending) return;
422
+ if (swapTimerRef.current) {
423
+ clearTimeout(swapTimerRef.current);
424
+ swapTimerRef.current = null;
425
+ }
426
+ setView(pending.payload);
427
+ setApplied(pending.key);
428
+ pendingRef.current = null;
429
+ },
430
+ [open],
431
+ );
432
+
433
+ /* -------------------------------- Swipe ----------------------------------- */
434
+
435
+ const SWIPE_DISMISS = 30;
436
+ const SWIPE_MAX = 20;
437
+ const buttonRef = useRef<HTMLButtonElement>(null);
438
+ const pointerStartRef = useRef<number | null>(null);
439
+ const onDismissRef = useRef(onDismiss);
440
+ onDismissRef.current = onDismiss;
441
+
442
+ useEffect(() => {
443
+ const el = buttonRef.current;
444
+ if (!el) return;
445
+
446
+ const onMove = (e: PointerEvent) => {
447
+ if (pointerStartRef.current === null) return;
448
+ const dy = e.clientY - pointerStartRef.current;
449
+ const sign = dy > 0 ? 1 : -1;
450
+ const clamped = Math.min(Math.abs(dy), SWIPE_MAX) * sign;
451
+ el.style.transform = `translateY(${clamped}px)`;
452
+ };
453
+
454
+ const onUp = (e: PointerEvent) => {
455
+ if (pointerStartRef.current === null) return;
456
+ const dy = e.clientY - pointerStartRef.current;
457
+ pointerStartRef.current = null;
458
+ el.style.transform = "";
459
+ if (Math.abs(dy) > SWIPE_DISMISS) {
460
+ onDismissRef.current?.();
461
+ }
462
+ };
463
+
464
+ el.addEventListener("pointermove", onMove, { passive: true });
465
+ el.addEventListener("pointerup", onUp, { passive: true });
466
+ return () => {
467
+ el.removeEventListener("pointermove", onMove);
468
+ el.removeEventListener("pointerup", onUp);
469
+ };
470
+ }, []);
471
+
472
+ const handlePointerDown = useCallback(
473
+ (e: React.PointerEvent<HTMLButtonElement>) => {
474
+ if (exiting || !onDismiss) return;
475
+ const target = e.target as HTMLElement;
476
+ if (target.closest("[data-gooey-button]")) return;
477
+ pointerStartRef.current = e.clientY;
478
+ e.currentTarget.setPointerCapture(e.pointerId);
479
+ },
480
+ [exiting, onDismiss],
481
+ );
482
+
483
+ /* --------------------------------- Render --------------------------------- */
484
+
485
+ return (
486
+ <button
487
+ ref={buttonRef}
488
+ type="button"
489
+ data-gooey-toast
490
+ data-ready={ready}
491
+ data-expanded={open}
492
+ data-exiting={exiting}
493
+ data-edge={expand}
494
+ data-position={position}
495
+ data-state={view.state}
496
+ className={className}
497
+ style={rootStyle}
498
+ onMouseEnter={handleEnter}
499
+ onMouseLeave={handleLeave}
500
+ onTransitionEnd={handleTransitionEnd}
501
+ onPointerDown={handlePointerDown}
502
+ >
503
+ <div data-gooey-canvas data-edge={expand}>
504
+ <svg
505
+ data-gooey-svg
506
+ width={WIDTH}
507
+ height={svgHeight}
508
+ viewBox={`0 0 ${WIDTH} ${svgHeight}`}
509
+ >
510
+ <title>Gooey Notification</title>
511
+ <GooeyDefs filterId={filterId} blur={blur} />
512
+ <g filter={`url(#${filterId})`}>
513
+ <rect
514
+ data-gooey-pill
515
+ x={pillX}
516
+ rx={resolvedRoundness}
517
+ ry={resolvedRoundness}
518
+ style={{ fill: view.fill || "var(--gooey-bg)" }}
519
+ />
520
+ <rect
521
+ data-gooey-body
522
+ y={HEIGHT}
523
+ width={WIDTH}
524
+ height={expandedContent}
525
+ rx={resolvedRoundness}
526
+ ry={resolvedRoundness}
527
+ style={{ fill: view.fill || "var(--gooey-bg)" }}
528
+ />
529
+ </g>
530
+ </svg>
531
+ </div>
532
+
533
+ <div ref={headerRef} data-gooey-header data-edge={expand}>
534
+ <div data-gooey-header-stack>
535
+ <div
536
+ ref={innerRef}
537
+ key={headerLayer.current.key}
538
+ data-gooey-header-inner
539
+ data-layer="current"
540
+ >
541
+ <div
542
+ data-gooey-badge
543
+ data-state={headerLayer.current.view.state}
544
+ className={headerLayer.current.view.styles?.badge}
545
+ >
546
+ {headerLayer.current.view.icon ??
547
+ STATE_ICON[headerLayer.current.view.state]}
548
+ </div>
549
+ <span
550
+ data-gooey-title
551
+ data-state={headerLayer.current.view.state}
552
+ className={headerLayer.current.view.styles?.title}
553
+ >
554
+ {headerLayer.current.view.title}
555
+ </span>
556
+ </div>
557
+ {headerLayer.prev && (
558
+ <div
559
+ key={headerLayer.prev.key}
560
+ data-gooey-header-inner
561
+ data-layer="prev"
562
+ data-exiting="true"
563
+ >
564
+ <div
565
+ data-gooey-badge
566
+ data-state={headerLayer.prev.view.state}
567
+ className={headerLayer.prev.view.styles?.badge}
568
+ >
569
+ {headerLayer.prev.view.icon ??
570
+ STATE_ICON[headerLayer.prev.view.state]}
571
+ </div>
572
+ <span
573
+ data-gooey-title
574
+ data-state={headerLayer.prev.view.state}
575
+ className={headerLayer.prev.view.styles?.title}
576
+ >
577
+ {headerLayer.prev.view.title}
578
+ </span>
579
+ </div>
580
+ )}
581
+ </div>
582
+ </div>
583
+
584
+ {hasDesc && (
585
+ <div data-gooey-content data-edge={expand} data-visible={open}>
586
+ <div
587
+ ref={contentRef}
588
+ data-gooey-description
589
+ className={view.styles?.description}
590
+ >
591
+ {view.description}
592
+ {view.button && (
593
+ // biome-ignore lint/a11y/useValidAnchor: cannot use button inside a button
594
+ <a
595
+ href="#"
596
+ type="button"
597
+ data-gooey-button
598
+ data-state={view.state}
599
+ className={view.styles?.button}
600
+ onClick={(e) => {
601
+ e.preventDefault();
602
+ e.stopPropagation();
603
+ view.button?.onClick();
604
+ }}
605
+ >
606
+ {view.button.title}
607
+ </a>
608
+ )}
609
+ </div>
610
+ </div>
611
+ )}
612
+ </button>
613
+ );
614
+ });
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type GooeyState =
4
+ | "success"
5
+ | "loading"
6
+ | "error"
7
+ | "warning"
8
+ | "info"
9
+ | "action";
10
+
11
+ export interface GooeyStyles {
12
+ title?: string;
13
+ description?: string;
14
+ badge?: string;
15
+ button?: string;
16
+ }
17
+
18
+ export interface GooeyButton {
19
+ title: string;
20
+ onClick: () => void;
21
+ }
22
+
23
+ export const GOOEY_POSITIONS = [
24
+ "top-left",
25
+ "top-center",
26
+ "top-right",
27
+ "bottom-left",
28
+ "bottom-center",
29
+ "bottom-right",
30
+ ] as const;
31
+
32
+ export type GooeyPosition = (typeof GOOEY_POSITIONS)[number];
33
+
34
+ export interface GooeyOptions {
35
+ title?: string;
36
+ description?: ReactNode | string;
37
+ position?: GooeyPosition;
38
+ duration?: number | null;
39
+ icon?: ReactNode | null;
40
+ styles?: GooeyStyles;
41
+ fill?: string;
42
+ roundness?: number;
43
+ autopilot?: boolean | { expand?: number; collapse?: number };
44
+ button?: GooeyButton;
45
+ }