@vamos-ui/react 0.3.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.js +1 -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 +6 -4
- package/dist/blocks/site/feature-grid.d.ts +1 -0
- package/dist/blocks/site/feature-grid.js +5 -4
- 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 +5 -4
- package/dist/blocks/site/hero.d.ts +3 -2
- package/dist/blocks/site/hero.js +7 -5
- package/dist/blocks/site/logo-strip.d.ts +1 -0
- package/dist/blocks/site/logo-strip.js +1 -1
- 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 +4 -3
- 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/process.d.ts +1 -0
- package/dist/blocks/site/process.js +2 -1
- 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/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 +3 -3
- package/dist/charts/bar-chart.js +1 -1
- 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/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/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/image.d.ts +18 -2
- package/dist/components/image.js +12 -5
- package/dist/components/menubar.d.ts +96 -0
- package/dist/components/menubar.js +123 -0
- package/dist/components/placeholder-art.d.ts +51 -0
- package/dist/components/placeholder-art.js +82 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lib/value-format.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/media-library.js +1 -1
- package/dist/motion/text-reveal.d.ts +3 -1
- package/dist/motion/text-reveal.js +5 -3
- package/dist/render/block-renderer.js +1 -1
- package/dist/styles/blocks.css +18 -9
- package/dist/styles/editors.css +2 -1
- package/dist/styles/media.css +31 -5
- package/dist/styles.css +39 -9
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Responsive `sizes` for site-block photos. Every photo in a site block passes one of these to
|
|
2
|
+
// BlockImage → the kit Image / `components.Image` (e.g. next/image), so a CDN loader picks a
|
|
3
|
+
// 400–800 px file on a phone instead of the 3840 px top step. Fractions follow the block's 12-col
|
|
4
|
+
// grid and are capped at the content container (`--container`, 76rem): a 2560 px monitor does
|
|
5
|
+
// not fetch a 1500 px file for a 33% column that is never wider than 25rem.
|
|
6
|
+
// Server-safe, no React.
|
|
7
|
+
/** `--container` (76rem) — the content column every Container caps at. */
|
|
8
|
+
export const CONTAINER_REM = 76;
|
|
9
|
+
/**
|
|
10
|
+
* A column that spans `fraction` of the container from `from` up, and `narrow` below.
|
|
11
|
+
* `columnSizes(5 / 12)` → `"(min-width: 56rem) min(42vw, 32rem), 100vw"`.
|
|
12
|
+
*/
|
|
13
|
+
export function columnSizes(fraction, opts = {}) {
|
|
14
|
+
const { from = "56rem", narrow = "100vw", bleed = false } = opts;
|
|
15
|
+
const vw = Math.ceil(fraction * 100);
|
|
16
|
+
// A column bleeding to the section edge grows past the container on very wide screens.
|
|
17
|
+
const wide = bleed ? `${vw}vw` : `min(${vw}vw, ${Math.ceil(fraction * CONTAINER_REM)}rem)`;
|
|
18
|
+
return `(min-width: ${from}) ${wide}, ${narrow}`;
|
|
19
|
+
}
|
|
20
|
+
/** Common presets. */
|
|
21
|
+
export const SITE_SIZES = {
|
|
22
|
+
/** Edge-to-edge photo (full-bleed hero, editorial strip). */
|
|
23
|
+
bleed: "100vw",
|
|
24
|
+
/** A photo as wide as the content container. */
|
|
25
|
+
container: `min(100vw, ${CONTAINER_REM}rem)`,
|
|
26
|
+
/** A photo only shown on narrow containers (< 56rem), full width there. */
|
|
27
|
+
narrowOnly: "min(100vw, 56rem)",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Filmstrip cell: fixed height `clamp(15rem, 38cqi, 30rem)`, width = height × ratio.
|
|
31
|
+
* `filmstripSizes(4 / 5)` → `"max(12rem, min(31vw, 24rem))"`.
|
|
32
|
+
*/
|
|
33
|
+
export function filmstripSizes(ratio) {
|
|
34
|
+
const r = ratio > 0 ? ratio : 0.8;
|
|
35
|
+
const n = (x) => Math.round(x * 10) / 10;
|
|
36
|
+
return `max(${n(15 * r)}rem, min(${Math.ceil(38 * r)}vw, ${n(30 * r)}rem))`;
|
|
37
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const carouselLabelKeys: readonly ["previous", "next", "slideCounter", "slideLabel", "carousel", "slide"];
|
|
2
2
|
export type CarouselLabels = Record<(typeof carouselLabelKeys)[number], string>;
|
|
3
|
-
export declare const openNowLabelKeys: readonly ["openNow", "
|
|
3
|
+
export declare const openNowLabelKeys: readonly ["openNow", "openUntil", "closingSoon", "closingSoonUntil", "openAllDay", "closed", "closedToday", "opensAt", "opensTodayAt", "opensTomorrowAt"];
|
|
4
4
|
export type OpenNowLabels = Record<(typeof openNowLabelKeys)[number], string>;
|
|
5
5
|
export declare const contactIslandLabelKeys: readonly ["send", "sending", "sent", "optional", "errorSummary", "errorServer", "errorServerCall", "privacyRead", "privacyLink", "privacyRequired", "marketingConsent", "honeypot", "choose", "errorRequired", "errorEmail", "errorPhone", "errorTooLong"];
|
|
6
6
|
export type ContactIslandLabels = Record<(typeof contactIslandLabelKeys)[number], string>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Label keys each client island needs. Kept out of the "use client" files: a server component
|
|
2
2
|
// importing a plain value from a client module would receive a client reference, not the array.
|
|
3
3
|
export const carouselLabelKeys = ["previous", "next", "slideCounter", "slideLabel", "carousel", "slide"];
|
|
4
|
-
export const openNowLabelKeys = ["openNow", "
|
|
4
|
+
export const openNowLabelKeys = ["openNow", "openUntil", "closingSoon", "closingSoonUntil", "openAllDay", "closed", "closedToday", "opensAt", "opensTodayAt", "opensTomorrowAt"];
|
|
5
5
|
export const contactIslandLabelKeys = [
|
|
6
6
|
"send",
|
|
7
7
|
"sending",
|
|
@@ -86,12 +86,15 @@ export declare const site2Labels: {
|
|
|
86
86
|
email: string;
|
|
87
87
|
hours: string;
|
|
88
88
|
openNow: string;
|
|
89
|
-
|
|
89
|
+
openUntil: string;
|
|
90
90
|
closingSoon: string;
|
|
91
|
+
closingSoonUntil: string;
|
|
92
|
+
openAllDay: string;
|
|
91
93
|
closed: string;
|
|
92
94
|
closedToday: string;
|
|
93
95
|
opensAt: string;
|
|
94
96
|
opensTodayAt: string;
|
|
97
|
+
opensTomorrowAt: string;
|
|
95
98
|
open24h: string;
|
|
96
99
|
openingHours: string;
|
|
97
100
|
openingHoursOf: string;
|
|
@@ -119,6 +122,11 @@ export declare const site2Labels: {
|
|
|
119
122
|
tagSpicy: string;
|
|
120
123
|
tagNew: string;
|
|
121
124
|
tagChefPick: string;
|
|
125
|
+
tagDairyFree: string;
|
|
126
|
+
tagNutFree: string;
|
|
127
|
+
tagHalal: string;
|
|
128
|
+
tagToShare: string;
|
|
129
|
+
tagSeasonal: string;
|
|
122
130
|
allergen_gluten: string;
|
|
123
131
|
allergen_crustaceans: string;
|
|
124
132
|
allergen_eggs: string;
|
|
@@ -95,13 +95,17 @@ export const site2Labels = {
|
|
|
95
95
|
email: "Email",
|
|
96
96
|
hours: "Hours",
|
|
97
97
|
// Map & hours
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// Status line "head · tail" (OpenNow). Each part says it once: "Open · until 1 AM".
|
|
99
|
+
openNow: "Open",
|
|
100
|
+
openUntil: "until {time}",
|
|
100
101
|
closingSoon: "Closing soon",
|
|
102
|
+
closingSoonUntil: "open until {time}",
|
|
103
|
+
openAllDay: "24 hours",
|
|
101
104
|
closed: "Closed",
|
|
102
105
|
closedToday: "Closed today",
|
|
103
106
|
opensAt: "opens {day} at {time}",
|
|
104
107
|
opensTodayAt: "opens at {time}",
|
|
108
|
+
opensTomorrowAt: "opens tomorrow at {time}",
|
|
105
109
|
open24h: "Open 24 hours",
|
|
106
110
|
openingHours: "Opening hours",
|
|
107
111
|
openingHoursOf: "Opening hours, {name}",
|
|
@@ -130,6 +134,11 @@ export const site2Labels = {
|
|
|
130
134
|
tagSpicy: "Spicy",
|
|
131
135
|
tagNew: "New",
|
|
132
136
|
tagChefPick: "Chef’s pick",
|
|
137
|
+
tagDairyFree: "Dairy-free",
|
|
138
|
+
tagNutFree: "Nut-free",
|
|
139
|
+
tagHalal: "Halal",
|
|
140
|
+
tagToShare: "To share",
|
|
141
|
+
tagSeasonal: "Seasonal",
|
|
133
142
|
allergen_gluten: "Gluten",
|
|
134
143
|
allergen_crustaceans: "Crustaceans",
|
|
135
144
|
allergen_eggs: "Eggs",
|
|
@@ -295,13 +304,17 @@ export const site2LabelsTr = {
|
|
|
295
304
|
phone: "Telefon",
|
|
296
305
|
email: "E-posta",
|
|
297
306
|
hours: "Çalışma saatleri",
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
// {timeTo} = "01:00’e", {timeAt} = "12:00’de" — the suffix follows the spoken time.
|
|
308
|
+
openNow: "Açık",
|
|
309
|
+
openUntil: "{timeTo} kadar",
|
|
300
310
|
closingSoon: "Birazdan kapanıyor",
|
|
311
|
+
closingSoonUntil: "{timeTo} kadar açık",
|
|
312
|
+
openAllDay: "24 saat",
|
|
301
313
|
closed: "Kapalı",
|
|
302
314
|
closedToday: "Bugün kapalı",
|
|
303
|
-
opensAt: "{day} {
|
|
304
|
-
opensTodayAt: "{
|
|
315
|
+
opensAt: "{day} {timeAt} açılıyor",
|
|
316
|
+
opensTodayAt: "bugün {timeAt} açılıyor",
|
|
317
|
+
opensTomorrowAt: "yarın {timeAt} açılıyor",
|
|
305
318
|
open24h: "24 saat açık",
|
|
306
319
|
openingHours: "Çalışma saatleri",
|
|
307
320
|
openingHoursOf: "Çalışma saatleri, {name}",
|
|
@@ -329,6 +342,11 @@ export const site2LabelsTr = {
|
|
|
329
342
|
tagSpicy: "Acılı",
|
|
330
343
|
tagNew: "Yeni",
|
|
331
344
|
tagChefPick: "Şefin seçimi",
|
|
345
|
+
tagDairyFree: "Sütsüz",
|
|
346
|
+
tagNutFree: "Kuruyemişsiz",
|
|
347
|
+
tagHalal: "Helal",
|
|
348
|
+
tagToShare: "Paylaşımlık",
|
|
349
|
+
tagSeasonal: "Mevsimlik",
|
|
332
350
|
allergen_gluten: "Gluten",
|
|
333
351
|
allergen_crustaceans: "Kabuklular",
|
|
334
352
|
allergen_eggs: "Yumurta",
|
|
@@ -15,7 +15,7 @@ export interface OpenNowProps {
|
|
|
15
15
|
className?: string;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* "Open
|
|
18
|
+
* "Open · until 10 PM" / "Closed · opens tomorrow at 12 PM" — computed in the business's time zone on the client, refreshed
|
|
19
19
|
* every minute. Renders nothing on the server and without JS: a cached page never shows a stale
|
|
20
20
|
* status. ~1 KB.
|
|
21
21
|
*/
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { cn2 as cn } from "./kit-2.js";
|
|
5
5
|
import { describeStatus, hoursStatus } from "./opening-hours.js";
|
|
6
6
|
/**
|
|
7
|
-
* "Open
|
|
7
|
+
* "Open · until 10 PM" / "Closed · opens tomorrow at 12 PM" — computed in the business's time zone on the client, refreshed
|
|
8
8
|
* every minute. Renders nothing on the server and without JS: a cached page never shows a stale
|
|
9
9
|
* status. ~1 KB.
|
|
10
10
|
*/
|
|
@@ -110,8 +110,24 @@ export declare function summarizeHours(hours: OpeningHours, locale?: string): {
|
|
|
110
110
|
}[];
|
|
111
111
|
/** schema.org `OpeningHoursSpecification[]` for JSON-LD (weekly + special days). */
|
|
112
112
|
export declare function openingHoursJsonLd(hours: OpeningHours): Record<string, unknown>[];
|
|
113
|
-
/**
|
|
114
|
-
export
|
|
113
|
+
/** Label keys `describeStatus` reads. */
|
|
114
|
+
export type StatusLabelKey = "openNow" | "openUntil" | "closingSoon" | "closingSoonUntil" | "openAllDay" | "closed" | "closedToday" | "opensAt" | "opensTodayAt" | "opensTomorrowAt";
|
|
115
|
+
/**
|
|
116
|
+
* Variables for a time in a status template: `{time}` ("01:00", "1 AM"), plus `{timeTo}` /
|
|
117
|
+
* `{timeAt}` — the time with the Turkish dative / locative suffix ("01:00’e", "12:00’de") so the
|
|
118
|
+
* TR strings read naturally; other locales get the plain time.
|
|
119
|
+
*/
|
|
120
|
+
export declare function timeVars(hhmm: string, locale?: string): {
|
|
121
|
+
time: string;
|
|
122
|
+
timeTo: string;
|
|
123
|
+
timeAt: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The status as `{ head, tail }`, each said once: "Open · until 1 AM", "Closing soon · open until
|
|
127
|
+
* 1 AM", "Open · 24 hours", "Closed · opens tomorrow at 12 PM", "Closed today · Staff day · opens
|
|
128
|
+
* Thu at 12 PM". TR: "Açık · 01:00’e kadar", "Kapalı · yarın 12:00’de açılıyor".
|
|
129
|
+
*/
|
|
130
|
+
export declare function describeStatus(s: OpenStatus, labels: Record<StatusLabelKey, string>, locale?: string): {
|
|
115
131
|
head: string;
|
|
116
132
|
tail?: string;
|
|
117
133
|
};
|
|
@@ -227,19 +227,48 @@ export function openingHoursJsonLd(hours) {
|
|
|
227
227
|
}
|
|
228
228
|
return out;
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
// Turkish case suffixes on a clock time follow the last spoken word: "01:00" is read "bir" →
|
|
231
|
+
// "01:00’e" (dative) / "01:00’de" (locative); "22:30" ends in "otuz" → "22:30’a" / "22:30’da".
|
|
232
|
+
const TR_UNITS_DAT = ["", "e", "ye", "e", "e", "e", "ya", "ye", "e", "a"];
|
|
233
|
+
const TR_UNITS_LOC = ["", "de", "de", "te", "te", "te", "da", "de", "de", "da"];
|
|
234
|
+
const TR_TENS_DAT = ["a", "a", "ye", "a", "a", "ye"]; // sıfır, on, yirmi, otuz, kırk, elli
|
|
235
|
+
const TR_TENS_LOC = ["da", "da", "de", "da", "ta", "de"];
|
|
236
|
+
function trSuffix(hhmm, kind) {
|
|
237
|
+
const [h = 0, m = 0] = hhmm.split(":").map(Number);
|
|
238
|
+
const n = m === 0 ? h % 24 : m; // the last word read aloud
|
|
239
|
+
const unit = n % 10;
|
|
240
|
+
if (unit)
|
|
241
|
+
return (kind === "dat" ? TR_UNITS_DAT : TR_UNITS_LOC)[unit];
|
|
242
|
+
return (kind === "dat" ? TR_TENS_DAT : TR_TENS_LOC)[Math.floor(n / 10)];
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Variables for a time in a status template: `{time}` ("01:00", "1 AM"), plus `{timeTo}` /
|
|
246
|
+
* `{timeAt}` — the time with the Turkish dative / locative suffix ("01:00’e", "12:00’de") so the
|
|
247
|
+
* TR strings read naturally; other locales get the plain time.
|
|
248
|
+
*/
|
|
249
|
+
export function timeVars(hhmm, locale = "en-US") {
|
|
250
|
+
const time = formatTime(hhmm, locale);
|
|
251
|
+
if (!/^tr\b/i.test(locale))
|
|
252
|
+
return { time, timeTo: time, timeAt: time };
|
|
253
|
+
return { time, timeTo: `${time}’${trSuffix(hhmm, "dat")}`, timeAt: `${time}’${trSuffix(hhmm, "loc")}` };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The status as `{ head, tail }`, each said once: "Open · until 1 AM", "Closing soon · open until
|
|
257
|
+
* 1 AM", "Open · 24 hours", "Closed · opens tomorrow at 12 PM", "Closed today · Staff day · opens
|
|
258
|
+
* Thu at 12 PM". TR: "Açık · 01:00’e kadar", "Kapalı · yarın 12:00’de açılıyor".
|
|
259
|
+
*/
|
|
231
260
|
export function describeStatus(s, labels, locale = "en-US") {
|
|
232
261
|
const f = (tpl, vars) => formatMessage(tpl, vars, locale);
|
|
233
262
|
if (s.state !== "closed") {
|
|
234
263
|
if (s.allDay)
|
|
235
|
-
return { head: f(labels.openNow), tail: f(labels.
|
|
236
|
-
const
|
|
237
|
-
return
|
|
264
|
+
return { head: f(labels.openNow), tail: f(labels.openAllDay) };
|
|
265
|
+
const vars = timeVars(s.closesAt, locale);
|
|
266
|
+
return s.state === "closing-soon" ? { head: f(labels.closingSoon), tail: f(labels.closingSoonUntil, vars) } : { head: f(labels.openNow), tail: f(labels.openUntil, vars) };
|
|
238
267
|
}
|
|
239
268
|
const head = s.special?.closed ? (s.special.note ? `${f(labels.closedToday)} · ${s.special.note}` : f(labels.closedToday)) : f(labels.closed);
|
|
240
269
|
if (!s.opensAt)
|
|
241
270
|
return { head };
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
return { head, tail };
|
|
271
|
+
const vars = { ...timeVars(s.opensAt.time, locale), day: weekdayName(s.opensAt.weekday, locale, "short") };
|
|
272
|
+
const tpl = s.opensAt.dayOffset === 0 ? labels.opensTodayAt : s.opensAt.dayOffset === 1 ? labels.opensTomorrowAt : labels.opensAt;
|
|
273
|
+
return { head, tail: f(tpl, vars) };
|
|
245
274
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** ≥ 24×24 CSS px everywhere (WCAG 2.5.8), ≥ 44px tall on touch. For text links in lists. */
|
|
2
|
+
export declare const TAP = "inline-flex min-h-6 items-center pointer-coarse:min-h-11";
|
|
3
|
+
/** A square icon control: ≥ 24px everywhere, 44×44 on touch. Combine with the control's own size. */
|
|
4
|
+
export declare const TAP_ICON = "min-h-6 min-w-6 pointer-coarse:min-h-11 pointer-coarse:min-w-11";
|
|
5
|
+
/**
|
|
6
|
+
* WCAG 2.5.3 Label in Name: an `aria-label` must contain the visible label. Returns `name` when
|
|
7
|
+
* it already does (case-insensitive), else `"<visible>, <name>"`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function nameWithLabel(visible: string | undefined, name: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Tap-target sizing for site blocks (WCAG 2.5.8 + touch comfort). Padding/min-size only — the
|
|
2
|
+
// visual stays the same on a mouse; on touch (`pointer: coarse`) primary controls grow to 44px.
|
|
3
|
+
/** ≥ 24×24 CSS px everywhere (WCAG 2.5.8), ≥ 44px tall on touch. For text links in lists. */
|
|
4
|
+
export const TAP = "inline-flex min-h-6 items-center pointer-coarse:min-h-11";
|
|
5
|
+
/** A square icon control: ≥ 24px everywhere, 44×44 on touch. Combine with the control's own size. */
|
|
6
|
+
export const TAP_ICON = "min-h-6 min-w-6 pointer-coarse:min-h-11 pointer-coarse:min-w-11";
|
|
7
|
+
/**
|
|
8
|
+
* WCAG 2.5.3 Label in Name: an `aria-label` must contain the visible label. Returns `name` when
|
|
9
|
+
* it already does (case-insensitive), else `"<visible>, <name>"`.
|
|
10
|
+
*/
|
|
11
|
+
export function nameWithLabel(visible, name) {
|
|
12
|
+
const v = visible?.trim();
|
|
13
|
+
if (!v)
|
|
14
|
+
return name;
|
|
15
|
+
return name.toLocaleLowerCase().includes(v.toLocaleLowerCase()) ? name : `${v}, ${name}`;
|
|
16
|
+
}
|
|
@@ -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;
|
|
@@ -42,14 +42,14 @@ export function RatingStars({ rating, label, size = "md", className }) {
|
|
|
42
42
|
}
|
|
43
43
|
function Attribution({ item, ctx, align = "start", as: Comp = "figcaption" }) {
|
|
44
44
|
const meta = [item.role, item.company].filter(Boolean);
|
|
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", 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] }));
|
|
46
46
|
}
|
|
47
47
|
function SourceLine({ item, ctx }) {
|
|
48
48
|
const showRating = item.rating !== undefined && item.source;
|
|
49
49
|
const name = item.source ? REVIEW_SOURCE_NAMES[item.source] : "";
|
|
50
50
|
if (!showRating && !item.date && !name)
|
|
51
51
|
return 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: "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] }));
|
|
53
53
|
}
|
|
54
54
|
function QuoteFigure({ item, index, ctx, size = "s" }) {
|
|
55
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 })] }));
|
|
@@ -62,5 +62,5 @@ function ReviewsLayout({ header, items, summary, ctx }) {
|
|
|
62
62
|
const sourceName = summary ? REVIEW_SOURCE_NAMES[summary.source] : "";
|
|
63
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
|
|
64
64
|
? ctx.t("ratingSummary", { rating: nf.number(summary.rating, { maximumFractionDigits: 1 }), source: sourceName })
|
|
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: "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))) })] }));
|
|
66
66
|
}
|
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();
|
|
@@ -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, 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:
|
|
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;
|