@stll/ui 0.61.0 → 0.64.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.
Files changed (38) hide show
  1. package/dist/calendar/day-calendar.logic.js +2 -2
  2. package/dist/components/accordion.js +1 -1
  3. package/dist/components/bidi-text.d.ts +7 -0
  4. package/dist/components/bidi-text.js +22 -9
  5. package/dist/components/button.d.ts +2 -1
  6. package/dist/components/button.js +57 -3
  7. package/dist/components/button.native-classes.js +5 -0
  8. package/dist/components/button.shared.d.ts +1 -1
  9. package/dist/components/button.shared.js +1 -0
  10. package/dist/components/checkbox.js +2 -0
  11. package/dist/components/court-filter.d.ts +42 -0
  12. package/dist/components/court-filter.js +210 -0
  13. package/dist/components/identity-text.js +4 -2
  14. package/dist/components/landing.js +1 -1
  15. package/dist/components/middle-truncate.d.ts +19 -0
  16. package/dist/components/middle-truncate.js +112 -0
  17. package/dist/components/middle-truncate.logic.d.ts +11 -0
  18. package/dist/components/middle-truncate.logic.js +63 -0
  19. package/dist/components/separator.d.ts +26 -1
  20. package/dist/components/separator.js +52 -1
  21. package/dist/components/sidebar.d.ts +8 -1
  22. package/dist/components/sidebar.js +21 -1
  23. package/dist/components/tabs.js +12 -1
  24. package/dist/components/text.js +1 -1
  25. package/dist/components/text.native.js +3 -11
  26. package/dist/components/text.shared.d.ts +0 -29
  27. package/dist/components/text.shared.js +3 -33
  28. package/dist/icons.d.ts +2 -2
  29. package/dist/icons.js +1 -1
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +4 -4
  32. package/dist/inspector/chrome.js +2 -1
  33. package/dist/inspector/facet-bar.js +14 -8
  34. package/dist/lib/inline-reorder.js +3 -3
  35. package/dist/lib/tab-order.d.ts +19 -1
  36. package/dist/lib/tab-order.js +12 -10
  37. package/dist/review/review-comment-card.js +1 -1
  38. package/package.json +14 -1
@@ -146,7 +146,7 @@ const slotAt = (ms, options) => rangeFromDrag({
146
146
  * member of a cluster shares the cluster's lane count, so their widths match.
147
147
  */
148
148
  const layoutOverlaps = (ranges) => {
149
- const sorted = ranges.toSorted((left, right) => left.startMs - right.startMs || right.endMs - left.endMs || compareIds(left.id, right.id));
149
+ const sorted = [...ranges].sort((left, right) => left.startMs - right.startMs || right.endMs - left.endMs || compareIds(left.id, right.id));
150
150
  const placements = /* @__PURE__ */ new Map();
151
151
  const seen = /* @__PURE__ */ new Set();
152
152
  let cluster = [];
@@ -186,7 +186,7 @@ const layoutOverlaps = (ranges) => {
186
186
  * an earlier entry already uses it.
187
187
  */
188
188
  const mergeShortSegments = (segments, { thresholdMs }) => {
189
- const sorted = segments.toSorted((left, right) => left.startMs - right.startMs || left.endMs - right.endMs || compareIds(left.id, right.id));
189
+ const sorted = [...segments].sort((left, right) => left.startMs - right.startMs || left.endMs - right.endMs || compareIds(left.id, right.id));
190
190
  const merged = [];
191
191
  const takenIds = new Set(segments.map(({ id }) => id));
192
192
  let run = [];
@@ -14,7 +14,7 @@ const AccordionItem = ({ className, ...props }) => /* @__PURE__ */ jsx(Accordion
14
14
  ...props
15
15
  });
16
16
  const TRIGGER_SIZE_CLASS_NAME = {
17
- compact: "py-2 text-xs",
17
+ compact: "py-1.5 text-xs",
18
18
  default: "py-4 text-sm",
19
19
  sm: "min-h-11 items-center py-2 text-sm"
20
20
  };
@@ -1,5 +1,12 @@
1
1
  import * as React$1 from "react";
2
2
  //#region src/components/bidi-text.d.ts
3
+ /**
4
+ * Isolation shields the text around a value, not the value itself: a
5
+ * directional control inside it still reorders its letters, so stored text
6
+ * loses those controls before it renders.
7
+ */
8
+ export declare const stripDirectionalControls: (text: string) => string;
9
+ export declare const withoutDirectionalControls: (children: React$1.ReactNode) => React$1.ReactNode;
3
10
  export declare const BidiText: (props: BidiTextProps) => React$1.JSX.Element;
4
11
  export declare const UserText: typeof BidiText;
5
12
  type BidiDirection = "auto" | "ltr" | "rtl";
@@ -2,37 +2,50 @@
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region src/components/bidi-text.tsx
5
+ /** Explicit embeddings, overrides and isolates; implicit marks stay. */
6
+ const DIRECTIONAL_CONTROLS = /[\u202A-\u202E\u2066-\u2069]/gu;
7
+ /**
8
+ * Isolation shields the text around a value, not the value itself: a
9
+ * directional control inside it still reorders its letters, so stored text
10
+ * loses those controls before it renders.
11
+ */
12
+ const stripDirectionalControls = (text) => text.replaceAll(DIRECTIONAL_CONTROLS, "");
13
+ const withoutDirectionalControls = (children) => typeof children === "string" ? stripDirectionalControls(children) : children;
5
14
  const BidiText = (props) => {
6
15
  if (props.as === "span") {
7
- const { as: _as, className, direction = "auto", ...rest } = props;
16
+ const { as: _as, children, className, direction = "auto", ...rest } = props;
8
17
  return /* @__PURE__ */ jsx("span", {
9
18
  className: cn("[unicode-bidi:isolate]", className),
10
19
  dir: direction,
11
- ...rest
20
+ ...rest,
21
+ children: withoutDirectionalControls(children)
12
22
  });
13
23
  }
14
24
  if (props.as === "div") {
15
- const { as: _as, className, direction = "auto", ...rest } = props;
25
+ const { as: _as, children, className, direction = "auto", ...rest } = props;
16
26
  return /* @__PURE__ */ jsx("div", {
17
27
  className: cn("[unicode-bidi:isolate]", className),
18
28
  dir: direction,
19
- ...rest
29
+ ...rest,
30
+ children: withoutDirectionalControls(children)
20
31
  });
21
32
  }
22
33
  if (props.as === "p") {
23
- const { as: _as, className, direction = "auto", ...rest } = props;
34
+ const { as: _as, children, className, direction = "auto", ...rest } = props;
24
35
  return /* @__PURE__ */ jsx("p", {
25
36
  className: cn("[unicode-bidi:isolate]", className),
26
37
  dir: direction,
27
- ...rest
38
+ ...rest,
39
+ children: withoutDirectionalControls(children)
28
40
  });
29
41
  }
30
- const { as: _as, direction = "auto", ...rest } = props;
42
+ const { as: _as, children, direction = "auto", ...rest } = props;
31
43
  return /* @__PURE__ */ jsx("bdi", {
32
44
  dir: direction,
33
- ...rest
45
+ ...rest,
46
+ children: withoutDirectionalControls(children)
34
47
  });
35
48
  };
36
49
  const UserText = BidiText;
37
50
  //#endregion
38
- export { BidiText, UserText };
51
+ export { BidiText, UserText, stripDirectionalControls, withoutDirectionalControls };
@@ -16,7 +16,7 @@ declare const BUTTON_LAYOUT_CLASS_NAMES: {
16
16
  readonly wrap: "h-auto min-h-11 items-start justify-start py-2 text-start whitespace-normal sm:h-auto";
17
17
  };
18
18
  declare const buttonVariants: (props?: ({
19
- size?: "chip" | "default" | "icon" | "icon-2xs" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "inline" | "lg" | "row" | "sm" | "xl" | "xs" | null | undefined;
19
+ size?: "chip" | "default" | "icon" | "icon-2xs" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "inline" | "lg" | "row" | "sm" | "touch" | "xl" | "xs" | null | undefined;
20
20
  layout?: "wrap" | null | undefined;
21
21
  variant?: "default" | "destructive" | "destructive-ghost" | "destructive-outline" | "ghost" | "link" | "muted" | "muted-destructive" | "outline" | "secondary" | null | undefined;
22
22
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -66,6 +66,7 @@ declare const BUTTON_SIZE_CONTENT: {
66
66
  readonly lg: "text";
67
67
  readonly row: "text";
68
68
  readonly sm: "text";
69
+ readonly touch: "text";
69
70
  readonly xl: "text";
70
71
  readonly xs: "text";
71
72
  };
@@ -6,6 +6,7 @@ import { MENU_ROW_CLASS_NAME } from "../lib/menu-row.js";
6
6
  import { BUTTON_PLAIN_VARIANT, BUTTON_VARIANT_TOKENS } from "./button.shared.js";
7
7
  import { Loader } from "./loader.js";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { Fragment as Fragment$1, isValidElement } from "react";
9
10
  import { mergeProps } from "@base-ui/react/merge-props";
10
11
  import { useRender } from "@base-ui/react/use-render";
11
12
  import { panic } from "better-result";
@@ -50,6 +51,7 @@ const BUTTON_SIZE_CLASS_NAMES = {
50
51
  lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
51
52
  row: `${MENU_ROW_CLASS_NAME} [&_svg]:mx-0`,
52
53
  sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] text-sm sm:h-7 sm:text-xs",
54
+ touch: "min-h-[44px] min-w-[44px] px-[calc(--spacing(3.5)-1px)]",
53
55
  xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
54
56
  xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
55
57
  };
@@ -65,7 +67,7 @@ const BUTTON_VARIANT_CLASS_NAMES = {
65
67
  outline: `${tokens("outline")} dark:bg-input/32 data-pressed:border-primary/48 data-pressed:bg-accent dark:data-pressed:bg-input/64 [:hover:not([data-pressed])]:bg-accent/50 dark:[:hover:not([data-pressed])]:bg-input/64 shadow-xs/5 not-dark:bg-clip-padding not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none`,
66
68
  secondary: `${tokens("secondary")} data-pressed:bg-background data-pressed:inset-ring-input active:bg-secondary/80 [:hover:not([data-pressed])]:bg-secondary/90 data-pressed:shadow-xs/5 data-pressed:inset-ring`
67
69
  };
68
- const buttonVariants = cva("focus-visible:ring-ring focus-visible:ring-offset-background relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg border text-base font-medium whitespace-nowrap outline-none select-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] focus-visible:ring-2 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-64 sm:text-sm pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4", {
70
+ const buttonVariants = cva("focus-visible:ring-ring focus-visible:ring-offset-background relative inline-flex max-w-full shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg border text-base font-medium whitespace-nowrap outline-none select-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] focus-visible:ring-2 focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-64 sm:text-sm pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:-mx-0.5 [&_svg]:shrink-0 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4", {
69
71
  compoundVariants: [{
70
72
  className: "[:hover:not([data-pressed])]:bg-transparent",
71
73
  size: "inline",
@@ -105,7 +107,7 @@ const buttonVariants = cva("focus-visible:ring-ring focus-visible:ring-offset-ba
105
107
  * inside the box: an outline paints above the content, so a card's own
106
108
  * surface cannot cover it the way it would cover an inset shadow.
107
109
  */
108
- const BUTTON_PLAIN_CLASS = "focus-visible:ring-ring focus-visible:ring-offset-background data-pressed:outline-primary/48 relative inline-block cursor-pointer text-start outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-default data-pressed:outline-2 data-pressed:-outline-offset-2 data-pressed:outline-solid";
110
+ const BUTTON_PLAIN_CLASS = "focus-visible:ring-ring focus-visible:ring-offset-background data-pressed:outline-primary/48 relative inline-block max-w-full cursor-pointer text-start outline-none focus-visible:ring-2 focus-visible:ring-offset-1 disabled:cursor-default data-pressed:outline-2 data-pressed:-outline-offset-2 data-pressed:outline-solid";
109
111
  /**
110
112
  * The outline a plain button's focus ring follows, matching its content:
111
113
  * `box` for a card, row or block of text (the container radius), `round` for
@@ -145,9 +147,57 @@ const BUTTON_SIZE_CONTENT = {
145
147
  lg: BUTTON_CONTENT.text,
146
148
  row: BUTTON_CONTENT.text,
147
149
  sm: BUTTON_CONTENT.text,
150
+ touch: BUTTON_CONTENT.text,
148
151
  xl: BUTTON_CONTENT.text,
149
152
  xs: BUTTON_CONTENT.text
150
153
  };
154
+ /**
155
+ * A button never grows past its container (`max-w-full` on every variant).
156
+ * A one-line text label is cut to fit with an ellipsis; the button's
157
+ * accessible name keeps the whole text, and `tooltip` can show it. The label
158
+ * is a plain span, not a `TruncatedText`: a tooltip root in every button's
159
+ * label would cost every screen with many buttons. `layout="wrap"` lets a
160
+ * label take several lines instead. An inline button is a word in a sentence
161
+ * and a plain button lays out its own content, so neither has a one-line
162
+ * label to cut.
163
+ */
164
+ const BUTTON_LABEL_CLASS = "block min-w-0 truncate";
165
+ const cutsLabel = ({ layout, size, variant }) => layout === void 0 && variant !== "plain" && variant !== "link" && size !== "inline" && BUTTON_SIZE_CONTENT[size ?? BUTTON_DEFAULT_SIZE] === BUTTON_CONTENT.text;
166
+ /**
167
+ * Each run of adjacent text among the children, cut to the button's width.
168
+ * Adjacent text (`Save {count} files`) stays one run, as it is one flex item
169
+ * in the button's box; children React renders as nothing (`null`, `false`)
170
+ * do not split it. Every other child keeps its place in a fragment keyed by
171
+ * its own key, or by its position when it has none, so keyed children keep
172
+ * their state when they move.
173
+ */
174
+ const containedLabel = (children) => {
175
+ const items = Array.isArray(children) ? children : [children];
176
+ const parts = [];
177
+ let run = [];
178
+ const closeRun = () => {
179
+ if (run.length > 0) {
180
+ parts.push(/* @__PURE__ */ jsx("span", {
181
+ className: BUTTON_LABEL_CLASS,
182
+ "data-slot": "button-label",
183
+ children: run
184
+ }, `label:${String(parts.length)}`));
185
+ run = [];
186
+ }
187
+ };
188
+ for (const child of items) {
189
+ if (child === null || child === void 0 || typeof child === "boolean") continue;
190
+ if (typeof child === "string" || typeof child === "number") {
191
+ run.push(child);
192
+ continue;
193
+ }
194
+ closeRun();
195
+ const key = isValidElement(child) && child.key !== null ? `key:${child.key}` : `part:${String(parts.length)}`;
196
+ parts.push(/* @__PURE__ */ jsx(Fragment$1, { children: child }, key));
197
+ }
198
+ closeRun();
199
+ return parts;
200
+ };
151
201
  const implicitTooltip = (size, accessibleName) => {
152
202
  const content = BUTTON_SIZE_CONTENT[size ?? BUTTON_DEFAULT_SIZE];
153
203
  switch (content) {
@@ -174,7 +224,11 @@ function Button({ className, variant: variantProp, size, render, layout, loading
174
224
  "data-slot": "button-loader",
175
225
  size: "sm",
176
226
  variant: "decorative"
177
- }) : icon, children] }),
227
+ }) : icon, cutsLabel({
228
+ layout,
229
+ size,
230
+ variant
231
+ }) ? containedLabel(children) : children] }),
178
232
  className: cn(variant === "plain" ? cn(BUTTON_PLAIN_CLASS, BUTTON_PLAIN_SHAPE_CLASS[shape], className) : cn(disposition === BUTTON_DISPOSITION.enabled && !isActivationPreservingDisabled && size !== "row" && size !== "inline" && variant !== "link" && buttonPressClass, buttonVariants({
179
233
  className,
180
234
  size,
@@ -68,6 +68,11 @@ const SIZES = {
68
68
  label: "text-sm",
69
69
  slop: 0
70
70
  },
71
+ touch: {
72
+ box: `${CONTROL} min-h-11 min-w-11 gap-2 rounded-lg px-4`,
73
+ label: "",
74
+ slop: 0
75
+ },
71
76
  xl: {
72
77
  box: `${CONTROL} min-h-14 gap-2 rounded-lg px-6`,
73
78
  label: "text-lg",
@@ -59,7 +59,7 @@ export type ButtonTokenVariant = keyof typeof BUTTON_VARIANT_TOKENS;
59
59
  export declare const BUTTON_PLAIN_VARIANT = "plain";
60
60
  export type ButtonVariant = ButtonTokenVariant | typeof BUTTON_PLAIN_VARIANT;
61
61
  /** Every size a button comes in, on every platform. */
62
- export declare const BUTTON_SIZES: readonly ["chip", "default", "icon", "icon-lg", "icon-sm", "icon-xl", "icon-2xs", "icon-xs", "inline", "lg", "row", "sm", "xl", "xs"];
62
+ export declare const BUTTON_SIZES: readonly ["chip", "default", "icon", "icon-lg", "icon-sm", "icon-xl", "icon-2xs", "icon-xs", "inline", "lg", "row", "sm", "touch", "xl", "xs"];
63
63
  export type ButtonSize = (typeof BUTTON_SIZES)[number];
64
64
  /**
65
65
  * The props every button renderer accepts, with one meaning. A renderer adds
@@ -68,6 +68,7 @@ const BUTTON_SIZES = [
68
68
  "lg",
69
69
  "row",
70
70
  "sm",
71
+ "touch",
71
72
  "xl",
72
73
  "xs"
73
74
  ];
@@ -13,6 +13,7 @@ const Checkbox = ({ className, ...props }) => /* @__PURE__ */ jsx(Checkbox$1.Roo
13
13
  render: (indicatorProps, state) => /* @__PURE__ */ jsx("span", {
14
14
  ...indicatorProps,
15
15
  children: state.indeterminate ? /* @__PURE__ */ jsxs("svg", {
16
+ "aria-hidden": "true",
16
17
  className: "size-3.5 sm:size-3",
17
18
  fill: "none",
18
19
  height: "24",
@@ -25,6 +26,7 @@ const Checkbox = ({ className, ...props }) => /* @__PURE__ */ jsx(Checkbox$1.Roo
25
26
  xmlns: "http://www.w3.org/2000/svg",
26
27
  children: [/* @__PURE__ */ jsx("title", { children: "Indeterminate" }), /* @__PURE__ */ jsx("path", { d: "M5.252 12h13.496" })]
27
28
  }) : /* @__PURE__ */ jsxs("svg", {
29
+ "aria-hidden": "true",
28
30
  className: "size-3.5 sm:size-3",
29
31
  fill: "none",
30
32
  height: "24",
@@ -0,0 +1,42 @@
1
+ //#region src/components/court-filter.d.ts
2
+ type CourtOption = {
3
+ value: string;
4
+ label?: string;
5
+ badgeText?: string;
6
+ };
7
+ type CourtFilterGroup = {
8
+ value: string;
9
+ label: string | null;
10
+ courts: readonly CourtOption[];
11
+ };
12
+ type CourtFilterBaseProps = {
13
+ groups: readonly CourtFilterGroup[];
14
+ label: string;
15
+ };
16
+ type SingleCourtFilterProps = CourtFilterBaseProps & {
17
+ selected: string | undefined;
18
+ onChange: (value: string | undefined) => void;
19
+ collapsedGroups: readonly string[];
20
+ visibleRowLimit: number;
21
+ showAllLabel: string;
22
+ };
23
+ type MultipleCourtFilterProps = CourtFilterBaseProps & {
24
+ selected: readonly string[];
25
+ onChange: (values: string[]) => void;
26
+ maxSelections: number;
27
+ selectionHint: string;
28
+ searchLabel: string;
29
+ clearLabel: string;
30
+ emptyLabel: string;
31
+ matchText: (value: string) => string;
32
+ };
33
+ export declare const toggleCourtSelection: (selected: readonly string[], values: readonly string[]) => string[];
34
+ /**
35
+ * Court filtering shares an owner while each surface keeps its interaction
36
+ * model. Each model is its own export, so a bundle carries only the one it
37
+ * renders.
38
+ */
39
+ export declare const MultipleCourtFilter: (options: MultipleCourtFilterProps) => import("react").JSX.Element;
40
+ export declare const SingleCourtFilter: (props: SingleCourtFilterProps) => import("react").JSX.Element;
41
+ //#endregion
42
+ export {};
@@ -0,0 +1,210 @@
1
+ "use client";
2
+ import { ChevronRightIcon } from "../icons.js";
3
+ import { cn } from "../lib/utils.js";
4
+ import { BidiText } from "./bidi-text.js";
5
+ import { ScrollArea } from "./scroll-area.js";
6
+ import { DirectionalIcon } from "./directional-icon.js";
7
+ import { Checkbox } from "./checkbox.js";
8
+ import { Field, FieldLabel } from "./field.js";
9
+ import { SearchField } from "./search-field.js";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useId, useState } from "react";
12
+ //#region src/components/court-filter.tsx
13
+ const toggleCourtSelection = (selected, values) => {
14
+ return values.every((value) => selected.includes(value)) ? selected.filter((value) => !values.includes(value)) : [.../* @__PURE__ */ new Set([...selected, ...values])];
15
+ };
16
+ const SingleCourtGroup = ({ group, options }) => {
17
+ const panelId = useId();
18
+ const [open, setOpen] = useState(!options.collapsedGroups.includes(group.value));
19
+ const [showAll, setShowAll] = useState(false);
20
+ const selectedHere = group.courts.find(({ value }) => value === options.selected);
21
+ const expanded = open || selectedHere !== void 0;
22
+ const items = showAll ? [...group.courts] : group.courts.slice(0, options.visibleRowLimit);
23
+ if (selectedHere !== void 0 && !items.includes(selectedHere)) items.push(selectedHere);
24
+ const hiddenCount = group.courts.length - items.length;
25
+ if (items.length === 0) return null;
26
+ return /* @__PURE__ */ jsxs("div", { children: [group.label !== null && /* @__PURE__ */ jsxs("button", {
27
+ type: "button",
28
+ "aria-controls": panelId,
29
+ "aria-expanded": expanded,
30
+ className: "text-foreground-strong-muted hover:text-foreground flex min-h-11 w-full items-center gap-1 py-1 text-start text-xs font-medium",
31
+ onClick: () => setOpen(!expanded),
32
+ children: [/* @__PURE__ */ jsx(DirectionalIcon, {
33
+ className: cn("size-3 shrink-0 transition-transform", expanded && "rotate-90"),
34
+ flip: !expanded,
35
+ icon: ChevronRightIcon
36
+ }), group.label]
37
+ }), /* @__PURE__ */ jsxs("div", {
38
+ hidden: group.label !== null && !expanded,
39
+ id: panelId,
40
+ children: [/* @__PURE__ */ jsx("ul", {
41
+ "aria-label": group.label ?? options.label,
42
+ className: "flex flex-col",
43
+ children: items.map(({ value, label, badgeText }) => {
44
+ const checked = options.selected === value;
45
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
46
+ className: "hover:bg-muted/60 has-[:focus-visible]:ring-ring flex min-h-11 cursor-pointer items-center gap-2 rounded-sm py-1.5 ps-1 pe-1 text-xs has-[:focus-visible]:ring-2",
47
+ children: [
48
+ /* @__PURE__ */ jsx("input", {
49
+ type: "radio",
50
+ name: `court-${panelId}`,
51
+ value,
52
+ checked,
53
+ className: "accent-primary size-3.5 shrink-0",
54
+ onChange: () => options.onChange(value),
55
+ onClick: () => {
56
+ if (checked) options.onChange(void 0);
57
+ }
58
+ }),
59
+ /* @__PURE__ */ jsx(BidiText, {
60
+ as: "span",
61
+ className: cn("text-foreground min-w-0 flex-1 truncate", checked && "font-medium"),
62
+ title: label ?? value,
63
+ children: label ?? value
64
+ }),
65
+ badgeText !== void 0 && /* @__PURE__ */ jsx("bdi", {
66
+ className: "text-muted-foreground text-2xs shrink-0 tabular-nums",
67
+ children: badgeText
68
+ })
69
+ ]
70
+ }) }, value);
71
+ })
72
+ }), hiddenCount > 0 && /* @__PURE__ */ jsx("button", {
73
+ type: "button",
74
+ className: "text-muted-foreground hover:text-foreground -mx-1 min-h-11 px-1 py-1.5 text-xs",
75
+ onClick: () => setShowAll(true),
76
+ children: options.showAllLabel
77
+ })]
78
+ })] });
79
+ };
80
+ const MultipleCourtGroup = ({ group, courts, options, hintId }) => {
81
+ const labelId = useId();
82
+ const { selected } = options;
83
+ const values = courts.map(({ value }) => value);
84
+ const tierSelected = values.every((value) => selected.includes(value));
85
+ const nextTier = toggleCourtSelection(selected, values);
86
+ const tierDisabled = !tierSelected && nextTier.length > options.maxSelections;
87
+ return /* @__PURE__ */ jsxs("fieldset", {
88
+ className: "min-w-0",
89
+ children: [
90
+ /* @__PURE__ */ jsx("legend", {
91
+ className: "sr-only",
92
+ children: group.label ?? options.label
93
+ }),
94
+ group.label !== null && /* @__PURE__ */ jsxs("label", {
95
+ id: `${labelId}-tier`,
96
+ className: "flex min-h-11 cursor-pointer items-center gap-2 text-xs font-medium",
97
+ children: [/* @__PURE__ */ jsx(Checkbox, {
98
+ "aria-labelledby": `${labelId}-tier`,
99
+ checked: tierSelected,
100
+ indeterminate: !tierSelected && values.some((value) => selected.includes(value)),
101
+ disabled: tierDisabled,
102
+ "aria-describedby": hintId,
103
+ onCheckedChange: () => {
104
+ if (tierSelected || nextTier.length <= options.maxSelections) options.onChange(nextTier);
105
+ }
106
+ }), group.label]
107
+ }),
108
+ /* @__PURE__ */ jsx("ul", {
109
+ className: "flex flex-col",
110
+ children: courts.map(({ value, label, badgeText }, index) => {
111
+ const checked = selected.includes(value);
112
+ const next = toggleCourtSelection(selected, [value]);
113
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
114
+ id: `${labelId}-${index}`,
115
+ className: "hover:bg-muted/60 has-[:focus-visible]:ring-ring flex min-h-11 cursor-pointer items-center gap-2 rounded-sm text-sm has-[:focus-visible]:ring-2",
116
+ children: [
117
+ /* @__PURE__ */ jsx(Checkbox, {
118
+ "aria-labelledby": `${labelId}-${index}`,
119
+ checked,
120
+ disabled: !checked && next.length > options.maxSelections,
121
+ "aria-describedby": hintId,
122
+ onCheckedChange: () => {
123
+ if (checked || next.length <= options.maxSelections) options.onChange(next);
124
+ }
125
+ }),
126
+ /* @__PURE__ */ jsx("bdi", {
127
+ className: "min-w-0 flex-1 wrap-break-word",
128
+ children: label ?? value
129
+ }),
130
+ badgeText !== void 0 && /* @__PURE__ */ jsx("bdi", {
131
+ className: "text-muted-foreground text-xs tabular-nums",
132
+ children: badgeText
133
+ })
134
+ ]
135
+ }) }, value);
136
+ })
137
+ })
138
+ ]
139
+ });
140
+ };
141
+ /**
142
+ * Court filtering shares an owner while each surface keeps its interaction
143
+ * model. Each model is its own export, so a bundle carries only the one it
144
+ * renders.
145
+ */
146
+ const MultipleCourtFilter = (options) => {
147
+ const [query, setQuery] = useState("");
148
+ const hintId = useId();
149
+ const needle = options.matchText(query);
150
+ const missing = options.selected.filter((name) => !options.groups.some((group) => group.courts.some(({ value }) => value === name)));
151
+ const groups = [...options.groups];
152
+ if (missing.length > 0) groups.push({
153
+ value: "selected",
154
+ label: null,
155
+ courts: missing.map((value) => ({ value }))
156
+ });
157
+ const visible = groups.flatMap((group) => {
158
+ const courts = options.matchText(group.label ?? "").includes(needle) ? group.courts : group.courts.filter(({ value, label }) => options.matchText(label ?? value).includes(needle));
159
+ return courts.length === 0 ? [] : [{
160
+ group,
161
+ courts
162
+ }];
163
+ });
164
+ return /* @__PURE__ */ jsxs("div", {
165
+ className: "min-w-0 space-y-2",
166
+ children: [
167
+ /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
168
+ className: "sr-only",
169
+ children: options.searchLabel
170
+ }), /* @__PURE__ */ jsx(SearchField, {
171
+ value: query,
172
+ onValueChange: setQuery,
173
+ clearLabel: options.clearLabel,
174
+ placeholder: options.searchLabel
175
+ })] }),
176
+ /* @__PURE__ */ jsx("p", {
177
+ id: hintId,
178
+ role: "status",
179
+ className: "text-muted-foreground text-xs",
180
+ children: options.selectionHint
181
+ }),
182
+ /* @__PURE__ */ jsx(ScrollArea, {
183
+ className: "h-72",
184
+ axis: "vertical",
185
+ "aria-label": options.label,
186
+ children: /* @__PURE__ */ jsxs("div", {
187
+ className: "space-y-2",
188
+ children: [visible.length === 0 && /* @__PURE__ */ jsx("p", {
189
+ className: "text-muted-foreground py-2 text-sm",
190
+ children: options.emptyLabel
191
+ }), visible.map(({ group, courts }) => /* @__PURE__ */ jsx(MultipleCourtGroup, {
192
+ group,
193
+ courts,
194
+ options,
195
+ hintId
196
+ }, group.value))]
197
+ })
198
+ })
199
+ ]
200
+ });
201
+ };
202
+ const SingleCourtFilter = (props) => /* @__PURE__ */ jsx("div", {
203
+ className: "flex flex-col gap-3",
204
+ children: props.groups.map((group) => /* @__PURE__ */ jsx(SingleCourtGroup, {
205
+ group,
206
+ options: props
207
+ }, group.value))
208
+ });
209
+ //#endregion
210
+ export { MultipleCourtFilter, SingleCourtFilter, toggleCourtSelection };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
4
+ import { withoutDirectionalControls } from "./bidi-text.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  import { useCallback, useId, useState } from "react";
6
7
  //#region src/components/identity-text.tsx
@@ -10,6 +11,7 @@ const IdentityText = ({ children, className }) => {
10
11
  const [overflow, setOverflow] = useState(false);
11
12
  const [hasFocusOwner, setHasFocusOwner] = useState(false);
12
13
  const [open, setOpen] = useState(false);
14
+ const name = withoutDirectionalControls(children);
13
15
  const textRef = useCallback((node) => {
14
16
  if (node === null) return;
15
17
  const measure = () => setOverflow(node.scrollWidth > node.clientWidth);
@@ -56,14 +58,14 @@ const IdentityText = ({ children, className }) => {
56
58
  role: overflow && !hasFocusOwner ? "button" : void 0,
57
59
  tabIndex: overflow && !hasFocusOwner ? 0 : void 0
58
60
  }),
59
- children
61
+ children: name
60
62
  }), /* @__PURE__ */ jsx(TooltipPopup, {
61
63
  className: "max-w-sm",
62
64
  role: "tooltip",
63
65
  children: /* @__PURE__ */ jsx("span", {
64
66
  className: "block max-w-sm wrap-break-word",
65
67
  dir: "auto",
66
- children
68
+ children: name
67
69
  })
68
70
  })]
69
71
  });
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
- import { ScrollArea } from "./scroll-area.js";
4
3
  import { BidiText } from "./bidi-text.js";
4
+ import { ScrollArea } from "./scroll-area.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/components/landing.tsx
7
7
  const LandingLayout = ({ actions, hero, children, footer }) => /* @__PURE__ */ jsxs("div", {
@@ -0,0 +1,19 @@
1
+ //#region src/components/middle-truncate.d.ts
2
+ type MiddleTruncateProps = {
3
+ value: string;
4
+ className?: string;
5
+ };
6
+ /**
7
+ * One line of text that, when it does not fit, gives up its middle and keeps
8
+ * its end: `Smlouva o dílo – revize…klienta.docx`. For names whose end tells
9
+ * them apart (file names, versions, case numbers), where a trailing ellipsis
10
+ * would cut exactly that part.
11
+ *
12
+ * The head clips after its last whole character and the tail slides over the
13
+ * slack, so the ellipsis sits flush against the text instead of after a gap
14
+ * or half a glyph. The ellipsis is generated content with empty alternative
15
+ * text: the element's text and accessible name stay the full value.
16
+ */
17
+ export declare const MiddleTruncate: ({ value, className }: MiddleTruncateProps) => import("react").JSX.Element;
18
+ //#endregion
19
+ export {};