@papernote/ui 1.10.16 → 1.10.18

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/dist/index.js CHANGED
@@ -327,7 +327,9 @@ function ButtonGroup({ options, value, values = [], onChange, onChangeMultiple,
327
327
  * ```
328
328
  */
329
329
  const Input = React.forwardRef(({ label, helperText, validationState, validationMessage, icon, iconPosition = 'left', showCount = false, prefix, suffix, prefixIcon, suffixIcon, showPasswordToggle = false, clearable = false, onClear, loading = false, className = '', id, type = 'text', value, maxLength, inputMode, enterKeyHint, size = 'md', ...props }, ref) => {
330
- const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
330
+ const generatedId = React.useId();
331
+ const inputId = id || generatedId;
332
+ const helperId = `${inputId}-helper`;
331
333
  const [showPassword, setShowPassword] = React.useState(false);
332
334
  // Handle clear button click
333
335
  const handleClear = () => {
@@ -408,7 +410,7 @@ const Input = React.forwardRef(({ label, helperText, validationState, validation
408
410
  return 'text-ink-600';
409
411
  }
410
412
  };
411
- return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { htmlFor: inputId, className: "label", children: [label, props.required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-500 text-sm", children: prefix })), prefixIcon && !prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: prefixIcon })), icon && iconPosition === 'left' && !prefix && !prefixIcon && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: icon })), jsxRuntime.jsx("input", { ref: ref, id: inputId, type: actualType, value: value, maxLength: maxLength, inputMode: effectiveInputMode, enterKeyHint: enterKeyHint, className: `
413
+ return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { htmlFor: inputId, className: "label", children: [label, props.required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-500 text-sm", children: prefix })), prefixIcon && !prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: prefixIcon })), icon && iconPosition === 'left' && !prefix && !prefixIcon && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: icon })), jsxRuntime.jsx("input", { ref: ref, id: inputId, type: actualType, value: value, maxLength: maxLength, inputMode: effectiveInputMode, enterKeyHint: enterKeyHint, "aria-invalid": validationState === 'error', "aria-describedby": helperText || validationMessage ? helperId : undefined, "aria-required": props.required, className: `
412
414
  input
413
415
  ${sizeClasses[size]}
414
416
  ${getValidationClasses()}
@@ -421,7 +423,7 @@ const Input = React.forwardRef(({ label, helperText, validationState, validation
421
423
  ${validationState && !suffix && !suffixIcon && !showPasswordToggle ? 'pr-10' : ''}
422
424
  ${(showPasswordToggle && type === 'password') || validationState || suffix || suffixIcon ? 'pr-20' : ''}
423
425
  ${className}
424
- `, ...props }), suffix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-ink-500 text-sm", children: suffix })), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center gap-1", children: [loading && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin" }) })), suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && !loading && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: suffixIcon })), showClearButton && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: `text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`, "aria-label": "Clear input", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })), type === 'password' && showPasswordToggle && (jsxRuntime.jsx("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: `text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`, "aria-label": showPassword ? 'Hide password' : 'Show password', children: showPassword ? jsxRuntime.jsx(lucideReact.EyeOff, { className: "h-5 w-5" }) : jsxRuntime.jsx(lucideReact.Eye, { className: "h-5 w-5" }) })), validationState && (jsxRuntime.jsx("div", { className: "pointer-events-none", children: getValidationIcon() })), icon && iconPosition === 'right' && !suffix && !suffixIcon && !validationState && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: icon }))] })] }), jsxRuntime.jsxs("div", { className: "flex justify-between items-center mt-2", children: [(helperText || validationMessage) && (jsxRuntime.jsx("p", { className: `text-xs ${validationMessage ? getValidationMessageColor() : 'text-ink-600'}`, children: validationMessage || helperText })), showCounter && (jsxRuntime.jsxs("p", { className: `text-xs ml-auto ${currentLength > maxLength ? 'text-error-600' : 'text-ink-500'}`, children: [currentLength, " / ", maxLength] }))] })] }));
426
+ `, ...props }), suffix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-ink-500 text-sm", children: suffix })), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center gap-1", children: [loading && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin" }) })), suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && !loading && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: suffixIcon })), showClearButton && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: `text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`, "aria-label": "Clear input", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })), type === 'password' && showPasswordToggle && (jsxRuntime.jsx("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: `text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`, "aria-label": showPassword ? 'Hide password' : 'Show password', children: showPassword ? jsxRuntime.jsx(lucideReact.EyeOff, { className: "h-5 w-5" }) : jsxRuntime.jsx(lucideReact.Eye, { className: "h-5 w-5" }) })), validationState && (jsxRuntime.jsx("div", { className: "pointer-events-none", children: getValidationIcon() })), icon && iconPosition === 'right' && !suffix && !suffixIcon && !validationState && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: icon }))] })] }), jsxRuntime.jsxs("div", { className: "flex justify-between items-center mt-2", children: [(helperText || validationMessage) && (jsxRuntime.jsx("p", { id: helperId, className: `text-xs ${validationMessage ? getValidationMessageColor() : 'text-ink-600'}`, role: validationState === 'error' ? 'alert' : undefined, "aria-live": validationState === 'error' ? 'assertive' : undefined, children: validationMessage || helperText })), showCounter && (jsxRuntime.jsxs("p", { className: `text-xs ml-auto ${currentLength > maxLength ? 'text-error-600' : 'text-ink-500'}`, children: [currentLength, " / ", maxLength] }))] })] }));
425
427
  });
426
428
  Input.displayName = 'Input';
427
429
 
@@ -804,7 +806,7 @@ const optionSizeClasses = {
804
806
  * ```
805
807
  */
806
808
  const Select = React.forwardRef((props, ref) => {
807
- const { options = [], groups = [], value, onChange, placeholder = 'Select an option', searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = '300px', virtualItemHeight = 42, size = 'md', mobileMode = 'auto', usePortal = true, } = props;
809
+ const { options = [], groups = [], value, onChange, placeholder = 'Select an option', searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = '300px', virtualItemHeight = 42, size = 'md', mobileMode = 'auto', usePortal = true, required = false, } = props;
808
810
  const [isOpen, setIsOpen] = React.useState(false);
809
811
  const [searchQuery, setSearchQuery] = React.useState('');
810
812
  const [scrollTop, setScrollTop] = React.useState(0);
@@ -1037,19 +1039,19 @@ const Select = React.forwardRef((props, ref) => {
1037
1039
  };
1038
1040
  // Native select for mobile (optional)
1039
1041
  if (useNativeSelect) {
1040
- return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsx("label", { id: labelId, className: "label", children: label })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("select", { ref: nativeSelectRef, value: value || '', onChange: (e) => onChange?.(e.target.value), disabled: disabled, className: `
1042
+ return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { id: labelId, className: "label", children: [label, required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("select", { ref: nativeSelectRef, value: value || '', onChange: (e) => onChange?.(e.target.value), disabled: disabled, className: `
1041
1043
  input w-full appearance-none pr-10
1042
1044
  ${sizeClasses$b[effectiveSize]}
1043
1045
  ${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
1044
1046
  ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
1045
- `, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (jsxRuntime.jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))), groups.map((group) => (jsxRuntime.jsx("optgroup", { label: group.label, children: group.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }, group.label)))] }), jsxRuntime.jsx(lucideReact.ChevronDown, { className: "absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-500 pointer-events-none" })] }), error && (jsxRuntime.jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsxRuntime.jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
1047
+ `, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), "aria-required": required, children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (jsxRuntime.jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))), groups.map((group) => (jsxRuntime.jsx("optgroup", { label: group.label, children: group.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }, group.label)))] }), jsxRuntime.jsx(lucideReact.ChevronDown, { className: "absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-500 pointer-events-none" })] }), error && (jsxRuntime.jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsxRuntime.jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
1046
1048
  }
1047
- return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsx("label", { id: labelId, className: "label", children: label })), jsxRuntime.jsx("div", { ref: selectRef, className: "relative", children: jsxRuntime.jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
1049
+ return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { id: labelId, className: "label", children: [label, required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsx("div", { ref: selectRef, className: "relative", children: jsxRuntime.jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
1048
1050
  input w-full flex items-center justify-between px-3
1049
1051
  ${sizeClasses$b[effectiveSize]}
1050
1052
  ${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
1051
1053
  ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
1052
- `, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? placeholder : undefined, "aria-activedescendant": activeDescendant, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), "aria-disabled": disabled, children: [jsxRuntime.jsxs("span", { className: `flex items-center gap-2 ${selectedOption ? 'text-ink-800' : 'text-ink-400'}`, children: [loading && jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin text-ink-500" }), !loading && selectedOption?.icon && jsxRuntime.jsx("span", { children: selectedOption.icon }), selectedOption ? selectedOption.label : placeholder] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [clearable && value && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
1054
+ `, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? placeholder : undefined, "aria-activedescendant": activeDescendant, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), "aria-disabled": disabled, "aria-required": required, children: [jsxRuntime.jsxs("span", { className: `flex items-center gap-2 ${selectedOption ? 'text-ink-800' : 'text-ink-400'}`, children: [loading && jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin text-ink-500" }), !loading && selectedOption?.icon && jsxRuntime.jsx("span", { children: selectedOption.icon }), selectedOption ? selectedOption.label : placeholder] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [clearable && value && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
1053
1055
  e.stopPropagation();
1054
1056
  onChange?.('');
1055
1057
  setIsOpen(false);
@@ -1654,7 +1656,7 @@ const DatePicker = React.forwardRef(({ value, onChange, label, placeholder = 'Se
1654
1656
  ${disabled ? 'bg-paper-100 text-ink-400 cursor-not-allowed' : 'cursor-pointer'}
1655
1657
  focus:outline-none focus:ring-2
1656
1658
  pr-10
1657
- `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Date picker' : undefined, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dialogId, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-3 gap-1", children: [showClearButton && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
1659
+ `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Date picker' : undefined, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dialogId, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, "aria-required": required, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-3 gap-1", children: [showClearButton && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
1658
1660
  e.stopPropagation();
1659
1661
  handleClear();
1660
1662
  }, className: "text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": "Clear date", children: jsxRuntime.jsx(lucideReact.X, { className: iconSizeClasses[size] }) })), jsxRuntime.jsx(lucideReact.Calendar, { className: `${iconSizeClasses[size]} text-ink-400` })] })] }), validationMessage && (jsxRuntime.jsx("p", { id: descriptionId, className: `mt-1 text-xs ${validationState ? validationMessageColors[validationState] : 'text-ink-500'}`, role: "alert", "aria-live": "polite", children: validationMessage })), helperText && !validationMessage && (jsxRuntime.jsx("p", { className: "mt-1 text-xs text-ink-500", children: helperText })), isOpen && (jsxRuntime.jsxs("div", { id: dialogId, className: "absolute z-50 mt-1 bg-white rounded-lg shadow-lg border border-paper-200 p-3 w-72", role: "dialog", "aria-modal": "true", "aria-label": "Date picker calendar", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [jsxRuntime.jsx("button", { type: "button", onClick: goToPrevMonth, className: "p-1 rounded hover:bg-paper-100 text-ink-600 hover:text-ink-900", "aria-label": "Previous month", children: jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-5 w-5" }) }), jsxRuntime.jsxs("span", { className: "text-sm font-semibold text-ink-900", children: [MONTHS[viewDate.getMonth()], " ", viewDate.getFullYear()] }), jsxRuntime.jsx("button", { type: "button", onClick: goToNextMonth, className: "p-1 rounded hover:bg-paper-100 text-ink-600 hover:text-ink-900", "aria-label": "Next month", children: jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-5 w-5" }) })] }), jsxRuntime.jsx("div", { className: "grid grid-cols-7 mb-1", children: DAYS.map(day => (jsxRuntime.jsx("div", { className: "text-center text-xs font-medium text-ink-500 py-1", children: day }, day))) }), jsxRuntime.jsx("div", { className: "grid grid-cols-7", children: generateCalendarDays().map(({ date, isCurrentMonth }, index) => {
@@ -1845,7 +1847,7 @@ const TimePicker = React.forwardRef(({ value = null, onChange, label, placeholde
1845
1847
  ${disabled ? 'bg-paper-100 text-ink-400 cursor-not-allowed' : ''}
1846
1848
  focus:outline-none focus:ring-2
1847
1849
  pr-20
1848
- `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Time picker' : undefined, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dropdownId, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
1850
+ `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Time picker' : undefined, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dropdownId, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, "aria-required": required, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
1849
1851
  e.stopPropagation();
1850
1852
  handleClear();
1851
1853
  }, className: "p-0.5 text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": "Clear", tabIndex: -1, children: jsxRuntime.jsx(lucideReact.X, { className: iconSizeClasses[size] }) })), jsxRuntime.jsx(lucideReact.Clock, { className: `${iconSizeClasses[size]} text-ink-400` })] })] }), validationMessage && (jsxRuntime.jsx("p", { id: descriptionId, className: `mt-1 text-xs ${validationState ? validationMessageColors[validationState] : 'text-ink-500'}`, role: "alert", "aria-live": "polite", children: validationMessage })), helperText && !validationMessage && (jsxRuntime.jsx("p", { className: "mt-1 text-xs text-ink-500", children: helperText })), isOpen && (jsxRuntime.jsx("div", { id: dropdownId, className: "absolute z-50 mt-1 bg-white rounded-md shadow-lg border border-paper-200", role: "dialog", "aria-modal": "true", "aria-label": "Time selection", children: jsxRuntime.jsxs("div", { className: "p-4 flex items-center gap-4", children: [jsxRuntime.jsx(TimeSpinner, { value: timeValue.hours, min: use12Hour ? 1 : 0, max: use12Hour ? 12 : 23, onChange: (hours) => updateTime({ hours }), label: "Hour" }), jsxRuntime.jsx("span", { className: "text-2xl font-bold text-ink-600", children: ":" }), jsxRuntime.jsx(TimeSpinner, { value: timeValue.minutes, min: 0, max: 59, step: minuteStep, options: minuteOptions, onChange: (minutes) => updateTime({ minutes }), label: "Min" }), showSeconds && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "text-2xl font-bold text-ink-600", children: ":" }), jsxRuntime.jsx(TimeSpinner, { value: timeValue.seconds, min: 0, max: 59, onChange: (seconds) => updateTime({ seconds }), label: "Sec" })] })), use12Hour && (jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [jsxRuntime.jsx("button", { type: "button", onClick: () => updateTime({ period: 'AM' }), className: `
@@ -2105,7 +2107,7 @@ const DateRangePicker = React.forwardRef(({ value = { start: null, end: null },
2105
2107
  ${disabled ? 'bg-paper-100 text-ink-400 cursor-not-allowed' : ''}
2106
2108
  focus:outline-none focus:ring-2
2107
2109
  pr-20
2108
- `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Date range picker' : undefined, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dialogId, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : (isOpen ? hintId : undefined), role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [(value.start || value.end) && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
2110
+ `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Date range picker' : undefined, "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dialogId, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : (isOpen ? hintId : undefined), "aria-required": required, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [(value.start || value.end) && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
2109
2111
  e.stopPropagation();
2110
2112
  handleClear();
2111
2113
  }, className: "p-0.5 text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": "Clear", tabIndex: -1, children: jsxRuntime.jsx(lucideReact.X, { className: iconSizeClasses[size] }) })), jsxRuntime.jsx(lucideReact.Calendar, { className: `${iconSizeClasses[size]} text-ink-400` })] })] }), validationMessage && (jsxRuntime.jsx("p", { id: descriptionId, className: `mt-1 text-xs ${validationState ? validationMessageColors[validationState] : 'text-ink-500'}`, role: "alert", "aria-live": "polite", children: validationMessage })), helperText && !validationMessage && (jsxRuntime.jsx("p", { className: "mt-1 text-xs text-ink-500", children: helperText })), isOpen && (jsxRuntime.jsx("div", { id: dialogId, className: "absolute z-50 mt-1 bg-white rounded-md shadow-lg border border-paper-200 p-4", role: "dialog", "aria-modal": "true", "aria-label": "Date range selection", children: jsxRuntime.jsxs("div", { className: "flex gap-4", children: [showPresets && (jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 border-r border-paper-200 pr-4", children: [jsxRuntime.jsx("button", { type: "button", onClick: () => handlePreset('today'), className: "text-left px-3 py-2 text-sm text-ink-700 hover:bg-primary-50 rounded whitespace-nowrap", children: "Today" }), jsxRuntime.jsx("button", { type: "button", onClick: () => handlePreset('yesterday'), className: "text-left px-3 py-2 text-sm text-ink-700 hover:bg-primary-50 rounded whitespace-nowrap", children: "Yesterday" }), jsxRuntime.jsx("button", { type: "button", onClick: () => handlePreset('last7days'), className: "text-left px-3 py-2 text-sm text-ink-700 hover:bg-primary-50 rounded whitespace-nowrap", children: "Last 7 days" }), jsxRuntime.jsx("button", { type: "button", onClick: () => handlePreset('last30days'), className: "text-left px-3 py-2 text-sm text-ink-700 hover:bg-primary-50 rounded whitespace-nowrap", children: "Last 30 days" }), jsxRuntime.jsx("button", { type: "button", onClick: () => handlePreset('thisMonth'), className: "text-left px-3 py-2 text-sm text-ink-700 hover:bg-primary-50 rounded whitespace-nowrap", children: "This month" }), jsxRuntime.jsx("button", { type: "button", onClick: () => handlePreset('lastMonth'), className: "text-left px-3 py-2 text-sm text-ink-700 hover:bg-primary-50 rounded whitespace-nowrap", children: "Last month" })] })), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [jsxRuntime.jsx("button", { type: "button", onClick: previousMonth, className: "p-2 text-ink-600 hover:bg-paper-100 rounded", "aria-label": "Previous month", children: jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-5 w-5" }) }), jsxRuntime.jsx("div", { className: "text-sm font-semibold text-ink-900", children: monthYear }), jsxRuntime.jsx("button", { type: "button", onClick: nextMonth, className: "p-2 text-ink-600 hover:bg-paper-100 rounded", "aria-label": "Next month", children: jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-5 w-5" }) })] }), jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1 mb-2", children: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((day) => (jsxRuntime.jsx("div", { className: "text-center text-xs font-medium text-ink-500 w-8", children: day }, day))) }), jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1", children: calendarDays.map((date, index) => {
@@ -2667,7 +2669,7 @@ const Combobox = React.forwardRef(({ value = '', onChange, options, onSearch, on
2667
2669
  ${disabled ? 'bg-paper-100 text-ink-400 cursor-not-allowed' : ''}
2668
2670
  focus:outline-none focus:ring-2
2669
2671
  pr-20
2670
- `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Combobox' : undefined, "aria-expanded": isOpen, "aria-autocomplete": "list", "aria-controls": listboxId, "aria-activedescendant": isOpen && filteredOptions.length > 0 ? `option-${highlightedIndex}` : undefined, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [loading && (jsxRuntime.jsx("div", { className: "animate-spin", children: jsxRuntime.jsx(lucideReact.Search, { className: `${iconSizeClasses[size]} text-ink-400` }) })), !loading && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "p-0.5 text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": "Clear", tabIndex: -1, children: jsxRuntime.jsx(lucideReact.X, { className: iconSizeClasses[size] }) })), !loading && (jsxRuntime.jsx(lucideReact.ChevronDown, { className: `${iconSizeClasses[size]} text-ink-400 transition-transform ${isOpen ? 'rotate-180' : ''}` }))] })] }) }), validationMessage && (jsxRuntime.jsx("p", { id: descriptionId, className: `mt-1 text-xs ${validationState ? validationMessageColors[validationState] : 'text-ink-500'}`, role: "alert", "aria-live": "polite", children: validationMessage })), helperText && !validationMessage && (jsxRuntime.jsx("p", { className: "mt-1 text-xs text-ink-500", children: helperText })), isOpen && (jsxRuntime.jsx("div", { className: "absolute z-50 mt-1 w-full bg-white rounded-md shadow-lg border border-paper-200 max-h-60 overflow-auto", role: "listbox", id: listboxId, "aria-label": "Available options", children: loading ? (jsxRuntime.jsx("div", { className: "px-4 py-8 text-center text-ink-500 text-sm", role: "status", "aria-live": "polite", children: "Loading..." })) : filteredOptions.length === 0 && !canCreateOption ? (jsxRuntime.jsx("div", { className: "px-4 py-8 text-center text-ink-500 text-sm", role: "status", "aria-live": "polite", children: "No options found" })) : (jsxRuntime.jsxs("ul", { ref: listRef, children: [filteredOptions.map((option, index) => {
2672
+ `, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Combobox' : undefined, "aria-expanded": isOpen, "aria-autocomplete": "list", "aria-controls": listboxId, "aria-activedescendant": isOpen && filteredOptions.length > 0 ? `option-${highlightedIndex}` : undefined, "aria-invalid": validationState === 'error' ? 'true' : undefined, "aria-describedby": validationMessage ? descriptionId : undefined, "aria-required": required, role: "combobox" }), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 flex items-center pr-2 gap-1", children: [loading && (jsxRuntime.jsx("div", { className: "animate-spin", children: jsxRuntime.jsx(lucideReact.Search, { className: `${iconSizeClasses[size]} text-ink-400` }) })), !loading && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "p-0.5 text-ink-400 hover:text-ink-600 focus:outline-none", "aria-label": "Clear", tabIndex: -1, children: jsxRuntime.jsx(lucideReact.X, { className: iconSizeClasses[size] }) })), !loading && (jsxRuntime.jsx(lucideReact.ChevronDown, { className: `${iconSizeClasses[size]} text-ink-400 transition-transform ${isOpen ? 'rotate-180' : ''}` }))] })] }) }), validationMessage && (jsxRuntime.jsx("p", { id: descriptionId, className: `mt-1 text-xs ${validationState ? validationMessageColors[validationState] : 'text-ink-500'}`, role: "alert", "aria-live": "polite", children: validationMessage })), helperText && !validationMessage && (jsxRuntime.jsx("p", { className: "mt-1 text-xs text-ink-500", children: helperText })), isOpen && (jsxRuntime.jsx("div", { className: "absolute z-50 mt-1 w-full bg-white rounded-md shadow-lg border border-paper-200 max-h-60 overflow-auto", role: "listbox", id: listboxId, "aria-label": "Available options", children: loading ? (jsxRuntime.jsx("div", { className: "px-4 py-8 text-center text-ink-500 text-sm", role: "status", "aria-live": "polite", children: "Loading..." })) : filteredOptions.length === 0 && !canCreateOption ? (jsxRuntime.jsx("div", { className: "px-4 py-8 text-center text-ink-500 text-sm", role: "status", "aria-live": "polite", children: "No options found" })) : (jsxRuntime.jsxs("ul", { ref: listRef, children: [filteredOptions.map((option, index) => {
2671
2673
  const Icon = option.icon;
2672
2674
  const isSelected = option.value === value;
2673
2675
  const isHighlighted = index === highlightedIndex;
@@ -4606,6 +4608,8 @@ function BottomSheetActions({ children, className = '' }) {
4606
4608
  return (jsxRuntime.jsx("div", { className: `flex flex-col gap-2 px-4 py-4 border-t border-paper-200 bg-white ${className}`, children: children }));
4607
4609
  }
4608
4610
 
4611
+ // Selector for all focusable elements
4612
+ const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
4609
4613
  const sizeClasses$9 = {
4610
4614
  sm: 'max-w-md',
4611
4615
  md: 'max-w-lg',
@@ -4683,29 +4687,78 @@ const sizeClasses$9 = {
4683
4687
  function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', scrollable = false, maxHeight, mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
4684
4688
  const modalRef = React.useRef(null);
4685
4689
  const mouseDownOnBackdrop = React.useRef(false);
4690
+ const previousActiveElement = React.useRef(null);
4686
4691
  const titleId = React.useId();
4687
4692
  const isMobile = useIsMobile();
4693
+ // Get all focusable elements within the modal
4694
+ const getFocusableElements = React.useCallback(() => {
4695
+ if (!modalRef.current)
4696
+ return [];
4697
+ return Array.from(modalRef.current.querySelectorAll(FOCUSABLE_SELECTOR))
4698
+ .filter(el => el.offsetParent !== null); // Filter out hidden elements
4699
+ }, []);
4688
4700
  // Determine if we should use BottomSheet
4689
4701
  const useBottomSheet = mobileMode === 'sheet' ||
4690
4702
  (mobileMode === 'auto' && isMobile);
4691
- // Handle escape key (only for modal mode, BottomSheet handles its own)
4703
+ // Handle escape key and focus trap (only for modal mode, BottomSheet handles its own)
4692
4704
  React.useEffect(() => {
4693
4705
  if (useBottomSheet)
4694
- return; // BottomSheet handles escape
4695
- const handleEscape = (e) => {
4706
+ return; // BottomSheet handles its own focus
4707
+ const handleKeyDown = (e) => {
4696
4708
  if (e.key === 'Escape' && isOpen) {
4697
4709
  onClose();
4710
+ return;
4711
+ }
4712
+ // Focus trap: keep focus within modal
4713
+ if (e.key === 'Tab' && isOpen) {
4714
+ const focusableElements = getFocusableElements();
4715
+ if (focusableElements.length === 0)
4716
+ return;
4717
+ const firstElement = focusableElements[0];
4718
+ const lastElement = focusableElements[focusableElements.length - 1];
4719
+ if (e.shiftKey) {
4720
+ // Shift+Tab: if on first element, wrap to last
4721
+ if (document.activeElement === firstElement) {
4722
+ e.preventDefault();
4723
+ lastElement.focus();
4724
+ }
4725
+ }
4726
+ else {
4727
+ // Tab: if on last element, wrap to first
4728
+ if (document.activeElement === lastElement) {
4729
+ e.preventDefault();
4730
+ firstElement.focus();
4731
+ }
4732
+ }
4698
4733
  }
4699
4734
  };
4700
4735
  if (isOpen) {
4701
- document.addEventListener('keydown', handleEscape);
4736
+ // Store the currently focused element to restore later
4737
+ previousActiveElement.current = document.activeElement;
4738
+ document.addEventListener('keydown', handleKeyDown);
4702
4739
  document.body.style.overflow = 'hidden';
4740
+ // Set initial focus to first focusable element
4741
+ // Use requestAnimationFrame to ensure the modal is rendered
4742
+ requestAnimationFrame(() => {
4743
+ const focusableElements = getFocusableElements();
4744
+ if (focusableElements.length > 0) {
4745
+ focusableElements[0].focus();
4746
+ }
4747
+ else if (modalRef.current) {
4748
+ // If no focusable elements, focus the modal container
4749
+ modalRef.current.focus();
4750
+ }
4751
+ });
4703
4752
  }
4704
4753
  return () => {
4705
- document.removeEventListener('keydown', handleEscape);
4754
+ document.removeEventListener('keydown', handleKeyDown);
4706
4755
  document.body.style.overflow = 'unset';
4756
+ // Restore focus to the previously focused element
4757
+ if (previousActiveElement.current && typeof previousActiveElement.current.focus === 'function') {
4758
+ previousActiveElement.current.focus();
4759
+ }
4707
4760
  };
4708
- }, [isOpen, onClose, useBottomSheet]);
4761
+ }, [isOpen, onClose, useBottomSheet, getFocusableElements]);
4709
4762
  // Track if mousedown originated on the backdrop
4710
4763
  const handleBackdropMouseDown = (e) => {
4711
4764
  if (e.target === e.currentTarget) {
@@ -4746,7 +4799,7 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
4746
4799
  return reactDom.createPortal(jsxRuntime.jsx(BottomSheet, { isOpen: isOpen, onClose: onClose, title: title, height: mobileHeight, showHandle: mobileShowHandle, showCloseButton: showCloseButton, children: children }), document.body);
4747
4800
  }
4748
4801
  // Render as standard modal on desktop
4749
- const modalContent = (jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onMouseDown: handleBackdropMouseDown, onClick: handleBackdropClick, children: jsxRuntime.jsxs("div", { ref: modalRef, className: `${sizeClasses$9[size]} w-full bg-white bg-subtle-grain rounded-xl shadow-2xl border border-paper-200 ${getAnimationClass()}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsxRuntime.jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsxRuntime.jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) }))] }), jsxRuntime.jsx("div", { className: `px-6 py-4 ${scrollable || maxHeight ? 'overflow-y-auto' : ''}`, style: {
4802
+ const modalContent = (jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onMouseDown: handleBackdropMouseDown, onClick: handleBackdropClick, children: jsxRuntime.jsxs("div", { ref: modalRef, className: `${sizeClasses$9[size]} w-full bg-white bg-subtle-grain rounded-xl shadow-2xl border border-paper-200 ${getAnimationClass()}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, tabIndex: -1, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsxRuntime.jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsxRuntime.jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) }))] }), jsxRuntime.jsx("div", { className: `px-6 py-4 ${scrollable || maxHeight ? 'overflow-y-auto' : ''}`, style: {
4750
4803
  maxHeight: maxHeight || (scrollable ? 'calc(100vh - 200px)' : undefined),
4751
4804
  }, children: children })] }) }));
4752
4805
  return reactDom.createPortal(modalContent, document.body);
@@ -6040,6 +6093,68 @@ function HelpTooltip({ content, icon = 'help', size = 'md', position = 'top', cl
6040
6093
  return (jsxRuntime.jsx(Tooltip, { content: content, position: position, children: jsxRuntime.jsx("span", { className: `inline-flex items-center justify-center text-ink-400 hover:text-ink-600 cursor-help transition-colors ${className}`, role: "button", "aria-label": "Help", tabIndex: 0, children: jsxRuntime.jsx(IconComponent, { className: sizeClasses$7[size] }) }) }));
6041
6094
  }
6042
6095
 
6096
+ /**
6097
+ * SkipLink - Accessibility skip link for keyboard navigation
6098
+ *
6099
+ * A skip link allows keyboard users to bypass repetitive navigation
6100
+ * and jump directly to the main content. The link is visually hidden
6101
+ * until focused, making it invisible to mouse users while remaining
6102
+ * accessible to keyboard and screen reader users.
6103
+ *
6104
+ * Place this component at the very beginning of your page layout,
6105
+ * before any navigation elements.
6106
+ *
6107
+ * @example Basic usage
6108
+ * ```tsx
6109
+ * // In your layout component:
6110
+ * <SkipLink targetId="main-content" />
6111
+ * <Navigation />
6112
+ * <main id="main-content">
6113
+ * {children}
6114
+ * </main>
6115
+ * ```
6116
+ *
6117
+ * @example Custom text
6118
+ * ```tsx
6119
+ * <SkipLink targetId="content">Skip navigation</SkipLink>
6120
+ * ```
6121
+ *
6122
+ * @example Multiple skip links
6123
+ * ```tsx
6124
+ * <div>
6125
+ * <SkipLink targetId="main-content">Skip to main content</SkipLink>
6126
+ * <SkipLink targetId="search">Skip to search</SkipLink>
6127
+ * </div>
6128
+ * ```
6129
+ */
6130
+ function SkipLink({ targetId, children = 'Skip to main content', className = '', }) {
6131
+ const handleClick = (e) => {
6132
+ e.preventDefault();
6133
+ const target = document.getElementById(targetId);
6134
+ if (target) {
6135
+ // Set tabindex to make the element focusable if it isn't already
6136
+ if (!target.hasAttribute('tabindex')) {
6137
+ target.setAttribute('tabindex', '-1');
6138
+ }
6139
+ target.focus();
6140
+ // Scroll the element into view
6141
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
6142
+ }
6143
+ };
6144
+ return (jsxRuntime.jsx("a", { href: `#${targetId}`, onClick: handleClick, className: `
6145
+ sr-only focus:not-sr-only
6146
+ focus:absolute focus:z-50
6147
+ focus:top-4 focus:left-4
6148
+ focus:px-4 focus:py-2
6149
+ focus:bg-accent-600 focus:text-white
6150
+ focus:rounded-lg focus:shadow-lg
6151
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
6152
+ font-medium text-sm
6153
+ transition-none
6154
+ ${className}
6155
+ `, children: children }));
6156
+ }
6157
+
6043
6158
  function Popover({ trigger: triggerElement, children, placement = 'bottom', triggerMode = 'click', showArrow = true, offset = 8, open: controlledOpen, onOpenChange, closeOnClickOutside = true, closeOnEscape = true, showDelay = 0, hideDelay = 0, className = '', disabled = false, }) {
6044
6159
  const [internalOpen, setInternalOpen] = React.useState(false);
6045
6160
  const [position, setPosition] = React.useState({ top: 0, left: 0 });
@@ -7654,7 +7769,7 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
7654
7769
  ${error
7655
7770
  ? 'border-error-500 focus:ring-error-400 focus:border-error-400'
7656
7771
  : 'border-paper-300'}
7657
- `, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (option.isHeader ? (
7772
+ `, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-required": required, "aria-busy": loading }), clearable && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (option.isHeader ? (
7658
7773
  // Render section header (non-selectable)
7659
7774
  jsxRuntime.jsx("div", { className: "px-3 py-2 text-xs font-semibold text-ink-500 uppercase tracking-wide bg-paper-50 border-t border-paper-200 first:border-t-0 first:rounded-t-lg cursor-default", role: "presentation", children: option.label }, `header-${option.value}`)) : (
7660
7775
  // Render selectable option
@@ -14041,6 +14156,10 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14041
14156
  const tableContainerRef = React.useRef(null);
14042
14157
  // Row hover state (for coordinating primary + secondary row highlighting)
14043
14158
  const [hoveredRowKey, setHoveredRowKey] = React.useState(null);
14159
+ // Keyboard navigation state
14160
+ const [focusedCell, setFocusedCell] = React.useState(null);
14161
+ const [announcement, setAnnouncement] = React.useState('');
14162
+ const tableBodyRef = React.useRef(null);
14044
14163
  // Temporary row highlight state (for flash animation)
14045
14164
  const [flashingRows, setFlashingRows] = React.useState(new Set());
14046
14165
  const flashTimeoutRef = React.useRef(null);
@@ -14106,6 +14225,11 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14106
14225
  const currentDensity = densityClasses[density];
14107
14226
  // Key extractor function - defined early for use in other functions
14108
14227
  const getRowKey = keyExtractor || ((row) => String(row.id));
14228
+ // Calculate if there are any actions (for keyboard navigation column calculation)
14229
+ // This is computed early so it can be used in keyboard handlers
14230
+ const hasAnyActions = !!(onEdit || onDelete || actions.length > 0 ||
14231
+ expandedRowConfig?.edit || expandedRowConfig?.details ||
14232
+ expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length);
14109
14233
  // Get row background class based on striping and highlighting
14110
14234
  const getRowBackgroundClass = (item, index) => {
14111
14235
  const classes = [];
@@ -14407,6 +14531,192 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14407
14531
  const handleCollapseExpansion = () => {
14408
14532
  setExpansionState(null);
14409
14533
  };
14534
+ // Keyboard navigation handler
14535
+ const handleKeyboardNavigation = React.useCallback((e) => {
14536
+ if (!data.length)
14537
+ return;
14538
+ const totalRows = data.length;
14539
+ const totalCols = visibleColumns.length;
14540
+ // If no cell is focused, focus first data cell on first arrow key
14541
+ if (!focusedCell) {
14542
+ if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
14543
+ e.preventDefault();
14544
+ setFocusedCell({ row: 0, col: 0 });
14545
+ const colHeader = visibleColumns[0]?.header || 'first column';
14546
+ setAnnouncement(`Row 1, ${colHeader}`);
14547
+ return;
14548
+ }
14549
+ return;
14550
+ }
14551
+ const { row, col } = focusedCell;
14552
+ switch (e.key) {
14553
+ case 'ArrowDown':
14554
+ e.preventDefault();
14555
+ if (row < totalRows - 1) {
14556
+ const newRow = row + 1;
14557
+ setFocusedCell({ row: newRow, col });
14558
+ const rowItem = data[newRow];
14559
+ const colHeader = visibleColumns[col]?.header || '';
14560
+ const cellValue = rowItem[visibleColumns[col]?.key];
14561
+ setAnnouncement(`Row ${newRow + 1}, ${colHeader}: ${cellValue || 'empty'}`);
14562
+ }
14563
+ break;
14564
+ case 'ArrowUp':
14565
+ e.preventDefault();
14566
+ if (row > 0) {
14567
+ const newRow = row - 1;
14568
+ setFocusedCell({ row: newRow, col });
14569
+ const rowItem = data[newRow];
14570
+ const colHeader = visibleColumns[col]?.header || '';
14571
+ const cellValue = rowItem[visibleColumns[col]?.key];
14572
+ setAnnouncement(`Row ${newRow + 1}, ${colHeader}: ${cellValue || 'empty'}`);
14573
+ }
14574
+ break;
14575
+ case 'ArrowRight':
14576
+ e.preventDefault();
14577
+ if (col < totalCols - 1) {
14578
+ const newCol = col + 1;
14579
+ setFocusedCell({ row, col: newCol });
14580
+ const rowItem = data[row];
14581
+ const colHeader = visibleColumns[newCol]?.header || '';
14582
+ const cellValue = rowItem[visibleColumns[newCol]?.key];
14583
+ setAnnouncement(`${colHeader}: ${cellValue || 'empty'}`);
14584
+ }
14585
+ break;
14586
+ case 'ArrowLeft':
14587
+ e.preventDefault();
14588
+ if (col > 0) {
14589
+ const newCol = col - 1;
14590
+ setFocusedCell({ row, col: newCol });
14591
+ const rowItem = data[row];
14592
+ const colHeader = visibleColumns[newCol]?.header || '';
14593
+ const cellValue = rowItem[visibleColumns[newCol]?.key];
14594
+ setAnnouncement(`${colHeader}: ${cellValue || 'empty'}`);
14595
+ }
14596
+ break;
14597
+ case 'Home':
14598
+ e.preventDefault();
14599
+ if (e.ctrlKey) {
14600
+ // Ctrl+Home: Go to first cell
14601
+ setFocusedCell({ row: 0, col: 0 });
14602
+ setAnnouncement(`First cell, Row 1, ${visibleColumns[0]?.header || ''}`);
14603
+ }
14604
+ else {
14605
+ // Home: Go to first cell in current row
14606
+ setFocusedCell({ row, col: 0 });
14607
+ const rowItem = data[row];
14608
+ const cellValue = rowItem[visibleColumns[0]?.key];
14609
+ setAnnouncement(`${visibleColumns[0]?.header || ''}: ${cellValue || 'empty'}`);
14610
+ }
14611
+ break;
14612
+ case 'End':
14613
+ e.preventDefault();
14614
+ if (e.ctrlKey) {
14615
+ // Ctrl+End: Go to last cell
14616
+ const lastRow = totalRows - 1;
14617
+ const lastCol = totalCols - 1;
14618
+ setFocusedCell({ row: lastRow, col: lastCol });
14619
+ setAnnouncement(`Last cell, Row ${lastRow + 1}, ${visibleColumns[lastCol]?.header || ''}`);
14620
+ }
14621
+ else {
14622
+ // End: Go to last cell in current row
14623
+ const lastCol = totalCols - 1;
14624
+ setFocusedCell({ row, col: lastCol });
14625
+ const rowItem = data[row];
14626
+ const cellValue = rowItem[visibleColumns[lastCol]?.key];
14627
+ setAnnouncement(`${visibleColumns[lastCol]?.header || ''}: ${cellValue || 'empty'}`);
14628
+ }
14629
+ break;
14630
+ case 'Enter':
14631
+ e.preventDefault();
14632
+ {
14633
+ const rowItem = data[row];
14634
+ const rowKey = getRowKey(rowItem);
14635
+ // Priority: Edit mode > Details mode > Row double-click handler
14636
+ if (onEdit) {
14637
+ onEdit(rowItem);
14638
+ setAnnouncement('Opening edit mode');
14639
+ }
14640
+ else if (expandedRowConfig?.edit) {
14641
+ handleExpansionWithMode(rowKey, 'edit');
14642
+ setAnnouncement('Opening inline edit');
14643
+ }
14644
+ else if (expandedRowConfig?.details) {
14645
+ handleExpansionWithMode(rowKey, 'details');
14646
+ setAnnouncement('Opening details view');
14647
+ }
14648
+ else if (onRowDoubleClick) {
14649
+ onRowDoubleClick(rowItem);
14650
+ setAnnouncement('Activating row');
14651
+ }
14652
+ else if (onRowClick) {
14653
+ onRowClick(rowItem);
14654
+ setAnnouncement('Row selected');
14655
+ }
14656
+ }
14657
+ break;
14658
+ case ' ':
14659
+ // Space: Toggle selection if selectable
14660
+ if (selectable) {
14661
+ e.preventDefault();
14662
+ const rowItem = data[row];
14663
+ const rowKey = getRowKey(rowItem);
14664
+ handleRowSelect(rowKey);
14665
+ const isNowSelected = !selectedRowsSet.has(rowKey);
14666
+ setAnnouncement(isNowSelected ? 'Row selected' : 'Row deselected');
14667
+ }
14668
+ break;
14669
+ case 'Escape':
14670
+ e.preventDefault();
14671
+ setFocusedCell(null);
14672
+ setAnnouncement('Table navigation exited');
14673
+ // Return focus to table container
14674
+ tableBodyRef.current?.closest('table')?.focus();
14675
+ break;
14676
+ case 'PageDown':
14677
+ e.preventDefault();
14678
+ {
14679
+ const jumpSize = 10;
14680
+ const newRow = Math.min(row + jumpSize, totalRows - 1);
14681
+ setFocusedCell({ row: newRow, col });
14682
+ const colHeader = visibleColumns[col]?.header || '';
14683
+ setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
14684
+ }
14685
+ break;
14686
+ case 'PageUp':
14687
+ e.preventDefault();
14688
+ {
14689
+ const jumpSize = 10;
14690
+ const newRow = Math.max(row - jumpSize, 0);
14691
+ setFocusedCell({ row: newRow, col });
14692
+ const colHeader = visibleColumns[col]?.header || '';
14693
+ setAnnouncement(`Row ${newRow + 1} of ${totalRows}, ${colHeader}`);
14694
+ }
14695
+ break;
14696
+ }
14697
+ }, [data, visibleColumns, focusedCell, selectable, expandedRowConfig, onEdit, onRowDoubleClick, onRowClick, getRowKey, handleExpansionWithMode, handleRowSelect, selectedRowsSet]);
14698
+ // Focus the appropriate cell when focusedCell changes
14699
+ React.useEffect(() => {
14700
+ if (focusedCell && tableBodyRef.current) {
14701
+ const { row, col } = focusedCell;
14702
+ const rows = tableBodyRef.current.querySelectorAll('tr[data-row-index]');
14703
+ const targetRow = rows[row];
14704
+ if (targetRow) {
14705
+ // Calculate actual column index including extra columns
14706
+ const hasSelectionCol = selectable;
14707
+ const hasExpandCol = (expandable || expandedRowConfig) && showExpandChevron;
14708
+ const hasActionsCol = hasAnyActions;
14709
+ const extraColsBefore = (hasSelectionCol ? 1 : 0) + (hasExpandCol ? 1 : 0) + (hasActionsCol ? 1 : 0);
14710
+ const cells = targetRow.querySelectorAll('td');
14711
+ const targetCell = cells[col + extraColsBefore];
14712
+ if (targetCell) {
14713
+ targetCell.focus();
14714
+ // Scroll into view if needed
14715
+ targetCell.scrollIntoView({ block: 'nearest', inline: 'nearest' });
14716
+ }
14717
+ }
14718
+ }
14719
+ }, [focusedCell, selectable, expandable, expandedRowConfig, showExpandChevron, hasAnyActions]);
14410
14720
  // Handle column header click for sorting
14411
14721
  const handleSort = (column) => {
14412
14722
  if (!column.sortable || !onSortChange)
@@ -14491,7 +14801,9 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14491
14801
  // Hover state for row pair (primary + secondary)
14492
14802
  const isHovered = hoveredRowKey === rowKey;
14493
14803
  const hoverClass = disableHover ? '' : (isHovered ? 'bg-paper-100' : '');
14494
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsxs("tr", { className: `table-row-stable ${onRowDoubleClick || onRowClick || onEdit || expandedRowConfig?.edit || expandedRowConfig?.details || expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length ? 'cursor-pointer' : ''} ${isSelected ? 'bg-accent-50 border-l-2 border-accent-500' : hoverClass || rowBgClass} ${borderClass}`, onMouseEnter: () => !disableHover && setHoveredRowKey(rowKey), onMouseLeave: () => !disableHover && setHoveredRowKey(null), onClick: () => onRowClick?.(item), onContextMenu: (e) => {
14804
+ // Check if this row is keyboard-focused
14805
+ const isKeyboardFocused = focusedCell?.row === index;
14806
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsxs("tr", { "data-row-index": index, className: `table-row-stable ${onRowDoubleClick || onRowClick || onEdit || expandedRowConfig?.edit || expandedRowConfig?.details || expandedRowConfig?.addRelated?.length || expandedRowConfig?.manageRelated?.length ? 'cursor-pointer' : ''} ${isSelected ? 'bg-accent-50 border-l-2 border-accent-500' : hoverClass || rowBgClass} ${borderClass} ${isKeyboardFocused ? 'ring-2 ring-inset ring-accent-400' : ''}`, onMouseEnter: () => !disableHover && setHoveredRowKey(rowKey), onMouseLeave: () => !disableHover && setHoveredRowKey(null), onClick: () => onRowClick?.(item), onContextMenu: (e) => {
14495
14807
  if (enableContextMenu && allActions.length > 0) {
14496
14808
  e.preventDefault();
14497
14809
  e.stopPropagation();
@@ -14569,7 +14881,9 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14569
14881
  // Reduce left padding on first column when there are action buttons
14570
14882
  const isFirstColumn = colIdx === 0;
14571
14883
  const paddingClass = isFirstColumn && allActions.length > 0 ? 'pl-3' : '';
14572
- return (jsxRuntime.jsx("td", { className: `${currentDensity.cell} ${paddingClass} ${column.className || ''} ${bordered ? `border ${borderColor}` : ''}`, style: getColumnStyle(column, dynamicWidth), children: jsxRuntime.jsx("div", { className: `${currentDensity.text} leading-tight`, children: primaryContent }) }, `${item.id}-${columnKey}`));
14884
+ // Check if this cell is keyboard-focused
14885
+ const isCellFocused = focusedCell?.row === index && focusedCell?.col === colIdx;
14886
+ return (jsxRuntime.jsx("td", { className: `${currentDensity.cell} ${paddingClass} ${column.className || ''} ${bordered ? `border ${borderColor}` : ''} ${isCellFocused ? 'outline outline-2 outline-accent-500 outline-offset-[-2px]' : ''}`, style: getColumnStyle(column, dynamicWidth), tabIndex: isCellFocused ? 0 : -1, role: "gridcell", "aria-colindex": colIdx + 1, children: jsxRuntime.jsx("div", { className: `${currentDensity.text} leading-tight`, children: primaryContent }) }, `${item.id}-${columnKey}`));
14573
14887
  })] }), hasSecondaryRow && (jsxRuntime.jsx("tr", { className: `secondary-row ${isSelected ? 'bg-accent-50 border-l-2 border-accent-500' : hoverClass || rowBgClass} border-b ${borderColor}`, onMouseEnter: () => !disableHover && setHoveredRowKey(rowKey), onMouseLeave: () => !disableHover && setHoveredRowKey(null), children: visibleColumns.map((column, colIdx) => {
14574
14888
  const columnKey = String(column.key);
14575
14889
  const dynamicWidth = columnWidths[columnKey];
@@ -14638,7 +14952,7 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14638
14952
  })()] }, rowKey));
14639
14953
  });
14640
14954
  };
14641
- const tableContent = (jsxRuntime.jsxs("div", { className: `bg-white rounded-lg shadow border-2 ${borderColor} ${virtualized ? 'overflow-hidden' : 'overflow-x-auto overflow-y-visible'} ${className}`, style: { position: 'relative' }, children: [loading && data.length > 0 && (jsxRuntime.jsx("div", { className: "absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-20", style: { backdropFilter: 'blur(2px)' }, children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3", children: [jsxRuntime.jsx("div", { className: "loading-spinner", style: { width: '32px', height: '32px', borderWidth: '3px' } }), jsxRuntime.jsx("span", { className: "text-sm font-medium text-ink-600", children: "Loading..." })] }) })), jsxRuntime.jsxs("table", { className: `table-stable w-full ${bordered ? 'border-collapse' : ''}`, children: [jsxRuntime.jsxs("colgroup", { children: [selectable && jsxRuntime.jsx("col", { className: "w-12" }), ((expandable || expandedRowConfig) && showExpandChevron) && jsxRuntime.jsx("col", { className: "w-10" }), allActions.length > 0 && jsxRuntime.jsx("col", { style: { width: '28px' } }), visibleColumns.map((column, index) => {
14955
+ const tableContent = (jsxRuntime.jsxs("div", { className: `bg-white rounded-lg shadow border-2 ${borderColor} ${virtualized ? 'overflow-hidden' : 'overflow-x-auto overflow-y-visible'} ${className}`, style: { position: 'relative' }, children: [loading && data.length > 0 && (jsxRuntime.jsx("div", { className: "absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-20", style: { backdropFilter: 'blur(2px)' }, children: jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3", children: [jsxRuntime.jsx("div", { className: "loading-spinner", style: { width: '32px', height: '32px', borderWidth: '3px' } }), jsxRuntime.jsx("span", { className: "text-sm font-medium text-ink-600", children: "Loading..." })] }) })), jsxRuntime.jsxs("table", { className: `table-stable w-full ${bordered ? 'border-collapse' : ''}`, role: "grid", "aria-label": "Data table", "aria-rowcount": data.length, "aria-colcount": visibleColumns.length, children: [jsxRuntime.jsxs("colgroup", { children: [selectable && jsxRuntime.jsx("col", { className: "w-12" }), ((expandable || expandedRowConfig) && showExpandChevron) && jsxRuntime.jsx("col", { className: "w-10" }), allActions.length > 0 && jsxRuntime.jsx("col", { style: { width: '28px' } }), visibleColumns.map((column, index) => {
14642
14956
  const columnKey = String(column.key);
14643
14957
  const dynamicWidth = columnWidths[columnKey];
14644
14958
  return (jsxRuntime.jsx("col", { style: getColumnStyle(column, dynamicWidth) }, index));
@@ -14660,7 +14974,7 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14660
14974
  const currentWidth = thRef.current?.offsetWidth || 100;
14661
14975
  handleResizeStart(e, columnKey, currentWidth);
14662
14976
  }, children: jsxRuntime.jsx("div", { className: "absolute right-0 top-0 bottom-0 w-1 bg-paper-300 group-hover:bg-accent-400 transition-colors" }) }))] }, columnKey));
14663
- })] }) }), jsxRuntime.jsx("tbody", { className: "bg-white table-loading transition-opacity duration-200", children: loading && data.length === 0 ? (renderLoadingSkeleton()) : data.length === 0 ? (renderEmptyStateContent()) : (renderDataRows()) })] })] }));
14977
+ })] }) }), jsxRuntime.jsx("tbody", { ref: tableBodyRef, className: "bg-white table-loading transition-opacity duration-200", onKeyDown: handleKeyboardNavigation, tabIndex: 0, role: "rowgroup", "aria-label": "Table data", children: loading && data.length === 0 ? (renderLoadingSkeleton()) : data.length === 0 ? (renderEmptyStateContent()) : (renderDataRows()) })] })] }));
14664
14978
  // Wrap in scrollable container if virtualized
14665
14979
  const finalContent = virtualized ? (jsxRuntime.jsx("div", { ref: tableContainerRef, onScroll: handleScroll, style: { height: virtualHeight, overflow: 'auto' }, className: "rounded-lg", children: tableContent })) : tableContent;
14666
14980
  // Calculate pagination values
@@ -14694,7 +15008,7 @@ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
14694
15008
  }
14695
15009
  }, selectable: selectable, selectedRows: selectedRowsSet, onSelectionChange: onRowSelect ? (rows) => onRowSelect(rows) : undefined, keyExtractor: getRowKey, actions: allActions, onEdit: onEdit, onDelete: onDelete, className: className, cardClassName: cardClassName, gap: cardGap })) : null;
14696
15010
  // Render with context menu
14697
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderPaginationControls(), shouldShowCardView ? cardViewContent : finalContent, contextMenuState.isOpen && contextMenuState.item && (jsxRuntime.jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
15011
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: announcement }), renderPaginationControls(), shouldShowCardView ? cardViewContent : finalContent, contextMenuState.isOpen && contextMenuState.item && (jsxRuntime.jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
14698
15012
  }
14699
15013
 
14700
15014
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -61480,6 +61794,7 @@ exports.SidebarGroup = SidebarGroup;
61480
61794
  exports.Skeleton = Skeleton;
61481
61795
  exports.SkeletonCard = SkeletonCard$1;
61482
61796
  exports.SkeletonTable = SkeletonTable;
61797
+ exports.SkipLink = SkipLink;
61483
61798
  exports.Slider = Slider;
61484
61799
  exports.Spreadsheet = Spreadsheet;
61485
61800
  exports.SpreadsheetReport = SpreadsheetReport;