@bearmenu/ui 0.8.52 → 0.8.54

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.
Files changed (44) hide show
  1. package/dist/{chunk-4WW3HPIW.js → chunk-6Z6MBILC.js} +8 -4
  2. package/dist/chunk-GIJGVFGC.js +177 -0
  3. package/dist/chunk-RKNPPYRU.js +86 -0
  4. package/dist/{chunk-2ADCZXZJ.js → chunk-RXXD3UYP.js} +3 -1
  5. package/dist/components/alert.d.ts +2 -2
  6. package/dist/components/badge.d.ts +1 -1
  7. package/dist/components/button.d.ts +2 -2
  8. package/dist/components/combobox.js +1 -1
  9. package/dist/components/event-detail-panel.d.ts +17 -6
  10. package/dist/components/event-detail-panel.js +58 -8
  11. package/dist/components/facet-group.d.ts +9 -2
  12. package/dist/components/facet-group.js +108 -10
  13. package/dist/components/filter-option-chip.d.ts +15 -0
  14. package/dist/components/filter-option-chip.js +3 -0
  15. package/dist/components/filter-option-group.d.ts +37 -0
  16. package/dist/components/filter-option-group.js +10 -0
  17. package/dist/components/filter-panel-footer.d.ts +15 -0
  18. package/dist/components/filter-panel-footer.js +63 -0
  19. package/dist/components/menu-item.d.ts +24 -3
  20. package/dist/components/menu-item.js +1 -1
  21. package/dist/components/multi-select-combobox.js +1 -1
  22. package/dist/components/place-card.js +1 -1
  23. package/dist/components/popover.js +1 -1
  24. package/dist/components/searchable-select.js +1 -1
  25. package/dist/components/spinner.d.ts +2 -2
  26. package/dist/components/tag.d.ts +2 -2
  27. package/dist/components/text.d.ts +1 -1
  28. package/dist/components/toggle.d.ts +1 -1
  29. package/dist/components/top-ranking-entry.js +1 -1
  30. package/package.json +11 -8
  31. package/src/components/event-detail-panel.stories.tsx +16 -0
  32. package/src/components/event-detail-panel.test.tsx +36 -0
  33. package/src/components/event-detail-panel.tsx +86 -14
  34. package/src/components/facet-group.tsx +153 -54
  35. package/src/components/filter-option-chip.test.tsx +50 -0
  36. package/src/components/filter-option-chip.tsx +81 -0
  37. package/src/components/filter-option-group.stories.tsx +250 -0
  38. package/src/components/filter-option-group.test.tsx +100 -0
  39. package/src/components/filter-option-group.tsx +231 -0
  40. package/src/components/filter-panel-footer.tsx +63 -0
  41. package/src/components/menu-item.stories.tsx +82 -0
  42. package/src/components/menu-item.test.tsx +55 -1
  43. package/src/components/menu-item.tsx +31 -4
  44. package/src/components/popover.tsx +3 -1
@@ -19,6 +19,7 @@ import { cn } from "../lib/utils";
19
19
  import { GradeAtom, type GradeAtomLabels } from "./grade-atom";
20
20
  import type { PlaceGrade } from "./place-types";
21
21
  import { Button } from "./button";
22
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
22
23
  import { EventPosterFallback } from "./event-poster-fallback";
23
24
  import { Separator } from "./separator";
24
25
  import { Skeleton } from "./skeleton";
@@ -144,8 +145,15 @@ export type EventDetailPanelProps = {
144
145
  /** The hype tier, rendered as given (the app's own badge) in the chip row. */
145
146
  hype?: React.ReactNode;
146
147
  venue?: EventDetailVenue;
147
- /** Activity tags, after status, fee and hype. */
148
- tags?: string[];
148
+ /** Activity tags. Pass a destination to make a tag a browse action. */
149
+ tags?: (string | { label: string; href: string; onClick?: () => void })[];
150
+ /** Entry information opened by the price/free-entry control. */
151
+ entryDetails?: {
152
+ label: string;
153
+ description: string;
154
+ action?: React.ReactNode;
155
+ onOpenChange?: (open: boolean) => void;
156
+ };
149
157
  /** Performer names, in the order the API gave them. */
150
158
  performers?: string[];
151
159
  /** How many performers to show before the "show all" chip. Default 5. */
@@ -197,6 +205,7 @@ export function EventDetailPanel({
197
205
  hype,
198
206
  venue,
199
207
  tags,
208
+ entryDetails,
200
209
  performers,
201
210
  performersPreview = 5,
202
211
  showAllPerformersLabel,
@@ -289,6 +298,8 @@ export function EventDetailPanel({
289
298
  soonLabel={soonLabel}
290
299
  hype={hype}
291
300
  tags={tags}
301
+ entryDetails={entryDetails}
302
+ linkComponent={LinkComponent}
292
303
  />
293
304
 
294
305
  {venue && (
@@ -675,7 +686,14 @@ export function EventDetailPanelSkeleton({
675
686
 
676
687
  export type EventDetailChipRowProps = Pick<
677
688
  EventDetailPanelProps,
678
- "event" | "formatters" | "status" | "soonLabel" | "hype" | "tags"
689
+ | "event"
690
+ | "formatters"
691
+ | "status"
692
+ | "soonLabel"
693
+ | "hype"
694
+ | "tags"
695
+ | "entryDetails"
696
+ | "linkComponent"
679
697
  > & {
680
698
  /** Only the chip strings — a page rendering the row alone need not name the section headings. */
681
699
  labels: Pick<EventDetailPanelLabels, "free" | "liveNow" | "ended">;
@@ -683,7 +701,7 @@ export type EventDetailChipRowProps = Pick<
683
701
  };
684
702
 
685
703
  /**
686
- * One wrapping row, all 24px: status, fee, hype, then activity tags. Exported
704
+ * One wrapping row: metadata, entry information, then activity links. Exported
687
705
  * so the page renders the same row above its title — it is the panel's row,
688
706
  * not a second one.
689
707
  */
@@ -695,11 +713,25 @@ export function EventDetailChipRow({
695
713
  soonLabel,
696
714
  hype,
697
715
  tags,
716
+ entryDetails,
717
+ linkComponent: LinkComponent = DefaultLink,
698
718
  className,
699
719
  }: EventDetailChipRowProps) {
700
720
  const fee = resolveEventEntryFee(event);
721
+ const priceLabel =
722
+ fee.kind === "free"
723
+ ? labels.free
724
+ : fee.kind === "paid"
725
+ ? formatters.price(fee.amountRon)
726
+ : null;
727
+ const priceIcon =
728
+ fee.kind === "free" ? (
729
+ <Gift className="h-3.5 w-3.5" aria-hidden />
730
+ ) : (
731
+ <Ticket className="h-3.5 w-3.5" aria-hidden />
732
+ );
701
733
  return (
702
- <div className={cn("flex flex-wrap gap-[7px]", className)}>
734
+ <div className={cn("flex flex-wrap items-center gap-[7px]", className)}>
703
735
  {status === "live" && (
704
736
  <span
705
737
  className={cn(
@@ -732,7 +764,33 @@ export function EventDetailChipRow({
732
764
  {labels.ended}
733
765
  </span>
734
766
  )}
735
- {fee.kind === "free" && (
767
+ {priceLabel && entryDetails ? (
768
+ <Popover onOpenChange={entryDetails.onOpenChange}>
769
+ <PopoverTrigger asChild>
770
+ <Button
771
+ variant="outline"
772
+ size="sm"
773
+ className="min-h-11 rounded-full"
774
+ aria-label={entryDetails.label}
775
+ >
776
+ {priceIcon}
777
+ {priceLabel}
778
+ <ChevronRight className="h-3.5 w-3.5" aria-hidden />
779
+ </Button>
780
+ </PopoverTrigger>
781
+ <PopoverContent
782
+ align="start"
783
+ aria-label={entryDetails.label}
784
+ className="flex max-w-[calc(100vw-2rem)] flex-col gap-3"
785
+ >
786
+ <p className="text-sm font-semibold">{priceLabel}</p>
787
+ <p className="text-sm text-muted-foreground">
788
+ {entryDetails.description}
789
+ </p>
790
+ {entryDetails.action}
791
+ </PopoverContent>
792
+ </Popover>
793
+ ) : fee.kind === "free" ? (
736
794
  <span
737
795
  className={cn(
738
796
  CHIP,
@@ -742,19 +800,33 @@ export function EventDetailChipRow({
742
800
  <Gift className="h-3 w-3" aria-hidden />
743
801
  {labels.free}
744
802
  </span>
745
- )}
746
- {fee.kind === "paid" && (
803
+ ) : fee.kind === "paid" ? (
747
804
  <span className={cn(CHIP, "text-foreground")}>
748
805
  <Ticket className="h-3 w-3" aria-hidden />
749
806
  {formatters.price(fee.amountRon)}
750
807
  </span>
751
- )}
808
+ ) : null}
752
809
  {hype}
753
- {tags?.map((tag) => (
754
- <span key={tag} className={CHIP}>
755
- {tag}
756
- </span>
757
- ))}
810
+ {tags?.map((tag) =>
811
+ typeof tag === "string" ? (
812
+ <span key={tag} className="text-xs text-muted-foreground">
813
+ {tag}
814
+ </span>
815
+ ) : (
816
+ <Button
817
+ key={tag.href}
818
+ asChild
819
+ variant="outline"
820
+ size="sm"
821
+ className="min-h-11 rounded-full"
822
+ >
823
+ <LinkComponent href={tag.href} onClick={tag.onClick}>
824
+ {tag.label}
825
+ <ChevronRight className="h-3.5 w-3.5" aria-hidden />
826
+ </LinkComponent>
827
+ </Button>
828
+ ),
829
+ )}
758
830
  </div>
759
831
  );
760
832
  }
@@ -3,6 +3,8 @@
3
3
  import { type ComponentPropsWithoutRef, useId, useState } from "react";
4
4
  import { Check, ChevronDown, type LucideIcon } from "lucide-react";
5
5
  import { cn } from "../lib/utils";
6
+ import { FilterOptionGroup } from "./filter-option-group";
7
+ import { Button } from "./button";
6
8
  import { Skeleton } from "./skeleton";
7
9
 
8
10
  /**
@@ -101,6 +103,12 @@ export type FacetGroupProps = Omit<
101
103
  */
102
104
  name?: string;
103
105
  size?: FacetGroupSize;
106
+ /** Wrapping native-input chips for filter panels. Rows remain the default. */
107
+ layout?: "rows" | "chips";
108
+ /** Optional bounded chip list; selected options always stay visible. */
109
+ visibleLimit?: number;
110
+ showAllLabel?: string | ((count: number) => string);
111
+ showLessLabel?: string;
104
112
  /**
105
113
  * Turns the legend into a disclosure toggle for this group's options.
106
114
  *
@@ -135,6 +143,10 @@ export function FacetGroup({
135
143
  selection = "multiple",
136
144
  name,
137
145
  size = "comfortable",
146
+ layout = "rows",
147
+ visibleLimit,
148
+ showAllLabel,
149
+ showLessLabel,
138
150
  collapsible = false,
139
151
  defaultOpen = true,
140
152
  onToggle,
@@ -155,6 +167,52 @@ export function FacetGroup({
155
167
  const fallbackName = useId();
156
168
  const groupName = name ?? fallbackName;
157
169
 
170
+ if (layout === "chips") {
171
+ const value = options
172
+ .filter((option) => option.checked)
173
+ .map((option) => option.id);
174
+ return (
175
+ <FilterOptionGroup
176
+ title={title}
177
+ options={options.map((option) => ({ ...option, value: option.id }))}
178
+ value={value}
179
+ selection={selection}
180
+ name={groupName}
181
+ defaultOpen={defaultOpen}
182
+ collapsible={collapsible}
183
+ divider={false}
184
+ visibleLimit={visibleLimit}
185
+ showAllLabel={showAllLabel}
186
+ showLessLabel={showLessLabel}
187
+ disabled={rest.disabled}
188
+ className={className}
189
+ onValueChange={(next) => {
190
+ if (selection === "single") {
191
+ if (next[0] != null) onToggle?.(String(next[0]), true);
192
+ return;
193
+ }
194
+ for (const option of options) {
195
+ const checked = next.includes(option.id);
196
+ if (checked !== !!option.checked) onToggle?.(option.id, checked);
197
+ }
198
+ }}
199
+ footer={
200
+ moreLabel && (
201
+ <Button
202
+ type="button"
203
+ variant="link"
204
+ className="min-h-11 px-0"
205
+ onClick={onMore}
206
+ aria-expanded={moreExpanded}
207
+ >
208
+ {moreLabel}
209
+ </Button>
210
+ )
211
+ }
212
+ />
213
+ );
214
+ }
215
+
158
216
  return (
159
217
  // NO SEPARATOR OF ITS OWN. This drew `<Separator className="absolute
160
218
  // inset-x-0 top-0" />`, which looked like a rule above each group and was
@@ -172,7 +230,7 @@ export function FacetGroup({
172
230
  <legend
173
231
  className={cn(
174
232
  "block w-full p-0 text-[11.5px] font-extrabold tracking-[0.1em] text-muted-foreground uppercase",
175
- collapsible ? "mb-0" : "mb-[11px]"
233
+ collapsible ? "mb-0" : "mb-[11px]",
176
234
  )}
177
235
  >
178
236
  {collapsible ? (
@@ -197,22 +255,34 @@ export function FacetGroup({
197
255
  // groups looked like the rules were mis-centred — they were
198
256
  // centred, the labels were not. Transitioned alongside the panel
199
257
  // so the two move as one gesture rather than the margin snapping.
200
- open ? "mb-[11px]" : "mb-0"
258
+ open ? "mb-[11px]" : "mb-0",
201
259
  )}
202
260
  >
203
- {Icon && <Icon aria-hidden className="size-3.5 shrink-0" strokeWidth={2.25} />}
261
+ {Icon && (
262
+ <Icon
263
+ aria-hidden
264
+ className="size-3.5 shrink-0"
265
+ strokeWidth={2.25}
266
+ />
267
+ )}
204
268
  {title}
205
269
  <ChevronDown
206
270
  aria-hidden
207
271
  className={cn(
208
272
  "ml-auto size-3.5 shrink-0 transition-transform duration-150 motion-reduce:transition-none",
209
- open ? "rotate-180" : "rotate-0"
273
+ open ? "rotate-180" : "rotate-0",
210
274
  )}
211
275
  />
212
276
  </button>
213
277
  ) : (
214
278
  <span className="flex items-center gap-1.5">
215
- {Icon && <Icon aria-hidden className="size-3.5 shrink-0" strokeWidth={2.25} />}
279
+ {Icon && (
280
+ <Icon
281
+ aria-hidden
282
+ className="size-3.5 shrink-0"
283
+ strokeWidth={2.25}
284
+ />
285
+ )}
216
286
  {title}
217
287
  </span>
218
288
  )}
@@ -244,7 +314,7 @@ export function FacetGroup({
244
314
  <div
245
315
  className={cn(
246
316
  "grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
247
- open ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
317
+ open ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
248
318
  )}
249
319
  >
250
320
  <div className="overflow-hidden">
@@ -254,54 +324,63 @@ export function FacetGroup({
254
324
  aria-hidden={!open || undefined}
255
325
  className={cn("flex flex-col", ROW_GAP[size])}
256
326
  >
257
- {options.map((option) => {
258
- const checked = !!option.checked;
327
+ {options.map((option) => {
328
+ const checked = !!option.checked;
259
329
 
260
- return (
261
- <label
262
- key={option.id}
263
- className={cn(
264
- "flex cursor-pointer items-center gap-[9px] text-[13.5px] select-none",
265
- "transition-colors duration-150 hover:bg-muted/60 motion-reduce:transition-none",
266
- ROW_SIZE[size]
267
- )}
268
- >
269
- <input
270
- type={isSingle ? "radio" : "checkbox"}
271
- name={isSingle ? groupName : undefined}
272
- className="peer sr-only"
273
- checked={checked}
274
- onChange={(event) => onToggle?.(option.id, event.target.checked)}
275
- />
276
- <span
277
- aria-hidden
278
- className={cn(
279
- "flex h-4 w-4 shrink-0 items-center justify-center border-[1.5px] transition-colors duration-150 motion-reduce:transition-none",
280
- isSingle ? "rounded-full" : "rounded-[5px]",
281
- "peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2",
282
- checked ? "border-primary bg-primary" : "border-border bg-background"
283
- )}
284
- >
285
- {checked &&
286
- (isSingle ? (
287
- <span className="size-[7px] rounded-full bg-primary-foreground" />
288
- ) : (
289
- <Check className="h-[11px] w-[11px] text-primary-foreground" strokeWidth={3} />
290
- ))}
291
- </span>
292
- <span
293
- title={option.label}
294
- className={cn(
295
- "min-w-0 truncate",
296
- checked ? "font-bold text-primary" : "font-normal text-foreground"
297
- )}
298
- >
299
- {option.label}
300
- </span>
301
- <span className="ml-auto shrink-0 font-mono text-[11.5px] tabular-nums text-muted-foreground">
302
- {option.count}
303
- </span>
304
- </label>
330
+ return (
331
+ <label
332
+ key={option.id}
333
+ className={cn(
334
+ "flex cursor-pointer items-center gap-[9px] text-[13.5px] select-none",
335
+ "transition-colors duration-150 hover:bg-muted/60 motion-reduce:transition-none",
336
+ ROW_SIZE[size],
337
+ )}
338
+ >
339
+ <input
340
+ type={isSingle ? "radio" : "checkbox"}
341
+ name={isSingle ? groupName : undefined}
342
+ className="peer sr-only"
343
+ checked={checked}
344
+ onChange={(event) =>
345
+ onToggle?.(option.id, event.target.checked)
346
+ }
347
+ />
348
+ <span
349
+ aria-hidden
350
+ className={cn(
351
+ "flex h-4 w-4 shrink-0 items-center justify-center border-[1.5px] transition-colors duration-150 motion-reduce:transition-none",
352
+ isSingle ? "rounded-full" : "rounded-[5px]",
353
+ "peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2",
354
+ checked
355
+ ? "border-primary bg-primary"
356
+ : "border-border bg-background",
357
+ )}
358
+ >
359
+ {checked &&
360
+ (isSingle ? (
361
+ <span className="size-[7px] rounded-full bg-primary-foreground" />
362
+ ) : (
363
+ <Check
364
+ className="h-[11px] w-[11px] text-primary-foreground"
365
+ strokeWidth={3}
366
+ />
367
+ ))}
368
+ </span>
369
+ <span
370
+ title={option.label}
371
+ className={cn(
372
+ "min-w-0 truncate",
373
+ checked
374
+ ? "font-bold text-primary"
375
+ : "font-normal text-foreground",
376
+ )}
377
+ >
378
+ {option.label}
379
+ </span>
380
+ <span className="ml-auto shrink-0 font-mono text-[11.5px] tabular-nums text-muted-foreground">
381
+ {option.count}
382
+ </span>
383
+ </label>
305
384
  );
306
385
  })}
307
386
  </div>
@@ -329,6 +408,7 @@ export type FacetGroupSkeletonProps = {
329
408
  showMore?: boolean;
330
409
  /** Must match the `<FacetGroup>` it stands in for, or rows shift when data lands. */
331
410
  size?: FacetGroupSize;
411
+ layout?: "rows" | "chips";
332
412
  className?: string;
333
413
  };
334
414
 
@@ -338,7 +418,23 @@ export function FacetGroupSkeleton({
338
418
  showMore,
339
419
  size = "comfortable",
340
420
  className,
421
+ layout = "rows",
341
422
  }: FacetGroupSkeletonProps) {
423
+ if (layout === "chips")
424
+ return (
425
+ <div aria-hidden="true" className={cn("py-3", className)}>
426
+ <Skeleton className="mb-4 h-4 w-24" />
427
+ <div className="flex flex-wrap gap-2">
428
+ {Array.from({ length: optionCount }, (_, index) => (
429
+ <Skeleton
430
+ key={index}
431
+ className={cn("h-11 rounded-2xl", index % 2 ? "w-28" : "w-24")}
432
+ />
433
+ ))}
434
+ </div>
435
+ {showMore && <Skeleton className="mt-3 h-4 w-20" />}
436
+ </div>
437
+ );
342
438
  return (
343
439
  // Matches the live group: no rule of its own, `py-3`, and the row gap keyed
344
440
  // to the same density — or the panel shifts on load by the difference.
@@ -347,7 +443,10 @@ export function FacetGroupSkeleton({
347
443
 
348
444
  <div className={cn("flex flex-col", ROW_GAP[size])}>
349
445
  {Array.from({ length: optionCount }).map((_, index) => (
350
- <div key={index} className={cn("flex items-center gap-[9px]", ROW_SIZE[size])}>
446
+ <div
447
+ key={index}
448
+ className={cn("flex items-center gap-[9px]", ROW_SIZE[size])}
449
+ >
351
450
  <Skeleton className="h-4 w-4 rounded-[5px]" />
352
451
  <Skeleton className="h-3.5 w-24" />
353
452
  <Skeleton className="ml-auto h-3 w-5" />
@@ -0,0 +1,50 @@
1
+ import { useState } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it } from "vitest";
5
+ import { FilterOptionChip } from "./filter-option-chip";
6
+
7
+ describe("FilterOptionChip", () => {
8
+ it("keeps zero-count selections operable by keyboard", async () => {
9
+ function Example() {
10
+ const [checked, setChecked] = useState(true);
11
+ return (
12
+ <FilterOptionChip
13
+ label="Italiană"
14
+ count={0}
15
+ checked={checked}
16
+ onChange={(e) => setChecked(e.target.checked)}
17
+ />
18
+ );
19
+ }
20
+ const user = userEvent.setup();
21
+ render(<Example />);
22
+ const checkbox = screen.getByRole("checkbox", { name: "Italiană" });
23
+ expect(checkbox).toBeChecked();
24
+ await user.tab();
25
+ expect(checkbox).toHaveFocus();
26
+ await user.keyboard(" ");
27
+ expect(checkbox).not.toBeChecked();
28
+ });
29
+
30
+ it("keeps native single selection for radio groups", async () => {
31
+ const user = userEvent.setup();
32
+ render(
33
+ <fieldset>
34
+ <legend>Budget</legend>
35
+ <FilterOptionChip
36
+ type="radio"
37
+ name="budget"
38
+ label="Budget friendly"
39
+ defaultChecked
40
+ />
41
+ <FilterOptionChip type="radio" name="budget" label="Premium" />
42
+ </fieldset>,
43
+ );
44
+ await user.click(screen.getByRole("radio", { name: "Premium" }));
45
+ expect(screen.getByRole("radio", { name: "Premium" })).toBeChecked();
46
+ expect(
47
+ screen.getByRole("radio", { name: "Budget friendly" }),
48
+ ).not.toBeChecked();
49
+ });
50
+ });
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { type ComponentPropsWithoutRef, type ReactNode, useId } from "react";
4
+ import { Check } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ export type FilterOptionChipProps = Omit<
8
+ ComponentPropsWithoutRef<"input">,
9
+ "type" | "children" | "size"
10
+ > & {
11
+ label: string;
12
+ /** Radios in one group must share a name. */
13
+ type?: "checkbox" | "radio";
14
+ /** Omit while counts are unresolved. Zero is a valid, selectable count. */
15
+ count?: number | null;
16
+ icon?: ReactNode;
17
+ };
18
+
19
+ /** A wrapping filter option with native selection and keyboard semantics. */
20
+ export function FilterOptionChip({
21
+ label,
22
+ type = "checkbox",
23
+ count,
24
+ icon,
25
+ className,
26
+ id,
27
+ disabled,
28
+ ...input
29
+ }: FilterOptionChipProps) {
30
+ const generatedId = useId();
31
+ const inputId = id ?? generatedId;
32
+ return (
33
+ <label
34
+ htmlFor={inputId}
35
+ className={cn(
36
+ "relative inline-flex max-w-full min-w-0",
37
+ disabled && "cursor-not-allowed opacity-50",
38
+ className,
39
+ )}
40
+ >
41
+ <input
42
+ {...input}
43
+ id={inputId}
44
+ type={type}
45
+ disabled={disabled}
46
+ aria-label={input["aria-label"] ?? label}
47
+ className="peer sr-only"
48
+ />
49
+ <span
50
+ className={cn(
51
+ "flex min-h-11 min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-2xl border border-border bg-background px-3 py-2 text-sm text-foreground",
52
+ "transition-colors hover:bg-muted motion-reduce:transition-none",
53
+ "peer-checked:border-primary peer-checked:bg-primary/10 peer-checked:shadow-[inset_0_0_0_1px_var(--primary)] peer-checked:text-primary",
54
+ "peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring",
55
+ "peer-disabled:pointer-events-none peer-disabled:cursor-not-allowed",
56
+ "[&_.filter-check]:hidden peer-checked:[&_.filter-check]:block peer-checked:[&_.filter-icon]:hidden",
57
+ )}
58
+ >
59
+ <Check
60
+ aria-hidden
61
+ className="filter-check size-4 shrink-0"
62
+ strokeWidth={2.5}
63
+ />
64
+ {icon && (
65
+ <span aria-hidden className="filter-icon shrink-0 [&>svg]:size-4">
66
+ {icon}
67
+ </span>
68
+ )}
69
+ <span className="min-w-0 [overflow-wrap:anywhere]">{label}</span>
70
+ {count != null && (
71
+ <span
72
+ aria-hidden
73
+ className="shrink-0 text-xs tabular-nums text-muted-foreground"
74
+ >
75
+ {count}
76
+ </span>
77
+ )}
78
+ </span>
79
+ </label>
80
+ );
81
+ }