entangle-ui 0.11.0 → 0.13.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/esm/assets/src/components/controls/Slider/{Slider.css.ts.vanilla-Cqm3fQ0S.css → Slider.css.ts.vanilla-CLf_RC0A.css} +50 -47
  3. package/dist/esm/assets/src/components/navigation/Menu/{Menu.css.ts.vanilla-B5iVe5S2.css → Menu.css.ts.vanilla-Bhh1L5yl.css} +14 -11
  4. package/dist/esm/assets/src/components/primitives/EditableText/EditableText.css.ts.vanilla-CrVGgD19.css +63 -0
  5. package/dist/esm/assets/src/components/primitives/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
  6. package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
  7. package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
  8. package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
  9. package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
  10. package/dist/esm/components/controls/Slider/Slider.js +40 -7
  11. package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
  12. package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js +3 -1
  13. package/dist/esm/components/editor/ViewportGizmo/ViewportGizmo.js.map +1 -1
  14. package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js +21 -27
  15. package/dist/esm/components/editor/ViewportGizmo/useGizmoInteraction.js.map +1 -1
  16. package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
  17. package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
  18. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js +1 -0
  19. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbEllipsis.js.map +1 -1
  20. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js +1 -0
  21. package/dist/esm/components/navigation/Breadcrumbs/BreadcrumbItem.js.map +1 -1
  22. package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
  23. package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
  24. package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
  25. package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
  26. package/dist/esm/components/navigation/Menu/Menu.js +2 -2
  27. package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
  28. package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js +1 -0
  29. package/dist/esm/components/navigation/PathBar/PathBarSiblingMenu.js.map +1 -1
  30. package/dist/esm/components/primitives/EditableText/EditableText.css.js +11 -0
  31. package/dist/esm/components/primitives/EditableText/EditableText.css.js.map +1 -0
  32. package/dist/esm/components/primitives/EditableText/EditableText.js +208 -0
  33. package/dist/esm/components/primitives/EditableText/EditableText.js.map +1 -0
  34. package/dist/esm/components/primitives/EditableText/editableTextLabels.js +16 -0
  35. package/dist/esm/components/primitives/EditableText/editableTextLabels.js.map +1 -0
  36. package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
  37. package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
  38. package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
  39. package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
  40. package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
  41. package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
  42. package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
  43. package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
  44. package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
  45. package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
  46. package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
  47. package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
  48. package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
  49. package/dist/esm/index.js +2 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/{styles.js → styles.css.js} +1 -1
  52. package/dist/esm/styles.css.js.map +1 -0
  53. package/dist/tokens/tokens.dark.css +1 -1
  54. package/dist/tokens/tokens.json +1 -1
  55. package/dist/tokens/tokens.light.css +1 -1
  56. package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
  57. package/dist/types/components/editor/ViewportGizmo/ViewportGizmo.types.d.ts +26 -3
  58. package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
  59. package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
  60. package/dist/types/components/primitives/EditableText/EditableText.d.ts +37 -0
  61. package/dist/types/components/primitives/EditableText/EditableText.types.d.ts +158 -0
  62. package/dist/types/components/primitives/EditableText/editableTextLabels.d.ts +11 -0
  63. package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
  64. package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
  65. package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
  66. package/dist/types/index.d.ts +3 -0
  67. package/package.json +2 -2
  68. package/dist/esm/styles.js.map +0 -1
@@ -2,12 +2,13 @@
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { assignInlineVars } from '@vanilla-extract/dynamic';
4
4
  import React, { useState, useRef, useCallback, useEffect } from 'react';
5
+ import { createPortal } from 'react-dom';
5
6
  import { FormHelperText } from '../../form/FormHelperText.js';
6
7
  import { FormLabel } from '../../form/FormLabel.js';
7
8
  import { InputWrapper } from '../../form/InputWrapper.js';
8
9
  import { useKeyboardContext } from '../../../context/KeyboardContext.js';
9
10
  import { cx } from '../../../utils/cx.js';
10
- import { sliderWrapperStyle, fillPercentageVar, fillRecipe, trackRecipe, thumbPercentageVar, thumbRecipe, ticksStyle, tickStyle, tooltipPercentageVar, tooltipRecipe, sliderContainerRecipe } from './Slider.css.js';
11
+ import { sliderWrapperStyle, fillPercentageVar, fillRecipe, trackRecipe, thumbPercentageVar, thumbRecipe, ticksStyle, tickStyle, sliderTooltipRecipe, sliderContainerRecipe } from './Slider.css.js';
11
12
 
12
13
  /**
13
14
  * Clamps a value between min and max bounds
@@ -73,10 +74,13 @@ function roundToPrecision(value, precision) {
73
74
  * />
74
75
  * ```
75
76
  */
76
- const Slider = ({ value, onChange, min = 0, max = 100, step = 1, precisionStep, largeStep, precision = 2, size = 'md', disabled = false, readOnly = false, label, helperText, errorMessage, error = false, required = false, unit, formatValue, showTooltip = true, showTicks = false, tickCount = 5, className, testId, onFocus, onBlur, onKeyDown, ref, ...props }) => {
77
+ const Slider = ({ value, onChange, min = 0, max = 100, step = 1, precisionStep, largeStep, precision = 2, size = 'md', disabled = false, readOnly = false, label, helperText, errorMessage, error = false, required = false, unit, formatValue, showTooltip = true, tooltipPlacement = 'top', showTicks = false, tickCount = 5, className, testId, onFocus, onBlur, onKeyDown, ref, ...props }) => {
77
78
  const [isDragging, setIsDragging] = useState(false);
78
79
  const [isFocused, setIsFocused] = useState(false);
79
80
  const [showTooltipState, setShowTooltipState] = useState(false);
81
+ // Viewport coordinates of the portaled value tooltip (null until a drag
82
+ // starts). Updated only while dragging so the portal stays off the idle path.
83
+ const [tooltipPos, setTooltipPos] = useState(null);
80
84
  const sliderRef = useRef(null);
81
85
  const trackRef = useRef(null);
82
86
  const dragStartRef = useRef(null);
@@ -135,6 +139,22 @@ const Slider = ({ value, onChange, min = 0, max = 100, step = 1, precisionStep,
135
139
  const steps = Math.round((newValue - min) / currentStep);
136
140
  return min + steps * currentStep;
137
141
  }, [min, max, value, getStepSize]);
142
+ /**
143
+ * Computes the portaled tooltip's viewport position for a (snapped) value,
144
+ * anchored to the thumb. Reads the same track rect the drag math already
145
+ * reads, so it adds no extra layout pass per frame.
146
+ */
147
+ const updateTooltipPos = useCallback((v) => {
148
+ const track = trackRef.current;
149
+ if (!track)
150
+ return;
151
+ const rect = track.getBoundingClientRect();
152
+ const pct = clamp((v - min) / (max - min), 0, 1);
153
+ setTooltipPos({
154
+ left: rect.left + pct * rect.width,
155
+ top: tooltipPlacement === 'bottom' ? rect.bottom : rect.top,
156
+ });
157
+ }, [min, max, tooltipPlacement]);
138
158
  /**
139
159
  * Handle mouse down on track or thumb
140
160
  */
@@ -147,11 +167,20 @@ const Slider = ({ value, onChange, min = 0, max = 100, step = 1, precisionStep,
147
167
  // Use the current step size (depends on keyboard modifiers)
148
168
  const newValue = positionToValue(event.clientX);
149
169
  applyValue(newValue);
170
+ if (showTooltip)
171
+ updateTooltipPos(newValue);
150
172
  dragStartRef.current = {
151
173
  startX: event.clientX,
152
174
  startValue: newValue,
153
175
  };
154
- }, [disabled, readOnly, positionToValue, applyValue, showTooltip]);
176
+ }, [
177
+ disabled,
178
+ readOnly,
179
+ positionToValue,
180
+ applyValue,
181
+ showTooltip,
182
+ updateTooltipPos,
183
+ ]);
155
184
  /**
156
185
  * Handle global mouse move during drag
157
186
  */
@@ -163,8 +192,10 @@ const Slider = ({ value, onChange, min = 0, max = 100, step = 1, precisionStep,
163
192
  // Use the current step size (depends on keyboard modifiers)
164
193
  const newValue = positionToValue(event.clientX);
165
194
  applyValue(newValue);
195
+ if (showTooltip)
196
+ updateTooltipPos(newValue);
166
197
  });
167
- }, [isDragging, positionToValue, applyValue]);
198
+ }, [isDragging, positionToValue, applyValue, showTooltip, updateTooltipPos]);
168
199
  /**
169
200
  * Handle global mouse up to end drag
170
201
  */
@@ -254,9 +285,11 @@ const Slider = ({ value, onChange, min = 0, max = 100, step = 1, precisionStep,
254
285
  [fillPercentageVar]: `${percentage}%`,
255
286
  }) }) }), jsx("div", { className: thumbRecipe({ size, error, isDragging }), style: assignInlineVars({
256
287
  [thumbPercentageVar]: `${percentage}%`,
257
- }) }), showTicks && (jsx("div", { className: ticksStyle, children: ticks.map(i => (jsx("div", { className: tickStyle }, i))) })), showTooltip && (jsx("div", { className: tooltipRecipe({ visible: showTooltipState }), style: assignInlineVars({
258
- [tooltipPercentageVar]: `${percentage}%`,
259
- }), children: displayValue }))] }) }), (helperText ?? (error && errorMessage)) && (jsx(FormHelperText, { error: error, children: error && errorMessage ? errorMessage : helperText }))] }));
288
+ }) }), showTicks && (jsx("div", { className: ticksStyle, children: ticks.map(i => (jsx("div", { className: tickStyle }, i))) })), showTooltip &&
289
+ showTooltipState &&
290
+ tooltipPos &&
291
+ typeof document !== 'undefined' &&
292
+ createPortal(jsx("div", { className: sliderTooltipRecipe({ placement: tooltipPlacement }), style: { left: tooltipPos.left, top: tooltipPos.top }, role: "presentation", "data-testid": testId ? `${testId}-tooltip` : undefined, children: displayValue }), document.body)] }) }), (helperText ?? (error && errorMessage)) && (jsx(FormHelperText, { error: error, children: error && errorMessage ? errorMessage : helperText }))] }));
260
293
  };
261
294
  Slider.displayName = 'Slider';
262
295
 
@@ -1 +1 @@
1
- {"version":3,"file":"Slider.js","sources":["src/components/controls/Slider/Slider.tsx"],"sourcesContent":["'use client';\n\n// src/components/controls/Slider/Slider.tsx\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, { useRef, useCallback, useState, useEffect } from 'react';\n\nimport { FormHelperText } from '@/components/form/FormHelperText';\nimport { FormLabel } from '@/components/form/FormLabel';\nimport { InputWrapper } from '@/components/form/InputWrapper';\nimport { useKeyboardContext } from '@/context/KeyboardContext';\nimport { cx } from '@/utils/cx';\n\nimport {\n sliderContainerRecipe,\n sliderWrapperStyle,\n trackRecipe,\n fillRecipe,\n fillPercentageVar,\n thumbRecipe,\n thumbPercentageVar,\n ticksStyle,\n tickStyle,\n tooltipRecipe,\n tooltipPercentageVar,\n} from './Slider.css';\n\nimport type { BaseComponent, Size } from '@/types/common';\nimport type { Prettify } from '@/types/utilities';\n\n/**\n * Props specific to Slider component\n */\nexport interface SliderBaseProps extends Omit<BaseComponent, 'onChange'> {\n /**\n * Current numeric value\n */\n value: number;\n\n /**\n * Callback when value changes\n */\n onChange: (value: number) => void;\n\n /**\n * Minimum allowed value\n * @default 0\n */\n min?: number;\n\n /**\n * Maximum allowed value\n * @default 100\n */\n max?: number;\n\n /**\n * Step size for value increments\n * @default 1\n */\n step?: number;\n\n /**\n * Step size when Shift is held (precision mode)\n * @default step / 10\n */\n precisionStep?: number;\n\n /**\n * Step size when Ctrl is held (large steps)\n * @default step * 10\n */\n largeStep?: number;\n\n /**\n * Number of decimal places to round to\n */\n precision?: number;\n\n /**\n * Slider size using standard library sizing\n * - `sm`: Compact for toolbars\n * - `md`: Standard for forms\n * - `lg`: Prominent sliders\n * @default \"md\"\n */\n size?: Size;\n\n /**\n * Whether the slider is disabled\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether the slider is read-only\n * @default false\n */\n readOnly?: boolean;\n\n /**\n * Slider label\n */\n label?: string;\n\n /**\n * Helper text displayed below the slider\n */\n helperText?: string;\n\n /**\n * Error message displayed when error is true\n */\n errorMessage?: string;\n\n /**\n * Whether the slider has an error state\n * @default false\n */\n error?: boolean;\n\n /**\n * Whether the slider is required\n * @default false\n */\n required?: boolean;\n\n /**\n * Unit suffix to display (e.g., \"px\", \"%\", \"°\")\n */\n unit?: string;\n\n /**\n * Format value for display\n */\n formatValue?: (value: number) => string;\n\n /**\n * Show value tooltip while dragging\n * @default true\n */\n showTooltip?: boolean;\n\n /**\n * Show tick marks along the track\n * @default false\n */\n showTicks?: boolean;\n\n /**\n * Number of tick marks to show (when showTicks is true)\n * @default 5\n */\n tickCount?: number;\n\n /**\n * Focus event handler\n */\n onFocus?: (event: React.FocusEvent<HTMLElement>) => void;\n\n /**\n * Blur event handler\n */\n onBlur?: (event: React.FocusEvent<HTMLElement>) => void;\n\n /**\n * Key down event handler\n */\n onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;\n}\n\n/**\n * Props for the Slider component with prettified type for better IntelliSense\n */\nexport type SliderProps = Prettify<SliderBaseProps>;\n\n/**\n * Clamps a value between min and max bounds\n */\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Rounds a number to specified decimal places\n */\nfunction roundToPrecision(value: number, precision?: number): number {\n if (precision === undefined) return value;\n const factor = Math.pow(10, precision);\n return Math.round(value * factor) / factor;\n}\n\n/**\n * A professional slider component with drag interaction and keyboard support.\n *\n * Features:\n * - Smooth drag interaction with visual feedback\n * - Keyboard navigation with arrow keys\n * - Modifier key support: Ctrl (large steps), Shift (precision)\n * - Optional tick marks and value tooltip\n * - Comprehensive theming and size variants\n * - Accessible with proper ARIA attributes\n *\n * @example\n * ```tsx\n * // Basic slider\n * <Slider\n * value={opacity}\n * onChange={setOpacity}\n * min={0}\n * max={1}\n * step={0.1}\n * unit=\"%\"\n * />\n *\n * // Slider with ticks and custom formatting\n * <Slider\n * value={rotation}\n * onChange={setRotation}\n * min={0}\n * max={360}\n * step={15}\n * unit=\"°\"\n * showTicks\n * tickCount={8}\n * formatValue={(v) => `${v}°`}\n * />\n *\n * // Precision slider for fine control\n * <Slider\n * value={scale}\n * onChange={setScale}\n * min={0.1}\n * max={5}\n * step={0.1}\n * precisionStep={0.01}\n * largeStep={1}\n * precision={2}\n * />\n * ```\n */\nexport const Slider: React.FC<SliderProps> = ({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n precisionStep,\n largeStep,\n precision = 2,\n size = 'md',\n disabled = false,\n readOnly = false,\n label,\n helperText,\n errorMessage,\n error = false,\n required = false,\n unit,\n formatValue,\n showTooltip = true,\n showTicks = false,\n tickCount = 5,\n className,\n testId,\n onFocus,\n onBlur,\n onKeyDown,\n ref,\n ...props\n}) => {\n const [isDragging, setIsDragging] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const [showTooltipState, setShowTooltipState] = useState(false);\n\n const sliderRef = useRef<HTMLDivElement>(null);\n const trackRef = useRef<HTMLDivElement>(null);\n const dragStartRef = useRef<{ startX: number; startValue: number } | null>(\n null\n );\n\n const { modifiers } = useKeyboardContext();\n\n // Calculate derived values\n const effectivePrecisionStep = precisionStep ?? step / 10;\n const effectiveLargeStep = largeStep ?? step * 10;\n const percentage = ((value - min) / (max - min)) * 100;\n const clampedValue = clamp(value, min, max);\n\n const getStepSize = useCallback((): number => {\n if (modifiers.shift) return effectivePrecisionStep;\n if (modifiers.control || modifiers.meta) return effectiveLargeStep;\n return step;\n }, [\n modifiers.shift,\n modifiers.control,\n modifiers.meta,\n effectivePrecisionStep,\n effectiveLargeStep,\n step,\n ]);\n\n // Format display value with consistent precision to prevent tooltip size changes\n const displayValue = formatValue\n ? formatValue(clampedValue)\n : unit\n ? `${clampedValue.toFixed(precision)}${unit}`\n : clampedValue.toFixed(precision);\n\n /**\n * Applies a new value with proper bounds checking and rounding\n */\n const applyValue = useCallback(\n (newValue: number): void => {\n if (disabled || readOnly) return;\n\n if (newValue === value) return;\n\n const rounded = roundToPrecision(newValue, precision);\n const clamped = clamp(rounded, min, max);\n\n if (clamped !== value) {\n onChange(clamped);\n }\n },\n [disabled, readOnly, precision, min, max, value, onChange]\n );\n\n /**\n * Converts mouse position to value\n */\n const positionToValue = useCallback(\n (clientX: number): number => {\n if (!trackRef.current) return value;\n\n const rect = trackRef.current.getBoundingClientRect();\n const percentage = clamp((clientX - rect.left) / rect.width, 0, 1);\n const newValue = min + percentage * (max - min);\n\n // Use the current step size (depends on keyboard modifiers)\n const currentStep = getStepSize();\n\n // Snap to step increments\n const steps = Math.round((newValue - min) / currentStep);\n return min + steps * currentStep;\n },\n [min, max, value, getStepSize]\n );\n\n /**\n * Handle mouse down on track or thumb\n */\n const handleMouseDown = useCallback(\n (event: React.MouseEvent) => {\n if (disabled || readOnly) return;\n\n event.preventDefault();\n setIsDragging(true);\n setShowTooltipState(showTooltip);\n\n // Use the current step size (depends on keyboard modifiers)\n const newValue = positionToValue(event.clientX);\n applyValue(newValue);\n\n dragStartRef.current = {\n startX: event.clientX,\n startValue: newValue,\n };\n },\n [disabled, readOnly, positionToValue, applyValue, showTooltip]\n );\n\n /**\n * Handle global mouse move during drag\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n if (!isDragging || !dragStartRef.current) return;\n // Use requestAnimationFrame for smoother performance\n requestAnimationFrame(() => {\n // Use the current step size (depends on keyboard modifiers)\n const newValue = positionToValue(event.clientX);\n applyValue(newValue);\n });\n },\n [isDragging, positionToValue, applyValue]\n );\n\n /**\n * Handle global mouse up to end drag\n */\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n setShowTooltipState(false);\n dragStartRef.current = null;\n }, []);\n\n /**\n * Handle keyboard navigation\n */\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (disabled || readOnly) return;\n\n const currentStep = getStepSize();\n let handled = true;\n\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n applyValue(value + currentStep);\n break;\n case 'ArrowLeft':\n case 'ArrowDown':\n applyValue(value - currentStep);\n break;\n case 'Home':\n applyValue(min);\n break;\n case 'End':\n applyValue(max);\n break;\n case 'PageUp':\n applyValue(value + effectiveLargeStep);\n break;\n case 'PageDown':\n applyValue(value - effectiveLargeStep);\n break;\n default:\n handled = false;\n }\n\n if (handled) {\n event.preventDefault();\n }\n\n onKeyDown?.(event);\n },\n [\n disabled,\n readOnly,\n getStepSize,\n applyValue,\n value,\n min,\n max,\n effectiveLargeStep,\n onKeyDown,\n ]\n );\n\n /**\n * Handle focus events\n */\n const handleFocus = useCallback(\n (event: React.FocusEvent<HTMLElement>) => {\n setIsFocused(true);\n onFocus?.(event);\n },\n [onFocus]\n );\n\n const handleBlur = useCallback(\n (event: React.FocusEvent<HTMLElement>) => {\n setIsFocused(false);\n onBlur?.(event);\n },\n [onBlur]\n );\n\n // Global mouse events for dragging\n useEffect(() => {\n if (!isDragging) return;\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, [isDragging, handleMouseMove, handleMouseUp]);\n\n // Generate tick marks\n const ticks = showTicks ? Array.from({ length: tickCount }, (_, i) => i) : [];\n\n const sliderId = React.useId();\n\n return (\n <div\n ref={ref}\n className={cx(sliderContainerRecipe({ disabled }), className)}\n data-testid={testId}\n {...props}\n >\n {label && (\n <FormLabel htmlFor={sliderId} disabled={disabled} required={required}>\n {label}\n </FormLabel>\n )}\n\n <InputWrapper\n size={size}\n error={error}\n disabled={disabled}\n focused={isFocused}\n style={{\n border: 'none',\n background: 'transparent',\n padding: 0,\n height: 5,\n }}\n >\n <div\n ref={sliderRef}\n className={sliderWrapperStyle}\n onMouseDown={handleMouseDown}\n tabIndex={disabled ? -1 : 0}\n role=\"slider\"\n aria-valuemin={min}\n aria-valuemax={max}\n aria-valuenow={clampedValue}\n aria-valuetext={displayValue}\n aria-disabled={disabled}\n aria-readonly={readOnly}\n aria-invalid={error}\n aria-labelledby={label ? sliderId : undefined}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n >\n <div ref={trackRef} className={trackRecipe({ size, error })}>\n <div\n className={fillRecipe({ error, isDragging })}\n style={assignInlineVars({\n [fillPercentageVar]: `${percentage}%`,\n })}\n />\n </div>\n\n <div\n className={thumbRecipe({ size, error, isDragging })}\n style={assignInlineVars({\n [thumbPercentageVar]: `${percentage}%`,\n })}\n />\n\n {showTicks && (\n <div className={ticksStyle}>\n {ticks.map(i => (\n <div key={i} className={tickStyle} />\n ))}\n </div>\n )}\n\n {showTooltip && (\n <div\n className={tooltipRecipe({ visible: showTooltipState })}\n style={assignInlineVars({\n [tooltipPercentageVar]: `${percentage}%`,\n })}\n >\n {displayValue}\n </div>\n )}\n </div>\n </InputWrapper>\n\n {(helperText ?? (error && errorMessage)) && (\n <FormHelperText error={error}>\n {error && errorMessage ? errorMessage : helperText}\n </FormHelperText>\n )}\n </div>\n );\n};\n\nSlider.displayName = 'Slider';\n"],"names":[],"mappings":";;;;;;;;;;;AA+KA;;AAEG;AACH;AACE;AACF;AAEA;;AAEG;AACH;;AAC+B;;;AAG/B;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDG;AACI;;;;AAkCL;AACA;AACA;AAIA;;AAGA;AACA;AACA;;AAGA;;AACuB;AACrB;AAAyC;AACzC;AACF;AACE;AACA;AACA;;;;AAID;;;AAIC;AACA;;AAEE;AAEJ;;AAEG;AACH;;;;;;;AASI;;;AAGF;AAIF;;AAEG;AACH;;AAE2B;;;;;AAOvB;;AAGA;AACA;;AAKJ;;AAEG;AACH;;;;;;;;;;;AAcM;;AAEJ;AAIF;;AAEG;AACH;AAEI;;;;;;;AAMA;;AAKJ;;AAEG;AACH;;;AAGE;;AAGF;;AAEG;AACH;;;AAII;;AAGA;AACE;AACA;AACE;;AAEF;AACA;AACE;;AAEF;;;AAGA;;;AAGA;AACE;;AAEF;AACE;;AAEF;;;;;;AAQF;AACF;;;;;;;;;;AAWC;AAGH;;AAEG;AACH;;AAGI;AACF;AAIF;;AAGI;AACF;;;AAMA;;AAEA;AACA;AAEA;AACE;AACA;AACF;;;AAIF;AAEA;AAEA;AAmBQ;AACA;AACA;AACA;;AAyBM;;AAQF;;AAgBE;AACD;AAef;AAEA;;"}
1
+ {"version":3,"file":"Slider.js","sources":["src/components/controls/Slider/Slider.tsx"],"sourcesContent":["'use client';\n\n// src/components/controls/Slider/Slider.tsx\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, { useRef, useCallback, useState, useEffect } from 'react';\nimport { createPortal } from 'react-dom';\n\nimport { FormHelperText } from '@/components/form/FormHelperText';\nimport { FormLabel } from '@/components/form/FormLabel';\nimport { InputWrapper } from '@/components/form/InputWrapper';\nimport { useKeyboardContext } from '@/context/KeyboardContext';\nimport { cx } from '@/utils/cx';\n\nimport {\n sliderContainerRecipe,\n sliderWrapperStyle,\n trackRecipe,\n fillRecipe,\n fillPercentageVar,\n thumbRecipe,\n thumbPercentageVar,\n ticksStyle,\n tickStyle,\n sliderTooltipRecipe,\n} from './Slider.css';\n\nimport type { BaseComponent, Size } from '@/types/common';\nimport type { Prettify } from '@/types/utilities';\n\n/**\n * Props specific to Slider component\n */\nexport interface SliderBaseProps extends Omit<BaseComponent, 'onChange'> {\n /**\n * Current numeric value\n */\n value: number;\n\n /**\n * Callback when value changes\n */\n onChange: (value: number) => void;\n\n /**\n * Minimum allowed value\n * @default 0\n */\n min?: number;\n\n /**\n * Maximum allowed value\n * @default 100\n */\n max?: number;\n\n /**\n * Step size for value increments\n * @default 1\n */\n step?: number;\n\n /**\n * Step size when Shift is held (precision mode)\n * @default step / 10\n */\n precisionStep?: number;\n\n /**\n * Step size when Ctrl is held (large steps)\n * @default step * 10\n */\n largeStep?: number;\n\n /**\n * Number of decimal places to round to\n */\n precision?: number;\n\n /**\n * Slider size using standard library sizing\n * - `sm`: Compact for toolbars\n * - `md`: Standard for forms\n * - `lg`: Prominent sliders\n * @default \"md\"\n */\n size?: Size;\n\n /**\n * Whether the slider is disabled\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether the slider is read-only\n * @default false\n */\n readOnly?: boolean;\n\n /**\n * Slider label\n */\n label?: string;\n\n /**\n * Helper text displayed below the slider\n */\n helperText?: string;\n\n /**\n * Error message displayed when error is true\n */\n errorMessage?: string;\n\n /**\n * Whether the slider has an error state\n * @default false\n */\n error?: boolean;\n\n /**\n * Whether the slider is required\n * @default false\n */\n required?: boolean;\n\n /**\n * Unit suffix to display (e.g., \"px\", \"%\", \"°\")\n */\n unit?: string;\n\n /**\n * Format value for display\n */\n formatValue?: (value: number) => string;\n\n /**\n * Show value tooltip while dragging\n * @default true\n */\n showTooltip?: boolean;\n\n /**\n * Placement of the value tooltip relative to the thumb. The tooltip is\n * portaled to `document.body`, so it is never clipped by an ancestor's\n * `overflow`.\n * @default \"top\"\n */\n tooltipPlacement?: 'top' | 'bottom';\n\n /**\n * Show tick marks along the track\n * @default false\n */\n showTicks?: boolean;\n\n /**\n * Number of tick marks to show (when showTicks is true)\n * @default 5\n */\n tickCount?: number;\n\n /**\n * Focus event handler\n */\n onFocus?: (event: React.FocusEvent<HTMLElement>) => void;\n\n /**\n * Blur event handler\n */\n onBlur?: (event: React.FocusEvent<HTMLElement>) => void;\n\n /**\n * Key down event handler\n */\n onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;\n}\n\n/**\n * Props for the Slider component with prettified type for better IntelliSense\n */\nexport type SliderProps = Prettify<SliderBaseProps>;\n\n/**\n * Clamps a value between min and max bounds\n */\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\n/**\n * Rounds a number to specified decimal places\n */\nfunction roundToPrecision(value: number, precision?: number): number {\n if (precision === undefined) return value;\n const factor = Math.pow(10, precision);\n return Math.round(value * factor) / factor;\n}\n\n/**\n * A professional slider component with drag interaction and keyboard support.\n *\n * Features:\n * - Smooth drag interaction with visual feedback\n * - Keyboard navigation with arrow keys\n * - Modifier key support: Ctrl (large steps), Shift (precision)\n * - Optional tick marks and value tooltip\n * - Comprehensive theming and size variants\n * - Accessible with proper ARIA attributes\n *\n * @example\n * ```tsx\n * // Basic slider\n * <Slider\n * value={opacity}\n * onChange={setOpacity}\n * min={0}\n * max={1}\n * step={0.1}\n * unit=\"%\"\n * />\n *\n * // Slider with ticks and custom formatting\n * <Slider\n * value={rotation}\n * onChange={setRotation}\n * min={0}\n * max={360}\n * step={15}\n * unit=\"°\"\n * showTicks\n * tickCount={8}\n * formatValue={(v) => `${v}°`}\n * />\n *\n * // Precision slider for fine control\n * <Slider\n * value={scale}\n * onChange={setScale}\n * min={0.1}\n * max={5}\n * step={0.1}\n * precisionStep={0.01}\n * largeStep={1}\n * precision={2}\n * />\n * ```\n */\nexport const Slider: React.FC<SliderProps> = ({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n precisionStep,\n largeStep,\n precision = 2,\n size = 'md',\n disabled = false,\n readOnly = false,\n label,\n helperText,\n errorMessage,\n error = false,\n required = false,\n unit,\n formatValue,\n showTooltip = true,\n tooltipPlacement = 'top',\n showTicks = false,\n tickCount = 5,\n className,\n testId,\n onFocus,\n onBlur,\n onKeyDown,\n ref,\n ...props\n}) => {\n const [isDragging, setIsDragging] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const [showTooltipState, setShowTooltipState] = useState(false);\n // Viewport coordinates of the portaled value tooltip (null until a drag\n // starts). Updated only while dragging so the portal stays off the idle path.\n const [tooltipPos, setTooltipPos] = useState<{\n left: number;\n top: number;\n } | null>(null);\n\n const sliderRef = useRef<HTMLDivElement>(null);\n const trackRef = useRef<HTMLDivElement>(null);\n const dragStartRef = useRef<{ startX: number; startValue: number } | null>(\n null\n );\n\n const { modifiers } = useKeyboardContext();\n\n // Calculate derived values\n const effectivePrecisionStep = precisionStep ?? step / 10;\n const effectiveLargeStep = largeStep ?? step * 10;\n const percentage = ((value - min) / (max - min)) * 100;\n const clampedValue = clamp(value, min, max);\n\n const getStepSize = useCallback((): number => {\n if (modifiers.shift) return effectivePrecisionStep;\n if (modifiers.control || modifiers.meta) return effectiveLargeStep;\n return step;\n }, [\n modifiers.shift,\n modifiers.control,\n modifiers.meta,\n effectivePrecisionStep,\n effectiveLargeStep,\n step,\n ]);\n\n // Format display value with consistent precision to prevent tooltip size changes\n const displayValue = formatValue\n ? formatValue(clampedValue)\n : unit\n ? `${clampedValue.toFixed(precision)}${unit}`\n : clampedValue.toFixed(precision);\n\n /**\n * Applies a new value with proper bounds checking and rounding\n */\n const applyValue = useCallback(\n (newValue: number): void => {\n if (disabled || readOnly) return;\n\n if (newValue === value) return;\n\n const rounded = roundToPrecision(newValue, precision);\n const clamped = clamp(rounded, min, max);\n\n if (clamped !== value) {\n onChange(clamped);\n }\n },\n [disabled, readOnly, precision, min, max, value, onChange]\n );\n\n /**\n * Converts mouse position to value\n */\n const positionToValue = useCallback(\n (clientX: number): number => {\n if (!trackRef.current) return value;\n\n const rect = trackRef.current.getBoundingClientRect();\n const percentage = clamp((clientX - rect.left) / rect.width, 0, 1);\n const newValue = min + percentage * (max - min);\n\n // Use the current step size (depends on keyboard modifiers)\n const currentStep = getStepSize();\n\n // Snap to step increments\n const steps = Math.round((newValue - min) / currentStep);\n return min + steps * currentStep;\n },\n [min, max, value, getStepSize]\n );\n\n /**\n * Computes the portaled tooltip's viewport position for a (snapped) value,\n * anchored to the thumb. Reads the same track rect the drag math already\n * reads, so it adds no extra layout pass per frame.\n */\n const updateTooltipPos = useCallback(\n (v: number): void => {\n const track = trackRef.current;\n if (!track) return;\n const rect = track.getBoundingClientRect();\n const pct = clamp((v - min) / (max - min), 0, 1);\n setTooltipPos({\n left: rect.left + pct * rect.width,\n top: tooltipPlacement === 'bottom' ? rect.bottom : rect.top,\n });\n },\n [min, max, tooltipPlacement]\n );\n\n /**\n * Handle mouse down on track or thumb\n */\n const handleMouseDown = useCallback(\n (event: React.MouseEvent) => {\n if (disabled || readOnly) return;\n\n event.preventDefault();\n setIsDragging(true);\n setShowTooltipState(showTooltip);\n\n // Use the current step size (depends on keyboard modifiers)\n const newValue = positionToValue(event.clientX);\n applyValue(newValue);\n if (showTooltip) updateTooltipPos(newValue);\n\n dragStartRef.current = {\n startX: event.clientX,\n startValue: newValue,\n };\n },\n [\n disabled,\n readOnly,\n positionToValue,\n applyValue,\n showTooltip,\n updateTooltipPos,\n ]\n );\n\n /**\n * Handle global mouse move during drag\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n if (!isDragging || !dragStartRef.current) return;\n // Use requestAnimationFrame for smoother performance\n requestAnimationFrame(() => {\n // Use the current step size (depends on keyboard modifiers)\n const newValue = positionToValue(event.clientX);\n applyValue(newValue);\n if (showTooltip) updateTooltipPos(newValue);\n });\n },\n [isDragging, positionToValue, applyValue, showTooltip, updateTooltipPos]\n );\n\n /**\n * Handle global mouse up to end drag\n */\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n setShowTooltipState(false);\n dragStartRef.current = null;\n }, []);\n\n /**\n * Handle keyboard navigation\n */\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (disabled || readOnly) return;\n\n const currentStep = getStepSize();\n let handled = true;\n\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n applyValue(value + currentStep);\n break;\n case 'ArrowLeft':\n case 'ArrowDown':\n applyValue(value - currentStep);\n break;\n case 'Home':\n applyValue(min);\n break;\n case 'End':\n applyValue(max);\n break;\n case 'PageUp':\n applyValue(value + effectiveLargeStep);\n break;\n case 'PageDown':\n applyValue(value - effectiveLargeStep);\n break;\n default:\n handled = false;\n }\n\n if (handled) {\n event.preventDefault();\n }\n\n onKeyDown?.(event);\n },\n [\n disabled,\n readOnly,\n getStepSize,\n applyValue,\n value,\n min,\n max,\n effectiveLargeStep,\n onKeyDown,\n ]\n );\n\n /**\n * Handle focus events\n */\n const handleFocus = useCallback(\n (event: React.FocusEvent<HTMLElement>) => {\n setIsFocused(true);\n onFocus?.(event);\n },\n [onFocus]\n );\n\n const handleBlur = useCallback(\n (event: React.FocusEvent<HTMLElement>) => {\n setIsFocused(false);\n onBlur?.(event);\n },\n [onBlur]\n );\n\n // Global mouse events for dragging\n useEffect(() => {\n if (!isDragging) return;\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, [isDragging, handleMouseMove, handleMouseUp]);\n\n // Generate tick marks\n const ticks = showTicks ? Array.from({ length: tickCount }, (_, i) => i) : [];\n\n const sliderId = React.useId();\n\n return (\n <div\n ref={ref}\n className={cx(sliderContainerRecipe({ disabled }), className)}\n data-testid={testId}\n {...props}\n >\n {label && (\n <FormLabel htmlFor={sliderId} disabled={disabled} required={required}>\n {label}\n </FormLabel>\n )}\n\n <InputWrapper\n size={size}\n error={error}\n disabled={disabled}\n focused={isFocused}\n style={{\n border: 'none',\n background: 'transparent',\n padding: 0,\n height: 5,\n }}\n >\n <div\n ref={sliderRef}\n className={sliderWrapperStyle}\n onMouseDown={handleMouseDown}\n tabIndex={disabled ? -1 : 0}\n role=\"slider\"\n aria-valuemin={min}\n aria-valuemax={max}\n aria-valuenow={clampedValue}\n aria-valuetext={displayValue}\n aria-disabled={disabled}\n aria-readonly={readOnly}\n aria-invalid={error}\n aria-labelledby={label ? sliderId : undefined}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n >\n <div ref={trackRef} className={trackRecipe({ size, error })}>\n <div\n className={fillRecipe({ error, isDragging })}\n style={assignInlineVars({\n [fillPercentageVar]: `${percentage}%`,\n })}\n />\n </div>\n\n <div\n className={thumbRecipe({ size, error, isDragging })}\n style={assignInlineVars({\n [thumbPercentageVar]: `${percentage}%`,\n })}\n />\n\n {showTicks && (\n <div className={ticksStyle}>\n {ticks.map(i => (\n <div key={i} className={tickStyle} />\n ))}\n </div>\n )}\n\n {showTooltip &&\n showTooltipState &&\n tooltipPos &&\n typeof document !== 'undefined' &&\n createPortal(\n <div\n className={sliderTooltipRecipe({ placement: tooltipPlacement })}\n style={{ left: tooltipPos.left, top: tooltipPos.top }}\n role=\"presentation\"\n data-testid={testId ? `${testId}-tooltip` : undefined}\n >\n {displayValue}\n </div>,\n document.body\n )}\n </div>\n </InputWrapper>\n\n {(helperText ?? (error && errorMessage)) && (\n <FormHelperText error={error}>\n {error && errorMessage ? errorMessage : helperText}\n </FormHelperText>\n )}\n </div>\n );\n};\n\nSlider.displayName = 'Slider';\n"],"names":[],"mappings":";;;;;;;;;;;;AAuLA;;AAEG;AACH;AACE;AACF;AAEA;;AAEG;AACH;;AAC+B;;;AAG/B;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDG;;;;;;;;AA0CD;AACA;AACA;AAIA;;AAGA;AACA;AACA;;AAGA;;AACuB;AACrB;AAAyC;AACzC;AACF;AACE;AACA;AACA;;;;AAID;;;AAIC;AACA;;AAEE;AAEJ;;AAEG;AACH;;;;;;;AASI;;;AAGF;AAIF;;AAEG;AACH;;AAE2B;;;;;AAOvB;;AAGA;AACA;;AAKJ;;;;AAIG;AACH;AAEI;AACA;;AACA;;AAEA;;AAEE;AACD;;AAKL;;AAEG;AACH;;;;;;;;;AAWI;;;;AAIE;;AAEJ;;;;;;;AAQC;AAGH;;AAEG;AACH;AAEI;;;;;;;AAME;;AACF;AACF;AAIF;;AAEG;AACH;;;AAGE;;AAGF;;AAEG;AACH;;;AAII;;AAGA;AACE;AACA;AACE;;AAEF;AACA;AACE;;AAEF;;;AAGA;;;AAGA;AACE;;AAEF;AACE;;AAEF;;;;;;AAQF;AACF;;;;;;;;;;AAWC;AAGH;;AAEG;AACH;;AAGI;AACF;AAIF;;AAGI;AACF;;;AAMA;;AAEA;AACA;AAEA;AACE;AACA;AACF;;;AAIF;AAEA;AAEA;AAmBQ;AACA;AACA;AACA;;AAyBM;;AAQF;AACD;;;;AAeD;AAqBZ;AAEA;;"}
@@ -9,7 +9,7 @@ import { gizmoCanvasStyle, ariaLiveRegionStyle, gizmoDiameterVar, gizmoWrapperRe
9
9
 
10
10
  const DEFAULT_LABELS = { x: 'X', y: 'Y', z: 'Z' };
11
11
  const DEFAULT_VISIBLE = { x: true, y: true, z: true };
12
- const ViewportGizmo = ({ orientation, upAxis = 'y-up', axisColorPreset = 'blender', axisConfig, showLabels = true, showNegativeAxes = true, showOrbitRing = true, showOriginHandle = true, background = 'subtle', interactionMode = 'full', orbitSpeed = 1, constrainPitch = true, onOrbit, onOrbitEnd, onSnapToView, onAxisClick, onOriginClick, diameter = 120, size = 'md', disabled = false, className, testId, id, style, ...rest }) => {
12
+ const ViewportGizmo = ({ orientation, upAxis = 'y-up', axisColorPreset = 'blender', axisConfig, showLabels = true, showNegativeAxes = true, showOrbitRing = true, showOriginHandle = true, background = 'subtle', interactionMode = 'full', orbitSpeed = 1, constrainPitch = true, invertYaw = false, invertPitch = false, onOrbit, onOrbitEnd, onSnapToView, onAxisClick, onOriginClick, diameter = 120, size = 'md', disabled = false, className, testId, id, style, ...rest }) => {
13
13
  const canvasRef = useRef(null);
14
14
  // Resolve axis colors from preset or custom config
15
15
  const axisColors = useMemo(() => {
@@ -51,6 +51,8 @@ const ViewportGizmo = ({ orientation, upAxis = 'y-up', axisColorPreset = 'blende
51
51
  interactionMode,
52
52
  orbitSpeed,
53
53
  constrainPitch,
54
+ invertYaw,
55
+ invertPitch,
54
56
  disabled,
55
57
  onOrbit,
56
58
  onOrbitEnd,
@@ -1 +1 @@
1
- {"version":3,"file":"ViewportGizmo.js","sources":["src/components/editor/ViewportGizmo/ViewportGizmo.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, { useRef, useMemo } from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { useGizmoInteraction } from './useGizmoInteraction';\nimport { useGizmoRenderer, AXIS_COLORS } from './useGizmoRenderer';\nimport {\n gizmoWrapperRecipe,\n gizmoCanvasStyle,\n gizmoDiameterVar,\n ariaLiveRegionStyle,\n} from './ViewportGizmo.css';\n\nimport type { ViewportGizmoProps } from './ViewportGizmo.types';\n\nconst DEFAULT_LABELS: Record<string, string> = { x: 'X', y: 'Y', z: 'Z' };\nconst DEFAULT_VISIBLE: Record<string, boolean> = { x: true, y: true, z: true };\n\nexport const ViewportGizmo: React.FC<ViewportGizmoProps> = ({\n orientation,\n upAxis = 'y-up',\n axisColorPreset = 'blender',\n axisConfig,\n showLabels = true,\n showNegativeAxes = true,\n showOrbitRing = true,\n showOriginHandle = true,\n background = 'subtle',\n interactionMode = 'full',\n orbitSpeed = 1,\n constrainPitch = true,\n onOrbit,\n onOrbitEnd,\n onSnapToView,\n onAxisClick,\n onOriginClick,\n diameter = 120,\n size = 'md',\n disabled = false,\n className,\n testId,\n id,\n style,\n ...rest\n}) => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // Resolve axis colors from preset or custom config\n const axisColors = useMemo((): Record<string, string> => {\n if (axisColorPreset === 'custom' && axisConfig) {\n return {\n x: axisConfig[0].color ?? '#E63946',\n y: axisConfig[1].color ?? '#6AA84F',\n z: axisConfig[2].color ?? '#4A86C8',\n };\n }\n return { ...AXIS_COLORS };\n }, [axisColorPreset, axisConfig]);\n\n const axisLabels = useMemo((): Record<string, string> => {\n if (axisConfig) {\n return {\n x: axisConfig[0].label ?? 'X',\n y: axisConfig[1].label ?? 'Y',\n z: axisConfig[2].label ?? 'Z',\n };\n }\n return DEFAULT_LABELS;\n }, [axisConfig]);\n\n const axisVisible = useMemo((): Record<string, boolean> => {\n if (axisConfig) {\n return {\n x: axisConfig[0].visible ?? true,\n y: axisConfig[1].visible ?? true,\n z: axisConfig[2].visible ?? true,\n };\n }\n return DEFAULT_VISIBLE;\n }, [axisConfig]);\n\n // Interaction\n const interaction = useGizmoInteraction({\n orientation,\n canvasRef,\n diameter,\n upAxis,\n interactionMode,\n orbitSpeed,\n constrainPitch,\n disabled,\n onOrbit,\n onOrbitEnd,\n onSnapToView,\n onAxisClick,\n onOriginClick,\n });\n\n // Renderer\n useGizmoRenderer({\n canvasRef,\n orientation,\n diameter,\n upAxis,\n size,\n axisColors,\n axisLabels,\n axisVisible,\n showLabels,\n showNegativeAxes,\n showOrbitRing,\n showOriginHandle,\n background,\n disabled,\n isDragging: interaction.isDragging,\n hoveredRegion: interaction.hoveredRegion,\n });\n\n // ARIA\n const ariaDescription = `3D orientation gizmo, yaw: ${Math.round(orientation.yaw)}deg, pitch: ${Math.round(orientation.pitch)}deg`;\n\n return (\n <div\n className={cx(gizmoWrapperRecipe({ background, disabled }), className)}\n style={{\n ...style,\n ...assignInlineVars({ [gizmoDiameterVar]: `${diameter}px` }),\n }}\n data-testid={testId}\n id={id}\n {...rest}\n >\n <canvas\n ref={canvasRef}\n className={gizmoCanvasStyle({ disabled })}\n // The canvas is a self-contained interactive widget driven entirely by\n // custom pointer/keyboard handlers, so it is exposed as an ARIA\n // application rather than its implicit canvas role.\n // eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role\n role=\"application\"\n aria-label=\"Viewport orientation gizmo\"\n aria-roledescription={ariaDescription}\n tabIndex={disabled ? -1 : 0}\n onPointerDown={interaction.handlers.onPointerDown}\n onPointerMove={interaction.handlers.onPointerMove}\n onPointerUp={interaction.handlers.onPointerUp}\n onKeyDown={interaction.handlers.onKeyDown}\n />\n <div className={ariaLiveRegionStyle} aria-live=\"polite\" role=\"status\">\n {ariaDescription}\n </div>\n </div>\n );\n};\n\nViewportGizmo.displayName = 'ViewportGizmo';\n"],"names":[],"mappings":";;;;;;;;;AAkBA;AACA;AAEO;AA2BL;;AAGA;AACE;;;;;;;AAOA;AACF;AAEA;;;;;;;;AAQE;AACF;AAEA;;;;;;;;AAQE;AACF;;;;;;;;;;;;;;;;AAiBC;;AAGD;;;;;;;;;;;;;;;;AAiBC;;;AAKD;AAIM;;;;;;;AAcA;AAcR;AAEA;;"}
1
+ {"version":3,"file":"ViewportGizmo.js","sources":["src/components/editor/ViewportGizmo/ViewportGizmo.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, { useRef, useMemo } from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { useGizmoInteraction } from './useGizmoInteraction';\nimport { useGizmoRenderer, AXIS_COLORS } from './useGizmoRenderer';\nimport {\n gizmoWrapperRecipe,\n gizmoCanvasStyle,\n gizmoDiameterVar,\n ariaLiveRegionStyle,\n} from './ViewportGizmo.css';\n\nimport type { ViewportGizmoProps } from './ViewportGizmo.types';\n\nconst DEFAULT_LABELS: Record<string, string> = { x: 'X', y: 'Y', z: 'Z' };\nconst DEFAULT_VISIBLE: Record<string, boolean> = { x: true, y: true, z: true };\n\nexport const ViewportGizmo: React.FC<ViewportGizmoProps> = ({\n orientation,\n upAxis = 'y-up',\n axisColorPreset = 'blender',\n axisConfig,\n showLabels = true,\n showNegativeAxes = true,\n showOrbitRing = true,\n showOriginHandle = true,\n background = 'subtle',\n interactionMode = 'full',\n orbitSpeed = 1,\n constrainPitch = true,\n invertYaw = false,\n invertPitch = false,\n onOrbit,\n onOrbitEnd,\n onSnapToView,\n onAxisClick,\n onOriginClick,\n diameter = 120,\n size = 'md',\n disabled = false,\n className,\n testId,\n id,\n style,\n ...rest\n}) => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // Resolve axis colors from preset or custom config\n const axisColors = useMemo((): Record<string, string> => {\n if (axisColorPreset === 'custom' && axisConfig) {\n return {\n x: axisConfig[0].color ?? '#E63946',\n y: axisConfig[1].color ?? '#6AA84F',\n z: axisConfig[2].color ?? '#4A86C8',\n };\n }\n return { ...AXIS_COLORS };\n }, [axisColorPreset, axisConfig]);\n\n const axisLabels = useMemo((): Record<string, string> => {\n if (axisConfig) {\n return {\n x: axisConfig[0].label ?? 'X',\n y: axisConfig[1].label ?? 'Y',\n z: axisConfig[2].label ?? 'Z',\n };\n }\n return DEFAULT_LABELS;\n }, [axisConfig]);\n\n const axisVisible = useMemo((): Record<string, boolean> => {\n if (axisConfig) {\n return {\n x: axisConfig[0].visible ?? true,\n y: axisConfig[1].visible ?? true,\n z: axisConfig[2].visible ?? true,\n };\n }\n return DEFAULT_VISIBLE;\n }, [axisConfig]);\n\n // Interaction\n const interaction = useGizmoInteraction({\n orientation,\n canvasRef,\n diameter,\n upAxis,\n interactionMode,\n orbitSpeed,\n constrainPitch,\n invertYaw,\n invertPitch,\n disabled,\n onOrbit,\n onOrbitEnd,\n onSnapToView,\n onAxisClick,\n onOriginClick,\n });\n\n // Renderer\n useGizmoRenderer({\n canvasRef,\n orientation,\n diameter,\n upAxis,\n size,\n axisColors,\n axisLabels,\n axisVisible,\n showLabels,\n showNegativeAxes,\n showOrbitRing,\n showOriginHandle,\n background,\n disabled,\n isDragging: interaction.isDragging,\n hoveredRegion: interaction.hoveredRegion,\n });\n\n // ARIA\n const ariaDescription = `3D orientation gizmo, yaw: ${Math.round(orientation.yaw)}deg, pitch: ${Math.round(orientation.pitch)}deg`;\n\n return (\n <div\n className={cx(gizmoWrapperRecipe({ background, disabled }), className)}\n style={{\n ...style,\n ...assignInlineVars({ [gizmoDiameterVar]: `${diameter}px` }),\n }}\n data-testid={testId}\n id={id}\n {...rest}\n >\n <canvas\n ref={canvasRef}\n className={gizmoCanvasStyle({ disabled })}\n // The canvas is a self-contained interactive widget driven entirely by\n // custom pointer/keyboard handlers, so it is exposed as an ARIA\n // application rather than its implicit canvas role.\n // eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role\n role=\"application\"\n aria-label=\"Viewport orientation gizmo\"\n aria-roledescription={ariaDescription}\n tabIndex={disabled ? -1 : 0}\n onPointerDown={interaction.handlers.onPointerDown}\n onPointerMove={interaction.handlers.onPointerMove}\n onPointerUp={interaction.handlers.onPointerUp}\n onKeyDown={interaction.handlers.onKeyDown}\n />\n <div className={ariaLiveRegionStyle} aria-live=\"polite\" role=\"status\">\n {ariaDescription}\n </div>\n </div>\n );\n};\n\nViewportGizmo.displayName = 'ViewportGizmo';\n"],"names":[],"mappings":";;;;;;;;;AAkBA;AACA;;AA+BE;;AAGA;AACE;;;;;;;AAOA;AACF;AAEA;;;;;;;;AAQE;AACF;AAEA;;;;;;;;AAQE;AACF;;;;;;;;;;;;;;;;;;AAmBC;;AAGD;;;;;;;;;;;;;;;;AAiBC;;;AAKD;AAIM;;;;;;;AAcA;AAcR;AAEA;;"}
@@ -7,7 +7,7 @@ const NONE_HIT = { type: 'none', distance: Infinity };
7
7
  const ORBIT_KEY_STEP = 15;
8
8
  const ORBIT_FINE_STEP = 5;
9
9
  function useGizmoInteraction(options) {
10
- const { orientation, canvasRef, diameter, upAxis, interactionMode, orbitSpeed, constrainPitch, disabled, onOrbit, onOrbitEnd, onSnapToView, onAxisClick, onOriginClick, } = options;
10
+ const { orientation, canvasRef, diameter, upAxis, interactionMode, orbitSpeed, constrainPitch, invertYaw, invertPitch, disabled, onOrbit, onOrbitEnd, onSnapToView, onAxisClick, onOriginClick, } = options;
11
11
  const [hoveredRegion, setHoveredRegion] = useState(NONE_HIT);
12
12
  // Ref drives synchronous reads inside the pointer handlers; the state mirror
13
13
  // is the value exposed for rendering (kept in sync on drag start/end).
@@ -27,6 +27,19 @@ function useGizmoInteraction(options) {
27
27
  const rect = canvas.getBoundingClientRect();
28
28
  return { px: e.clientX - rect.left, py: e.clientY - rect.top };
29
29
  }, [canvasRef]);
30
+ // Single emission point for orbit deltas so the pointer and keyboard paths
31
+ // stay consistent. Inversion is applied last, then pitch is constrained
32
+ // against the value that will actually be applied — this keeps the
33
+ // constraint correct at both poles regardless of `invertPitch`.
34
+ const emitOrbit = useCallback((rawYaw, rawPitch) => {
35
+ const deltaYaw = invertYaw ? -rawYaw : rawYaw;
36
+ let deltaPitch = invertPitch ? -rawPitch : rawPitch;
37
+ if (constrainPitch) {
38
+ deltaPitch =
39
+ clamp(orientation.pitch + deltaPitch, -90, 90) - orientation.pitch;
40
+ }
41
+ onOrbit?.({ deltaYaw, deltaPitch });
42
+ }, [invertYaw, invertPitch, constrainPitch, orientation.pitch, onOrbit]);
30
43
  const onPointerDown = useCallback((e) => {
31
44
  if (disabled || interactionMode === 'display-only' || e.button !== 0)
32
45
  return;
@@ -67,13 +80,7 @@ function useGizmoInteraction(options) {
67
80
  hasDraggedRef.current = true;
68
81
  }
69
82
  if (hasDraggedRef.current) {
70
- const deltaYaw = dx * orbitSpeed * 0.5;
71
- let deltaPitch = -dy * orbitSpeed * 0.5;
72
- if (constrainPitch) {
73
- const newPitch = orientation.pitch + deltaPitch;
74
- deltaPitch = clamp(newPitch, -90, 90) - orientation.pitch;
75
- }
76
- onOrbit?.({ deltaYaw, deltaPitch });
83
+ emitOrbit(dx * orbitSpeed * 0.5, -dy * orbitSpeed * 0.5);
77
84
  }
78
85
  lastPointerRef.current = { x: e.clientX, y: e.clientY };
79
86
  return;
@@ -101,7 +108,7 @@ function useGizmoInteraction(options) {
101
108
  canvasRef,
102
109
  allowOrbit,
103
110
  orbitSpeed,
104
- constrainPitch,
111
+ emitOrbit,
105
112
  orientation,
106
113
  interactionMode,
107
114
  disabled,
@@ -109,7 +116,6 @@ function useGizmoInteraction(options) {
109
116
  center,
110
117
  armLength,
111
118
  upAxis,
112
- onOrbit,
113
119
  ]);
114
120
  const onPointerUp = useCallback((e) => {
115
121
  const canvas = canvasRef.current;
@@ -166,35 +172,25 @@ function useGizmoInteraction(options) {
166
172
  switch (e.key) {
167
173
  case 'ArrowLeft':
168
174
  if (allowOrbit) {
169
- onOrbit?.({ deltaYaw: -step, deltaPitch: 0 });
175
+ emitOrbit(-step, 0);
170
176
  handled = true;
171
177
  }
172
178
  break;
173
179
  case 'ArrowRight':
174
180
  if (allowOrbit) {
175
- onOrbit?.({ deltaYaw: step, deltaPitch: 0 });
181
+ emitOrbit(step, 0);
176
182
  handled = true;
177
183
  }
178
184
  break;
179
185
  case 'ArrowUp':
180
186
  if (allowOrbit) {
181
- let deltaPitch = step;
182
- if (constrainPitch) {
183
- deltaPitch =
184
- clamp(orientation.pitch + step, -90, 90) - orientation.pitch;
185
- }
186
- onOrbit?.({ deltaYaw: 0, deltaPitch });
187
+ emitOrbit(0, step);
187
188
  handled = true;
188
189
  }
189
190
  break;
190
191
  case 'ArrowDown':
191
192
  if (allowOrbit) {
192
- let deltaPitch = -step;
193
- if (constrainPitch) {
194
- deltaPitch =
195
- clamp(orientation.pitch - step, -90, 90) - orientation.pitch;
196
- }
197
- onOrbit?.({ deltaYaw: 0, deltaPitch });
193
+ emitOrbit(0, -step);
198
194
  handled = true;
199
195
  }
200
196
  break;
@@ -230,9 +226,7 @@ function useGizmoInteraction(options) {
230
226
  interactionMode,
231
227
  allowOrbit,
232
228
  allowSnap,
233
- constrainPitch,
234
- orientation,
235
- onOrbit,
229
+ emitOrbit,
236
230
  onSnapToView,
237
231
  onOriginClick,
238
232
  ]);
@@ -1 +1 @@
1
- {"version":3,"file":"useGizmoInteraction.js","sources":["src/components/editor/ViewportGizmo/useGizmoInteraction.ts"],"sourcesContent":["'use client';\n\nimport { useState, useCallback, useMemo, useRef } from 'react';\n\nimport { clamp } from '@/utils/mathUtils';\n\nimport { gizmoHitTest, axisToPresetView } from './gizmoMath';\n\nimport type {\n GizmoOrientation,\n GizmoUpAxis,\n GizmoPresetView,\n GizmoHitRegion,\n OrbitDelta,\n} from './ViewportGizmo.types';\nimport type React from 'react';\n\ninterface UseGizmoInteractionOptions {\n orientation: GizmoOrientation;\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n diameter: number;\n upAxis: GizmoUpAxis;\n interactionMode: 'full' | 'snap-only' | 'orbit-only' | 'display-only';\n orbitSpeed: number;\n constrainPitch: boolean;\n disabled: boolean;\n onOrbit?: (delta: OrbitDelta) => void;\n onOrbitEnd?: (finalOrientation: GizmoOrientation) => void;\n onSnapToView?: (view: GizmoPresetView) => void;\n onAxisClick?: (axis: 'x' | 'y' | 'z', positive: boolean) => void;\n onOriginClick?: () => void;\n}\n\ninterface UseGizmoInteractionReturn {\n isDragging: boolean;\n hoveredRegion: GizmoHitRegion;\n handlers: {\n onPointerDown: (e: React.PointerEvent) => void;\n onPointerMove: (e: React.PointerEvent) => void;\n onPointerUp: (e: React.PointerEvent) => void;\n onKeyDown: (e: React.KeyboardEvent) => void;\n };\n}\n\nconst NONE_HIT: GizmoHitRegion = { type: 'none', distance: Infinity };\n\nconst ORBIT_KEY_STEP = 15;\nconst ORBIT_FINE_STEP = 5;\n\nexport function useGizmoInteraction(\n options: UseGizmoInteractionOptions\n): UseGizmoInteractionReturn {\n const {\n orientation,\n canvasRef,\n diameter,\n upAxis,\n interactionMode,\n orbitSpeed,\n constrainPitch,\n disabled,\n onOrbit,\n onOrbitEnd,\n onSnapToView,\n onAxisClick,\n onOriginClick,\n } = options;\n\n const [hoveredRegion, setHoveredRegion] = useState<GizmoHitRegion>(NONE_HIT);\n // Ref drives synchronous reads inside the pointer handlers; the state mirror\n // is the value exposed for rendering (kept in sync on drag start/end).\n const isDraggingRef = useRef(false);\n const [isDragging, setIsDragging] = useState(false);\n const lastPointerRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n const startPointerRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n const hasDraggedRef = useRef(false);\n\n const allowSnap =\n interactionMode === 'full' || interactionMode === 'snap-only';\n const allowOrbit =\n interactionMode === 'full' || interactionMode === 'orbit-only';\n\n const armLength = (diameter / 2) * 0.65;\n const center = useMemo(\n () => ({ x: diameter / 2, y: diameter / 2 }),\n [diameter]\n );\n\n const getPointerPos = useCallback(\n (e: React.PointerEvent) => {\n const canvas = canvasRef.current;\n if (!canvas) return { px: 0, py: 0 };\n const rect = canvas.getBoundingClientRect();\n return { px: e.clientX - rect.left, py: e.clientY - rect.top };\n },\n [canvasRef]\n );\n\n const onPointerDown = useCallback(\n (e: React.PointerEvent) => {\n if (disabled || interactionMode === 'display-only' || e.button !== 0)\n return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const { px, py } = getPointerPos(e);\n\n isDraggingRef.current = true;\n hasDraggedRef.current = false;\n lastPointerRef.current = { x: e.clientX, y: e.clientY };\n startPointerRef.current = { x: e.clientX, y: e.clientY };\n setIsDragging(true);\n canvas.setPointerCapture(e.pointerId);\n\n // Store hit for click detection on pointer up\n const hit = gizmoHitTest(px, py, orientation, center, armLength, upAxis);\n setHoveredRegion(hit);\n },\n [\n disabled,\n interactionMode,\n canvasRef,\n getPointerPos,\n orientation,\n center,\n armLength,\n upAxis,\n ]\n );\n\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n if (isDraggingRef.current && allowOrbit) {\n const dx = e.clientX - lastPointerRef.current.x;\n const dy = e.clientY - lastPointerRef.current.y;\n\n // Only count as drag if moved > 3px from start\n const totalDx = e.clientX - startPointerRef.current.x;\n const totalDy = e.clientY - startPointerRef.current.y;\n if (Math.sqrt(totalDx ** 2 + totalDy ** 2) > 3) {\n hasDraggedRef.current = true;\n }\n\n if (hasDraggedRef.current) {\n const deltaYaw = dx * orbitSpeed * 0.5;\n let deltaPitch = -dy * orbitSpeed * 0.5;\n\n if (constrainPitch) {\n const newPitch = orientation.pitch + deltaPitch;\n deltaPitch = clamp(newPitch, -90, 90) - orientation.pitch;\n }\n\n onOrbit?.({ deltaYaw, deltaPitch });\n }\n\n lastPointerRef.current = { x: e.clientX, y: e.clientY };\n return;\n }\n\n // Hover detection\n if (interactionMode === 'display-only') return;\n const { px, py } = getPointerPos(e);\n const hit = gizmoHitTest(px, py, orientation, center, armLength, upAxis);\n setHoveredRegion(hit);\n\n // Cursor\n if (disabled) {\n canvas.style.cursor = 'default';\n } else if (hit.type === 'axis-positive' || hit.type === 'origin') {\n canvas.style.cursor = 'pointer';\n } else if (allowOrbit) {\n canvas.style.cursor = isDraggingRef.current ? 'grabbing' : 'grab';\n } else {\n canvas.style.cursor = 'default';\n }\n },\n [\n canvasRef,\n allowOrbit,\n orbitSpeed,\n constrainPitch,\n orientation,\n interactionMode,\n disabled,\n getPointerPos,\n center,\n armLength,\n upAxis,\n onOrbit,\n ]\n );\n\n const onPointerUp = useCallback(\n (e: React.PointerEvent) => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n if (isDraggingRef.current) {\n isDraggingRef.current = false;\n setIsDragging(false);\n canvas.releasePointerCapture(e.pointerId);\n\n if (hasDraggedRef.current) {\n // Finished orbit drag\n onOrbitEnd?.(orientation);\n } else if (allowSnap) {\n // Click (no drag) — check hit\n const { px, py } = getPointerPos(e);\n const hit = gizmoHitTest(\n px,\n py,\n orientation,\n center,\n armLength,\n upAxis\n );\n\n if (hit.type === 'origin') {\n onOriginClick?.();\n } else if (hit.type === 'axis-positive' && hit.axis) {\n onAxisClick?.(hit.axis, true);\n const view = axisToPresetView(hit.axis, true, upAxis);\n if (view) onSnapToView?.(view);\n } else if (hit.type === 'axis-negative' && hit.axis) {\n onAxisClick?.(hit.axis, false);\n const view = axisToPresetView(hit.axis, false, upAxis);\n if (view) onSnapToView?.(view);\n }\n }\n }\n },\n [\n canvasRef,\n allowSnap,\n getPointerPos,\n orientation,\n center,\n armLength,\n upAxis,\n onOrbitEnd,\n onOriginClick,\n onAxisClick,\n onSnapToView,\n ]\n );\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled || interactionMode === 'display-only') return;\n\n const fine = e.shiftKey;\n const step = fine ? ORBIT_FINE_STEP : ORBIT_KEY_STEP;\n let handled = false;\n\n switch (e.key) {\n case 'ArrowLeft':\n if (allowOrbit) {\n onOrbit?.({ deltaYaw: -step, deltaPitch: 0 });\n handled = true;\n }\n break;\n case 'ArrowRight':\n if (allowOrbit) {\n onOrbit?.({ deltaYaw: step, deltaPitch: 0 });\n handled = true;\n }\n break;\n case 'ArrowUp':\n if (allowOrbit) {\n let deltaPitch = step;\n if (constrainPitch) {\n deltaPitch =\n clamp(orientation.pitch + step, -90, 90) - orientation.pitch;\n }\n onOrbit?.({ deltaYaw: 0, deltaPitch });\n handled = true;\n }\n break;\n case 'ArrowDown':\n if (allowOrbit) {\n let deltaPitch = -step;\n if (constrainPitch) {\n deltaPitch =\n clamp(orientation.pitch - step, -90, 90) - orientation.pitch;\n }\n onOrbit?.({ deltaYaw: 0, deltaPitch });\n handled = true;\n }\n break;\n case '1':\n if (allowSnap) {\n onSnapToView?.(e.ctrlKey ? 'back' : 'front');\n handled = true;\n }\n break;\n case '3':\n if (allowSnap) {\n onSnapToView?.(e.ctrlKey ? 'left' : 'right');\n handled = true;\n }\n break;\n case '7':\n if (allowSnap) {\n onSnapToView?.(e.ctrlKey ? 'bottom' : 'top');\n handled = true;\n }\n break;\n case '5':\n case 'Home':\n onOriginClick?.();\n handled = true;\n break;\n }\n\n if (handled) {\n e.preventDefault();\n }\n },\n [\n disabled,\n interactionMode,\n allowOrbit,\n allowSnap,\n constrainPitch,\n orientation,\n onOrbit,\n onSnapToView,\n onOriginClick,\n ]\n );\n\n return {\n isDragging,\n hoveredRegion,\n handlers: {\n onPointerDown,\n onPointerMove,\n onPointerUp,\n onKeyDown,\n },\n };\n}\n"],"names":[],"mappings":";;;;;AA4CA;AAEA;AACA;AAEM;AAGJ;;;;AAmBA;;AAEA;AACA;AACA;;;;;AAaA;AAEI;AACA;;AACA;;AAEF;AAIF;;;AAII;AACA;;;AAIA;AACA;AACA;AACA;;AAEA;;AAGA;;AAEF;;;;;;;;;AAUC;AAGH;AAEI;AACA;;AAEA;;;;;;AAOE;AACE;;AAGF;AACE;;;AAIE;AACA;;;;AAMJ;;;;;;;AAOF;;;;AAKE;;AACK;AACL;;;AAEA;;;AAEA;;AAEJ;;;;;;;;;;;;;AAcC;AAGH;AAEI;AACA;;AAEA;AACE;;AAEA;AAEA;;AAEE;;;;;AAIA;AASA;;;;;AAIE;AACA;AAAU;;;;AAGV;AACA;AAAU;;;;AAIlB;;;;;;;;;;;;AAaC;AAGH;AAEI;;AAEA;;;AAIA;AACE;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;;;;AAKQ;;;;;;AAMR;;AAEI;;;AAGI;;;;;;AAMR;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;AACA;;;;;;;;AASJ;;;;;;;;;;AAWC;;;;AAMD;;;;;AAKC;;AAEL;;"}
1
+ {"version":3,"file":"useGizmoInteraction.js","sources":["src/components/editor/ViewportGizmo/useGizmoInteraction.ts"],"sourcesContent":["'use client';\n\nimport { useState, useCallback, useMemo, useRef } from 'react';\n\nimport { clamp } from '@/utils/mathUtils';\n\nimport { gizmoHitTest, axisToPresetView } from './gizmoMath';\n\nimport type {\n GizmoOrientation,\n GizmoUpAxis,\n GizmoPresetView,\n GizmoHitRegion,\n OrbitDelta,\n} from './ViewportGizmo.types';\nimport type React from 'react';\n\ninterface UseGizmoInteractionOptions {\n orientation: GizmoOrientation;\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n diameter: number;\n upAxis: GizmoUpAxis;\n interactionMode: 'full' | 'snap-only' | 'orbit-only' | 'display-only';\n orbitSpeed: number;\n constrainPitch: boolean;\n invertYaw: boolean;\n invertPitch: boolean;\n disabled: boolean;\n onOrbit?: (delta: OrbitDelta) => void;\n onOrbitEnd?: (finalOrientation: GizmoOrientation) => void;\n onSnapToView?: (view: GizmoPresetView) => void;\n onAxisClick?: (axis: 'x' | 'y' | 'z', positive: boolean) => void;\n onOriginClick?: () => void;\n}\n\ninterface UseGizmoInteractionReturn {\n isDragging: boolean;\n hoveredRegion: GizmoHitRegion;\n handlers: {\n onPointerDown: (e: React.PointerEvent) => void;\n onPointerMove: (e: React.PointerEvent) => void;\n onPointerUp: (e: React.PointerEvent) => void;\n onKeyDown: (e: React.KeyboardEvent) => void;\n };\n}\n\nconst NONE_HIT: GizmoHitRegion = { type: 'none', distance: Infinity };\n\nconst ORBIT_KEY_STEP = 15;\nconst ORBIT_FINE_STEP = 5;\n\nexport function useGizmoInteraction(\n options: UseGizmoInteractionOptions\n): UseGizmoInteractionReturn {\n const {\n orientation,\n canvasRef,\n diameter,\n upAxis,\n interactionMode,\n orbitSpeed,\n constrainPitch,\n invertYaw,\n invertPitch,\n disabled,\n onOrbit,\n onOrbitEnd,\n onSnapToView,\n onAxisClick,\n onOriginClick,\n } = options;\n\n const [hoveredRegion, setHoveredRegion] = useState<GizmoHitRegion>(NONE_HIT);\n // Ref drives synchronous reads inside the pointer handlers; the state mirror\n // is the value exposed for rendering (kept in sync on drag start/end).\n const isDraggingRef = useRef(false);\n const [isDragging, setIsDragging] = useState(false);\n const lastPointerRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n const startPointerRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n const hasDraggedRef = useRef(false);\n\n const allowSnap =\n interactionMode === 'full' || interactionMode === 'snap-only';\n const allowOrbit =\n interactionMode === 'full' || interactionMode === 'orbit-only';\n\n const armLength = (diameter / 2) * 0.65;\n const center = useMemo(\n () => ({ x: diameter / 2, y: diameter / 2 }),\n [diameter]\n );\n\n const getPointerPos = useCallback(\n (e: React.PointerEvent) => {\n const canvas = canvasRef.current;\n if (!canvas) return { px: 0, py: 0 };\n const rect = canvas.getBoundingClientRect();\n return { px: e.clientX - rect.left, py: e.clientY - rect.top };\n },\n [canvasRef]\n );\n\n // Single emission point for orbit deltas so the pointer and keyboard paths\n // stay consistent. Inversion is applied last, then pitch is constrained\n // against the value that will actually be applied — this keeps the\n // constraint correct at both poles regardless of `invertPitch`.\n const emitOrbit = useCallback(\n (rawYaw: number, rawPitch: number) => {\n const deltaYaw = invertYaw ? -rawYaw : rawYaw;\n let deltaPitch = invertPitch ? -rawPitch : rawPitch;\n\n if (constrainPitch) {\n deltaPitch =\n clamp(orientation.pitch + deltaPitch, -90, 90) - orientation.pitch;\n }\n\n onOrbit?.({ deltaYaw, deltaPitch });\n },\n [invertYaw, invertPitch, constrainPitch, orientation.pitch, onOrbit]\n );\n\n const onPointerDown = useCallback(\n (e: React.PointerEvent) => {\n if (disabled || interactionMode === 'display-only' || e.button !== 0)\n return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const { px, py } = getPointerPos(e);\n\n isDraggingRef.current = true;\n hasDraggedRef.current = false;\n lastPointerRef.current = { x: e.clientX, y: e.clientY };\n startPointerRef.current = { x: e.clientX, y: e.clientY };\n setIsDragging(true);\n canvas.setPointerCapture(e.pointerId);\n\n // Store hit for click detection on pointer up\n const hit = gizmoHitTest(px, py, orientation, center, armLength, upAxis);\n setHoveredRegion(hit);\n },\n [\n disabled,\n interactionMode,\n canvasRef,\n getPointerPos,\n orientation,\n center,\n armLength,\n upAxis,\n ]\n );\n\n const onPointerMove = useCallback(\n (e: React.PointerEvent) => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n if (isDraggingRef.current && allowOrbit) {\n const dx = e.clientX - lastPointerRef.current.x;\n const dy = e.clientY - lastPointerRef.current.y;\n\n // Only count as drag if moved > 3px from start\n const totalDx = e.clientX - startPointerRef.current.x;\n const totalDy = e.clientY - startPointerRef.current.y;\n if (Math.sqrt(totalDx ** 2 + totalDy ** 2) > 3) {\n hasDraggedRef.current = true;\n }\n\n if (hasDraggedRef.current) {\n emitOrbit(dx * orbitSpeed * 0.5, -dy * orbitSpeed * 0.5);\n }\n\n lastPointerRef.current = { x: e.clientX, y: e.clientY };\n return;\n }\n\n // Hover detection\n if (interactionMode === 'display-only') return;\n const { px, py } = getPointerPos(e);\n const hit = gizmoHitTest(px, py, orientation, center, armLength, upAxis);\n setHoveredRegion(hit);\n\n // Cursor\n if (disabled) {\n canvas.style.cursor = 'default';\n } else if (hit.type === 'axis-positive' || hit.type === 'origin') {\n canvas.style.cursor = 'pointer';\n } else if (allowOrbit) {\n canvas.style.cursor = isDraggingRef.current ? 'grabbing' : 'grab';\n } else {\n canvas.style.cursor = 'default';\n }\n },\n [\n canvasRef,\n allowOrbit,\n orbitSpeed,\n emitOrbit,\n orientation,\n interactionMode,\n disabled,\n getPointerPos,\n center,\n armLength,\n upAxis,\n ]\n );\n\n const onPointerUp = useCallback(\n (e: React.PointerEvent) => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n if (isDraggingRef.current) {\n isDraggingRef.current = false;\n setIsDragging(false);\n canvas.releasePointerCapture(e.pointerId);\n\n if (hasDraggedRef.current) {\n // Finished orbit drag\n onOrbitEnd?.(orientation);\n } else if (allowSnap) {\n // Click (no drag) — check hit\n const { px, py } = getPointerPos(e);\n const hit = gizmoHitTest(\n px,\n py,\n orientation,\n center,\n armLength,\n upAxis\n );\n\n if (hit.type === 'origin') {\n onOriginClick?.();\n } else if (hit.type === 'axis-positive' && hit.axis) {\n onAxisClick?.(hit.axis, true);\n const view = axisToPresetView(hit.axis, true, upAxis);\n if (view) onSnapToView?.(view);\n } else if (hit.type === 'axis-negative' && hit.axis) {\n onAxisClick?.(hit.axis, false);\n const view = axisToPresetView(hit.axis, false, upAxis);\n if (view) onSnapToView?.(view);\n }\n }\n }\n },\n [\n canvasRef,\n allowSnap,\n getPointerPos,\n orientation,\n center,\n armLength,\n upAxis,\n onOrbitEnd,\n onOriginClick,\n onAxisClick,\n onSnapToView,\n ]\n );\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled || interactionMode === 'display-only') return;\n\n const fine = e.shiftKey;\n const step = fine ? ORBIT_FINE_STEP : ORBIT_KEY_STEP;\n let handled = false;\n\n switch (e.key) {\n case 'ArrowLeft':\n if (allowOrbit) {\n emitOrbit(-step, 0);\n handled = true;\n }\n break;\n case 'ArrowRight':\n if (allowOrbit) {\n emitOrbit(step, 0);\n handled = true;\n }\n break;\n case 'ArrowUp':\n if (allowOrbit) {\n emitOrbit(0, step);\n handled = true;\n }\n break;\n case 'ArrowDown':\n if (allowOrbit) {\n emitOrbit(0, -step);\n handled = true;\n }\n break;\n case '1':\n if (allowSnap) {\n onSnapToView?.(e.ctrlKey ? 'back' : 'front');\n handled = true;\n }\n break;\n case '3':\n if (allowSnap) {\n onSnapToView?.(e.ctrlKey ? 'left' : 'right');\n handled = true;\n }\n break;\n case '7':\n if (allowSnap) {\n onSnapToView?.(e.ctrlKey ? 'bottom' : 'top');\n handled = true;\n }\n break;\n case '5':\n case 'Home':\n onOriginClick?.();\n handled = true;\n break;\n }\n\n if (handled) {\n e.preventDefault();\n }\n },\n [\n disabled,\n interactionMode,\n allowOrbit,\n allowSnap,\n emitOrbit,\n onSnapToView,\n onOriginClick,\n ]\n );\n\n return {\n isDragging,\n hoveredRegion,\n handlers: {\n onPointerDown,\n onPointerMove,\n onPointerUp,\n onKeyDown,\n },\n };\n}\n"],"names":[],"mappings":";;;;;AA8CA;AAEA;AACA;AAEM;AAGJ;;;;AAqBA;;AAEA;AACA;AACA;;;;;AAaA;AAEI;AACA;;AACA;;AAEF;;;;;;AAUE;AACA;;;AAII;;;AAIN;AAIF;;;AAII;AACA;;;AAIA;AACA;AACA;AACA;;AAEA;;AAGA;;AAEF;;;;;;;;;AAUC;AAGH;AAEI;AACA;;AAEA;;;;;;AAOE;AACE;;AAGF;AACE;;AAGF;;;;;;;AAOF;;;;AAKE;;AACK;AACL;;;AAEA;;;AAEA;;AAEJ;;;;;;;;;;;;AAaC;AAGH;AAEI;AACA;;AAEA;AACE;;AAEA;AAEA;;AAEE;;;;;AAIA;AASA;;;;;AAIE;AACA;AAAU;;;;AAGV;AACA;AAAU;;;;AAIlB;;;;;;;;;;;;AAaC;AAGH;AAEI;;AAEA;;;AAIA;AACE;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;;AAEI;;;;AAIJ;AACA;;;;;;;;AASJ;;;;;;;;AASC;;;;AAMD;;;;;AAKC;;AAEL;;"}
@@ -25,16 +25,20 @@ function resolveColor(color) {
25
25
  * Loading / activity indicator.
26
26
  *
27
27
  * Announces itself via `role="status"` and `aria-label`; honors
28
- * `prefers-reduced-motion` by halting animation and dimming the indicator.
28
+ * `prefers-reduced-motion` by halting animation and dimming the indicator. Pass
29
+ * `decorative` to opt out of the live region when nested inside one (e.g. a
30
+ * `StatusBar`).
29
31
  *
30
32
  * @example
31
33
  * ```tsx
32
34
  * <Spinner />
33
35
  * <Spinner variant="dots" size="lg" />
34
36
  * <Spinner showLabel label="Fetching results..." />
37
+ * // Inside a StatusBar (already role="status"):
38
+ * <Spinner decorative size="sm" />
35
39
  * ```
36
40
  */
37
- const Spinner = /*#__PURE__*/ React.memo(({ size = 'md', variant = 'ring', color = 'accent', label = 'Loading...', showLabel = false, className, style, testId, ref, ...rest }) => {
41
+ const Spinner = /*#__PURE__*/ React.memo(({ size = 'md', variant = 'ring', color = 'accent', label = 'Loading...', showLabel = false, decorative = false, className, style, testId, ref, ...rest }) => {
38
42
  const resolvedColor = resolveColor(color);
39
43
  const resolvedSize = SIZE_MAP[size];
40
44
  const inlineVars = assignInlineVars({
@@ -42,7 +46,12 @@ const Spinner = /*#__PURE__*/ React.memo(({ size = 'md', variant = 'ring', color
42
46
  [spinnerSizeVar]: resolvedSize,
43
47
  });
44
48
  const indicator = variant === 'ring' ? (jsx("span", { className: spinnerRingStyle, "aria-hidden": true })) : variant === 'pulse' ? (jsx("span", { className: spinnerPulseStyle, "aria-hidden": true })) : (jsxs("span", { className: spinnerDotsSizerStyle, "aria-hidden": true, children: [jsx("span", { className: spinnerDotRecipe({ delay: '0' }) }), jsx("span", { className: spinnerDotRecipe({ delay: '1' }) }), jsx("span", { className: spinnerDotRecipe({ delay: '2' }) })] }));
45
- return (jsxs("span", { ref: ref, role: "status", "aria-live": "polite", "aria-label": label, className: cx(spinnerRootStyle, className), style: { ...inlineVars, ...style }, "data-testid": testId, ...rest, children: [indicator, showLabel ? (jsx("span", { className: spinnerLabelStyle, children: label })) : (jsx("span", { className: visuallyHiddenStyle, children: label }))] }));
49
+ // Decorative spinners drop the live region (and label) so a surrounding
50
+ // live region (e.g. StatusBar's role="status") is the single announcer.
51
+ const semanticProps = decorative
52
+ ? { role: 'presentation', 'aria-hidden': true }
53
+ : { role: 'status', 'aria-live': 'polite', 'aria-label': label };
54
+ return (jsxs("span", { ref: ref, ...semanticProps, className: cx(spinnerRootStyle, className), style: { ...inlineVars, ...style }, "data-testid": testId, ...rest, children: [indicator, showLabel ? (jsx("span", { className: spinnerLabelStyle, children: label })) : decorative ? null : (jsx("span", { className: visuallyHiddenStyle, children: label }))] }));
46
55
  });
47
56
  Spinner.displayName = 'Spinner';
48
57
 
@@ -1 +1 @@
1
- {"version":3,"file":"Spinner.js","sources":["src/components/feedback/Spinner/Spinner.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React from 'react';\n\nimport { vars } from '@/theme/contract.css';\nimport { cx } from '@/utils/cx';\n\nimport {\n spinnerColorVar,\n spinnerDotRecipe,\n spinnerDotsSizerStyle,\n spinnerLabelStyle,\n spinnerPulseStyle,\n spinnerRingStyle,\n spinnerRootStyle,\n spinnerSizeVar,\n visuallyHiddenStyle,\n} from './Spinner.css';\n\nimport type { SpinnerColor, SpinnerProps, SpinnerSize } from './Spinner.types';\n\nconst SIZE_MAP: Record<SpinnerSize, string> = {\n xs: '12px',\n sm: '14px',\n md: '16px',\n lg: '20px',\n};\n\nconst NAMED_COLOR_MAP: Record<string, string> = {\n primary: vars.colors.text.primary,\n secondary: vars.colors.text.secondary,\n muted: vars.colors.text.muted,\n accent: vars.colors.accent.primary,\n};\n\nfunction resolveColor(color: SpinnerColor): string {\n return NAMED_COLOR_MAP[color] ?? color;\n}\n\n/**\n * Loading / activity indicator.\n *\n * Announces itself via `role=\"status\"` and `aria-label`; honors\n * `prefers-reduced-motion` by halting animation and dimming the indicator.\n *\n * @example\n * ```tsx\n * <Spinner />\n * <Spinner variant=\"dots\" size=\"lg\" />\n * <Spinner showLabel label=\"Fetching results...\" />\n * ```\n */\nexport const Spinner = /*#__PURE__*/ React.memo<SpinnerProps>(\n ({\n size = 'md',\n variant = 'ring',\n color = 'accent',\n label = 'Loading...',\n showLabel = false,\n className,\n style,\n testId,\n ref,\n ...rest\n }) => {\n const resolvedColor = resolveColor(color);\n const resolvedSize = SIZE_MAP[size];\n\n const inlineVars = assignInlineVars({\n [spinnerColorVar]: resolvedColor,\n [spinnerSizeVar]: resolvedSize,\n });\n\n const indicator =\n variant === 'ring' ? (\n <span className={spinnerRingStyle} aria-hidden />\n ) : variant === 'pulse' ? (\n <span className={spinnerPulseStyle} aria-hidden />\n ) : (\n <span className={spinnerDotsSizerStyle} aria-hidden>\n <span className={spinnerDotRecipe({ delay: '0' })} />\n <span className={spinnerDotRecipe({ delay: '1' })} />\n <span className={spinnerDotRecipe({ delay: '2' })} />\n </span>\n );\n\n return (\n <span\n ref={ref}\n role=\"status\"\n aria-live=\"polite\"\n aria-label={label}\n className={cx(spinnerRootStyle, className)}\n style={{ ...inlineVars, ...style }}\n data-testid={testId}\n {...rest}\n >\n {indicator}\n {showLabel ? (\n <span className={spinnerLabelStyle}>{label}</span>\n ) : (\n <span className={visuallyHiddenStyle}>{label}</span>\n )}\n </span>\n );\n }\n);\n\nSpinner.displayName = 'Spinner';\n"],"names":[],"mappings":";;;;;;;;AAsBA;AACE;AACA;AACA;AACA;;AAGF;AACE;AACA;AACA;AACA;;AAGF;AACE;AACF;AAEA;;;;;;;;;;;;AAYG;;AAcC;AACA;;;;AAKC;AAED;AAaA;AAmBF;AAGF;;"}
1
+ {"version":3,"file":"Spinner.js","sources":["src/components/feedback/Spinner/Spinner.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React from 'react';\n\nimport { vars } from '@/theme/contract.css';\nimport { cx } from '@/utils/cx';\n\nimport {\n spinnerColorVar,\n spinnerDotRecipe,\n spinnerDotsSizerStyle,\n spinnerLabelStyle,\n spinnerPulseStyle,\n spinnerRingStyle,\n spinnerRootStyle,\n spinnerSizeVar,\n visuallyHiddenStyle,\n} from './Spinner.css';\n\nimport type { SpinnerColor, SpinnerProps, SpinnerSize } from './Spinner.types';\n\nconst SIZE_MAP: Record<SpinnerSize, string> = {\n xs: '12px',\n sm: '14px',\n md: '16px',\n lg: '20px',\n};\n\nconst NAMED_COLOR_MAP: Record<string, string> = {\n primary: vars.colors.text.primary,\n secondary: vars.colors.text.secondary,\n muted: vars.colors.text.muted,\n accent: vars.colors.accent.primary,\n};\n\nfunction resolveColor(color: SpinnerColor): string {\n return NAMED_COLOR_MAP[color] ?? color;\n}\n\n/**\n * Loading / activity indicator.\n *\n * Announces itself via `role=\"status\"` and `aria-label`; honors\n * `prefers-reduced-motion` by halting animation and dimming the indicator. Pass\n * `decorative` to opt out of the live region when nested inside one (e.g. a\n * `StatusBar`).\n *\n * @example\n * ```tsx\n * <Spinner />\n * <Spinner variant=\"dots\" size=\"lg\" />\n * <Spinner showLabel label=\"Fetching results...\" />\n * // Inside a StatusBar (already role=\"status\"):\n * <Spinner decorative size=\"sm\" />\n * ```\n */\nexport const Spinner = /*#__PURE__*/ React.memo<SpinnerProps>(\n ({\n size = 'md',\n variant = 'ring',\n color = 'accent',\n label = 'Loading...',\n showLabel = false,\n decorative = false,\n className,\n style,\n testId,\n ref,\n ...rest\n }) => {\n const resolvedColor = resolveColor(color);\n const resolvedSize = SIZE_MAP[size];\n\n const inlineVars = assignInlineVars({\n [spinnerColorVar]: resolvedColor,\n [spinnerSizeVar]: resolvedSize,\n });\n\n const indicator =\n variant === 'ring' ? (\n <span className={spinnerRingStyle} aria-hidden />\n ) : variant === 'pulse' ? (\n <span className={spinnerPulseStyle} aria-hidden />\n ) : (\n <span className={spinnerDotsSizerStyle} aria-hidden>\n <span className={spinnerDotRecipe({ delay: '0' })} />\n <span className={spinnerDotRecipe({ delay: '1' })} />\n <span className={spinnerDotRecipe({ delay: '2' })} />\n </span>\n );\n\n // Decorative spinners drop the live region (and label) so a surrounding\n // live region (e.g. StatusBar's role=\"status\") is the single announcer.\n const semanticProps: React.HTMLAttributes<HTMLSpanElement> = decorative\n ? { role: 'presentation', 'aria-hidden': true }\n : { role: 'status', 'aria-live': 'polite', 'aria-label': label };\n\n return (\n <span\n ref={ref}\n {...semanticProps}\n className={cx(spinnerRootStyle, className)}\n style={{ ...inlineVars, ...style }}\n data-testid={testId}\n {...rest}\n >\n {indicator}\n {showLabel ? (\n <span className={spinnerLabelStyle}>{label}</span>\n ) : decorative ? null : (\n <span className={visuallyHiddenStyle}>{label}</span>\n )}\n </span>\n );\n }\n);\n\nSpinner.displayName = 'Spinner';\n"],"names":[],"mappings":";;;;;;;;AAsBA;AACE;AACA;AACA;AACA;;AAGF;AACE;AACA;AACA;AACA;;AAGF;AACE;AACF;AAEA;;;;;;;;;;;;;;;;AAgBG;;AAeC;AACA;;;;AAKC;AAED;;;;;AAiBE;AAEF;AAiBF;AAGF;;"}
@@ -9,6 +9,7 @@ import '../../primitives/Checkbox/Checkbox.js';
9
9
  import '../../primitives/Checkbox/CheckboxGroup.js';
10
10
  import '../../primitives/Code/Code.js';
11
11
  import '../../primitives/Collapsible/Collapsible.js';
12
+ import '../../primitives/EditableText/EditableText.js';
12
13
  import '../../primitives/HoverCard/HoverCard.js';
13
14
  import '../../primitives/Icon/Icon.js';
14
15
  import '../../primitives/IconButton/IconButton.js';
@@ -1 +1 @@
1
- {"version":3,"file":"BreadcrumbEllipsis.js","sources":["src/components/navigation/Breadcrumbs/BreadcrumbEllipsis.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\n\nimport { Tooltip } from '@/components/primitives';\nimport { cx } from '@/utils/cx';\n\nimport {\n breadcrumbEllipsisButtonStyle,\n breadcrumbEllipsisStyle,\n breadcrumbEllipsisTextStyle,\n} from './Breadcrumbs.css';\n\nimport type { BreadcrumbEllipsisProps } from './Breadcrumbs.types';\n\n/**\n * Ellipsis placeholder for collapsed breadcrumb segments.\n *\n * @example\n * ```tsx\n * <BreadcrumbEllipsis onClick={expand} tooltip=\"src / components\" />\n * ```\n */\nexport const BreadcrumbEllipsis =\n /*#__PURE__*/ React.memo<BreadcrumbEllipsisProps>(\n ({ onClick, tooltip, className, style, testId, ref, ...rest }) => {\n const ellipsis = onClick ? (\n <button\n type=\"button\"\n className={breadcrumbEllipsisButtonStyle}\n onClick={onClick}\n aria-label=\"Show all breadcrumbs\"\n >\n …\n </button>\n ) : (\n <span className={breadcrumbEllipsisTextStyle}>…</span>\n );\n\n return (\n <li\n ref={ref as React.Ref<HTMLLIElement>}\n className={cx(breadcrumbEllipsisStyle, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n {tooltip ? (\n <Tooltip title={tooltip} delay={300}>\n {ellipsis}\n </Tooltip>\n ) : (\n ellipsis\n )}\n </li>\n );\n }\n );\n\nBreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA;;;;;;;AAOG;;AAED;AAEI;;AA8BF;AAGJ;;"}
1
+ {"version":3,"file":"BreadcrumbEllipsis.js","sources":["src/components/navigation/Breadcrumbs/BreadcrumbEllipsis.tsx"],"sourcesContent":["'use client';\n\nimport React from 'react';\n\nimport { Tooltip } from '@/components/primitives';\nimport { cx } from '@/utils/cx';\n\nimport {\n breadcrumbEllipsisButtonStyle,\n breadcrumbEllipsisStyle,\n breadcrumbEllipsisTextStyle,\n} from './Breadcrumbs.css';\n\nimport type { BreadcrumbEllipsisProps } from './Breadcrumbs.types';\n\n/**\n * Ellipsis placeholder for collapsed breadcrumb segments.\n *\n * @example\n * ```tsx\n * <BreadcrumbEllipsis onClick={expand} tooltip=\"src / components\" />\n * ```\n */\nexport const BreadcrumbEllipsis =\n /*#__PURE__*/ React.memo<BreadcrumbEllipsisProps>(\n ({ onClick, tooltip, className, style, testId, ref, ...rest }) => {\n const ellipsis = onClick ? (\n <button\n type=\"button\"\n className={breadcrumbEllipsisButtonStyle}\n onClick={onClick}\n aria-label=\"Show all breadcrumbs\"\n >\n …\n </button>\n ) : (\n <span className={breadcrumbEllipsisTextStyle}>…</span>\n );\n\n return (\n <li\n ref={ref as React.Ref<HTMLLIElement>}\n className={cx(breadcrumbEllipsisStyle, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n {tooltip ? (\n <Tooltip title={tooltip} delay={300}>\n {ellipsis}\n </Tooltip>\n ) : (\n ellipsis\n )}\n </li>\n );\n }\n );\n\nBreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA;;;;;;;AAOG;;AAED;AAEI;;AA8BF;AAGJ;;"}
@@ -9,6 +9,7 @@ import '../../primitives/Checkbox/Checkbox.js';
9
9
  import '../../primitives/Checkbox/CheckboxGroup.js';
10
10
  import '../../primitives/Code/Code.js';
11
11
  import '../../primitives/Collapsible/Collapsible.js';
12
+ import '../../primitives/EditableText/EditableText.js';
12
13
  import '../../primitives/HoverCard/HoverCard.js';
13
14
  import '../../primitives/Icon/Icon.js';
14
15
  import '../../primitives/IconButton/IconButton.js';
@@ -1 +1 @@
1
- {"version":3,"file":"BreadcrumbItem.js","sources":["src/components/navigation/Breadcrumbs/BreadcrumbItem.tsx"],"sourcesContent":["'use client';\n\nimport React, { useCallback } from 'react';\n\nimport { Tooltip } from '@/components/primitives';\nimport { cx } from '@/utils/cx';\n\nimport {\n breadcrumbContentRecipe,\n breadcrumbIconStyle,\n breadcrumbItemStyle,\n breadcrumbLabelStyle,\n} from './Breadcrumbs.css';\n\nimport type { BreadcrumbItemProps } from './Breadcrumbs.types';\n\nfunction getTruncatedLabel(children: React.ReactNode, maxLength?: number) {\n if (\n typeof children !== 'string' ||\n !maxLength ||\n children.length <= maxLength\n ) {\n return { label: children, fullLabel: undefined };\n }\n\n const endIndex = Math.max(0, maxLength - 1);\n return {\n label: `${children.slice(0, endIndex)}…`,\n fullLabel: children,\n };\n}\n\n/**\n * Individual segment in a breadcrumb trail.\n *\n * Renders as a link, clickable span for client routing, current-page text, or\n * disabled text depending on the props provided.\n *\n * @example\n * ```tsx\n * <BreadcrumbItem href=\"/components\">Components</BreadcrumbItem>\n * <BreadcrumbItem isCurrent>Button</BreadcrumbItem>\n * ```\n */\nexport const BreadcrumbItem = /*#__PURE__*/ React.memo<BreadcrumbItemProps>(\n ({\n href,\n onClick,\n isCurrent = false,\n icon,\n maxLength,\n endContent,\n children,\n className,\n style,\n testId,\n ref,\n ...rest\n }) => {\n const { label, fullLabel } = getTruncatedLabel(children, maxLength);\n const isInteractive =\n (href !== undefined || onClick !== undefined) && !isCurrent;\n const state = isCurrent ? 'current' : isInteractive ? 'link' : 'disabled';\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (!onClick || isCurrent) {\n return;\n }\n\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.currentTarget.click();\n }\n },\n [isCurrent, onClick]\n );\n\n const content = (\n <>\n {icon && <span className={breadcrumbIconStyle}>{icon}</span>}\n <span className={breadcrumbLabelStyle}>{label}</span>\n </>\n );\n\n let inner: React.ReactElement;\n\n if (isCurrent) {\n inner = (\n <span\n className={breadcrumbContentRecipe({ state })}\n aria-current=\"page\"\n >\n {content}\n </span>\n );\n } else if (href) {\n inner = (\n <a className={breadcrumbContentRecipe({ state })} href={href}>\n {content}\n </a>\n );\n } else if (onClick) {\n inner = (\n <span\n className={breadcrumbContentRecipe({ state })}\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={handleKeyDown}\n >\n {content}\n </span>\n );\n } else {\n inner = (\n <span className={breadcrumbContentRecipe({ state })}>{content}</span>\n );\n }\n\n return (\n <li\n ref={ref as React.Ref<HTMLLIElement>}\n className={cx(breadcrumbItemStyle, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n {fullLabel ? (\n <Tooltip title={fullLabel} delay={300}>\n {inner}\n </Tooltip>\n ) : (\n inner\n )}\n {endContent}\n </li>\n );\n }\n);\n\nBreadcrumbItem.displayName = 'BreadcrumbItem';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA;;AAGI;AACA;;;AAKF;;;AAGE;;AAEJ;AAEA;;;;;;;;;;;AAWG;AACI;AAeH;AACA;AAEA;AAEA;AAEI;;;AAIA;;AAEE;;AAEJ;;AAWF;;AAGE;;;AASA;;;AAMA;;;AAYA;;;AAuBJ;AAGF;;"}
1
+ {"version":3,"file":"BreadcrumbItem.js","sources":["src/components/navigation/Breadcrumbs/BreadcrumbItem.tsx"],"sourcesContent":["'use client';\n\nimport React, { useCallback } from 'react';\n\nimport { Tooltip } from '@/components/primitives';\nimport { cx } from '@/utils/cx';\n\nimport {\n breadcrumbContentRecipe,\n breadcrumbIconStyle,\n breadcrumbItemStyle,\n breadcrumbLabelStyle,\n} from './Breadcrumbs.css';\n\nimport type { BreadcrumbItemProps } from './Breadcrumbs.types';\n\nfunction getTruncatedLabel(children: React.ReactNode, maxLength?: number) {\n if (\n typeof children !== 'string' ||\n !maxLength ||\n children.length <= maxLength\n ) {\n return { label: children, fullLabel: undefined };\n }\n\n const endIndex = Math.max(0, maxLength - 1);\n return {\n label: `${children.slice(0, endIndex)}…`,\n fullLabel: children,\n };\n}\n\n/**\n * Individual segment in a breadcrumb trail.\n *\n * Renders as a link, clickable span for client routing, current-page text, or\n * disabled text depending on the props provided.\n *\n * @example\n * ```tsx\n * <BreadcrumbItem href=\"/components\">Components</BreadcrumbItem>\n * <BreadcrumbItem isCurrent>Button</BreadcrumbItem>\n * ```\n */\nexport const BreadcrumbItem = /*#__PURE__*/ React.memo<BreadcrumbItemProps>(\n ({\n href,\n onClick,\n isCurrent = false,\n icon,\n maxLength,\n endContent,\n children,\n className,\n style,\n testId,\n ref,\n ...rest\n }) => {\n const { label, fullLabel } = getTruncatedLabel(children, maxLength);\n const isInteractive =\n (href !== undefined || onClick !== undefined) && !isCurrent;\n const state = isCurrent ? 'current' : isInteractive ? 'link' : 'disabled';\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (!onClick || isCurrent) {\n return;\n }\n\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.currentTarget.click();\n }\n },\n [isCurrent, onClick]\n );\n\n const content = (\n <>\n {icon && <span className={breadcrumbIconStyle}>{icon}</span>}\n <span className={breadcrumbLabelStyle}>{label}</span>\n </>\n );\n\n let inner: React.ReactElement;\n\n if (isCurrent) {\n inner = (\n <span\n className={breadcrumbContentRecipe({ state })}\n aria-current=\"page\"\n >\n {content}\n </span>\n );\n } else if (href) {\n inner = (\n <a className={breadcrumbContentRecipe({ state })} href={href}>\n {content}\n </a>\n );\n } else if (onClick) {\n inner = (\n <span\n className={breadcrumbContentRecipe({ state })}\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={handleKeyDown}\n >\n {content}\n </span>\n );\n } else {\n inner = (\n <span className={breadcrumbContentRecipe({ state })}>{content}</span>\n );\n }\n\n return (\n <li\n ref={ref as React.Ref<HTMLLIElement>}\n className={cx(breadcrumbItemStyle, className)}\n style={style}\n data-testid={testId}\n {...rest}\n >\n {fullLabel ? (\n <Tooltip title={fullLabel} delay={300}>\n {inner}\n </Tooltip>\n ) : (\n inner\n )}\n {endContent}\n </li>\n );\n }\n);\n\nBreadcrumbItem.displayName = 'BreadcrumbItem';\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA;;AAGI;AACA;;;AAKF;;;AAGE;;AAEJ;AAEA;;;;;;;;;;;AAWG;AACI;AAeH;AACA;AAEA;AAEA;AAEI;;;AAIA;;AAEE;;AAEJ;;AAWF;;AAGE;;;AASA;;;AAMA;;;AAYA;;;AAuBJ;AAGF;;"}
@@ -2,7 +2,7 @@
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import { ContextMenu as ContextMenu$1 } from '@base-ui/react/context-menu';
4
4
  import React from 'react';
5
- import { menuContentStyle } from '../Menu/Menu.css.js';
5
+ import { menuPositionerStyle, menuContentStyle } from '../Menu/Menu.css.js';
6
6
  import { MenuGapContext, DEFAULT_MENU_GAP } from '../Menu/MenuGapContext.js';
7
7
  import { cx } from '../../../utils/cx.js';
8
8
 
@@ -38,7 +38,7 @@ const ContextMenuTrigger = ({ children, render, className, style, testId, ref, .
38
38
  // needed; otherwise wrap so the trigger is transparent to layout.
39
39
  style: render ? style : { display: 'contents', ...style }, "data-testid": testId, ...rest, children: render ? undefined : children }));
40
40
  ContextMenuTrigger.displayName = 'ContextMenu.Trigger';
41
- const ContextMenuContent = ({ children, className, testId, ref, ...rest }) => (jsx(ContextMenu$1.Portal, { children: jsx(ContextMenu$1.Positioner, { children: jsx(ContextMenu$1.Popup, { ref: ref, className: cx(menuContentStyle, className), "data-testid": testId, ...rest, children: children }) }) }));
41
+ const ContextMenuContent = ({ children, className, testId, ref, ...rest }) => (jsx(ContextMenu$1.Portal, { children: jsx(ContextMenu$1.Positioner, { className: menuPositionerStyle, children: jsx(ContextMenu$1.Popup, { ref: ref, className: cx(menuContentStyle, className), "data-testid": testId, ...rest, children: children }) }) }));
42
42
  ContextMenuContent.displayName = 'ContextMenu.Content';
43
43
  /**
44
44
  * Compound ContextMenu API. Items reuse the shared `Menu.*` primitives.
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMenu.js","sources":["src/components/navigation/ContextMenu/ContextMenu.tsx"],"sourcesContent":["'use client';\n\nimport { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu';\nimport React from 'react';\n\nimport { menuContentStyle } from '@/components/navigation/Menu/Menu.css';\nimport {\n MenuGapContext,\n DEFAULT_MENU_GAP,\n} from '@/components/navigation/Menu/MenuGapContext';\nimport { cx } from '@/utils/cx';\n\nimport type {\n ContextMenuProps,\n ContextMenuTriggerProps,\n ContextMenuContentProps,\n} from './ContextMenu.types';\nimport type { MenuRootActions } from '@base-ui/react/menu';\n\n/**\n * Right-click context menu, built on `@base-ui/react` ContextMenu primitives.\n *\n * Compose the trigger area and content as children. Reuse the shared `Menu.*`\n * primitives for items, or pass a fully custom panel into\n * `ContextMenu.Content`. Define separate menus per area by giving each area its\n * own `ContextMenu`.\n *\n * @example\n * ```tsx\n * <ContextMenu>\n * <ContextMenu.Trigger>\n * <div className=\"viewport\">Right-click me</div>\n * </ContextMenu.Trigger>\n * <ContextMenu.Content>\n * <Menu.Item icon={<CutIcon />} shortcut=\"⌘X\">Cut</Menu.Item>\n * <Menu.Item icon={<CopyIcon />} shortcut=\"⌘C\">Copy</Menu.Item>\n * </ContextMenu.Content>\n * </ContextMenu>\n * ```\n */\nconst ContextMenuRoot = ({\n children,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n gap = DEFAULT_MENU_GAP,\n ref,\n}: ContextMenuProps): React.ReactElement => {\n const actionsRef = React.useRef<MenuRootActions | null>(null);\n React.useImperativeHandle(\n ref,\n () => ({ close: () => actionsRef.current?.close() }),\n []\n );\n return (\n <MenuGapContext.Provider value={gap}>\n <BaseContextMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n disabled={disabled}\n actionsRef={actionsRef}\n >\n {children}\n </BaseContextMenu.Root>\n </MenuGapContext.Provider>\n );\n};\nContextMenuRoot.displayName = 'ContextMenu';\n\nconst ContextMenuTrigger = ({\n children,\n render,\n className,\n style,\n testId,\n ref,\n ...rest\n}: ContextMenuTriggerProps): React.ReactElement => (\n <BaseContextMenu.Trigger\n ref={ref}\n render={render}\n className={className}\n // With `render`, the user's element is the trigger, so no wrapper is\n // needed; otherwise wrap so the trigger is transparent to layout.\n style={render ? style : { display: 'contents', ...style }}\n data-testid={testId}\n {...rest}\n >\n {render ? undefined : children}\n </BaseContextMenu.Trigger>\n);\nContextMenuTrigger.displayName = 'ContextMenu.Trigger';\n\nconst ContextMenuContent = ({\n children,\n className,\n testId,\n ref,\n ...rest\n}: ContextMenuContentProps): React.ReactElement => (\n <BaseContextMenu.Portal>\n <BaseContextMenu.Positioner>\n <BaseContextMenu.Popup\n ref={ref}\n className={cx(menuContentStyle, className)}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseContextMenu.Popup>\n </BaseContextMenu.Positioner>\n </BaseContextMenu.Portal>\n);\nContextMenuContent.displayName = 'ContextMenu.Content';\n\n/**\n * Compound ContextMenu API. Items reuse the shared `Menu.*` primitives.\n */\nexport const ContextMenu = /*#__PURE__*/ Object.assign(ContextMenuRoot, {\n Trigger: ContextMenuTrigger,\n Content: ContextMenuContent,\n});\n"],"names":[],"mappings":";;;;;;;;AAmBA;;;;;;;;;;;;;;;;;;;;AAoBG;AACH;;;AAeE;AAaF;AACA;AAEA;;;AAeI;AAOJ;AAEA;AAoBA;AAEA;;AAEG;AACI;AACL;AACA;AACD;;"}
1
+ {"version":3,"file":"ContextMenu.js","sources":["src/components/navigation/ContextMenu/ContextMenu.tsx"],"sourcesContent":["'use client';\n\nimport { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu';\nimport React from 'react';\n\nimport {\n menuContentStyle,\n menuPositionerStyle,\n} from '@/components/navigation/Menu/Menu.css';\nimport {\n MenuGapContext,\n DEFAULT_MENU_GAP,\n} from '@/components/navigation/Menu/MenuGapContext';\nimport { cx } from '@/utils/cx';\n\nimport type {\n ContextMenuProps,\n ContextMenuTriggerProps,\n ContextMenuContentProps,\n} from './ContextMenu.types';\nimport type { MenuRootActions } from '@base-ui/react/menu';\n\n/**\n * Right-click context menu, built on `@base-ui/react` ContextMenu primitives.\n *\n * Compose the trigger area and content as children. Reuse the shared `Menu.*`\n * primitives for items, or pass a fully custom panel into\n * `ContextMenu.Content`. Define separate menus per area by giving each area its\n * own `ContextMenu`.\n *\n * @example\n * ```tsx\n * <ContextMenu>\n * <ContextMenu.Trigger>\n * <div className=\"viewport\">Right-click me</div>\n * </ContextMenu.Trigger>\n * <ContextMenu.Content>\n * <Menu.Item icon={<CutIcon />} shortcut=\"⌘X\">Cut</Menu.Item>\n * <Menu.Item icon={<CopyIcon />} shortcut=\"⌘C\">Copy</Menu.Item>\n * </ContextMenu.Content>\n * </ContextMenu>\n * ```\n */\nconst ContextMenuRoot = ({\n children,\n open,\n defaultOpen,\n onOpenChange,\n disabled,\n gap = DEFAULT_MENU_GAP,\n ref,\n}: ContextMenuProps): React.ReactElement => {\n const actionsRef = React.useRef<MenuRootActions | null>(null);\n React.useImperativeHandle(\n ref,\n () => ({ close: () => actionsRef.current?.close() }),\n []\n );\n return (\n <MenuGapContext.Provider value={gap}>\n <BaseContextMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n disabled={disabled}\n actionsRef={actionsRef}\n >\n {children}\n </BaseContextMenu.Root>\n </MenuGapContext.Provider>\n );\n};\nContextMenuRoot.displayName = 'ContextMenu';\n\nconst ContextMenuTrigger = ({\n children,\n render,\n className,\n style,\n testId,\n ref,\n ...rest\n}: ContextMenuTriggerProps): React.ReactElement => (\n <BaseContextMenu.Trigger\n ref={ref}\n render={render}\n className={className}\n // With `render`, the user's element is the trigger, so no wrapper is\n // needed; otherwise wrap so the trigger is transparent to layout.\n style={render ? style : { display: 'contents', ...style }}\n data-testid={testId}\n {...rest}\n >\n {render ? undefined : children}\n </BaseContextMenu.Trigger>\n);\nContextMenuTrigger.displayName = 'ContextMenu.Trigger';\n\nconst ContextMenuContent = ({\n children,\n className,\n testId,\n ref,\n ...rest\n}: ContextMenuContentProps): React.ReactElement => (\n <BaseContextMenu.Portal>\n <BaseContextMenu.Positioner className={menuPositionerStyle}>\n <BaseContextMenu.Popup\n ref={ref}\n className={cx(menuContentStyle, className)}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseContextMenu.Popup>\n </BaseContextMenu.Positioner>\n </BaseContextMenu.Portal>\n);\nContextMenuContent.displayName = 'ContextMenu.Content';\n\n/**\n * Compound ContextMenu API. Items reuse the shared `Menu.*` primitives.\n */\nexport const ContextMenu = /*#__PURE__*/ Object.assign(ContextMenuRoot, {\n Trigger: ContextMenuTrigger,\n Content: ContextMenuContent,\n});\n"],"names":[],"mappings":";;;;;;;;AAsBA;;;;;;;;;;;;;;;;;;;;AAoBG;AACH;;;AAeE;AAaF;AACA;AAEA;;;AAeI;AAOJ;AAEA;AAoBA;AAEA;;AAEG;AACI;AACL;AACA;AACD;;"}
@@ -1,13 +1,14 @@
1
- import './../../../assets/src/components/navigation/Menu/Menu.css.ts.vanilla-B5iVe5S2.css';
1
+ import './../../../assets/src/components/navigation/Menu/Menu.css.ts.vanilla-Bhh1L5yl.css';
2
2
 
3
- var groupLabelStyle = 'Menu_groupLabelStyle__1pkkb716';
4
- var itemEndSlotStyle = 'Menu_itemEndSlotStyle__1pkkb714';
5
- var itemLabelStyle = 'Menu_itemLabelStyle__1pkkb713';
6
- var itemStartSlotStyle = 'Menu_itemStartSlotStyle__1pkkb712';
7
- var menuContentStyle = 'Menu_menuContentStyle__1pkkb710';
8
- var menuItemStyle = 'Menu_menuItemStyle__1pkkb711';
9
- var separatorStyle = 'Menu_separatorStyle__1pkkb717';
10
- var shortcutStyle = 'Menu_shortcutStyle__1pkkb715';
3
+ var groupLabelStyle = 'Menu_groupLabelStyle__1pkkb717';
4
+ var itemEndSlotStyle = 'Menu_itemEndSlotStyle__1pkkb715';
5
+ var itemLabelStyle = 'Menu_itemLabelStyle__1pkkb714';
6
+ var itemStartSlotStyle = 'Menu_itemStartSlotStyle__1pkkb713';
7
+ var menuContentStyle = 'Menu_menuContentStyle__1pkkb711';
8
+ var menuItemStyle = 'Menu_menuItemStyle__1pkkb712';
9
+ var menuPositionerStyle = 'Menu_menuPositionerStyle__1pkkb710';
10
+ var separatorStyle = 'Menu_separatorStyle__1pkkb718';
11
+ var shortcutStyle = 'Menu_shortcutStyle__1pkkb716';
11
12
 
12
- export { groupLabelStyle, itemEndSlotStyle, itemLabelStyle, itemStartSlotStyle, menuContentStyle, menuItemStyle, separatorStyle, shortcutStyle };
13
+ export { groupLabelStyle, itemEndSlotStyle, itemLabelStyle, itemStartSlotStyle, menuContentStyle, menuItemStyle, menuPositionerStyle, separatorStyle, shortcutStyle };
13
14
  //# sourceMappingURL=Menu.css.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Menu.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;"}
1
+ {"version":3,"file":"Menu.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;"}
@@ -8,7 +8,7 @@ import { CircleIcon } from '../../Icons/CircleIcon.js';
8
8
  import { Button } from '../../primitives/Button/Button.js';
9
9
  import { useLatest } from '../../../hooks/useLatest/useLatest.js';
10
10
  import { cx } from '../../../utils/cx.js';
11
- import { menuItemStyle, separatorStyle, itemStartSlotStyle, itemLabelStyle, itemEndSlotStyle, shortcutStyle, menuContentStyle, groupLabelStyle } from './Menu.css.js';
11
+ import { menuItemStyle, separatorStyle, itemStartSlotStyle, itemLabelStyle, itemEndSlotStyle, shortcutStyle, menuPositionerStyle, menuContentStyle, groupLabelStyle } from './Menu.css.js';
12
12
  import { MenuGapContext, DEFAULT_MENU_GAP } from './MenuGapContext.js';
13
13
 
14
14
  /**
@@ -63,7 +63,7 @@ const MenuTrigger = ({ children, render, disabled, openOnHover, className, testI
63
63
  MenuTrigger.displayName = 'Menu.Trigger';
64
64
  const MenuContent = ({ children, className, side, align = 'start', sideOffset, alignOffset, testId, ref, ...rest }) => {
65
65
  const gap = React.useContext(MenuGapContext);
66
- return (jsx(Menu$1.Portal, { children: jsx(Menu$1.Positioner, { side: side, align: align, sideOffset: sideOffset ?? gap, alignOffset: alignOffset, children: jsx(Menu$1.Popup, { ref: ref, className: cx(menuContentStyle, className), "data-testid": testId, ...rest, children: children }) }) }));
66
+ return (jsx(Menu$1.Portal, { children: jsx(Menu$1.Positioner, { side: side, align: align, sideOffset: sideOffset ?? gap, alignOffset: alignOffset, className: menuPositionerStyle, children: jsx(Menu$1.Popup, { ref: ref, className: cx(menuContentStyle, className), "data-testid": testId, ...rest, children: children }) }) }));
67
67
  };
68
68
  MenuContent.displayName = 'Menu.Content';
69
69
  const MenuItem = React.memo(({ children, icon, shortcut, endContent, disabled, closeOnClick = true, onClick, onSelect, className, testId, ref, ...rest }) => {