jaml-ui 0.21.2 → 0.21.4

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 (119) hide show
  1. package/DESIGN.md +36 -6
  2. package/dist/components/JamlAnalyzerFullscreen.d.ts +1 -1
  3. package/dist/components/JamlAnalyzerFullscreen.js +5 -81
  4. package/dist/components/JamlCurator.js +1 -1
  5. package/dist/components/JamlSpeedometer.d.ts +7 -2
  6. package/dist/components/JamlSpeedometer.js +8 -15
  7. package/dist/components/jamlMap/JamlMapEditor.js +42 -38
  8. package/dist/components/jamlMap/JokerPicker.js +2 -2
  9. package/dist/components/jamlMap/MysterySlot.js +4 -4
  10. package/dist/hooks/useSearch.d.ts +2 -1
  11. package/dist/hooks/useSearch.js +111 -8
  12. package/dist/lib/SpriteMapper.d.ts +10 -0
  13. package/dist/lib/SpriteMapper.js +48 -0
  14. package/dist/lib/cardParser.d.ts +8 -0
  15. package/dist/lib/cardParser.js +65 -0
  16. package/dist/lib/classes/BuyMetaData.d.ts +11 -0
  17. package/dist/lib/classes/BuyMetaData.js +1 -0
  18. package/dist/lib/config.d.ts +13 -0
  19. package/dist/lib/config.js +15 -0
  20. package/dist/lib/const.d.ts +61 -0
  21. package/dist/lib/const.js +521 -0
  22. package/dist/lib/data/constants.d.ts +11 -0
  23. package/dist/lib/data/constants.js +17 -0
  24. package/dist/lib/hooks/useDragScroll.d.ts +4 -0
  25. package/dist/lib/hooks/useDragScroll.js +48 -0
  26. package/dist/lib/hooks/useJamlFilter.d.ts +48 -0
  27. package/dist/lib/hooks/useJamlFilter.js +219 -0
  28. package/dist/lib/hooks/useSeedAnalyzer.d.ts +6 -0
  29. package/dist/lib/hooks/useSeedAnalyzer.js +48 -0
  30. package/dist/lib/jaml/jamlCompletion.d.ts +12 -0
  31. package/dist/lib/jaml/jamlCompletion.js +13 -0
  32. package/dist/lib/jaml/jamlData.d.ts +3 -0
  33. package/dist/lib/jaml/jamlData.js +8 -0
  34. package/dist/lib/jaml/jamlObjectives.d.ts +13 -0
  35. package/dist/lib/jaml/jamlObjectives.js +97 -0
  36. package/dist/lib/jaml/jamlParser.d.ts +14 -0
  37. package/dist/lib/jaml/jamlParser.js +47 -0
  38. package/dist/lib/jaml/jamlPresets.d.ts +8 -0
  39. package/dist/lib/jaml/jamlPresets.js +61 -0
  40. package/dist/lib/jaml/jamlSchema.d.ts +54 -0
  41. package/dist/lib/jaml/jamlSchema.js +91 -0
  42. package/dist/lib/parseDailyRitual.d.ts +45 -0
  43. package/dist/lib/parseDailyRitual.js +69 -0
  44. package/dist/lib/tts/getRevealPos.d.ts +5 -0
  45. package/dist/lib/tts/getRevealPos.js +16 -0
  46. package/dist/lib/tts/splitTtsDisplay.d.ts +19 -0
  47. package/dist/lib/tts/splitTtsDisplay.js +35 -0
  48. package/dist/lib/types.d.ts +121 -0
  49. package/dist/lib/types.js +1 -0
  50. package/dist/lib/utils.d.ts +2 -0
  51. package/dist/lib/utils.js +5 -0
  52. package/dist/ui/JimboIconButton.d.ts +10 -0
  53. package/dist/ui/JimboIconButton.js +28 -0
  54. package/dist/ui/JimboInputModal.d.ts +13 -0
  55. package/dist/ui/JimboInputModal.js +60 -0
  56. package/dist/ui/JimboSelect.d.ts +18 -0
  57. package/dist/ui/JimboSelect.js +43 -0
  58. package/dist/ui/PanelSplitter.d.ts +7 -0
  59. package/dist/ui/PanelSplitter.js +76 -0
  60. package/dist/ui/ide/AgnosticSeedCard.d.ts +19 -0
  61. package/dist/ui/ide/AgnosticSeedCard.js +48 -0
  62. package/dist/ui/ide/DeckSprite.d.ts +1 -0
  63. package/dist/ui/ide/DeckSprite.js +2 -0
  64. package/dist/ui/ide/JamlBuilder.d.ts +1 -0
  65. package/dist/ui/ide/JamlBuilder.js +112 -0
  66. package/dist/ui/ide/JamlEditor.d.ts +7 -0
  67. package/dist/ui/ide/JamlEditor.js +496 -0
  68. package/dist/ui/ide/JamlEditorMonaco.d.ts +8 -0
  69. package/dist/ui/ide/JamlEditorMonaco.js +78 -0
  70. package/dist/ui/ide/WasmStatus.d.ts +1 -0
  71. package/dist/ui/ide/WasmStatus.js +42 -0
  72. package/dist/ui/jimbo.css +336 -31
  73. package/dist/ui/jimboApp.d.ts +12 -0
  74. package/dist/ui/jimboApp.js +15 -0
  75. package/dist/ui/jimboInfoCard.d.ts +31 -0
  76. package/dist/ui/jimboInfoCard.js +26 -0
  77. package/dist/ui/jimboInset.d.ts +9 -0
  78. package/dist/ui/jimboInset.js +9 -0
  79. package/dist/ui/jimboSectionHeader.d.ts +11 -0
  80. package/dist/ui/jimboSectionHeader.js +9 -0
  81. package/dist/ui/jimboStatGrid.d.ts +13 -0
  82. package/dist/ui/jimboStatGrid.js +9 -0
  83. package/dist/ui/jimboWordmark.d.ts +10 -0
  84. package/dist/ui/jimboWordmark.js +9 -0
  85. package/dist/ui/mascot/JammySpeechBox.d.ts +9 -0
  86. package/dist/ui/mascot/JammySpeechBox.js +30 -0
  87. package/dist/ui/mascot/SeedMascot.d.ts +37 -0
  88. package/dist/ui/mascot/SeedMascot.js +17 -0
  89. package/dist/ui/mascot/index.d.ts +3 -0
  90. package/dist/ui/mascot/index.js +3 -0
  91. package/dist/ui/mascot/menuConfig.d.ts +102 -0
  92. package/dist/ui/mascot/menuConfig.js +12 -0
  93. package/dist/ui/panel.d.ts +1 -1
  94. package/dist/ui/panel.js +3 -21
  95. package/dist/ui/radial/RadialBadge.d.ts +17 -0
  96. package/dist/ui/radial/RadialBadge.js +43 -0
  97. package/dist/ui/radial/RadialBreadcrumb.d.ts +12 -0
  98. package/dist/ui/radial/RadialBreadcrumb.js +18 -0
  99. package/dist/ui/radial/RadialButton.d.ts +61 -0
  100. package/dist/ui/radial/RadialButton.js +102 -0
  101. package/dist/ui/radial/RadialMenu.d.ts +38 -0
  102. package/dist/ui/radial/RadialMenu.js +168 -0
  103. package/dist/ui/radial/RadialPill.d.ts +18 -0
  104. package/dist/ui/radial/RadialPill.js +15 -0
  105. package/dist/ui/radial/index.d.ts +16 -0
  106. package/dist/ui/radial/index.js +18 -0
  107. package/dist/ui/radial/radialMenuStore.d.ts +31 -0
  108. package/dist/ui/radial/radialMenuStore.js +122 -0
  109. package/dist/ui/radial/radialMenuViewport.d.ts +6 -0
  110. package/dist/ui/radial/radialMenuViewport.js +59 -0
  111. package/dist/ui/radial/useRadialMenu.d.ts +35 -0
  112. package/dist/ui/radial/useRadialMenu.js +107 -0
  113. package/dist/ui/showcase.d.ts +14 -6
  114. package/dist/ui/showcase.js +13 -21
  115. package/dist/ui/tokens.d.ts +5 -19
  116. package/dist/ui/tokens.js +5 -21
  117. package/dist/ui.d.ts +14 -0
  118. package/dist/ui.js +15 -0
  119. package/package.json +145 -146
@@ -0,0 +1,168 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+ import { RadialPill } from "./RadialPill";
5
+ import { RadialButton } from "./RadialButton";
6
+ import { RadialBadge } from "./RadialBadge";
7
+ // ── Type guards ───────────────────────────────────────────────────────────────
8
+ function hasDim(item) {
9
+ return "_dim" in item;
10
+ }
11
+ function hasSouth(item) {
12
+ return "_south" in item && item._south === true;
13
+ }
14
+ function hasIcon(item) {
15
+ return "icon" in item && typeof item.icon === "string";
16
+ }
17
+ function hasCount(item) {
18
+ return "count" in item && typeof item.count === "number";
19
+ }
20
+ function isToggleItem(item) {
21
+ return "active" in item && typeof item.active === "boolean";
22
+ }
23
+ // ── Orbit geometry constants ──────────────────────────────────────────────────
24
+ /** Degrees reserved on each side of south (90°) for the south-pinned button. */
25
+ const FULL_CIRCLE_RAD = 2 * Math.PI;
26
+ const SOUTH_ANGLE_RAD = Math.PI / 2;
27
+ /** ~half height of a radial pill (px) — used with width for clearance vs round mascot. */
28
+ const PILL_HALF_H = 15;
29
+ /**
30
+ * Estimated pill width for clamp math — tuned for m6x11 ~12px + padding + toggle dot.
31
+ */
32
+ function estimatePillWidth(item) {
33
+ const label = item.label || "";
34
+ const charW = 7.25;
35
+ const basePad = 36;
36
+ const hasToggle = "active" in item && typeof item.active === "boolean";
37
+ const dot = hasToggle ? 16 : 0;
38
+ const w = basePad + dot + label.length * charW;
39
+ return Math.min(200, Math.max(56, w));
40
+ }
41
+ /**
42
+ * Minimum distance from orbit center to pill center so the pill clears the round mascot bitmap.
43
+ */
44
+ function minDistanceFromMascotCenter(mascotHalfPx, pillHalfW) {
45
+ const gap = 10;
46
+ const cornerReach = Math.hypot(pillHalfW, PILL_HALF_H);
47
+ return mascotHalfPx + gap + cornerReach;
48
+ }
49
+ /**
50
+ * Even ellipse; if a slot sits inside the mascot keep-out, scale **outward** along the same ray
51
+ * (`k ≥ 1`). We do **not** clamp X toward 0 — that was pulling pills **inward** onto Jammy.
52
+ * Edge overflow is handled by orbit radius + narrow column in `SeedMascot` / layout, not here.
53
+ */
54
+ function layoutOrbitalEllipse(items, Rx, Ry, startAngle, stepRad, mascotSizePx) {
55
+ if (items.length === 0)
56
+ return [];
57
+ const mascotHalf = Math.max(0, mascotSizePx) / 2;
58
+ return items.map((item, i) => {
59
+ const angle = startAngle + i * stepRad;
60
+ const ux = Math.cos(angle) * Rx;
61
+ const uy = Math.sin(angle) * Ry;
62
+ const d0 = Math.hypot(ux, uy);
63
+ const halfW = estimatePillWidth(item) / 2;
64
+ const minDist = minDistanceFromMascotCenter(mascotHalf, halfW);
65
+ if (d0 < 1e-4) {
66
+ return { item, x: minDist, y: 0 };
67
+ }
68
+ const k = Math.max(1, minDist / d0);
69
+ const x = ux * k;
70
+ const y = uy * k;
71
+ return { item, x, y };
72
+ });
73
+ }
74
+ /**
75
+ * Orbital radial menu layout engine.
76
+ *
77
+ * South (`_south`) stays pinned under the mascot. Everyone else sits on an ellipse
78
+ * at equal angle steps, with a single radial push if a pill would overlap the
79
+ * center — no iterative “solver” (that was causing mushy, uneven layouts).
80
+ */
81
+ export function RadialMenu({ items, showClosing, mascotSizePx, orbitRadiusX, orbitRadiusY, mascotTranslateY, currentMenu, onItemClick, onBack, breadcrumb, showPageControls = false, onPagePrev, onPageNext, }) {
82
+ const rootRef = useRef(null);
83
+ const [containerW, setContainerW] = useState(375);
84
+ useLayoutEffect(() => {
85
+ const el = rootRef.current;
86
+ if (!el)
87
+ return;
88
+ const apply = () => setContainerW(Math.max(1, el.clientWidth));
89
+ apply();
90
+ const ro = new ResizeObserver(apply);
91
+ ro.observe(el);
92
+ return () => ro.disconnect();
93
+ }, []);
94
+ if (items.length === 0)
95
+ return null;
96
+ // ── Separate south-pinned item from orbital items ─────────────────────────
97
+ const southItem = items.find(hasSouth) ?? null;
98
+ const orbitalItems = items.filter((item) => !hasSouth(item));
99
+ const totalSlots = orbitalItems.length + (southItem ? 1 : 0);
100
+ const slotStepRad = FULL_CIRCLE_RAD / totalSlots;
101
+ const orbitalStartAngle = southItem ? SOUTH_ANGLE_RAD + slotStepRad : SOUTH_ANGLE_RAD;
102
+ // South button (Start / Back) — always clearly wider than any orbital pill.
103
+ // Minimum 160px so it reads as the primary action even with few orbital items.
104
+ const MIN_SOUTH_WIDTH = 148;
105
+ const southWidth = Math.max(MIN_SOUTH_WIDTH, totalSlots <= 1
106
+ ? Math.min(Math.round(2 * orbitRadiusX * 1.05), containerW - 16)
107
+ : Math.min(Math.round(2 * orbitRadiusX * Math.sin(slotStepRad / 2) * 1.28), containerW - 16));
108
+ // Breadcrumb above south (Back): fixed ctr–ctr gap so it stays attached above the southmost pill.
109
+ const southButtonY = orbitRadiusY + 8;
110
+ const breadcrumbCenterY = southButtonY - 46;
111
+ const pageControlWidth = Math.max(56, Math.floor(southWidth * 0.5));
112
+ const pageControlY = southButtonY - 20;
113
+ const pageControlLeftX = -(southWidth / 2) + pageControlWidth / 2;
114
+ const pageControlRightX = (southWidth / 2) - pageControlWidth / 2;
115
+ const makeClickHandler = (item) => (e) => {
116
+ e.stopPropagation();
117
+ if (item.action === "back-action") {
118
+ onBack();
119
+ }
120
+ else {
121
+ onItemClick(item);
122
+ }
123
+ };
124
+ return (_jsx("div", { ref: rootRef, className: "pointer-events-none absolute inset-0 z-30 flex min-w-0 items-center justify-center", style: { transform: `translateY(${mascotTranslateY}px)` }, children: _jsxs("div", { className: "relative h-0 w-0", children: [breadcrumb ? (_jsx("div", { className: "pointer-events-auto absolute", style: {
125
+ left: 0,
126
+ top: breadcrumbCenterY,
127
+ transform: "translate(-50%, -50%)",
128
+ zIndex: 20,
129
+ }, children: breadcrumb })) : null, (() => {
130
+ const solved = layoutOrbitalEllipse(orbitalItems, orbitRadiusX, orbitRadiusY, orbitalStartAngle, slotStepRad, mascotSizePx);
131
+ return solved.map(({ item, x, y }, i) => {
132
+ const isDim = hasDim(item) && item._dim === true;
133
+ const extraClass = isDim ? "opacity-40" : "";
134
+ return (_jsx(RadialPill, { x: x, y: y, hiding: showClosing, children: renderItem(item, extraClass, makeClickHandler(item)) }, `${item.label}-${i}-${currentMenu}`));
135
+ });
136
+ })(), southItem && (_jsx(RadialPill, { x: 0, y: southButtonY, hiding: showClosing, children: renderSouthItem(southItem, southWidth, makeClickHandler(southItem)) })), southItem && showPageControls && (_jsxs(_Fragment, { children: [_jsx(RadialPill, { x: pageControlLeftX, y: pageControlY, hiding: showClosing, children: _jsx(RadialButton, { label: "<", color: "blue", tooltip: "Previous page", onClick: (e) => {
137
+ e.stopPropagation();
138
+ onPagePrev?.();
139
+ }, style: { width: pageControlWidth } }) }), _jsx(RadialPill, { x: pageControlRightX, y: pageControlY, hiding: showClosing, children: _jsx(RadialButton, { label: ">", color: "blue", tooltip: "Next page", onClick: (e) => {
140
+ e.stopPropagation();
141
+ onPageNext?.();
142
+ }, style: { width: pageControlWidth } }) })] }))] }) }));
143
+ }
144
+ // ── South item renderer ───────────────────────────────────────────────────────
145
+ function renderSouthItem(item, width, onClick) {
146
+ const variant = item.label === "Start" ? "start" : "back";
147
+ return (_jsx(RadialButton, { variant: variant, label: item.label, color: "orange", tooltip: item.tooltip, onClick: onClick, style: { width } }));
148
+ }
149
+ // ── Regular item renderer ─────────────────────────────────────────────────────
150
+ function renderItem(item, className, onClick) {
151
+ // Badge (display-only)
152
+ if (item.badge) {
153
+ return (_jsx(RadialBadge, { label: item.badge.label, state: item.badge.state, color: item.color, tooltip: item.tooltip }));
154
+ }
155
+ // Toggle
156
+ if (isToggleItem(item)) {
157
+ return (_jsx(RadialButton, { variant: "toggle", label: item.label, active: item.active, disabled: item.disabled === true, color: item.color, tooltip: item.tooltip, className: className, onClick: onClick }));
158
+ }
159
+ // Count indicator (with icon or numeric count)
160
+ if (hasIcon(item)) {
161
+ return (_jsx(RadialButton, { variant: "count", label: item.label, count: 0, icon: item.icon, color: item.color, tooltip: item.tooltip, className: className, onClick: onClick }));
162
+ }
163
+ if (hasCount(item)) {
164
+ return (_jsx(RadialButton, { variant: "count", label: item.label, count: item.count, color: item.color, tooltip: item.tooltip, className: className, onClick: onClick }));
165
+ }
166
+ // Default action button
167
+ return (_jsx(RadialButton, { label: item.label, color: item.color, tooltip: item.tooltip, className: className, onClick: onClick }));
168
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import "./radial-navigation.css";
3
+ export interface RadialPillProps {
4
+ /** Horizontal offset from center (px). */
5
+ x: number;
6
+ /** Vertical offset from center (px). */
7
+ y: number;
8
+ /** When true, pill collapses to center (exit animation). */
9
+ hiding: boolean;
10
+ children: React.ReactNode;
11
+ }
12
+ /**
13
+ * Absolutely-positioned wrapper for a radial menu item.
14
+ *
15
+ * Uses CSS custom properties + a shared `@starting-style` rule
16
+ * instead of injecting per-instance `<style>` tags.
17
+ */
18
+ export declare function RadialPill({ x, y, hiding, children }: RadialPillProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import "./radial-navigation.css";
4
+ /**
5
+ * Absolutely-positioned wrapper for a radial menu item.
6
+ *
7
+ * Uses CSS custom properties + a shared `@starting-style` rule
8
+ * instead of injecting per-instance `<style>` tags.
9
+ */
10
+ export function RadialPill({ x, y, hiding, children }) {
11
+ return (_jsx("div", { className: "orbital-pill", "data-hiding": hiding ? "true" : "false", style: {
12
+ "--orbital-x": `${hiding ? 0 : x}px`,
13
+ "--orbital-y": `${hiding ? 0 : y}px`,
14
+ }, children: _jsx("span", { className: "orbital-pill-inner", children: children }) }));
15
+ }
@@ -0,0 +1,16 @@
1
+ export { RadialMenu } from "./RadialMenu";
2
+ export type { RadialMenuProps } from "./RadialMenu";
3
+ export { RadialPill } from "./RadialPill";
4
+ export type { RadialPillProps } from "./RadialPill";
5
+ export { RadialButton } from "./RadialButton";
6
+ export type { RadialButtonProps, RadialButtonColor, RadialButtonActionProps, RadialButtonToggleProps, RadialButtonCountProps, RadialButtonBackProps, } from "./RadialButton";
7
+ export { RadialBadge } from "./RadialBadge";
8
+ export type { RadialBadgeProps, RadialBadgeState } from "./RadialBadge";
9
+ export { RadialBreadcrumb } from "./RadialBreadcrumb";
10
+ export type { RadialBreadcrumbProps } from "./RadialBreadcrumb";
11
+ export { useRadialMenu } from "./useRadialMenu";
12
+ export type { UseRadialMenuProps, RadialMenuState } from "./useRadialMenu";
13
+ export { RadialButton as JimboRadialNavigationButton } from "./RadialButton";
14
+ export { RadialBadge as JimboRadialNavigationBadge } from "./RadialBadge";
15
+ export { RadialBreadcrumb as BreadcrumNavPill } from "./RadialBreadcrumb";
16
+ export { RadialPill as JimboOrbitalPill } from "./RadialPill";
@@ -0,0 +1,18 @@
1
+ // Jimbo UI — Radial Navigation Module
2
+ // Orbital/radial menu system for the Jammy mascot.
3
+ // Layout
4
+ export { RadialMenu } from "./RadialMenu";
5
+ // Primitives
6
+ export { RadialPill } from "./RadialPill";
7
+ export { RadialButton } from "./RadialButton";
8
+ export { RadialBadge } from "./RadialBadge";
9
+ export { RadialBreadcrumb } from "./RadialBreadcrumb";
10
+ // State hook
11
+ export { useRadialMenu } from "./useRadialMenu";
12
+ // ── Backwards-compatibility aliases ───────────────────────────────────────────
13
+ // These match the old export names from the flat RadialNavigation.tsx file.
14
+ // Consumers using the old names will keep working without import changes.
15
+ export { RadialButton as JimboRadialNavigationButton } from "./RadialButton";
16
+ export { RadialBadge as JimboRadialNavigationBadge } from "./RadialBadge";
17
+ export { RadialBreadcrumb as BreadcrumNavPill } from "./RadialBreadcrumb";
18
+ export { RadialPill as JimboOrbitalPill } from "./RadialPill";
@@ -0,0 +1,31 @@
1
+ /** Clears pending close/navigate animation timeout (e.g. unmount). */
2
+ export declare function clearRadialMenuTimers(): void;
3
+ /** Full reset when `SeedMascot` unmounts — Zustand survives the component; hook `useState` did not. */
4
+ export declare function resetRadialMenuState(): void;
5
+ export interface RadialMenuNav {
6
+ stack: string[];
7
+ page: number;
8
+ }
9
+ interface RadialMenuSlice {
10
+ nav: RadialMenuNav;
11
+ breadcrumbStack: string[];
12
+ isClosing: boolean;
13
+ announcementPrimedForDismiss: boolean;
14
+ prevMaxScore: number;
15
+ prevResultCount: number;
16
+ }
17
+ export interface RadialMenuStore extends RadialMenuSlice {
18
+ open: (isWelcome: boolean) => void;
19
+ close: () => void;
20
+ back: () => void;
21
+ navigateTo: (submenuLabel: string) => void;
22
+ nextPage: (totalPages: number) => void;
23
+ prevPage: (totalPages: number) => void;
24
+ setAnnouncementPrimedForDismiss: (v: boolean) => void;
25
+ resetAnnouncementPrimed: () => void;
26
+ updateSeedResults: (seedResults: Array<{
27
+ score: number;
28
+ }>) => void;
29
+ }
30
+ export declare const useRadialMenuStore: import("zustand").UseBoundStore<import("zustand").StoreApi<RadialMenuStore>>;
31
+ export {};
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import { create } from "zustand";
3
+ import { JIMBO_ANIMATIONS } from "../tokens";
4
+ let closingTimer = null;
5
+ /** Clears pending close/navigate animation timeout (e.g. unmount). */
6
+ export function clearRadialMenuTimers() {
7
+ if (closingTimer !== null) {
8
+ clearTimeout(closingTimer);
9
+ closingTimer = null;
10
+ }
11
+ }
12
+ /** Full reset when `SeedMascot` unmounts — Zustand survives the component; hook `useState` did not. */
13
+ export function resetRadialMenuState() {
14
+ clearRadialMenuTimers();
15
+ useRadialMenuStore.setState({
16
+ nav: { stack: [], page: 0 },
17
+ breadcrumbStack: [],
18
+ isClosing: false,
19
+ announcementPrimedForDismiss: false,
20
+ });
21
+ }
22
+ function armTimer(fn, ms) {
23
+ clearRadialMenuTimers();
24
+ closingTimer = setTimeout(() => {
25
+ closingTimer = null;
26
+ fn();
27
+ }, ms);
28
+ }
29
+ export const useRadialMenuStore = create((set, get) => ({
30
+ nav: { stack: [], page: 0 },
31
+ breadcrumbStack: [],
32
+ isClosing: false,
33
+ announcementPrimedForDismiss: false,
34
+ prevMaxScore: -1,
35
+ prevResultCount: 0,
36
+ resetAnnouncementPrimed: () => {
37
+ if (!get().announcementPrimedForDismiss)
38
+ return;
39
+ set({ announcementPrimedForDismiss: false });
40
+ },
41
+ setAnnouncementPrimedForDismiss: (v) => {
42
+ if (get().announcementPrimedForDismiss === v)
43
+ return;
44
+ set({ announcementPrimedForDismiss: v });
45
+ },
46
+ open: (isWelcome) => {
47
+ const { nav } = get();
48
+ const target = isWelcome ? "welcome" : "main";
49
+ // Guard: skip if already at the target root — avoids creating a new array
50
+ // reference that would trigger Zustand subscribers and cause infinite re-renders
51
+ // in effects that depend on the radial menu state object.
52
+ if (nav.stack.length === 1 && nav.stack[0] === target && nav.page === 0)
53
+ return;
54
+ set({ nav: { stack: [target], page: 0 } });
55
+ },
56
+ close: () => {
57
+ clearRadialMenuTimers();
58
+ set({ isClosing: true });
59
+ armTimer(() => {
60
+ set({ nav: { stack: [], page: 0 }, breadcrumbStack: [], isClosing: false });
61
+ }, JIMBO_ANIMATIONS.MENU_ORBIT_DURATION);
62
+ },
63
+ back: () => {
64
+ const stackBefore = get().nav.stack;
65
+ set((s) => ({ breadcrumbStack: s.breadcrumbStack.slice(0, -1) }));
66
+ if (stackBefore.length <= 1) {
67
+ get().close();
68
+ return;
69
+ }
70
+ clearRadialMenuTimers();
71
+ set({ isClosing: true });
72
+ armTimer(() => {
73
+ set((s) => ({
74
+ nav: { stack: s.nav.stack.slice(0, -1), page: 0 },
75
+ isClosing: false,
76
+ }));
77
+ }, JIMBO_ANIMATIONS.MENU_SINK_DURATION);
78
+ },
79
+ navigateTo: (submenuLabel) => {
80
+ clearRadialMenuTimers();
81
+ set((s) => ({
82
+ breadcrumbStack: [...s.breadcrumbStack, submenuLabel],
83
+ isClosing: true,
84
+ }));
85
+ armTimer(() => {
86
+ set((s) => ({
87
+ nav: { stack: [...s.nav.stack, submenuLabel], page: 0 },
88
+ isClosing: false,
89
+ }));
90
+ }, JIMBO_ANIMATIONS.MENU_SINK_DURATION);
91
+ },
92
+ nextPage: (totalPages) => set((s) => {
93
+ const pages = Math.max(1, totalPages);
94
+ const next = pages <= 1 ? 0 : (s.nav.page + 1) % pages;
95
+ return { nav: { ...s.nav, page: next } };
96
+ }),
97
+ prevPage: (totalPages) => set((s) => {
98
+ const pages = Math.max(1, totalPages);
99
+ const prev = pages <= 1 ? 0 : (s.nav.page - 1 + pages) % pages;
100
+ return { nav: { ...s.nav, page: prev } };
101
+ }),
102
+ updateSeedResults: (seedResults) => {
103
+ const state = get();
104
+ let shouldClose = false;
105
+ if (seedResults.length > 0) {
106
+ const currentMax = Math.max(...seedResults.map((r) => r.score));
107
+ if (currentMax > state.prevMaxScore) {
108
+ shouldClose = state.nav.stack.length > 1;
109
+ }
110
+ }
111
+ if (seedResults.length > 0 && seedResults.length > state.prevResultCount) {
112
+ shouldClose = shouldClose || state.nav.stack.length > 0;
113
+ }
114
+ set({
115
+ prevMaxScore: seedResults.length > 0 ? Math.max(...seedResults.map(r => r.score)) : -1,
116
+ prevResultCount: seedResults.length
117
+ });
118
+ if (shouldClose) {
119
+ get().close();
120
+ }
121
+ },
122
+ }));
@@ -0,0 +1,6 @@
1
+ export interface RadialViewportSnapshot {
2
+ baseRadius: number;
3
+ keyboardHeight: number;
4
+ }
5
+ /** Keyboard dodge + narrow-orbit radius — `useSyncExternalStore` (stable snapshot identity). */
6
+ export declare function useRadialViewportGeometry(): RadialViewportSnapshot;
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { useSyncExternalStore } from "react";
3
+ /**
4
+ * React requires `getSnapshot` to return the **same object reference** when values are unchanged.
5
+ * A fresh `{ ... }` every call makes `useSyncExternalStore` think the store changed every render →
6
+ * maximum update depth. See: https://react.dev/reference/react/useSyncExternalStore
7
+ */
8
+ const SERVER_SNAPSHOT = Object.freeze({
9
+ baseRadius: 66,
10
+ keyboardHeight: 0,
11
+ });
12
+ const clientCache = {
13
+ baseRadius: SERVER_SNAPSHOT.baseRadius,
14
+ keyboardHeight: SERVER_SNAPSHOT.keyboardHeight,
15
+ };
16
+ function getServerSnapshot() {
17
+ return SERVER_SNAPSHOT;
18
+ }
19
+ function readGeometry() {
20
+ if (typeof window === "undefined") {
21
+ return { baseRadius: 66, keyboardHeight: 0 };
22
+ }
23
+ const w = window.innerWidth;
24
+ const baseRadius = w < 375 ? 58 : 66;
25
+ const vv = window.visualViewport;
26
+ let keyboardHeight = 0;
27
+ if (vv) {
28
+ const raw = Math.max(0, window.innerHeight - vv.height);
29
+ keyboardHeight = raw > 100 ? raw * 0.6 : 0;
30
+ }
31
+ return { baseRadius, keyboardHeight };
32
+ }
33
+ function getSnapshot() {
34
+ if (typeof window === "undefined")
35
+ return SERVER_SNAPSHOT;
36
+ const { baseRadius, keyboardHeight } = readGeometry();
37
+ if (clientCache.baseRadius === baseRadius && clientCache.keyboardHeight === keyboardHeight) {
38
+ return clientCache;
39
+ }
40
+ clientCache.baseRadius = baseRadius;
41
+ clientCache.keyboardHeight = keyboardHeight;
42
+ return clientCache;
43
+ }
44
+ function subscribe(onStoreChange) {
45
+ if (typeof window === "undefined")
46
+ return () => { };
47
+ const run = () => onStoreChange();
48
+ const vv = window.visualViewport;
49
+ window.addEventListener("resize", run);
50
+ vv?.addEventListener("resize", run);
51
+ return () => {
52
+ window.removeEventListener("resize", run);
53
+ vv?.removeEventListener("resize", run);
54
+ };
55
+ }
56
+ /** Keyboard dodge + narrow-orbit radius — `useSyncExternalStore` (stable snapshot identity). */
57
+ export function useRadialViewportGeometry() {
58
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
59
+ }
@@ -0,0 +1,35 @@
1
+ export interface UseRadialMenuProps {
2
+ isWelcome: boolean;
3
+ announcementActive: boolean;
4
+ isTalking: boolean;
5
+ seedResults: {
6
+ seed: string;
7
+ score: number;
8
+ }[];
9
+ onDismissAnnouncement: () => void;
10
+ }
11
+ export interface RadialMenuState {
12
+ menuStack: string[];
13
+ breadcrumbStack: string[];
14
+ currentMenu: string;
15
+ isClosing: boolean;
16
+ showMenu: boolean;
17
+ showClosing: boolean;
18
+ showBack: boolean;
19
+ menuPage: number;
20
+ baseRadius: number;
21
+ keyboardHeight: number;
22
+ mascotTranslateY: number;
23
+ open: () => void;
24
+ close: () => void;
25
+ back: () => void;
26
+ navigateTo: (submenuLabel: string) => void;
27
+ nextPage: (totalPages: number) => void;
28
+ prevPage: (totalPages: number) => void;
29
+ handleTap: () => void;
30
+ }
31
+ /**
32
+ * Radial menu: **Zustand** (`useRadialMenuStore`) for stack / pagination / animations;
33
+ * **useSyncExternalStore** (`useRadialViewportGeometry`) for keyboard + narrow-width radius.
34
+ */
35
+ export declare function useRadialMenu({ isWelcome, announcementActive, isTalking, seedResults, onDismissAnnouncement, }: UseRadialMenuProps): RadialMenuState;
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useMemo } from "react";
3
+ import { useRadialMenuStore } from "./radialMenuStore";
4
+ import { useRadialViewportGeometry } from "./radialMenuViewport";
5
+ /**
6
+ * Radial menu: **Zustand** (`useRadialMenuStore`) for stack / pagination / animations;
7
+ * **useSyncExternalStore** (`useRadialViewportGeometry`) for keyboard + narrow-width radius.
8
+ */
9
+ export function useRadialMenu({ isWelcome, announcementActive, isTalking, seedResults, onDismissAnnouncement, }) {
10
+ const menuStack = useRadialMenuStore((s) => s.nav.stack);
11
+ const menuPage = useRadialMenuStore((s) => s.nav.page);
12
+ const breadcrumbStack = useRadialMenuStore((s) => s.breadcrumbStack);
13
+ const isClosing = useRadialMenuStore((s) => s.isClosing);
14
+ const storeOpen = useRadialMenuStore((s) => s.open);
15
+ const storeClose = useRadialMenuStore((s) => s.close);
16
+ const storeBack = useRadialMenuStore((s) => s.back);
17
+ const storeNavigateTo = useRadialMenuStore((s) => s.navigateTo);
18
+ const storeNextPage = useRadialMenuStore((s) => s.nextPage);
19
+ const storePrevPage = useRadialMenuStore((s) => s.prevPage);
20
+ const { baseRadius, keyboardHeight } = useRadialViewportGeometry();
21
+ useEffect(() => {
22
+ if (!announcementActive) {
23
+ useRadialMenuStore.getState().resetAnnouncementPrimed();
24
+ }
25
+ }, [announcementActive]);
26
+ const currentMenu = useMemo(() => (isWelcome ? "welcome" : menuStack.at(-1) ?? "main"), [isWelcome, menuStack]);
27
+ const showMenu = useMemo(() => !announcementActive && (menuStack.length > 0 || seedResults.length > 0) && !isClosing, [announcementActive, menuStack.length, seedResults.length, isClosing]);
28
+ const showClosing = useMemo(() => isClosing && menuStack.length > 0, [isClosing, menuStack.length]);
29
+ const showBack = useMemo(() => menuStack.length >= 1, [menuStack.length]);
30
+ const mascotTranslateY = useMemo(() => -keyboardHeight + 2, [keyboardHeight]);
31
+ const open = useCallback(() => {
32
+ storeOpen(isWelcome);
33
+ }, [isWelcome, storeOpen]);
34
+ const close = useCallback(() => {
35
+ storeClose();
36
+ }, [storeClose]);
37
+ const back = useCallback(() => {
38
+ storeBack();
39
+ }, [storeBack]);
40
+ const navigateTo = useCallback((submenuLabel) => {
41
+ storeNavigateTo(submenuLabel);
42
+ }, [storeNavigateTo]);
43
+ const nextPage = useCallback((totalPages) => {
44
+ storeNextPage(totalPages);
45
+ }, [storeNextPage]);
46
+ const prevPage = useCallback((totalPages) => {
47
+ storePrevPage(totalPages);
48
+ }, [storePrevPage]);
49
+ const handleTap = useCallback(() => {
50
+ const api = useRadialMenuStore.getState();
51
+ if (announcementActive) {
52
+ if (isTalking) {
53
+ return;
54
+ }
55
+ onDismissAnnouncement();
56
+ api.resetAnnouncementPrimed();
57
+ api.open(isWelcome);
58
+ return;
59
+ }
60
+ const menuIsOpen = api.nav.stack.length > 0;
61
+ if (!menuIsOpen && !api.isClosing) {
62
+ api.open(isWelcome);
63
+ }
64
+ else {
65
+ api.close();
66
+ }
67
+ }, [announcementActive, isTalking, isWelcome, onDismissAnnouncement]);
68
+ return useMemo(() => ({
69
+ menuStack,
70
+ breadcrumbStack,
71
+ currentMenu,
72
+ isClosing,
73
+ showMenu,
74
+ showClosing,
75
+ showBack,
76
+ menuPage,
77
+ baseRadius,
78
+ keyboardHeight,
79
+ mascotTranslateY,
80
+ open,
81
+ close,
82
+ back,
83
+ navigateTo,
84
+ nextPage,
85
+ prevPage,
86
+ handleTap,
87
+ }), [
88
+ menuStack,
89
+ breadcrumbStack,
90
+ currentMenu,
91
+ isClosing,
92
+ showMenu,
93
+ showClosing,
94
+ showBack,
95
+ menuPage,
96
+ baseRadius,
97
+ keyboardHeight,
98
+ mascotTranslateY,
99
+ open,
100
+ close,
101
+ back,
102
+ navigateTo,
103
+ nextPage,
104
+ prevPage,
105
+ handleTap,
106
+ ]);
107
+ }
@@ -1,8 +1,9 @@
1
+ import { type JimboSectionTone } from './jimboSectionHeader.js';
1
2
  export interface ShowcaseFilter {
2
3
  name: string;
3
4
  author: string;
4
5
  hits: string;
5
- tone: 'blue' | 'red' | 'gold' | 'green';
6
+ tone: JimboSectionTone;
6
7
  sample: string[];
7
8
  }
8
9
  export interface ShowcaseRecentFind {
@@ -15,16 +16,23 @@ export interface ShowcaseLiveStats {
15
16
  matches: string;
16
17
  speed: string;
17
18
  }
19
+ export interface ShowcaseMcpInfo {
20
+ runtime: string;
21
+ engine: string;
22
+ features: string;
23
+ }
18
24
  export interface ShowcaseProps {
25
+ title?: string;
26
+ subtitle?: string;
19
27
  hotFilters?: ShowcaseFilter[];
20
28
  recentFinds?: ShowcaseRecentFind[];
21
- stats?: ShowcaseLiveStats;
29
+ mcpInfo?: ShowcaseMcpInfo;
22
30
  onNewSearch?: () => void;
23
31
  onBrowseFilters?: () => void;
24
- onBack?: () => void;
32
+ onFilterClick?: (filter: ShowcaseFilter, index: number) => void;
25
33
  }
26
34
  /**
27
- * Landing/showcase screen for the seed curator.
28
- * All styling via jimbo.css `.j-showcase` classes zero inline styles.
35
+ * Landing/showcase screen 375×667, NO SCROLL.
36
+ * Every pixel accounted for. No flex stretching. No gaps.
29
37
  */
30
- export declare function Showcase({ hotFilters, recentFinds, stats, onNewSearch, onBrowseFilters, onBack, }: ShowcaseProps): import("react/jsx-runtime").JSX.Element;
38
+ export declare function Showcase({ title, subtitle, hotFilters, recentFinds, mcpInfo, onNewSearch, onBrowseFilters, onFilterClick, }: ShowcaseProps): import("react/jsx-runtime").JSX.Element;