@bmsuisse/ui 0.6.3 → 0.8.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
@@ -880,7 +880,8 @@ function FormField({
880
880
  // src/patterns/combobox/Combobox.tsx
881
881
  import { Check as Check4, ChevronsUpDown, X as X3 } from "lucide-react";
882
882
  import { useEffect, useMemo, useRef, useState as useState3 } from "react";
883
- import { jsx as jsx24, jsxs as jsxs10 } from "react/jsx-runtime";
883
+
884
+ // src/lib/optionGrouping.ts
884
885
  function groupMembers(options, group) {
885
886
  return options.filter((o) => o.group === group && !o.disabled).map((o) => o.value);
886
887
  }
@@ -890,6 +891,24 @@ function groupCheckState(options, selectedValues, group) {
890
891
  if (selectedCount === 0) return "unchecked";
891
892
  return selectedCount === members.length ? "checked" : "indeterminate";
892
893
  }
894
+ function buildRenderChunks(visibleOptions) {
895
+ const chunks = [];
896
+ visibleOptions.forEach((option, index) => {
897
+ const row = { option, index };
898
+ const last = chunks[chunks.length - 1];
899
+ if (option.group && last?.kind === "group" && last.group === option.group) {
900
+ last.rows.push(row);
901
+ } else if (option.group) {
902
+ chunks.push({ kind: "group", group: option.group, rows: [row] });
903
+ } else {
904
+ chunks.push({ kind: "single", row });
905
+ }
906
+ });
907
+ return chunks;
908
+ }
909
+
910
+ // src/patterns/combobox/Combobox.tsx
911
+ import { jsx as jsx24, jsxs as jsxs10 } from "react/jsx-runtime";
893
912
  function resolveGroupTriggerLabel(options, selectedValues, groupLabel) {
894
913
  const selectedSet = new Set(selectedValues);
895
914
  const groupsPresent = Array.from(
@@ -909,21 +928,6 @@ function resolveGroupTriggerLabel(options, selectedValues, groupLabel) {
909
928
  if (fullyCoveredGroups.length === 0 || hasLeftover) return null;
910
929
  return fullyCoveredGroups.map(groupLabel).join(", ");
911
930
  }
912
- function buildRenderChunks(visibleOptions) {
913
- const chunks = [];
914
- visibleOptions.forEach((option, index) => {
915
- const row = { option, index };
916
- const last = chunks[chunks.length - 1];
917
- if (option.group && last?.kind === "group" && last.group === option.group) {
918
- last.rows.push(row);
919
- } else if (option.group) {
920
- chunks.push({ kind: "group", group: option.group, rows: [row] });
921
- } else {
922
- chunks.push({ kind: "single", row });
923
- }
924
- });
925
- return chunks;
926
- }
927
931
  function Combobox(props) {
928
932
  const {
929
933
  options,
@@ -1149,9 +1153,249 @@ function Combobox(props) {
1149
1153
  ] });
1150
1154
  }
1151
1155
 
1156
+ // src/patterns/tag-combobox/TagCombobox.tsx
1157
+ import { X as X4 } from "lucide-react";
1158
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
1159
+ import { jsx as jsx25, jsxs as jsxs11 } from "react/jsx-runtime";
1160
+ function TagCombobox({
1161
+ options,
1162
+ value,
1163
+ onChange,
1164
+ placeholder = "Select\u2026",
1165
+ emptyMessage = "No matches.",
1166
+ disabled,
1167
+ loading = false,
1168
+ loadingMessage = "Loading\u2026",
1169
+ className,
1170
+ id,
1171
+ onSearchChange,
1172
+ container,
1173
+ groupLabels,
1174
+ ...rest
1175
+ }) {
1176
+ const testId = rest["data-testid"];
1177
+ const [open, setOpen] = useState4(false);
1178
+ const [search, setSearch] = useState4("");
1179
+ const [activeIndex, setActiveIndex] = useState4(0);
1180
+ const inputRef = useRef2(null);
1181
+ const fieldRef = useRef2(null);
1182
+ const selectedOptions = useMemo2(
1183
+ () => value.map((v) => options.find((o) => o.value === v) ?? { value: v, label: v }),
1184
+ [options, value]
1185
+ );
1186
+ const visibleOptions = useMemo2(() => {
1187
+ if (onSearchChange) return options;
1188
+ const term = search.trim().toLowerCase();
1189
+ if (!term) return options;
1190
+ return options.filter((option) => option.label.toLowerCase().includes(term));
1191
+ }, [options, search, onSearchChange]);
1192
+ useEffect2(() => {
1193
+ setActiveIndex((i) => Math.min(Math.max(i, 0), Math.max(visibleOptions.length - 1, 0)));
1194
+ }, [visibleOptions.length]);
1195
+ const renderChunks = useMemo2(() => buildRenderChunks(visibleOptions), [visibleOptions]);
1196
+ useEffect2(() => {
1197
+ if (!open) return;
1198
+ setSearch("");
1199
+ onSearchChange?.("");
1200
+ const firstSelected = value[0];
1201
+ const selectedIdx = firstSelected ? options.findIndex((o) => o.value === firstSelected) : -1;
1202
+ setActiveIndex(selectedIdx >= 0 ? selectedIdx : 0);
1203
+ }, [open]);
1204
+ function updateSearch(next) {
1205
+ setSearch(next);
1206
+ onSearchChange?.(next);
1207
+ if (!open) setOpen(true);
1208
+ }
1209
+ function toggleOption(option) {
1210
+ if (option.disabled) return;
1211
+ const next = value.includes(option.value) ? value.filter((v) => v !== option.value) : [...value, option.value];
1212
+ onChange(next);
1213
+ updateSearch("");
1214
+ inputRef.current?.focus();
1215
+ }
1216
+ function removeValue(v) {
1217
+ if (options.find((o) => o.value === v)?.disabled) return;
1218
+ onChange(value.filter((existing) => existing !== v));
1219
+ }
1220
+ const groupLabel = (group) => groupLabels?.[group] ?? group;
1221
+ function toggleGroup(group) {
1222
+ const members = groupMembers(options, group);
1223
+ const next = groupCheckState(options, value, group) === "checked" ? value.filter((v) => !members.includes(v)) : Array.from(/* @__PURE__ */ new Set([...value, ...members]));
1224
+ onChange(next);
1225
+ inputRef.current?.focus();
1226
+ }
1227
+ function handleKeyDown(event) {
1228
+ if (event.key === "ArrowDown") {
1229
+ event.preventDefault();
1230
+ setOpen(true);
1231
+ setActiveIndex((i) => Math.min(i + 1, Math.max(visibleOptions.length - 1, 0)));
1232
+ } else if (event.key === "ArrowUp") {
1233
+ event.preventDefault();
1234
+ setActiveIndex((i) => Math.max(i - 1, 0));
1235
+ } else if (event.key === "Enter") {
1236
+ event.preventDefault();
1237
+ const option = visibleOptions[activeIndex];
1238
+ if (option) toggleOption(option);
1239
+ } else if (event.key === "Escape") {
1240
+ setOpen(false);
1241
+ } else if (event.key === "Backspace" && search === "") {
1242
+ const last = value[value.length - 1];
1243
+ if (last !== void 0) removeValue(last);
1244
+ }
1245
+ }
1246
+ function renderOption({ option, index }) {
1247
+ const isSelected = value.includes(option.value);
1248
+ const isActive = index === activeIndex;
1249
+ return /* @__PURE__ */ jsxs11(
1250
+ "button",
1251
+ {
1252
+ type: "button",
1253
+ role: "option",
1254
+ "aria-selected": isSelected,
1255
+ disabled: option.disabled,
1256
+ "data-testid": testId ? `${testId}-option` : void 0,
1257
+ "data-option-value": option.value,
1258
+ onMouseEnter: () => setActiveIndex(index),
1259
+ onClick: () => toggleOption(option),
1260
+ className: cn(
1261
+ "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none disabled:pointer-events-none disabled:opacity-50",
1262
+ isActive && "bg-accent text-accent-foreground"
1263
+ ),
1264
+ children: [
1265
+ /* @__PURE__ */ jsx25(
1266
+ "input",
1267
+ {
1268
+ type: "checkbox",
1269
+ checked: isSelected,
1270
+ readOnly: true,
1271
+ tabIndex: -1,
1272
+ className: "pointer-events-none h-3.5 w-3.5 shrink-0 accent-primary"
1273
+ }
1274
+ ),
1275
+ /* @__PURE__ */ jsx25("span", { className: "truncate", children: option.label })
1276
+ ]
1277
+ },
1278
+ option.value
1279
+ );
1280
+ }
1281
+ return /* @__PURE__ */ jsxs11(Popover, { open, onOpenChange: setOpen, children: [
1282
+ /* @__PURE__ */ jsx25(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs11(
1283
+ "div",
1284
+ {
1285
+ ref: fieldRef,
1286
+ "data-testid": testId,
1287
+ role: "combobox",
1288
+ "aria-expanded": open,
1289
+ "aria-disabled": disabled,
1290
+ className: cn(
1291
+ "flex min-h-9 w-full flex-wrap items-center gap-1.5 rounded-md border border-input bg-transparent px-2 py-1.5 text-sm",
1292
+ "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring",
1293
+ disabled && "cursor-not-allowed opacity-50",
1294
+ className
1295
+ ),
1296
+ onClick: () => {
1297
+ if (!disabled) inputRef.current?.focus();
1298
+ },
1299
+ children: [
1300
+ selectedOptions.map((option) => /* @__PURE__ */ jsxs11(
1301
+ "span",
1302
+ {
1303
+ className: "inline-flex items-center gap-1 rounded-full bg-muted py-0.5 pr-1 pl-2.5 text-xs font-medium text-muted-foreground",
1304
+ children: [
1305
+ /* @__PURE__ */ jsx25("span", { className: "max-w-48 truncate", children: option.label }),
1306
+ !disabled && /* @__PURE__ */ jsx25(
1307
+ "button",
1308
+ {
1309
+ type: "button",
1310
+ "aria-label": `Remove ${option.label}`,
1311
+ className: "rounded-full p-0.5 hover:bg-accent hover:text-accent-foreground",
1312
+ onClick: (event) => {
1313
+ event.stopPropagation();
1314
+ removeValue(option.value);
1315
+ },
1316
+ children: /* @__PURE__ */ jsx25(X4, { className: "h-3 w-3" })
1317
+ }
1318
+ )
1319
+ ]
1320
+ },
1321
+ option.value
1322
+ )),
1323
+ /* @__PURE__ */ jsx25(
1324
+ "input",
1325
+ {
1326
+ ref: inputRef,
1327
+ id,
1328
+ value: search,
1329
+ disabled,
1330
+ placeholder: selectedOptions.length === 0 ? placeholder : void 0,
1331
+ "aria-label": placeholder,
1332
+ className: "min-w-24 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed",
1333
+ onFocus: () => setOpen(true),
1334
+ onChange: (event) => updateSearch(event.target.value),
1335
+ onKeyDown: handleKeyDown
1336
+ }
1337
+ )
1338
+ ]
1339
+ }
1340
+ ) }),
1341
+ /* @__PURE__ */ jsx25(
1342
+ PopoverContent,
1343
+ {
1344
+ align: "start",
1345
+ container,
1346
+ className: "w-[var(--radix-popover-trigger-width)] min-w-[10rem] p-2",
1347
+ onOpenAutoFocus: (event) => event.preventDefault(),
1348
+ onCloseAutoFocus: (event) => event.preventDefault(),
1349
+ onInteractOutside: (event) => {
1350
+ if (fieldRef.current?.contains(event.target)) event.preventDefault();
1351
+ },
1352
+ children: /* @__PURE__ */ jsx25("div", { role: "listbox", className: "flex max-h-60 flex-col gap-0.5 overflow-y-auto", children: loading ? /* @__PURE__ */ jsx25("p", { className: "py-2 text-center text-sm text-muted-foreground", children: loadingMessage }) : renderChunks.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "py-2 text-center text-sm text-muted-foreground", children: emptyMessage }) : renderChunks.map((chunk) => {
1353
+ if (chunk.kind === "single") return renderOption(chunk.row);
1354
+ const headerState = groupCheckState(options, value, chunk.group);
1355
+ return (
1356
+ // The header and every one of its group's rows share this wrapper -- its
1357
+ // bounds are the sticky header's containing block, so `sticky top-0` keeps
1358
+ // the header pinned for the group's whole scroll extent instead of just
1359
+ // past its first row. Spacing above a group lives on this wrapper's
1360
+ // `pt-1`/`first:pt-0`, not on the header itself -- see `Combobox`'s own
1361
+ // identical structure for why both of these matter.
1362
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-0.5 pt-1 first:pt-0", children: [
1363
+ /* @__PURE__ */ jsxs11(
1364
+ "div",
1365
+ {
1366
+ "data-group-header": true,
1367
+ className: "sticky top-0 z-10 flex items-center gap-2 rounded-sm bg-muted px-2 py-1.5 text-sm font-semibold text-foreground",
1368
+ children: [
1369
+ /* @__PURE__ */ jsx25(
1370
+ "input",
1371
+ {
1372
+ type: "checkbox",
1373
+ checked: headerState === "checked",
1374
+ ref: (el) => {
1375
+ if (el) el.indeterminate = headerState === "indeterminate";
1376
+ },
1377
+ onChange: () => toggleGroup(chunk.group),
1378
+ onClick: (event) => event.stopPropagation(),
1379
+ className: "h-3.5 w-3.5 shrink-0 accent-primary",
1380
+ "aria-label": `Select all of ${groupLabel(chunk.group)}`
1381
+ }
1382
+ ),
1383
+ /* @__PURE__ */ jsx25("span", { className: "truncate", children: groupLabel(chunk.group) })
1384
+ ]
1385
+ }
1386
+ ),
1387
+ chunk.rows.map(renderOption)
1388
+ ] }, `group-${chunk.group}`)
1389
+ );
1390
+ }) })
1391
+ }
1392
+ )
1393
+ ] });
1394
+ }
1395
+
1152
1396
  // src/patterns/alert-box/AlertBox.tsx
1153
1397
  import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
1154
- import { jsx as jsx25, jsxs as jsxs11 } from "react/jsx-runtime";
1398
+ import { jsx as jsx26, jsxs as jsxs12 } from "react/jsx-runtime";
1155
1399
  var variantStyles = {
1156
1400
  error: "border-destructive bg-destructive/10 text-destructive",
1157
1401
  warning: "border-amber-500 bg-amber-500/10 text-amber-700 dark:text-amber-300",
@@ -1159,24 +1403,24 @@ var variantStyles = {
1159
1403
  success: "border-emerald-500 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
1160
1404
  };
1161
1405
  var defaultIcons = {
1162
- error: /* @__PURE__ */ jsx25(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
1163
- warning: /* @__PURE__ */ jsx25(AlertTriangle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
1164
- info: /* @__PURE__ */ jsx25(Info, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
1165
- success: /* @__PURE__ */ jsx25(CheckCircle2, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" })
1406
+ error: /* @__PURE__ */ jsx26(AlertCircle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
1407
+ warning: /* @__PURE__ */ jsx26(AlertTriangle, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
1408
+ info: /* @__PURE__ */ jsx26(Info, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
1409
+ success: /* @__PURE__ */ jsx26(CheckCircle2, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" })
1166
1410
  };
1167
1411
  var AlertBox = ({ variant, title, children, className, icon }) => {
1168
1412
  const resolvedIcon = icon === void 0 ? defaultIcons[variant] : icon;
1169
- return /* @__PURE__ */ jsx25("div", { className: cn("rounded-md border p-3 text-sm", variantStyles[variant], className), children: /* @__PURE__ */ jsxs11("div", { className: "flex flex-row items-start gap-2", children: [
1170
- resolvedIcon != null && /* @__PURE__ */ jsx25("span", { className: "mt-0.5 shrink-0", children: resolvedIcon }),
1171
- /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-1", children: [
1172
- title && /* @__PURE__ */ jsx25("p", { className: "font-bold", children: title }),
1173
- /* @__PURE__ */ jsx25("div", { children })
1413
+ return /* @__PURE__ */ jsx26("div", { className: cn("rounded-md border p-3 text-sm", variantStyles[variant], className), children: /* @__PURE__ */ jsxs12("div", { className: "flex flex-row items-start gap-2", children: [
1414
+ resolvedIcon != null && /* @__PURE__ */ jsx26("span", { className: "mt-0.5 shrink-0", children: resolvedIcon }),
1415
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-1", children: [
1416
+ title && /* @__PURE__ */ jsx26("p", { className: "font-bold", children: title }),
1417
+ /* @__PURE__ */ jsx26("div", { children })
1174
1418
  ] })
1175
1419
  ] }) });
1176
1420
  };
1177
1421
 
1178
1422
  // src/patterns/status-badge/StatusBadge.tsx
1179
- import { jsx as jsx26 } from "react/jsx-runtime";
1423
+ import { jsx as jsx27 } from "react/jsx-runtime";
1180
1424
  var TONE_CLASSES = {
1181
1425
  success: "border-transparent bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300",
1182
1426
  warning: "border-transparent bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300",
@@ -1223,11 +1467,11 @@ function resolveTone(status, tone, toneMap) {
1223
1467
  var StatusBadge = ({ status, label, tone, toneMap, className, ...props }) => {
1224
1468
  const resolvedTone = resolveTone(status, tone, toneMap);
1225
1469
  const displayLabel = label ?? deriveLabel(status);
1226
- return /* @__PURE__ */ jsx26(Badge, { variant: "outline", className: cn(TONE_CLASSES[resolvedTone], className), ...props, children: displayLabel });
1470
+ return /* @__PURE__ */ jsx27(Badge, { variant: "outline", className: cn(TONE_CLASSES[resolvedTone], className), ...props, children: displayLabel });
1227
1471
  };
1228
1472
 
1229
1473
  // src/patterns/button-group/ButtonGroup.tsx
1230
- import { jsx as jsx27, jsxs as jsxs12 } from "react/jsx-runtime";
1474
+ import { jsx as jsx28, jsxs as jsxs13 } from "react/jsx-runtime";
1231
1475
  function ButtonGroup({
1232
1476
  options,
1233
1477
  value,
@@ -1237,10 +1481,10 @@ function ButtonGroup({
1237
1481
  className,
1238
1482
  ...props
1239
1483
  }) {
1240
- return /* @__PURE__ */ jsx27("div", { role: "group", "aria-label": props["aria-label"], className: cn("inline-flex", className), "data-testid": props["data-testid"], children: options.map((option, index) => {
1484
+ return /* @__PURE__ */ jsx28("div", { role: "group", "aria-label": props["aria-label"], className: cn("inline-flex", className), "data-testid": props["data-testid"], children: options.map((option, index) => {
1241
1485
  const selected = option.value === value;
1242
1486
  const Icon2 = option.icon;
1243
- return /* @__PURE__ */ jsxs12(
1487
+ return /* @__PURE__ */ jsxs13(
1244
1488
  Button,
1245
1489
  {
1246
1490
  type: "button",
@@ -1258,7 +1502,7 @@ function ButtonGroup({
1258
1502
  index > 0 && "-ml-px"
1259
1503
  ),
1260
1504
  children: [
1261
- Icon2 && /* @__PURE__ */ jsx27(Icon2, { className: "h-4 w-4" }),
1505
+ Icon2 && /* @__PURE__ */ jsx28(Icon2, { className: "h-4 w-4" }),
1262
1506
  option.label
1263
1507
  ]
1264
1508
  },
@@ -1270,7 +1514,7 @@ function ButtonGroup({
1270
1514
  // src/patterns/loading-spinner/LoadingSpinner.tsx
1271
1515
  import { cva as cva4 } from "class-variance-authority";
1272
1516
  import { Loader2 } from "lucide-react";
1273
- import { jsx as jsx28, jsxs as jsxs13 } from "react/jsx-runtime";
1517
+ import { jsx as jsx29, jsxs as jsxs14 } from "react/jsx-runtime";
1274
1518
  var loadingSpinnerIconVariants = cva4("animate-spin text-muted-foreground", {
1275
1519
  variants: {
1276
1520
  size: {
@@ -1288,27 +1532,27 @@ var LoadingSpinner = ({
1288
1532
  label,
1289
1533
  className,
1290
1534
  ...props
1291
- }) => /* @__PURE__ */ jsxs13("span", { role: "status", className: cn("inline-flex items-center gap-2", className), ...props, children: [
1292
- /* @__PURE__ */ jsx28(Loader2, { className: cn(loadingSpinnerIconVariants({ size })), "aria-hidden": "true" }),
1293
- label ? /* @__PURE__ */ jsx28("span", { children: label }) : null
1535
+ }) => /* @__PURE__ */ jsxs14("span", { role: "status", className: cn("inline-flex items-center gap-2", className), ...props, children: [
1536
+ /* @__PURE__ */ jsx29(Loader2, { className: cn(loadingSpinnerIconVariants({ size })), "aria-hidden": "true" }),
1537
+ label ? /* @__PURE__ */ jsx29("span", { children: label }) : null
1294
1538
  ] });
1295
1539
  var LoadingOverlay = ({
1296
1540
  size = "lg",
1297
1541
  label,
1298
1542
  className,
1299
1543
  ...props
1300
- }) => /* @__PURE__ */ jsx28(
1544
+ }) => /* @__PURE__ */ jsx29(
1301
1545
  "div",
1302
1546
  {
1303
1547
  className: cn("flex h-full min-h-32 w-full items-center justify-center", className),
1304
1548
  ...props,
1305
- children: /* @__PURE__ */ jsx28(LoadingSpinner, { size, label })
1549
+ children: /* @__PURE__ */ jsx29(LoadingSpinner, { size, label })
1306
1550
  }
1307
1551
  );
1308
1552
 
1309
1553
  // src/patterns/search-bar/SearchBar.tsx
1310
- import { Search, X as X4 } from "lucide-react";
1311
- import { jsx as jsx29, jsxs as jsxs14 } from "react/jsx-runtime";
1554
+ import { Search, X as X5 } from "lucide-react";
1555
+ import { jsx as jsx30, jsxs as jsxs15 } from "react/jsx-runtime";
1312
1556
  function SearchBar({
1313
1557
  value,
1314
1558
  onChange,
@@ -1320,9 +1564,9 @@ function SearchBar({
1320
1564
  ...props
1321
1565
  }) {
1322
1566
  const showClear = onClear !== false && value.length > 0;
1323
- return /* @__PURE__ */ jsxs14("div", { className: cn("relative", className), children: [
1324
- /* @__PURE__ */ jsx29("span", { className: "pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-muted-foreground", children: isLoading ? /* @__PURE__ */ jsx29(LoadingSpinner, { size: "sm" }) : /* @__PURE__ */ jsx29(Search, { className: "h-4 w-4", "aria-hidden": "true" }) }),
1325
- /* @__PURE__ */ jsx29(
1567
+ return /* @__PURE__ */ jsxs15("div", { className: cn("relative", className), children: [
1568
+ /* @__PURE__ */ jsx30("span", { className: "pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-muted-foreground", children: isLoading ? /* @__PURE__ */ jsx30(LoadingSpinner, { size: "sm" }) : /* @__PURE__ */ jsx30(Search, { className: "h-4 w-4", "aria-hidden": "true" }) }),
1569
+ /* @__PURE__ */ jsx30(
1326
1570
  "input",
1327
1571
  {
1328
1572
  ref: inputRef,
@@ -1341,14 +1585,255 @@ function SearchBar({
1341
1585
  ...props
1342
1586
  }
1343
1587
  ),
1344
- showClear && /* @__PURE__ */ jsx29(
1588
+ showClear && /* @__PURE__ */ jsx30(
1345
1589
  "button",
1346
1590
  {
1347
1591
  type: "button",
1348
1592
  onClick: () => onClear ? onClear() : onChange(""),
1349
1593
  "aria-label": clearLabel,
1350
1594
  className: "absolute top-1/2 right-2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground",
1351
- children: /* @__PURE__ */ jsx29(X4, { className: "h-4 w-4", "aria-hidden": "true" })
1595
+ children: /* @__PURE__ */ jsx30(X5, { className: "h-4 w-4", "aria-hidden": "true" })
1596
+ }
1597
+ )
1598
+ ] });
1599
+ }
1600
+
1601
+ // src/patterns/sidebar/context.ts
1602
+ import { createContext, useContext } from "react";
1603
+ var SidebarContext = createContext({ collapsed: false });
1604
+ var SidebarContextProvider = SidebarContext.Provider;
1605
+ function useSidebarCollapsed() {
1606
+ return useContext(SidebarContext).collapsed;
1607
+ }
1608
+
1609
+ // src/patterns/sidebar/NavItem.tsx
1610
+ import { jsx as jsx31, jsxs as jsxs16 } from "react/jsx-runtime";
1611
+ function NavItem({
1612
+ as,
1613
+ icon: Icon2,
1614
+ label,
1615
+ active = false,
1616
+ badge,
1617
+ className,
1618
+ ...rest
1619
+ }) {
1620
+ const collapsed = useSidebarCollapsed();
1621
+ const Comp = as ?? "a";
1622
+ const row = /* @__PURE__ */ jsxs16(
1623
+ Comp,
1624
+ {
1625
+ className: cn(
1626
+ "group relative flex items-center rounded-lg px-2.5 py-2 text-[13px] font-medium transition-colors",
1627
+ "focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset focus-visible:outline-none",
1628
+ collapsed ? "justify-center gap-0" : "gap-2.5",
1629
+ active ? "bg-nav-primary/8 font-semibold text-foreground" : "text-foreground/55 hover:bg-muted/60 hover:text-foreground",
1630
+ className
1631
+ ),
1632
+ ...rest,
1633
+ children: [
1634
+ Icon2 && /* @__PURE__ */ jsx31(
1635
+ Icon2,
1636
+ {
1637
+ className: cn("h-[18px] w-[18px] shrink-0", active ? "text-nav-primary" : "text-muted-foreground"),
1638
+ strokeWidth: active ? 2 : 1.75,
1639
+ "aria-hidden": true
1640
+ }
1641
+ ),
1642
+ /* @__PURE__ */ jsx31(
1643
+ "span",
1644
+ {
1645
+ className: cn(
1646
+ "truncate transition-[opacity,max-width] duration-150",
1647
+ collapsed ? "max-w-0 opacity-0" : "max-w-[10rem] opacity-100"
1648
+ ),
1649
+ children: label
1650
+ }
1651
+ ),
1652
+ !collapsed && badge
1653
+ ]
1654
+ }
1655
+ );
1656
+ if (!collapsed) return row;
1657
+ return /* @__PURE__ */ jsxs16(Tooltip, { children: [
1658
+ /* @__PURE__ */ jsx31(TooltipTrigger, { asChild: true, children: row }),
1659
+ /* @__PURE__ */ jsxs16(TooltipContent, { side: "right", className: "flex items-center gap-1.5", children: [
1660
+ label,
1661
+ badge
1662
+ ] })
1663
+ ] });
1664
+ }
1665
+
1666
+ // src/patterns/sidebar/NavGroup.tsx
1667
+ import { ChevronDown as ChevronDown2 } from "lucide-react";
1668
+ import { useState as useState5 } from "react";
1669
+ import { jsx as jsx32, jsxs as jsxs17 } from "react/jsx-runtime";
1670
+ function NavGroup({
1671
+ label,
1672
+ defaultCollapsed = false,
1673
+ collapsed,
1674
+ onCollapsedChange,
1675
+ children,
1676
+ className
1677
+ }) {
1678
+ const [internalCollapsed, setInternalCollapsed] = useState5(defaultCollapsed);
1679
+ const groupCollapsed = collapsed ?? internalCollapsed;
1680
+ const railCollapsed = useSidebarCollapsed();
1681
+ const setGroupCollapsed = (next) => {
1682
+ onCollapsedChange?.(next);
1683
+ if (collapsed === void 0) setInternalCollapsed(next);
1684
+ };
1685
+ return /* @__PURE__ */ jsxs17("div", { className: cn("flex flex-col gap-0.5", className), children: [
1686
+ label && (railCollapsed ? /* @__PURE__ */ jsx32("div", { className: "mx-2 my-1 h-px bg-border/50", "aria-hidden": true }) : /* @__PURE__ */ jsxs17(
1687
+ "button",
1688
+ {
1689
+ type: "button",
1690
+ onClick: () => setGroupCollapsed(!groupCollapsed),
1691
+ "aria-expanded": !groupCollapsed,
1692
+ className: "group flex w-full items-center justify-between px-2.5 py-0.5 text-[11px] font-medium text-muted-foreground/80 hover:text-foreground",
1693
+ children: [
1694
+ /* @__PURE__ */ jsx32("span", { children: label }),
1695
+ /* @__PURE__ */ jsx32(
1696
+ ChevronDown2,
1697
+ {
1698
+ className: cn(
1699
+ "h-4 w-4 opacity-40 transition-transform group-hover:opacity-100",
1700
+ groupCollapsed && "-rotate-90"
1701
+ ),
1702
+ "aria-hidden": true
1703
+ }
1704
+ )
1705
+ ]
1706
+ }
1707
+ )),
1708
+ !groupCollapsed && /* @__PURE__ */ jsx32("div", { className: "flex flex-col gap-0.5", children })
1709
+ ] });
1710
+ }
1711
+
1712
+ // src/patterns/sidebar/Sidebar.tsx
1713
+ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
1714
+ import {
1715
+ useCallback,
1716
+ useEffect as useEffect3,
1717
+ useRef as useRef3,
1718
+ useState as useState6
1719
+ } from "react";
1720
+ import { jsx as jsx33, jsxs as jsxs18 } from "react/jsx-runtime";
1721
+ function resolve(value, collapsed) {
1722
+ return typeof value === "function" ? value(collapsed) : value;
1723
+ }
1724
+ function Sidebar({
1725
+ collapsed = false,
1726
+ onCollapsedChange,
1727
+ resizable = false,
1728
+ width,
1729
+ onWidthChange,
1730
+ defaultWidth = 240,
1731
+ minWidth = 180,
1732
+ maxWidth = 420,
1733
+ railWidth = 56,
1734
+ header,
1735
+ footer,
1736
+ children,
1737
+ className
1738
+ }) {
1739
+ const [internalWidth, setInternalWidth] = useState6(defaultWidth);
1740
+ const currentWidth = width ?? internalWidth;
1741
+ const [isDragging, setIsDragging] = useState6(false);
1742
+ const setWidth = useCallback(
1743
+ (next) => {
1744
+ const clamped = Math.min(maxWidth, Math.max(minWidth, next));
1745
+ onWidthChange?.(clamped);
1746
+ if (width === void 0) setInternalWidth(clamped);
1747
+ },
1748
+ [maxWidth, minWidth, onWidthChange, width]
1749
+ );
1750
+ const dragStartRef = useRef3(null);
1751
+ const handlePointerDown = (event) => {
1752
+ event.currentTarget.setPointerCapture(event.pointerId);
1753
+ dragStartRef.current = { x: event.clientX, width: currentWidth };
1754
+ setIsDragging(true);
1755
+ };
1756
+ const handlePointerMove = (event) => {
1757
+ if (!dragStartRef.current) return;
1758
+ setWidth(dragStartRef.current.width + (event.clientX - dragStartRef.current.x));
1759
+ };
1760
+ const endDrag = (event) => {
1761
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1762
+ event.currentTarget.releasePointerCapture(event.pointerId);
1763
+ }
1764
+ dragStartRef.current = null;
1765
+ setIsDragging(false);
1766
+ };
1767
+ return /* @__PURE__ */ jsx33(SidebarContextProvider, { value: { collapsed }, children: /* @__PURE__ */ jsx33(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs18(
1768
+ "aside",
1769
+ {
1770
+ style: { width: collapsed ? railWidth : currentWidth },
1771
+ className: cn(
1772
+ "relative flex h-full shrink-0 flex-col border-r border-border bg-sidebar",
1773
+ !isDragging && "transition-[width] duration-150",
1774
+ className
1775
+ ),
1776
+ children: [
1777
+ (header || onCollapsedChange) && /* @__PURE__ */ jsxs18("div", { className: "flex h-16 items-center gap-2 border-b border-border px-3", children: [
1778
+ header && /* @__PURE__ */ jsx33("div", { className: "min-w-0 flex-1", children: resolve(header, collapsed) }),
1779
+ onCollapsedChange && /* @__PURE__ */ jsx33(
1780
+ "button",
1781
+ {
1782
+ type: "button",
1783
+ onClick: () => onCollapsedChange(!collapsed),
1784
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
1785
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground",
1786
+ children: collapsed ? /* @__PURE__ */ jsx33(PanelLeftOpen, { className: "h-4 w-4", "aria-hidden": true }) : /* @__PURE__ */ jsx33(PanelLeftClose, { className: "h-4 w-4", "aria-hidden": true })
1787
+ }
1788
+ )
1789
+ ] }),
1790
+ /* @__PURE__ */ jsx33(SidebarNav, { children }),
1791
+ footer && /* @__PURE__ */ jsx33("div", { className: "border-t border-border/50 px-2 py-2", children: resolve(footer, collapsed) }),
1792
+ resizable && !collapsed && /* @__PURE__ */ jsx33(
1793
+ "div",
1794
+ {
1795
+ onPointerDown: handlePointerDown,
1796
+ onPointerMove: handlePointerMove,
1797
+ onPointerUp: endDrag,
1798
+ onDoubleClick: () => setWidth(defaultWidth),
1799
+ className: "absolute top-0 right-0 h-full w-1.5 cursor-col-resize touch-none hover:bg-primary/40"
1800
+ }
1801
+ )
1802
+ ]
1803
+ }
1804
+ ) }) });
1805
+ }
1806
+ function SidebarNav({ children, className, ...rest }) {
1807
+ const ref = useRef3(null);
1808
+ const [showFade, setShowFade] = useState6(false);
1809
+ useEffect3(() => {
1810
+ const el = ref.current;
1811
+ if (!el) return;
1812
+ const update = () => setShowFade(el.scrollHeight - el.scrollTop - el.clientHeight > 1);
1813
+ update();
1814
+ el.addEventListener("scroll", update);
1815
+ const observer = new ResizeObserver(update);
1816
+ observer.observe(el);
1817
+ return () => {
1818
+ el.removeEventListener("scroll", update);
1819
+ observer.disconnect();
1820
+ };
1821
+ }, []);
1822
+ return /* @__PURE__ */ jsxs18("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
1823
+ /* @__PURE__ */ jsx33(
1824
+ "div",
1825
+ {
1826
+ ref,
1827
+ className: cn("flex flex-1 flex-col gap-3 overflow-y-auto px-2 py-2", className),
1828
+ ...rest,
1829
+ children
1830
+ }
1831
+ ),
1832
+ showFade && /* @__PURE__ */ jsx33(
1833
+ "div",
1834
+ {
1835
+ className: "pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-sidebar to-transparent",
1836
+ "aria-hidden": true
1352
1837
  }
1353
1838
  )
1354
1839
  ] });
@@ -1396,6 +1881,8 @@ export {
1396
1881
  LoadingOverlay,
1397
1882
  LoadingSpinner,
1398
1883
  Modal,
1884
+ NavGroup,
1885
+ NavItem,
1399
1886
  Popover,
1400
1887
  PopoverAnchor,
1401
1888
  PopoverContent,
@@ -1419,6 +1906,8 @@ export {
1419
1906
  SheetPortal,
1420
1907
  SheetTitle,
1421
1908
  SheetTrigger,
1909
+ Sidebar,
1910
+ SidebarNav,
1422
1911
  Skeleton,
1423
1912
  StatusBadge,
1424
1913
  Switch,
@@ -1434,6 +1923,7 @@ export {
1434
1923
  TabsContent,
1435
1924
  TabsList,
1436
1925
  TabsTrigger,
1926
+ TagCombobox,
1437
1927
  Textarea,
1438
1928
  Tooltip,
1439
1929
  TooltipContent,
@@ -1443,6 +1933,7 @@ export {
1443
1933
  buttonVariants,
1444
1934
  cn,
1445
1935
  loadingSpinnerIconVariants,
1446
- sheetVariants
1936
+ sheetVariants,
1937
+ useSidebarCollapsed
1447
1938
  };
1448
1939
  //# sourceMappingURL=index.js.map