@stll/ui 0.44.1 → 0.46.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 (71) hide show
  1. package/README.md +2 -1
  2. package/dist/components/accordion.d.ts +6 -2
  3. package/dist/components/accordion.js +4 -4
  4. package/dist/components/ai-label.d.ts +33 -0
  5. package/dist/components/ai-label.js +42 -0
  6. package/dist/components/alert-dialog.js +3 -2
  7. package/dist/components/animated-count.d.ts +13 -0
  8. package/dist/components/animated-count.js +63 -0
  9. package/dist/components/application-rail.js +1 -1
  10. package/dist/components/brand-icons.d.ts +1 -0
  11. package/dist/components/brand-icons.js +8 -1
  12. package/dist/components/button.d.ts +2 -2
  13. package/dist/components/button.js +12 -2
  14. package/dist/components/command.js +1 -1
  15. package/dist/components/court-badge.d.ts +7 -1
  16. package/dist/components/court-badge.js +13 -3
  17. package/dist/components/date-picker-popover.d.ts +12 -2
  18. package/dist/components/date-picker-popover.js +105 -9
  19. package/dist/components/date-range-filter.d.ts +14 -0
  20. package/dist/components/date-range-filter.js +74 -0
  21. package/dist/components/dialog.js +3 -2
  22. package/dist/components/document-identity-badge.d.ts +12 -0
  23. package/dist/components/document-identity-badge.js +94 -0
  24. package/dist/components/document-identity-badge.logic.d.ts +25 -0
  25. package/dist/components/document-identity-badge.logic.js +25 -0
  26. package/dist/components/input-group.d.ts +1 -1
  27. package/dist/components/input-otp.js +1 -1
  28. package/dist/components/landing.d.ts +1 -1
  29. package/dist/components/landing.js +2 -2
  30. package/dist/components/list.d.ts +8 -2
  31. package/dist/components/list.js +10 -4
  32. package/dist/components/matter-icon.d.ts +16 -0
  33. package/dist/components/matter-icon.js +56 -0
  34. package/dist/components/menu.d.ts +9 -1
  35. package/dist/components/menu.js +6 -6
  36. package/dist/components/outline-rail.d.ts +8 -1
  37. package/dist/components/outline-rail.js +95 -48
  38. package/dist/components/popover.js +1 -1
  39. package/dist/components/search-field.js +30 -34
  40. package/dist/components/select.js +1 -1
  41. package/dist/components/sheet.js +22 -4
  42. package/dist/components/sidebar.d.ts +1 -1
  43. package/dist/components/sidebar.js +8 -8
  44. package/dist/components/status-mark.d.ts +17 -0
  45. package/dist/components/status-mark.js +74 -0
  46. package/dist/components/swap-text.d.ts +16 -0
  47. package/dist/components/swap-text.js +52 -0
  48. package/dist/components/switch.d.ts +5 -0
  49. package/dist/components/switch.js +16 -0
  50. package/dist/components/table-chart.d.ts +20 -0
  51. package/dist/components/table-chart.js +274 -0
  52. package/dist/components/tabs.js +1 -1
  53. package/dist/components/toast.js +42 -25
  54. package/dist/components/workspace-shell.js +5 -4
  55. package/dist/icons.js +1 -2
  56. package/dist/index.d.ts +3 -3
  57. package/dist/index.js +3 -3
  58. package/dist/inspector/dock.js +1 -1
  59. package/dist/inspector/entity-tab.js +1 -2
  60. package/dist/kanban/virtual-cell.js +1 -1
  61. package/dist/lib/control-size.d.ts +1 -1
  62. package/dist/lib/matter-colors.d.ts +38 -0
  63. package/dist/lib/matter-colors.js +65 -0
  64. package/dist/lib/panel-inset.d.ts +11 -0
  65. package/dist/lib/panel-inset.js +33 -0
  66. package/dist/lib/safe-area.d.ts +11 -0
  67. package/dist/lib/safe-area.js +12 -0
  68. package/dist/review/review-word-diff.d.ts +13 -0
  69. package/dist/review/review-word-diff.js +78 -0
  70. package/dist/styles/theme.css +79 -0
  71. package/package.json +64 -8
package/README.md CHANGED
@@ -213,7 +213,8 @@ column and keep their trailing column, so they never scroll sideways.
213
213
  <ListGroupHeader>
214
214
  <ListGroupHeading>
215
215
  <ListGroupTitle id="apps">
216
- Apps with access<ListGroupCount>3</ListGroupCount>
216
+ Apps with access
217
+ <ListGroupCount count="3" />
217
218
  </ListGroupTitle>
218
219
  <ListGroupDescription>Apps you authorized.</ListGroupDescription>
219
220
  </ListGroupHeading>
@@ -2,7 +2,11 @@ import { Accordion as Accordion$1 } from "@base-ui/react/accordion";
2
2
  //#region src/components/accordion.d.ts
3
3
  declare const Accordion: <Value = unknown>(props: Accordion$1.Root.Props<Value>) => import("react").JSX.Element;
4
4
  declare const AccordionItem: ({ className, ...props }: Accordion$1.Item.Props) => import("react").JSX.Element;
5
- declare const AccordionTrigger: ({ className, children, ...props }: Accordion$1.Trigger.Props) => import("react").JSX.Element;
6
- declare const AccordionPanel: ({ className, children, ...props }: Accordion$1.Panel.Props) => import("react").JSX.Element;
5
+ declare const AccordionTrigger: ({ className, children, size, ...props }: Accordion$1.Trigger.Props & {
6
+ size?: "default" | "compact";
7
+ }) => import("react").JSX.Element;
8
+ declare const AccordionPanel: ({ className, children, size, ...props }: Accordion$1.Panel.Props & {
9
+ size?: "default" | "compact";
10
+ }) => import("react").JSX.Element;
7
11
  //#endregion
8
12
  export { Accordion, AccordionPanel as AccordionContent, AccordionPanel, AccordionItem, AccordionTrigger };
@@ -13,10 +13,10 @@ const AccordionItem = ({ className, ...props }) => /* @__PURE__ */ jsx(Accordion
13
13
  "data-slot": "accordion-item",
14
14
  ...props
15
15
  });
16
- const AccordionTrigger = ({ className, children, ...props }) => /* @__PURE__ */ jsx(Accordion$1.Header, {
16
+ const AccordionTrigger = ({ className, children, size = "default", ...props }) => /* @__PURE__ */ jsx(Accordion$1.Header, {
17
17
  className: "flex",
18
18
  children: /* @__PURE__ */ jsxs(Accordion$1.Trigger, {
19
- className: cn("focus-visible:ring-ring flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180", className),
19
+ className: cn("focus-visible:ring-ring flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md text-start font-medium outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180", size === "compact" ? "py-2 text-xs" : "py-4 text-sm", className),
20
20
  "data-slot": "accordion-trigger",
21
21
  ...props,
22
22
  children: [children, /* @__PURE__ */ jsx(ChevronDownIcon, {
@@ -25,12 +25,12 @@ const AccordionTrigger = ({ className, children, ...props }) => /* @__PURE__ */
25
25
  })]
26
26
  })
27
27
  });
28
- const AccordionPanel = ({ className, children, ...props }) => /* @__PURE__ */ jsx(Accordion$1.Panel, {
28
+ const AccordionPanel = ({ className, children, size = "default", ...props }) => /* @__PURE__ */ jsx(Accordion$1.Panel, {
29
29
  className: "text-muted-foreground h-(--accordion-panel-height) overflow-hidden text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0",
30
30
  "data-slot": "accordion-panel",
31
31
  ...props,
32
32
  children: /* @__PURE__ */ jsx("div", {
33
- className: cn("pt-0 pb-4", className),
33
+ className: cn("pt-0", size === "compact" ? "pb-3" : "pb-4", className),
34
34
  children
35
35
  })
36
36
  });
@@ -0,0 +1,33 @@
1
+ //#region src/components/ai-label.d.ts
2
+ type AiLabelProps = {
3
+ /**
4
+ * The words the mark shows ("AI", "AI topics"), already translated. They
5
+ * are also the control's accessible name, so what a sighted reader sees is
6
+ * what a screen-reader user hears.
7
+ */
8
+ label: string;
9
+ /**
10
+ * What the reader is told about the content the mark stands beside, already
11
+ * translated. Hosts pass the sentence for one closed disclosure context
12
+ * (`@stll/api-contract/ai-disclosure`), never free text.
13
+ */
14
+ disclosure: string;
15
+ className?: string | undefined;
16
+ };
17
+ /**
18
+ * The mark on content a model wrote, and the one place a reader learns what
19
+ * that means.
20
+ *
21
+ * It keeps the weight of the plain label it replaces: the surrounding text's
22
+ * size, colour, case and tracking, a small sparkle, no chip and no tint. The
23
+ * only addition is that it is a button: a click, a tap, Enter or Space opens
24
+ * a small popover with the disclosure, and Escape or a click outside closes
25
+ * it. Nothing depends on hover.
26
+ *
27
+ * The trigger stays inline, so it can sit inside a heading or a line of
28
+ * text. Never inside another control (a `<summary>`, a link, a button): it is
29
+ * one itself, and nested interactive content is unreachable to assistive
30
+ * technology.
31
+ */
32
+ export declare const AiLabel: ({ label, disclosure, className }: AiLabelProps) => import("react").JSX.Element;
33
+ //#endregion
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { SparklesIcon } from "../icons.js";
3
+ import { cn } from "../lib/utils.js";
4
+ import { Popover, PopoverContent as PopoverPopup, PopoverDescription, PopoverTrigger } from "./popover.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/components/ai-label.tsx
7
+ /**
8
+ * The mark on content a model wrote, and the one place a reader learns what
9
+ * that means.
10
+ *
11
+ * It keeps the weight of the plain label it replaces: the surrounding text's
12
+ * size, colour, case and tracking, a small sparkle, no chip and no tint. The
13
+ * only addition is that it is a button: a click, a tap, Enter or Space opens
14
+ * a small popover with the disclosure, and Escape or a click outside closes
15
+ * it. Nothing depends on hover.
16
+ *
17
+ * The trigger stays inline, so it can sit inside a heading or a line of
18
+ * text. Never inside another control (a `<summary>`, a link, a button): it is
19
+ * one itself, and nested interactive content is unreachable to assistive
20
+ * technology.
21
+ */
22
+ const AiLabel = ({ label, disclosure, className }) => /* @__PURE__ */ jsxs(Popover, { children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
23
+ className: cn("focus-visible:ring-ring relative inline-flex cursor-pointer items-center justify-center gap-1 rounded-sm align-middle tracking-[inherit] text-inherit [text-transform:inherit] decoration-dotted underline-offset-2 outline-none [font:inherit] hover:underline focus-visible:ring-2 data-popup-open:underline pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11", className),
24
+ "data-slot": "ai-label",
25
+ type: "button",
26
+ children: [/* @__PURE__ */ jsx(SparklesIcon, {
27
+ "aria-hidden": true,
28
+ className: "size-3 shrink-0"
29
+ }), label]
30
+ }), /* @__PURE__ */ jsx(PopoverPopup, {
31
+ "aria-label": label,
32
+ className: "max-w-72",
33
+ "data-slot": "ai-label-popup",
34
+ padding: "sm",
35
+ side: "bottom",
36
+ children: /* @__PURE__ */ jsx(PopoverDescription, {
37
+ className: "text-xs leading-relaxed text-pretty",
38
+ children: disclosure
39
+ })
40
+ })] });
41
+ //#endregion
42
+ export { AiLabel };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { OVERLAY_LAYER_CLASS_NAMES } from "../lib/overlay-layer.js";
4
+ import { SAFE_AREA_OVERLAY_GUTTER_CLASS_NAME } from "../lib/safe-area.js";
4
5
  import { renderTooltipTrigger } from "./tooltip-trigger-helper.js";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  import { AlertDialog as AlertDialog$1 } from "@base-ui/react/alert-dialog";
@@ -24,7 +25,7 @@ const AlertDialogBackdrop = ({ className, layer = "default", ...props }) => /* @
24
25
  ...props
25
26
  });
26
27
  const AlertDialogViewport = ({ className, layer = "default", ...props }) => /* @__PURE__ */ jsx(AlertDialog$1.Viewport, {
27
- className: cn("fixed inset-0 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4", OVERLAY_LAYER_CLASS_NAMES[layer], className),
28
+ className: cn("fixed inset-0 grid grid-rows-[1fr_auto_3fr] justify-items-center", SAFE_AREA_OVERLAY_GUTTER_CLASS_NAME, OVERLAY_LAYER_CLASS_NAMES[layer], className),
28
29
  "data-slot": "alert-dialog-viewport",
29
30
  ...props
30
31
  });
@@ -32,7 +33,7 @@ const AlertDialogPopup = ({ className, bottomStickOnMobile = true, layer = "defa
32
33
  className: cn(bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12"),
33
34
  layer,
34
35
  children: /* @__PURE__ */ jsx(AlertDialog$1.Popup, {
35
- className: cn("bg-popover text-popover-foreground relative row-start-2 flex max-h-full min-h-0 w-full max-w-lg min-w-0 -translate-y-[calc(1.25rem*var(--nested-dialogs))] scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-nested:data-ending-style:translate-y-8 data-nested-dialog-open:origin-top data-starting-style:scale-98 data-starting-style:opacity-0 data-nested:data-starting-style:translate-y-8 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", bottomStickOnMobile && "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:before:hidden max-sm:before:rounded-none max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4", className),
36
+ className: cn("bg-popover text-popover-foreground relative row-start-2 flex max-h-full min-h-0 w-full max-w-lg min-w-0 -translate-y-[calc(1.25rem*var(--nested-dialogs))] scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-out will-change-transform not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-ending-style:duration-150 data-nested:data-ending-style:translate-y-8 data-nested-dialog-open:origin-top data-starting-style:scale-98 data-starting-style:opacity-0 data-nested:data-starting-style:translate-y-8 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", bottomStickOnMobile && "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:pb-(--safe-area-bottom) max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:before:hidden max-sm:before:rounded-none max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4", className),
36
37
  "data-slot": "alert-dialog-popup",
37
38
  ...props
38
39
  })
@@ -0,0 +1,13 @@
1
+ //#region src/components/animated-count.d.ts
2
+ /**
3
+ * A formatted number whose changed characters fade in. The caller formats
4
+ * (locale digits, units); the glyphs stay plain inline text so bidirectional
5
+ * ordering is the browser's, and only opacity moves. The first render is
6
+ * still: a count that loads does not announce itself.
7
+ */
8
+ declare const AnimatedCount: ({ className, value }: {
9
+ className?: string;
10
+ value: string;
11
+ }) => import("react").JSX.Element;
12
+ //#endregion
13
+ export { AnimatedCount };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { cn } from "../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { useState } from "react";
5
+ //#region src/components/animated-count.tsx
6
+ /**
7
+ * Aligns characters from the end, where digits keep their place value, so
8
+ * 99 → 100 changes every glyph but 120 → 121 only the last one. An unchanged
9
+ * glyph keeps its key and therefore its DOM node: it never replays its fade.
10
+ */
11
+ const toGlyphs = (value, previous, generation) => {
12
+ const chars = Array.from(value);
13
+ return chars.map((char, index) => {
14
+ const fromEnd = chars.length - index;
15
+ const priorIndex = previous === null ? -1 : previous.length - fromEnd;
16
+ const prior = priorIndex >= 0 ? previous?.[priorIndex] : void 0;
17
+ if (prior?.char === char) return prior;
18
+ return {
19
+ char,
20
+ enters: previous !== null,
21
+ key: `${generation}:${fromEnd}`
22
+ };
23
+ });
24
+ };
25
+ /**
26
+ * A formatted number whose changed characters fade in. The caller formats
27
+ * (locale digits, units); the glyphs stay plain inline text so bidirectional
28
+ * ordering is the browser's, and only opacity moves. The first render is
29
+ * still: a count that loads does not announce itself.
30
+ */
31
+ const AnimatedCount = ({ className, value }) => {
32
+ const [state, setState] = useState(() => ({
33
+ generation: 0,
34
+ glyphs: toGlyphs(value, null, 0),
35
+ value
36
+ }));
37
+ let { glyphs } = state;
38
+ if (state.value !== value) {
39
+ const generation = state.generation + 1;
40
+ glyphs = toGlyphs(value, state.glyphs, generation);
41
+ setState({
42
+ generation,
43
+ glyphs,
44
+ value
45
+ });
46
+ }
47
+ return /* @__PURE__ */ jsxs("span", {
48
+ className: cn("tabular-nums", className),
49
+ "data-slot": "animated-count",
50
+ children: [/* @__PURE__ */ jsx("span", {
51
+ className: "sr-only",
52
+ children: value
53
+ }), /* @__PURE__ */ jsx("span", {
54
+ "aria-hidden": "true",
55
+ children: glyphs.map((glyph) => /* @__PURE__ */ jsx("span", {
56
+ className: cn(glyph.enters && "motion-safe:animate-text-fade"),
57
+ children: glyph.char
58
+ }, glyph.key))
59
+ })]
60
+ });
61
+ };
62
+ //#endregion
63
+ export { AnimatedCount };
@@ -22,7 +22,7 @@ const ApplicationRailHeader = ({ className, ...props }) => /* @__PURE__ */ jsx("
22
22
  ...props
23
23
  });
24
24
  const ApplicationRailContent = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
25
- className: cn("flex min-h-0 flex-1 [scrollbar-width:none] flex-col gap-2 overflow-x-hidden overflow-y-auto [&::-webkit-scrollbar]:hidden", className),
25
+ className: cn("flex min-h-0 flex-1 [scrollbar-width:none] flex-col gap-2 overflow-x-hidden overflow-y-auto overscroll-contain [&::-webkit-scrollbar]:hidden", className),
26
26
  "data-slot": "application-rail-content",
27
27
  ...props
28
28
  });
@@ -3,4 +3,5 @@ import * as React$1 from "react";
3
3
  type BrandIconProps = React$1.ComponentProps<"svg">;
4
4
  export declare const DiscordLogoIcon: (props: BrandIconProps) => React$1.JSX.Element;
5
5
  export declare const GitHubLogoIcon: (props: BrandIconProps) => React$1.JSX.Element;
6
+ export declare const XLogoIcon: (props: BrandIconProps) => React$1.JSX.Element;
6
7
  //#endregion
@@ -14,5 +14,12 @@ const GitHubLogoIcon = (props) => /* @__PURE__ */ jsx("svg", {
14
14
  ...props,
15
15
  children: /* @__PURE__ */ jsx("path", { d: "M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656" })
16
16
  });
17
+ const XLogoIcon = (props) => /* @__PURE__ */ jsx("svg", {
18
+ "aria-hidden": "true",
19
+ fill: "currentColor",
20
+ viewBox: "0 0 24 24",
21
+ ...props,
22
+ children: /* @__PURE__ */ jsx("path", { d: "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.64 7.584H.47l8.6-9.835L0 1.153h7.594l5.243 6.932 6.064-6.932Zm-1.29 19.49h2.039L6.486 3.24H4.298l13.313 17.403Z" })
23
+ });
17
24
  //#endregion
18
- export { DiscordLogoIcon, GitHubLogoIcon };
25
+ export { DiscordLogoIcon, GitHubLogoIcon, XLogoIcon };
@@ -13,8 +13,8 @@ import { VariantProps } from "class-variance-authority";
13
13
  */
14
14
  declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
15
15
  declare const buttonVariants: (props?: ({
16
- size?: "icon" | "sm" | "default" | "lg" | "row" | "chip" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "xl" | "xs" | null | undefined;
17
- variant?: "link" | "destructive" | "muted" | "secondary" | "default" | "destructive-outline" | "destructive-ghost" | "ghost" | "outline" | null | undefined;
16
+ size?: "default" | "row" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "sm" | "xl" | "xs" | null | undefined;
17
+ variant?: "link" | "default" | "destructive" | "muted" | "destructive-outline" | "destructive-ghost" | "ghost" | "outline" | "secondary" | null | undefined;
18
18
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
19
19
  type ButtonProps = {
20
20
  variant?: VariantProps<typeof buttonVariants>["variant"];
@@ -19,7 +19,17 @@ import { cva } from "class-variance-authority";
19
19
  * swaps the cursor so the control still reads as unavailable on hover.
20
20
  */
21
21
  const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
22
- 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 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", {
22
+ /**
23
+ * Press feedback: the button gives slightly under the pointer.
24
+ *
25
+ * Applied by `Button` only while it can act, so a disabled or loading button
26
+ * never appears to respond. `scale` is its own property in Tailwind v4, so it
27
+ * composes with a caller's `translate` (a vertically centred icon button) and
28
+ * the transition names only the transform family. Menu rows and link-styled
29
+ * buttons keep the treatment of their surface instead.
30
+ */
31
+ const buttonPressClass = "motion-safe:transition-transform motion-safe:duration-150 ease-out motion-safe:active:scale-[0.98]";
32
+ 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", {
23
33
  defaultVariants: {
24
34
  size: "default",
25
35
  variant: "default"
@@ -68,7 +78,7 @@ function Button({ className, variant, size, render, loading, children, disabled,
68
78
  size: "sm",
69
79
  variant: "decorative"
70
80
  }), children] }) : children,
71
- className: cn(buttonVariants({
81
+ className: cn(disposition === BUTTON_DISPOSITION.enabled && size !== "row" && variant !== "link" && buttonPressClass, buttonVariants({
72
82
  className,
73
83
  size,
74
84
  variant
@@ -66,7 +66,7 @@ const CommandEmpty = ({ className, ...props }) => /* @__PURE__ */ jsx(Autocomple
66
66
  ...props
67
67
  });
68
68
  const CommandList = ({ className, ...props }) => /* @__PURE__ */ jsx(Autocomplete.List, {
69
- className: cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className),
69
+ className: cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto overscroll-contain", className),
70
70
  "data-slot": "command-list",
71
71
  ...props
72
72
  });
@@ -9,10 +9,16 @@
9
9
  * carries meaning on its own anyway: the court's name stands beside it.
10
10
  */
11
11
  export type CourtBadgeWeight = "solid" | "tinted" | "outline" | "dashed";
12
+ /**
13
+ * `default` stands on its own beside a court's name; `inline` sits inside a
14
+ * line of small text (a list row's meta line) without making it taller.
15
+ */
16
+ export type CourtBadgeSize = "default" | "inline";
12
17
  type CourtBadgeProps = {
13
18
  /** The court's own abbreviation, in the language its name is in. */
14
19
  abbreviation: string;
15
20
  weight: CourtBadgeWeight;
21
+ size?: CourtBadgeSize | undefined;
16
22
  className?: string;
17
23
  };
18
24
  /**
@@ -23,5 +29,5 @@ type CourtBadgeProps = {
23
29
  * abbreviation is actually known. There is no placeholder: a court nobody
24
30
  * abbreviates gets no chip.
25
31
  */
26
- export declare const CourtBadge: ({ abbreviation, className, weight }: CourtBadgeProps) => import("react").JSX.Element;
32
+ export declare const CourtBadge: ({ abbreviation, className, size, weight }: CourtBadgeProps) => import("react").JSX.Element;
27
33
  //#endregion
@@ -1,7 +1,17 @@
1
1
  import { cn } from "../lib/utils.js";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  //#region src/components/court-badge.tsx
4
- const COURT_BADGE_BASE_CLASS = "inline-flex shrink-0 items-center rounded-sm border px-1 py-0.5 font-mono text-xs leading-4 font-semibold tracking-wide whitespace-nowrap";
4
+ const COURT_BADGE_BASE_CLASS = "inline-flex shrink-0 items-center rounded-sm border font-mono font-semibold tracking-wide whitespace-nowrap";
5
+ /**
6
+ * The line box runs a third taller than the type, because these capitals
7
+ * carry diacritics (Ú, Ř, Š) whose ink rises above cap height; a tighter box
8
+ * sets the acute against the pill's border. `inline` keeps that ratio at a
9
+ * smaller size and drops the padding, so the chip fits a 16px text line.
10
+ */
11
+ const SIZE_CLASS = {
12
+ default: "px-1 py-0.5 text-xs leading-4",
13
+ inline: "px-0.5 text-3xs leading-3.5"
14
+ };
5
15
  /**
6
16
  * Every weight keeps a border, because `--muted` is a 4% tint: a borderless
7
17
  * tinted chip on a sidebar or a muted row is a pill whose edge nobody can
@@ -31,8 +41,8 @@ const WEIGHT_CLASS = {
31
41
  * abbreviation is actually known. There is no placeholder: a court nobody
32
42
  * abbreviates gets no chip.
33
43
  */
34
- const CourtBadge = ({ abbreviation, className, weight }) => /* @__PURE__ */ jsx("span", {
35
- className: cn(COURT_BADGE_BASE_CLASS, WEIGHT_CLASS[weight], className),
44
+ const CourtBadge = ({ abbreviation, className, size = "default", weight }) => /* @__PURE__ */ jsx("span", {
45
+ className: cn(COURT_BADGE_BASE_CLASS, SIZE_CLASS[size], WEIGHT_CLASS[weight], className),
36
46
  "data-slot": "court-badge",
37
47
  dir: "ltr",
38
48
  children: abbreviation
@@ -41,6 +41,16 @@ type DatePickerPopoverProps = {
41
41
  placeholderLabel?: string;
42
42
  clearLabel?: string;
43
43
  defaultOpen?: boolean;
44
+ open?: boolean;
45
+ triggerRef?: React.Ref<HTMLButtonElement> | undefined;
46
+ /** Keep the trigger focused when opening a following range bound. */
47
+ focusTriggerOnOpen?: boolean;
48
+ /** Enables direct date entry, with host-localized validation. */
49
+ dateInputLabel?: string;
50
+ outOfRangeLabel?: string;
51
+ noFuture?: boolean;
52
+ /** Host-owned calendar date; defaults to the browser timezone. */
53
+ getToday?: () => string;
44
54
  onOpenChange?: (open: boolean) => void;
45
55
  /** Label for the "go to today" button. Auto-localized from the locale when omitted. */
46
56
  todayLabel?: string;
@@ -57,8 +67,8 @@ type DatePickerPopoverProps = {
57
67
  /** Label of the time input in `date-time` mode. */
58
68
  timeLabel?: string;
59
69
  /** Inclusive `YYYY-MM-DD` bounds; in `date-time` mode they bound the day. */
60
- minDate?: string;
61
- maxDate?: string;
70
+ minDate?: string | undefined;
71
+ maxDate?: string | undefined;
62
72
  isDateDisabled?: (date: string) => boolean;
63
73
  layer?: OverlayLayer;
64
74
  };
@@ -3,6 +3,7 @@ import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "../icons.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { Button } from "./button.js";
5
5
  import { DirectionalIcon } from "./directional-icon.js";
6
+ import { Input } from "./input.js";
6
7
  import { FIELD_TRIGGER_CLASS_NAME } from "../lib/field-trigger.js";
7
8
  import { getLocaleWeekInfo, getWeekendDays } from "../lib/week.js";
8
9
  import { DATE_PICKER_MODE, formatDateTimeValue, getHourOptions, getMinuteOptions, getTimeFieldNames, joinPickerTime, localDateFromTimestamp, millisecondsUntilNextLocalDate, parseDateTimeValue, resolveCalendarViewMonth, shiftCalendarDate, splitPickerTime } from "./date-picker-popover.logic.js";
@@ -10,7 +11,7 @@ import { Popover, PopoverContent as PopoverPopup, PopoverTrigger } from "./popov
10
11
  import { Select, SelectContent as SelectPopup, SelectItem, SelectTrigger, SelectValue } from "./select.js";
11
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
13
  import { useCallback, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
13
- import { panic } from "better-result";
14
+ import { Result, panic } from "better-result";
14
15
  import { Temporal } from "temporal-polyfill/full";
15
16
  //#region src/components/date-picker-popover.tsx
16
17
  const toISODate = (date) => date.toString();
@@ -57,7 +58,7 @@ const subscribeToLocalDate = (onStoreChange) => {
57
58
  if (localDateListeners.size === 0) stopLocalDateSubscription();
58
59
  };
59
60
  };
60
- const useHydrationSafeToday = () => useSyncExternalStore(subscribeToLocalDate, getLocalToday, () => HYDRATION_DATE);
61
+ const useHydrationSafeToday = (getToday) => useSyncExternalStore(subscribeToLocalDate, getToday, () => HYDRATION_DATE);
61
62
  const useHydrationSafeBrowserLocale = () => useSyncExternalStore(noopSubscribe, () => navigator.language || HYDRATION_LOCALE, () => HYDRATION_LOCALE);
62
63
  const getFirstDayOfWeek = (locale) => {
63
64
  const info = getLocaleWeekInfo(locale);
@@ -252,7 +253,7 @@ const PICKER_SIZE_CLASS_NAMES = {
252
253
  }
253
254
  };
254
255
  const DatePickerPopoverContent = (props) => {
255
- const { id, labelledBy, className, disabled, hideClear, size = "compact", variant = "inline", mode = DATE_PICKER_MODE.date, value: rawValue, onChange, locale, isOverdue = false, showIcon = true, placeholderLabel, clearLabel = "Clear date", defaultOpen = false, onOpenChange, todayLabel: todayLabelProp, overdueLabel, minDate, maxDate, isDateDisabled, layer = "default", today } = props;
256
+ const { id, labelledBy, className, disabled, hideClear, size = "compact", variant = "inline", mode = DATE_PICKER_MODE.date, value: rawValue, onChange, locale, isOverdue = false, showIcon = true, placeholderLabel, clearLabel = "Clear date", defaultOpen = false, open: controlledOpen, triggerRef, focusTriggerOnOpen = false, dateInputLabel, outOfRangeLabel, onOpenChange, todayLabel: todayLabelProp, overdueLabel, minDate, maxDate, isDateDisabled, layer = "default", today } = props;
256
257
  const selection = resolveSelection(mode, rawValue);
257
258
  const value = selection.date;
258
259
  const labels = resolvePopupLabels(props);
@@ -294,14 +295,19 @@ const DatePickerPopoverContent = (props) => {
294
295
  maxDate,
295
296
  isDateDisabled
296
297
  ]);
298
+ const dateDraft = useDatePickerDraft({
299
+ value,
300
+ isDayDisabled,
301
+ onChange
302
+ });
297
303
  const displayLabel = formatSelection(locale, selection) ?? placeholderLabel ?? "—";
298
304
  const selectDay = (date) => {
299
305
  switch (selection.mode) {
300
306
  case DATE_PICKER_MODE.date:
301
- onChange(date);
307
+ dateDraft.apply(date);
302
308
  return;
303
309
  case DATE_PICKER_MODE.dateTime:
304
- onChange(formatDateTimeValue({
310
+ dateDraft.apply(formatDateTimeValue({
305
311
  date,
306
312
  time: selection.time ?? "00:00"
307
313
  }));
@@ -311,7 +317,7 @@ const DatePickerPopoverContent = (props) => {
311
317
  };
312
318
  const handleTimeChange = (clock) => {
313
319
  if (value === "") return;
314
- onChange(formatDateTimeValue({
320
+ dateDraft.apply(formatDateTimeValue({
315
321
  date: value,
316
322
  time: joinPickerTime(clock)
317
323
  }));
@@ -443,6 +449,7 @@ const DatePickerPopoverContent = (props) => {
443
449
  })();
444
450
  return /* @__PURE__ */ jsxs(Popover, {
445
451
  defaultOpen,
452
+ open: controlledOpen,
446
453
  onOpenChange: handleOpenChange,
447
454
  children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
448
455
  disabled,
@@ -451,6 +458,7 @@ const DatePickerPopoverContent = (props) => {
451
458
  "aria-labelledby": labelledBy ? `${labelledBy} ${displayValueId}` : void 0,
452
459
  className: cn(TRIGGER_VARIANT_CLASS_NAMES[variant], triggerToneClassName, className),
453
460
  id,
461
+ ref: triggerRef,
454
462
  type: "button"
455
463
  }),
456
464
  children: [
@@ -466,7 +474,8 @@ const DatePickerPopoverContent = (props) => {
466
474
  })
467
475
  ]
468
476
  }), /* @__PURE__ */ jsx(PopoverPopup, {
469
- initialFocus: () => gridRef.current?.querySelector(`[data-date="${value || today}"]`) ?? gridRef.current,
477
+ onBlur: dateDraft.handleBlur,
478
+ initialFocus: focusTriggerOnOpen ? false : () => gridRef.current?.querySelector(`[data-date="${value || today}"]`) ?? gridRef.current,
470
479
  className: "data-starting-style:scale-100",
471
480
  layer,
472
481
  padding: "sm",
@@ -516,6 +525,7 @@ const DatePickerPopoverContent = (props) => {
516
525
  onClick: () => {
517
526
  if (!isUnavailable) selectDay(day.date);
518
527
  },
528
+ onFocus: () => setFocusedDate(day.date),
519
529
  role: "gridcell",
520
530
  tabIndex: isFocused ? 0 : -1,
521
531
  type: "button",
@@ -546,11 +556,21 @@ const DatePickerPopoverContent = (props) => {
546
556
  onChange: handleTimeChange,
547
557
  time: selection.time
548
558
  }),
559
+ /* @__PURE__ */ jsx(TypedDateField, {
560
+ mode: selection.mode,
561
+ label: dateInputLabel,
562
+ value: dateDraft.value,
563
+ invalid: dateDraft.invalid,
564
+ errorLabel: outOfRangeLabel,
565
+ onDraftChange: dateDraft.edit,
566
+ onCommit: dateDraft.submit
567
+ }),
549
568
  /* @__PURE__ */ jsxs("div", {
550
569
  className: "mt-1 flex items-center gap-1 border-t pt-1",
551
570
  children: [/* @__PURE__ */ jsx(Button, {
552
571
  className: "flex-1",
553
572
  onClick: () => {
573
+ dateDraft.discard();
554
574
  const todayDate = Temporal.PlainDate.from(today);
555
575
  setViewMonthOverride({
556
576
  month: todayDate.month - 1,
@@ -563,7 +583,7 @@ const DatePickerPopoverContent = (props) => {
563
583
  children: todayLabel
564
584
  }), value && !hideClear && /* @__PURE__ */ jsx(Button, {
565
585
  className: "flex-1",
566
- onClick: () => onChange(null),
586
+ onClick: () => dateDraft.apply(null),
567
587
  size: "xs",
568
588
  variant: "ghost",
569
589
  children: clearLabel
@@ -576,10 +596,12 @@ const DatePickerPopoverContent = (props) => {
576
596
  };
577
597
  const DatePickerPopover = (props) => {
578
598
  const browserLocale = useHydrationSafeBrowserLocale();
579
- const today = useHydrationSafeToday();
599
+ const today = useHydrationSafeToday(props.getToday ?? getLocalToday);
580
600
  const locale = props.locale ?? browserLocale;
601
+ const maxDate = props.noFuture && (!props.maxDate || props.maxDate > today) ? today : props.maxDate;
581
602
  return /* @__PURE__ */ jsx(DatePickerPopoverContent, {
582
603
  ...props,
604
+ maxDate,
583
605
  locale,
584
606
  today
585
607
  });
@@ -738,5 +760,79 @@ const YearGrid = ({ decadeBase, currentYear, cellClassName, onSelect, today }) =
738
760
  }, row[0]))
739
761
  });
740
762
  };
763
+ const useDatePickerDraft = ({ value, isDayDisabled, onChange }) => {
764
+ const [draft, setDraft] = useState(null);
765
+ const commitStatus = useRef("consumed");
766
+ const hasDraft = draft?.baseValue === value;
767
+ const inputValue = hasDraft ? draft.value : value;
768
+ const parsed = Result.try(() => Temporal.PlainDate.from(inputValue));
769
+ const invalid = hasDraft && inputValue !== "" && (parsed.isErr() || parsed.value.toString() !== inputValue || isDayDisabled(inputValue));
770
+ const discard = () => {
771
+ commitStatus.current = "consumed";
772
+ setDraft(null);
773
+ };
774
+ const apply = (next) => {
775
+ discard();
776
+ onChange(next);
777
+ };
778
+ const commit = () => {
779
+ if (commitStatus.current !== "pending" || invalid || inputValue === value) return;
780
+ apply(inputValue || null);
781
+ };
782
+ return {
783
+ value: inputValue,
784
+ invalid,
785
+ edit: (next) => {
786
+ commitStatus.current = "pending";
787
+ setDraft({
788
+ baseValue: value,
789
+ value: next
790
+ });
791
+ },
792
+ discard,
793
+ apply,
794
+ submit: () => {
795
+ if (!invalid) apply(inputValue || null);
796
+ },
797
+ handleBlur: (event) => {
798
+ if (event.currentTarget.contains(event.relatedTarget)) return;
799
+ commit();
800
+ }
801
+ };
802
+ };
803
+ const TypedDateField = ({ mode, label, value, invalid, errorLabel = "Choose a date within the allowed range.", onDraftChange, onCommit }) => {
804
+ const id = useId();
805
+ if (!label || mode !== DATE_PICKER_MODE.date) return null;
806
+ return /* @__PURE__ */ jsxs("div", {
807
+ className: "mt-2 flex flex-col gap-1",
808
+ children: [
809
+ /* @__PURE__ */ jsx("label", {
810
+ htmlFor: id,
811
+ className: "text-xs",
812
+ children: label
813
+ }),
814
+ /* @__PURE__ */ jsx(Input, {
815
+ id,
816
+ value,
817
+ placeholder: "YYYY-MM-DD",
818
+ dir: "ltr",
819
+ "aria-invalid": invalid,
820
+ "aria-describedby": invalid ? `${id}-error` : void 0,
821
+ onChange: (event) => onDraftChange(event.currentTarget.value),
822
+ onKeyDown: (event) => {
823
+ if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
824
+ event.preventDefault();
825
+ onCommit();
826
+ }
827
+ }),
828
+ invalid && /* @__PURE__ */ jsx("p", {
829
+ id: `${id}-error`,
830
+ role: "alert",
831
+ className: "text-destructive text-xs",
832
+ children: errorLabel
833
+ })
834
+ ]
835
+ });
836
+ };
741
837
  //#endregion
742
838
  export { DatePickerPopover };
@@ -0,0 +1,14 @@
1
+ import { DatePickerPopoverProps } from "./date-picker-popover.js";
2
+ //#region src/components/date-range-filter.d.ts
3
+ type DateRangeFilterProps = Omit<DatePickerPopoverProps, "value" | "onChange" | "minDate" | "open" | "defaultOpen" | "onOpenChange" | "triggerRef" | "focusTriggerOnOpen" | "id" | "labelledBy" | "mode"> & {
4
+ from: string | null;
5
+ to: string | null;
6
+ onFromChange: (value: string | null) => void;
7
+ onToChange: (value: string | null) => void;
8
+ fromLabel: string;
9
+ toLabel: string;
10
+ };
11
+ /** Both calendar bounds and the focus handoff belong to the range. */
12
+ declare const DateRangeFilter: ({ from, to, onFromChange, onToChange, fromLabel, toLabel, className, maxDate, ...pickerProps }: DateRangeFilterProps) => import("react").JSX.Element;
13
+ //#endregion
14
+ export { DateRangeFilter, type DateRangeFilterProps };