entangle-ui 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/dist/esm/assets/src/components/controls/Slider/{Slider.css.ts.vanilla-Cqm3fQ0S.css → Slider.css.ts.vanilla-CLf_RC0A.css} +50 -47
- package/dist/esm/assets/src/components/navigation/Menu/{Menu.css.ts.vanilla-B5iVe5S2.css → Menu.css.ts.vanilla-Bhh1L5yl.css} +14 -11
- package/dist/esm/assets/src/components/primitives/Tooltip/{Tooltip.css.ts.vanilla-CrEFoLBX.css → Tooltip.css.ts.vanilla-BqaiGrFQ.css} +12 -9
- package/dist/esm/assets/src/components/shell/AppShell/{AppShell.css.ts.vanilla-BYmtU0O_.css → AppShell.css.ts.vanilla-BLK2-DRL.css} +1 -0
- package/dist/esm/assets/src/components/shell/MenuBar/{MenuBar.css.ts.vanilla-TgPcl4yA.css → MenuBar.css.ts.vanilla-Dt3w4f-q.css} +18 -6
- package/dist/esm/components/controls/Slider/Slider.css.js +10 -11
- package/dist/esm/components/controls/Slider/Slider.css.js.map +1 -1
- package/dist/esm/components/controls/Slider/Slider.js +40 -7
- package/dist/esm/components/controls/Slider/Slider.js.map +1 -1
- package/dist/esm/components/feedback/Spinner/Spinner.js +12 -3
- package/dist/esm/components/feedback/Spinner/Spinner.js.map +1 -1
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js +2 -2
- package/dist/esm/components/navigation/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.css.js +11 -10
- package/dist/esm/components/navigation/Menu/Menu.css.js.map +1 -1
- package/dist/esm/components/navigation/Menu/Menu.js +2 -2
- package/dist/esm/components/navigation/Menu/Menu.js.map +1 -1
- package/dist/esm/components/primitives/IconButton/IconButton.js +4 -2
- package/dist/esm/components/primitives/IconButton/IconButton.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js +6 -5
- package/dist/esm/components/primitives/Tooltip/Tooltip.css.js.map +1 -1
- package/dist/esm/components/primitives/Tooltip/Tooltip.js +12 -2
- package/dist/esm/components/primitives/Tooltip/Tooltip.js.map +1 -1
- package/dist/esm/components/primitives/viewport/Viewport.js +5 -2
- package/dist/esm/components/primitives/viewport/Viewport.js.map +1 -1
- package/dist/esm/components/shell/AppShell/AppShell.css.js +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js +7 -6
- package/dist/esm/components/shell/MenuBar/MenuBar.css.js.map +1 -1
- package/dist/esm/components/shell/MenuBar/MenuBar.js +52 -3
- package/dist/esm/components/shell/MenuBar/MenuBar.js.map +1 -1
- package/dist/esm/{styles.js → styles.css.js} +1 -1
- package/dist/esm/styles.css.js.map +1 -0
- package/dist/tokens/tokens.dark.css +1 -1
- package/dist/tokens/tokens.json +1 -1
- package/dist/tokens/tokens.light.css +1 -1
- package/dist/types/components/controls/Slider/Slider.d.ts +7 -0
- package/dist/types/components/feedback/Spinner/Spinner.d.ts +6 -1
- package/dist/types/components/feedback/Spinner/Spinner.types.d.ts +8 -0
- package/dist/types/components/primitives/IconButton/IconButton.d.ts +2 -1
- package/dist/types/components/primitives/viewport/Viewport.d.ts +5 -2
- package/dist/types/components/shell/MenuBar/MenuBar.d.ts +862 -0
- package/package.json +2 -2
- package/dist/esm/styles.js.map +0 -1
|
@@ -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;;"}
|
|
@@ -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
|
-
|
|
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
|
|
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;;"}
|
|
@@ -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 {
|
|
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-
|
|
1
|
+
import './../../../assets/src/components/navigation/Menu/Menu.css.ts.vanilla-Bhh1L5yl.css';
|
|
2
2
|
|
|
3
|
-
var groupLabelStyle = '
|
|
4
|
-
var itemEndSlotStyle = '
|
|
5
|
-
var itemLabelStyle = '
|
|
6
|
-
var itemStartSlotStyle = '
|
|
7
|
-
var menuContentStyle = '
|
|
8
|
-
var menuItemStyle = '
|
|
9
|
-
var
|
|
10
|
-
var
|
|
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 }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menu.js","sources":["src/components/navigation/Menu/Menu.tsx"],"sourcesContent":["'use client';\n\nimport { Menu as BaseMenu } from '@base-ui/react/menu';\nimport React from 'react';\n\nimport { CheckIcon } from '@/components/Icons/CheckIcon';\nimport { ChevronRightIcon } from '@/components/Icons/ChevronRightIcon';\nimport { CircleIcon } from '@/components/Icons/CircleIcon';\nimport { Button } from '@/components/primitives/Button';\nimport { useLatest } from '@/hooks/useLatest';\nimport { cx } from '@/utils/cx';\n\nimport {\n menuContentStyle,\n menuItemStyle,\n itemStartSlotStyle,\n itemLabelStyle,\n itemEndSlotStyle,\n shortcutStyle,\n groupLabelStyle,\n separatorStyle,\n} from './Menu.css';\nimport { MenuGapContext, DEFAULT_MENU_GAP } from './MenuGapContext';\n\nimport type {\n MenuRootProps,\n MenuTriggerProps,\n MenuContentProps,\n MenuItemProps,\n MenuGroupProps,\n MenuSeparatorProps,\n MenuRadioGroupProps,\n MenuRadioItemProps,\n MenuCheckboxItemProps,\n MenuSubProps,\n MenuSubTriggerProps,\n} from './Menu.types';\nimport type { MenuRootActions } from '@base-ui/react/menu';\n\n/**\n * Internal three-slot row used by every interactive menu entry.\n */\nconst ItemLayout = ({\n start,\n children,\n shortcut,\n endContent,\n}: {\n start?: React.ReactNode;\n children?: React.ReactNode;\n shortcut?: React.ReactNode;\n endContent?: React.ReactNode;\n}): React.ReactElement => (\n <>\n {start != null && <span className={itemStartSlotStyle}>{start}</span>}\n <span className={itemLabelStyle}>{children}</span>\n {(shortcut != null || endContent != null) && (\n <span className={itemEndSlotStyle}>\n {shortcut != null && <span className={shortcutStyle}>{shortcut}</span>}\n {endContent}\n </span>\n )}\n </>\n);\n\n/**\n * Merge `onClick` and its `onSelect` alias into one stable handler so a new\n * function identity isn't handed to Base UI on every render. Returns\n * `undefined` when neither is set, so no listener is attached.\n */\nconst useActivate = (\n onClick?: React.MouseEventHandler<HTMLElement>,\n onSelect?: React.MouseEventHandler<HTMLElement>\n): React.MouseEventHandler<HTMLElement> | undefined => {\n const handlersRef = useLatest({ onClick, onSelect });\n const handleActivate = React.useCallback<\n React.MouseEventHandler<HTMLElement>\n >(\n event => {\n handlersRef.current.onClick?.(event);\n handlersRef.current.onSelect?.(event);\n },\n [handlersRef]\n );\n return onClick || onSelect ? handleActivate : undefined;\n};\n\n/**\n * Composable menu for editor interfaces, built on `@base-ui/react` Menu\n * primitives. Compose the trigger, content and items as children — no config\n * object.\n *\n * @example\n * ```tsx\n * <Menu>\n * <Menu.Trigger>File</Menu.Trigger>\n * <Menu.Content>\n * <Menu.Item icon={<SaveIcon />} shortcut=\"⌘S\" onClick={save}>\n * Save\n * </Menu.Item>\n * <Menu.Separator />\n * <Menu.Sub>\n * <Menu.SubTrigger>Export</Menu.SubTrigger>\n * <Menu.SubContent>\n * <Menu.Item>PNG</Menu.Item>\n * <Menu.Item>SVG</Menu.Item>\n * </Menu.SubContent>\n * </Menu.Sub>\n * </Menu.Content>\n * </Menu>\n * ```\n */\nconst MenuRoot = ({\n children,\n open,\n defaultOpen,\n onOpenChange,\n modal,\n disabled,\n gap = DEFAULT_MENU_GAP,\n ref,\n}: MenuRootProps): 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 <BaseMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n modal={modal}\n disabled={disabled}\n actionsRef={actionsRef}\n >\n {children}\n </BaseMenu.Root>\n </MenuGapContext.Provider>\n );\n};\nMenuRoot.displayName = 'Menu';\n\nconst MenuTrigger = ({\n children,\n render,\n disabled,\n openOnHover,\n className,\n testId,\n ...rest\n}: MenuTriggerProps): React.ReactElement => (\n <BaseMenu.Trigger\n render={render ?? <Button />}\n disabled={disabled}\n openOnHover={openOnHover}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Trigger>\n);\nMenuTrigger.displayName = 'Menu.Trigger';\n\nconst MenuContent = ({\n children,\n className,\n side,\n align = 'start',\n sideOffset,\n alignOffset,\n testId,\n ref,\n ...rest\n}: MenuContentProps): React.ReactElement => {\n const gap = React.useContext(MenuGapContext);\n return (\n <BaseMenu.Portal>\n <BaseMenu.Positioner\n side={side}\n align={align}\n sideOffset={sideOffset ?? gap}\n alignOffset={alignOffset}\n >\n <BaseMenu.Popup\n ref={ref}\n className={cx(menuContentStyle, className)}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Popup>\n </BaseMenu.Positioner>\n </BaseMenu.Portal>\n );\n};\nMenuContent.displayName = 'Menu.Content';\n\nconst MenuItem = React.memo(\n ({\n children,\n icon,\n shortcut,\n endContent,\n disabled,\n closeOnClick = true,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.Item\n ref={ref}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} shortcut={shortcut} endContent={endContent}>\n {children}\n </ItemLayout>\n </BaseMenu.Item>\n );\n }\n);\nMenuItem.displayName = 'Menu.Item';\n\nconst MenuGroup = ({\n children,\n label,\n className,\n testId,\n ...rest\n}: MenuGroupProps): React.ReactElement => (\n <BaseMenu.Group className={className} data-testid={testId} {...rest}>\n {label != null && (\n <BaseMenu.GroupLabel className={groupLabelStyle}>\n {label}\n </BaseMenu.GroupLabel>\n )}\n {children}\n </BaseMenu.Group>\n);\nMenuGroup.displayName = 'Menu.Group';\n\nconst MenuSeparator = React.memo(\n ({ className, testId, ...rest }: MenuSeparatorProps): React.ReactElement => (\n <BaseMenu.Separator\n className={cx(separatorStyle, className)}\n data-testid={testId}\n {...rest}\n />\n )\n);\nMenuSeparator.displayName = 'Menu.Separator';\n\nconst MenuRadioGroup = ({\n children,\n value,\n defaultValue,\n onValueChange,\n className,\n testId,\n ...rest\n}: MenuRadioGroupProps): React.ReactElement => (\n <BaseMenu.RadioGroup\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.RadioGroup>\n);\nMenuRadioGroup.displayName = 'Menu.RadioGroup';\n\nconst MenuRadioItem = React.memo(\n ({\n children,\n value,\n indicator = <CircleIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuRadioItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.RadioItem\n ref={ref}\n value={value}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.RadioItemIndicator>\n {indicator}\n </BaseMenu.RadioItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.RadioItem>\n );\n }\n);\nMenuRadioItem.displayName = 'Menu.RadioItem';\n\nconst MenuCheckboxItem = React.memo(\n ({\n children,\n checked,\n defaultChecked,\n onCheckedChange,\n indicator = <CheckIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuCheckboxItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.CheckboxItem\n ref={ref}\n checked={checked}\n defaultChecked={defaultChecked}\n onCheckedChange={onCheckedChange}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.CheckboxItemIndicator>\n {indicator}\n </BaseMenu.CheckboxItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.CheckboxItem>\n );\n }\n);\nMenuCheckboxItem.displayName = 'Menu.CheckboxItem';\n\nconst MenuSub = ({\n children,\n defaultOpen,\n}: MenuSubProps): React.ReactElement => (\n <BaseMenu.SubmenuRoot defaultOpen={defaultOpen}>\n {children}\n </BaseMenu.SubmenuRoot>\n);\nMenuSub.displayName = 'Menu.Sub';\n\nconst MenuSubTrigger = React.memo(\n ({\n children,\n icon,\n disabled,\n onClick,\n className,\n testId,\n ref,\n ...rest\n }: MenuSubTriggerProps): React.ReactElement => (\n <BaseMenu.SubmenuTrigger\n ref={ref}\n disabled={disabled}\n onClick={onClick}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} endContent={<ChevronRightIcon size=\"sm\" />}>\n {children}\n </ItemLayout>\n </BaseMenu.SubmenuTrigger>\n )\n);\nMenuSubTrigger.displayName = 'Menu.SubTrigger';\n\n/**\n * Compound Menu API.\n *\n * `Content` and `SubContent` are the same popup surface. The gap from the\n * anchor is set once on the `Menu` root (`gap` prop) and inherited by both.\n */\nexport const Menu = /*#__PURE__*/ Object.assign(MenuRoot, {\n Trigger: MenuTrigger,\n Content: MenuContent,\n Item: MenuItem,\n Group: MenuGroup,\n Separator: MenuSeparator,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n CheckboxItem: MenuCheckboxItem,\n Sub: MenuSub,\n SubTrigger: MenuSubTrigger,\n SubContent: MenuContent,\n});\n"],"names":[],"mappings":";;;;;;;;;;;;;AAuCA;;AAEG;AACH;AAuBA;;;;AAIG;AACH;;;;;AAWI;;AAIJ;AAEA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACH;;;AAgBE;AAcF;AACA;AAEA;AAoBA;AAEA;;;AA+BA;AACA;AAEA;;;AA+BE;AAEF;AAEA;AAgBA;AAEA;AASA;AAEA;AAoBA;AAEA;;AAiBI;AAwBF;AAEF;AAEA;;AAmBI;AA0BF;AAEF;AAEA;AAQA;AAEA;AAyBA;AAEA;;;;;AAKG;AACI;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACD;;"}
|
|
1
|
+
{"version":3,"file":"Menu.js","sources":["src/components/navigation/Menu/Menu.tsx"],"sourcesContent":["'use client';\n\nimport { Menu as BaseMenu } from '@base-ui/react/menu';\nimport React from 'react';\n\nimport { CheckIcon } from '@/components/Icons/CheckIcon';\nimport { ChevronRightIcon } from '@/components/Icons/ChevronRightIcon';\nimport { CircleIcon } from '@/components/Icons/CircleIcon';\nimport { Button } from '@/components/primitives/Button';\nimport { useLatest } from '@/hooks/useLatest';\nimport { cx } from '@/utils/cx';\n\nimport {\n menuContentStyle,\n menuPositionerStyle,\n menuItemStyle,\n itemStartSlotStyle,\n itemLabelStyle,\n itemEndSlotStyle,\n shortcutStyle,\n groupLabelStyle,\n separatorStyle,\n} from './Menu.css';\nimport { MenuGapContext, DEFAULT_MENU_GAP } from './MenuGapContext';\n\nimport type {\n MenuRootProps,\n MenuTriggerProps,\n MenuContentProps,\n MenuItemProps,\n MenuGroupProps,\n MenuSeparatorProps,\n MenuRadioGroupProps,\n MenuRadioItemProps,\n MenuCheckboxItemProps,\n MenuSubProps,\n MenuSubTriggerProps,\n} from './Menu.types';\nimport type { MenuRootActions } from '@base-ui/react/menu';\n\n/**\n * Internal three-slot row used by every interactive menu entry.\n */\nconst ItemLayout = ({\n start,\n children,\n shortcut,\n endContent,\n}: {\n start?: React.ReactNode;\n children?: React.ReactNode;\n shortcut?: React.ReactNode;\n endContent?: React.ReactNode;\n}): React.ReactElement => (\n <>\n {start != null && <span className={itemStartSlotStyle}>{start}</span>}\n <span className={itemLabelStyle}>{children}</span>\n {(shortcut != null || endContent != null) && (\n <span className={itemEndSlotStyle}>\n {shortcut != null && <span className={shortcutStyle}>{shortcut}</span>}\n {endContent}\n </span>\n )}\n </>\n);\n\n/**\n * Merge `onClick` and its `onSelect` alias into one stable handler so a new\n * function identity isn't handed to Base UI on every render. Returns\n * `undefined` when neither is set, so no listener is attached.\n */\nconst useActivate = (\n onClick?: React.MouseEventHandler<HTMLElement>,\n onSelect?: React.MouseEventHandler<HTMLElement>\n): React.MouseEventHandler<HTMLElement> | undefined => {\n const handlersRef = useLatest({ onClick, onSelect });\n const handleActivate = React.useCallback<\n React.MouseEventHandler<HTMLElement>\n >(\n event => {\n handlersRef.current.onClick?.(event);\n handlersRef.current.onSelect?.(event);\n },\n [handlersRef]\n );\n return onClick || onSelect ? handleActivate : undefined;\n};\n\n/**\n * Composable menu for editor interfaces, built on `@base-ui/react` Menu\n * primitives. Compose the trigger, content and items as children — no config\n * object.\n *\n * @example\n * ```tsx\n * <Menu>\n * <Menu.Trigger>File</Menu.Trigger>\n * <Menu.Content>\n * <Menu.Item icon={<SaveIcon />} shortcut=\"⌘S\" onClick={save}>\n * Save\n * </Menu.Item>\n * <Menu.Separator />\n * <Menu.Sub>\n * <Menu.SubTrigger>Export</Menu.SubTrigger>\n * <Menu.SubContent>\n * <Menu.Item>PNG</Menu.Item>\n * <Menu.Item>SVG</Menu.Item>\n * </Menu.SubContent>\n * </Menu.Sub>\n * </Menu.Content>\n * </Menu>\n * ```\n */\nconst MenuRoot = ({\n children,\n open,\n defaultOpen,\n onOpenChange,\n modal,\n disabled,\n gap = DEFAULT_MENU_GAP,\n ref,\n}: MenuRootProps): 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 <BaseMenu.Root\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n modal={modal}\n disabled={disabled}\n actionsRef={actionsRef}\n >\n {children}\n </BaseMenu.Root>\n </MenuGapContext.Provider>\n );\n};\nMenuRoot.displayName = 'Menu';\n\nconst MenuTrigger = ({\n children,\n render,\n disabled,\n openOnHover,\n className,\n testId,\n ...rest\n}: MenuTriggerProps): React.ReactElement => (\n <BaseMenu.Trigger\n render={render ?? <Button />}\n disabled={disabled}\n openOnHover={openOnHover}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Trigger>\n);\nMenuTrigger.displayName = 'Menu.Trigger';\n\nconst MenuContent = ({\n children,\n className,\n side,\n align = 'start',\n sideOffset,\n alignOffset,\n testId,\n ref,\n ...rest\n}: MenuContentProps): React.ReactElement => {\n const gap = React.useContext(MenuGapContext);\n return (\n <BaseMenu.Portal>\n <BaseMenu.Positioner\n side={side}\n align={align}\n sideOffset={sideOffset ?? gap}\n alignOffset={alignOffset}\n className={menuPositionerStyle}\n >\n <BaseMenu.Popup\n ref={ref}\n className={cx(menuContentStyle, className)}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.Popup>\n </BaseMenu.Positioner>\n </BaseMenu.Portal>\n );\n};\nMenuContent.displayName = 'Menu.Content';\n\nconst MenuItem = React.memo(\n ({\n children,\n icon,\n shortcut,\n endContent,\n disabled,\n closeOnClick = true,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.Item\n ref={ref}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} shortcut={shortcut} endContent={endContent}>\n {children}\n </ItemLayout>\n </BaseMenu.Item>\n );\n }\n);\nMenuItem.displayName = 'Menu.Item';\n\nconst MenuGroup = ({\n children,\n label,\n className,\n testId,\n ...rest\n}: MenuGroupProps): React.ReactElement => (\n <BaseMenu.Group className={className} data-testid={testId} {...rest}>\n {label != null && (\n <BaseMenu.GroupLabel className={groupLabelStyle}>\n {label}\n </BaseMenu.GroupLabel>\n )}\n {children}\n </BaseMenu.Group>\n);\nMenuGroup.displayName = 'Menu.Group';\n\nconst MenuSeparator = React.memo(\n ({ className, testId, ...rest }: MenuSeparatorProps): React.ReactElement => (\n <BaseMenu.Separator\n className={cx(separatorStyle, className)}\n data-testid={testId}\n {...rest}\n />\n )\n);\nMenuSeparator.displayName = 'Menu.Separator';\n\nconst MenuRadioGroup = ({\n children,\n value,\n defaultValue,\n onValueChange,\n className,\n testId,\n ...rest\n}: MenuRadioGroupProps): React.ReactElement => (\n <BaseMenu.RadioGroup\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n className={className}\n data-testid={testId}\n {...rest}\n >\n {children}\n </BaseMenu.RadioGroup>\n);\nMenuRadioGroup.displayName = 'Menu.RadioGroup';\n\nconst MenuRadioItem = React.memo(\n ({\n children,\n value,\n indicator = <CircleIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuRadioItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.RadioItem\n ref={ref}\n value={value}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.RadioItemIndicator>\n {indicator}\n </BaseMenu.RadioItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.RadioItem>\n );\n }\n);\nMenuRadioItem.displayName = 'Menu.RadioItem';\n\nconst MenuCheckboxItem = React.memo(\n ({\n children,\n checked,\n defaultChecked,\n onCheckedChange,\n indicator = <CheckIcon size=\"sm\" />,\n shortcut,\n endContent,\n disabled,\n closeOnClick = false,\n onClick,\n onSelect,\n className,\n testId,\n ref,\n ...rest\n }: MenuCheckboxItemProps): React.ReactElement => {\n const handleActivate = useActivate(onClick, onSelect);\n return (\n <BaseMenu.CheckboxItem\n ref={ref}\n checked={checked}\n defaultChecked={defaultChecked}\n onCheckedChange={onCheckedChange}\n disabled={disabled}\n closeOnClick={closeOnClick}\n onClick={handleActivate}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout\n start={\n <BaseMenu.CheckboxItemIndicator>\n {indicator}\n </BaseMenu.CheckboxItemIndicator>\n }\n shortcut={shortcut}\n endContent={endContent}\n >\n {children}\n </ItemLayout>\n </BaseMenu.CheckboxItem>\n );\n }\n);\nMenuCheckboxItem.displayName = 'Menu.CheckboxItem';\n\nconst MenuSub = ({\n children,\n defaultOpen,\n}: MenuSubProps): React.ReactElement => (\n <BaseMenu.SubmenuRoot defaultOpen={defaultOpen}>\n {children}\n </BaseMenu.SubmenuRoot>\n);\nMenuSub.displayName = 'Menu.Sub';\n\nconst MenuSubTrigger = React.memo(\n ({\n children,\n icon,\n disabled,\n onClick,\n className,\n testId,\n ref,\n ...rest\n }: MenuSubTriggerProps): React.ReactElement => (\n <BaseMenu.SubmenuTrigger\n ref={ref}\n disabled={disabled}\n onClick={onClick}\n className={cx(menuItemStyle, className)}\n data-testid={testId}\n {...rest}\n >\n <ItemLayout start={icon} endContent={<ChevronRightIcon size=\"sm\" />}>\n {children}\n </ItemLayout>\n </BaseMenu.SubmenuTrigger>\n )\n);\nMenuSubTrigger.displayName = 'Menu.SubTrigger';\n\n/**\n * Compound Menu API.\n *\n * `Content` and `SubContent` are the same popup surface. The gap from the\n * anchor is set once on the `Menu` root (`gap` prop) and inherited by both.\n */\nexport const Menu = /*#__PURE__*/ Object.assign(MenuRoot, {\n Trigger: MenuTrigger,\n Content: MenuContent,\n Item: MenuItem,\n Group: MenuGroup,\n Separator: MenuSeparator,\n RadioGroup: MenuRadioGroup,\n RadioItem: MenuRadioItem,\n CheckboxItem: MenuCheckboxItem,\n Sub: MenuSub,\n SubTrigger: MenuSubTrigger,\n SubContent: MenuContent,\n});\n"],"names":[],"mappings":";;;;;;;;;;;;;AAwCA;;AAEG;AACH;AAuBA;;;;AAIG;AACH;;;;;AAWI;;AAIJ;AAEA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACH;;;AAgBE;AAcF;AACA;AAEA;AAoBA;AAEA;;AAYE;AAoBF;AACA;AAEA;;;AA+BE;AAEF;AAEA;AAgBA;AAEA;AASA;AAEA;AAoBA;AAEA;;AAiBI;AAwBF;AAEF;AAEA;;AAmBI;AA0BF;AAEF;AAEA;AAQA;AAEA;AAyBA;AAEA;;;;;AAKG;AACI;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACD;;"}
|
|
@@ -48,13 +48,15 @@ import { loadingSpinnerRecipe, iconButtonRecipe } from './IconButton.css.js';
|
|
|
48
48
|
* </IconButton>
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
|
-
const IconButton = /*#__PURE__*/ React.memo(({ children, className, size = 'md', variant = 'ghost', radius = 'md', disabled = false, loading = false, pressed = false, onClick, 'aria-label': ariaLabel, testId, style, ref, ...props }) => {
|
|
51
|
+
const IconButton = /*#__PURE__*/ React.memo(({ children, icon, className, size = 'md', variant = 'ghost', radius = 'md', disabled = false, loading = false, pressed = false, onClick, 'aria-label': ariaLabel, testId, style, ref, ...props }) => {
|
|
52
|
+
// `icon` mirrors Button's API; fall back to `children` for back-compat.
|
|
53
|
+
const glyph = icon ?? children;
|
|
52
54
|
return (jsx("button", { ref: ref, className: cx(iconButtonRecipe({
|
|
53
55
|
size,
|
|
54
56
|
variant,
|
|
55
57
|
radius,
|
|
56
58
|
pressed: pressed || undefined,
|
|
57
|
-
}), className), disabled: disabled || loading, onClick: onClick, "aria-label": ariaLabel, "aria-pressed": pressed, "data-testid": testId, style: style, ...props, children: loading ?
|
|
59
|
+
}), className), disabled: disabled || loading, onClick: onClick, "aria-label": ariaLabel, "aria-pressed": pressed, "data-testid": testId, style: style, ...props, children: loading ? jsx("div", { className: loadingSpinnerRecipe({ size }) }) : glyph }));
|
|
58
60
|
});
|
|
59
61
|
IconButton.displayName = 'IconButton';
|
|
60
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButton.js","sources":["src/components/primitives/IconButton/IconButton.tsx"],"sourcesContent":["'use client';\n\n// src/primitives/IconButton/IconButton.tsx\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { iconButtonRecipe, loadingSpinnerRecipe } from './IconButton.css';\n\nimport type { Size, Variant } from '@/types/common';\nimport type { Prettify } from '@/types/utilities';\n\n/**\n * Standard size variants for IconButton using library sizing.\n */\nexport type IconButtonSize = Size;\n\n/**\n * Visual variants for IconButton - strict typing for controlled API.\n */\nexport type IconButtonVariant = Variant;\n\n/**\n * Border radius variants for IconButton shape control.\n */\nexport type IconButtonRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';\n\nexport interface IconButtonBaseProps extends Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n 'css' | 'children'\n> {\n /**\n * Icon component to display inside the button.\n * Should be an Icon component or similar icon element.\n * The button will automatically size itself based on the icon size.\n *\n * @example <SaveIcon />, <AddIcon />\n */\n children
|
|
1
|
+
{"version":3,"file":"IconButton.js","sources":["src/components/primitives/IconButton/IconButton.tsx"],"sourcesContent":["'use client';\n\n// src/primitives/IconButton/IconButton.tsx\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { iconButtonRecipe, loadingSpinnerRecipe } from './IconButton.css';\n\nimport type { Size, Variant } from '@/types/common';\nimport type { Prettify } from '@/types/utilities';\n\n/**\n * Standard size variants for IconButton using library sizing.\n */\nexport type IconButtonSize = Size;\n\n/**\n * Visual variants for IconButton - strict typing for controlled API.\n */\nexport type IconButtonVariant = Variant;\n\n/**\n * Border radius variants for IconButton shape control.\n */\nexport type IconButtonRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';\n\nexport interface IconButtonBaseProps extends Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n 'css' | 'children'\n> {\n /**\n * Icon component to display inside the button.\n * Should be an Icon component or similar icon element.\n * The button will automatically size itself based on the icon size.\n *\n * Equivalent to the `icon` prop — provide the glyph either way. When both are\n * set, `icon` wins. Kept optional so `<IconButton icon={…} />` matches\n * `Button`'s `icon` API.\n *\n * @example <SaveIcon />, <AddIcon />\n */\n children?: React.ReactNode;\n\n /**\n * Icon element to display inside the button, mirroring `Button`'s `icon`\n * prop so the two components share one API. Takes precedence over\n * `children` when both are provided.\n *\n * @example <SaveIcon />, <AddIcon />\n */\n icon?: React.ReactNode;\n\n /**\n * Button size using standard library sizing\n * Button will be square and sized appropriately for the icon\n * - `sm`: 20px square (for 12px icons)\n * - `md`: 24px square (for 16px icons)\n * - `lg`: 32px square (for 20px icons)\n * @default \"md\"\n */\n size?: IconButtonSize;\n\n /**\n * Button visual variant\n * - `default`: Transparent with border, fills on hover\n * - `ghost`: No border, subtle hover state\n * - `filled`: Solid background with accent color\n * @default \"ghost\"\n */\n variant?: IconButtonVariant;\n\n /**\n * Border radius for button shape control\n * - `none`: Sharp corners (0px)\n * - `sm`: Slightly rounded (2px)\n * - `md`: Moderately rounded (4px)\n * - `lg`: More rounded (6px)\n * - `full`: Fully circular\n * @default \"md\"\n */\n radius?: IconButtonRadius;\n\n /**\n * Whether the button is disabled\n * When true, button becomes non-interactive with reduced opacity\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Whether the button is in a loading state\n * Shows spinner and disables interaction\n * @default false\n */\n loading?: boolean;\n\n /**\n * Whether the button appears pressed/active\n * Useful for toggle states and active tool indicators\n * @default false\n */\n pressed?: boolean;\n\n /**\n * Accessible label for screen readers\n * Required for proper accessibility\n * @example \"Save file\", \"Delete item\", \"Close dialog\"\n */\n 'aria-label': string;\n\n /**\n * Click event handler\n * Called when button is clicked (not when disabled/loading)\n */\n onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\n /**\n * Test identifier for automated testing\n */\n testId?: string;\n\n ref?: React.Ref<HTMLButtonElement>;\n}\n\n/**\n * Props for the IconButton component with prettified type for better IntelliSense\n */\nexport type IconButtonProps = Prettify<IconButtonBaseProps>;\n\n/**\n * A specialized square button component for displaying icons in editor interfaces.\n *\n * Designed for toolbar actions, quick controls, and icon-based interactions.\n * Always square and sized appropriately for the contained icon.\n * Supports various border radius options from sharp corners to fully circular.\n *\n * @example\n * ```tsx\n * // Basic ghost button (default)\n * <IconButton aria-label=\"Save file\">\n * <SaveIcon />\n * </IconButton>\n *\n * // Circular filled button\n * <IconButton\n * variant=\"filled\"\n * radius=\"full\"\n * size=\"lg\"\n * aria-label=\"Add item\"\n * >\n * <AddIcon />\n * </IconButton>\n *\n * // Sharp corners for toolbar\n * <IconButton\n * variant=\"default\"\n * radius=\"none\"\n * aria-label=\"Settings\"\n * >\n * <SettingsIcon />\n * </IconButton>\n *\n * // Toggle state\n * <IconButton\n * pressed={isVisible}\n * radius=\"sm\"\n * aria-label=\"Toggle visibility\"\n * onClick={toggleVisibility}\n * >\n * <EyeIcon />\n * </IconButton>\n * ```\n */\nexport const IconButton = /*#__PURE__*/ React.memo<IconButtonProps>(\n ({\n children,\n icon,\n className,\n size = 'md',\n variant = 'ghost',\n radius = 'md',\n disabled = false,\n loading = false,\n pressed = false,\n onClick,\n 'aria-label': ariaLabel,\n testId,\n style,\n ref,\n ...props\n }) => {\n // `icon` mirrors Button's API; fall back to `children` for back-compat.\n const glyph = icon ?? children;\n return (\n <button\n ref={ref}\n className={cx(\n iconButtonRecipe({\n size,\n variant,\n radius,\n pressed: pressed || undefined,\n }),\n className\n )}\n disabled={disabled || loading}\n onClick={onClick}\n aria-label={ariaLabel}\n aria-pressed={pressed}\n data-testid={testId}\n style={style}\n {...props}\n >\n {loading ? <div className={loadingSpinnerRecipe({ size })} /> : glyph}\n </button>\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n"],"names":[],"mappings":";;;;;;AAkIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CG;AACI;;AAmBH;;;;;;;AAwBF;AAGF;;"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import './../../../assets/src/components/primitives/Tooltip/Tooltip.css.ts.vanilla-
|
|
1
|
+
import './../../../assets/src/components/primitives/Tooltip/Tooltip.css.ts.vanilla-BqaiGrFQ.css';
|
|
2
2
|
|
|
3
|
-
var arrowFillStyle = '
|
|
4
|
-
var tooltipArrowStyle = '
|
|
3
|
+
var arrowFillStyle = 'Tooltip_arrowFillStyle__u81kis3';
|
|
4
|
+
var tooltipArrowStyle = 'Tooltip_tooltipArrowStyle__u81kis4';
|
|
5
5
|
var tooltipContentStyle = 'Tooltip_tooltipContentStyle__u81kis0';
|
|
6
|
-
var
|
|
6
|
+
var tooltipPositionerStyle = 'Tooltip_tooltipPositionerStyle__u81kis1';
|
|
7
|
+
var tooltipTriggerStyle = 'Tooltip_tooltipTriggerStyle__u81kis2';
|
|
7
8
|
|
|
8
|
-
export { arrowFillStyle, tooltipArrowStyle, tooltipContentStyle, tooltipTriggerStyle };
|
|
9
|
+
export { arrowFillStyle, tooltipArrowStyle, tooltipContentStyle, tooltipPositionerStyle, tooltipTriggerStyle };
|
|
9
10
|
//# sourceMappingURL=Tooltip.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tooltip.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Tooltip.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
|
@@ -3,7 +3,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
3
3
|
import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
|
|
4
4
|
import { cx } from '../../../utils/cx.js';
|
|
5
5
|
import { StyledTooltipArrow, ArrowSvg } from './Arrow.js';
|
|
6
|
-
import { tooltipTriggerStyle, tooltipContentStyle } from './Tooltip.css.js';
|
|
6
|
+
import { tooltipPositionerStyle, tooltipTriggerStyle, tooltipContentStyle } from './Tooltip.css.js';
|
|
7
7
|
import { parsePlacement, parseCollisionStrategy } from './utils.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -135,6 +135,16 @@ const Tooltip = ({ children, title, placement = 'top', collision = 'smart', coll
|
|
|
135
135
|
finalPositionerProps.collisionBoundary = boundary;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
// Merge our z-index positioner class with any caller-provided className,
|
|
139
|
+
// honoring Base UI's function-className form (power-user `positionerProps`).
|
|
140
|
+
const incomingPositionerClassName = finalPositionerProps.className;
|
|
141
|
+
let positionerClassName;
|
|
142
|
+
if (typeof incomingPositionerClassName === 'function') {
|
|
143
|
+
positionerClassName = state => cx(tooltipPositionerStyle, incomingPositionerClassName(state));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
positionerClassName = cx(tooltipPositionerStyle, incomingPositionerClassName);
|
|
147
|
+
}
|
|
138
148
|
// Handle cursor tracking. Copy into a local object instead of mutating the
|
|
139
149
|
// caller-owned `rootProps` prop.
|
|
140
150
|
const finalRootProps = { ...rootProps };
|
|
@@ -155,7 +165,7 @@ const Tooltip = ({ children, title, placement = 'top', collision = 'smart', coll
|
|
|
155
165
|
transition: `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`,
|
|
156
166
|
}
|
|
157
167
|
: { transition: 'none' };
|
|
158
|
-
return (jsx(Tooltip$1.Provider, { delay: delay, closeDelay: closeDelay, ...finalRootProps, children: jsxs(Tooltip$1.Root, { children: [jsx(Tooltip$1.Trigger, { render: props => (jsx("div", { ...props, className: cx(tooltipTriggerStyle, props.className) })), children: children }), jsx(Tooltip$1.Portal, { children: jsx(Tooltip$1.Positioner, { ...finalPositionerProps, children: jsx(Tooltip$1.Popup, { render: props => (jsxs("div", { ...props, className: cx(tooltipContentStyle, className, props.className), "data-testid": testId, id: id, style: {
|
|
168
|
+
return (jsx(Tooltip$1.Provider, { delay: delay, closeDelay: closeDelay, ...finalRootProps, children: jsxs(Tooltip$1.Root, { children: [jsx(Tooltip$1.Trigger, { render: props => (jsx("div", { ...props, className: cx(tooltipTriggerStyle, props.className) })), children: children }), jsx(Tooltip$1.Portal, { children: jsx(Tooltip$1.Positioner, { ...finalPositionerProps, className: positionerClassName, children: jsx(Tooltip$1.Popup, { render: props => (jsxs("div", { ...props, className: cx(tooltipContentStyle, className, props.className), "data-testid": testId, id: id, style: {
|
|
159
169
|
...animationStyle,
|
|
160
170
|
...style,
|
|
161
171
|
...props.style,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tooltip.js","sources":["src/components/primitives/Tooltip/Tooltip.tsx"],"sourcesContent":["'use client';\n\n// src/components/primitives/Tooltip/Tooltip.tsx\nimport { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { ArrowSvg, StyledTooltipArrow } from './Arrow';\nimport { tooltipContentStyle, tooltipTriggerStyle } from './Tooltip.css';\nimport {\n CollisionAvoidance,\n TooltipAnimation,\n TooltipCollisionConfig,\n TooltipCollisionStrategy,\n TooltipPlacement,\n TooltipPositioner,\n BaseTooltipPositionerProps,\n BaseTooltipRootProps,\n} from './types';\nimport { parseCollisionStrategy, parsePlacement } from './utils';\n\nimport type { Prettify } from '@/types/utilities';\n\ninterface TooltipBaseProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'css' | 'title'\n> {\n /**\n * The trigger element that will show the tooltip on hover.\n * Must be a single React element or component.\n *\n * @example\n * <Button>Hover me</Button>\n * <IconButton><SaveIcon /></IconButton>\n */\n children: React.ReactElement;\n\n /**\n * The content to display in the tooltip.\n * Can be simple text or a complex React node for advanced customization.\n *\n * @example\n * \"Save file\"\n * <div><strong>Save</strong><br />Ctrl+S</div>\n */\n title: React.ReactNode;\n\n /**\n * Intuitive placement similar to MUI\n * @default \"top\"\n */\n placement?: TooltipPlacement;\n\n /**\n * Collision handling strategy\n * @default \"smart\"\n */\n collision?: TooltipCollisionStrategy;\n\n /**\n * Advanced collision configuration for power users\n * When provided, overrides the collision strategy\n */\n collisionConfig?: TooltipCollisionConfig;\n\n /**\n * Advanced positioning configuration\n */\n positioner?: TooltipPositioner;\n\n /**\n * Animation configuration\n */\n animation?: TooltipAnimation;\n\n /**\n * Delay in milliseconds before showing the tooltip\n * @default 600\n */\n delay?: number;\n\n /**\n * Delay in milliseconds before hiding the tooltip\n * @default 0\n */\n closeDelay?: number;\n\n /**\n * Whether to show the arrow pointing to the trigger\n * @default true\n */\n arrow?: boolean;\n\n /**\n * Whether the tooltip is disabled\n * When true, tooltip will not appear\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Direct props passed to Base UI Root component\n * For power users who need full control\n */\n rootProps?: Partial<BaseTooltipRootProps>;\n\n /**\n * Direct props passed to Base UI Positioner component\n * For power users who need full control\n */\n positionerProps?: Partial<BaseTooltipPositionerProps>;\n\n /**\n * Test identifier for automated testing\n */\n testId?: string;\n\n ref?: React.Ref<HTMLDivElement>;\n}\n\n/**\n * Props for the Tooltip component with prettified type for better IntelliSense\n */\nexport type TooltipProps = Prettify<TooltipBaseProps>;\n\n/**\n * A tooltip component that displays contextual information on hover.\n *\n * Built on @base-ui/react for robust accessibility and positioning.\n * Provides an intuitive API similar to MUI with advanced positioning and collision handling.\n * Supports both simple text and complex React nodes for advanced tooltip content.\n *\n * @example\n * ```tsx\n * // Simple text tooltip\n * <Tooltip title=\"Save your work\">\n * <Button>Save</Button>\n * </Tooltip>\n *\n * // Intuitive positioning (MUI-style)\n * <Tooltip placement=\"bottom-start\" title=\"Menu options\">\n * <IconButton><MenuIcon /></IconButton>\n * </Tooltip>\n *\n * // Intelligent collision handling (new default)\n * <Tooltip collision=\"smart\" title=\"Intelligent positioning\">\n * <Button>Smart tooltip</Button>\n * </Tooltip>\n *\n * // Advanced collision configuration\n * <Tooltip\n * collisionConfig={{\n * side: 'flip',\n * align: 'shift',\n * fallbackAxisSide: 'start',\n * hideWhenDetached: true\n * }}\n * title=\"Custom collision handling\"\n * >\n * <Button>Advanced collision</Button>\n * </Tooltip>\n *\n * // Advanced positioning\n * <Tooltip\n * positioner={{\n * offset: 12,\n * padding: 10,\n * sticky: true,\n * boundary: '#container'\n * }}\n * title=\"Advanced positioning\"\n * >\n * <Button>Advanced</Button>\n * </Tooltip>\n *\n * // Custom animations\n * <Tooltip\n * animation={{\n * animated: true,\n * duration: 300,\n * easing: 'ease-in-out'\n * }}\n * title=\"Custom animation\"\n * >\n * <Button>Animated</Button>\n * </Tooltip>\n *\n * // Interactive tooltip\n * <Tooltip\n * hoverable={true}\n * closeDelay={300}\n * title={\n * <div>\n * <strong>Interactive Content</strong>\n * <br />\n * <button>Click me</button>\n * </div>\n * }\n * >\n * <Button>Hoverable tooltip</Button>\n * </Tooltip>\n *\n * // Full control with raw props\n * <Tooltip\n * arrow={false}\n * rootProps={{ trackCursorAxis: 'x' }}\n * positionerProps={{ arrowPadding: 20 }}\n * title=\"Full control\"\n * >\n * <Button>Power user</Button>\n * </Tooltip>\n * ```\n */\nexport const Tooltip: React.FC<TooltipProps> = ({\n children,\n title,\n placement = 'top',\n collision = 'smart',\n collisionConfig,\n positioner = {},\n animation = {},\n delay = 600,\n closeDelay = 0,\n arrow = true,\n disabled = false,\n rootProps = {},\n positionerProps = {},\n className,\n testId,\n id,\n style,\n ref,\n ...htmlProps\n}) => {\n // If disabled, return just the children without tooltip\n if (disabled) {\n return children;\n }\n\n // Parse placement into side and align\n const { side, align } = parsePlacement(placement);\n\n // Parse collision strategy or use custom config\n const collisionSettings = collisionConfig\n ? {\n collisionAvoidance: {\n side: collisionConfig.side ?? 'flip',\n align: collisionConfig.align ?? 'shift',\n fallbackAxisSide: collisionConfig.fallbackAxisSide ?? 'none',\n } as CollisionAvoidance,\n }\n : parseCollisionStrategy(collision);\n\n // Prepare positioner configuration\n const {\n offset = 8,\n padding = 8,\n sticky = false,\n boundary,\n trackCursor,\n } = positioner;\n\n // Prepare final positioner props\n const finalPositionerProps: Partial<BaseTooltipPositionerProps> = {\n side,\n align,\n sideOffset: offset,\n collisionPadding: padding,\n sticky,\n collisionAvoidance: collisionSettings.collisionAvoidance,\n ...positionerProps,\n };\n\n // Handle boundary if specified\n if (boundary) {\n if (typeof boundary === 'string') {\n const boundaryElement = document.querySelector(boundary);\n if (boundaryElement) {\n finalPositionerProps.collisionBoundary = boundaryElement;\n }\n } else {\n finalPositionerProps.collisionBoundary = boundary;\n }\n }\n\n // Handle cursor tracking. Copy into a local object instead of mutating the\n // caller-owned `rootProps` prop.\n const finalRootProps: Partial<BaseTooltipRootProps> = { ...rootProps };\n if (trackCursor) {\n finalRootProps.trackCursorAxis = trackCursor;\n }\n\n // Build animation transition style. Honors `prefers-reduced-motion: reduce`\n // by collapsing to `transition: none` regardless of caller-passed config —\n // user OS-level preference always wins over component config.\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n const animated = animation.animated !== false && !prefersReducedMotion;\n const duration = animation.duration ?? 200;\n const easing = animation.easing ?? 'ease-out';\n\n const animationStyle: React.CSSProperties = animated\n ? {\n transition: `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`,\n }\n : { transition: 'none' };\n\n return (\n <BaseTooltip.Provider\n delay={delay}\n closeDelay={closeDelay}\n {...finalRootProps}\n >\n <BaseTooltip.Root>\n <BaseTooltip.Trigger\n render={props => (\n <div\n {...props}\n className={cx(tooltipTriggerStyle, props.className)}\n />\n )}\n >\n {children}\n </BaseTooltip.Trigger>\n\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner {...finalPositionerProps}>\n <BaseTooltip.Popup\n render={props => (\n <div\n {...props}\n className={cx(\n tooltipContentStyle,\n className,\n props.className\n )}\n data-testid={testId}\n id={id}\n style={{\n ...animationStyle,\n ...style,\n ...props.style,\n }}\n ref={ref}\n {...htmlProps}\n >\n {title}\n {arrow && (\n <StyledTooltipArrow>\n <ArrowSvg />\n </StyledTooltipArrow>\n )}\n </div>\n )}\n />\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n </BaseTooltip.Root>\n </BaseTooltip.Provider>\n );\n};\n\nTooltip.displayName = 'Tooltip';\n"],"names":[],"mappings":";;;;;;;;AA8HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFG;AACI;;;AAuBH;;;;;;AAQA;AACI;AACE;AACA;AACA;AACqB;AACxB;AACH;;AAGF;;AASA;;;AAGE;AACA;;;AAGA;;;;AAKA;;;AAGI;;;;AAGF;;;;;AAMJ;;AAEE;;;;;AAMF;AAEE;AACA;;AAEF;AACA;;AAGE;;AAEG;AACH;;AAkCgB;AACA;;;AAoBpB;AAEA;;"}
|
|
1
|
+
{"version":3,"file":"Tooltip.js","sources":["src/components/primitives/Tooltip/Tooltip.tsx"],"sourcesContent":["'use client';\n\n// src/components/primitives/Tooltip/Tooltip.tsx\nimport { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';\nimport React from 'react';\n\nimport { cx } from '@/utils/cx';\n\nimport { ArrowSvg, StyledTooltipArrow } from './Arrow';\nimport {\n tooltipContentStyle,\n tooltipTriggerStyle,\n tooltipPositionerStyle,\n} from './Tooltip.css';\nimport {\n CollisionAvoidance,\n TooltipAnimation,\n TooltipCollisionConfig,\n TooltipCollisionStrategy,\n TooltipPlacement,\n TooltipPositioner,\n BaseTooltipPositionerProps,\n BaseTooltipRootProps,\n} from './types';\nimport { parseCollisionStrategy, parsePlacement } from './utils';\n\nimport type { Prettify } from '@/types/utilities';\n\ninterface TooltipBaseProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'css' | 'title'\n> {\n /**\n * The trigger element that will show the tooltip on hover.\n * Must be a single React element or component.\n *\n * @example\n * <Button>Hover me</Button>\n * <IconButton><SaveIcon /></IconButton>\n */\n children: React.ReactElement;\n\n /**\n * The content to display in the tooltip.\n * Can be simple text or a complex React node for advanced customization.\n *\n * @example\n * \"Save file\"\n * <div><strong>Save</strong><br />Ctrl+S</div>\n */\n title: React.ReactNode;\n\n /**\n * Intuitive placement similar to MUI\n * @default \"top\"\n */\n placement?: TooltipPlacement;\n\n /**\n * Collision handling strategy\n * @default \"smart\"\n */\n collision?: TooltipCollisionStrategy;\n\n /**\n * Advanced collision configuration for power users\n * When provided, overrides the collision strategy\n */\n collisionConfig?: TooltipCollisionConfig;\n\n /**\n * Advanced positioning configuration\n */\n positioner?: TooltipPositioner;\n\n /**\n * Animation configuration\n */\n animation?: TooltipAnimation;\n\n /**\n * Delay in milliseconds before showing the tooltip\n * @default 600\n */\n delay?: number;\n\n /**\n * Delay in milliseconds before hiding the tooltip\n * @default 0\n */\n closeDelay?: number;\n\n /**\n * Whether to show the arrow pointing to the trigger\n * @default true\n */\n arrow?: boolean;\n\n /**\n * Whether the tooltip is disabled\n * When true, tooltip will not appear\n * @default false\n */\n disabled?: boolean;\n\n /**\n * Direct props passed to Base UI Root component\n * For power users who need full control\n */\n rootProps?: Partial<BaseTooltipRootProps>;\n\n /**\n * Direct props passed to Base UI Positioner component\n * For power users who need full control\n */\n positionerProps?: Partial<BaseTooltipPositionerProps>;\n\n /**\n * Test identifier for automated testing\n */\n testId?: string;\n\n ref?: React.Ref<HTMLDivElement>;\n}\n\n/**\n * Props for the Tooltip component with prettified type for better IntelliSense\n */\nexport type TooltipProps = Prettify<TooltipBaseProps>;\n\n/**\n * A tooltip component that displays contextual information on hover.\n *\n * Built on @base-ui/react for robust accessibility and positioning.\n * Provides an intuitive API similar to MUI with advanced positioning and collision handling.\n * Supports both simple text and complex React nodes for advanced tooltip content.\n *\n * @example\n * ```tsx\n * // Simple text tooltip\n * <Tooltip title=\"Save your work\">\n * <Button>Save</Button>\n * </Tooltip>\n *\n * // Intuitive positioning (MUI-style)\n * <Tooltip placement=\"bottom-start\" title=\"Menu options\">\n * <IconButton><MenuIcon /></IconButton>\n * </Tooltip>\n *\n * // Intelligent collision handling (new default)\n * <Tooltip collision=\"smart\" title=\"Intelligent positioning\">\n * <Button>Smart tooltip</Button>\n * </Tooltip>\n *\n * // Advanced collision configuration\n * <Tooltip\n * collisionConfig={{\n * side: 'flip',\n * align: 'shift',\n * fallbackAxisSide: 'start',\n * hideWhenDetached: true\n * }}\n * title=\"Custom collision handling\"\n * >\n * <Button>Advanced collision</Button>\n * </Tooltip>\n *\n * // Advanced positioning\n * <Tooltip\n * positioner={{\n * offset: 12,\n * padding: 10,\n * sticky: true,\n * boundary: '#container'\n * }}\n * title=\"Advanced positioning\"\n * >\n * <Button>Advanced</Button>\n * </Tooltip>\n *\n * // Custom animations\n * <Tooltip\n * animation={{\n * animated: true,\n * duration: 300,\n * easing: 'ease-in-out'\n * }}\n * title=\"Custom animation\"\n * >\n * <Button>Animated</Button>\n * </Tooltip>\n *\n * // Interactive tooltip\n * <Tooltip\n * hoverable={true}\n * closeDelay={300}\n * title={\n * <div>\n * <strong>Interactive Content</strong>\n * <br />\n * <button>Click me</button>\n * </div>\n * }\n * >\n * <Button>Hoverable tooltip</Button>\n * </Tooltip>\n *\n * // Full control with raw props\n * <Tooltip\n * arrow={false}\n * rootProps={{ trackCursorAxis: 'x' }}\n * positionerProps={{ arrowPadding: 20 }}\n * title=\"Full control\"\n * >\n * <Button>Power user</Button>\n * </Tooltip>\n * ```\n */\nexport const Tooltip: React.FC<TooltipProps> = ({\n children,\n title,\n placement = 'top',\n collision = 'smart',\n collisionConfig,\n positioner = {},\n animation = {},\n delay = 600,\n closeDelay = 0,\n arrow = true,\n disabled = false,\n rootProps = {},\n positionerProps = {},\n className,\n testId,\n id,\n style,\n ref,\n ...htmlProps\n}) => {\n // If disabled, return just the children without tooltip\n if (disabled) {\n return children;\n }\n\n // Parse placement into side and align\n const { side, align } = parsePlacement(placement);\n\n // Parse collision strategy or use custom config\n const collisionSettings = collisionConfig\n ? {\n collisionAvoidance: {\n side: collisionConfig.side ?? 'flip',\n align: collisionConfig.align ?? 'shift',\n fallbackAxisSide: collisionConfig.fallbackAxisSide ?? 'none',\n } as CollisionAvoidance,\n }\n : parseCollisionStrategy(collision);\n\n // Prepare positioner configuration\n const {\n offset = 8,\n padding = 8,\n sticky = false,\n boundary,\n trackCursor,\n } = positioner;\n\n // Prepare final positioner props\n const finalPositionerProps: Partial<BaseTooltipPositionerProps> = {\n side,\n align,\n sideOffset: offset,\n collisionPadding: padding,\n sticky,\n collisionAvoidance: collisionSettings.collisionAvoidance,\n ...positionerProps,\n };\n\n // Handle boundary if specified\n if (boundary) {\n if (typeof boundary === 'string') {\n const boundaryElement = document.querySelector(boundary);\n if (boundaryElement) {\n finalPositionerProps.collisionBoundary = boundaryElement;\n }\n } else {\n finalPositionerProps.collisionBoundary = boundary;\n }\n }\n\n // Merge our z-index positioner class with any caller-provided className,\n // honoring Base UI's function-className form (power-user `positionerProps`).\n const incomingPositionerClassName = finalPositionerProps.className;\n let positionerClassName: typeof incomingPositionerClassName;\n if (typeof incomingPositionerClassName === 'function') {\n positionerClassName = state =>\n cx(tooltipPositionerStyle, incomingPositionerClassName(state));\n } else {\n positionerClassName = cx(\n tooltipPositionerStyle,\n incomingPositionerClassName\n );\n }\n\n // Handle cursor tracking. Copy into a local object instead of mutating the\n // caller-owned `rootProps` prop.\n const finalRootProps: Partial<BaseTooltipRootProps> = { ...rootProps };\n if (trackCursor) {\n finalRootProps.trackCursorAxis = trackCursor;\n }\n\n // Build animation transition style. Honors `prefers-reduced-motion: reduce`\n // by collapsing to `transition: none` regardless of caller-passed config —\n // user OS-level preference always wins over component config.\n const prefersReducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n const animated = animation.animated !== false && !prefersReducedMotion;\n const duration = animation.duration ?? 200;\n const easing = animation.easing ?? 'ease-out';\n\n const animationStyle: React.CSSProperties = animated\n ? {\n transition: `opacity ${duration}ms ${easing}, transform ${duration}ms ${easing}`,\n }\n : { transition: 'none' };\n\n return (\n <BaseTooltip.Provider\n delay={delay}\n closeDelay={closeDelay}\n {...finalRootProps}\n >\n <BaseTooltip.Root>\n <BaseTooltip.Trigger\n render={props => (\n <div\n {...props}\n className={cx(tooltipTriggerStyle, props.className)}\n />\n )}\n >\n {children}\n </BaseTooltip.Trigger>\n\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner\n {...finalPositionerProps}\n className={positionerClassName}\n >\n <BaseTooltip.Popup\n render={props => (\n <div\n {...props}\n className={cx(\n tooltipContentStyle,\n className,\n props.className\n )}\n data-testid={testId}\n id={id}\n style={{\n ...animationStyle,\n ...style,\n ...props.style,\n }}\n ref={ref}\n {...htmlProps}\n >\n {title}\n {arrow && (\n <StyledTooltipArrow>\n <ArrowSvg />\n </StyledTooltipArrow>\n )}\n </div>\n )}\n />\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n </BaseTooltip.Root>\n </BaseTooltip.Provider>\n );\n};\n\nTooltip.displayName = 'Tooltip';\n"],"names":[],"mappings":";;;;;;;;AAkIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFG;AACI;;;AAuBH;;;;;;AAQA;AACI;AACE;AACA;AACA;AACqB;AACxB;AACH;;AAGF;;AASA;;;AAGE;AACA;;;AAGA;;;;AAKA;;;AAGI;;;;AAGF;;;;;AAMJ;AACA;AACA;AACE;;;AAGA;;;;AAQF;;AAEE;;;;;AAMF;AAEE;AACA;;AAEF;AACA;;AAGE;;AAEG;AACH;;AAqCgB;AACA;;;AAoBpB;AAEA;;"}
|
|
@@ -56,8 +56,11 @@ function categorizeChildren(children) {
|
|
|
56
56
|
return { layers, worldChildren, overlayChildren };
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Viewport — pan/zoom canvas + HTML container for editor-style
|
|
60
|
-
* such as node graphs, timelines, and 2D world editors.
|
|
59
|
+
* Viewport — **2D only.** A pan/zoom canvas + HTML container for editor-style
|
|
60
|
+
* surfaces such as node graphs, timelines, and 2D world editors. Despite the
|
|
61
|
+
* name it is *not* a 3D viewport: it owns a 2D pan/zoom transform, not a camera.
|
|
62
|
+
* For 3D, host your own WebGL/WebGPU canvas (e.g. inside an `AppShell.Dock`) and
|
|
63
|
+
* reach for `Viewport` only for 2D editor surfaces.
|
|
61
64
|
*
|
|
62
65
|
* Composes:
|
|
63
66
|
* - Multiple perf-isolated `<ViewportLayer>` canvases (one draw cycle each).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Viewport.js","sources":["src/components/primitives/viewport/Viewport.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, {\n useCallback,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\n\nimport { useControlledState, useResizeObserver } from '@/hooks';\nimport { cx } from '@/utils/cx';\nimport { clamp } from '@/utils/mathUtils';\n\nimport { useViewportGestures } from './useViewportGestures';\nimport {\n viewportRootRecipe,\n viewportHeightVar,\n marqueeRectStyle,\n} from './Viewport.css';\nimport { ViewportStoreContext, useViewportStore } from './ViewportContext';\nimport { ViewportStore } from './ViewportStore';\n\nimport type { ViewportProps, ViewportTransform } from './Viewport.types';\n\nconst IDENTITY_TRANSFORM: ViewportTransform = { x: 0, y: 0, zoom: 1 };\n\n// Stable empty defaults so `useViewportGestures` does not re-compute config\n// objects on every render when the consumer passes nothing.\nconst EMPTY_PAN = {};\nconst EMPTY_ZOOM = {};\nconst EMPTY_SELECTION = {};\n\ninterface CategorizedChildren {\n layers: React.ReactElement[];\n worldChildren: React.ReactElement[];\n overlayChildren: React.ReactElement[];\n}\n\nfunction getSlotKind(\n child: React.ReactElement\n): 'layer' | 'world' | 'overlay' | null {\n const displayName = (child.type as { displayName?: string } | undefined)\n ?.displayName;\n if (displayName === 'ViewportLayer') return 'layer';\n if (displayName === 'ViewportWorld') return 'world';\n if (displayName === 'ViewportOverlay') return 'overlay';\n // Compound components rendered in the overlay slot (e.g. <ViewportMinimap />).\n if (displayName === 'ViewportMinimap') return 'overlay';\n return null;\n}\n\nfunction categorizeChildren(children: React.ReactNode): CategorizedChildren {\n const layers: React.ReactElement[] = [];\n const worldChildren: React.ReactElement[] = [];\n const overlayChildren: React.ReactElement[] = [];\n\n React.Children.forEach(children, child => {\n if (!React.isValidElement(child)) return;\n const kind = getSlotKind(child);\n if (kind === 'layer') layers.push(child);\n else if (kind === 'world') worldChildren.push(child);\n else if (kind === 'overlay') overlayChildren.push(child);\n });\n\n return { layers, worldChildren, overlayChildren };\n}\n\n/**\n * Viewport — pan/zoom canvas + HTML container for editor-style
|
|
1
|
+
{"version":3,"file":"Viewport.js","sources":["src/components/primitives/viewport/Viewport.tsx"],"sourcesContent":["'use client';\n\nimport { assignInlineVars } from '@vanilla-extract/dynamic';\nimport React, {\n useCallback,\n useImperativeHandle,\n useLayoutEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from 'react';\n\nimport { useControlledState, useResizeObserver } from '@/hooks';\nimport { cx } from '@/utils/cx';\nimport { clamp } from '@/utils/mathUtils';\n\nimport { useViewportGestures } from './useViewportGestures';\nimport {\n viewportRootRecipe,\n viewportHeightVar,\n marqueeRectStyle,\n} from './Viewport.css';\nimport { ViewportStoreContext, useViewportStore } from './ViewportContext';\nimport { ViewportStore } from './ViewportStore';\n\nimport type { ViewportProps, ViewportTransform } from './Viewport.types';\n\nconst IDENTITY_TRANSFORM: ViewportTransform = { x: 0, y: 0, zoom: 1 };\n\n// Stable empty defaults so `useViewportGestures` does not re-compute config\n// objects on every render when the consumer passes nothing.\nconst EMPTY_PAN = {};\nconst EMPTY_ZOOM = {};\nconst EMPTY_SELECTION = {};\n\ninterface CategorizedChildren {\n layers: React.ReactElement[];\n worldChildren: React.ReactElement[];\n overlayChildren: React.ReactElement[];\n}\n\nfunction getSlotKind(\n child: React.ReactElement\n): 'layer' | 'world' | 'overlay' | null {\n const displayName = (child.type as { displayName?: string } | undefined)\n ?.displayName;\n if (displayName === 'ViewportLayer') return 'layer';\n if (displayName === 'ViewportWorld') return 'world';\n if (displayName === 'ViewportOverlay') return 'overlay';\n // Compound components rendered in the overlay slot (e.g. <ViewportMinimap />).\n if (displayName === 'ViewportMinimap') return 'overlay';\n return null;\n}\n\nfunction categorizeChildren(children: React.ReactNode): CategorizedChildren {\n const layers: React.ReactElement[] = [];\n const worldChildren: React.ReactElement[] = [];\n const overlayChildren: React.ReactElement[] = [];\n\n React.Children.forEach(children, child => {\n if (!React.isValidElement(child)) return;\n const kind = getSlotKind(child);\n if (kind === 'layer') layers.push(child);\n else if (kind === 'world') worldChildren.push(child);\n else if (kind === 'overlay') overlayChildren.push(child);\n });\n\n return { layers, worldChildren, overlayChildren };\n}\n\n/**\n * Viewport — **2D only.** A pan/zoom canvas + HTML container for editor-style\n * surfaces such as node graphs, timelines, and 2D world editors. Despite the\n * name it is *not* a 3D viewport: it owns a 2D pan/zoom transform, not a camera.\n * For 3D, host your own WebGL/WebGPU canvas (e.g. inside an `AppShell.Dock`) and\n * reach for `Viewport` only for 2D editor surfaces.\n *\n * Composes:\n * - Multiple perf-isolated `<ViewportLayer>` canvases (one draw cycle each).\n * - `<ViewportWorld>` for HTML positioned in world coordinates.\n * - `<ViewportOverlay>` for HTML pinned to the viewport (toolbars, minimap).\n *\n * Owns pan/zoom gestures (drag-to-pan, wheel-zoom-toward-cursor, optional\n * pinch and space-to-pan) and optional marquee selection. Transform is\n * controlled or uncontrolled (`useControlledState`-style) and mirrored\n * into an internal `ViewportStore` that powers slice-level subscriptions\n * for layers, world, and overlay children.\n *\n * @example\n * ```tsx\n * <Viewport responsive selectionRect={{ enabled: true }}>\n * <ViewportLayer name=\"grid\" draw={drawGrid} invalidateOn={[gridStep]} />\n * <ViewportLayer name=\"content\" draw={drawNodes} invalidateOn={[nodes]} />\n * <ViewportWorld>\n * <NodeView style={{ position: 'absolute', left: 100, top: 200 }} />\n * </ViewportWorld>\n * <ViewportOverlay>\n * <Toolbar />\n * </ViewportOverlay>\n * </Viewport>\n * ```\n */\nexport const Viewport = ({\n transform: transformProp,\n defaultTransform,\n onTransformChange,\n minZoom = 0.1,\n maxZoom = 8,\n pan,\n zoom,\n selectionRect,\n onSelectionChange,\n onPanStart,\n onPanEnd,\n onZoomStart,\n onZoomEnd,\n responsive = false,\n height = 300,\n disabled = false,\n role = 'application',\n ariaLabel,\n ariaRoledescription,\n className,\n style,\n testId,\n id,\n children,\n ref,\n ...rest\n}: ViewportProps): React.ReactElement => {\n const rootRef = useRef<HTMLDivElement>(null);\n\n // One store per <Viewport> instance, stable for the lifetime of the component.\n const store = useMemo(() => new ViewportStore(), []);\n\n // ── Controlled / uncontrolled transform (React state) ──\n const clampTransform = useCallback(\n (t: ViewportTransform): ViewportTransform => ({\n x: t.x,\n y: t.y,\n zoom: clamp(t.zoom, minZoom, maxZoom),\n }),\n [minZoom, maxZoom]\n );\n\n const [transformRaw, setTransformRaw] = useControlledState<ViewportTransform>(\n {\n value: transformProp,\n defaultValue: defaultTransform,\n onChange: onTransformChange,\n fallback: IDENTITY_TRANSFORM,\n }\n );\n\n const transform = useMemo(\n () => clampTransform(transformRaw),\n [transformRaw, clampTransform]\n );\n const setTransform = useCallback(\n (next: ViewportTransform) => {\n setTransformRaw(clampTransform(next));\n },\n [setTransformRaw, clampTransform]\n );\n\n // Mirror React state → store before paint so layer/world subscribers see the\n // latest transform in the same frame.\n useLayoutEffect(() => {\n store.setTransform(transform);\n }, [store, transform]);\n\n // Wire the store's imperative methods (fitToContent etc.) to React's\n // setTransform and current zoom limits.\n useLayoutEffect(() => {\n store.configureImperative({ setTransform, minZoom, maxZoom });\n }, [store, setTransform, minZoom, maxZoom]);\n\n // ── Size tracking ──\n useResizeObserver(rootRef, entry => {\n store.setSize({\n width: entry.contentRect.width,\n height: entry.contentRect.height,\n });\n });\n\n // Seed size on mount in case ResizeObserver fires asynchronously.\n useLayoutEffect(() => {\n const el = rootRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n store.setSize({ width: rect.width, height: rect.height });\n }, [store]);\n\n // ── Imperative ref ──\n useImperativeHandle(ref, () => store.handle, [store]);\n\n // ── Gestures ──\n const { handlers } = useViewportGestures({\n viewportRef: rootRef,\n store,\n setTransform,\n disabled,\n minZoom,\n maxZoom,\n pan: pan ?? EMPTY_PAN,\n zoom: zoom ?? EMPTY_ZOOM,\n selectionRect: selectionRect ?? EMPTY_SELECTION,\n callbacks: {\n onPanStart,\n onPanEnd,\n onZoomStart,\n onZoomEnd,\n onSelectionChange,\n },\n });\n\n // Subscribe to isPanning only — drives the cursor class on the wrapper.\n const isPanning = useSyncExternalStore(\n store.subscribeIsPanning,\n store.getIsPanning\n );\n\n // ── Children categorization ──\n const { layers, worldChildren, overlayChildren } = useMemo(\n () => categorizeChildren(children),\n [children]\n );\n\n const rootStyle = useMemo(\n () => ({\n ...style,\n ...(!responsive\n ? assignInlineVars({ [viewportHeightVar]: `${height}px` })\n : {}),\n }),\n [style, responsive, height]\n );\n\n return (\n <div\n ref={rootRef}\n id={id}\n data-testid={testId}\n role={role}\n aria-label={ariaLabel}\n aria-roledescription={ariaRoledescription}\n tabIndex={disabled ? -1 : 0}\n className={cx(\n viewportRootRecipe({\n responsive,\n disabled,\n panning: isPanning,\n }),\n className\n )}\n style={rootStyle}\n onPointerDown={handlers.onPointerDown}\n onPointerMove={handlers.onPointerMove}\n onPointerUp={handlers.onPointerUp}\n onPointerCancel={handlers.onPointerCancel}\n onPointerEnter={handlers.onPointerEnter}\n onPointerLeave={handlers.onPointerLeave}\n onContextMenu={handlers.onContextMenu}\n {...rest}\n >\n <ViewportStoreContext.Provider value={store}>\n {layers}\n {worldChildren}\n {overlayChildren}\n <MarqueeRect />\n </ViewportStoreContext.Provider>\n </div>\n );\n};\n\nViewport.displayName = 'Viewport';\n\n/**\n * Internal — renders the marquee selection rectangle. Subscribes only to\n * the marquee slice so it re-renders only when the rectangle changes,\n * never on transform/size/isPanning mutations.\n */\nconst MarqueeRect: React.FC = () => {\n const store = useViewportStore();\n const rect = useSyncExternalStore(\n store.subscribeMarquee,\n store.getMarqueeRect\n );\n if (!rect) return null;\n return (\n <div\n className={marqueeRectStyle}\n style={{\n left: rect.x,\n top: rect.y,\n width: rect.width,\n height: rect.height,\n }}\n data-viewport-marquee=\"\"\n />\n );\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA;AAEA;AACA;AACA;AACA;AACA;AAQA;AAGE;AACE;;AACmC;;AACA;;AACE;;;AAEA;AACvC;AACF;AAEA;;;;;AAMI;;AACA;;AACsB;;AACK;;AACE;AAC/B;AAEA;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;AACI;AA4BL;;AAGA;;;;;;AAQG;AAIH;AAEI;AACA;AACA;AACA;AACD;AAGH;AAIA;AAEI;AACF;;;;AAOA;AACF;;;;;;;AASA;;AAEI;AACA;AACD;AACH;;;AAIE;AACA;;AACA;AACA;AACF;;AAGA;;AAGA;AACE;;;;;;;;;AASA;;;;;;AAMC;AACF;;AAGD;;;AAWA;AAEI;;AAEE;;;AAMN;;;AAaQ;AACD;AAqBT;AAEA;AAEA;;;;AAIG;AACH;AACE;AACA;AAIA;AAAW;AACX;;;;;;AAYF;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import './../../../assets/src/components/shell/AppShell/AppShell.css.ts.vanilla-
|
|
1
|
+
import './../../../assets/src/components/shell/AppShell/AppShell.css.ts.vanilla-BLK2-DRL.css';
|
|
2
2
|
import { createRuntimeFn } from '@vanilla-extract/recipes/createRuntimeFn';
|
|
3
3
|
|
|
4
4
|
var dockSlot = 'AppShell_dockSlot__1m62ws2l';
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import './../../../assets/src/components/shell/MenuBar/MenuBar.css.ts.vanilla-
|
|
1
|
+
import './../../../assets/src/components/shell/MenuBar/MenuBar.css.ts.vanilla-Dt3w4f-q.css';
|
|
2
2
|
import { createRuntimeFn } from '@vanilla-extract/recipes/createRuntimeFn';
|
|
3
3
|
|
|
4
4
|
var dropdown = 'MenuBar_dropdown__1h81mxv7';
|
|
5
|
+
var indicatorSlot = 'MenuBar_indicatorSlot__1h81mxva';
|
|
5
6
|
var menuBarRoot = createRuntimeFn({defaultClassName:'MenuBar_menuBarRoot__1h81mxv0',variantClassNames:{size:{sm:'MenuBar_menuBarRoot_size_sm__1h81mxv1',md:'MenuBar_menuBarRoot_size_md__1h81mxv2'}},defaultVariants:{size:'md'},compoundVariants:[]});
|
|
6
7
|
var menuContainer = 'MenuBar_menuContainer__1h81mxv3';
|
|
7
8
|
var menuItem = 'MenuBar_menuItem__1h81mxv8';
|
|
8
|
-
var separator = '
|
|
9
|
+
var separator = 'MenuBar_separator__1h81mxvb';
|
|
9
10
|
var shortcut = 'MenuBar_shortcut__1h81mxv9';
|
|
10
|
-
var subContainer = '
|
|
11
|
-
var subDropdown = '
|
|
12
|
-
var subTrigger = '
|
|
11
|
+
var subContainer = 'MenuBar_subContainer__1h81mxve';
|
|
12
|
+
var subDropdown = 'MenuBar_subDropdown__1h81mxvd';
|
|
13
|
+
var subTrigger = 'MenuBar_subTrigger__1h81mxvc MenuBar_menuItem__1h81mxv8';
|
|
13
14
|
var trigger = createRuntimeFn({defaultClassName:'MenuBar_trigger__1h81mxv4',variantClassNames:{active:{true:'MenuBar_trigger_active_true__1h81mxv5',false:'MenuBar_trigger_active_false__1h81mxv6'}},defaultVariants:{active:false},compoundVariants:[]});
|
|
14
15
|
|
|
15
|
-
export { dropdown, menuBarRoot, menuContainer, menuItem, separator, shortcut, subContainer, subDropdown, subTrigger, trigger };
|
|
16
|
+
export { dropdown, indicatorSlot, menuBarRoot, menuContainer, menuItem, separator, shortcut, subContainer, subDropdown, subTrigger, trigger };
|
|
16
17
|
//# sourceMappingURL=MenuBar.css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuBar.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MenuBar.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
|