@geomak/ui 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2364,9 +2364,105 @@ function Dropdown({
2364
2364
  ]
2365
2365
  }
2366
2366
  ),
2367
- hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-center text-status-error text-xs mt-1", children: errorMessage })
2367
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-status-error text-xs mt-1", children: errorMessage })
2368
2368
  ] });
2369
2369
  }
2370
+ var SHIMMER = [
2371
+ "relative overflow-hidden rounded-sm bg-surface-raised",
2372
+ 'before:absolute before:inset-0 before:content-[""]',
2373
+ "before:bg-gradient-to-r before:from-transparent before:via-white/30 before:to-transparent",
2374
+ "before:animate-[shimmer_1.6s_linear_infinite]",
2375
+ // Respect prefers-reduced-motion — the resting bg-surface-raised is still
2376
+ // a perfectly legible placeholder for users who have animations off.
2377
+ "motion-reduce:before:hidden"
2378
+ ].join(" ");
2379
+ function SkeletonBox({ width, height = 16, radius, className = "", style }) {
2380
+ return /* @__PURE__ */ jsx(
2381
+ "span",
2382
+ {
2383
+ role: "presentation",
2384
+ "aria-hidden": "true",
2385
+ className: `block ${SHIMMER} ${className}`,
2386
+ style: {
2387
+ width: width ?? "100%",
2388
+ height,
2389
+ borderRadius: radius ?? "var(--radius-md)",
2390
+ ...style
2391
+ }
2392
+ }
2393
+ );
2394
+ }
2395
+ function SkeletonText({
2396
+ lines = 3,
2397
+ lastLineWidth = 60,
2398
+ lineHeight = 14,
2399
+ gap = 8,
2400
+ className = "",
2401
+ style
2402
+ }) {
2403
+ return /* @__PURE__ */ jsx(
2404
+ "div",
2405
+ {
2406
+ role: "presentation",
2407
+ "aria-hidden": "true",
2408
+ className: `flex flex-col ${className}`,
2409
+ style: { gap, ...style },
2410
+ children: Array.from({ length: lines }).map((_, i) => {
2411
+ const isLast = i === lines - 1;
2412
+ const width = isLast && lines > 1 ? `${lastLineWidth}%` : "100%";
2413
+ return /* @__PURE__ */ jsx(
2414
+ "span",
2415
+ {
2416
+ className: `block ${SHIMMER}`,
2417
+ style: { height: lineHeight, width, borderRadius: "var(--radius-sm)" }
2418
+ },
2419
+ i
2420
+ );
2421
+ })
2422
+ }
2423
+ );
2424
+ }
2425
+ function SkeletonCircle({ size = 40, className = "", style }) {
2426
+ return /* @__PURE__ */ jsx(
2427
+ "span",
2428
+ {
2429
+ role: "presentation",
2430
+ "aria-hidden": "true",
2431
+ className: `block flex-shrink-0 ${SHIMMER} ${className}`,
2432
+ style: {
2433
+ width: size,
2434
+ height: size,
2435
+ borderRadius: "50%",
2436
+ ...style
2437
+ }
2438
+ }
2439
+ );
2440
+ }
2441
+ function SkeletonCard({ hasAvatar = true, lines = 3, className = "", style }) {
2442
+ return /* @__PURE__ */ jsxs(
2443
+ "div",
2444
+ {
2445
+ role: "presentation",
2446
+ "aria-hidden": "true",
2447
+ className: `rounded-lg border border-border bg-surface p-4 ${className}`,
2448
+ style,
2449
+ children: [
2450
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
2451
+ hasAvatar && /* @__PURE__ */ jsx(SkeletonCircle, { size: 36 }),
2452
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col gap-2", children: [
2453
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 12, width: "55%" }),
2454
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 10, width: "35%" })
2455
+ ] })
2456
+ ] }),
2457
+ /* @__PURE__ */ jsx(SkeletonText, { lines, lastLineWidth: 55 }),
2458
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-2", children: [
2459
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 72 }),
2460
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 56 })
2461
+ ] })
2462
+ ]
2463
+ }
2464
+ );
2465
+ }
2370
2466
  var DEFAULT_PICKER = [
2371
2467
  { key: 1, value: 5, label: 5 },
2372
2468
  { key: 2, value: 10, label: 10 },
@@ -2552,7 +2648,9 @@ function Table({
2552
2648
  expandRow = DEFAULT_EXPAND,
2553
2649
  hasSearch = true,
2554
2650
  footer = null,
2555
- header = null
2651
+ header = null,
2652
+ loading = false,
2653
+ loadingRowCount = 8
2556
2654
  }) {
2557
2655
  const searchRef = useRef(null);
2558
2656
  const [searchTerm, setSearchTerm] = useState("");
@@ -2636,9 +2734,16 @@ function Table({
2636
2734
  )
2637
2735
  ] }),
2638
2736
  /* @__PURE__ */ jsx("div", { children: header }),
2639
- /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-lg", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse", children: [
2737
+ /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-lg", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse", "aria-busy": loading || void 0, children: [
2640
2738
  /* @__PURE__ */ jsx(TableHeader, { columns, hasExpand: !!expandRow.enabled }),
2641
- /* @__PURE__ */ jsx(
2739
+ loading ? /* @__PURE__ */ jsx(
2740
+ TableSkeletonBody,
2741
+ {
2742
+ columns,
2743
+ rowCount: loadingRowCount,
2744
+ hasExpand: !!expandRow.enabled
2745
+ }
2746
+ ) : /* @__PURE__ */ jsx(
2642
2747
  TableBody,
2643
2748
  {
2644
2749
  columns,
@@ -2651,6 +2756,23 @@ function Table({
2651
2756
  /* @__PURE__ */ jsx("div", { children: footer })
2652
2757
  ] });
2653
2758
  }
2759
+ function TableSkeletonBody({
2760
+ columns,
2761
+ rowCount,
2762
+ hasExpand
2763
+ }) {
2764
+ return /* @__PURE__ */ jsx("tbody", { "aria-hidden": "true", children: Array.from({ length: rowCount }).map((_, i) => /* @__PURE__ */ jsxs(
2765
+ "tr",
2766
+ {
2767
+ className: `border-b border-border ${i % 2 === 0 ? "bg-surface" : "bg-surface-raised"}`,
2768
+ children: [
2769
+ hasExpand && /* @__PURE__ */ jsx("td", { className: "p-0 align-middle w-9" }),
2770
+ columns.map((col) => /* @__PURE__ */ jsx("td", { className: "py-3 px-3 align-middle", children: /* @__PURE__ */ jsx(SkeletonBox, { height: 12, width: `${50 + i % 4 * 12}%` }) }, col.key))
2771
+ ]
2772
+ },
2773
+ i
2774
+ )) });
2775
+ }
2654
2776
  function ThemeSwitch({ checked, onChange, label = "Toggle dark mode" }) {
2655
2777
  const id = useId();
2656
2778
  return /* @__PURE__ */ jsx("label", { htmlFor: id, className: "flex items-center gap-2 cursor-pointer select-none", children: /* @__PURE__ */ jsx(
@@ -3083,102 +3205,6 @@ function ThemeProvider({
3083
3205
  )
3084
3206
  ] });
3085
3207
  }
3086
- var SHIMMER = [
3087
- "relative overflow-hidden rounded-sm bg-surface-raised",
3088
- 'before:absolute before:inset-0 before:content-[""]',
3089
- "before:bg-gradient-to-r before:from-transparent before:via-white/30 before:to-transparent",
3090
- "before:animate-[shimmer_1.6s_linear_infinite]",
3091
- // Respect prefers-reduced-motion — the resting bg-surface-raised is still
3092
- // a perfectly legible placeholder for users who have animations off.
3093
- "motion-reduce:before:hidden"
3094
- ].join(" ");
3095
- function SkeletonBox({ width, height = 16, radius, className = "", style }) {
3096
- return /* @__PURE__ */ jsx(
3097
- "span",
3098
- {
3099
- role: "presentation",
3100
- "aria-hidden": "true",
3101
- className: `block ${SHIMMER} ${className}`,
3102
- style: {
3103
- width: width ?? "100%",
3104
- height,
3105
- borderRadius: radius ?? "var(--radius-md)",
3106
- ...style
3107
- }
3108
- }
3109
- );
3110
- }
3111
- function SkeletonText({
3112
- lines = 3,
3113
- lastLineWidth = 60,
3114
- lineHeight = 14,
3115
- gap = 8,
3116
- className = "",
3117
- style
3118
- }) {
3119
- return /* @__PURE__ */ jsx(
3120
- "div",
3121
- {
3122
- role: "presentation",
3123
- "aria-hidden": "true",
3124
- className: `flex flex-col ${className}`,
3125
- style: { gap, ...style },
3126
- children: Array.from({ length: lines }).map((_, i) => {
3127
- const isLast = i === lines - 1;
3128
- const width = isLast && lines > 1 ? `${lastLineWidth}%` : "100%";
3129
- return /* @__PURE__ */ jsx(
3130
- "span",
3131
- {
3132
- className: `block ${SHIMMER}`,
3133
- style: { height: lineHeight, width, borderRadius: "var(--radius-sm)" }
3134
- },
3135
- i
3136
- );
3137
- })
3138
- }
3139
- );
3140
- }
3141
- function SkeletonCircle({ size = 40, className = "", style }) {
3142
- return /* @__PURE__ */ jsx(
3143
- "span",
3144
- {
3145
- role: "presentation",
3146
- "aria-hidden": "true",
3147
- className: `block flex-shrink-0 ${SHIMMER} ${className}`,
3148
- style: {
3149
- width: size,
3150
- height: size,
3151
- borderRadius: "50%",
3152
- ...style
3153
- }
3154
- }
3155
- );
3156
- }
3157
- function SkeletonCard({ hasAvatar = true, lines = 3, className = "", style }) {
3158
- return /* @__PURE__ */ jsxs(
3159
- "div",
3160
- {
3161
- role: "presentation",
3162
- "aria-hidden": "true",
3163
- className: `rounded-lg border border-border bg-surface p-4 ${className}`,
3164
- style,
3165
- children: [
3166
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
3167
- hasAvatar && /* @__PURE__ */ jsx(SkeletonCircle, { size: 36 }),
3168
- /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col gap-2", children: [
3169
- /* @__PURE__ */ jsx(SkeletonBox, { height: 12, width: "55%" }),
3170
- /* @__PURE__ */ jsx(SkeletonBox, { height: 10, width: "35%" })
3171
- ] })
3172
- ] }),
3173
- /* @__PURE__ */ jsx(SkeletonText, { lines, lastLineWidth: 55 }),
3174
- /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-2", children: [
3175
- /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 72 }),
3176
- /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 56 })
3177
- ] })
3178
- ]
3179
- }
3180
- );
3181
- }
3182
3208
  function TextInput({
3183
3209
  value,
3184
3210
  onChange,
@@ -3196,48 +3222,50 @@ function TextInput({
3196
3222
  }) {
3197
3223
  const errorId = useId();
3198
3224
  const hasError = errorMessage != null;
3199
- return /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center justify-center", children: [
3225
+ return (
3226
+ // In horizontal mode the row layout is [label, input-with-error-column].
3227
+ // The error sits under the input ONLY, not spanning the label too.
3228
+ // In vertical mode the whole thing is a column.
3200
3229
  /* @__PURE__ */ jsxs(
3201
3230
  "div",
3202
3231
  {
3203
- className: `flex ${layout === "vertical" ? "flex-col" : "flex-row items-center gap-2"}`,
3232
+ className: `flex ${layout === "vertical" ? "flex-col gap-1" : "flex-row items-start gap-2"}`,
3204
3233
  style: style ?? {},
3205
3234
  children: [
3206
- label && // Render <label> only when a label is provided. An empty
3207
- // <label htmlFor=…> announces as an unlabeled control in
3208
- // some screen readers.
3209
- /* @__PURE__ */ jsx(
3235
+ label && /* @__PURE__ */ jsx(
3210
3236
  "label",
3211
3237
  {
3212
3238
  style: { color: labelColor || void 0 },
3213
- className: `text-sm font-medium ml-1 max-content ${!labelColor && "text-foreground"}`,
3239
+ className: `text-sm font-medium ${layout === "horizontal" ? "mt-2" : ""} max-content ${!labelColor && "text-foreground"}`,
3214
3240
  htmlFor,
3215
3241
  children: label
3216
3242
  }
3217
3243
  ),
3218
- /* @__PURE__ */ jsx(
3219
- "input",
3220
- {
3221
- autoComplete: "off",
3222
- disabled,
3223
- value,
3224
- onChange,
3225
- onBlur,
3226
- type: "text",
3227
- name,
3228
- id: htmlFor,
3229
- "aria-invalid": hasError || void 0,
3230
- "aria-describedby": hasError ? errorId : void 0,
3231
- className: `${hasError ? "border border-status-error" : "border border-border"} bg-surface text-foreground p-2 h-9 w-60 mt-1 rounded-lg disabled:bg-surface-raised disabled:text-foreground-muted disabled:cursor-not-allowed focus:outline-none focus:border-transparent focus:ring-2 focus:ring-accent transition-colors`,
3232
- style: inputStyle ?? {},
3233
- placeholder: placeholder ?? ""
3234
- }
3235
- )
3244
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
3245
+ /* @__PURE__ */ jsx(
3246
+ "input",
3247
+ {
3248
+ autoComplete: "off",
3249
+ disabled,
3250
+ value,
3251
+ onChange,
3252
+ onBlur,
3253
+ type: "text",
3254
+ name,
3255
+ id: htmlFor,
3256
+ "aria-invalid": hasError || void 0,
3257
+ "aria-describedby": hasError ? errorId : void 0,
3258
+ className: `${hasError ? "border border-status-error" : "border border-border"} bg-surface text-foreground p-2 h-9 w-60 rounded-lg disabled:bg-surface-raised disabled:text-foreground-muted disabled:cursor-not-allowed focus:outline-none focus:border-transparent focus:ring-2 focus:ring-accent transition-colors`,
3259
+ style: inputStyle ?? {},
3260
+ placeholder: placeholder ?? ""
3261
+ }
3262
+ ),
3263
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-status-error text-xs mt-1", children: errorMessage })
3264
+ ] })
3236
3265
  ]
3237
3266
  }
3238
- ),
3239
- hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-center text-status-error text-xs mt-1", children: errorMessage })
3240
- ] });
3267
+ )
3268
+ );
3241
3269
  }
3242
3270
  function NumberInput({
3243
3271
  step = 1,
@@ -3379,64 +3407,71 @@ function Password({
3379
3407
  const [passwordVisible, setPasswordVisible] = useState(false);
3380
3408
  const errorId = useId();
3381
3409
  const hasError = errorMessage != null;
3382
- return /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center justify-center", style: style ?? {}, children: [
3383
- /* @__PURE__ */ jsxs("div", { className: `flex ${layout === "vertical" ? "flex-col" : "flex-row items-center gap-2"}`, children: [
3384
- label && /* @__PURE__ */ jsx(
3385
- "label",
3386
- {
3387
- style: { color: labelColor || void 0 },
3388
- className: `text-sm font-medium ml-1 max-content ${!labelColor && "text-foreground"}`,
3389
- htmlFor,
3390
- children: label
3391
- }
3392
- ),
3393
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3394
- /* @__PURE__ */ jsx(
3395
- "input",
3410
+ return /* @__PURE__ */ jsxs(
3411
+ "div",
3412
+ {
3413
+ className: `flex ${layout === "vertical" ? "flex-col gap-1" : "flex-row items-start gap-2"}`,
3414
+ style: style ?? {},
3415
+ children: [
3416
+ label && /* @__PURE__ */ jsx(
3417
+ "label",
3396
3418
  {
3397
- autoComplete: "off",
3398
- disabled,
3399
- value,
3400
- onChange,
3401
- onBlur,
3402
- type: passwordVisible ? "text" : "password",
3403
- name,
3404
- id: htmlFor,
3405
- "aria-invalid": hasError || void 0,
3406
- "aria-describedby": hasError ? errorId : void 0,
3407
- className: `${hasError ? "border border-status-error" : "border border-border"} bg-surface text-foreground p-2 h-9 w-52 mt-1 rounded-lg disabled:bg-surface-raised disabled:text-foreground-muted disabled:cursor-not-allowed focus:outline-none focus:border-transparent focus:ring-2 focus:ring-accent transition-colors`,
3408
- style: inputStyle ?? {},
3409
- placeholder: placeholder ?? ""
3419
+ style: { color: labelColor || void 0 },
3420
+ className: `text-sm font-medium ${layout === "horizontal" ? "mt-2" : ""} max-content ${!labelColor && "text-foreground"}`,
3421
+ htmlFor,
3422
+ children: label
3410
3423
  }
3411
3424
  ),
3412
- /* @__PURE__ */ jsx(
3413
- "button",
3414
- {
3415
- type: "button",
3416
- className: "cursor-pointer p-1 text-foreground-secondary hover:text-foreground rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
3417
- style: iconColor ? { color: iconColor } : void 0,
3418
- onClick: () => setPasswordVisible(!passwordVisible),
3419
- "aria-label": passwordVisible ? "Hide password" : "Show password",
3420
- children: passwordVisible ? (
3421
- /* EyeSlash */
3422
- /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-6 h-6", children: [
3423
- /* @__PURE__ */ jsx("path", { d: "M3.53 2.47a.75.75 0 00-1.06 1.06l18 18a.75.75 0 101.06-1.06l-18-18zM22.676 12.553a11.249 11.249 0 01-2.631 4.31l-3.099-3.099a5.25 5.25 0 00-6.71-6.71L7.759 4.577a11.217 11.217 0 014.242-.827c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113z" }),
3424
- /* @__PURE__ */ jsx("path", { d: "M15.75 12c0 .18-.013.357-.037.53l-4.244-4.243A3.75 3.75 0 0115.75 12zM12.53 15.713l-4.243-4.244a3.75 3.75 0 004.243 4.243z" }),
3425
- /* @__PURE__ */ jsx("path", { d: "M6.75 12c0-.619.107-1.213.304-1.764l-3.1-3.1a11.25 11.25 0 00-2.63 4.31c-.12.362-.12.752 0 1.114 1.489 4.467 5.704 7.69 10.675 7.69 1.5 0 2.933-.294 4.242-.827l-2.477-2.477A5.25 5.25 0 016.75 12z" })
3426
- ] })
3427
- ) : (
3428
- /* Eye */
3429
- /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-6 h-6", children: [
3430
- /* @__PURE__ */ jsx("path", { d: "M12 15a3 3 0 100-6 3 3 0 000 6z" }),
3431
- /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 010-1.113zM17.25 12a5.25 5.25 0 11-10.5 0 5.25 5.25 0 0110.5 0z", clipRule: "evenodd" })
3432
- ] })
3425
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
3426
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3427
+ /* @__PURE__ */ jsx(
3428
+ "input",
3429
+ {
3430
+ autoComplete: "off",
3431
+ disabled,
3432
+ value,
3433
+ onChange,
3434
+ onBlur,
3435
+ type: passwordVisible ? "text" : "password",
3436
+ name,
3437
+ id: htmlFor,
3438
+ "aria-invalid": hasError || void 0,
3439
+ "aria-describedby": hasError ? errorId : void 0,
3440
+ className: `${hasError ? "border border-status-error" : "border border-border"} bg-surface text-foreground p-2 h-9 w-52 rounded-lg disabled:bg-surface-raised disabled:text-foreground-muted disabled:cursor-not-allowed focus:outline-none focus:border-transparent focus:ring-2 focus:ring-accent transition-colors`,
3441
+ style: inputStyle ?? {},
3442
+ placeholder: placeholder ?? ""
3443
+ }
3444
+ ),
3445
+ /* @__PURE__ */ jsx(
3446
+ "button",
3447
+ {
3448
+ type: "button",
3449
+ className: "cursor-pointer p-1 text-foreground-secondary hover:text-foreground rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
3450
+ style: iconColor ? { color: iconColor } : void 0,
3451
+ onClick: () => setPasswordVisible(!passwordVisible),
3452
+ "aria-label": passwordVisible ? "Hide password" : "Show password",
3453
+ children: passwordVisible ? (
3454
+ /* EyeSlash */
3455
+ /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-6 h-6", children: [
3456
+ /* @__PURE__ */ jsx("path", { d: "M3.53 2.47a.75.75 0 00-1.06 1.06l18 18a.75.75 0 101.06-1.06l-18-18zM22.676 12.553a11.249 11.249 0 01-2.631 4.31l-3.099-3.099a5.25 5.25 0 00-6.71-6.71L7.759 4.577a11.217 11.217 0 014.242-.827c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113z" }),
3457
+ /* @__PURE__ */ jsx("path", { d: "M15.75 12c0 .18-.013.357-.037.53l-4.244-4.243A3.75 3.75 0 0115.75 12zM12.53 15.713l-4.243-4.244a3.75 3.75 0 004.243 4.243z" }),
3458
+ /* @__PURE__ */ jsx("path", { d: "M6.75 12c0-.619.107-1.213.304-1.764l-3.1-3.1a11.25 11.25 0 00-2.63 4.31c-.12.362-.12.752 0 1.114 1.489 4.467 5.704 7.69 10.675 7.69 1.5 0 2.933-.294 4.242-.827l-2.477-2.477A5.25 5.25 0 016.75 12z" })
3459
+ ] })
3460
+ ) : (
3461
+ /* Eye */
3462
+ /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-6 h-6", children: [
3463
+ /* @__PURE__ */ jsx("path", { d: "M12 15a3 3 0 100-6 3 3 0 000 6z" }),
3464
+ /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 010-1.113zM17.25 12a5.25 5.25 0 11-10.5 0 5.25 5.25 0 0110.5 0z", clipRule: "evenodd" })
3465
+ ] })
3466
+ )
3467
+ }
3433
3468
  )
3434
- }
3435
- )
3436
- ] })
3437
- ] }),
3438
- hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-center text-status-error text-xs mt-1", children: errorMessage })
3439
- ] });
3469
+ ] }),
3470
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-status-error text-xs mt-1", children: errorMessage })
3471
+ ] })
3472
+ ]
3473
+ }
3474
+ );
3440
3475
  }
3441
3476
  function Checkbox({
3442
3477
  checked,
@@ -3549,15 +3584,54 @@ function AutoComplete({
3549
3584
  inputStyle,
3550
3585
  style,
3551
3586
  layout = "vertical",
3552
- items = [],
3587
+ items,
3588
+ onSearch,
3589
+ debounce = 250,
3553
3590
  onItemClick,
3554
- emptyText = "No results found"
3591
+ emptyText = "No results found",
3592
+ loadingText = "Searching\u2026"
3555
3593
  }) {
3556
3594
  const [term, setTerm] = useState("");
3557
3595
  const [open, setOpen] = useState(false);
3558
- const foundItems = term.trim() ? items.filter(
3596
+ const [asyncItems, setAsyncItems] = useState([]);
3597
+ const [loading, setLoading] = useState(false);
3598
+ const isAsync = typeof onSearch === "function";
3599
+ const debounceRef = useRef(null);
3600
+ const requestIdRef = useRef(0);
3601
+ const staticFiltered = isAsync || !items ? [] : term.trim() ? items.filter(
3559
3602
  ({ key, label: label2 }) => label2.toLowerCase().includes(term.toLowerCase()) || key.toLowerCase().includes(term.toLowerCase())
3560
3603
  ) : [];
3604
+ useEffect(() => {
3605
+ if (!isAsync) return;
3606
+ if (debounceRef.current) clearTimeout(debounceRef.current);
3607
+ if (!term.trim()) {
3608
+ setAsyncItems([]);
3609
+ setLoading(false);
3610
+ return;
3611
+ }
3612
+ const myId = ++requestIdRef.current;
3613
+ setLoading(true);
3614
+ debounceRef.current = setTimeout(async () => {
3615
+ try {
3616
+ const res = await onSearch(term);
3617
+ if (myId === requestIdRef.current) {
3618
+ setAsyncItems(res);
3619
+ }
3620
+ } catch {
3621
+ if (myId === requestIdRef.current) {
3622
+ setAsyncItems([]);
3623
+ }
3624
+ } finally {
3625
+ if (myId === requestIdRef.current) {
3626
+ setLoading(false);
3627
+ }
3628
+ }
3629
+ }, debounce);
3630
+ return () => {
3631
+ if (debounceRef.current) clearTimeout(debounceRef.current);
3632
+ };
3633
+ }, [term, isAsync, debounce, onSearch]);
3634
+ const foundItems = isAsync ? asyncItems : staticFiltered;
3561
3635
  const handleSelect = (item) => {
3562
3636
  setTerm(`${item.label} (${item.value})`);
3563
3637
  onItemClick?.(item.value);
@@ -3590,10 +3664,11 @@ function AutoComplete({
3590
3664
  autoComplete: "off",
3591
3665
  "aria-haspopup": "listbox",
3592
3666
  "aria-expanded": open,
3593
- "aria-autocomplete": "list"
3667
+ "aria-autocomplete": "list",
3668
+ "aria-busy": loading || void 0
3594
3669
  }
3595
3670
  ),
3596
- /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-5 h-5 flex-shrink-0 text-foreground-muted", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M10.5 3.75a6.75 6.75 0 100 13.5 6.75 6.75 0 000-13.5zM2.25 10.5a8.25 8.25 0 1114.59 5.28l4.69 4.69a.75.75 0 11-1.06 1.06l-4.69-4.69A8.25 8.25 0 012.25 10.5z", clipRule: "evenodd" }) })
3671
+ loading ? /* @__PURE__ */ jsx("span", { className: "w-5 h-5 flex-shrink-0 flex items-center justify-center text-accent", "aria-hidden": "true", children: /* @__PURE__ */ jsx(LoadingSpinner, { inline: true, size: "xs", spinnerColor: "currentColor" }) }) : /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-5 h-5 flex-shrink-0 text-foreground-muted", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M10.5 3.75a6.75 6.75 0 100 13.5 6.75 6.75 0 000-13.5zM2.25 10.5a8.25 8.25 0 1114.59 5.28l4.69 4.69a.75.75 0 11-1.06 1.06l-4.69-4.69A8.25 8.25 0 012.25 10.5z", clipRule: "evenodd" }) })
3597
3672
  ] }) }),
3598
3673
  /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
3599
3674
  Popover.Content,
@@ -3602,36 +3677,33 @@ function AutoComplete({
3602
3677
  sideOffset: 4,
3603
3678
  onOpenAutoFocus: (e) => e.preventDefault(),
3604
3679
  className: "w-64 bg-surface border border-border rounded-lg mt-1 shadow-md z-50 overflow-y-auto max-h-36 animate-in fade-in-0 zoom-in-95",
3605
- children: foundItems.length === 0 ? /* @__PURE__ */ jsx("div", { className: "h-full w-full flex flex-col items-center justify-center py-4 text-sm text-foreground-secondary", children: emptyText }) : /* @__PURE__ */ jsx("div", { role: "listbox", children: foundItems.map((item) => (
3606
- // tabIndex + Enter/Space onKeyDown
3607
- // makes each option keyboard-activatable.
3608
- // Full roving-tabindex / arrow-key nav
3609
- // is deferred to the Phase-5 rewrite.
3610
- /* @__PURE__ */ jsxs(
3611
- "div",
3612
- {
3613
- role: "option",
3614
- tabIndex: 0,
3615
- className: "text-sm flex items-center gap-2 p-2 transition-colors duration-150 hover:bg-surface-raised cursor-pointer text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
3616
- onClick: () => handleSelect(item),
3617
- onKeyDown: (e) => {
3618
- if (e.key === "Enter" || e.key === " ") {
3619
- e.preventDefault();
3620
- handleSelect(item);
3621
- }
3622
- },
3623
- children: [
3624
- item.icon,
3625
- /* @__PURE__ */ jsxs("span", { children: [
3626
- item.label,
3627
- " (",
3628
- item.value,
3629
- ")"
3630
- ] })
3631
- ]
3680
+ children: loading ? /* @__PURE__ */ jsxs("div", { className: "h-full w-full flex items-center justify-center gap-2 py-4 text-sm text-foreground-secondary", children: [
3681
+ /* @__PURE__ */ jsx(LoadingSpinner, { inline: true, size: "xs" }),
3682
+ /* @__PURE__ */ jsx("span", { children: loadingText })
3683
+ ] }) : foundItems.length === 0 ? /* @__PURE__ */ jsx("div", { className: "h-full w-full flex flex-col items-center justify-center py-4 text-sm text-foreground-secondary", children: emptyText }) : /* @__PURE__ */ jsx("div", { role: "listbox", children: foundItems.map((item) => /* @__PURE__ */ jsxs(
3684
+ "div",
3685
+ {
3686
+ role: "option",
3687
+ tabIndex: 0,
3688
+ className: "text-sm flex items-center gap-2 p-2 transition-colors duration-150 hover:bg-surface-raised cursor-pointer text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
3689
+ onClick: () => handleSelect(item),
3690
+ onKeyDown: (e) => {
3691
+ if (e.key === "Enter" || e.key === " ") {
3692
+ e.preventDefault();
3693
+ handleSelect(item);
3694
+ }
3632
3695
  },
3633
- item.key
3634
- )
3696
+ children: [
3697
+ item.icon,
3698
+ /* @__PURE__ */ jsxs("span", { children: [
3699
+ item.label,
3700
+ " (",
3701
+ item.value,
3702
+ ")"
3703
+ ] })
3704
+ ]
3705
+ },
3706
+ item.key
3635
3707
  )) })
3636
3708
  }
3637
3709
  ) })