@octavius2929-personal/design-system 1.5.0 → 1.5.2

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
@@ -2089,40 +2089,122 @@ import {
2089
2089
  useRef as useRef4,
2090
2090
  useState as useState6
2091
2091
  } from "react";
2092
+ import { createPortal } from "react-dom";
2093
+
2094
+ // src/hooks/use-floating-position/index.ts
2095
+ import { useLayoutEffect } from "react";
2096
+
2097
+ // src/hooks/use-floating-position/compute-position.ts
2098
+ function computeFloatingPosition({
2099
+ anchorRect,
2100
+ overlaySize,
2101
+ viewport,
2102
+ placement = "bottom",
2103
+ align: align2 = "center",
2104
+ offset = 4,
2105
+ margin = 8
2106
+ }) {
2107
+ const spaceBelow = viewport.height - anchorRect.bottom;
2108
+ const spaceAbove = anchorRect.top;
2109
+ const needed = overlaySize.height + offset;
2110
+ const fitsBelow = spaceBelow >= needed;
2111
+ const fitsAbove = spaceAbove >= needed;
2112
+ let resolvedPlacement = placement;
2113
+ if (placement === "bottom" && !fitsBelow && fitsAbove) {
2114
+ resolvedPlacement = "top";
2115
+ } else if (placement === "top" && !fitsAbove && fitsBelow) {
2116
+ resolvedPlacement = "bottom";
2117
+ }
2118
+ const top = resolvedPlacement === "bottom" ? anchorRect.bottom + offset : anchorRect.top - offset - overlaySize.height;
2119
+ let left;
2120
+ if (align2 === "start") {
2121
+ left = anchorRect.left;
2122
+ } else if (align2 === "end") {
2123
+ left = anchorRect.right - overlaySize.width;
2124
+ } else {
2125
+ left = anchorRect.left + anchorRect.width / 2 - overlaySize.width / 2;
2126
+ }
2127
+ const minLeft = margin;
2128
+ const maxLeft = viewport.width - overlaySize.width - margin;
2129
+ left = maxLeft < minLeft ? minLeft : Math.min(Math.max(left, minLeft), maxLeft);
2130
+ return { placement: resolvedPlacement, top, left };
2131
+ }
2132
+
2133
+ // src/hooks/use-floating-position/index.ts
2134
+ function useFloatingPosition(anchorRef, overlayRef, { open, placement, align: align2, offset, margin }) {
2135
+ useLayoutEffect(() => {
2136
+ if (!open) return;
2137
+ const recalc = () => {
2138
+ const anchor = anchorRef.current;
2139
+ const overlay2 = overlayRef.current;
2140
+ if (!anchor || !overlay2) return;
2141
+ const next = computeFloatingPosition({
2142
+ anchorRect: anchor.getBoundingClientRect(),
2143
+ overlaySize: { width: overlay2.offsetWidth, height: overlay2.offsetHeight },
2144
+ viewport: { width: document.documentElement.clientWidth, height: window.innerHeight },
2145
+ placement,
2146
+ align: align2,
2147
+ offset,
2148
+ margin
2149
+ });
2150
+ overlay2.style.top = `${next.top}px`;
2151
+ overlay2.style.left = `${next.left}px`;
2152
+ overlay2.style.visibility = "visible";
2153
+ };
2154
+ recalc();
2155
+ window.addEventListener("scroll", recalc, true);
2156
+ window.addEventListener("resize", recalc);
2157
+ return () => {
2158
+ window.removeEventListener("scroll", recalc, true);
2159
+ window.removeEventListener("resize", recalc);
2160
+ };
2161
+ }, [open, placement, align2, offset, margin, anchorRef, overlayRef]);
2162
+ }
2092
2163
 
2093
2164
  // src/components/tooltip/use-styles.ts
2094
2165
  import { useMemo as useMemo23 } from "react";
2095
2166
 
2096
2167
  // src/components/tooltip/use-styles.css.ts
2097
2168
  var bubble = "use-styles_bubble__h9kvh1 surfaces_inkySurface__1qa7atn2";
2098
- var placement = { top: "use-styles_placement_top__h9kvh2", bottom: "use-styles_placement_bottom__h9kvh3" };
2099
2169
  var wrapper = "use-styles_wrapper__h9kvh0";
2100
2170
 
2101
2171
  // src/components/tooltip/use-styles.ts
2102
- function useStyles19({
2103
- placement: placement2 = "top"
2104
- }) {
2172
+ function useStyles19() {
2105
2173
  const { themeClass } = useTheme();
2106
2174
  const wrapper4 = useMemo23(
2107
2175
  () => [themeClass, wrapper].filter(Boolean).join(" "),
2108
2176
  [themeClass]
2109
2177
  );
2110
- const bubble2 = useMemo23(
2111
- () => [bubble, placement[placement2]].filter(Boolean).join(" "),
2112
- [placement2]
2113
- );
2178
+ const bubble2 = useMemo23(() => [themeClass, bubble].filter(Boolean).join(" "), [themeClass]);
2114
2179
  return { wrapper: wrapper4, bubble: bubble2 };
2115
2180
  }
2116
2181
 
2117
2182
  // src/components/tooltip/index.tsx
2118
2183
  import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
2119
2184
  var HIDE_DELAY = 120;
2120
- var Tooltip = forwardRef21(function Tooltip2({ label: label8, children, placement: placement2, testId }, ref) {
2185
+ var GAP = 4;
2186
+ function assignRef(ref, value) {
2187
+ if (typeof ref === "function") ref(value);
2188
+ else if (ref) ref.current = value;
2189
+ }
2190
+ var Tooltip = forwardRef21(function Tooltip2({ label: label8, children, placement = "top", testId }, ref) {
2121
2191
  const [open, setOpen] = useState6(false);
2122
2192
  const tooltipId = useId3();
2123
- const { wrapper: wrapper4, bubble: bubble2 } = useStyles19({ placement: placement2 });
2193
+ const { wrapper: wrapper4, bubble: bubble2 } = useStyles19();
2124
2194
  const { testId: dataTestId, slot } = useTestId("feedback", testId);
2125
2195
  const hideTimer = useRef4(null);
2196
+ const wrapperRef = useRef4(null);
2197
+ const setWrapperRef = (node) => {
2198
+ wrapperRef.current = node;
2199
+ assignRef(ref, node);
2200
+ };
2201
+ const bubbleRef = useRef4(null);
2202
+ useFloatingPosition(wrapperRef, bubbleRef, {
2203
+ open,
2204
+ placement,
2205
+ align: "center",
2206
+ offset: GAP
2207
+ });
2126
2208
  const clearHide = () => {
2127
2209
  if (hideTimer.current) {
2128
2210
  clearTimeout(hideTimer.current);
@@ -2158,7 +2240,7 @@ var Tooltip = forwardRef21(function Tooltip2({ label: label8, children, placemen
2158
2240
  return /* @__PURE__ */ jsxs19(
2159
2241
  "span",
2160
2242
  {
2161
- ref,
2243
+ ref: setWrapperRef,
2162
2244
  className: wrapper4,
2163
2245
  "data-testid": dataTestId,
2164
2246
  onMouseEnter: show,
@@ -2168,18 +2250,22 @@ var Tooltip = forwardRef21(function Tooltip2({ label: label8, children, placemen
2168
2250
  onKeyDown: handleKeyDown,
2169
2251
  children: [
2170
2252
  trigger2,
2171
- open && /* @__PURE__ */ jsx31(
2172
- "span",
2173
- {
2174
- id: tooltipId,
2175
- role: "tooltip",
2176
- className: bubble2,
2177
- "data-testid": slot("bubble"),
2178
- onMouseEnter: show,
2179
- onMouseLeave: scheduleHide,
2180
- children: label8
2181
- }
2182
- )
2253
+ open && typeof document !== "undefined" ? createPortal(
2254
+ /* @__PURE__ */ jsx31(
2255
+ "span",
2256
+ {
2257
+ ref: bubbleRef,
2258
+ id: tooltipId,
2259
+ role: "tooltip",
2260
+ className: bubble2,
2261
+ "data-testid": slot("bubble"),
2262
+ onMouseEnter: show,
2263
+ onMouseLeave: scheduleHide,
2264
+ children: label8
2265
+ }
2266
+ ),
2267
+ document.body
2268
+ ) : null
2183
2269
  ]
2184
2270
  }
2185
2271
  );
@@ -2897,11 +2983,12 @@ var Tabs = forwardRef28(function Tabs2({ items, value, onChange, testId, ...rest
2897
2983
  import {
2898
2984
  cloneElement as cloneElement2,
2899
2985
  forwardRef as forwardRef29,
2986
+ useCallback as useCallback2,
2900
2987
  useEffect as useEffect6,
2901
- useLayoutEffect,
2902
2988
  useRef as useRef7,
2903
2989
  useState as useState9
2904
2990
  } from "react";
2991
+ import { createPortal as createPortal2 } from "react-dom";
2905
2992
 
2906
2993
  // src/components/menu/use-styles.ts
2907
2994
  import { useMemo as useMemo31 } from "react";
@@ -2918,7 +3005,7 @@ function useStyles27() {
2918
3005
  return useMemo31(
2919
3006
  () => ({
2920
3007
  wrapper: [themeClass, wrapper3].filter(Boolean).join(" "),
2921
- list: list2,
3008
+ list: [themeClass, list2].filter(Boolean).join(" "),
2922
3009
  item: item2,
2923
3010
  dangerItem: [item2, danger].join(" ")
2924
3011
  }),
@@ -2929,7 +3016,9 @@ function useStyles27() {
2929
3016
  // src/components/menu/index.tsx
2930
3017
  import { jsx as jsx42, jsxs as jsxs27 } from "react/jsx-runtime";
2931
3018
  var ICON_SIZE4 = 16;
2932
- function assignRef(ref, value) {
3019
+ var GAP2 = 4;
3020
+ var TYPEAHEAD_RESET_MS = 500;
3021
+ function assignRef2(ref, value) {
2933
3022
  if (typeof ref === "function") ref(value);
2934
3023
  else if (ref) ref.current = value;
2935
3024
  }
@@ -2937,15 +3026,28 @@ var Menu = forwardRef29(function Menu2({ trigger: trigger2, items, testId }, ref
2937
3026
  const { wrapper: wrapper4, list: list3, item: item3, dangerItem } = useStyles27();
2938
3027
  const { testId: rootTestId, slot } = useTestId("menu", testId);
2939
3028
  const [open, setOpen] = useState9(false);
2940
- const [alignEnd, setAlignEnd] = useState9(false);
2941
3029
  const rootRef = useRef7(null);
2942
3030
  const setRootRef = (node) => {
2943
3031
  rootRef.current = node;
2944
- assignRef(ref, node);
3032
+ assignRef2(ref, node);
2945
3033
  };
2946
3034
  const listRef = useRef7(null);
2947
3035
  const triggerRef = useRef7(null);
2948
- const getMenuItems = () => Array.from(listRef.current?.querySelectorAll('[role="menuitem"]') ?? []);
3036
+ const pendingFocusRef = useRef7("first");
3037
+ const typeAheadRef = useRef7({
3038
+ query: "",
3039
+ timeout: null
3040
+ });
3041
+ useFloatingPosition(rootRef, listRef, {
3042
+ open,
3043
+ placement: "bottom",
3044
+ align: "start",
3045
+ offset: GAP2
3046
+ });
3047
+ const getMenuItems = useCallback2(
3048
+ () => Array.from(listRef.current?.querySelectorAll('[role="menuitem"]') ?? []),
3049
+ []
3050
+ );
2949
3051
  const focusItemAt = (index) => {
2950
3052
  const menuItems = getMenuItems();
2951
3053
  if (menuItems.length === 0) return;
@@ -2956,23 +3058,41 @@ var Menu = forwardRef29(function Menu2({ trigger: trigger2, items, testId }, ref
2956
3058
  setOpen(false);
2957
3059
  triggerRef.current?.focus();
2958
3060
  };
2959
- useLayoutEffect(() => {
2960
- if (!open) {
2961
- setAlignEnd(false);
2962
- return;
3061
+ const resetTypeAhead = useCallback2(() => {
3062
+ if (typeAheadRef.current.timeout) clearTimeout(typeAheadRef.current.timeout);
3063
+ typeAheadRef.current = { query: "", timeout: null };
3064
+ }, []);
3065
+ const handleTypeAhead = (key) => {
3066
+ if (typeAheadRef.current.timeout) clearTimeout(typeAheadRef.current.timeout);
3067
+ const query = typeAheadRef.current.query + key.toLowerCase();
3068
+ typeAheadRef.current.query = query;
3069
+ typeAheadRef.current.timeout = setTimeout(() => {
3070
+ typeAheadRef.current = { query: "", timeout: null };
3071
+ }, TYPEAHEAD_RESET_MS);
3072
+ const menuItems = getMenuItems();
3073
+ if (menuItems.length === 0) return;
3074
+ const current2 = menuItems.indexOf(document.activeElement);
3075
+ const startIndex = current2 === -1 ? 0 : current2 + 1;
3076
+ for (let i = 0; i < menuItems.length; i++) {
3077
+ const candidate = menuItems[(startIndex + i) % menuItems.length];
3078
+ const text2 = candidate?.textContent?.trim().toLowerCase() ?? "";
3079
+ if (text2.startsWith(query)) {
3080
+ candidate?.focus();
3081
+ return;
3082
+ }
2963
3083
  }
2964
- const listEl = listRef.current;
2965
- const rootEl = rootRef.current;
2966
- if (!listEl || !rootEl) return;
2967
- const rootLeft = rootEl.getBoundingClientRect().left;
2968
- const viewport = document.documentElement.clientWidth;
2969
- setAlignEnd(rootLeft + listEl.offsetWidth > viewport);
2970
- }, [open]);
3084
+ };
2971
3085
  useEffect6(() => {
2972
3086
  if (!open) return;
2973
- listRef.current?.querySelector('[role="menuitem"]')?.focus();
3087
+ const menuItems = getMenuItems();
3088
+ const index = pendingFocusRef.current === "last" ? menuItems.length - 1 : 0;
3089
+ menuItems[index]?.focus();
3090
+ pendingFocusRef.current = "first";
2974
3091
  const onDocMouseDown = (event) => {
2975
- if (rootRef.current && !rootRef.current.contains(event.target)) {
3092
+ const target = event.target;
3093
+ const insideTrigger = rootRef.current?.contains(target) ?? false;
3094
+ const insideList = listRef.current?.contains(target) ?? false;
3095
+ if (!insideTrigger && !insideList) {
2976
3096
  setOpen(false);
2977
3097
  }
2978
3098
  };
@@ -2987,8 +3107,9 @@ var Menu = forwardRef29(function Menu2({ trigger: trigger2, items, testId }, ref
2987
3107
  return () => {
2988
3108
  document.removeEventListener("mousedown", onDocMouseDown);
2989
3109
  document.removeEventListener("keydown", onKeyDown);
3110
+ resetTypeAhead();
2990
3111
  };
2991
- }, [open]);
3112
+ }, [open, getMenuItems, resetTypeAhead]);
2992
3113
  const onMenuKeyDown = (event) => {
2993
3114
  const menuItems = getMenuItems();
2994
3115
  const current2 = menuItems.indexOf(document.activeElement);
@@ -3016,13 +3137,45 @@ var Menu = forwardRef29(function Menu2({ trigger: trigger2, items, testId }, ref
3016
3137
  case "Tab":
3017
3138
  setOpen(false);
3018
3139
  break;
3140
+ case " ":
3141
+ event.preventDefault();
3142
+ if (typeAheadRef.current.query.length > 0) {
3143
+ handleTypeAhead(event.key);
3144
+ } else {
3145
+ menuItems[current2]?.click();
3146
+ }
3147
+ break;
3148
+ default:
3149
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
3150
+ event.preventDefault();
3151
+ handleTypeAhead(event.key);
3152
+ }
3153
+ break;
3019
3154
  }
3020
3155
  };
3021
3156
  const triggerProps = trigger2.props;
3022
3157
  const consumerRef = trigger2.ref;
3023
3158
  const mergedTriggerRef = (node) => {
3024
3159
  triggerRef.current = node;
3025
- assignRef(consumerRef, node);
3160
+ assignRef2(consumerRef, node);
3161
+ };
3162
+ const handleTriggerKeyDown = (event) => {
3163
+ triggerProps.onKeyDown?.(event);
3164
+ if (open) return;
3165
+ switch (event.key) {
3166
+ case "Enter":
3167
+ case " ":
3168
+ case "ArrowDown":
3169
+ event.preventDefault();
3170
+ pendingFocusRef.current = "first";
3171
+ setOpen(true);
3172
+ break;
3173
+ case "ArrowUp":
3174
+ event.preventDefault();
3175
+ pendingFocusRef.current = "last";
3176
+ setOpen(true);
3177
+ break;
3178
+ }
3026
3179
  };
3027
3180
  const clonedTrigger = cloneElement2(trigger2, {
3028
3181
  ref: mergedTriggerRef,
@@ -3030,8 +3183,12 @@ var Menu = forwardRef29(function Menu2({ trigger: trigger2, items, testId }, ref
3030
3183
  "aria-expanded": open,
3031
3184
  onClick: (event) => {
3032
3185
  triggerProps.onClick?.(event);
3033
- setOpen((prev) => !prev);
3034
- }
3186
+ setOpen((prev) => {
3187
+ if (!prev) pendingFocusRef.current = "first";
3188
+ return !prev;
3189
+ });
3190
+ },
3191
+ onKeyDown: handleTriggerKeyDown
3035
3192
  });
3036
3193
  return /* @__PURE__ */ jsxs27(
3037
3194
  "div",
@@ -3042,39 +3199,41 @@ var Menu = forwardRef29(function Menu2({ trigger: trigger2, items, testId }, ref
3042
3199
  "data-state": open ? "open" : "closed",
3043
3200
  children: [
3044
3201
  clonedTrigger,
3045
- open && /* @__PURE__ */ jsx42(
3046
- "div",
3047
- {
3048
- ref: listRef,
3049
- role: "menu",
3050
- className: list3,
3051
- "data-align": alignEnd ? "end" : "start",
3052
- "data-testid": slot("list"),
3053
- onKeyDown: onMenuKeyDown,
3054
- children: items.map((entry, index) => {
3055
- const ItemIcon = entry.icon;
3056
- return /* @__PURE__ */ jsxs27(
3057
- "button",
3058
- {
3059
- type: "button",
3060
- role: "menuitem",
3061
- tabIndex: -1,
3062
- className: entry.danger ? dangerItem : item3,
3063
- "data-testid": slot(`item-${index}`),
3064
- onClick: () => {
3065
- entry.onClick?.();
3066
- setOpen(false);
3202
+ open && typeof document !== "undefined" ? createPortal2(
3203
+ /* @__PURE__ */ jsx42(
3204
+ "div",
3205
+ {
3206
+ ref: listRef,
3207
+ role: "menu",
3208
+ className: list3,
3209
+ "data-testid": slot("list"),
3210
+ onKeyDown: onMenuKeyDown,
3211
+ children: items.map((entry, index) => {
3212
+ const ItemIcon = entry.icon;
3213
+ return /* @__PURE__ */ jsxs27(
3214
+ "button",
3215
+ {
3216
+ type: "button",
3217
+ role: "menuitem",
3218
+ tabIndex: -1,
3219
+ className: entry.danger ? dangerItem : item3,
3220
+ "data-testid": slot(`item-${index}`),
3221
+ onClick: () => {
3222
+ entry.onClick?.();
3223
+ setOpen(false);
3224
+ },
3225
+ children: [
3226
+ ItemIcon ? /* @__PURE__ */ jsx42(ItemIcon, { size: ICON_SIZE4 }) : null,
3227
+ entry.label
3228
+ ]
3067
3229
  },
3068
- children: [
3069
- ItemIcon ? /* @__PURE__ */ jsx42(ItemIcon, { size: ICON_SIZE4 }) : null,
3070
- entry.label
3071
- ]
3072
- },
3073
- index
3074
- );
3075
- })
3076
- }
3077
- )
3230
+ index
3231
+ );
3232
+ })
3233
+ }
3234
+ ),
3235
+ document.body
3236
+ ) : null
3078
3237
  ]
3079
3238
  }
3080
3239
  );
@@ -3087,7 +3246,7 @@ import {
3087
3246
  useId as useId6,
3088
3247
  useRef as useRef8
3089
3248
  } from "react";
3090
- import { createPortal } from "react-dom";
3249
+ import { createPortal as createPortal3 } from "react-dom";
3091
3250
 
3092
3251
  // src/components/dialog/use-styles.ts
3093
3252
  import { useMemo as useMemo32 } from "react";
@@ -3115,7 +3274,7 @@ function useStyles28() {
3115
3274
  // src/components/dialog/index.tsx
3116
3275
  import { jsx as jsx43, jsxs as jsxs28 } from "react/jsx-runtime";
3117
3276
  var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3118
- function assignRef2(ref, value) {
3277
+ function assignRef3(ref, value) {
3119
3278
  if (typeof ref === "function") ref(value);
3120
3279
  else if (ref) ref.current = value;
3121
3280
  }
@@ -3125,7 +3284,7 @@ var Dialog = forwardRef30(function Dialog2({ open, onClose, title, actions: acti
3125
3284
  const surfaceRef = useRef8(null);
3126
3285
  const setSurfaceRef = (node) => {
3127
3286
  surfaceRef.current = node;
3128
- assignRef2(ref, node);
3287
+ assignRef3(ref, node);
3129
3288
  };
3130
3289
  const previouslyFocused = useRef8(null);
3131
3290
  const generatedId = useId6();
@@ -3180,7 +3339,7 @@ var Dialog = forwardRef30(function Dialog2({ open, onClose, title, actions: acti
3180
3339
  first.focus();
3181
3340
  }
3182
3341
  };
3183
- return createPortal(
3342
+ return createPortal3(
3184
3343
  // biome-ignore lint/a11y/useKeyWithClickEvents: ESC handled by a document keydown listener.
3185
3344
  /* @__PURE__ */ jsx43("div", { className: overlay2, "data-testid": slot("overlay"), onClick: onClose, children: /* @__PURE__ */ jsxs28(
3186
3345
  "div",
@@ -3208,11 +3367,11 @@ var Dialog = forwardRef30(function Dialog2({ open, onClose, title, actions: acti
3208
3367
  // src/components/snackbar/index.tsx
3209
3368
  import {
3210
3369
  forwardRef as forwardRef31,
3211
- useCallback as useCallback2,
3370
+ useCallback as useCallback3,
3212
3371
  useEffect as useEffect8,
3213
3372
  useRef as useRef9
3214
3373
  } from "react";
3215
- import { createPortal as createPortal2 } from "react-dom";
3374
+ import { createPortal as createPortal4 } from "react-dom";
3216
3375
 
3217
3376
  // src/components/snackbar/use-styles.ts
3218
3377
  import { useMemo as useMemo33 } from "react";
@@ -3259,13 +3418,13 @@ var Snackbar = forwardRef31(function Snackbar2({
3259
3418
  }, [onClose]);
3260
3419
  const interactingRef = useRef9({ hover: false, focus: false });
3261
3420
  const timeoutRef = useRef9();
3262
- const clearTimer = useCallback2(() => {
3421
+ const clearTimer = useCallback3(() => {
3263
3422
  if (timeoutRef.current !== void 0) {
3264
3423
  clearTimeout(timeoutRef.current);
3265
3424
  timeoutRef.current = void 0;
3266
3425
  }
3267
3426
  }, []);
3268
- const scheduleTimer = useCallback2(() => {
3427
+ const scheduleTimer = useCallback3(() => {
3269
3428
  clearTimer();
3270
3429
  if (!open || duration == null || !onCloseRef.current) return;
3271
3430
  if (interactingRef.current.hover || interactingRef.current.focus) return;
@@ -3298,7 +3457,7 @@ var Snackbar = forwardRef31(function Snackbar2({
3298
3457
  onBlur?.(event);
3299
3458
  };
3300
3459
  if (!open || typeof document === "undefined") return null;
3301
- return createPortal2(
3460
+ return createPortal4(
3302
3461
  /* @__PURE__ */ jsxs29(
3303
3462
  "div",
3304
3463
  {