UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

1 lines 21.5 kB
{"version":3,"file":"resizable_utils-DhuzXRdP.cjs","names":[],"sources":["../components/resizable/resizable_constants.ts","../components/resizable/resizable_utils.ts"],"sourcesContent":["/**\n * Resizable panel system — types and injection keys.\n *\n * @see resizable.vue — provides all keys\n * @see resizable_panel.vue — consumes panel/layout keys\n * @see resizable_handle.vue — consumes handle/resize keys\n */\nimport type { InjectionKey, ComputedRef, ComponentInternalInstance } from 'vue';\nimport type { LayoutResult } from './composables/computeLayout';\n\n// ─── Sizing Types ──────────────────────────────────────────────────────────\n\n/**\n * Resizable panels accept size tokens (numeric strings mapped to pixel values)\n * or percentage values with 'p' suffix (e.g., '50p').\n * parseSizeToPixels() warns for unrecognized values.\n */\nexport type ResizableSizeValue = string;\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport type ResizableDirection = 'row' | 'column';\n\nexport interface ResizablePanelConfig {\n /** Panel identifier. Auto-generated by ResizableGroup if not provided. */\n id: string;\n /** Initial size - size token (e.g., '925') or percentage with 'p' suffix (e.g., '50p') */\n initialSize?: ResizableSizeValue;\n\n // ─── User Drag Limits (Absolute Boundaries) ───\n /** Hard floor for user dragging. User cannot drag panel smaller than this. */\n userMinSize?: ResizableSizeValue;\n /** Hard ceiling for user dragging. User cannot drag panel larger than this. */\n userMaxSize?: ResizableSizeValue;\n\n // ─── System Scaling Limits (Operating Range) ───\n /**\n * System scaling floor. System won't compress panel below this during viewport resize.\n * Must be >= userMinSize. Falls back to userMinSize if not specified.\n */\n systemMinSize?: ResizableSizeValue;\n /**\n * System scaling ceiling. System won't expand panel above this during viewport resize.\n * Must be <= userMaxSize. Falls back to userMaxSize if not specified.\n */\n systemMaxSize?: ResizableSizeValue;\n\n // ─── Auto-Collapse ───\n /**\n * Container width threshold for auto-collapse.\n * When container width drops below this value, panel auto-collapses.\n * This is the container width, not the panel width.\n */\n collapseSize?: ResizableSizeValue;\n\n // ─── Behavior Flags ───\n /** Whether this panel can be resized by user dragging */\n resizable?: boolean;\n /** Whether this panel can be manually collapsed via UI */\n collapsible?: boolean;\n /** Initial collapsed state */\n collapsed?: boolean;\n\n}\n\nexport interface ResizablePanelState extends ResizablePanelConfig {\n // ─── Core State ───\n /** Current size in pixels (primary storage) */\n pixelSize: number;\n\n // ─── User Drag Constraints (Computed Pixels) ───\n /** Computed pixel value for userMinSize (absolute floor for user dragging) */\n userMinSizePixels?: number;\n /** Computed pixel value for userMaxSize (absolute ceiling for user dragging) */\n userMaxSizePixels?: number;\n\n // ─── System Scaling Constraints (Computed Pixels) ───\n /** Computed pixel value for systemMinSize (system scaling floor) */\n systemMinSizePixels?: number;\n /** Computed pixel value for systemMaxSize (system scaling ceiling) */\n systemMaxSizePixels?: number;\n\n // ─── Auto-Collapse State ───\n /** Computed pixel value for collapseSize (container width threshold) */\n collapseSizePixels?: number;\n /** True if panel was auto-collapsed (vs manually collapsed via UI) */\n autoCollapsed?: boolean;\n\n // ─── Manual Resize Tracking ───\n /**\n * Internal: Stores the user's preferred pixel size when they drag a panel.\n * Used by the drag system to track intent before converting to ratio.\n * Not exposed as a component prop.\n */\n manualTargetSize?: number;\n /**\n * Stores the user's preferred ratio (0–1) when they drag a panel.\n * - undefined: no manual target, panel scales proportionally with viewport\n * - number: panel targets (ratio × containerSize) pixels on each render,\n * still subject to min/max constraints.\n *\n * Enables correct proportional scaling when the viewport changes.\n * Panel feels \"sticky\" at the chosen proportion.\n */\n manualTargetRatio?: number;\n\n // ─── Storage Restoration ───\n /**\n * True if this panel's state was loaded from localStorage on the current\n * initialization pass. Prevents double-restoration when loadFromStorage is\n * called multiple times (e.g. the deferred 100ms retry in useResizableCore).\n */\n restoredFromStorage?: boolean;\n\n}\n\nexport interface ResizableHandleConfig {\n id: string;\n beforePanelId: string;\n afterPanelId: string;\n direction: ResizableDirection;\n}\n\nexport type ResizableSizeMode = 'percentage' | 'pixels';\n\n\nexport interface ResizableGroupConfig {\n direction: ResizableDirection;\n panels: ResizablePanelConfig[];\n storageKey?: string; // Optional localStorage key for persistence\n sizeMode?: ResizableSizeMode; // Whether to use percentage or pixel-based sizing\n limitToParent?: boolean; // Limit resizing to parent viewport bounds\n}\n\nexport interface ResizableGroupState {\n direction: ResizableDirection;\n panels: ResizablePanelState[];\n containerSize: number; // Total container size in pixels\n isResizing: boolean;\n activeHandleId?: string;\n}\n\nexport interface ResizableEvents {\n 'panel-resize': (panelId: string, size: number) => void;\n 'panel-collapse': (panelId: string, collapsed: boolean) => void;\n 'resize-start': (handleId: string) => void;\n 'resize-end': (handleId: string) => void;\n}\n\n/**\n * Defines collapse behavior for a panel during space constraints.\n * Used to determine which panels collapse first when viewport shrinks.\n */\nexport interface CollapseRule {\n /** Panel ID this rule applies to */\n panelId: string;\n /** Collapse priority - lower numbers collapse first */\n priority: number;\n /** Optional threshold that triggers collapse (overrides panel's userMinSize) */\n minSizeBeforeCollapse?: ResizableSizeValue;\n}\n\n/**\n// ─── Defaults ─────────────────────────────────────────────────────────────\n\n/** Default panel size when no initialSize is specified (50% of container). */\nexport const DEFAULT_PANEL_SIZE = '50p';\n\n/** Minimum panel size in pixels below which a resize is rejected. */\nexport const MIN_PANEL_SIZE_PX = 10;\n\n// ─── Storage Adapter ───────────────────────────────────────────────────────\n\n/**\n * Interface for pluggable storage backends.\n * Implement this to persist panel layouts to Pinia, Vuex, IndexedDB, etc.\n * The built-in `localStorageAdapter(key)` factory creates a localStorage-backed adapter.\n */\nexport interface ResizableStorageAdapter {\n /** Persist the current panel layout. */\n save(data: ResizableStoragePanelData[]): void;\n /** Load a previously saved layout. Returns null if nothing is stored. */\n load(): ResizableStoragePanelData[] | null;\n /** Remove all persisted data for this layout. */\n clear(): void;\n}\n\n/**\n * Shape of a single panel's persisted data.\n * Intentionally minimal — only what's needed to restore a layout.\n */\nexport interface ResizableStoragePanelData {\n id: string;\n pixelSize: number;\n collapsed?: boolean;\n autoCollapsed?: boolean;\n manualTargetRatio?: number;\n}\n\n// ─── Injection Context ──────────────────────────────────────────────────────\n// Single context object for provide/inject between ResizableGroup, Panel, Handle.\n\nexport interface ResizableContext {\n // Reactive state\n layout: ComputedRef<LayoutResult>;\n panels: ComputedRef<ResizablePanelState[]>;\n panelMap: ComputedRef<Map<string, ResizablePanelState>>;\n direction: ComputedRef<ResizableDirection>;\n containerSize: ComputedRef<number>;\n containerElement: ComputedRef<HTMLElement | null>;\n isResizing: ComputedRef<boolean>;\n activeHandleId: ComputedRef<string | undefined>;\n isInitializing: ComputedRef<boolean>;\n messages: Record<string, string>;\n\n // Offset (from fixed header/toolbar)\n offsetHandleStyles: ComputedRef<Record<string, string>>;\n offsetContentStyles: ComputedRef<Record<string, string>>;\n\n // Operations\n startResize: (handleId: string) => void;\n resetPanels: (\n beforePanelId?: string,\n afterPanelId?: string,\n behavior?: 'both' | 'before' | 'after' | 'all',\n ) => void;\n registerHandle: (instance: ComponentInternalInstance | null) => void;\n unregisterHandle: (instance: ComponentInternalInstance | null) => void;\n registerPanel: (config: ResizablePanelConfig) => void;\n unregisterPanel: (id: string) => void;\n saveToStorage: () => void;\n announce: (message: string) => void;\n collapsePanel: (panelId: string, collapsed: boolean) => void;\n emitPanelResize: (panelId: string, size: number) => void;\n commitPanelSize: (panelId: string, pixels: number) => void;\n updateSavedPanel: (panelId: string, updates: Partial<ResizableStoragePanelData>) => void;\n}\n\nexport const RESIZABLE_CONTEXT_KEY: InjectionKey<ResizableContext> = Symbol('resizable-context');\n\n/**\n * Build a composite handle ID from the before and after panel IDs.\n * Handles use the format \"{beforePanelId}:{afterPanelId}\" throughout the system.\n */\nexport function buildHandleId(beforeId: string, afterId: string): string {\n return `${beforeId}:${afterId}`;\n}\n","import type { ResizableSizeValue } from './resizable_constants';\n\n// ─── Size Token Resolution ─────────────────────────────────────────────────\n// Resolves Dialtone size tokens (e.g., '925') to pixel values.\n// Reads --dt-size-{token} CSS custom properties at runtime to stay in sync\n// with the token pipeline. Falls back to a static map in environments where\n// CSS custom properties aren't available (tests, SSR).\n//\n// Will map to --dt-layout-* tokens when they land on the `next` branch.\n\n/** Cache for resolved token pixel values (populated on first use). */\nconst tokenCache = new Map<string, number>();\n\n/** Root font size cache — read once from getComputedStyle. */\nlet cachedRootFontSize: number | null = null;\n\nfunction getRootFontSize(): number {\n if (cachedRootFontSize !== null) return cachedRootFontSize;\n if (typeof document !== 'undefined') {\n cachedRootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 10;\n } else {\n cachedRootFontSize = 10; // Dialtone default\n }\n return cachedRootFontSize;\n}\n\n/**\n * Resolve a Dialtone size token to pixels via CSS custom properties.\n * Falls back to FALLBACK_SIZE_TOKENS when CSS isn't available.\n */\nfunction resolveTokenPixels(token: string): number | undefined {\n if (tokenCache.has(token)) return tokenCache.get(token);\n\n // Try runtime CSS resolution\n if (typeof document !== 'undefined') {\n const cssValue = getComputedStyle(document.documentElement)\n .getPropertyValue(`--dt-size-${token}`)\n .trim();\n\n if (cssValue) {\n const remMatch = cssValue.match(/^([\\d.]+)rem$/);\n if (remMatch) {\n const px = parseFloat(remMatch[1]) * getRootFontSize();\n tokenCache.set(token, px);\n return px;\n }\n const pxMatch = cssValue.match(/^([\\d.]+)px$/);\n if (pxMatch) {\n const px = parseFloat(pxMatch[1]);\n tokenCache.set(token, px);\n return px;\n }\n }\n }\n\n // Fallback: static map mirrors --dt-size-* tokens from dialtone-tokens\n // Kept for jsdom tests and SSR where CSS custom properties aren't loaded.\n if (token in FALLBACK_SIZE_TOKENS) {\n const px = FALLBACK_SIZE_TOKENS[token];\n tokenCache.set(token, px);\n return px;\n }\n\n return undefined;\n}\n\n/**\n * Static fallback map — mirrors Dialtone size tokens (base/default.json).\n * Only used when CSS custom properties are unavailable (tests, SSR).\n */\nconst FALLBACK_SIZE_TOKENS: Record<string, number> = {\n '0': 0, '50': 0.5, '100': 1, '200': 2, '300': 4, '350': 6,\n '400': 8, '450': 12, '500': 16, '525': 20, '550': 24, '600': 32,\n '625': 42, '650': 48, '700': 64, '720': 72, '730': 84, '750': 96,\n '760': 102, '775': 114, '800': 128, '825': 164, '850': 192, '875': 216,\n '900': 256, '905': 264, '925': 332, '950': 384, '975': 464, '1000': 512,\n '1020': 628, '1040': 764, '1050': 768, '1060': 828, '1080': 912,\n '1100': 1024, '1115': 1140, '1120': 1268, '1125': 1280, '1130': 1340,\n '1150': 1536, '1200': 2048,\n};\n\n// ─── Percentage Resolution ─────────────────────────────────────────────────\n// Percentage tokens use a simple pattern: numeric value + 'p' suffix.\n\nfunction parsePercentage(value: string): number | undefined {\n if (!value.endsWith('p')) return undefined;\n const num = parseFloat(value.slice(0, -1));\n return isFinite(num) && num >= 0 && num <= 100 ? num : undefined;\n}\n\n// ─── Token Helpers ──────────────────────────────────────────────────────────\n\nfunction isSizeToken(value: string): boolean {\n return resolveTokenPixels(value) !== undefined;\n}\n\nfunction isPercentageToken(value: string): boolean {\n return parsePercentage(value) !== undefined;\n}\n\nexport function isValidSizing(value: string): boolean {\n return isSizeToken(value) || isPercentageToken(value);\n}\n\nfunction parseTokenToPixels(value: string, containerSize: number): number {\n const sizePixels = resolveTokenPixels(value);\n if (sizePixels !== undefined) return sizePixels;\n\n const percentage = parsePercentage(value);\n if (percentage !== undefined) return (percentage / 100) * containerSize;\n\n console.warn(`[resizable] Invalid sizing value: ${value}`);\n return 0;\n}\n\n// ─── Public API ─────────────────────────────────────────────────────────────\n\nexport interface ParseSizeOptions {\n /**\n * When true, clamps the result to container size.\n * Panels cannot exceed their parent container.\n * @default true\n */\n clampToContainer?: boolean;\n}\n\n/**\n * Parses a ResizableSizeValue and returns the pixel value.\n * Handles size tokens (e.g., '925') and percentage tokens (e.g., '50p').\n *\n * Size tokens resolve from --dt-size-{token} CSS custom properties at runtime,\n * falling back to a static map in test/SSR environments.\n *\n * @param value - Size token or percentage token\n * @param containerSize - Container size in pixels\n * @param options - Optional configuration\n * @returns Pixel value, clamped to container by default\n *\n * @example\n * parseSizeToPixels('925', 1000) // Returns 332 (from --dt-size-925)\n * parseSizeToPixels('50p', 1000) // Returns 500 (50% of 1000)\n * parseSizeToPixels('1100', 1000) // Returns 1000 (clamped from 1024px)\n */\nexport function parseSizeToPixels(\n value: ResizableSizeValue,\n containerSize: number,\n options?: ParseSizeOptions\n): number {\n const { clampToContainer = true } = options ?? {};\n const validatedContainerSize = validateContainerSize(containerSize);\n\n if (isCollapsedPanel(validatedContainerSize, value)) {\n return 0;\n }\n\n const calculationContainerSize = validatedContainerSize === 0 ? 1000 : validatedContainerSize;\n\n if (typeof value === 'string' && isValidSizing(value)) {\n const result = parseTokenToPixels(value, calculationContainerSize);\n return validatePixelResult(result, value, validatedContainerSize, clampToContainer);\n }\n\n console.warn(\n `[resizable] Invalid ResizableSizeValue: ${value}. Expected a size token or percentage with 'p' suffix.`\n );\n return 0;\n}\n\nexport function validateContainerSize(containerSize: number): number {\n if (!isFinite(containerSize) || containerSize < 0) {\n console.warn(`[resizable] Invalid containerSize: ${containerSize}. Using fallback value of 1000px.`);\n return 1000;\n }\n\n if (containerSize > 10000) {\n console.warn(`[resizable] Unusually large containerSize: ${containerSize}px. Capping at 10000px.`);\n return 10000;\n }\n\n return containerSize;\n}\n\nfunction isCollapsedPanel(containerSize: number, value: ResizableSizeValue): boolean {\n return containerSize === 0 && value === '0';\n}\n\nfunction validatePixelResult(\n result: number,\n value: ResizableSizeValue,\n containerSize: number,\n clampToContainer: boolean\n): number {\n if (!isFinite(result) || result < 0) {\n console.warn(\n `[resizable] Invalid pixel calculation result: ${result} for value: ${value}, containerSize: ${containerSize}`\n );\n return 0;\n }\n\n if (clampToContainer && containerSize > 0 && result > containerSize) {\n console.warn(\n `[resizable] Size value '${value}' (${result}px) exceeds container (${containerSize}px). Clamping to container.`\n );\n return containerSize;\n }\n\n return result;\n}\n\nexport function isPercentageValue(value: ResizableSizeValue): boolean {\n return isPercentageToken(value);\n}\n\nexport function isCSSValue(value: ResizableSizeValue): boolean {\n return isSizeToken(value);\n}\n\nexport function pixelsToPercentage(pixels: number, containerSize: number): number {\n return (pixels / containerSize) * 100;\n}\n\n/**\n * Checks if a panel's userMinSize is percentage-based (e.g., '50p').\n */\nexport function hasPercentageMinSize(panel: { userMinSize?: ResizableSizeValue }): boolean {\n if (!panel.userMinSize) return false;\n return isPercentageToken(panel.userMinSize);\n}\n\n/**\n * Invalidate the token cache. Call when the theme changes or\n * when token values may have been updated at runtime.\n */\nexport function invalidateTokenCache(): void {\n tokenCache.clear();\n cachedRootFontSize = null;\n}\n"],"mappings":"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"}