@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.
- package/dist/blocks/site/_kit-1.d.ts +1 -0
- package/dist/blocks/site/_kit-1.js +2 -0
- package/dist/blocks/site/cta.js +2 -1
- package/dist/blocks/site/faq.js +2 -1
- package/dist/blocks/site/feature-grid.js +2 -1
- package/dist/blocks/site/gallery-section.js +3 -2
- package/dist/blocks/site/hero.js +12 -6
- package/dist/blocks/site/logo-strip.d.ts +5 -3
- package/dist/blocks/site/logo-strip.js +8 -7
- package/dist/blocks/site/media-text.js +2 -1
- package/dist/blocks/site/pricing-table.js +2 -1
- package/dist/blocks/site/process.js +2 -1
- package/dist/blocks/site/site-shell.d.ts +11 -0
- package/dist/blocks/site/site-shell.js +43 -16
- package/dist/blocks/site/stats-band.d.ts +3 -1
- package/dist/blocks/site/stats-band.js +11 -7
- package/dist/blocks/site/testimonials.js +2 -1
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.js +2 -0
- package/dist/business/signer-status.d.ts +53 -0
- package/dist/business/signer-status.js +76 -0
- package/dist/charts/bar-chart.js +5 -1
- package/dist/charts/combo-chart.js +1 -1
- package/dist/components/_qr-core.d.ts +26 -0
- package/dist/components/_qr-core.js +376 -0
- package/dist/components/backup-codes.d.ts +42 -0
- package/dist/components/backup-codes.js +67 -0
- package/dist/components/breadcrumb.d.ts +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/command.d.ts +8 -2
- package/dist/components/command.js +7 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/pin-map.d.ts +58 -0
- package/dist/components/pin-map.js +167 -0
- package/dist/components/qr-code.d.ts +36 -0
- package/dist/components/qr-code.js +27 -0
- package/dist/components/record-detail-layout.d.ts +2 -2
- package/dist/components/record-detail-layout.js +1 -1
- package/dist/components/sidebar.js +8 -8
- package/dist/components/sla-timer.d.ts +75 -0
- package/dist/components/sla-timer.js +142 -0
- package/dist/components/slug-input.js +1 -1
- package/dist/components/tour.d.ts +60 -0
- package/dist/components/tour.js +139 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/public.d.ts +1 -0
- package/dist/lib/public.js +1 -0
- package/dist/locale-data/core/holidays.d.ts +1 -1
- package/dist/locale-data/core/holidays.js +1 -1
- package/dist/media/gallery.js +3 -1
- package/dist/motion/core.d.ts +32 -0
- package/dist/motion/core.js +77 -0
- package/dist/motion/count-up.d.ts +29 -0
- package/dist/motion/count-up.js +92 -0
- package/dist/motion/index.d.ts +11 -0
- package/dist/motion/index.js +13 -0
- package/dist/motion/motion-scope.d.ts +17 -0
- package/dist/motion/motion-scope.js +16 -0
- package/dist/motion/page-transition.d.ts +33 -0
- package/dist/motion/page-transition.js +61 -0
- package/dist/motion/parallax.d.ts +37 -0
- package/dist/motion/parallax.js +90 -0
- package/dist/motion/pointer.d.ts +38 -0
- package/dist/motion/pointer.js +117 -0
- package/dist/motion/reveal.d.ts +72 -0
- package/dist/motion/reveal.js +133 -0
- package/dist/motion/scroll-shrink.d.ts +18 -0
- package/dist/motion/scroll-shrink.js +47 -0
- package/dist/motion/skeleton-swap.d.ts +17 -0
- package/dist/motion/skeleton-swap.js +43 -0
- package/dist/motion/text-reveal.d.ts +30 -0
- package/dist/motion/text-reveal.js +29 -0
- package/dist/motion/ticker.d.ts +35 -0
- package/dist/motion/ticker.js +16 -0
- package/dist/sortable/sortable-tree.d.ts +4 -0
- package/dist/sortable/sortable-tree.js +16 -4
- package/dist/styles/blocks.css +3 -3
- package/dist/styles/motion.css +389 -0
- package/dist/styles.css +2 -1
- 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 ›
|
|
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: "
|
|
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: "
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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>>;
|
package/dist/components/input.js
CHANGED
|
@@ -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="
|
|
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 ·
|
|
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 ·
|
|
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 ·
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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);
|