@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
|
@@ -3,6 +3,8 @@ import { cn } from "../../lib/cn.js";
|
|
|
3
3
|
import { Container, Section } from "../section.js";
|
|
4
4
|
import { SectionHeader } from "../section-header.js";
|
|
5
5
|
import { RichText } from "../rich-text.js";
|
|
6
|
+
import { CountUp } from "../../motion/count-up.js";
|
|
7
|
+
import { Reveal, Stagger } from "../../motion/reveal.js";
|
|
6
8
|
import { siteText, useBlockIds } from "./_kit-1.js";
|
|
7
9
|
function formatValue(v, locale) {
|
|
8
10
|
if (typeof v !== "number")
|
|
@@ -12,7 +14,9 @@ function formatValue(v, locale) {
|
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Concrete, sourced numbers ("4.8 on Google · 1,240 reviews"). Semantic `dl`, Intl formatting,
|
|
15
|
-
* units spelled out for screen readers
|
|
17
|
+
* units spelled out for screen readers. Numeric figures count up once when they scroll into view
|
|
18
|
+
* (motion kit CountUp: width reserved, final value for screen readers, still under reduced
|
|
19
|
+
* motion / `motion="off"`); the items reveal in sequence. Server component.
|
|
16
20
|
*
|
|
17
21
|
* ```tsx
|
|
18
22
|
* <StatsBand items={[{ value: 4.8, label: "Google rating", source: "1,240 reviews, Nov 2026" }]} />
|
|
@@ -23,19 +27,19 @@ export function StatsBand({ variant = "inline", eyebrow, title, lead, body, item
|
|
|
23
27
|
const { base, headingId } = useBlockIds(id, "stats");
|
|
24
28
|
const figure = (s, size) => {
|
|
25
29
|
const spoken = s.unitLabel ?? (s.unit === "%" ? t("percent") : s.unit);
|
|
26
|
-
return (_jsxs("span", { className: cn("font-display tabular text-fg", size), children: [formatValue(s.value, locale), s.unit ? (_jsxs(_Fragment, { children: [_jsx("sup", { "aria-hidden": true, className: "vb-stat-unit ms-[0.08em] align-super font-sans text-[0.38em] font-medium tracking-normal text-fg-muted", children: s.unit }), _jsxs("span", { className: "sr-only", children: [" ", spoken] })] })) : null] }));
|
|
30
|
+
return (_jsxs("span", { className: cn("font-display tabular text-fg", size), children: [typeof s.value === "number" && !editMode ? _jsx(CountUp, { value: s.value, locale: locale }) : formatValue(s.value, locale), s.unit ? (_jsxs(_Fragment, { children: [_jsx("sup", { "aria-hidden": true, className: "vb-stat-unit ms-[0.08em] align-super font-sans text-[0.38em] font-medium tracking-normal text-fg-muted", children: s.unit }), _jsxs("span", { className: "sr-only", children: [" ", spoken] })] })) : null] }));
|
|
27
31
|
};
|
|
28
32
|
const source = (s, cls) => s.source ? (_jsxs("dd", { className: cn("font-mono text-[0.6875rem] leading-snug tracking-[0.02em] text-fg-subtle", cls), children: [_jsx("span", { className: "sr-only", children: t("source", { source: "" }) }), s.source] })) : null;
|
|
29
|
-
const header = title || eyebrow || (lead && variant !== "with-text") ? (_jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: variant === "with-text" ? undefined : lead, headingLevel: headingLevel, headingId: headingId, editMode: editMode, labels: labels, locale: locale })) : null;
|
|
33
|
+
const header = title || eyebrow || (lead && variant !== "with-text") ? (_jsx(Reveal, { disabled: editMode, children: _jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: variant === "with-text" ? undefined : lead, headingLevel: headingLevel, headingId: headingId, editMode: editMode, labels: labels, locale: locale }) })) : null;
|
|
30
34
|
let content;
|
|
31
35
|
if (variant === "ledger") {
|
|
32
|
-
content = (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12", children: [header ? _jsx("div", { className: "@4xl:col-span-4", children: header }) : null, _jsx("dl", { className: cn("flex flex-col border-t border-line", header ? "@4xl:col-span-7 @4xl:col-start-6" : "@4xl:col-span-12"), children: items.map((s, i) => (_jsxs("div", { className: "grid grid-cols-[auto_1fr_auto] items-baseline gap-x-4 gap-y-1 border-b border-line py-5", "data-field": editMode ? `props.items.${i}` : undefined, children: [_jsx("dt", { className: "text-body text-fg", children: s.label }), _jsx("span", { "aria-hidden": true, className: "vb-leader self-end" }), _jsx("dd", { className: "text-end", children: figure(s, "text-display-m") }), source(s, "col-span-3")] }, i))) })] }));
|
|
36
|
+
content = (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12", children: [header ? _jsx("div", { className: "@4xl:col-span-4", children: header }) : null, _jsx(Stagger, { asChild: true, effect: "fade", disabled: editMode, children: _jsx("dl", { className: cn("flex flex-col border-t border-line", header ? "@4xl:col-span-7 @4xl:col-start-6" : "@4xl:col-span-12"), children: items.map((s, i) => (_jsxs("div", { className: "grid grid-cols-[auto_1fr_auto] items-baseline gap-x-4 gap-y-1 border-b border-line py-5", "data-field": editMode ? `props.items.${i}` : undefined, children: [_jsx("dt", { className: "text-body text-fg", children: s.label }), _jsx("span", { "aria-hidden": true, className: "vb-leader self-end" }), _jsx("dd", { className: "text-end", children: figure(s, "text-display-m") }), source(s, "col-span-3")] }, i))) }) })] }));
|
|
33
37
|
}
|
|
34
38
|
else if (variant === "with-text") {
|
|
35
|
-
content = (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-12 @4xl:grid-cols-12 @4xl:items-start", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-5", children: [header, lead ? _jsx("p", { className: "vb-lead text-lead text-fg-muted", children: lead }) : null, body ? _jsx(RichText, { doc: body, components: components, labels: labels, locale: locale, headingOffset: 1, editMode: editMode, field: "props.body" }) : null] }), _jsx("dl", { className: "grid grid-cols-2 border-s border-t border-line @4xl:col-span-6 @4xl:col-start-7", children: items.slice(0, 4).map((s, i) => (_jsxs("div", { className: "flex flex-col gap-2 border-e border-b border-line p-[clamp(1rem,3cqi,2rem)]", "data-field": editMode ? `props.items.${i}` : undefined, children: [_jsx("dt", { className: "order-2 text-small text-fg-muted text-pretty", children: s.label }), _jsx("dd", { className: "order-1", children: figure(s, "text-display-l") }), source(s, "order-3 mt-auto pt-3")] }, i))) })] }));
|
|
39
|
+
content = (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-12 @4xl:grid-cols-12 @4xl:items-start", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-5", children: [header, lead ? _jsx("p", { className: "vb-lead text-lead text-fg-muted", children: lead }) : null, body ? _jsx(RichText, { doc: body, components: components, labels: labels, locale: locale, headingOffset: 1, editMode: editMode, field: "props.body" }) : null] }), _jsx(Stagger, { asChild: true, disabled: editMode, children: _jsx("dl", { className: "grid grid-cols-2 border-s border-t border-line @4xl:col-span-6 @4xl:col-start-7", children: items.slice(0, 4).map((s, i) => (_jsxs("div", { className: "flex flex-col gap-2 border-e border-b border-line p-[clamp(1rem,3cqi,2rem)]", "data-field": editMode ? `props.items.${i}` : undefined, children: [_jsx("dt", { className: "order-2 text-small text-fg-muted text-pretty", children: s.label }), _jsx("dd", { className: "order-1", children: figure(s, "text-display-l") }), source(s, "order-3 mt-auto pt-3")] }, i))) }) })] }));
|
|
36
40
|
}
|
|
37
41
|
else {
|
|
38
|
-
content = (_jsxs("div", { className: "flex flex-col gap-12", children: [header, _jsx("dl", { className: cn("grid grid-cols-2 gap-y-10", items.length >= 3 && "@4xl:grid-cols-3", items.length >= 4 && "@4xl:grid-cols-4", items.length >= 5 && "@6xl:grid-cols-5"), children: items.map((s, i) => (_jsxs("div", { className: "vb-stat-inline flex flex-col gap-2 border-line ps-[clamp(0.75rem,2.5cqi,2rem)]", "data-field": editMode ? `props.items.${i}` : undefined, children: [_jsx("dt", { className: "order-2 max-w-[24ch] text-small text-fg-muted text-pretty", children: s.label }), _jsx("dd", { className: "order-1", children: figure(s, "text-display-l") }), source(s, "order-3")] }, i))) })] }));
|
|
42
|
+
content = (_jsxs("div", { className: "flex flex-col gap-12", children: [header, _jsx(Stagger, { asChild: true, disabled: editMode, children: _jsx("dl", { className: cn("grid grid-cols-2 gap-y-10", items.length >= 3 && "@4xl:grid-cols-3", items.length >= 4 && "@4xl:grid-cols-4", items.length >= 5 && "@6xl:grid-cols-5"), children: items.map((s, i) => (_jsxs("div", { className: "vb-stat-inline flex flex-col gap-2 border-line ps-[clamp(0.75rem,2.5cqi,2rem)]", "data-field": editMode ? `props.items.${i}` : undefined, children: [_jsx("dt", { className: "order-2 max-w-[24ch] text-small text-fg-muted text-pretty", children: s.label }), _jsx("dd", { className: "order-1", children: figure(s, "text-display-l") }), source(s, "order-3")] }, i))) }) })] }));
|
|
39
43
|
}
|
|
40
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: base, blockType: "stats", editMode: editMode, className: cn("vb-stats", className), "data-variant": variant, children: _jsx(Container, {
|
|
44
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: base, blockType: "stats", editMode: editMode, className: cn("vb-stats", className), "data-variant": variant, children: _jsx(Container, { children: content }) }));
|
|
41
45
|
}
|
|
@@ -11,7 +11,7 @@ export interface TeamMember {
|
|
|
11
11
|
role: React.ReactNode;
|
|
12
12
|
/** Specialty line in `list` ("Deep tissue · Sports massage"). */
|
|
13
13
|
specialty?: React.ReactNode;
|
|
14
|
-
/** 4:5 portrait. No photo = an empty inset area, never initials. */
|
|
14
|
+
/** 4:5 portrait. No photo = an empty inset area, never initials. `sizes`: grid `min(25vw, 19rem)` / 50vw · rows 56px · feature `min(42vw, 32rem)` / 100vw · compact 160px. */
|
|
15
15
|
photo?: SiteImage2;
|
|
16
16
|
bio?: React.ReactNode;
|
|
17
17
|
/** Up to 3 profile links, rendered as short text links ("LinkedIn"). */
|
package/dist/blocks/site/team.js
CHANGED
|
@@ -7,6 +7,7 @@ import { BlockLink } from "../block-link.js";
|
|
|
7
7
|
import { site2Text } from "./shared/labels-2.js";
|
|
8
8
|
import { fieldAttr, textOf, useBlockIds2 } from "./shared/kit-2.js";
|
|
9
9
|
import { BRAND_NAMES } from "./shared/brand-icons.js";
|
|
10
|
+
import { columnSizes } from "./shared/image-sizes.js";
|
|
10
11
|
/**
|
|
11
12
|
* Team and specialist introduction — spa therapists, clinic doctors, an agency team. Card-less
|
|
12
13
|
* by default (D6-1). Server component; the hover portrait in `list` is CSS only.
|
|
@@ -30,7 +31,7 @@ function ProfileLinks({ member, ctx, className }) {
|
|
|
30
31
|
if (!member.links?.length)
|
|
31
32
|
return null;
|
|
32
33
|
const name = textOf(member.name);
|
|
33
|
-
return (_jsx("span", { className: cn("flex flex-wrap gap-x-3 gap-y-1", className), children: member.links.slice(0, 3).map((l, i) => (_jsx(BlockLink, { href: l.href, newTab: true, components: ctx.components, labels: ctx.labels, locale: ctx.locale, "aria-label": name ? ctx.t("profileOf", { name, network: BRAND_NAMES[l.network] }) : undefined, className: "relative z-10 text-small text-fg-muted underline decoration-line underline-offset-[0.25em] transition-colors hover:text-fg hover:decoration-current focus-ring rounded-[2px]", children: BRAND_NAMES[l.network] }, i))) }));
|
|
34
|
+
return (_jsx("span", { className: cn("flex flex-wrap gap-x-3 gap-y-1 pointer-coarse:gap-y-0", className), children: member.links.slice(0, 3).map((l, i) => (_jsx(BlockLink, { href: l.href, newTab: true, components: ctx.components, labels: ctx.labels, locale: ctx.locale, "aria-label": name ? ctx.t("profileOf", { name, network: BRAND_NAMES[l.network] }) : undefined, className: "relative z-10 inline-flex min-h-6 items-center pointer-coarse:min-h-11 text-small text-fg-muted underline decoration-line underline-offset-[0.25em] transition-colors hover:text-fg hover:decoration-current focus-ring rounded-[2px]", children: BRAND_NAMES[l.network] }, i))) }));
|
|
34
35
|
}
|
|
35
36
|
function Portrait({ photo, ctx, className, sizes }) {
|
|
36
37
|
if (!photo?.src)
|
|
@@ -38,11 +39,11 @@ function Portrait({ photo, ctx, className, sizes }) {
|
|
|
38
39
|
return _jsx(BlockImage, { ...photo, alt: photo.alt ?? "", decorative: !photo.alt, aspect: "4:5", sizes: sizes, className: cn("rounded-ui", className), components: ctx.components });
|
|
39
40
|
}
|
|
40
41
|
function PortraitMember({ member, index, ctx }) {
|
|
41
|
-
return (_jsxs("li", { className: "flex flex-col gap-4", children: [_jsx(Portrait, { photo: member.photo, ctx: ctx, sizes:
|
|
42
|
+
return (_jsxs("li", { className: "flex flex-col gap-4", children: [_jsx(Portrait, { photo: member.photo, ctx: ctx, sizes: columnSizes(1 / 4, { narrow: "50vw" }) }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1", children: [_jsx("h3", { className: "text-h3 font-semibold tracking-[-0.012em] text-fg", ...fieldAttr(ctx.editMode, `members.${index}.name`), children: _jsx(DisplayName, { member: member }) }), _jsx(ProfileLinks, { member: member, ctx: ctx })] }), _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted", children: member.role }), member.bio ? _jsx("p", { className: "mt-2 text-small text-fg-muted text-pretty", children: member.bio }) : null] })] }));
|
|
42
43
|
}
|
|
43
44
|
function ListMember({ member, index, ctx }) {
|
|
44
45
|
return (_jsxs("li", { className: "vb-team-row group/row relative grid grid-cols-[3.5rem_1fr] items-center gap-x-4 gap-y-1 border-t border-line py-6 @2xl:grid-cols-12 @2xl:gap-x-(--gutter)", children: [_jsx("span", { className: "row-span-2 @2xl:hidden", children: _jsx(Portrait, { photo: member.photo, ctx: ctx, className: "w-14 rounded-ui-sm", sizes: "56px" }) }), _jsx("h3", { className: "text-h3 font-semibold tracking-[-0.012em] text-fg @2xl:col-span-4", ...fieldAttr(ctx.editMode, `members.${index}.name`), children: _jsx(DisplayName, { member: member }) }), _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted @2xl:col-span-3", children: member.role }), _jsxs("div", { className: "col-start-2 flex flex-wrap items-baseline gap-x-6 gap-y-1 @2xl:col-span-5 @2xl:col-start-auto @2xl:justify-between", children: [member.specialty ? _jsx("p", { className: "text-small text-fg-muted", children: member.specialty }) : _jsx("span", {}), _jsx(ProfileLinks, { member: member, ctx: ctx })] }), member.photo?.src ? (_jsx("span", { "aria-hidden": true, className: "vb-team-hover pointer-events-none absolute end-[22%] top-1/2 z-10 hidden w-40 -translate-y-1/2 @4xl:block", children: _jsx(BlockImage, { ...member.photo, decorative: true, aspect: "4:5", sizes: "160px", className: "rounded-ui", components: ctx.components }) })) : null] }));
|
|
45
46
|
}
|
|
46
47
|
function FeatureMember({ member, header, ctx }) {
|
|
47
|
-
return (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: "@4xl:col-span-5", children: _jsx(Portrait, { photo: member.photo, ctx: ctx, sizes:
|
|
48
|
+
return (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: "@4xl:col-span-5", children: _jsx(Portrait, { photo: member.photo, ctx: ctx, sizes: columnSizes(5 / 12) }) }), _jsxs("div", { className: "flex flex-col gap-8 @4xl:col-span-6 @4xl:col-start-7 @4xl:pt-6", children: [header, member.bio ? _jsx("div", { className: "text-lead text-fg-muted text-pretty [&_p+p]:mt-4", children: typeof member.bio === "string" ? member.bio.split(/\n{2,}/).map((p, i) => _jsx("p", { children: p }, i)) : member.bio }) : null, _jsxs("div", { className: "flex flex-col gap-2 border-t border-line pt-6", children: [_jsx("p", { className: "vb-signature font-display text-display-m italic text-fg", ...fieldAttr(ctx.editMode, "members.0.name"), children: _jsx(DisplayName, { member: member }) }), _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted", children: member.role }), _jsx(ProfileLinks, { member: member, ctx: ctx, className: "mt-2" })] })] })] }));
|
|
48
49
|
}
|
|
@@ -10,9 +10,9 @@ export interface TestimonialItem {
|
|
|
10
10
|
author: React.ReactNode;
|
|
11
11
|
role?: React.ReactNode;
|
|
12
12
|
company?: React.ReactNode;
|
|
13
|
-
/** Small
|
|
13
|
+
/** Small round portrait, 40px (`sizes="40px"`). No avatar = name only (never an initials circle). */
|
|
14
14
|
avatar?: SiteImage2;
|
|
15
|
-
/** Company logo (feature variant). */
|
|
15
|
+
/** Company logo (feature variant), 6rem wide (`sizes="6rem"`). */
|
|
16
16
|
logo?: SiteImage2;
|
|
17
17
|
/** 1–5. Rendered only with a `source` (D6-8). */
|
|
18
18
|
rating?: number;
|
|
@@ -11,6 +11,7 @@ import { labelTemplates, site2Text } from "./shared/labels-2.js";
|
|
|
11
11
|
import { fieldAttr, formatDate2, isoDay, useBlockIds2 } from "./shared/kit-2.js";
|
|
12
12
|
import { TestimonialsCarousel } from "./shared/testimonials-carousel.js";
|
|
13
13
|
import { carouselLabelKeys } from "./shared/island-keys.js";
|
|
14
|
+
import { Reveal, Stagger } from "../../motion/reveal.js";
|
|
14
15
|
export const REVIEW_SOURCE_NAMES = { google: "Google", yandex: "Yandex", tripadvisor: "Tripadvisor", trustpilot: "Trustpilot", direct: "" };
|
|
15
16
|
/**
|
|
16
17
|
* Customer voices: agency references, Google reviews for a spa or restaurant, SaaS quotes.
|
|
@@ -27,7 +28,7 @@ export function Testimonials({ variant: variantProp = "feature", eyebrow, title,
|
|
|
27
28
|
const variant = variantProp === "carousel" && items.length < 2 ? "feature" : variantProp;
|
|
28
29
|
const ctx = { t, locale, components, labels, editMode };
|
|
29
30
|
const header = title || eyebrow || lead ? _jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, headingId: headingId, headingLevel: headingLevel, editMode: editMode, labels: labels, locale: locale }) : null;
|
|
30
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "testimonials", editMode: editMode, className: cn("vb-testimonials", className), "data-variant": variant, children: _jsx(Container, { className: "flex flex-col gap-12 @4xl:gap-16", children: variant === "carousel" ? (_jsx(TestimonialsCarousel, { header: header, count: items.length, labels: labelTemplates(labels, carouselLabelKeys), locale: locale, children: items.map((item, i) => (_jsx(QuoteFigure, { item: item, index: i, ctx: ctx, size: "m" }, i))) })) : (_jsxs(_Fragment, { children: [variant !== "reviews" ? header : null, variant === "feature" ? (_jsx(FeatureQuote, { item: items[0], ctx: ctx })) : variant === "wall" ? (_jsx("ul", { className: "vb-quote-wall gap-x-(--gutter) @2xl:columns-2 @4xl:columns-3", children: items.map((item, i) => (_jsx("li", { className: "mb-12 break-inside-avoid border-t border-line pt-6", children: _jsx(QuoteFigure, { item: item, index: i, ctx: ctx }) }, i))) })) : (_jsx(ReviewsLayout, { header: header, items: items, summary: summary, ctx: ctx }))] })) }) }));
|
|
31
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "testimonials", editMode: editMode, className: cn("vb-testimonials", className), "data-variant": variant, children: _jsx(Container, { className: "flex flex-col gap-12 @4xl:gap-16", children: variant === "carousel" ? (_jsx(TestimonialsCarousel, { header: header, count: items.length, labels: labelTemplates(labels, carouselLabelKeys), locale: locale, children: items.map((item, i) => (_jsx(QuoteFigure, { item: item, index: i, ctx: ctx, size: "m" }, i))) })) : (_jsxs(_Fragment, { children: [variant !== "reviews" ? header : null, variant === "feature" ? (_jsx(Reveal, { disabled: editMode, children: _jsx(FeatureQuote, { item: items[0], ctx: ctx }) })) : variant === "wall" ? (_jsx(Stagger, { asChild: true, disabled: editMode, children: _jsx("ul", { className: "vb-quote-wall gap-x-(--gutter) @2xl:columns-2 @4xl:columns-3", children: items.map((item, i) => (_jsx("li", { className: "mb-12 break-inside-avoid border-t border-line pt-6", children: _jsx(QuoteFigure, { item: item, index: i, ctx: ctx }) }, i))) }) })) : (_jsx(Reveal, { disabled: editMode, children: _jsx(ReviewsLayout, { header: header, items: items, summary: summary, ctx: ctx }) }))] })) }) }));
|
|
31
32
|
}
|
|
32
33
|
/** Monochrome stars with a text alternative ("4.8 out of 5"). */
|
|
33
34
|
const RATING_STAR_SIZE = { sm: "size-3", md: "size-3.5", lg: "size-5" };
|
|
@@ -41,14 +42,14 @@ export function RatingStars({ rating, label, size = "md", className }) {
|
|
|
41
42
|
}
|
|
42
43
|
function Attribution({ item, ctx, align = "start", as: Comp = "figcaption" }) {
|
|
43
44
|
const meta = [item.role, item.company].filter(Boolean);
|
|
44
|
-
return (_jsxs(Comp, { className: cn("flex items-center gap-3", align === "between" && "justify-between"), children: [_jsxs("span", { className: "flex items-center gap-3", children: [item.avatar ? (_jsx(BlockImage, { ...item.avatar, alt: item.avatar.alt ?? "", decorative: !item.avatar.alt, aspect: "1:1", sizes: "40px", className: "size-10 shrink-0 rounded-full", components: ctx.components })) : null, _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm font-medium text-fg", children: item.author }), meta.length ? (_jsx("span", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted", children: meta.map((m, i) => (_jsxs(React.Fragment, { children: [i > 0 ? " · " : null, m] }, i))) })) : null] })] }), align === "between" && item.logo ? (_jsx(BlockImage, { ...item.logo, alt: item.logo.alt ?? "", decorative: !item.logo.alt, fit: "contain", className: "h-7 w-24 bg-transparent opacity-80", imgClassName: "vb-logo-mono", components: ctx.components })) : null] }));
|
|
45
|
+
return (_jsxs(Comp, { className: cn("flex items-center gap-3", align === "between" && "justify-between"), children: [_jsxs("span", { className: "flex items-center gap-3", children: [item.avatar ? (_jsx(BlockImage, { ...item.avatar, alt: item.avatar.alt ?? "", decorative: !item.avatar.alt, aspect: "1:1", sizes: "40px", className: "size-10 shrink-0 rounded-full", components: ctx.components })) : null, _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm font-medium text-fg", children: item.author }), meta.length ? (_jsx("span", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted", children: meta.map((m, i) => (_jsxs(React.Fragment, { children: [i > 0 ? " · " : null, m] }, i))) })) : null] })] }), align === "between" && item.logo ? (_jsx(BlockImage, { ...item.logo, alt: item.logo.alt ?? "", decorative: !item.logo.alt, fit: "contain", sizes: "6rem", className: "h-7 w-24 bg-transparent opacity-80", imgClassName: "vb-logo-mono", components: ctx.components })) : null] }));
|
|
45
46
|
}
|
|
46
47
|
function SourceLine({ item, ctx }) {
|
|
47
48
|
const showRating = item.rating !== undefined && item.source;
|
|
48
49
|
const name = item.source ? REVIEW_SOURCE_NAMES[item.source] : "";
|
|
49
50
|
if (!showRating && !item.date && !name)
|
|
50
51
|
return null;
|
|
51
|
-
return (_jsxs("p", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-fg-muted", children: [showRating ? _jsx(RatingStars, { rating: item.rating, label: ctx.t("ratingOutOf", { rating: item.rating }) }) : null, name ? (item.link ? (_jsx(BlockLink, { href: item.link.href, newTab: item.link.newTab ?? true, components: ctx.components, labels: ctx.labels, locale: ctx.locale, className: "underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", children: name })) : (_jsx("span", { children: name }))) : null, item.date ? _jsx("time", { dateTime: isoDay(item.date), children: formatDate2(isoDay(item.date), ctx.locale, "medium") }) : null] }));
|
|
52
|
+
return (_jsxs("p", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-fg-muted", children: [showRating ? _jsx(RatingStars, { rating: item.rating, label: ctx.t("ratingOutOf", { rating: item.rating }) }) : null, name ? (item.link ? (_jsx(BlockLink, { href: item.link.href, newTab: item.link.newTab ?? true, components: ctx.components, labels: ctx.labels, locale: ctx.locale, className: "inline-flex min-h-6 items-center underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", children: name })) : (_jsx("span", { children: name }))) : null, item.date ? _jsx("time", { dateTime: isoDay(item.date), children: formatDate2(isoDay(item.date), ctx.locale, "medium") }) : null] }));
|
|
52
53
|
}
|
|
53
54
|
function QuoteFigure({ item, index, ctx, size = "s" }) {
|
|
54
55
|
return (_jsxs("figure", { className: "flex flex-col gap-5", children: [_jsx(SourceLine, { item: item, ctx: ctx }), _jsx("blockquote", { className: cn("text-fg text-pretty", size === "m" ? "text-lead" : "text-body"), ...fieldAttr(ctx.editMode, `items.${index}.quote`), children: _jsxs("p", { children: [_jsx("span", { "aria-hidden": true, children: ctx.t("quoteOpen") }), item.quote, _jsx("span", { "aria-hidden": true, children: ctx.t("quoteClose") })] }) }), _jsx(Attribution, { item: item, ctx: ctx })] }));
|
|
@@ -61,5 +62,5 @@ function ReviewsLayout({ header, items, summary, ctx }) {
|
|
|
61
62
|
const sourceName = summary ? REVIEW_SOURCE_NAMES[summary.source] : "";
|
|
62
63
|
return (_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-12 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-8 @4xl:sticky @4xl:top-[calc(var(--topbar-h,0px)+2rem)] @4xl:col-span-4 @4xl:self-start", children: [header, summary ? (_jsxs("div", { className: "flex flex-col gap-3 border-t border-line pt-6", children: [_jsxs("p", { className: "flex items-baseline gap-3", children: [_jsx("span", { className: "font-display text-display-l tabular text-fg", children: nf.number(summary.rating, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) }), _jsx(RatingStars, { rating: summary.rating, label: ctx.t("ratingOutOf", { rating: nf.number(summary.rating, { maximumFractionDigits: 1 }) }) })] }), _jsxs("p", { className: "text-small text-fg-muted", children: [sourceName
|
|
63
64
|
? ctx.t("ratingSummary", { rating: nf.number(summary.rating, { maximumFractionDigits: 1 }), source: sourceName })
|
|
64
|
-
: ctx.t("ratingSummaryDirect", { rating: nf.number(summary.rating, { maximumFractionDigits: 1 }) }), " ", "\u2014 ", ctx.t("reviewCount", { count: summary.count })] }), summary.link ? (_jsx(BlockLink, { href: summary.link.href, newTab: summary.link.newTab ?? true, appearance: "link", components: ctx.components, labels: ctx.labels, locale: ctx.locale, className: "self-start text-small", children: summary.link.label ?? ctx.t("readAllReviews") })) : null] })) : null] }), _jsx("ol", { className: "flex flex-col @4xl:col-span-7 @4xl:col-start-6", children: items.map((item, i) => (_jsx("li", { className: "border-t border-line py-7 first:border-t-0 first:pt-0 @4xl:first:border-t @4xl:first:pt-7", children: _jsx(QuoteFigure, { item: item, index: i, ctx: ctx }) }, i))) })] }));
|
|
65
|
+
: ctx.t("ratingSummaryDirect", { rating: nf.number(summary.rating, { maximumFractionDigits: 1 }) }), " ", "\u2014 ", ctx.t("reviewCount", { count: summary.count })] }), summary.link ? (_jsx(BlockLink, { href: summary.link.href, newTab: summary.link.newTab ?? true, appearance: "link", components: ctx.components, labels: ctx.labels, locale: ctx.locale, className: "inline-flex min-h-6 items-center self-start text-small", children: summary.link.label ?? ctx.t("readAllReviews") })) : null] })) : null] }), _jsx("ol", { className: "flex flex-col @4xl:col-span-7 @4xl:col-start-6", children: items.map((item, i) => (_jsx("li", { className: "border-t border-line py-7 first:border-t-0 first:pt-0 @4xl:first:border-t @4xl:first:pt-7", children: _jsx(QuoteFigure, { item: item, index: i, ctx: ctx }) }, i))) })] }));
|
|
65
66
|
}
|
package/dist/business/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { exportToBlob, toExportMatrix, exportFilename, downloadBlob, maskValue,
|
|
|
9
9
|
export { SetupChecklist, SetupChecklistLauncher, setupChecklistLabels, type SetupChecklistProps, type SetupChecklistLauncherProps, type SetupChecklistLabels, type SetupItem, type SetupItemStatus, } from "./setup-checklist.js";
|
|
10
10
|
export { PermissionMatrix, PermissionMatrixCell, PermissionDiff, PermissionCompare, permissionMatrixLabels, type PermissionMatrixProps, type PermissionMatrixCellProps, type PermissionDiffProps, type PermissionCompareProps, type PermissionMatrixLabels, type PermissionResource, type PermissionAction, } from "./permission-matrix.js";
|
|
11
11
|
export { diffPermissions, applyDependencies, effectiveLevel, scopeRank, isGranted, setMany, setPermission, SCOPE_ORDER, type PermissionScope, type PermissionLevel, type PermissionValue, type PermissionDependency, type PermissionChange, } from "./permission-core.js";
|
|
12
|
+
export { SignerStatus, signerStatusLabels, type SignerStatusProps, type SignerStatusLabels, type Signer, type SignerState, } from "./signer-status.js";
|
package/dist/business/index.js
CHANGED
|
@@ -18,3 +18,5 @@ export { SetupChecklist, SetupChecklistLauncher, setupChecklistLabels, } from ".
|
|
|
18
18
|
// Permission matrix
|
|
19
19
|
export { PermissionMatrix, PermissionMatrixCell, PermissionDiff, PermissionCompare, permissionMatrixLabels, } from "./permission-matrix.js";
|
|
20
20
|
export { diffPermissions, applyDependencies, effectiveLevel, scopeRank, isGranted, setMany, setPermission, SCOPE_ORDER, } from "./permission-core.js";
|
|
21
|
+
// Signer status (e-signature progress)
|
|
22
|
+
export { SignerStatus, signerStatusLabels, } from "./signer-status.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type SignerState = "waiting" | "sent" | "viewed" | "signed" | "declined";
|
|
3
|
+
export interface Signer {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
/** "Client", "Account manager", "Witness". */
|
|
8
|
+
role?: string;
|
|
9
|
+
status: SignerState;
|
|
10
|
+
/** When the current status happened (ISO date-time). */
|
|
11
|
+
at?: string;
|
|
12
|
+
/** Decline reason or a note. */
|
|
13
|
+
note?: React.ReactNode;
|
|
14
|
+
/** Avatar image. */
|
|
15
|
+
avatarSrc?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const signerStatusLabels: {
|
|
18
|
+
title: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
declinedSummary: string;
|
|
21
|
+
complete: string;
|
|
22
|
+
waiting: string;
|
|
23
|
+
sent: string;
|
|
24
|
+
viewed: string;
|
|
25
|
+
signed: string;
|
|
26
|
+
declined: string;
|
|
27
|
+
remind: string;
|
|
28
|
+
remindName: string;
|
|
29
|
+
reminded: string;
|
|
30
|
+
order: string;
|
|
31
|
+
};
|
|
32
|
+
export type SignerStatusLabels = typeof signerStatusLabels;
|
|
33
|
+
export interface SignerStatusProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
34
|
+
signers: Signer[];
|
|
35
|
+
/** Signers sign in list order — numbers the rows and draws the chain. */
|
|
36
|
+
sequential?: boolean;
|
|
37
|
+
/** Heading. Default "Signatures"; `null` hides the header row. */
|
|
38
|
+
title?: React.ReactNode;
|
|
39
|
+
/** Shows "Remind" on sent / viewed rows. Resolve to show "Reminder sent". */
|
|
40
|
+
onRemind?: (signer: Signer) => void | Promise<void>;
|
|
41
|
+
/** Fixed reference time for relative dates (epoch ms) — tests, screenshots. */
|
|
42
|
+
now?: number;
|
|
43
|
+
labels?: Partial<SignerStatusLabels>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* E-signature progress for a quote, contract or offer letter: who signs, in which order,
|
|
47
|
+
* where each person is (sent, viewed, signed, declined) and when — with a reminder action.
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <SignerStatus sequential signers={contract.signers} onRemind={(s) => api.remind(s.id)} />
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare const SignerStatus: React.ForwardRefExoticComponent<SignerStatusProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { BellRing, Check, Clock3, Eye, Send, X } 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 { Avatar } from "../components/avatar.js";
|
|
9
|
+
import { Button } from "../components/button.js";
|
|
10
|
+
export const signerStatusLabels = {
|
|
11
|
+
title: "Signatures",
|
|
12
|
+
summary: "{signed} of {total} signed",
|
|
13
|
+
declinedSummary: "Declined by {name}",
|
|
14
|
+
complete: "Fully signed",
|
|
15
|
+
waiting: "Waiting",
|
|
16
|
+
sent: "Sent",
|
|
17
|
+
viewed: "Viewed",
|
|
18
|
+
signed: "Signed",
|
|
19
|
+
declined: "Declined",
|
|
20
|
+
remind: "Remind",
|
|
21
|
+
remindName: "Send a reminder to {name}",
|
|
22
|
+
reminded: "Reminder sent",
|
|
23
|
+
order: "Signs {position}",
|
|
24
|
+
};
|
|
25
|
+
const stateIcon = {
|
|
26
|
+
waiting: Clock3,
|
|
27
|
+
sent: Send,
|
|
28
|
+
viewed: Eye,
|
|
29
|
+
signed: Check,
|
|
30
|
+
declined: X,
|
|
31
|
+
};
|
|
32
|
+
const stateTone = {
|
|
33
|
+
waiting: "text-fg-subtle",
|
|
34
|
+
sent: "tone-info tone-soft",
|
|
35
|
+
viewed: "tone-warning tone-soft",
|
|
36
|
+
signed: "tone-success tone-soft",
|
|
37
|
+
declined: "tone-danger tone-soft",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* E-signature progress for a quote, contract or offer letter: who signs, in which order,
|
|
41
|
+
* where each person is (sent, viewed, signed, declined) and when — with a reminder action.
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <SignerStatus sequential signers={contract.signers} onRemind={(s) => api.remind(s.id)} />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const SignerStatus = React.forwardRef(function SignerStatus({ signers, sequential, title, onRemind, now, labels, className, ...props }, ref) {
|
|
48
|
+
const t = useLabels("signerStatus", signerStatusLabels, labels);
|
|
49
|
+
const f = useFormat();
|
|
50
|
+
const titleId = React.useId();
|
|
51
|
+
const [reminded, setReminded] = React.useState({});
|
|
52
|
+
const signed = signers.filter((s) => s.status === "signed").length;
|
|
53
|
+
const declined = signers.find((s) => s.status === "declined");
|
|
54
|
+
const total = signers.length;
|
|
55
|
+
const heading = title === undefined ? t("title") : title;
|
|
56
|
+
const summary = declined ? t("declinedSummary", { name: declined.name }) : signed === total && total > 0 ? t("complete") : t("summary", { signed: f.number(signed), total: f.number(total) });
|
|
57
|
+
const remind = async (s) => {
|
|
58
|
+
if (!onRemind)
|
|
59
|
+
return;
|
|
60
|
+
setReminded((r) => ({ ...r, [s.id]: "busy" }));
|
|
61
|
+
try {
|
|
62
|
+
await onRemind(s);
|
|
63
|
+
setReminded((r) => ({ ...r, [s.id]: "done" }));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
setReminded(({ [s.id]: _drop, ...rest }) => rest);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return (_jsxs("section", { ref: ref, "data-slot": "signer-status", "aria-labelledby": heading ? titleId : undefined, className: cn("grid gap-3", className), ...props, children: [heading ? (_jsxs("header", { className: "grid gap-2", children: [_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: cn("text-xs tabular", declined ? "text-danger" : signed === total ? "text-success" : "text-fg-muted"), children: summary })] }), _jsx("div", { "aria-hidden": "true", className: "flex gap-1", children: signers.map((s) => (_jsx("span", { className: cn("h-1 flex-1 rounded-full", s.status === "signed" ? "bg-success" : s.status === "declined" ? "bg-danger" : s.status === "viewed" ? "bg-warning" : s.status === "sent" ? "bg-info" : "bg-track") }, s.id))) })] })) : null, _jsx("ol", { className: "grid", children: signers.map((s, i) => {
|
|
70
|
+
const Icon = stateIcon[s.status];
|
|
71
|
+
const canRemind = onRemind && (s.status === "sent" || s.status === "viewed");
|
|
72
|
+
const r = reminded[s.id];
|
|
73
|
+
const when = s.at ? f.relative(s.at, now != null ? { now } : undefined) : null;
|
|
74
|
+
return (_jsxs("li", { "data-state": s.status, className: "relative flex items-start gap-3 py-2.5 not-last:border-b not-last:border-line", children: [_jsxs("span", { className: "relative shrink-0", children: [_jsx(Avatar, { name: s.name, src: s.avatarSrc, size: "sm" }), sequential ? (_jsx("span", { "aria-hidden": "true", className: "absolute -end-1 -bottom-1 grid size-4 place-items-center rounded-full bg-surface text-[10px] font-semibold text-fg-muted ring-1 ring-line tabular", children: i + 1 })) : null] }), _jsxs("div", { className: "grid min-w-0 flex-1 gap-0.5", children: [_jsxs("p", { className: "flex min-w-0 items-baseline gap-2 text-[13px]", children: [_jsx("span", { className: "truncate font-medium text-fg", children: s.name }), s.role ? _jsx("span", { className: "truncate text-xs text-fg-subtle", children: s.role }) : null, sequential ? _jsx("span", { className: "sr-only", children: t("order", { position: i + 1 }) }) : null] }), s.email ? _jsx("p", { className: "truncate text-xs text-fg-muted", children: s.email }) : null, s.note ? _jsx("div", { className: "mt-1 text-xs text-fg-muted", children: s.note }) : null] }), _jsxs("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [_jsxs("span", { className: cn("inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[11px] font-medium [&_svg]:size-3", stateTone[s.status]), children: [_jsx(Icon, { "aria-hidden": "true" }), t(s.status)] }), when ? (_jsx("time", { dateTime: s.at, title: f.dateTime(s.at), className: "text-[11px] text-fg-subtle tabular", suppressHydrationWarning: true, children: when })) : null, canRemind ? (r === "done" ? (_jsxs("span", { role: "status", className: "inline-flex items-center gap-1 text-[11px] text-fg-muted", children: [_jsx(Check, { "aria-hidden": "true", className: "size-3" }), t("reminded")] })) : (_jsxs(Button, { size: "xs", variant: "ghost", className: "-me-1.5", loading: r === "busy", "aria-label": t("remindName", { name: s.name }), onClick: () => remind(s), children: [_jsx(BellRing, { "aria-hidden": "true" }), t("remind")] }))) : null] })] }, s.id));
|
|
75
|
+
}) })] }));
|
|
76
|
+
});
|
package/dist/charts/bar-chart.js
CHANGED
|
@@ -14,7 +14,7 @@ import { estimateAxisWidth, extent, formatValue, seriesColor } from "./utils.js"
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export function BarChart({ series, layout = "vertical", stack = "none", radius = 4, highlight, valueLabels, barSize = "auto", reference, ...props }) {
|
|
17
|
-
return (_jsx(Chart, { skeleton: layout === "horizontal" ? "bar-horizontal" : "bar", ...props, children: _jsx(BarPlot, { series: series, layout: layout, stack: stack, radius: radius, highlight: highlight, valueLabels: valueLabels, barSize: barSize, reference: reference }) }));
|
|
17
|
+
return (_jsx(Chart, { skeleton: layout === "horizontal" ? "bar-horizontal" : "bar", barKeys: series ?? Object.keys(props.config), ...props, children: _jsx(BarPlot, { series: series, layout: layout, stack: stack, radius: radius, highlight: highlight, valueLabels: valueLabels, barSize: barSize, reference: reference }) }));
|
|
18
18
|
}
|
|
19
19
|
function BarPlot({ series: keys, layout, stack, radius = 4, highlight, valueLabels, barSize, reference }) {
|
|
20
20
|
const chart = useChart();
|
|
@@ -26,7 +26,11 @@ function BarPlot({ series: keys, layout, stack, radius = 4, highlight, valueLabe
|
|
|
26
26
|
const [min] = extent(chart.data, visible.map((s) => s.key), stacked);
|
|
27
27
|
const valueWidth = useValueAxisWidth(visible.map((s) => s.key), { stacked, percent });
|
|
28
28
|
const categoryWidth = React.useMemo(() => (horizontal && chart.x ? Math.min(160, estimateAxisWidth(chart.data.map((r) => chart.formatX(r[chart.x])), 12)) : 0), [horizontal, chart]);
|
|
29
|
-
|
|
29
|
+
// A time series (dates on x) legitimately has many bars; the hint is for long category lists.
|
|
30
|
+
const timeAxis = (typeof chart.xFormat === "object" && chart.xFormat !== null && ("date" in chart.xFormat || "relative" in chart.xFormat)) ||
|
|
31
|
+
(chart.x != null && typeof chart.data[0]?.[chart.x] === "string" && /^\d{4}-\d{2}-\d{2}/.test(chart.data[0][chart.x])) ||
|
|
32
|
+
(chart.x != null && (chart.data[0]?.[chart.x] instanceof Date || typeof chart.data[0]?.[chart.x] === "number" && chart.data[0][chart.x] > 1e11));
|
|
33
|
+
if (process.env.NODE_ENV !== "production" && chart.data.length > 20 && !horizontal && !timeAxis) {
|
|
30
34
|
console.warn("[vamos-ui] BarChart: more than 20 categories — use layout=\"horizontal\" or group the tail into \"Other\".");
|
|
31
35
|
}
|
|
32
36
|
const lastKey = visible[visible.length - 1]?.key;
|
|
@@ -59,6 +59,8 @@ export interface ChartContextValue {
|
|
|
59
59
|
onPointClick?: (row: Row, seriesKey: string) => void;
|
|
60
60
|
height: number | undefined;
|
|
61
61
|
idPrefix: string;
|
|
62
|
+
/** Series drawn as bars: no stroke pattern, so their legend and tooltip swatch is a solid square. */
|
|
63
|
+
isBar: (key: string) => boolean;
|
|
62
64
|
}
|
|
63
65
|
/** Chart context: config, formatters and hidden series. Use inside `<Chart>`. */
|
|
64
66
|
export declare function useChart(): ChartContextValue;
|
|
@@ -100,6 +102,11 @@ export interface ChartProps<T extends object = Row> extends Omit<React.HTMLAttri
|
|
|
100
102
|
labels?: Partial<ChartLabels>;
|
|
101
103
|
/** Replace the generated data table. */
|
|
102
104
|
table?: ChartTableSpec;
|
|
105
|
+
/**
|
|
106
|
+
* Series drawn as bars. Bars carry no stroke pattern, so their legend and tooltip swatch is a
|
|
107
|
+
* solid square even when the other series get dashed/dotted lines. Bar and Combo charts set it.
|
|
108
|
+
*/
|
|
109
|
+
barKeys?: string[];
|
|
103
110
|
/** Content beside (or under) the plot — e.g. the Donut legend list. */
|
|
104
111
|
aside?: React.ReactNode;
|
|
105
112
|
asidePosition?: "side" | "bottom";
|
|
@@ -127,8 +134,8 @@ export interface ChartLegendContentProps extends React.HTMLAttributes<HTMLDivEle
|
|
|
127
134
|
keys?: string[];
|
|
128
135
|
/** Click toggles visibility. Default `true`. */
|
|
129
136
|
interactive?: boolean;
|
|
130
|
-
/** Swatch shape. Default: a line for line/area patterns, a dot otherwise. */
|
|
131
|
-
swatch?: "dot" | "line";
|
|
137
|
+
/** Swatch shape. Default: a solid square for bar series, a line for line/area patterns, a dot otherwise. */
|
|
138
|
+
swatch?: "dot" | "line" | "square";
|
|
132
139
|
}
|
|
133
140
|
/** The legend: one toggle per series (`aria-pressed`). At least one series always stays visible. */
|
|
134
141
|
export declare function ChartLegendContent({ keys, interactive, swatch, className, ...props }: ChartLegendContentProps): React.JSX.Element;
|
|
@@ -52,7 +52,7 @@ const capitalise = (s) => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
54
|
export function Chart(props) {
|
|
55
|
-
const { data: dataProp, config, x, xLabel, xFormat, yFormat, height, aspect, state: stateProp, title, summary, tableToggle, legend, tooltipIndicator = "dot", animate = true, onPointClick, hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, labels, table, aside, asidePosition = "side", skeleton = "area", toolbar, className, style, children, ...rest } = props;
|
|
55
|
+
const { data: dataProp, config, x, xLabel, xFormat, yFormat, height, aspect, state: stateProp, title, summary, tableToggle, legend, tooltipIndicator = "dot", animate = true, onPointClick, hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, labels, table, aside, asidePosition = "side", skeleton = "area", toolbar, barKeys, className, style, children, ...rest } = props;
|
|
56
56
|
const data = dataProp;
|
|
57
57
|
const f = useFormat();
|
|
58
58
|
const t = useLabels("chart", chartLabels, labels);
|
|
@@ -79,6 +79,9 @@ export function Chart(props) {
|
|
|
79
79
|
const status = stateProp ?? (data.length === 0 ? { status: "empty" } : { status: "ready" });
|
|
80
80
|
const [showTable, setShowTable] = React.useState(false);
|
|
81
81
|
const seriesFormat = React.useCallback((key) => config[key]?.format, [config]);
|
|
82
|
+
const barKeySet = barKeys?.join("\u0000") ?? "";
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
const isBar = React.useCallback((key) => (barKeys ?? []).includes(key), [barKeySet]);
|
|
82
85
|
const ctx = React.useMemo(() => ({
|
|
83
86
|
config,
|
|
84
87
|
series,
|
|
@@ -99,7 +102,8 @@ export function Chart(props) {
|
|
|
99
102
|
onPointClick: onPointClick,
|
|
100
103
|
height: height === "auto" ? undefined : typeof height === "string" ? CHART_HEIGHTS[height] : height,
|
|
101
104
|
idPrefix,
|
|
102
|
-
|
|
105
|
+
isBar,
|
|
106
|
+
}), [config, series, visibleSeries, hiddenSet, toggleSeries, data, x, xFormat, yFormat, f, t, animate, reduced, tooltipIndicator, onPointClick, height, idPrefix, seriesFormat, isBar]);
|
|
103
107
|
const vars = React.useMemo(() => {
|
|
104
108
|
const out = {};
|
|
105
109
|
for (const s of series)
|
|
@@ -149,19 +153,21 @@ export function Chart(props) {
|
|
|
149
153
|
}
|
|
150
154
|
/** The legend: one toggle per series (`aria-pressed`). At least one series always stays visible. */
|
|
151
155
|
export function ChartLegendContent({ keys, interactive = true, swatch, className, ...props }) {
|
|
152
|
-
const { series, isHidden, toggleSeries, visibleSeries, t } = useChart();
|
|
156
|
+
const { series, isHidden, toggleSeries, visibleSeries, t, isBar } = useChart();
|
|
153
157
|
const items = keys ? series.filter((s) => keys.includes(s.key)) : series;
|
|
154
158
|
return (_jsx("div", { role: "group", "aria-label": t("legend"), "data-slot": "chart-legend", className: cn("flex flex-wrap items-center gap-x-1 gap-y-1", className), ...props, children: items.map((s) => {
|
|
155
159
|
const hidden = isHidden(s.key);
|
|
156
160
|
const locked = !hidden && visibleSeries.length <= 1;
|
|
157
161
|
const Icon = s.icon;
|
|
158
|
-
const content = (_jsxs(_Fragment, { children: [Icon ? _jsx(Icon, { className: "size-3.5" }) : _jsx(LegendSwatch, { color: seriesColor(s.key), pattern: s.pattern, shape: swatch ?? (s.pattern !== "solid" ? "line" : "dot") }), _jsx("span", { className: cn(hidden && "line-through decoration-fg-subtle"), children: s.label })] }));
|
|
162
|
+
const content = (_jsxs(_Fragment, { children: [Icon ? _jsx(Icon, { className: "size-3.5" }) : _jsx(LegendSwatch, { color: seriesColor(s.key), pattern: s.pattern, shape: swatch ?? (isBar(s.key) ? "square" : s.pattern !== "solid" ? "line" : "dot") }), _jsx("span", { className: cn(hidden && "line-through decoration-fg-subtle"), children: s.label })] }));
|
|
159
163
|
if (!interactive)
|
|
160
164
|
return (_jsx("span", { className: "inline-flex items-center gap-1.5 px-1.5 text-[12.5px] text-fg-muted", children: content }, s.key));
|
|
161
165
|
return (_jsx("button", { type: "button", "aria-pressed": !hidden, "aria-disabled": locked || undefined, onClick: () => toggleSeries(s.key), className: cn("inline-flex h-7 items-center gap-1.5 rounded-ui-sm px-2 text-[12.5px] text-fg-muted transition-[color,background-color,opacity] focus-ring hover:bg-hover hover:text-fg", hidden && "opacity-55", locked && "cursor-default hover:bg-transparent"), children: content }, s.key));
|
|
162
166
|
}) }));
|
|
163
167
|
}
|
|
164
168
|
function LegendSwatch({ color, pattern, shape }) {
|
|
169
|
+
if (shape === "square")
|
|
170
|
+
return _jsx("span", { "aria-hidden": true, "data-swatch": "square", className: "size-2.5 shrink-0 rounded-[2px]", style: { background: color } });
|
|
165
171
|
if (shape === "line")
|
|
166
172
|
return (_jsx("svg", { width: "14", height: "8", viewBox: "0 0 14 8", "aria-hidden": true, className: "shrink-0", children: _jsx("line", { x1: "1", y1: "4", x2: "13", y2: "4", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeDasharray: pattern === "dashed" ? "4 3" : pattern === "dotted" ? "0.5 3" : undefined }) }));
|
|
167
173
|
return _jsx("span", { "aria-hidden": true, className: "size-2 shrink-0 rounded-[3px]", style: { background: color } });
|
package/dist/charts/chart.js
CHANGED
|
@@ -35,7 +35,7 @@ export function ChartTooltipContent({ active, payload, label, indicator, showTot
|
|
|
35
35
|
return (_jsxs("div", { "data-slot": "chart-tooltip", className: cn("vui-chart-tooltip min-w-36 rounded-ui-sm px-2.5 py-2 text-[12px]", className), children: [header ? _jsx("div", { className: "mb-1.5 font-medium text-fg", children: header }) : null, _jsxs("div", { className: "grid gap-1", children: [rows.map((p) => {
|
|
36
36
|
const key = String(p.dataKey);
|
|
37
37
|
const s = chart.series.find((x) => x.key === key);
|
|
38
|
-
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ChartIndicator, { color: seriesColor(key), variant: ind, pattern: s?.pattern }), _jsx("span", { className: "me-auto text-fg-muted", children: s?.label ?? String(p.name ?? key) }), _jsx("span", { className: "font-mono text-fg tabular", children: chart.formatSeries(key, raw[key] ?? p.value) }), showShare && total ? _jsx("span", { className: "w-11 text-end font-mono text-fg-subtle tabular", children: chart.f.percent(numeric(key) / total) }) : null] }, key));
|
|
38
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ChartIndicator, { color: seriesColor(key), variant: chart.isBar(key) ? "dot" : ind, pattern: chart.isBar(key) ? undefined : s?.pattern }), _jsx("span", { className: "me-auto text-fg-muted", children: s?.label ?? String(p.name ?? key) }), _jsx("span", { className: "font-mono text-fg tabular", children: chart.formatSeries(key, raw[key] ?? p.value) }), showShare && total ? _jsx("span", { className: "w-11 text-end font-mono text-fg-subtle tabular", children: chart.f.percent(numeric(key) / total) }) : null] }, key));
|
|
39
39
|
}), showTotal && rows.length > 1 ? (_jsxs("div", { className: "mt-0.5 flex items-center gap-2 border-t border-line pt-1.5", children: [_jsx("span", { className: "me-auto text-fg-muted", children: chart.t("total") }), _jsx("span", { className: "font-mono font-medium text-fg tabular", children: chart.formatSeries(String(rows[0]?.dataKey), total) })] })) : null] })] }));
|
|
40
40
|
}
|
|
41
41
|
/** Recharts Tooltip wired to `ChartTooltipContent` and the themed cursor. */
|
|
@@ -5,7 +5,7 @@ import { Area, Bar, ComposedChart, Line } from "recharts";
|
|
|
5
5
|
import { useLocale } from "../lib/locale.js";
|
|
6
6
|
import { Chart, useChart } from "./chart-shell.js";
|
|
7
7
|
import { ChartFrame, ChartGrid, ChartTooltip, ChartXAxis, ChartYAxis, useValueAxisWidth } from "./chart.js";
|
|
8
|
-
import {
|
|
8
|
+
import { alignedAxes, extent, patternDash, seriesColor } from "./utils.js";
|
|
9
9
|
/**
|
|
10
10
|
* Volume and rate on two axes — bars on the start axis, a line on the end axis, zero lines aligned
|
|
11
11
|
* so neither scale exaggerates. Axis sides are logical and swap in RTL.
|
|
@@ -15,7 +15,7 @@ import { alignZeroDomains, extent, patternDash, seriesColor } from "./utils.js";
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
export function ComboChart({ bars, lines, areas = [], endAxisFormat, endAxis = true, stackBars, startAxisLabel, endAxisLabel, ...props }) {
|
|
18
|
-
return (_jsx(Chart, { skeleton: "bar", ...props, children: _jsx(ComboPlot, { bars: bars, lines: lines, areas: areas, endAxisFormat: endAxisFormat, endAxis: endAxis, stackBars: stackBars, startAxisLabel: startAxisLabel, endAxisLabel: endAxisLabel }) }));
|
|
18
|
+
return (_jsx(Chart, { skeleton: "bar", barKeys: bars, ...props, children: _jsx(ComboPlot, { bars: bars, lines: lines, areas: areas, endAxisFormat: endAxisFormat, endAxis: endAxis, stackBars: stackBars, startAxisLabel: startAxisLabel, endAxisLabel: endAxisLabel }) }));
|
|
19
19
|
}
|
|
20
20
|
function ComboPlot({ bars, lines, areas = [], endAxisFormat, endAxis, stackBars, startAxisLabel, endAxisLabel }) {
|
|
21
21
|
const chart = useChart();
|
|
@@ -27,20 +27,18 @@ function ComboPlot({ bars, lines, areas = [], endAxisFormat, endAxis, stackBars,
|
|
|
27
27
|
const startStacked = Boolean(stackBars);
|
|
28
28
|
const endFormat = endAxisFormat ?? chart.config[endKeys[0] ?? ""]?.format;
|
|
29
29
|
const startFormat = chart.config[startKeys[0] ?? ""]?.format ?? chart.yFormat;
|
|
30
|
-
const [startDomain, endDomain] = React.useMemo(() => {
|
|
30
|
+
const { domains: [startDomain, endDomain], ticks: [startTicks, endTicks] } = React.useMemo(() => {
|
|
31
31
|
const a = extent(chart.data, startKeys, startStacked);
|
|
32
32
|
const b = extent(chart.data, endKeys);
|
|
33
|
-
return
|
|
33
|
+
return alignedAxes(a, b);
|
|
34
34
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
35
|
}, [chart.data, startKeys.join(), endKeys.join(), startStacked]);
|
|
36
|
-
// Non-negative data: four equal steps on both axes, so the grid lines serve both scales.
|
|
37
|
-
const ticksOf = (d) => (d[0] === 0 ? [0, 1, 2, 3, 4].map((i) => Number(((d[1] * i) / 4).toPrecision(12))) : undefined);
|
|
38
36
|
const startWidth = useValueAxisWidth(startKeys, { stacked: startStacked, format: startFormat });
|
|
39
37
|
const endWidth = useValueAxisWidth(endKeys, { format: endFormat });
|
|
40
38
|
const left = rtl ? "end" : "start";
|
|
41
39
|
const yId = (key, fallback) => axisOf(key, fallback);
|
|
42
40
|
const titleProps = (value, side) => value ? { value, angle: -90, position: side === "left" ? "insideLeft" : "insideRight", className: "vui-chart-axis-title", offset: 0, style: { textAnchor: "middle" } } : undefined;
|
|
43
|
-
return (_jsx(ChartFrame, { children: _jsxs(ComposedChart, { data: chart.data, margin: { top: 8, right: 4, bottom: 0, left: 4 }, barCategoryGap: "30%", children: [_jsx(ChartGrid, { yAxisId: "start" }), _jsx(ChartXAxis, {}), _jsx(ChartYAxis, { yAxisId: "start", orientation: left === "start" ? "left" : "right", domain: startDomain, ticks:
|
|
41
|
+
return (_jsx(ChartFrame, { children: _jsxs(ComposedChart, { data: chart.data, stackOffset: startStacked && startDomain[0] < 0 ? "sign" : undefined, margin: { top: 8, right: 4, bottom: 0, left: 4 }, barCategoryGap: "30%", children: [_jsx(ChartGrid, { yAxisId: "start" }), _jsx(ChartXAxis, {}), _jsx(ChartYAxis, { yAxisId: "start", orientation: left === "start" ? "left" : "right", domain: startDomain, ticks: startTicks, width: startWidth + (startAxisLabel ? 16 : 0), tickFormatter: (v) => chart.formatTick(v, startFormat), label: titleProps(startAxisLabel, left === "start" ? "left" : "right"), keys: startKeys }), _jsx(ChartYAxis, { yAxisId: "end", orientation: left === "start" ? "right" : "left", domain: endDomain, ticks: endTicks, hide: !endAxis || endKeys.length === 0, width: endWidth + (endAxisLabel ? 16 : 0), tickFormatter: (v) => chart.formatTick(v, endFormat), label: titleProps(endAxisLabel, left === "start" ? "right" : "left"), keys: endKeys }), _jsx(ChartTooltip, { cursor: { fill: "var(--hover)" } }), areas.map((key) => (_jsx(Area, { yAxisId: yId(key, "start"), dataKey: key, name: chart.config[key]?.label, hide: chart.isHidden(key), type: "monotone", stroke: seriesColor(key), strokeWidth: 1.5, fill: `color-mix(in oklch, ${seriesColor(key)} 16%, transparent)`, isAnimationActive: chart.animate }, key))), bars.map((key, i) => (_jsx(Bar, { yAxisId: yId(key, "start"), dataKey: key, name: chart.config[key]?.label, hide: chart.isHidden(key), stackId: stackBars ? "bars" : undefined, fill: seriesColor(key), radius: stackBars && i < bars.length - 1 ? 0 : [4, 4, 0, 0], maxBarSize: 40, isAnimationActive: chart.animate }, key))), lines.map((key) => {
|
|
44
42
|
const s = chart.series.find((x) => x.key === key);
|
|
45
43
|
return (_jsx(Line, { yAxisId: yId(key, "end"), dataKey: key, name: s?.label, hide: chart.isHidden(key), type: "monotone", stroke: seriesColor(key), strokeWidth: 2, strokeDasharray: patternDash(s?.pattern), dot: { r: 2.5, fill: seriesColor(key), stroke: "var(--chart-tooltip)", strokeWidth: 1.5 }, activeDot: { r: 4, strokeWidth: 2, stroke: "var(--chart-tooltip)", fill: seriesColor(key) }, isAnimationActive: chart.animate }, key));
|
|
46
44
|
})] }) }));
|
package/dist/charts/utils.d.ts
CHANGED
|
@@ -100,6 +100,16 @@ export declare function niceStep(v: number): number;
|
|
|
100
100
|
* Returns domains with the same negative share, so neither axis exaggerates.
|
|
101
101
|
*/
|
|
102
102
|
export declare function alignZeroDomains(a: [number, number], b: [number, number]): [[number, number], [number, number]];
|
|
103
|
+
/**
|
|
104
|
+
* Two value axes (Combo) with nice, evenly spaced ticks whose zero lines meet: both axes get the
|
|
105
|
+
* same number of steps below and above zero, each with its own nice step (1, 2, 2.5, 5… × 10ⁿ), so
|
|
106
|
+
* one set of grid lines serves both scales. An axis whose data never goes below zero keeps only its
|
|
107
|
+
* non-negative ticks (its domain still reaches below zero to line up with the other axis).
|
|
108
|
+
*/
|
|
109
|
+
export declare function alignedAxes(a: [number, number], b: [number, number]): {
|
|
110
|
+
domains: [[number, number], [number, number]];
|
|
111
|
+
ticks: [number[], number[]];
|
|
112
|
+
};
|
|
103
113
|
export interface SeriesStats {
|
|
104
114
|
key: string;
|
|
105
115
|
min: number;
|
package/dist/charts/utils.js
CHANGED
|
@@ -188,6 +188,52 @@ export function alignZeroDomains(a, b) {
|
|
|
188
188
|
};
|
|
189
189
|
return [fit(aLo, aHi), fit(bLo, bHi)];
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Two value axes (Combo) with nice, evenly spaced ticks whose zero lines meet: both axes get the
|
|
193
|
+
* same number of steps below and above zero, each with its own nice step (1, 2, 2.5, 5… × 10ⁿ), so
|
|
194
|
+
* one set of grid lines serves both scales. An axis whose data never goes below zero keeps only its
|
|
195
|
+
* non-negative ticks (its domain still reaches below zero to line up with the other axis).
|
|
196
|
+
*/
|
|
197
|
+
export function alignedAxes(a, b) {
|
|
198
|
+
const norm = ([lo, hi]) => [Math.min(0, Number.isFinite(lo) ? lo : 0), Math.max(0, Number.isFinite(hi) ? hi : 0)];
|
|
199
|
+
const axes = [norm(a), norm(b)];
|
|
200
|
+
const round = (v) => Number(v.toPrecision(12));
|
|
201
|
+
const build = (neg, pos, steps) => axes.map(([lo], i) => {
|
|
202
|
+
const step = steps[i];
|
|
203
|
+
const all = Array.from({ length: neg + pos + 1 }, (_, k) => round((k - neg) * step));
|
|
204
|
+
return { domain: [round(-neg * step), round(pos * step)], ticks: lo < 0 ? all : all.filter((v) => v >= 0) };
|
|
205
|
+
});
|
|
206
|
+
const anyNeg = axes.some(([lo]) => lo < 0);
|
|
207
|
+
const anyPos = axes.some(([, hi]) => hi > 0);
|
|
208
|
+
if (!anyNeg) {
|
|
209
|
+
// Non-negative data: four equal steps on both axes.
|
|
210
|
+
const steps = axes.map(([, hi]) => niceStep(hi / 4) || 0.25);
|
|
211
|
+
const [x, y] = build(0, 4, steps);
|
|
212
|
+
return { domains: [x.domain, y.domain], ticks: [x.ticks, y.ticks] };
|
|
213
|
+
}
|
|
214
|
+
let best = null;
|
|
215
|
+
for (const total of [4, 5, 6]) {
|
|
216
|
+
for (let neg = 1; neg <= total; neg++) {
|
|
217
|
+
const pos = total - neg;
|
|
218
|
+
if (anyPos && pos === 0)
|
|
219
|
+
continue;
|
|
220
|
+
const steps = axes.map(([lo, hi]) => {
|
|
221
|
+
const need = Math.max(neg ? -lo / neg : 0, pos ? hi / pos : hi > 0 ? Infinity : 0);
|
|
222
|
+
return Number.isFinite(need) ? niceStep(need) || 1 : Infinity;
|
|
223
|
+
});
|
|
224
|
+
if (!steps.every(Number.isFinite))
|
|
225
|
+
continue;
|
|
226
|
+
// Share of each axis left empty, plus a small cost per extra step.
|
|
227
|
+
const waste = axes.reduce((sum, [lo, hi], i) => sum + 1 - (hi - lo) / (steps[i] * total || 1), 0);
|
|
228
|
+
const score = waste + (total - 4) * 0.06;
|
|
229
|
+
if (!best || score < best.score - 1e-9)
|
|
230
|
+
best = { score, neg, pos, steps };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const { neg, pos, steps } = best;
|
|
234
|
+
const [x, y] = build(neg, pos, steps);
|
|
235
|
+
return { domains: [x.domain, y.domain], ticks: [x.ticks, y.ticks] };
|
|
236
|
+
}
|
|
191
237
|
/** Min / max / average of one series (nulls skipped). `null` when the series has no numbers. */
|
|
192
238
|
export function seriesStats(rows, key) {
|
|
193
239
|
let min = Infinity;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type QrErrorCorrection = "L" | "M" | "Q" | "H";
|
|
2
|
+
export interface QrMatrix {
|
|
3
|
+
/** Modules per side (version × 4 + 17). */
|
|
4
|
+
size: number;
|
|
5
|
+
version: number;
|
|
6
|
+
mask: number;
|
|
7
|
+
errorCorrection: QrErrorCorrection;
|
|
8
|
+
/** `modules[y][x]` — true = dark. */
|
|
9
|
+
modules: boolean[][];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Encode text (UTF-8, byte mode) as a QR matrix.
|
|
13
|
+
* Throws when the text does not fit version 40 at the chosen level.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const qr = encodeQr("otpauth://totp/Northpoint:ada@example.com?secret=…", { errorCorrection: "M" });
|
|
17
|
+
* qr.size; // 41 (version 6)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function encodeQr(text: string, options?: {
|
|
21
|
+
errorCorrection?: QrErrorCorrection;
|
|
22
|
+
minVersion?: number;
|
|
23
|
+
mask?: number;
|
|
24
|
+
}): QrMatrix;
|
|
25
|
+
/** One SVG path (`d`) drawing every dark module, offset by `margin` modules. */
|
|
26
|
+
export declare function qrPath(qr: Pick<QrMatrix, "size" | "modules">, margin?: number): string;
|