@morphika/andami 0.1.3 → 0.1.5

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 (84) hide show
  1. package/app/(site)/[slug]/page.tsx +2 -2
  2. package/app/(site)/layout.tsx +1 -0
  3. package/app/(site)/page.tsx +2 -2
  4. package/app/(site)/preview/page.tsx +4 -4
  5. package/app/(site)/work/[slug]/page.tsx +2 -2
  6. package/app/admin/layout.tsx +2 -2
  7. package/app/admin/login/page.tsx +5 -5
  8. package/app/admin/navigation/page.tsx +255 -157
  9. package/app/api/admin/assets/relink/confirm/route.ts +1 -1
  10. package/app/api/admin/pages/[slug]/route.ts +1 -1
  11. package/app/api/admin/settings/route.ts +40 -15
  12. package/app/api/admin/setup/complete/route.ts +1 -1
  13. package/app/api/admin/setup/route.ts +6 -3
  14. package/components/admin/index.ts +7 -0
  15. package/components/admin/nav-builder/NavGeneralSettings.tsx +11 -15
  16. package/components/admin/nav-builder/NavItemSettings.tsx +29 -5
  17. package/components/admin/nav-builder/NavLivePreview.tsx +4 -1
  18. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -0
  19. package/components/admin/nav-builder/NavMobileSettings.tsx +223 -0
  20. package/components/admin/nav-builder/index.ts +2 -0
  21. package/components/blocks/BlockRenderer.tsx +65 -13
  22. package/components/blocks/ButtonBlockRenderer.tsx +29 -6
  23. package/components/blocks/CoverBlockRenderer.tsx +36 -14
  24. package/components/blocks/ImageBlockRenderer.tsx +5 -3
  25. package/components/blocks/ImageGridBlockRenderer.tsx +13 -6
  26. package/components/blocks/PageRenderer.tsx +4 -2
  27. package/components/blocks/ProjectGridBlockRenderer.tsx +18 -3
  28. package/components/blocks/SectionRenderer.tsx +9 -8
  29. package/components/blocks/SectionV2Renderer.tsx +8 -8
  30. package/components/blocks/SpacerBlockRenderer.tsx +4 -2
  31. package/components/blocks/TextBlockRenderer.tsx +9 -4
  32. package/components/builder/BuilderCanvas.tsx +10 -4
  33. package/components/builder/ColorPicker.tsx +51 -243
  34. package/components/builder/ColorSwatchPicker.tsx +214 -274
  35. package/components/builder/DndWrapper.tsx +5 -2
  36. package/components/builder/SectionV2Canvas.tsx +15 -4
  37. package/components/builder/asset-browser/useAssetBrowser.ts +9 -1
  38. package/components/builder/color-picker/AlphaSlider.tsx +141 -0
  39. package/components/builder/color-picker/AngleControl.tsx +138 -0
  40. package/components/builder/color-picker/ColorInputs.tsx +105 -0
  41. package/components/builder/color-picker/EyedropperButton.tsx +74 -0
  42. package/components/builder/color-picker/GradientBar.tsx +222 -0
  43. package/components/builder/color-picker/GradientPreview.tsx +53 -0
  44. package/components/builder/color-picker/HueSlider.tsx +124 -0
  45. package/components/builder/color-picker/MeshCanvas.tsx +172 -0
  46. package/components/builder/color-picker/MeshPointEditor.tsx +133 -0
  47. package/components/builder/color-picker/MeshPointList.tsx +200 -0
  48. package/components/builder/color-picker/PositionControl.tsx +158 -0
  49. package/components/builder/color-picker/SaturationCanvas.tsx +142 -0
  50. package/components/builder/color-picker/StopEditor.tsx +178 -0
  51. package/components/builder/color-picker/SwatchBar.tsx +93 -0
  52. package/components/builder/color-picker/UnifiedColorPicker.tsx +713 -0
  53. package/components/builder/color-picker/index.ts +62 -0
  54. package/components/builder/color-picker/types.ts +115 -0
  55. package/components/builder/color-picker/utils.ts +138 -0
  56. package/components/builder/editors/CoverBlockEditor.tsx +86 -32
  57. package/components/builder/editors/ProjectGridEditor.tsx +51 -4
  58. package/components/builder/hooks/useColumnDrag.ts +25 -27
  59. package/components/builder/settings-panel/BlockLayoutTab.tsx +29 -7
  60. package/components/builder/settings-panel/LayoutTab.tsx +382 -310
  61. package/components/builder/settings-panel/PageSettings.tsx +6 -4
  62. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  63. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +392 -312
  64. package/components/builder/settings-panel/SectionV2Settings.tsx +65 -35
  65. package/components/ui/Navbar.tsx +95 -25
  66. package/components/ui/PortfolioTracker.tsx +3 -3
  67. package/lib/assets.ts +1 -1
  68. package/lib/auth.ts +1 -1
  69. package/lib/builder/gradient-presets.ts +128 -0
  70. package/lib/builder/layout-styles.ts +16 -10
  71. package/lib/builder/serializer.ts +1 -0
  72. package/lib/builder/store-blocks.ts +48 -61
  73. package/lib/builder/store-helpers.ts +31 -14
  74. package/lib/builder/store.ts +59 -41
  75. package/lib/builder/types.ts +14 -0
  76. package/lib/color-utils.ts +200 -0
  77. package/lib/revalidate.ts +2 -2
  78. package/lib/sanity/queries.ts +4 -3
  79. package/lib/sanity/types.ts +76 -1
  80. package/lib/setup/detect.ts +1 -1
  81. package/package.json +8 -2
  82. package/sanity/schemas/siteSettings.ts +34 -0
  83. package/styles/base.css +3 -3
  84. package/app/globals.css +0 -7
@@ -66,6 +66,8 @@ export default function ProjectGridBlockRenderer({
66
66
  const containerTopRef = useRef<number | null>(null);
67
67
  const [containerWidth, setContainerWidth] = useState(0);
68
68
  const [resolvedProjects, setResolvedProjects] = useState<ResolvedProject[]>([]);
69
+ // BLK-005: Track fetch errors so we can provide user feedback
70
+ const [fetchError, setFetchError] = useState(false);
69
71
 
70
72
  // ─── Measure container width via ResizeObserver ───
71
73
  // Uses callback ref so the observer is set up the instant the DOM node
@@ -116,8 +118,9 @@ export default function ProjectGridBlockRenderer({
116
118
 
117
119
  const slugs = block.projects.map((p) => p.project_slug);
118
120
 
121
+ setFetchError(false);
119
122
  fetch("/api/projects")
120
- .then((r) => (r.ok ? r.json() : { projects: [] }))
123
+ .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`API ${r.status}`))))
121
124
  .then((data) => {
122
125
  const projectMap = new Map<string, { title: string; subtitle: string; thumbnail_path?: string; cover_video?: string }>();
123
126
  for (const proj of data.projects || []) {
@@ -151,8 +154,10 @@ export default function ProjectGridBlockRenderer({
151
154
 
152
155
  setResolvedProjects(resolved);
153
156
  })
154
- .catch(() => {
157
+ .catch((err) => {
158
+ console.error("[ProjectGridBlock] Failed to fetch projects:", err);
155
159
  setResolvedProjects([]);
160
+ setFetchError(true);
156
161
  });
157
162
  }, [block.projects]);
158
163
 
@@ -270,7 +275,17 @@ export default function ProjectGridBlockRenderer({
270
275
  return indices;
271
276
  }, [entranceEnabled, masonry.items, aboveFoldKeys, gapV]);
272
277
 
273
- if (resolvedProjects.length === 0) return null;
278
+ if (resolvedProjects.length === 0) {
279
+ // BLK-005: Show subtle error message if fetch failed (not just empty data)
280
+ if (fetchError) {
281
+ return (
282
+ <div className="py-8 text-center font-sans text-sm text-brand-muted opacity-60">
283
+ Unable to load projects. Please try refreshing the page.
284
+ </div>
285
+ );
286
+ }
287
+ return null;
288
+ }
274
289
 
275
290
  return (
276
291
  <div
@@ -16,6 +16,8 @@ import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
16
16
  import BlockRenderer from "./BlockRenderer";
17
17
  import EnterAnimationWrapper from "./EnterAnimationWrapper";
18
18
  import { getRowLayoutStyles, hexToRgba } from "../../lib/builder/layout-styles";
19
+ import { colorToOverrideRule, borderColorToOverrideRule, parseColorField } from "../../lib/color-utils";
20
+ import type { ColorField } from "../../lib/sanity/types";
19
21
  import { BREAKPOINTS } from "../../lib/builder/constants";
20
22
 
21
23
  /**
@@ -81,9 +83,9 @@ function buildSectionResponsiveCss(section: PageSection): string | null {
81
83
  }
82
84
  }
83
85
 
84
- // Border color
86
+ // Border color (supports solid + gradients via ColorField bridge)
85
87
  if (overrides.border_color) {
86
- rules.push(`border-color:${overrides.border_color}!important`);
88
+ rules.push(borderColorToOverrideRule(parseColorField(overrides.border_color)));
87
89
  }
88
90
 
89
91
  // Border style
@@ -91,14 +93,13 @@ function buildSectionResponsiveCss(section: PageSection): string | null {
91
93
  rules.push(`border-style:${overrides.border_style}!important`);
92
94
  }
93
95
 
94
- // Background color + opacity rgba()
96
+ // Background color + opacity (gradient-safe via ColorField bridge)
95
97
  if (overrides.background_color) {
96
98
  const opacity = overrides.background_opacity as number | undefined;
97
- if (opacity !== undefined && opacity < 100) {
98
- rules.push(`background-color:${hexToRgba(overrides.background_color as string, opacity / 100)}!important`);
99
- } else {
100
- rules.push(`background-color:${overrides.background_color}!important`);
101
- }
99
+ rules.push(colorToOverrideRule(
100
+ parseColorField(overrides.background_color),
101
+ opacity
102
+ ));
102
103
  }
103
104
 
104
105
  // Background image + sub-properties
@@ -25,6 +25,7 @@ import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
25
25
  import BlockRenderer from "./BlockRenderer";
26
26
  import EnterAnimationWrapper from "./EnterAnimationWrapper";
27
27
  import { getRowLayoutStyles, getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, hexToRgba } from "../../lib/builder/layout-styles";
28
+ import { parseColorField, colorToOverrideRule, borderColorToOverrideRule } from "../../lib/color-utils";
28
29
  import { BREAKPOINTS } from "../../lib/builder/constants";
29
30
 
30
31
  // ── Responsive CSS generation ──
@@ -99,9 +100,9 @@ function buildSettingsOverrideRules(
99
100
  }
100
101
  }
101
102
 
102
- // Border color
103
+ // Border color (supports solid + gradients via ColorField bridge)
103
104
  if (overrides.border_color) {
104
- rules.push(`border-color:${overrides.border_color}!important`);
105
+ rules.push(borderColorToOverrideRule(parseColorField(overrides.border_color)));
105
106
  }
106
107
 
107
108
  // Border style
@@ -109,14 +110,13 @@ function buildSettingsOverrideRules(
109
110
  rules.push(`border-style:${overrides.border_style}!important`);
110
111
  }
111
112
 
112
- // Background color + opacity
113
+ // Background color + opacity (supports solid + gradients via ColorField bridge)
113
114
  if (overrides.background_color) {
114
115
  const opacity = overrides.background_opacity;
115
- if (opacity !== undefined && opacity < 100) {
116
- rules.push(`background-color:${hexToRgba(overrides.background_color, opacity / 100)}!important`);
117
- } else {
118
- rules.push(`background-color:${overrides.background_color}!important`);
119
- }
116
+ rules.push(colorToOverrideRule(
117
+ parseColorField(overrides.background_color),
118
+ opacity
119
+ ));
120
120
  }
121
121
 
122
122
  return rules;
@@ -8,8 +8,10 @@ const heightMap: Record<string, string> = {
8
8
  };
9
9
 
10
10
  export default function SpacerBlockRenderer({ block }: { block: SpacerBlock }) {
11
- if (block.height === "custom" && block.custom_height) {
12
- return <div style={{ height: `${block.custom_height}px` }} aria-hidden />;
11
+ // BLK-007: Use != null instead of truthiness to allow custom_height of 0
12
+ if (block.height === "custom" && block.custom_height != null) {
13
+ const h = Math.max(0, block.custom_height);
14
+ return <div style={{ height: `${h}px` }} aria-hidden />;
13
15
  }
14
16
  return (
15
17
  <div className={heightMap[block.height ?? "medium"]} aria-hidden />
@@ -3,7 +3,8 @@ import { PortableText } from "next-sanity";
3
3
 
4
4
  /** Resolve fontSize: supports numeric px and legacy string enum */
5
5
  function resolvePublicFontSize(fontSize?: number | string): string | undefined {
6
- if (typeof fontSize === "number") return `${fontSize}px`;
6
+ // BLK-015: Guard against negative or zero font sizes
7
+ if (typeof fontSize === "number") return fontSize > 0 ? `${fontSize}px` : undefined;
7
8
  // Legacy Tailwind class mapping
8
9
  const tailwindMap: Record<string, string> = {
9
10
  small: "text-sm",
@@ -49,7 +50,7 @@ export function getTextBlockStyles(block: TextBlock): { className: string; style
49
50
  const isNumericWeight = s?.fontWeight && !isNaN(parseInt(s.fontWeight, 10));
50
51
 
51
52
  const classes = [
52
- "font-mono",
53
+ "font-sans",
53
54
  !isNumericFontSize ? resolvePublicFontSize(s?.fontSize) : undefined,
54
55
  alignmentMap[s?.alignment ?? "left"],
55
56
  !isNumericWeight ? resolvePublicFontWeight(s?.fontWeight) : undefined,
@@ -58,9 +59,13 @@ export function getTextBlockStyles(block: TextBlock): { className: string; style
58
59
  .join(" ");
59
60
 
60
61
  const inlineStyle: React.CSSProperties = {};
61
- if (isNumericFontSize) inlineStyle.fontSize = `${s!.fontSize}px`;
62
+ if (isNumericFontSize && (s!.fontSize as number) > 0) inlineStyle.fontSize = `${s!.fontSize}px`;
62
63
  if (isNumericWeight) inlineStyle.fontWeight = parseInt(s!.fontWeight!, 10);
63
- if (s?.color) inlineStyle.color = s.color;
64
+ // Guard: text color must be a solid hex string, not a gradient.
65
+ // resolveColorHex extracts a representative hex if a gradient slips through.
66
+ if (s?.color) {
67
+ inlineStyle.color = typeof s.color === "string" ? s.color : "#000000";
68
+ }
64
69
  if (s?.lineHeight) inlineStyle.lineHeight = s.lineHeight;
65
70
  if (s?.letterSpacing) inlineStyle.letterSpacing = s.letterSpacing;
66
71
  if (s?.maxWidth) inlineStyle.maxWidth = s.maxWidth;
@@ -70,9 +70,15 @@ export default function BuilderCanvas({ children }: BuilderCanvasProps) {
70
70
  const zoomRef = useRef(zoom);
71
71
  const panXRef = useRef(panX);
72
72
  const panYRef = useRef(panY);
73
+ // LEAK-002 fix: Also ref action functions so flushWheel/handleWheel
74
+ // callbacks are truly stable and never cause wheel listener resubscription.
75
+ const zoomToPointRef = useRef(zoomToPoint);
76
+ const setCanvasPanRef = useRef(setCanvasPan);
73
77
  useEffect(() => { zoomRef.current = zoom; }, [zoom]);
74
78
  useEffect(() => { panXRef.current = panX; }, [panX]);
75
79
  useEffect(() => { panYRef.current = panY; }, [panY]);
80
+ useEffect(() => { zoomToPointRef.current = zoomToPoint; }, [zoomToPoint]);
81
+ useEffect(() => { setCanvasPanRef.current = setCanvasPan; }, [setCanvasPan]);
76
82
 
77
83
  // ---- Trigger smooth animation for programmatic zoom/pan ----
78
84
  const triggerAnimation = useCallback(() => {
@@ -128,7 +134,7 @@ export default function BuilderCanvas({ children }: BuilderCanvasProps) {
128
134
  // Throttled via requestAnimationFrame to prevent jank on rapid scroll.
129
135
  // Deltas accumulate between frames so no input is lost.
130
136
 
131
- // Stable flush — reads current values from refs, never re-created
137
+ // Stable flush — reads current values from refs, never re-created (LEAK-002)
132
138
  const flushWheel = useCallback(() => {
133
139
  wheelRafRef.current = null;
134
140
  const accum = wheelAccumRef.current;
@@ -138,11 +144,11 @@ export default function BuilderCanvas({ children }: BuilderCanvasProps) {
138
144
  if (accum.isZoom) {
139
145
  const zoomDelta = -accum.deltaY * 0.005;
140
146
  const newZoom = zoomRef.current * (1 + zoomDelta);
141
- zoomToPoint(newZoom, accum.cursorX, accum.cursorY);
147
+ zoomToPointRef.current(newZoom, accum.cursorX, accum.cursorY);
142
148
  } else {
143
- setCanvasPan(panXRef.current - accum.deltaX, panYRef.current - accum.deltaY);
149
+ setCanvasPanRef.current(panXRef.current - accum.deltaX, panYRef.current - accum.deltaY);
144
150
  }
145
- }, [zoomToPoint, setCanvasPan]);
151
+ }, []); // No deps — all values read from refs
146
152
 
147
153
  // Stable wheel handler — never re-subscribed during zoom/pan
148
154
  const handleWheel = useCallback(
@@ -1,243 +1,51 @@
1
- "use client";
2
-
3
- /**
4
- * ColorPicker Full-featured HSL color picker with hex/RGB display.
5
- * Used in the Global Styles palette editor and inline block editors.
6
- */
7
-
8
- import { useState, useRef, useCallback, useEffect } from "react";
9
- import { hexToHSL, hsvToHex, hexToHSV, isValidHex } from "../../lib/color-utils";
10
-
11
- // ─── ColorPicker Component ───
12
-
13
- interface ColorPickerProps {
14
- color: string;
15
- onChange: (hex: string) => void;
16
- onClose?: () => void;
17
- /** Label for the confirm button */
18
- confirmLabel?: string;
19
- /** If true, shows a compact version (no confirm/cancel buttons, live onChange) */
20
- compact?: boolean;
21
- }
22
-
23
- export default function ColorPicker({
24
- color,
25
- onChange,
26
- onClose,
27
- confirmLabel = "Confirm",
28
- compact = false,
29
- }: ColorPickerProps) {
30
- const initHsv = hexToHSV(isValidHex(color) ? color : "#ffffff");
31
- const [hue, setHue] = useState(initHsv.h);
32
- const [sat, setSat] = useState(initHsv.s);
33
- const [val, setVal] = useState(initHsv.v);
34
- const [hex, setHex] = useState(isValidHex(color) ? color : "#ffffff");
35
- const canvasRef = useRef<HTMLDivElement>(null);
36
- const hueRef = useRef<HTMLDivElement>(null);
37
- const draggingCanvas = useRef(false);
38
- const draggingHue = useRef(false);
39
-
40
- // Sync hex from HSV
41
- const updateHex = useCallback((h: number, s: number, v: number) => {
42
- const newHex = hsvToHex(h, s, v);
43
- setHex(newHex);
44
- if (compact) onChange(newHex);
45
- }, [compact, onChange]);
46
-
47
- // Canvas (saturation + value) interaction
48
- const handleCanvasMove = useCallback((e: React.MouseEvent | MouseEvent) => {
49
- const rect = canvasRef.current?.getBoundingClientRect();
50
- if (!rect) return;
51
- const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
52
- const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
53
- const s = Math.round(x * 100);
54
- const v = Math.round((1 - y) * 100);
55
- setSat(s);
56
- setVal(v);
57
- updateHex(hue, s, v);
58
- }, [hue, updateHex]);
59
-
60
- // Hue slider interaction
61
- const handleHueMove = useCallback((e: React.MouseEvent | MouseEvent) => {
62
- const rect = hueRef.current?.getBoundingClientRect();
63
- if (!rect) return;
64
- const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
65
- const h = Math.round(x * 360);
66
- setHue(h);
67
- updateHex(h, sat, val);
68
- }, [sat, val, updateHex]);
69
-
70
- // Global mouse up
71
- useEffect(() => {
72
- const handleUp = () => {
73
- draggingCanvas.current = false;
74
- draggingHue.current = false;
75
- };
76
- const handleMove = (e: MouseEvent) => {
77
- if (draggingCanvas.current) handleCanvasMove(e);
78
- if (draggingHue.current) handleHueMove(e);
79
- };
80
- window.addEventListener("mouseup", handleUp);
81
- window.addEventListener("mousemove", handleMove);
82
- return () => {
83
- window.removeEventListener("mouseup", handleUp);
84
- window.removeEventListener("mousemove", handleMove);
85
- };
86
- }, [handleCanvasMove, handleHueMove]);
87
-
88
- const handleHexInput = (v: string) => {
89
- setHex(v);
90
- if (isValidHex(v)) {
91
- const hsv = hexToHSV(v);
92
- setHue(hsv.h);
93
- setSat(hsv.s);
94
- setVal(hsv.v);
95
- if (compact) onChange(v);
96
- }
97
- };
98
-
99
- // Keyboard handlers for canvas (saturation/value) and hue slider
100
- const handleCanvasKeyDown = useCallback((e: React.KeyboardEvent) => {
101
- const step = e.shiftKey ? 10 : 2;
102
- let newSat = sat;
103
- let newVal = val;
104
- switch (e.key) {
105
- case "ArrowRight": newSat = Math.min(100, sat + step); break;
106
- case "ArrowLeft": newSat = Math.max(0, sat - step); break;
107
- case "ArrowUp": newVal = Math.min(100, val + step); break;
108
- case "ArrowDown": newVal = Math.max(0, val - step); break;
109
- default: return;
110
- }
111
- e.preventDefault();
112
- setSat(newSat);
113
- setVal(newVal);
114
- updateHex(hue, newSat, newVal);
115
- }, [sat, val, hue, updateHex]);
116
-
117
- const handleHueKeyDown = useCallback((e: React.KeyboardEvent) => {
118
- const step = e.shiftKey ? 20 : 5;
119
- let newHue = hue;
120
- switch (e.key) {
121
- case "ArrowRight": newHue = Math.min(360, hue + step); break;
122
- case "ArrowLeft": newHue = Math.max(0, hue - step); break;
123
- default: return;
124
- }
125
- e.preventDefault();
126
- setHue(newHue);
127
- updateHex(newHue, sat, val);
128
- }, [hue, sat, val, updateHex]);
129
-
130
- const r = parseInt(hex.slice(1, 3), 16) || 0;
131
- const g = parseInt(hex.slice(3, 5), 16) || 0;
132
- const b = parseInt(hex.slice(5, 7), 16) || 0;
133
-
134
- return (
135
- <div className="bg-white rounded-2xl p-4 w-[260px] shadow-xl border border-neutral-200">
136
- {/* Saturation/Value canvas */}
137
- <div
138
- ref={canvasRef}
139
- role="slider"
140
- tabIndex={0}
141
- aria-label="Color saturation and brightness"
142
- aria-valuetext={`Saturation ${sat}%, Brightness ${val}%`}
143
- className="w-full h-[160px] rounded-xl cursor-crosshair relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#076bff]"
144
- style={{
145
- background: `linear-gradient(to bottom, transparent, #000), linear-gradient(to right, #fff, hsl(${hue}, 100%, 50%))`,
146
- }}
147
- onMouseDown={(e) => { draggingCanvas.current = true; handleCanvasMove(e); }}
148
- onKeyDown={handleCanvasKeyDown}
149
- >
150
- <div
151
- className="absolute w-4 h-4 rounded-full border-2 border-white pointer-events-none"
152
- style={{
153
- left: `${sat}%`,
154
- top: `${100 - val}%`,
155
- transform: "translate(-50%, -50%)",
156
- boxShadow: "0 0 0 1px rgba(0,0,0,0.3), 0 2px 6px rgba(0,0,0,0.3)",
157
- }}
158
- />
159
- </div>
160
-
161
- {/* Hue slider */}
162
- <div
163
- ref={hueRef}
164
- role="slider"
165
- tabIndex={0}
166
- aria-label="Color hue"
167
- aria-valuemin={0}
168
- aria-valuemax={360}
169
- aria-valuenow={hue}
170
- aria-valuetext={`Hue ${hue} degrees`}
171
- className="w-full h-3.5 rounded-full mt-3 cursor-pointer relative select-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#076bff]"
172
- style={{
173
- background: "linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)",
174
- }}
175
- onMouseDown={(e) => { draggingHue.current = true; handleHueMove(e); }}
176
- onKeyDown={handleHueKeyDown}
177
- >
178
- <div
179
- className="absolute w-4 h-4 rounded-full border-2 border-white pointer-events-none"
180
- style={{
181
- left: `${(hue / 360) * 100}%`,
182
- top: "50%",
183
- transform: "translate(-50%, -50%)",
184
- background: `hsl(${hue}, 100%, 50%)`,
185
- boxShadow: "0 1px 3px rgba(0,0,0,0.4)",
186
- }}
187
- />
188
- </div>
189
-
190
- {/* Hex input */}
191
- <div className="flex items-center gap-2.5 mt-3.5">
192
- <div
193
- className="w-8 h-8 rounded-lg border border-neutral-200 shrink-0"
194
- style={{ background: hex }}
195
- />
196
- <input
197
- value={hex.toUpperCase()}
198
- onChange={(e) => handleHexInput(e.target.value)}
199
- className="flex-1 bg-neutral-50 border border-neutral-200 rounded-lg px-2.5 py-1.5 text-neutral-900 text-xs font-mono outline-none focus:border-[#076bff] focus:ring-2 focus:ring-[#076bff]/10 transition-colors"
200
- />
201
- </div>
202
-
203
- {/* RGB readout */}
204
- <div className="flex gap-2 mt-2.5">
205
- {[
206
- { label: "R", val: r },
207
- { label: "G", val: g },
208
- { label: "B", val: b },
209
- ].map(({ label, val: v }) => (
210
- <div key={label} className="flex-1">
211
- <div className="text-[9px] text-neutral-400 uppercase tracking-widest mb-0.5">{label}</div>
212
- <div className="bg-neutral-50 border border-neutral-200 rounded-md px-2 py-1 text-neutral-600 text-[11px] text-center font-mono">
213
- {v}
214
- </div>
215
- </div>
216
- ))}
217
- </div>
218
-
219
- {/* Buttons (non-compact mode) */}
220
- {!compact && (
221
- <div className="flex gap-2 mt-3.5">
222
- {onClose && (
223
- <button
224
- onClick={onClose}
225
- className="flex-1 py-2 rounded-lg border border-neutral-200 bg-transparent text-neutral-500 text-xs font-mono cursor-pointer hover:border-neutral-300 hover:text-neutral-700 transition-colors"
226
- >
227
- Cancel
228
- </button>
229
- )}
230
- <button
231
- onClick={() => { onChange(hex); onClose?.(); }}
232
- className="flex-1 py-2 rounded-lg border-none bg-[#076bff] text-white text-xs font-mono font-semibold cursor-pointer hover:bg-[#0559d4] transition-colors"
233
- >
234
- {confirmLabel}
235
- </button>
236
- </div>
237
- )}
238
- </div>
239
- );
240
- }
241
-
242
- // ─── Export helpers ───
243
- export { isValidHex, hexToHSL, hexToHSV, hsvToHex };
1
+ "use client";
2
+
3
+ /**
4
+ * @deprecated Use `UnifiedColorPicker` from `./color-picker` instead.
5
+ *
6
+ * This file is kept for backward compatibility. It re-exports the new
7
+ * UnifiedColorPicker with an adapter that maps the old props to the new API.
8
+ *
9
+ * Old API: ColorPicker({ color, onChange, onClose, confirmLabel, compact })
10
+ * New API: UnifiedColorPicker({ value, onChange, onClose, confirmLabel, ... })
11
+ */
12
+
13
+ import { UnifiedColorPicker } from "./color-picker";
14
+ import { isValidHex } from "../../lib/color-utils";
15
+
16
+ // Re-export helpers for any consumers that imported from here
17
+ export { isValidHex } from "../../lib/color-utils";
18
+ export { hexToHSL, hexToHSV, hsvToHex } from "../../lib/color-utils";
19
+
20
+ // ─── Legacy ColorPicker Adapter ───
21
+
22
+ interface ColorPickerProps {
23
+ /** @deprecated Use `value` instead */
24
+ color: string;
25
+ onChange: (hex: string) => void;
26
+ onClose?: () => void;
27
+ confirmLabel?: string;
28
+ /** @deprecated Compact mode is no longer supported. Ignored. */
29
+ compact?: boolean;
30
+ }
31
+
32
+ /**
33
+ * @deprecated Use `import { UnifiedColorPicker } from "./color-picker"` instead.
34
+ */
35
+ export default function ColorPicker({
36
+ color,
37
+ onChange,
38
+ onClose,
39
+ confirmLabel = "Confirm",
40
+ }: ColorPickerProps) {
41
+ const handleClose = onClose ?? (() => {});
42
+
43
+ return (
44
+ <UnifiedColorPicker
45
+ value={isValidHex(color) ? color : "#ffffff"}
46
+ onChange={(val) => onChange(typeof val === "string" ? val : "#000000")}
47
+ onClose={handleClose}
48
+ confirmLabel={confirmLabel}
49
+ />
50
+ );
51
+ }