@vamos-ui/react 0.2.0 → 0.4.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/block-image.js +1 -1
- package/dist/blocks/block-link.js +1 -1
- package/dist/blocks/index.d.ts +2 -2
- package/dist/blocks/index.js +2 -2
- package/dist/blocks/site/_kit-1.d.ts +1 -0
- package/dist/blocks/site/_kit-1.js +3 -1
- package/dist/blocks/site/announcement-bar.js +1 -1
- package/dist/blocks/site/article-layout.d.ts +1 -0
- package/dist/blocks/site/article-layout.js +2 -1
- package/dist/blocks/site/blog-list.d.ts +1 -0
- package/dist/blocks/site/blog-list.js +3 -2
- package/dist/blocks/site/contact-dock.js +3 -1
- package/dist/blocks/site/contact-form.js +1 -1
- package/dist/blocks/site/cta.d.ts +2 -2
- package/dist/blocks/site/cta.js +8 -5
- package/dist/blocks/site/faq.js +2 -1
- package/dist/blocks/site/feature-grid.d.ts +1 -0
- package/dist/blocks/site/feature-grid.js +7 -5
- package/dist/blocks/site/footer.d.ts +1 -1
- package/dist/blocks/site/footer.js +8 -7
- package/dist/blocks/site/gallery-section.d.ts +6 -1
- package/dist/blocks/site/gallery-section.js +6 -4
- package/dist/blocks/site/hero.d.ts +3 -2
- package/dist/blocks/site/hero.js +14 -6
- package/dist/blocks/site/logo-strip.d.ts +6 -3
- package/dist/blocks/site/logo-strip.js +8 -7
- package/dist/blocks/site/map-hours.d.ts +2 -2
- package/dist/blocks/site/map-hours.js +4 -2
- package/dist/blocks/site/media-text.d.ts +4 -1
- package/dist/blocks/site/media-text.js +6 -4
- package/dist/blocks/site/menu-list.d.ts +7 -3
- package/dist/blocks/site/menu-list.js +34 -7
- package/dist/blocks/site/navbar.d.ts +1 -0
- package/dist/blocks/site/navbar.js +6 -5
- package/dist/blocks/site/not-found.js +2 -1
- package/dist/blocks/site/page-intro.d.ts +1 -0
- package/dist/blocks/site/page-intro.js +2 -1
- package/dist/blocks/site/pricing-table.js +2 -1
- package/dist/blocks/site/process.d.ts +1 -0
- package/dist/blocks/site/process.js +4 -2
- package/dist/blocks/site/registry-1.d.ts +1 -1
- package/dist/blocks/site/registry-2.d.ts +5 -0
- package/dist/blocks/site/registry-2.js +2 -2
- package/dist/blocks/site/rich-text-section.js +2 -1
- package/dist/blocks/site/shared/heading-lines.d.ts +9 -0
- package/dist/blocks/site/shared/heading-lines.js +32 -0
- package/dist/blocks/site/shared/image-sizes.d.ts +25 -0
- package/dist/blocks/site/shared/image-sizes.js +37 -0
- package/dist/blocks/site/shared/island-keys.d.ts +1 -1
- package/dist/blocks/site/shared/island-keys.js +1 -1
- package/dist/blocks/site/shared/labels-2.d.ts +9 -1
- package/dist/blocks/site/shared/labels-2.js +24 -6
- package/dist/blocks/site/shared/open-now.d.ts +1 -1
- package/dist/blocks/site/shared/open-now.js +1 -1
- package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
- package/dist/blocks/site/shared/opening-hours.js +36 -7
- package/dist/blocks/site/shared/tap.d.ts +9 -0
- package/dist/blocks/site/shared/tap.js +16 -0
- 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/team.d.ts +1 -1
- package/dist/blocks/site/team.js +4 -3
- package/dist/blocks/site/testimonials.d.ts +2 -2
- package/dist/blocks/site/testimonials.js +5 -4
- 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 +6 -2
- package/dist/charts/chart-shell.d.ts +9 -2
- package/dist/charts/chart-shell.js +10 -4
- package/dist/charts/chart.js +1 -1
- package/dist/charts/combo-chart.js +5 -7
- package/dist/charts/utils.d.ts +10 -0
- package/dist/charts/utils.js +46 -0
- package/dist/components/_qr-core.d.ts +26 -0
- package/dist/components/_qr-core.js +376 -0
- package/dist/components/activity-timeline.js +5 -5
- package/dist/components/aspect-ratio.d.ts +26 -0
- package/dist/components/aspect-ratio.js +42 -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/carousel.d.ts +12 -3
- package/dist/components/carousel.js +8 -5
- package/dist/components/collapsible.d.ts +36 -0
- package/dist/components/collapsible.js +37 -0
- package/dist/components/command.d.ts +8 -2
- package/dist/components/command.js +7 -2
- package/dist/components/image.d.ts +18 -2
- package/dist/components/image.js +12 -5
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/menubar.d.ts +96 -0
- package/dist/components/menubar.js +123 -0
- package/dist/components/pin-map.d.ts +58 -0
- package/dist/components/pin-map.js +167 -0
- package/dist/components/placeholder-art.d.ts +51 -0
- package/dist/components/placeholder-art.js +82 -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 +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/public.d.ts +1 -0
- package/dist/lib/public.js +1 -0
- package/dist/lib/value-format.js +1 -1
- package/dist/locale-data/core/holidays.d.ts +1 -1
- package/dist/locale-data/core/holidays.js +1 -1
- package/dist/locales/ar.d.ts +2087 -0
- package/dist/locales/ar.js +2088 -0
- package/dist/locales/de.d.ts +2087 -0
- package/dist/locales/de.js +2088 -0
- package/dist/locales/en.d.ts +1 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/es.d.ts +2087 -0
- package/dist/locales/es.js +2087 -0
- package/dist/locales/fr.d.ts +2087 -0
- package/dist/locales/fr.js +2088 -0
- package/dist/locales/tr.d.ts +1 -0
- package/dist/locales/tr.js +1 -0
- package/dist/media/gallery.js +3 -1
- package/dist/media/media-library.js +1 -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 +32 -0
- package/dist/motion/text-reveal.js +31 -0
- package/dist/motion/ticker.d.ts +35 -0
- package/dist/motion/ticker.js +16 -0
- package/dist/render/block-renderer.js +1 -1
- package/dist/sortable/sortable-tree.d.ts +4 -0
- package/dist/sortable/sortable-tree.js +16 -4
- package/dist/styles/blocks.css +21 -12
- package/dist/styles/editors.css +2 -1
- package/dist/styles/media.css +31 -5
- package/dist/styles/motion.css +389 -0
- package/dist/styles.css +41 -10
- package/package.json +5 -1
|
@@ -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,51 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type PlaceholderArtPattern = "grid" | "dots" | "lines" | "rings" | "glow";
|
|
3
|
+
export declare const PLACEHOLDER_ART_PATTERNS: PlaceholderArtPattern[];
|
|
4
|
+
/** FNV-1a 32-bit — stable across server and client, so SSR and hydration agree. */
|
|
5
|
+
export declare function hashSeed(seed: string): number;
|
|
6
|
+
export interface PlaceholderArtRecipe {
|
|
7
|
+
pattern: PlaceholderArtPattern;
|
|
8
|
+
/** Chart palette slot (1–6) used as the tint. */
|
|
9
|
+
hue: number;
|
|
10
|
+
/** Second slot for the glow. */
|
|
11
|
+
hue2: number;
|
|
12
|
+
/** Line angle in degrees (lines). */
|
|
13
|
+
angle: number;
|
|
14
|
+
/** Pattern cell in px. */
|
|
15
|
+
cell: number;
|
|
16
|
+
/** Focal point in % (rings, glow, highlight). */
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
}
|
|
20
|
+
/** The same seed always gives the same recipe. `pattern` overrides the seeded choice. */
|
|
21
|
+
export declare function placeholderArtRecipe(seed: string, pattern?: PlaceholderArtPattern | "auto"): PlaceholderArtRecipe;
|
|
22
|
+
/** CSS background for a recipe. Token colours only, mixed per theme by the browser. */
|
|
23
|
+
export declare function placeholderArtStyle(recipe: PlaceholderArtRecipe, strength?: "subtle" | "bold"): React.CSSProperties;
|
|
24
|
+
export interface PlaceholderArtProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
25
|
+
/** Any stable string (a slug, a name, a record id). Same seed → same art, on server and client. */
|
|
26
|
+
seed?: string;
|
|
27
|
+
/** Force a pattern. Default `"auto"` (picked from the seed). */
|
|
28
|
+
pattern?: PlaceholderArtPattern | "auto";
|
|
29
|
+
/** Centre icon. Default: an image glyph. `false` hides it. */
|
|
30
|
+
icon?: React.ReactNode | false;
|
|
31
|
+
/**
|
|
32
|
+
* What the missing image would show. Set → `role="img"` with this as its name; empty → decorative
|
|
33
|
+
* (`aria-hidden`).
|
|
34
|
+
*/
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Print the label (or a custom node) in the bottom corner, e.g. "Photo · Harbour terrace". */
|
|
37
|
+
caption?: boolean | React.ReactNode;
|
|
38
|
+
/** `bold` doubles the tint — for large hero areas on plain pages. Default `subtle`. */
|
|
39
|
+
strength?: "subtle" | "bold";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A tasteful, deterministic stand-in for a missing image: a token-tinted pattern (grid, dots, lines,
|
|
43
|
+
* rings or glow) chosen from a seed, an optional glyph and caption. No assets, no network, no
|
|
44
|
+
* dependencies; follows theme and accent. Fills its parent — give the parent a size, or use it inside
|
|
45
|
+
* `AspectRatio` / `Image`.
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <AspectRatio ratio="4/3"><PlaceholderArt seed="harbour-terrace" label="Harbour terrace at dusk" /></AspectRatio>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const PlaceholderArt: React.ForwardRefExoticComponent<PlaceholderArtProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Image as ImageIcon } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
export const PLACEHOLDER_ART_PATTERNS = ["grid", "dots", "lines", "rings", "glow"];
|
|
6
|
+
/** FNV-1a 32-bit — stable across server and client, so SSR and hydration agree. */
|
|
7
|
+
export function hashSeed(seed) {
|
|
8
|
+
let h = 0x811c9dc5;
|
|
9
|
+
for (let i = 0; i < seed.length; i++) {
|
|
10
|
+
h ^= seed.charCodeAt(i);
|
|
11
|
+
h = Math.imul(h, 0x01000193);
|
|
12
|
+
}
|
|
13
|
+
return h >>> 0;
|
|
14
|
+
}
|
|
15
|
+
/** The same seed always gives the same recipe. `pattern` overrides the seeded choice. */
|
|
16
|
+
export function placeholderArtRecipe(seed, pattern) {
|
|
17
|
+
const h = hashSeed(seed || "vamos");
|
|
18
|
+
const pick = (shift, n) => (h >>> shift) % n;
|
|
19
|
+
const hue = pick(0, 6) + 1;
|
|
20
|
+
return {
|
|
21
|
+
pattern: pattern && pattern !== "auto" ? pattern : PLACEHOLDER_ART_PATTERNS[pick(3, PLACEHOLDER_ART_PATTERNS.length)],
|
|
22
|
+
hue,
|
|
23
|
+
hue2: ((hue + pick(6, 4)) % 6) + 1,
|
|
24
|
+
angle: [30, 45, 60, 120, 135, 150][pick(9, 6)],
|
|
25
|
+
cell: [14, 18, 22, 26][pick(12, 4)],
|
|
26
|
+
x: 15 + pick(14, 70),
|
|
27
|
+
y: 10 + pick(21, 60),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** CSS background for a recipe. Token colours only, mixed per theme by the browser. */
|
|
31
|
+
export function placeholderArtStyle(recipe, strength = "subtle") {
|
|
32
|
+
const { pattern, hue, hue2, angle, cell, x, y } = recipe;
|
|
33
|
+
const tint = `var(--chart-${hue})`;
|
|
34
|
+
const k = strength === "bold" ? 1.6 : 1;
|
|
35
|
+
const ink = `color-mix(in oklab, ${tint} ${Math.round(30 * k)}%, transparent)`;
|
|
36
|
+
const base = `color-mix(in oklab, ${tint} ${Math.round(9 * k)}%, var(--surface-2))`;
|
|
37
|
+
const highlight = `radial-gradient(120% 90% at ${x}% ${y}%, color-mix(in oklab, ${tint} ${Math.round(16 * k)}%, transparent), transparent 65%)`;
|
|
38
|
+
let layers;
|
|
39
|
+
let sizes;
|
|
40
|
+
switch (pattern) {
|
|
41
|
+
case "grid":
|
|
42
|
+
layers = [highlight, `linear-gradient(${ink} 1px, transparent 1px)`, `linear-gradient(90deg, ${ink} 1px, transparent 1px)`];
|
|
43
|
+
sizes = ["100% 100%", `${cell}px ${cell}px`, `${cell}px ${cell}px`];
|
|
44
|
+
break;
|
|
45
|
+
case "dots":
|
|
46
|
+
layers = [highlight, `radial-gradient(circle, ${ink} 1.1px, transparent 1.6px)`];
|
|
47
|
+
sizes = ["100% 100%", `${cell}px ${cell}px`];
|
|
48
|
+
break;
|
|
49
|
+
case "lines":
|
|
50
|
+
layers = [highlight, `repeating-linear-gradient(${angle}deg, ${ink} 0 1px, transparent 1px ${Math.round(cell / 2)}px)`];
|
|
51
|
+
sizes = ["100% 100%", "100% 100%"];
|
|
52
|
+
break;
|
|
53
|
+
case "rings":
|
|
54
|
+
layers = [highlight, `repeating-radial-gradient(circle at ${x}% ${y}%, transparent 0 ${cell - 1}px, ${ink} ${cell - 1}px ${cell}px)`];
|
|
55
|
+
sizes = ["100% 100%", "100% 100%"];
|
|
56
|
+
break;
|
|
57
|
+
case "glow":
|
|
58
|
+
default:
|
|
59
|
+
layers = [
|
|
60
|
+
`radial-gradient(70% 80% at ${x}% ${y}%, color-mix(in oklab, ${tint} ${Math.round(34 * k)}%, transparent), transparent 70%)`,
|
|
61
|
+
`radial-gradient(60% 70% at ${100 - x}% ${100 - y}%, color-mix(in oklab, var(--chart-${hue2}) ${Math.round(26 * k)}%, transparent), transparent 70%)`,
|
|
62
|
+
];
|
|
63
|
+
sizes = ["100% 100%", "100% 100%"];
|
|
64
|
+
}
|
|
65
|
+
return { backgroundColor: base, backgroundImage: layers.join(", "), backgroundSize: sizes.join(", "), backgroundPosition: "center" };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A tasteful, deterministic stand-in for a missing image: a token-tinted pattern (grid, dots, lines,
|
|
69
|
+
* rings or glow) chosen from a seed, an optional glyph and caption. No assets, no network, no
|
|
70
|
+
* dependencies; follows theme and accent. Fills its parent — give the parent a size, or use it inside
|
|
71
|
+
* `AspectRatio` / `Image`.
|
|
72
|
+
*
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <AspectRatio ratio="4/3"><PlaceholderArt seed="harbour-terrace" label="Harbour terrace at dusk" /></AspectRatio>
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const PlaceholderArt = React.forwardRef(function PlaceholderArt({ seed, pattern = "auto", icon, label, caption, strength = "subtle", className, style, ...props }, ref) {
|
|
78
|
+
const recipe = placeholderArtRecipe(seed ?? label ?? "", pattern);
|
|
79
|
+
const a11y = label ? { role: "img", "aria-label": label } : { "aria-hidden": true };
|
|
80
|
+
const captionNode = caption === true ? label : caption || null;
|
|
81
|
+
return (_jsxs("div", { ref: ref, "data-slot": "placeholder-art", "data-pattern": recipe.pattern, ...a11y, className: cn("relative flex size-full min-h-16 items-center justify-center overflow-hidden text-fg-subtle", className), style: { ...placeholderArtStyle(recipe, strength), ...style }, ...props, children: [icon !== false ? (_jsx("span", { "aria-hidden": true, "data-slot": "placeholder-art-icon", className: "inline-flex size-10 items-center justify-center rounded-full border border-line bg-[color-mix(in_oklab,var(--surface)_72%,transparent)] [&_svg]:size-[18px]", children: icon ?? _jsx(ImageIcon, { strokeWidth: 1.5 }) })) : null, captionNode ? (_jsx("span", { "aria-hidden": true, "data-slot": "placeholder-art-caption", className: "absolute start-3 bottom-2.5 max-w-[calc(100%-1.5rem)] truncate font-mono text-[10.5px] tracking-[0.06em] text-fg-muted uppercase", children: captionNode })) : null] }));
|
|
82
|
+
});
|
|
@@ -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);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type SlaState = "on-track" | "at-risk" | "breached" | "met" | "missed" | "paused";
|
|
3
|
+
export declare const slaTimerLabels: {
|
|
4
|
+
dueIn: string;
|
|
5
|
+
overdue: string;
|
|
6
|
+
met: string;
|
|
7
|
+
missed: string;
|
|
8
|
+
paused: string;
|
|
9
|
+
pausedOverdue: string;
|
|
10
|
+
stateOnTrack: string;
|
|
11
|
+
stateAtRisk: string;
|
|
12
|
+
stateBreached: string;
|
|
13
|
+
stateMet: string;
|
|
14
|
+
stateMissed: string;
|
|
15
|
+
statePaused: string;
|
|
16
|
+
};
|
|
17
|
+
export type SlaTimerLabels = typeof slaTimerLabels;
|
|
18
|
+
type DateLike = string | number | Date;
|
|
19
|
+
export interface SlaModel {
|
|
20
|
+
state: SlaState;
|
|
21
|
+
/** Milliseconds left (negative when overdue); frozen for met/missed/paused. */
|
|
22
|
+
remaining: number;
|
|
23
|
+
/** Share of the window used, 0…1+ (needs `start`). */
|
|
24
|
+
used: number | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pure SLA state: `on-track`, `at-risk` (less than `warnAt` of the window or `warnMinutes`
|
|
28
|
+
* left), `breached`, and the final `met` / `missed` once `completedAt` is set.
|
|
29
|
+
*/
|
|
30
|
+
export declare function slaState({ due, start, completedAt, pausedAt, now, warnAt, warnMinutes }: {
|
|
31
|
+
due: DateLike;
|
|
32
|
+
start?: DateLike;
|
|
33
|
+
completedAt?: DateLike | null;
|
|
34
|
+
pausedAt?: DateLike | null;
|
|
35
|
+
now: number;
|
|
36
|
+
warnAt?: number;
|
|
37
|
+
warnMinutes?: number;
|
|
38
|
+
}): SlaModel;
|
|
39
|
+
export interface SlaTimerProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
40
|
+
/** Deadline (ISO string, epoch ms or Date). */
|
|
41
|
+
due: DateLike;
|
|
42
|
+
/** When the clock started — enables the proportional warning and the bar. */
|
|
43
|
+
start?: DateLike;
|
|
44
|
+
/** Target reached (first reply sent, ticket solved) → `met` or `missed`, frozen. */
|
|
45
|
+
completedAt?: DateLike | null;
|
|
46
|
+
/** Clock stopped (waiting on customer) → `paused`, frozen. */
|
|
47
|
+
pausedAt?: DateLike | null;
|
|
48
|
+
/** Fixed "now" (epoch ms) for static screens, tests and screenshots. Omit to tick live. */
|
|
49
|
+
now?: number;
|
|
50
|
+
/** Warn when less than this share of the window is left (needs `start`). Default `0.25`. */
|
|
51
|
+
warnAt?: number;
|
|
52
|
+
/** Warn when fewer minutes than this are left (overrides `warnAt`). */
|
|
53
|
+
warnMinutes?: number;
|
|
54
|
+
/** `"badge"` (default), `"inline"`, or `"bar"` (label row + progress line). */
|
|
55
|
+
variant?: "badge" | "inline" | "bar";
|
|
56
|
+
/** What the SLA measures, prefixed to the accessible text: "First response". */
|
|
57
|
+
label?: string;
|
|
58
|
+
/** Show seconds under an hour left. Default `true`. */
|
|
59
|
+
seconds?: boolean;
|
|
60
|
+
/** Hide the icon (badge / inline). */
|
|
61
|
+
hideIcon?: boolean;
|
|
62
|
+
labels?: Partial<SlaTimerLabels>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Countdown to a service-level deadline — first response, resolution, delivery window.
|
|
66
|
+
* Colour follows the state (on track → at risk → breached), and it freezes as met/missed
|
|
67
|
+
* once `completedAt` is set. `role="timer"` with a full sentence for screen readers.
|
|
68
|
+
*
|
|
69
|
+
* ```tsx
|
|
70
|
+
* <SlaTimer label="First response" start={ticket.createdAt} due={ticket.firstResponseDue} />
|
|
71
|
+
* <SlaTimer variant="bar" label="Resolution" start={t.createdAt} due={t.resolveDue} completedAt={t.solvedAt} />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare const SlaTimer: React.ForwardRefExoticComponent<SlaTimerProps & React.RefAttributes<HTMLSpanElement>>;
|
|
75
|
+
export {};
|