@vamos-ui/react 0.2.0 → 0.3.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 (82) hide show
  1. package/dist/blocks/site/_kit-1.d.ts +1 -0
  2. package/dist/blocks/site/_kit-1.js +2 -0
  3. package/dist/blocks/site/cta.js +2 -1
  4. package/dist/blocks/site/faq.js +2 -1
  5. package/dist/blocks/site/feature-grid.js +2 -1
  6. package/dist/blocks/site/gallery-section.js +3 -2
  7. package/dist/blocks/site/hero.js +12 -6
  8. package/dist/blocks/site/logo-strip.d.ts +5 -3
  9. package/dist/blocks/site/logo-strip.js +8 -7
  10. package/dist/blocks/site/media-text.js +2 -1
  11. package/dist/blocks/site/pricing-table.js +2 -1
  12. package/dist/blocks/site/process.js +2 -1
  13. package/dist/blocks/site/site-shell.d.ts +11 -0
  14. package/dist/blocks/site/site-shell.js +43 -16
  15. package/dist/blocks/site/stats-band.d.ts +3 -1
  16. package/dist/blocks/site/stats-band.js +11 -7
  17. package/dist/blocks/site/testimonials.js +2 -1
  18. package/dist/business/index.d.ts +1 -0
  19. package/dist/business/index.js +2 -0
  20. package/dist/business/signer-status.d.ts +53 -0
  21. package/dist/business/signer-status.js +76 -0
  22. package/dist/charts/bar-chart.js +5 -1
  23. package/dist/charts/combo-chart.js +1 -1
  24. package/dist/components/_qr-core.d.ts +26 -0
  25. package/dist/components/_qr-core.js +376 -0
  26. package/dist/components/backup-codes.d.ts +42 -0
  27. package/dist/components/backup-codes.js +67 -0
  28. package/dist/components/breadcrumb.d.ts +2 -2
  29. package/dist/components/breadcrumb.js +1 -1
  30. package/dist/components/command.d.ts +8 -2
  31. package/dist/components/command.js +7 -2
  32. package/dist/components/input.d.ts +1 -1
  33. package/dist/components/input.js +1 -1
  34. package/dist/components/pin-map.d.ts +58 -0
  35. package/dist/components/pin-map.js +167 -0
  36. package/dist/components/qr-code.d.ts +36 -0
  37. package/dist/components/qr-code.js +27 -0
  38. package/dist/components/record-detail-layout.d.ts +2 -2
  39. package/dist/components/record-detail-layout.js +1 -1
  40. package/dist/components/sidebar.js +8 -8
  41. package/dist/components/sla-timer.d.ts +75 -0
  42. package/dist/components/sla-timer.js +142 -0
  43. package/dist/components/slug-input.js +1 -1
  44. package/dist/components/tour.d.ts +60 -0
  45. package/dist/components/tour.js +139 -0
  46. package/dist/index.d.ts +5 -0
  47. package/dist/index.js +5 -0
  48. package/dist/lib/public.d.ts +1 -0
  49. package/dist/lib/public.js +1 -0
  50. package/dist/locale-data/core/holidays.d.ts +1 -1
  51. package/dist/locale-data/core/holidays.js +1 -1
  52. package/dist/media/gallery.js +3 -1
  53. package/dist/motion/core.d.ts +32 -0
  54. package/dist/motion/core.js +77 -0
  55. package/dist/motion/count-up.d.ts +29 -0
  56. package/dist/motion/count-up.js +92 -0
  57. package/dist/motion/index.d.ts +11 -0
  58. package/dist/motion/index.js +13 -0
  59. package/dist/motion/motion-scope.d.ts +17 -0
  60. package/dist/motion/motion-scope.js +16 -0
  61. package/dist/motion/page-transition.d.ts +33 -0
  62. package/dist/motion/page-transition.js +61 -0
  63. package/dist/motion/parallax.d.ts +37 -0
  64. package/dist/motion/parallax.js +90 -0
  65. package/dist/motion/pointer.d.ts +38 -0
  66. package/dist/motion/pointer.js +117 -0
  67. package/dist/motion/reveal.d.ts +72 -0
  68. package/dist/motion/reveal.js +133 -0
  69. package/dist/motion/scroll-shrink.d.ts +18 -0
  70. package/dist/motion/scroll-shrink.js +47 -0
  71. package/dist/motion/skeleton-swap.d.ts +17 -0
  72. package/dist/motion/skeleton-swap.js +43 -0
  73. package/dist/motion/text-reveal.d.ts +30 -0
  74. package/dist/motion/text-reveal.js +29 -0
  75. package/dist/motion/ticker.d.ts +35 -0
  76. package/dist/motion/ticker.js +16 -0
  77. package/dist/sortable/sortable-tree.d.ts +4 -0
  78. package/dist/sortable/sortable-tree.js +16 -4
  79. package/dist/styles/blocks.css +3 -3
  80. package/dist/styles/motion.css +389 -0
  81. package/dist/styles.css +2 -1
  82. package/package.json +5 -1
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Check, Copy, Download, RefreshCw } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useLabels } from "../lib/labels.js";
7
+ import { useFormat } from "../lib/locale.js";
8
+ import { Button } from "./button.js";
9
+ export const backupCodesLabels = {
10
+ title: "Backup codes",
11
+ copy: "Copy",
12
+ copied: "Copied",
13
+ copiedAnnouncement: "Backup codes copied to the clipboard",
14
+ download: "Download",
15
+ regenerate: "New codes",
16
+ used: "used",
17
+ remaining: "{left} of {total} left",
18
+ fileHeader: "Backup codes — each code works once. Keep them somewhere safe.",
19
+ };
20
+ /**
21
+ * One-time recovery codes after two-factor enrolment: a numbered monospace grid with copy,
22
+ * download and regenerate. Used codes stay visible, struck through.
23
+ *
24
+ * ```tsx
25
+ * <BackupCodes codes={codes} used={["4K7Q-PX2M"]} onRegenerate={regenerate} />
26
+ * ```
27
+ */
28
+ export const BackupCodes = React.forwardRef(function BackupCodes({ codes, used = [], title, filename = "backup-codes.txt", fileNote, onCopy, onDownload, onRegenerate, regenerating, labels, className, ...props }, ref) {
29
+ const t = useLabels("backupCodes", backupCodesLabels, labels);
30
+ const f = useFormat();
31
+ const [copied, setCopied] = React.useState(false);
32
+ const usedSet = React.useMemo(() => new Set(used), [used]);
33
+ const titleId = React.useId();
34
+ const text = React.useMemo(() => [fileNote, t("fileHeader"), "", ...codes.map((c, i) => `${String(i + 1).padStart(2, " ")}. ${c}${usedSet.has(c) ? ` (${t("used")})` : ""}`)].filter((l) => l !== undefined).join("\n") + "\n", [codes, usedSet, fileNote, t]);
35
+ React.useEffect(() => {
36
+ if (!copied)
37
+ return;
38
+ const id = window.setTimeout(() => setCopied(false), 2000);
39
+ return () => window.clearTimeout(id);
40
+ }, [copied]);
41
+ const copy = async () => {
42
+ try {
43
+ await navigator.clipboard?.writeText(text);
44
+ }
45
+ catch {
46
+ /* clipboard blocked — still report so the user can select manually */
47
+ }
48
+ setCopied(true);
49
+ onCopy?.(text);
50
+ };
51
+ const download = () => {
52
+ if (onDownload)
53
+ return onDownload(text);
54
+ const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" }));
55
+ const a = document.createElement("a");
56
+ a.href = url;
57
+ a.download = filename;
58
+ a.click();
59
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
60
+ };
61
+ const left = codes.filter((c) => !usedSet.has(c)).length;
62
+ const heading = title === undefined ? t("title") : title;
63
+ return (_jsxs("div", { ref: ref, "data-slot": "backup-codes", role: "group", "aria-labelledby": heading ? titleId : undefined, className: cn("grid gap-3", className), ...props, children: [heading ? (_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [_jsx("h3", { id: titleId, className: "text-sm font-medium text-fg", children: heading }), _jsx("span", { className: "text-xs text-fg-muted tabular", children: t("remaining", { left: f.number(left), total: f.number(codes.length) }) })] })) : null, _jsx("ol", { className: "grid grid-cols-2 gap-x-6 gap-y-1.5 rounded-ui border border-line bg-surface-2 px-4 py-3.5 font-mono text-[13px] tabular", translate: "no", children: codes.map((code, i) => {
64
+ const spent = usedSet.has(code);
65
+ return (_jsxs("li", { "data-used": spent || undefined, className: "flex items-baseline gap-3", children: [_jsx("span", { "aria-hidden": "true", className: "w-4 text-end text-[11px] text-fg-subtle", children: i + 1 }), _jsx("span", { className: cn("tracking-wide select-all", spent ? "text-fg-subtle line-through decoration-fg-subtle" : "text-fg"), children: code }), spent ? _jsxs("span", { className: "sr-only", children: ["(", t("used"), ")"] }) : null] }, code));
66
+ }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { size: "sm", variant: "secondary", onClick: copy, children: [copied ? _jsx(Check, { "aria-hidden": "true" }) : _jsx(Copy, { "aria-hidden": "true" }), copied ? t("copied") : t("copy")] }), _jsxs(Button, { size: "sm", variant: "secondary", onClick: download, children: [_jsx(Download, { "aria-hidden": "true" }), t("download")] }), onRegenerate ? (_jsxs(Button, { size: "sm", variant: "ghost", className: "ms-auto", onClick: onRegenerate, loading: regenerating, children: [_jsx(RefreshCw, { "aria-hidden": "true" }), t("regenerate")] })) : null, _jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: copied ? t("copiedAnnouncement") : "" })] })] }));
67
+ });
@@ -15,7 +15,7 @@ export interface BreadcrumbItemData {
15
15
  label: React.ReactNode;
16
16
  href?: string;
17
17
  icon?: React.ReactNode;
18
- /** Jump to siblings ("Companies › Acme ▾" → other companies). */
18
+ /** Jump to siblings ("Companies › Northpoint ▾" → other companies). */
19
19
  menu?: BreadcrumbMenuLink[];
20
20
  }
21
21
  export interface BreadcrumbProps extends React.HTMLAttributes<HTMLElement> {
@@ -37,7 +37,7 @@ export interface BreadcrumbProps extends React.HTMLAttributes<HTMLElement> {
37
37
  * `aria-current="page"` on the last item). Links go through the app's router `Link`.
38
38
  *
39
39
  * ```tsx
40
- * <Breadcrumb items={[{ label: "Companies", href: "/companies" }, { label: "Acme", href: "/companies/acme" }, { label: "Contacts" }]} />
40
+ * <Breadcrumb items={[{ label: "Companies", href: "/companies" }, { label: "Northpoint", href: "/companies/northpoint" }, { label: "Contacts" }]} />
41
41
  * ```
42
42
  *
43
43
  * Compound: `<Breadcrumb><BreadcrumbList><BreadcrumbItem><BreadcrumbLink href/></BreadcrumbItem><BreadcrumbSeparator/>…<BreadcrumbPage/></BreadcrumbList></Breadcrumb>`.
@@ -21,7 +21,7 @@ const SizeContext = React.createContext("md");
21
21
  * `aria-current="page"` on the last item). Links go through the app's router `Link`.
22
22
  *
23
23
  * ```tsx
24
- * <Breadcrumb items={[{ label: "Companies", href: "/companies" }, { label: "Acme", href: "/companies/acme" }, { label: "Contacts" }]} />
24
+ * <Breadcrumb items={[{ label: "Companies", href: "/companies" }, { label: "Northpoint", href: "/companies/northpoint" }, { label: "Contacts" }]} />
25
25
  * ```
26
26
  *
27
27
  * Compound: `<Breadcrumb><BreadcrumbList><BreadcrumbItem><BreadcrumbLink href/></BreadcrumbItem><BreadcrumbSeparator/>…<BreadcrumbPage/></BreadcrumbList></Breadcrumb>`.
@@ -102,7 +102,10 @@ export interface CommandItemProps extends React.ComponentPropsWithoutRef<typeof
102
102
  icon?: React.ReactNode;
103
103
  /** Second line under the label. */
104
104
  description?: React.ReactNode;
105
- /** `"mod+shift+n"` or `["mod","shift","n"]` — display only. */
105
+ /**
106
+ * `"mod+shift+n"` or `["mod","shift","n"]` — display only. An array without modifier keys is a
107
+ * key sequence (`["g","o"]` shows G then O).
108
+ */
106
109
  shortcut?: string | string[];
107
110
  /** Trailing meta (an app badge, a count). */
108
111
  badge?: React.ReactNode;
@@ -113,7 +116,10 @@ export interface CommandItemProps extends React.ComponentPropsWithoutRef<typeof
113
116
  }
114
117
  /** One selectable row. Highlighted with the arrow keys; Enter or click runs `onSelect`. */
115
118
  export declare const CommandItem: React.ForwardRefExoticComponent<CommandItemProps & React.RefAttributes<HTMLDivElement>>;
116
- /** Shortcut keycaps at the end of an item. */
119
+ /**
120
+ * Shortcut keycaps at the end of an item. A string or an array with modifiers is one combination
121
+ * (`"mod+k"`, `["mod","k"]`); an array of plain keys is a sequence (`["g","o"]` → G O).
122
+ */
117
123
  export declare function CommandShortcut({ keys, className }: {
118
124
  keys: string | string[];
119
125
  className?: string;
@@ -87,9 +87,14 @@ export const CommandSeparator = React.forwardRef(function CommandSeparator({ cla
87
87
  export const CommandItem = React.forwardRef(function CommandItem({ className, icon, description, shortcut, badge, chevron, loading, children, ...props }, ref) {
88
88
  return (_jsxs(Cmdk.Item, { ref: ref, "data-slot": "command-item", "aria-busy": loading || undefined, className: cn(itemVariants({ size: "md" }), "min-h-[calc(var(--control-h)*0.95)] gap-2.5 px-2.5 data-[selected=true]:bg-selected", className), ...props, children: [loading ? (_jsx(Spinner, { size: "xs", className: "shrink-0" })) : icon ? (_jsx("span", { "aria-hidden": true, className: cn("flex shrink-0 items-center text-fg-muted", description && "self-start pt-0.5"), children: icon })) : null, _jsxs("span", { className: itemSlots.label, children: [children, description ? _jsx("span", { className: cn(itemSlots.description, "mt-0.5 truncate"), children: description }) : null] }), badge != null ? _jsx("span", { className: "ms-auto flex shrink-0 items-center ps-3", children: badge }) : null, shortcut ? _jsx(CommandShortcut, { keys: shortcut, className: badge == null ? "ms-auto" : undefined }) : null, chevron ? _jsx(ChevronRight, { "aria-hidden": true, className: cn("size-3.5! shrink-0 text-fg-subtle rtl:-scale-x-100", !shortcut && badge == null && "ms-auto") }) : null] }));
89
89
  });
90
- /** Shortcut keycaps at the end of an item. */
90
+ const MODIFIER_KEYS = /^(mod|ctrl|control|meta|cmd|command|alt|option|shift)$/i;
91
+ /**
92
+ * Shortcut keycaps at the end of an item. A string or an array with modifiers is one combination
93
+ * (`"mod+k"`, `["mod","k"]`); an array of plain keys is a sequence (`["g","o"]` → G O).
94
+ */
91
95
  export function CommandShortcut({ keys, className }) {
92
- return _jsx(KbdGroup, { keys: Array.isArray(keys) ? keys.join("+") : keys, size: "sm", variant: "ghost", className: cn("shrink-0 ps-3", className) });
96
+ const sequence = Array.isArray(keys) && keys.length > 1 && !keys.some((k) => MODIFIER_KEYS.test(k));
97
+ return (_jsx(KbdGroup, { keys: sequence ? keys : Array.isArray(keys) ? keys.join("+") : keys, size: "sm", variant: "ghost", className: cn("shrink-0 ps-3", className) }));
93
98
  }
94
99
  /**
95
100
  * Command inside a top-anchored dialog (fullscreen on phones). Focus goes to the input; Esc closes
@@ -47,7 +47,7 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
47
47
  * <Input maxLength={60} showCount />
48
48
  * </Field>
49
49
  * <Input leading={<Search />} placeholder="Search contacts" clearable />
50
- * <Input addonStart="https://" addonEnd=".vamos.ist" defaultValue="acme" />
50
+ * <Input addonStart="https://" addonEnd=".vamos.ist" defaultValue="northpoint" />
51
51
  * ```
52
52
  */
53
53
  export declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
@@ -36,7 +36,7 @@ const setNativeValue = (el, value) => {
36
36
  * <Input maxLength={60} showCount />
37
37
  * </Field>
38
38
  * <Input leading={<Search />} placeholder="Search contacts" clearable />
39
- * <Input addonStart="https://" addonEnd=".vamos.ist" defaultValue="acme" />
39
+ * <Input addonStart="https://" addonEnd=".vamos.ist" defaultValue="northpoint" />
40
40
  * ```
41
41
  */
42
42
  export const Input = React.forwardRef(function Input(rawProps, forwardedRef) {
@@ -0,0 +1,58 @@
1
+ import * as React from "react";
2
+ export type MapPinTone = "accent" | "neutral" | "info" | "success" | "warning" | "danger";
3
+ export interface MapPin {
4
+ id: string;
5
+ lat: number;
6
+ lng: number;
7
+ /** Accessible name and callout title. */
8
+ label: string;
9
+ /** Callout body (address, status, ETA). */
10
+ description?: React.ReactNode;
11
+ tone?: MapPinTone;
12
+ /** Size for `variant="bubble"` (visitors, orders). */
13
+ value?: number;
14
+ /** Live ring — vehicles en route, new visitors. Off under reduced motion. */
15
+ pulse?: boolean;
16
+ /** Small glyph inside the marker (pin variant), e.g. a 12px icon or initials. */
17
+ icon?: React.ReactNode;
18
+ }
19
+ export interface PinMapBounds {
20
+ north: number;
21
+ south: number;
22
+ west: number;
23
+ east: number;
24
+ }
25
+ export interface PinMapProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
26
+ pins: MapPin[];
27
+ /** Visible area. Omit to fit every pin. Expanded to match `aspectRatio` so nothing distorts. */
28
+ bounds?: PinMapBounds;
29
+ /** Width ÷ height. Default `16 / 10`. */
30
+ aspectRatio?: number;
31
+ /** `"pin"` (default) markers, small `"dot"`s, or value-sized `"bubble"`s. */
32
+ variant?: "pin" | "dot" | "bubble";
33
+ selectedId?: string | null;
34
+ defaultSelectedId?: string | null;
35
+ onSelectedIdChange?: (id: string | null) => void;
36
+ /** Callout for the selected pin. Default: label + description. `false` hides it. */
37
+ callout?: false | ((pin: MapPin) => React.ReactNode);
38
+ /** Always show labels next to markers (few pins only). */
39
+ showLabels?: boolean;
40
+ /** Replace the drawn sheet with your own layer (a static tile image, an SVG of districts). */
41
+ backdrop?: React.ReactNode;
42
+ /** Hide the coordinate ticks and scale bar. */
43
+ bare?: boolean;
44
+ /** Required: what the map shows ("Stores in Istanbul"). */
45
+ "aria-label": string;
46
+ }
47
+ /**
48
+ * Markers on a projected sheet — stores, field jobs, vehicles, live visitors — without a tile
49
+ * provider. Pins are positioned in Web Mercator inside `bounds`; pass a `backdrop` to lay them
50
+ * over your own static map. Selection is controlled or not; arrow keys move to the nearest pin
51
+ * in that direction, Enter selects, Escape clears.
52
+ *
53
+ * ```tsx
54
+ * <PinMap aria-label="Stores" pins={stores.map((s) => ({ id: s.id, lat: s.lat, lng: s.lng, label: s.name }))}
55
+ * selectedId={selected} onSelectedIdChange={setSelected} />
56
+ * ```
57
+ */
58
+ export declare const PinMap: React.ForwardRefExoticComponent<PinMapProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,167 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useFormat } from "../lib/locale.js";
6
+ import { useControllableState } from "../lib/use-controllable-state.js";
7
+ const mercY = (lat) => Math.log(Math.tan(Math.PI / 4 + (Math.max(-85, Math.min(85, lat)) * Math.PI) / 360));
8
+ const toRad = (deg) => (deg * Math.PI) / 180;
9
+ /** Percent string with fixed precision — identical on server and client. */
10
+ const pct = (v) => `${(v * 100).toFixed(3)}%`;
11
+ function fitBounds(pins, bounds, aspect) {
12
+ let b = bounds;
13
+ if (!b) {
14
+ if (pins.length === 0)
15
+ b = { north: 1, south: -1, west: -1, east: 1 };
16
+ else {
17
+ const lats = pins.map((p) => p.lat);
18
+ const lngs = pins.map((p) => p.lng);
19
+ const [n, s, w, e] = [Math.max(...lats), Math.min(...lats), Math.min(...lngs), Math.max(...lngs)];
20
+ const padLat = Math.max((n - s) * 0.14, 0.01);
21
+ const padLng = Math.max((e - w) * 0.1, 0.01);
22
+ b = { north: n + padLat, south: s - padLat, west: w - padLng, east: e + padLng };
23
+ }
24
+ }
25
+ // Expand the short side so the Mercator projection keeps true shapes at this aspect ratio.
26
+ const xSpan = toRad(b.east - b.west);
27
+ const ySpan = mercY(b.north) - mercY(b.south);
28
+ if (xSpan / ySpan < aspect) {
29
+ const extra = (ySpan * aspect - xSpan) / 2;
30
+ const d = (extra * 180) / Math.PI;
31
+ return { ...b, west: b.west - d, east: b.east + d };
32
+ }
33
+ const targetY = xSpan / aspect;
34
+ const mid = (mercY(b.north) + mercY(b.south)) / 2;
35
+ const inv = (y) => (Math.atan(Math.exp(y)) * 360) / Math.PI - 90;
36
+ return { ...b, north: inv(mid + targetY / 2), south: inv(mid - targetY / 2) };
37
+ }
38
+ const toneFill = {
39
+ accent: "bg-accent text-accent-fg",
40
+ neutral: "bg-fg-muted text-scene",
41
+ info: "bg-info text-scene",
42
+ success: "bg-success text-scene",
43
+ warning: "bg-warning text-scene",
44
+ danger: "bg-danger text-scene",
45
+ };
46
+ const toneVar = {
47
+ accent: "var(--accent)",
48
+ neutral: "var(--fg-muted)",
49
+ info: "var(--info)",
50
+ success: "var(--success)",
51
+ warning: "var(--warning)",
52
+ danger: "var(--danger)",
53
+ };
54
+ /** A tidy step for ticks: 0.01, 0.02, 0.05, 0.1 … */
55
+ function niceStep(span, target) {
56
+ const raw = span / target;
57
+ const pow = 10 ** Math.floor(Math.log10(raw));
58
+ const m = raw / pow;
59
+ return (m < 1.5 ? 1 : m < 3.5 ? 2 : m < 7.5 ? 5 : 10) * pow;
60
+ }
61
+ /**
62
+ * Markers on a projected sheet — stores, field jobs, vehicles, live visitors — without a tile
63
+ * provider. Pins are positioned in Web Mercator inside `bounds`; pass a `backdrop` to lay them
64
+ * over your own static map. Selection is controlled or not; arrow keys move to the nearest pin
65
+ * in that direction, Enter selects, Escape clears.
66
+ *
67
+ * ```tsx
68
+ * <PinMap aria-label="Stores" pins={stores.map((s) => ({ id: s.id, lat: s.lat, lng: s.lng, label: s.name }))}
69
+ * selectedId={selected} onSelectedIdChange={setSelected} />
70
+ * ```
71
+ */
72
+ export const PinMap = React.forwardRef(function PinMap({ pins, bounds, aspectRatio = 16 / 10, variant = "pin", selectedId: selectedProp, defaultSelectedId = null, onSelectedIdChange, callout, showLabels, backdrop, bare, className, style, children, ...props }, ref) {
73
+ const f = useFormat();
74
+ const [selectedId, setSelectedId] = useControllableState({ value: selectedProp, defaultValue: defaultSelectedId, onChange: onSelectedIdChange, name: "PinMap" });
75
+ const [focusId, setFocusId] = React.useState(null);
76
+ const pinRefs = React.useRef(new Map());
77
+ const calloutId = React.useId();
78
+ const b = React.useMemo(() => fitBounds(pins, bounds, aspectRatio), [pins, bounds, aspectRatio]);
79
+ const yTop = mercY(b.north);
80
+ const ySpan = yTop - mercY(b.south);
81
+ const project = React.useCallback((lat, lng) => ({ x: (lng - b.west) / (b.east - b.west), y: (yTop - mercY(lat)) / ySpan }), [b, yTop, ySpan]);
82
+ const placed = React.useMemo(() => pins.map((p) => ({ pin: p, ...project(p.lat, p.lng) })), [pins, project]);
83
+ const maxValue = Math.max(1, ...pins.map((p) => p.value ?? 0));
84
+ const tabStop = focusId && pins.some((p) => p.id === focusId) ? focusId : selectedId && pins.some((p) => p.id === selectedId) ? selectedId : pins[0]?.id;
85
+ const moveFocus = (id) => {
86
+ if (!id)
87
+ return;
88
+ setFocusId(id);
89
+ pinRefs.current.get(id)?.focus();
90
+ };
91
+ const onKeyDown = (e, from) => {
92
+ const dir = { ArrowRight: [1, 0], ArrowLeft: [-1, 0], ArrowDown: [0, 1], ArrowUp: [0, -1] }[e.key];
93
+ if (dir) {
94
+ e.preventDefault();
95
+ const rtl = getComputedStyle(e.currentTarget).direction === "rtl" && dir[0] !== 0;
96
+ const [dx, dy] = rtl ? [-dir[0], dir[1]] : dir;
97
+ let best;
98
+ let bestScore = Infinity;
99
+ for (const p of placed) {
100
+ if (p.pin.id === from.pin.id)
101
+ continue;
102
+ const vx = (p.x - from.x) * aspectRatio;
103
+ const vy = p.y - from.y;
104
+ const along = vx * dx + vy * dy;
105
+ if (along <= 0.0001)
106
+ continue;
107
+ const across = Math.abs(vx * dy - vy * dx);
108
+ const score = along + across * 2;
109
+ if (score < bestScore) {
110
+ bestScore = score;
111
+ best = p;
112
+ }
113
+ }
114
+ moveFocus(best?.pin.id);
115
+ }
116
+ else if (e.key === "Home") {
117
+ e.preventDefault();
118
+ moveFocus(pins[0]?.id);
119
+ }
120
+ else if (e.key === "End") {
121
+ e.preventDefault();
122
+ moveFocus(pins.at(-1)?.id);
123
+ }
124
+ else if (e.key === "Escape" && selectedId) {
125
+ e.preventDefault();
126
+ setSelectedId(null);
127
+ }
128
+ };
129
+ // Ticks and scale bar.
130
+ const latStep = niceStep(b.north - b.south, 4);
131
+ const lngStep = niceStep(b.east - b.west, 6);
132
+ const latTicks = [];
133
+ for (let v = Math.ceil(b.south / latStep) * latStep; v <= b.north; v += latStep)
134
+ latTicks.push(Number(v.toFixed(6)));
135
+ const lngTicks = [];
136
+ for (let v = Math.ceil(b.west / lngStep) * lngStep; v <= b.east; v += lngStep)
137
+ lngTicks.push(Number(v.toFixed(6)));
138
+ const decimals = Math.max(0, -Math.floor(Math.log10(Math.min(latStep, lngStep))));
139
+ const widthKm = (b.east - b.west) * 111.32 * Math.cos(toRad((b.north + b.south) / 2));
140
+ const scaleKm = niceStep(widthKm, 5);
141
+ const scaleShare = scaleKm / widthKm;
142
+ const scaleLabel = scaleKm >= 1 ? f.number(scaleKm, { style: "unit", unit: "kilometer", unitDisplay: "short" }) : f.number(Math.round(scaleKm * 1000), { style: "unit", unit: "meter", unitDisplay: "short" });
143
+ const selected = placed.find((p) => p.pin.id === selectedId);
144
+ const renderCallout = callout === false ? null : (callout ?? ((p) => (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-[13px] font-medium text-fg", children: p.label }), p.description ? _jsx("div", { className: "mt-0.5 text-xs text-fg-muted", children: p.description }) : null] }))));
145
+ return (_jsxs("div", { ref: ref, role: "group", "data-slot": "pin-map", className: cn("relative isolate w-full overflow-hidden rounded-ui border border-line bg-surface-2 select-none", className), style: { aspectRatio: String(aspectRatio), ...style }, ...props, children: [_jsx("div", { "aria-hidden": "true", className: "absolute inset-0 -z-10", children: backdrop ?? (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute inset-0 bg-[radial-gradient(color-mix(in_oklch,var(--fg)_14%,transparent)_0.8px,transparent_1px)] bg-[length:14px_14px] opacity-70" }), _jsxs("svg", { className: "absolute inset-0 size-full", preserveAspectRatio: "none", viewBox: "0 0 100 100", children: [latTicks.map((v) => {
146
+ const y = project(v, b.west).y * 100;
147
+ return _jsx("line", { x1: "0", x2: "100", y1: y, y2: y, vectorEffect: "non-scaling-stroke", stroke: "var(--line)", strokeWidth: "1" }, `la${v}`);
148
+ }), lngTicks.map((v) => {
149
+ const x = project(b.north, v).x * 100;
150
+ return _jsx("line", { y1: "0", y2: "100", x1: x, x2: x, vectorEffect: "non-scaling-stroke", stroke: "var(--line)", strokeWidth: "1" }, `lo${v}`);
151
+ })] })] })) }), !bare ? (_jsxs("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 font-mono text-[9.5px] text-fg-subtle tabular", children: [latTicks.map((v) => (_jsxs("span", { className: "absolute start-1.5 -translate-y-full pb-0.5", style: { top: pct(project(v, b.west).y) }, children: [Math.abs(v).toFixed(decimals), "\u00B0", v >= 0 ? "N" : "S"] }, `tl${v}`))), lngTicks.slice(1).map((v) => (_jsxs("span", { className: "absolute top-1.5 ps-1", style: { left: pct(project(b.north, v).x) }, children: [Math.abs(v).toFixed(decimals), "\u00B0", v >= 0 ? "E" : "W"] }, `tg${v}`))), _jsxs("span", { className: "absolute inset-x-3 bottom-2.5 flex flex-col items-end gap-1", children: [_jsx("span", { children: scaleLabel }), _jsx("span", { className: "block h-1 border-x border-b border-fg-subtle", style: { width: pct(scaleShare) } })] })] })) : null, _jsxs("div", { className: "absolute inset-0", children: [placed.map(({ pin, x, y }) => {
152
+ const tone = pin.tone ?? "accent";
153
+ const isSelected = pin.id === selectedId;
154
+ const size = variant === "bubble" ? Math.round(10 + Math.sqrt((pin.value ?? 0) / maxValue) * 34) : variant === "dot" ? 10 : 22;
155
+ return (_jsxs("button", { ref: (el) => {
156
+ if (el)
157
+ pinRefs.current.set(pin.id, el);
158
+ else
159
+ pinRefs.current.delete(pin.id);
160
+ }, type: "button", "data-slot": "pin-map-pin", "data-selected": isSelected || undefined, "aria-pressed": isSelected, "aria-label": pin.label, "aria-describedby": isSelected && renderCallout ? calloutId : undefined, tabIndex: pin.id === tabStop ? 0 : -1, onFocus: () => setFocusId(pin.id), onClick: () => setSelectedId(isSelected ? null : pin.id), onKeyDown: (e) => onKeyDown(e, { pin, x, y }), className: cn("group/pin absolute grid -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-2)]", isSelected ? "z-20" : "z-10 hover:z-20 focus-visible:z-20"), style: { left: pct(x), top: pct(y), width: Math.max(size, 24), height: Math.max(size, 24) }, children: [pin.pulse ? (_jsx("span", { "aria-hidden": "true", className: "absolute rounded-full opacity-60 motion-safe:animate-ping", style: { width: Math.min(size, 22), height: Math.min(size, 22), background: toneVar[tone] } })) : null, variant === "bubble" ? (_jsx("span", { "aria-hidden": "true", className: "rounded-full border transition-transform duration-200 group-hover/pin:scale-105 motion-reduce:transition-none", style: {
161
+ width: size,
162
+ height: size,
163
+ background: `color-mix(in oklch, ${toneVar[tone]} ${isSelected ? 55 : 30}%, transparent)`,
164
+ borderColor: toneVar[tone],
165
+ } })) : (_jsx("span", { "aria-hidden": "true", className: cn("relative grid place-items-center rounded-full ring-2 ring-[var(--surface-solid)] shadow-[0_1px_3px_rgb(0_0_0/0.25)] transition-transform duration-200 group-hover/pin:scale-110 motion-reduce:transition-none [&_svg]:size-3", toneFill[tone], isSelected && "scale-125 ring-[var(--fg)]"), style: { width: variant === "dot" ? 9 : 18, height: variant === "dot" ? 9 : 18 }, children: variant === "pin" ? pin.icon : null })), showLabels ? (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute start-full ms-0.5 rounded-ui-sm bg-[color-mix(in_oklch,var(--surface-solid)_82%,transparent)] px-1 text-[11px] font-medium whitespace-nowrap text-fg", children: pin.label })) : null] }, pin.id));
166
+ }), selected && renderCallout ? (_jsx("div", { id: calloutId, "data-slot": "pin-map-callout", className: cn("surface-popover ui-pop pointer-events-none absolute z-30 w-max max-w-56 -translate-x-1/2 rounded-ui px-3 py-2", selected.y < 0.35 ? "mt-4" : "-translate-y-full -mt-4"), style: { left: `clamp(7rem, ${pct(selected.x)}, calc(100% - 7rem))`, top: pct(selected.y) }, children: renderCallout(selected.pin) })) : null] }), children] }));
167
+ });
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import { encodeQr, qrPath, type QrErrorCorrection, type QrMatrix } from "./_qr-core.js";
3
+ export { encodeQr, qrPath, type QrErrorCorrection, type QrMatrix };
4
+ export interface QrCodeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
5
+ /** Text to encode (URL, otpauth:// URI, Wi-Fi string…). UTF-8, byte mode. */
6
+ value: string;
7
+ /** Accessible name — say what scanning does: "QR code to add Kiln & Linen to your authenticator app". */
8
+ "aria-label": string;
9
+ /** `"M"` (default) recovers ~15% damage; a centre mark switches to `"H"` automatically. */
10
+ errorCorrection?: QrErrorCorrection;
11
+ /** Rendered width (px or any CSS length). Default `176`. */
12
+ size?: number | string;
13
+ /**
14
+ * Quiet zone in modules. Default `4` without a plate, `2` on the plate (the plate's padding
15
+ * adds the rest). Scanners need light space around the code.
16
+ */
17
+ margin?: number;
18
+ /**
19
+ * Draw the code on a light plate (default). Codes scan most reliably dark-on-light, so the plate
20
+ * stays light in dark mode. `false` uses the current text colour on a transparent background.
21
+ */
22
+ plate?: boolean;
23
+ /** Shown when `value` is too long for version 40. Default "Too long for a QR code". */
24
+ fallback?: React.ReactNode;
25
+ /** Small mark in the centre (logo, icon). Keep it under ~20% of the width. */
26
+ children?: React.ReactNode;
27
+ }
28
+ /**
29
+ * A QR code drawn as one SVG path — for two-factor enrolment, invite links, tickets and
30
+ * payment slips. Encoded on the server or client with no dependency; works in RSC.
31
+ *
32
+ * ```tsx
33
+ * <QrCode value={otpauthUri} aria-label="QR code for your authenticator app" />
34
+ * ```
35
+ */
36
+ export declare const QrCode: React.ForwardRefExoticComponent<QrCodeProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../lib/cn.js";
4
+ import { encodeQr, qrPath } from "./_qr-core.js";
5
+ export { encodeQr, qrPath };
6
+ /**
7
+ * A QR code drawn as one SVG path — for two-factor enrolment, invite links, tickets and
8
+ * payment slips. Encoded on the server or client with no dependency; works in RSC.
9
+ *
10
+ * ```tsx
11
+ * <QrCode value={otpauthUri} aria-label="QR code for your authenticator app" />
12
+ * ```
13
+ */
14
+ export const QrCode = React.forwardRef(function QrCode({ value, errorCorrection, size = 176, margin, plate = true, fallback = "Too long for a QR code", children, className, style, ...props }, ref) {
15
+ const ecl = errorCorrection ?? (children ? "H" : "M");
16
+ let qr = null;
17
+ try {
18
+ qr = encodeQr(value, { errorCorrection: ecl });
19
+ }
20
+ catch {
21
+ qr = null;
22
+ }
23
+ const quiet = margin ?? (plate ? 2 : 4);
24
+ const dim = qr ? qr.size + quiet * 2 : 0;
25
+ const width = typeof size === "number" ? `${size}px` : size;
26
+ return (_jsxs("div", { ref: ref, role: "img", "data-slot": "qr-code", "data-version": qr?.version, className: cn("relative inline-grid shrink-0 place-items-center", plate && "light rounded-ui border border-line bg-[var(--surface-solid)] p-2 text-fg shadow-[var(--surface-shadow)]", className), style: { width, ...style }, ...props, children: [qr ? (_jsx("svg", { viewBox: `0 0 ${dim} ${dim}`, "aria-hidden": "true", focusable: "false", shapeRendering: "crispEdges", className: "block size-full", children: _jsx("path", { d: qrPath(qr, quiet), fill: "currentColor" }) })) : (_jsx("span", { className: "grid aspect-square w-full place-items-center p-3 text-center text-xs text-fg-muted", children: fallback })), children && qr ? (_jsx("span", { "data-slot": "qr-code-mark", className: "absolute top-1/2 left-1/2 grid size-[22%] -translate-x-1/2 -translate-y-1/2 place-items-center rounded-ui-sm bg-[var(--surface-solid)] p-[3%] [&>svg]:size-full [&>img]:size-full [&>img]:object-contain", children: children })) : null] }));
27
+ });
@@ -68,7 +68,7 @@ export interface RecordLayoutProps extends React.HTMLAttributes<HTMLDivElement>
68
68
  *
69
69
  * ```tsx
70
70
  * <RecordLayout>
71
- * <RecordHeader media={<Avatar name="Jane Doe" size="xl" />} title="Jane Doe" subtitle="Head of Ops · Acme"
71
+ * <RecordHeader media={<Avatar name="Jane Doe" size="xl" />} title="Jane Doe" subtitle="Head of Ops · Northpoint"
72
72
  * status={<Badge tone="success">Customer</Badge>} facts={[{ label: "Owner", value: "Meltem Aydın" }]}
73
73
  * pager={<RecordPager index={3} total={128} prevHref="/contacts/41" nextHref="/contacts/43" />} />
74
74
  * <RecordMain>{tabs}</RecordMain>
@@ -86,7 +86,7 @@ export interface RecordHeaderProps extends Omit<React.HTMLAttributes<HTMLElement
86
86
  media?: React.ReactNode;
87
87
  /** The page's `<h1>`. */
88
88
  title: React.ReactNode;
89
- /** "Head of Operations · Acme Corp". */
89
+ /** "Head of Operations · Northpoint Ltd". */
90
90
  subtitle?: React.ReactNode;
91
91
  /** Status badge next to the title. */
92
92
  status?: React.ReactNode;
@@ -114,7 +114,7 @@ export const SidePaneHandle = React.forwardRef(function SidePaneHandle({ value,
114
114
  *
115
115
  * ```tsx
116
116
  * <RecordLayout>
117
- * <RecordHeader media={<Avatar name="Jane Doe" size="xl" />} title="Jane Doe" subtitle="Head of Ops · Acme"
117
+ * <RecordHeader media={<Avatar name="Jane Doe" size="xl" />} title="Jane Doe" subtitle="Head of Ops · Northpoint"
118
118
  * status={<Badge tone="success">Customer</Badge>} facts={[{ label: "Owner", value: "Meltem Aydın" }]}
119
119
  * pager={<RecordPager index={3} total={128} prevHref="/contacts/41" nextHref="/contacts/43" />} />
120
120
  * <RecordMain>{tabs}</RecordMain>
@@ -82,7 +82,7 @@ export const SidebarFooter = React.forwardRef(function SidebarFooter({ className
82
82
  export const SidebarGroup = React.forwardRef(function SidebarGroup({ label, className, children, ...props }, ref) {
83
83
  const { collapsed } = useSidebarCtx();
84
84
  const id = React.useId();
85
- return (_jsxs("div", { ref: ref, "data-slot": "sidebar-group", role: label ? "group" : undefined, "aria-labelledby": label ? id : undefined, className: cn("grid", className), ...props, children: [label ? (collapsed ? (_jsxs(_Fragment, { children: [_jsx("span", { id: id, className: "sr-only", children: label }), _jsx("span", { "aria-hidden": true, className: "mx-4 my-2 h-px bg-nav-line" })] })) : (_jsx("span", { id: id, className: "px-4.5 pt-4 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/50", children: label }))) : null, _jsx("ul", { className: "grid gap-px", children: children })] }));
85
+ return (_jsxs("div", { ref: ref, "data-slot": "sidebar-group", role: label ? "group" : undefined, "aria-labelledby": label ? id : undefined, className: cn("grid", className), ...props, children: [label ? (collapsed ? (_jsxs(_Fragment, { children: [_jsx("span", { id: id, className: "sr-only", children: label }), _jsx("span", { "aria-hidden": true, className: "mx-4 my-2 h-px bg-nav-line" })] })) : (_jsx("span", { id: id, className: "px-4.5 pt-4 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/70", children: label }))) : null, _jsx("ul", { className: "grid gap-px", children: children })] }));
86
86
  });
87
87
  /** A hairline between sections. */
88
88
  export function SidebarSeparator({ className }) {
@@ -286,7 +286,7 @@ function SidebarFlyoutItem({ label, href, rowClass, current, inTrail, external,
286
286
  e.preventDefault();
287
287
  close(true);
288
288
  }
289
- }, children: [collapsed ? _jsx("p", { className: "px-2.5 pt-1 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/60", children: label }) : null, _jsx(SubContext.Provider, { value: { placement: "flyout", depth: 1, close: () => close(false) }, children: _jsx("ul", { className: "grid gap-px", children: sub }) })] }) }) })] }));
289
+ }, children: [collapsed ? _jsx("p", { className: "px-2.5 pt-1 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/70", children: label }) : null, _jsx(SubContext.Provider, { value: { placement: "flyout", depth: 1, close: () => close(false) }, children: _jsx("ul", { className: "grid gap-px", children: sub }) })] }) }) })] }));
290
290
  }
291
291
  /** A level-2 (or level-3) link under a `SidebarItem`. */
292
292
  export function SidebarSubItem({ id, label, href, badge, disabled, external, active, children, className }) {
@@ -300,9 +300,9 @@ export function SidebarSubItem({ id, label, href, badge, disabled, external, act
300
300
  const flyout = placement === "flyout";
301
301
  const pad = flyout ? (depth > 1 ? "ps-5 pe-2.5" : "px-2.5") : depth > 1 ? "ps-[54px] pe-3" : "ps-[42px] pe-3";
302
302
  const rowClass = cn(rowBase, "gap-2 py-1.5 text-[13px]", flyout ? "rounded-ui-sm hover:bg-nav-hover hover:text-nav-fg-strong" : "hover:text-nav-fg-strong", pad, current ? "font-medium text-nav-fg-strong" : undefined, className);
303
- const body = (_jsxs(_Fragment, { children: [current && flyout ? _jsx(Check, { "aria-hidden": true, className: "size-3.5 shrink-0 text-accent" }) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), badgeText ? _jsxs("span", { className: "sr-only", children: [", ", badgeText] }) : null, typeof disabled === "string" ? _jsx("span", { className: "shrink-0 text-[10.5px] text-nav-fg/60", children: disabled }) : _jsx(SidebarBadgeView, { spec: badge, rail: false }), (external ?? (href ? isAbsoluteHref(href) : false)) ? _jsx(ExternalMark, {}) : null] }));
303
+ const body = (_jsxs(_Fragment, { children: [current && flyout ? _jsx(Check, { "aria-hidden": true, className: "size-3.5 shrink-0 text-accent" }) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), badgeText ? _jsxs("span", { className: "sr-only", children: [", ", badgeText] }) : null, typeof disabled === "string" ? _jsx("span", { className: "shrink-0 text-[10.5px] text-nav-fg/70", children: disabled }) : _jsx(SidebarBadgeView, { spec: badge, rail: false }), (external ?? (href ? isAbsoluteHref(href) : false)) ? _jsx(ExternalMark, {}) : null] }));
304
304
  if (hasChildren && flyout) {
305
- return (_jsxs("li", { "data-slot": "sidebar-sub-item", className: "mt-1 first:mt-0", children: [_jsx("p", { className: "px-2.5 pt-1.5 pb-1 text-[11px] font-medium tracking-[0.06em] text-nav-fg/55", children: label }), _jsx("ul", { className: "grid gap-px", children: _jsx(SubContext.Provider, { value: { placement, depth: depth + 1, close }, children: children }) })] }));
305
+ return (_jsxs("li", { "data-slot": "sidebar-sub-item", className: "mt-1 first:mt-0", children: [_jsx("p", { className: "px-2.5 pt-1.5 pb-1 text-[11px] font-medium tracking-[0.06em] text-nav-fg/70", children: label }), _jsx("ul", { className: "grid gap-px", children: _jsx(SubContext.Provider, { value: { placement, depth: depth + 1, close }, children: children }) })] }));
306
306
  }
307
307
  if (hasChildren) {
308
308
  return (_jsx(Collapsible.Root, { asChild: true, open: open, onOpenChange: setOpen, children: _jsxs("li", { "data-slot": "sidebar-sub-item", children: [_jsx(Collapsible.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn(rowClass, inTrail && "text-nav-fg-strong"), children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), _jsx(ChevronDown, { "aria-hidden": true, className: cn("size-3.5 shrink-0 opacity-60 transition-transform", open && "rotate-180") })] }) }), _jsx(Collapsible.Content, { className: "ui-collapse", children: _jsx("ul", { "aria-label": label, className: "grid gap-px", children: _jsx(SubContext.Provider, { value: { placement, depth: depth + 1, close }, children: children }) }) })] }) }));
@@ -401,16 +401,16 @@ export function AppSwitcher({ apps, activeApp, onSelect, subtitle, workspaces, a
401
401
  return null;
402
402
  const single = apps.length < 2 && !(workspaces && workspaces.length > 1);
403
403
  const workspace = workspaces?.find((w) => w.id === activeWorkspace);
404
- const tile = (_jsxs(_Fragment, { children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-[9px] bg-accent text-accent-fg [&_svg]:size-[17px]", children: _jsx(AppIcon, { icon: app.icon }) }), _jsxs("span", { className: cn("min-w-0 flex-1 leading-tight", collapsed && "sr-only"), children: [_jsx("span", { className: "block truncate text-[13.5px] font-semibold text-nav-fg-strong", children: app.name }), _jsx("span", { className: "block truncate text-[11.5px] text-nav-fg/60", children: subtitle ?? (workspace ? workspace.name : app.description) })] })] }));
404
+ const tile = (_jsxs(_Fragment, { children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-[9px] bg-accent text-accent-fg [&_svg]:size-[17px]", children: _jsx(AppIcon, { icon: app.icon }) }), _jsxs("span", { className: cn("min-w-0 flex-1 leading-tight", collapsed && "sr-only"), children: [_jsx("span", { className: "block truncate text-[13.5px] font-semibold text-nav-fg-strong", children: app.name }), _jsx("span", { className: "block truncate text-[11.5px] text-nav-fg/75", children: subtitle ?? (workspace ? workspace.name : app.description) })] })] }));
405
405
  const boxClass = cn("flex w-full items-center gap-2.5 rounded-ui-sm p-1.5 text-start", collapsed && "justify-center");
406
406
  if (single)
407
407
  return _jsx("div", { className: cn(boxClass, className), children: tile });
408
- return (_jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn(boxClass, "transition-colors hover:bg-nav-hover focus-ring", className), children: [_jsx("span", { className: "sr-only", children: t("switchApp", { app: app.name }) }), _jsx("span", { "aria-hidden": true, className: "contents", children: tile }), !collapsed ? _jsx(ChevronsUpDown, { "aria-hidden": true, className: "size-4 shrink-0 text-nav-fg/50" }) : null] }) }), _jsx(PopoverPrimitive.Portal, { container: container, children: _jsx(Layer, { asChild: true, children: _jsxs(PopoverPrimitive.Content, { side: collapsed ? (dir === "rtl" ? "left" : "right") : "bottom", align: "start", sideOffset: 6, collisionPadding: 12, "aria-label": t("apps"), "data-variant": variant, className: "vui-sidebar-flyout ui-pop z-(--z-popover) w-64 max-w-[calc(100vw-1.5rem)] rounded-ui-lg p-1.5 outline-none", children: [workspaces && workspaces.length > 1 ? (_jsxs("div", { role: "group", "aria-label": t("workspaces"), className: "mb-1 border-b border-nav-line pb-1.5", children: [_jsx("p", { className: "px-2.5 pt-1 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/55", children: t("workspaces") }), workspaces.map((w) => (_jsxs("button", { type: "button", "aria-pressed": w.id === activeWorkspace, onClick: () => {
408
+ return (_jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn(boxClass, "transition-colors hover:bg-nav-hover focus-ring", className), children: [_jsx("span", { className: "sr-only", children: t("switchApp", { app: app.name }) }), _jsx("span", { "aria-hidden": true, className: "contents", children: tile }), !collapsed ? _jsx(ChevronsUpDown, { "aria-hidden": true, className: "size-4 shrink-0 text-nav-fg/50" }) : null] }) }), _jsx(PopoverPrimitive.Portal, { container: container, children: _jsx(Layer, { asChild: true, children: _jsxs(PopoverPrimitive.Content, { side: collapsed ? (dir === "rtl" ? "left" : "right") : "bottom", align: "start", sideOffset: 6, collisionPadding: 12, "aria-label": t("apps"), "data-variant": variant, className: "vui-sidebar-flyout ui-pop z-(--z-popover) w-64 max-w-[calc(100vw-1.5rem)] rounded-ui-lg p-1.5 outline-none", children: [workspaces && workspaces.length > 1 ? (_jsxs("div", { role: "group", "aria-label": t("workspaces"), className: "mb-1 border-b border-nav-line pb-1.5", children: [_jsx("p", { className: "px-2.5 pt-1 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/70", children: t("workspaces") }), workspaces.map((w) => (_jsxs("button", { type: "button", "aria-pressed": w.id === activeWorkspace, onClick: () => {
409
409
  onWorkspaceChange?.(w.id);
410
410
  setOpen(false);
411
- }, className: "flex w-full items-center gap-2.5 rounded-ui-sm px-2 py-1.5 text-start text-[13px] text-nav-fg hover:bg-nav-hover hover:text-nav-fg-strong focus-ring", children: [_jsx("span", { "aria-hidden": true, className: "grid size-6 place-items-center rounded-[7px] bg-nav-active text-[11px] font-semibold text-nav-fg-strong", children: w.name.slice(0, 1) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: w.name }), w.id === activeWorkspace ? _jsx(Check, { "aria-hidden": true, className: "size-4 text-accent" }) : null] }, w.id)))] })) : null, _jsx("p", { className: "px-2.5 pt-1 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/55", children: t("apps") }), _jsx("ul", { className: "grid gap-px", children: apps.map((a) => {
411
+ }, className: "flex w-full items-center gap-2.5 rounded-ui-sm px-2 py-1.5 text-start text-[13px] text-nav-fg hover:bg-nav-hover hover:text-nav-fg-strong focus-ring", children: [_jsx("span", { "aria-hidden": true, className: "grid size-6 place-items-center rounded-[7px] bg-nav-active text-[11px] font-semibold text-nav-fg-strong", children: w.name.slice(0, 1) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: w.name }), w.id === activeWorkspace ? _jsx(Check, { "aria-hidden": true, className: "size-4 text-accent" }) : null] }, w.id)))] })) : null, _jsx("p", { className: "px-2.5 pt-1 pb-1.5 text-[11px] font-medium tracking-[0.06em] text-nav-fg/70", children: t("apps") }), _jsx("ul", { className: "grid gap-px", children: apps.map((a) => {
412
412
  const isActive = a.id === app.id;
413
- const inner = (_jsxs(_Fragment, { children: [_jsx("span", { className: cn("grid size-7 shrink-0 place-items-center rounded-[8px] [&_svg]:size-[15px]", isActive ? "bg-accent text-accent-fg" : "bg-nav-active text-nav-fg-strong"), children: _jsx(AppIcon, { icon: a.icon }) }), _jsxs("span", { className: "min-w-0 flex-1 leading-tight", children: [_jsx("span", { className: "block text-[13px] font-medium text-nav-fg-strong", children: a.name }), a.description ? _jsx("span", { className: "block truncate text-[11.5px] text-nav-fg/55", children: a.description }) : null] }), isActive ? _jsx(Check, { "aria-hidden": true, className: "size-4 shrink-0 text-accent" }) : null] }));
413
+ const inner = (_jsxs(_Fragment, { children: [_jsx("span", { className: cn("grid size-7 shrink-0 place-items-center rounded-[8px] [&_svg]:size-[15px]", isActive ? "bg-accent text-accent-fg" : "bg-nav-active text-nav-fg-strong"), children: _jsx(AppIcon, { icon: a.icon }) }), _jsxs("span", { className: "min-w-0 flex-1 leading-tight", children: [_jsx("span", { className: "block text-[13px] font-medium text-nav-fg-strong", children: a.name }), a.description ? _jsx("span", { className: "block truncate text-[11.5px] text-nav-fg/70", children: a.description }) : null] }), isActive ? _jsx(Check, { "aria-hidden": true, className: "size-4 shrink-0 text-accent" }) : null] }));
414
414
  const cls = "flex w-full items-center gap-2.5 rounded-ui-sm px-2 py-1.5 text-start hover:bg-nav-hover focus-ring";
415
415
  return (_jsx("li", { children: onSelect ? (_jsx("button", { type: "button", "aria-current": isActive ? "true" : undefined, className: cls, onClick: () => {
416
416
  onSelect(a.id);