@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
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;
|
|
@@ -30,11 +30,11 @@ const indicatorVariants = cva("relative z-[1] grid size-(--tl-ind) shrink-0 plac
|
|
|
30
30
|
variants: {
|
|
31
31
|
tone: {
|
|
32
32
|
neutral: "border-line bg-[color-mix(in_oklch,var(--surface)_90%,var(--scene))]",
|
|
33
|
-
accent: "tone-accent border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone)",
|
|
34
|
-
success: "tone-success border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone)",
|
|
35
|
-
warning: "tone-warning border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone)",
|
|
36
|
-
danger: "tone-danger border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone)",
|
|
37
|
-
info: "tone-info border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone)",
|
|
33
|
+
accent: "tone-accent border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone-ink)",
|
|
34
|
+
success: "tone-success border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone-ink)",
|
|
35
|
+
warning: "tone-warning border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone-ink)",
|
|
36
|
+
danger: "tone-danger border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone-ink)",
|
|
37
|
+
info: "tone-info border-[color-mix(in_srgb,var(--tone)_35%,var(--line))] bg-[color-mix(in_oklch,var(--tone)_14%,color-mix(in_oklch,var(--surface)_92%,var(--scene)))] text-(--tone-ink)",
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
defaultVariants: { tone: "neutral" },
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type VariantProps } from "class-variance-authority";
|
|
3
|
+
/** `"16/9"`, `"4/3"`, `"1/1"`… or a number (`1.5`). */
|
|
4
|
+
export type AspectRatioValue = `${number}/${number}` | number;
|
|
5
|
+
/** `"16/9"` → 1.777…, `"16:9"` → 1.777…, `1.5` → 1.5. Invalid values return `undefined`. */
|
|
6
|
+
export declare function parseAspectRatio(ratio: AspectRatioValue | string | undefined): number | undefined;
|
|
7
|
+
export declare const aspectRatioVariants: (props?: ({
|
|
8
|
+
rounded?: "ui" | "ui-lg" | "full" | "none" | null | undefined;
|
|
9
|
+
fit?: "fill" | "contain" | "cover" | null | undefined;
|
|
10
|
+
tone?: "inset" | "none" | null | undefined;
|
|
11
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
12
|
+
export interface AspectRatioProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof aspectRatioVariants> {
|
|
13
|
+
/** Width ÷ height. `"16/9"` (default), `"4/3"`, `"1/1"`, `"21/9"` or a number. */
|
|
14
|
+
ratio?: AspectRatioValue;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Keeps a box at a fixed ratio whatever its width, and stretches the direct child to fill it —
|
|
18
|
+
* an iframe, a video, a map, an image or any element. Pure CSS (`aspect-ratio`), server-safe.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <AspectRatio ratio="16/9" rounded="ui-lg">
|
|
22
|
+
* <iframe src="https://www.youtube-nocookie.com/embed/…" title="Product tour" />
|
|
23
|
+
* </AspectRatio>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare const AspectRatio: React.ForwardRefExoticComponent<AspectRatioProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
/** `"16/9"` → 1.777…, `"16:9"` → 1.777…, `1.5` → 1.5. Invalid values return `undefined`. */
|
|
6
|
+
export function parseAspectRatio(ratio) {
|
|
7
|
+
if (typeof ratio === "number")
|
|
8
|
+
return Number.isFinite(ratio) && ratio > 0 ? ratio : undefined;
|
|
9
|
+
if (!ratio)
|
|
10
|
+
return undefined;
|
|
11
|
+
const [w, h] = ratio.split(/[/:]/).map((s) => Number(s.trim()));
|
|
12
|
+
if (w && h && w > 0 && h > 0)
|
|
13
|
+
return w / h;
|
|
14
|
+
const n = Number(ratio);
|
|
15
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
16
|
+
}
|
|
17
|
+
export const aspectRatioVariants = cva("relative isolate block w-full overflow-hidden", {
|
|
18
|
+
variants: {
|
|
19
|
+
rounded: { none: "", ui: "rounded-ui", "ui-lg": "rounded-ui-lg", full: "rounded-full" },
|
|
20
|
+
/** How a direct `img` / `video` child fills the box. */
|
|
21
|
+
fit: { cover: "[&>img]:object-cover [&>video]:object-cover", contain: "[&>img]:object-contain [&>video]:object-contain", fill: "[&>img]:object-fill [&>video]:object-fill" },
|
|
22
|
+
/** `inset`: a `surface-2` backdrop (letterboxing for `contain`, a frame while an iframe loads). */
|
|
23
|
+
tone: { none: "", inset: "bg-surface-2" },
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { rounded: "none", fit: "cover", tone: "none" },
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Keeps a box at a fixed ratio whatever its width, and stretches the direct child to fill it —
|
|
29
|
+
* an iframe, a video, a map, an image or any element. Pure CSS (`aspect-ratio`), server-safe.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <AspectRatio ratio="16/9" rounded="ui-lg">
|
|
33
|
+
* <iframe src="https://www.youtube-nocookie.com/embed/…" title="Product tour" />
|
|
34
|
+
* </AspectRatio>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const AspectRatio = React.forwardRef(function AspectRatio({ ratio = "16/9", rounded, fit, tone, className, style, children, ...props }, ref) {
|
|
38
|
+
const value = parseAspectRatio(ratio) ?? 16 / 9;
|
|
39
|
+
return (_jsx("div", { ref: ref, "data-slot": "aspect-ratio", className: cn(aspectRatioVariants({ rounded, fit, tone }),
|
|
40
|
+
// The child fills the box; iframes lose their default border.
|
|
41
|
+
"[&>*]:absolute [&>*]:inset-0 [&>*]:size-full [&>iframe]:border-0", className), style: { aspectRatio: String(value), ...style }, ...props, children: children }));
|
|
42
|
+
});
|
|
@@ -19,6 +19,7 @@ declare const carouselLabels: {
|
|
|
19
19
|
thumbs: string;
|
|
20
20
|
empty: string;
|
|
21
21
|
loading: string;
|
|
22
|
+
marqueePause: string;
|
|
22
23
|
};
|
|
23
24
|
export type CarouselLabels = typeof carouselLabels;
|
|
24
25
|
export type CarouselPreset = "default" | "gallery" | "hero" | "strip";
|
|
@@ -161,10 +162,18 @@ export interface MarqueeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
161
162
|
/** Fade the edges. Default `true`. */
|
|
162
163
|
fade?: boolean;
|
|
163
164
|
gap?: "sm" | "md" | "lg";
|
|
165
|
+
/**
|
|
166
|
+
* A small pause control (a native checkbox, works without JavaScript). Moving content that runs
|
|
167
|
+
* longer than five seconds needs one (WCAG 2.2.2). Default `true`.
|
|
168
|
+
*/
|
|
169
|
+
controls?: boolean;
|
|
170
|
+
/** Accessible name of the pause control. Default "Pause scrolling" (dictionary `carousel.marqueePause`). */
|
|
171
|
+
pauseLabel?: string;
|
|
164
172
|
}
|
|
165
173
|
/**
|
|
166
|
-
* Endless logo band in pure CSS:
|
|
167
|
-
*
|
|
174
|
+
* Endless logo band in pure CSS: renders with SSR. The items are listed once for assistive tech;
|
|
175
|
+
* a duplicate track (`aria-hidden` + `inert`, so its links never take focus) closes the loop.
|
|
176
|
+
* Pauses on hover/focus and with its pause control. Reduced motion: static, scrollable row.
|
|
168
177
|
*/
|
|
169
|
-
export declare function Marquee({ label, duration, reverse, pauseOnHover, fade, gap, className, style, children, ...props }: MarqueeProps): React.JSX.Element;
|
|
178
|
+
export declare function Marquee({ label, duration, reverse, pauseOnHover, fade, gap, controls, pauseLabel, className, style, children, ...props }: MarqueeProps): React.JSX.Element;
|
|
170
179
|
export {};
|
|
@@ -27,6 +27,7 @@ const carouselLabels = {
|
|
|
27
27
|
thumbs: "Slide thumbnails",
|
|
28
28
|
empty: "Nothing to show yet",
|
|
29
29
|
loading: "Loading slides",
|
|
30
|
+
marqueePause: "Pause scrolling",
|
|
30
31
|
};
|
|
31
32
|
const CarouselContext = React.createContext(null);
|
|
32
33
|
function useCarouselContext(part) {
|
|
@@ -338,11 +339,13 @@ export const CarouselCounter = React.forwardRef(function CarouselCounter({ class
|
|
|
338
339
|
return (_jsxs("span", { ref: ref, "data-slot": "carousel-counter", className: cn("tabular text-[13px] text-fg-muted", className), ...props, children: [_jsx("span", { className: "sr-only", children: c.t("counter", { index: c.nf.format(c.index + 1), total: c.nf.format(c.count) }) }), _jsxs("span", { "aria-hidden": true, children: [_jsx("span", { className: "font-medium text-fg", children: c.nf.format(c.index + 1) }), _jsx("span", { className: "mx-1 text-fg-subtle", children: "/" }), c.nf.format(c.count)] })] }));
|
|
339
340
|
});
|
|
340
341
|
/**
|
|
341
|
-
* Endless logo band in pure CSS:
|
|
342
|
-
*
|
|
342
|
+
* Endless logo band in pure CSS: renders with SSR. The items are listed once for assistive tech;
|
|
343
|
+
* a duplicate track (`aria-hidden` + `inert`, so its links never take focus) closes the loop.
|
|
344
|
+
* Pauses on hover/focus and with its pause control. Reduced motion: static, scrollable row.
|
|
343
345
|
*/
|
|
344
|
-
export function Marquee({ label, duration = 40, reverse, pauseOnHover = true, fade = true, gap = "lg", className, style, children, ...props }) {
|
|
346
|
+
export function Marquee({ label, duration = 40, reverse, pauseOnHover = true, fade = true, gap = "lg", controls = true, pauseLabel, className, style, children, ...props }) {
|
|
347
|
+
const t = useLabels("carousel", carouselLabels, pauseLabel ? { marqueePause: pauseLabel } : undefined);
|
|
345
348
|
const items = React.Children.toArray(children);
|
|
346
|
-
const track = (hidden) => (_jsx("ul", { "aria-hidden": hidden || undefined, "aria-label": hidden ? undefined : label, className: "vui-marquee-track flex shrink-0 items-center", children: items.map((child, i) => (_jsx("li", { className: "flex shrink-0 items-center", children: child }, i))) }));
|
|
347
|
-
return (_jsxs("div", { "data-slot": "marquee", "data-reverse": reverse ? "" : undefined, "data-pause-on-hover": pauseOnHover ? "" : undefined, "data-fade": fade ? "" : undefined, className: cn("vui-marquee
|
|
349
|
+
const track = (hidden) => (_jsx("ul", { "aria-hidden": hidden || undefined, inert: hidden || undefined, "aria-label": hidden ? undefined : label, className: "vui-marquee-track flex shrink-0 items-center", children: items.map((child, i) => (_jsx("li", { className: "flex shrink-0 items-center", children: child }, i))) }));
|
|
350
|
+
return (_jsxs("div", { "data-slot": "marquee", "data-reverse": reverse ? "" : undefined, "data-pause-on-hover": pauseOnHover ? "" : undefined, "data-fade": fade ? "" : undefined, "data-controls": controls ? "" : undefined, className: cn("vui-marquee relative", className), style: { "--marquee-duration": `${duration}s`, "--marquee-gap": GAP[gap], ...style }, ...props, children: [_jsxs("div", { className: "vui-marquee-viewport flex overflow-hidden", children: [track(false), track(true)] }), controls ? (_jsxs("label", { className: "vui-marquee-toggle", children: [_jsx("input", { type: "checkbox", className: "vui-marquee-pause sr-only", "aria-label": t("marqueePause") }), _jsx(Pause, { "aria-hidden": true, className: "vui-marquee-icon-pause" }), _jsx(Play, { "aria-hidden": true, className: "vui-marquee-icon-play" })] })) : null] }));
|
|
348
351
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Collapsible as CollapsiblePrimitive } from "radix-ui";
|
|
3
|
+
export interface CollapsibleProps extends React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root> {
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* One show/hide region with its trigger (WAI-ARIA disclosure). Controlled (`open` / `onOpenChange`)
|
|
7
|
+
* or uncontrolled (`defaultOpen`). For several sections that coordinate, use Accordion.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <Collapsible>
|
|
11
|
+
* <CollapsibleTrigger>Advanced settings</CollapsibleTrigger>
|
|
12
|
+
* <CollapsibleContent>…</CollapsibleContent>
|
|
13
|
+
* </Collapsible>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare const Collapsible: React.ForwardRefExoticComponent<CollapsibleProps & React.RefAttributes<HTMLDivElement>>;
|
|
17
|
+
export interface CollapsibleTriggerProps extends React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger> {
|
|
18
|
+
/** Trailing chevron that turns when open. Default `true` (ignored with `asChild`). */
|
|
19
|
+
chevron?: boolean;
|
|
20
|
+
/** Leading icon. */
|
|
21
|
+
icon?: React.ReactNode;
|
|
22
|
+
/** Right-side summary while closed, e.g. "4 fields". Part of the button's name. */
|
|
23
|
+
meta?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The toggle button (`aria-expanded`, `aria-controls`). Styled as a quiet full-width row; pass
|
|
27
|
+
* `asChild` to make any button (a Button, a link-styled text) the trigger instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CollapsibleTrigger: React.ForwardRefExoticComponent<CollapsibleTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
30
|
+
export interface CollapsibleContentProps extends React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content> {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The hidden region. Animates its height (`ui-collapse`); no animation with
|
|
34
|
+
* `prefers-reduced-motion` or inside `data-motion="off"`. `forceMount` keeps it in the DOM while closed.
|
|
35
|
+
*/
|
|
36
|
+
export declare const CollapsibleContent: React.ForwardRefExoticComponent<CollapsibleContentProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Collapsible as CollapsiblePrimitive } from "radix-ui";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
7
|
+
/**
|
|
8
|
+
* One show/hide region with its trigger (WAI-ARIA disclosure). Controlled (`open` / `onOpenChange`)
|
|
9
|
+
* or uncontrolled (`defaultOpen`). For several sections that coordinate, use Accordion.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <Collapsible>
|
|
13
|
+
* <CollapsibleTrigger>Advanced settings</CollapsibleTrigger>
|
|
14
|
+
* <CollapsibleContent>…</CollapsibleContent>
|
|
15
|
+
* </Collapsible>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const Collapsible = React.forwardRef(function Collapsible({ className, ...props }, ref) {
|
|
19
|
+
return _jsx(CollapsiblePrimitive.Root, { ref: ref, "data-slot": "collapsible", className: cn("group/collapsible", className), ...props });
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* The toggle button (`aria-expanded`, `aria-controls`). Styled as a quiet full-width row; pass
|
|
23
|
+
* `asChild` to make any button (a Button, a link-styled text) the trigger instead.
|
|
24
|
+
*/
|
|
25
|
+
export const CollapsibleTrigger = React.forwardRef(function CollapsibleTrigger({ className, chevron = true, icon, meta, asChild, children, ...props }, ref) {
|
|
26
|
+
if (asChild) {
|
|
27
|
+
return (_jsx(CollapsiblePrimitive.Trigger, { ref: ref, "data-slot": "collapsible-trigger", asChild: true, className: className, ...props, children: children }));
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs(CollapsiblePrimitive.Trigger, { ref: ref, "data-slot": "collapsible-trigger", className: cn("group/collapsible-trigger flex w-full min-w-0 items-center gap-2.5 rounded-ui-sm py-2 text-start text-sm font-medium text-fg outline-none transition-colors focus-ring", "hover:[&_[data-slot=collapsible-title]]:underline hover:[&_[data-slot=collapsible-title]]:decoration-line hover:[&_[data-slot=collapsible-title]]:underline-offset-4", "disabled:cursor-not-allowed disabled:opacity-45", "[&_svg]:size-4 [&_svg]:shrink-0", className), ...props, children: [icon ? (_jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0 text-fg-muted", children: icon })) : null, _jsx("span", { "data-slot": "collapsible-title", className: "min-w-0 flex-1 text-pretty", children: children }), meta ? _jsx("span", { className: "sr-only", children: ", " }) : null, meta ? (_jsx("span", { "data-slot": "collapsible-meta", className: "tabular shrink-0 truncate text-[13px] font-normal text-fg-subtle", children: meta })) : null, chevron ? (_jsx(ChevronDown, { "aria-hidden": true, className: "text-fg-subtle transition-transform duration-200 group-data-[state=open]/collapsible-trigger:rotate-180 motion-reduce:transition-none in-data-[motion=off]:transition-none" })) : null] }));
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* The hidden region. Animates its height (`ui-collapse`); no animation with
|
|
33
|
+
* `prefers-reduced-motion` or inside `data-motion="off"`. `forceMount` keeps it in the DOM while closed.
|
|
34
|
+
*/
|
|
35
|
+
export const CollapsibleContent = React.forwardRef(function CollapsibleContent({ className, ...props }, ref) {
|
|
36
|
+
return _jsx(CollapsiblePrimitive.Content, { ref: ref, "data-slot": "collapsible-content", className: cn("ui-collapse in-data-[motion=off]:animate-none", className), ...props });
|
|
37
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
import type { CropRect, ImageLoader, MediaAsset } from "../media/types.js";
|
|
4
|
+
import { type PlaceholderArtPattern } from "./placeholder-art.js";
|
|
4
5
|
export type ImageAspect = "auto" | "1/1" | "4/3" | "3/2" | "16/9" | "21/9" | "4/5" | "1.91/1" | number;
|
|
5
6
|
/** Default `srcset` steps. */
|
|
6
7
|
export declare const IMAGE_WIDTHS: number[];
|
|
@@ -57,7 +58,7 @@ declare const imageLabels: {
|
|
|
57
58
|
error: string;
|
|
58
59
|
};
|
|
59
60
|
export type ImageLabels = typeof imageLabels;
|
|
60
|
-
export type ImagePlaceholder = "blur" | "color" | "skeleton" | "none";
|
|
61
|
+
export type ImagePlaceholder = "blur" | "color" | "skeleton" | "art" | "none";
|
|
61
62
|
export interface ImageBaseProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "placeholder" | "width" | "height" | "alt">, Omit<VariantProps<typeof imageVariants>, "fit"> {
|
|
62
63
|
width?: number;
|
|
63
64
|
height?: number;
|
|
@@ -77,8 +78,20 @@ export interface ImageBaseProps extends Omit<React.ImgHTMLAttributes<HTMLImageEl
|
|
|
77
78
|
quality?: number;
|
|
78
79
|
/** Above the fold: eager loading + `fetchpriority="high"`. */
|
|
79
80
|
priority?: boolean;
|
|
80
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* While loading. Default: `blur` with a blur source, `color` with a dominant colour, else `skeleton`.
|
|
83
|
+
* `art` shows a seeded token pattern (PlaceholderArt) while loading and on error. Without a `src`
|
|
84
|
+
* the image always renders as art.
|
|
85
|
+
*/
|
|
81
86
|
placeholder?: ImagePlaceholder;
|
|
87
|
+
/** Seed for the art placeholder. Default: `alt`, then the URL. Same seed → same pattern. */
|
|
88
|
+
seed?: string;
|
|
89
|
+
/** Force the art pattern. Default `"auto"` (from the seed). */
|
|
90
|
+
placeholderPattern?: PlaceholderArtPattern | "auto";
|
|
91
|
+
/** Glyph in the art placeholder. `false` hides it. */
|
|
92
|
+
placeholderIcon?: React.ReactNode | false;
|
|
93
|
+
/** Print `alt` (or this node) as a small caption on the art placeholder. */
|
|
94
|
+
placeholderCaption?: boolean | React.ReactNode;
|
|
82
95
|
/** Tiny blurred preview (LQIP data URL or a small URL). Defaults to `asset.placeholder`. */
|
|
83
96
|
blurDataURL?: string;
|
|
84
97
|
/** Placeholder colour. Defaults to `asset.dominantColor`. */
|
|
@@ -103,6 +116,9 @@ export type ImageProps = ImageBaseProps & ({
|
|
|
103
116
|
} | {
|
|
104
117
|
src: MediaAsset;
|
|
105
118
|
alt?: string;
|
|
119
|
+
} | {
|
|
120
|
+
src?: null | "";
|
|
121
|
+
alt: string;
|
|
106
122
|
});
|
|
107
123
|
/**
|
|
108
124
|
* The image primitive: no layout shift (width/height + `aspect-ratio`), focal point →
|
package/dist/components/image.js
CHANGED
|
@@ -9,6 +9,7 @@ import { DefaultImage, useImage, useMediaAdapter } from "../lib/provider.js";
|
|
|
9
9
|
import { useComposedRefs } from "../lib/compose-refs.js";
|
|
10
10
|
import { useIsomorphicLayoutEffect } from "../lib/use-controllable-state.js";
|
|
11
11
|
import { Spinner } from "./spinner.js";
|
|
12
|
+
import { PlaceholderArt } from "./placeholder-art.js";
|
|
12
13
|
/** Default `srcset` steps. */
|
|
13
14
|
export const IMAGE_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
|
|
14
15
|
/** `"16/9"` → 1.777…, `1.5` → 1.5, `"auto"` → the intrinsic ratio (or `undefined`). */
|
|
@@ -107,9 +108,10 @@ const FILENAME_ALT = /(^(img|dsc|pxl|screenshot)[-_ ]?\d+)|\.(jpe?g|png|gif|webp
|
|
|
107
108
|
* <Image src={asset} alt="Lobby of the Kadıköy office" aspect="16/9" sizes="(min-width: 1024px) 50vw, 100vw" rounded="ui" />
|
|
108
109
|
* ```
|
|
109
110
|
*/
|
|
110
|
-
export const Image = React.forwardRef(function Image({ src: source, alt: altProp, width: widthProp, height: heightProp, aspect = "auto", fit = "cover", focal: focalProp, crop, sizes, widths, quality, priority, placeholder: placeholderProp, blurDataURL, color: colorProp, loader, fallback, processing: processingProp, rounded, frame, className, imgClassName, style, labels, onLoad, onError, loading, decoding, ...props }, forwardedRef) {
|
|
111
|
+
export const Image = React.forwardRef(function Image({ src: source, alt: altProp, width: widthProp, height: heightProp, aspect = "auto", fit = "cover", focal: focalProp, crop, sizes, widths, quality, priority, placeholder: placeholderProp, blurDataURL, seed, placeholderPattern, placeholderIcon, placeholderCaption, color: colorProp, loader, fallback, processing: processingProp, rounded, frame, className, imgClassName, style, labels, onLoad, onError, loading, decoding, ...props }, forwardedRef) {
|
|
111
112
|
const t = useLabels("image", imageLabels, labels);
|
|
112
113
|
const Bridge = useImage();
|
|
114
|
+
const empty = !source || (typeof source === "string" && !source.trim());
|
|
113
115
|
const asset = isAsset(source) ? source : undefined;
|
|
114
116
|
const alt = altProp ?? asset?.alt ?? "";
|
|
115
117
|
const width = widthProp ?? asset?.width;
|
|
@@ -118,9 +120,10 @@ export const Image = React.forwardRef(function Image({ src: source, alt: altProp
|
|
|
118
120
|
const blur = blurDataURL ?? asset?.placeholder;
|
|
119
121
|
const color = colorProp ?? asset?.dominantColor;
|
|
120
122
|
const processing = processingProp ?? asset?.status === "processing";
|
|
121
|
-
const placeholder = placeholderProp ?? (blur ? "blur" : color ? "color" : "skeleton");
|
|
122
|
-
|
|
123
|
-
const
|
|
123
|
+
const placeholder = empty ? "art" : placeholderProp ?? (blur ? "blur" : color ? "color" : "skeleton");
|
|
124
|
+
// An empty image has no intrinsic size: fall back to 4:3 so it never collapses.
|
|
125
|
+
const ratio = aspectRatioOf(aspect, width, height) ?? (empty ? 4 / 3 : undefined);
|
|
126
|
+
const sources = useImageSources(source || "", { sizes, widths, loader, quality, focal, crop });
|
|
124
127
|
const [state, setState] = React.useState("loading");
|
|
125
128
|
const imgRef = React.useRef(null);
|
|
126
129
|
const ref = useComposedRefs(forwardedRef, imgRef);
|
|
@@ -138,6 +141,10 @@ export const Image = React.forwardRef(function Image({ src: source, alt: altProp
|
|
|
138
141
|
if (process.env.NODE_ENV !== "production" && alt && FILENAME_ALT.test(alt.trim())) {
|
|
139
142
|
console.warn(`Image: alt text "${alt}" looks like a file name. Describe the image instead, or pass alt="" if it is decorative.`);
|
|
140
143
|
}
|
|
144
|
+
const art = (decorative) => (_jsx(PlaceholderArt, { "data-slot": "image-art", seed: seed ?? (alt || (typeof source === "string" ? source : asset?.id) || ""), pattern: placeholderPattern, icon: placeholderIcon, caption: placeholderCaption === true ? alt || undefined : placeholderCaption, label: decorative ? undefined : alt || undefined, className: "absolute inset-0" }));
|
|
145
|
+
if (empty) {
|
|
146
|
+
return (_jsx("span", { "data-slot": "image", "data-state": "empty", "data-placeholder": "art", className: cn(imageVariants({ rounded, frame }), "bg-transparent", className), style: { aspectRatio: String(ratio), ...style }, children: art(false) }));
|
|
147
|
+
}
|
|
141
148
|
const objectPosition = focalToObjectPosition(focal);
|
|
142
149
|
const positioned = ratio !== undefined;
|
|
143
150
|
const imgClasses = cn("vui-image-img block", positioned ? "absolute inset-0 size-full" : "h-auto w-full", fit === "contain" ? "object-contain" : "object-cover", imgClassName);
|
|
@@ -156,7 +163,7 @@ export const Image = React.forwardRef(function Image({ src: source, alt: altProp
|
|
|
156
163
|
...(positioned ? { aspectRatio: String(ratio) } : null),
|
|
157
164
|
...(placeholder === "color" && color ? { backgroundColor: color } : null),
|
|
158
165
|
...style,
|
|
159
|
-
}, children: [placeholder === "blur" && blur && state !== "error" ? (_jsx("img", { "aria-hidden": true, alt: "", src: blur, "data-slot": "image-blur", className: "vui-image-blur pointer-events-none absolute inset-0 size-full object-cover", style: imgStyle })) : null, placeholder === "skeleton" && state === "loading" ? _jsx("span", { "aria-hidden": true, "data-slot": "image-skeleton", className: "ui-shimmer absolute inset-0" }) : null, state === "error" ? (fallback ? (_jsx("span", { "data-slot": "image-fallback", className: cn(positioned ? "absolute inset-0" : "block min-h-24", "flex items-center justify-center"), children: fallback })) : (_jsx("span", { "data-slot": "image-fallback", ...(alt ? { role: "img", "aria-label": alt } : { "aria-hidden": true }), title: t("error"), className: cn(positioned ? "absolute inset-0" : "block min-h-24", "flex items-center justify-center text-fg-subtle [&_svg]:size-6"), children: _jsx(ImageOff, { "aria-hidden": true, strokeWidth: 1.5 }) }))) : (img), processing ? (_jsxs("span", { "data-slot": "image-processing", className: "surface-popover absolute start-2 top-2 z-10 inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium", children: [_jsx(Spinner, { size: "xs", tone: "accent" }), t("processing")] })) : null] }));
|
|
166
|
+
}, children: [placeholder === "blur" && blur && state !== "error" ? (_jsx("img", { "aria-hidden": true, alt: "", src: blur, "data-slot": "image-blur", className: "vui-image-blur pointer-events-none absolute inset-0 size-full object-cover", style: imgStyle })) : null, placeholder === "skeleton" && state === "loading" ? _jsx("span", { "aria-hidden": true, "data-slot": "image-skeleton", className: "ui-shimmer absolute inset-0" }) : null, placeholder === "art" && state === "loading" ? art(true) : null, state === "error" ? (placeholder === "art" && !fallback ? (_jsx("span", { "data-slot": "image-fallback", title: t("error"), className: cn(positioned ? "absolute inset-0" : "relative block min-h-24"), children: art(false) })) : fallback ? (_jsx("span", { "data-slot": "image-fallback", className: cn(positioned ? "absolute inset-0" : "block min-h-24", "flex items-center justify-center"), children: fallback })) : (_jsx("span", { "data-slot": "image-fallback", ...(alt ? { role: "img", "aria-label": alt } : { "aria-hidden": true }), title: t("error"), className: cn(positioned ? "absolute inset-0" : "block min-h-24", "flex items-center justify-center text-fg-subtle [&_svg]:size-6"), children: _jsx(ImageOff, { "aria-hidden": true, strokeWidth: 1.5 }) }))) : (img), processing ? (_jsxs("span", { "data-slot": "image-processing", className: "surface-popover absolute start-2 top-2 z-10 inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium", children: [_jsx(Spinner, { size: "xs", tone: "accent" }), t("processing")] })) : null] }));
|
|
160
167
|
});
|
|
161
168
|
/** `<figure>` for a captioned image (blocks, galleries, articles). */
|
|
162
169
|
export const Figure = React.forwardRef(function Figure({ gap = "sm", className, ...props }, ref) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Menubar as MenubarPrimitive } from "radix-ui";
|
|
3
|
+
import { type VariantProps } from "class-variance-authority";
|
|
4
|
+
import type { MenuEntry } from "../lib/variants.js";
|
|
5
|
+
export declare const menubarVariants: (props?: ({
|
|
6
|
+
variant?: "ghost" | "bar" | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
/** One top-level menu in the data-driven form. */
|
|
9
|
+
export interface MenubarMenuEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
/** Trigger text ("File", "Edit", "View"…). */
|
|
12
|
+
label: React.ReactNode;
|
|
13
|
+
/** The menu's entries — the same `MenuEntry` model as Dropdown Menu, Context Menu and Command. */
|
|
14
|
+
items: MenuEntry[];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Hidden unless `can(permission)` is true (see `useCan`). */
|
|
17
|
+
permission?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MenubarProps extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>, VariantProps<typeof menubarVariants> {
|
|
20
|
+
/** Data-driven menus. Rendered before `children`. */
|
|
21
|
+
menus?: MenubarMenuEntry[];
|
|
22
|
+
/** Minimum panel width of every menu: `sm` 180px, `md` 220px. Default `md`. */
|
|
23
|
+
size?: "sm" | "md";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A desktop-app menu bar (WAI-ARIA `menubar`): File / Edit / View… One tab stop; ← → move between
|
|
27
|
+
* menus (and keep the open one open), ↓ / Enter / Space open, Esc closes, typeahead inside a menu.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <Menubar aria-label="Editor" menus={[
|
|
31
|
+
* { id: "file", label: "File", items: [{ id: "new", label: "New document", shortcut: "mod+n", onSelect: create }] },
|
|
32
|
+
* ]} />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const Menubar: React.ForwardRefExoticComponent<MenubarProps & React.RefAttributes<HTMLDivElement>>;
|
|
36
|
+
/** One menu: a `MenubarTrigger` plus its `MenubarContent`. `value` is needed only for a controlled bar. */
|
|
37
|
+
export declare const MenubarMenu: typeof MenubarPrimitive.Menu;
|
|
38
|
+
export declare const MenubarGroup: React.ForwardRefExoticComponent<MenubarPrimitive.MenubarGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
39
|
+
export declare const MenubarSub: React.FC<MenubarPrimitive.MenubarSubProps>;
|
|
40
|
+
export declare const MenubarRadioGroup: React.ForwardRefExoticComponent<MenubarPrimitive.MenubarRadioGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
/** The top-level button ("File"). */
|
|
42
|
+
export declare const MenubarTrigger: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
43
|
+
export interface MenubarContentProps extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> {
|
|
44
|
+
/** Overrides the bar's `size` for this menu. */
|
|
45
|
+
size?: "sm" | "md";
|
|
46
|
+
}
|
|
47
|
+
/** A menu panel under its trigger. Portals into the nearest `ThemeScope`; only this first layer blurs in glass mode. */
|
|
48
|
+
export declare const MenubarContent: React.ForwardRefExoticComponent<MenubarContentProps & React.RefAttributes<HTMLDivElement>>;
|
|
49
|
+
export interface MenubarItemProps extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> {
|
|
50
|
+
icon?: React.ReactNode;
|
|
51
|
+
description?: React.ReactNode;
|
|
52
|
+
/** Shortcut hint, e.g. `"mod+s"` (rendered with Kbd). Display only — bind it with `useHotkeys`. */
|
|
53
|
+
shortcut?: string | string[];
|
|
54
|
+
badge?: React.ReactNode;
|
|
55
|
+
tone?: "default" | "danger";
|
|
56
|
+
/** Reserve the indicator gutter so labels align with checkbox/radio items. */
|
|
57
|
+
inset?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** One command. `onSelect` fires on click, Enter and Space, then the menu closes (`event.preventDefault()` keeps it open). */
|
|
60
|
+
export declare const MenubarItem: React.ForwardRefExoticComponent<MenubarItemProps & React.RefAttributes<HTMLDivElement>>;
|
|
61
|
+
export interface MenubarCheckboxItemProps extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> {
|
|
62
|
+
shortcut?: string | string[];
|
|
63
|
+
description?: React.ReactNode;
|
|
64
|
+
}
|
|
65
|
+
/** A toggle (`menuitemcheckbox`) — "Show ruler", "Word wrap". */
|
|
66
|
+
export declare const MenubarCheckboxItem: React.ForwardRefExoticComponent<MenubarCheckboxItemProps & React.RefAttributes<HTMLDivElement>>;
|
|
67
|
+
export interface MenubarRadioItemProps extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> {
|
|
68
|
+
shortcut?: string | string[];
|
|
69
|
+
description?: React.ReactNode;
|
|
70
|
+
}
|
|
71
|
+
/** One choice inside a `MenubarRadioGroup` (`menuitemradio`) — zoom level, sort order. */
|
|
72
|
+
export declare const MenubarRadioItem: React.ForwardRefExoticComponent<MenubarRadioItemProps & React.RefAttributes<HTMLDivElement>>;
|
|
73
|
+
export declare const MenubarLabel: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
74
|
+
inset?: boolean;
|
|
75
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
76
|
+
export declare const MenubarSeparator: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
77
|
+
/** Trailing text hint (when you don't want keycaps). */
|
|
78
|
+
export declare function MenubarShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>): React.JSX.Element;
|
|
79
|
+
export interface MenubarSubTriggerProps extends React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> {
|
|
80
|
+
icon?: React.ReactNode;
|
|
81
|
+
inset?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** Opens a submenu on hover, Enter, Space or → (← in RTL). */
|
|
84
|
+
export declare const MenubarSubTrigger: React.ForwardRefExoticComponent<MenubarSubTriggerProps & React.RefAttributes<HTMLDivElement>>;
|
|
85
|
+
/** A submenu panel. One layer deeper, so it is opaque in glass mode. */
|
|
86
|
+
export declare const MenubarSubContent: React.ForwardRefExoticComponent<Omit<MenubarPrimitive.MenubarSubContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
87
|
+
size?: "sm" | "md";
|
|
88
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
89
|
+
/** Renders `MenuEntry[]` as menubar items (inside your own `MenubarContent`). */
|
|
90
|
+
export declare function MenubarEntries({ items }: {
|
|
91
|
+
items: MenuEntry[];
|
|
92
|
+
}): React.JSX.Element;
|
|
93
|
+
/** Renders `MenubarMenuEntry[]` (what `Menubar menus` uses). Menus with no visible entries are dropped. */
|
|
94
|
+
export declare function MenubarMenus({ menus }: {
|
|
95
|
+
menus: MenubarMenuEntry[];
|
|
96
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Menubar as MenubarPrimitive } from "radix-ui";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { usePortalContainer } from "../lib/theme-scope.js";
|
|
8
|
+
import { Layer } from "../lib/layer.js";
|
|
9
|
+
import { useCan } from "../lib/provider.js";
|
|
10
|
+
import { MenuCheckIcon, MenuEntries, MenuItemContent, MenuRadioDot, MenuSizeContext, menuContentVariants, menuIndicatorClass, menuItemClass, menuLabelClass, menuSeparatorClass, useVisibleEntries, } from "../lib/menu.js";
|
|
11
|
+
/* ------------------------------------------------------------------ */
|
|
12
|
+
/* Bar */
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
export const menubarVariants = cva("flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none]", {
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
/** A hairline surface strip — the top of an editor or a desktop-style app window. */
|
|
18
|
+
bar: "surface rounded-ui-lg p-1",
|
|
19
|
+
/** No chrome: sits inside a Top bar, toolbar or window header. */
|
|
20
|
+
ghost: "",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: { variant: "bar" },
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* A desktop-app menu bar (WAI-ARIA `menubar`): File / Edit / View… One tab stop; ← → move between
|
|
27
|
+
* menus (and keep the open one open), ↓ / Enter / Space open, Esc closes, typeahead inside a menu.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <Menubar aria-label="Editor" menus={[
|
|
31
|
+
* { id: "file", label: "File", items: [{ id: "new", label: "New document", shortcut: "mod+n", onSelect: create }] },
|
|
32
|
+
* ]} />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const Menubar = React.forwardRef(function Menubar({ className, variant, menus, size = "md", children, loop = true, ...props }, ref) {
|
|
36
|
+
return (_jsx(MenuSizeContext.Provider, { value: size, children: _jsxs(MenubarPrimitive.Root, { ref: ref, "data-slot": "menubar", "data-variant": variant ?? "bar", loop: loop, className: cn(menubarVariants({ variant }), className), ...props, children: [menus ? _jsx(MenubarMenus, { menus: menus }) : null, children] }) }));
|
|
37
|
+
});
|
|
38
|
+
/** One menu: a `MenubarTrigger` plus its `MenubarContent`. `value` is needed only for a controlled bar. */
|
|
39
|
+
export const MenubarMenu = MenubarPrimitive.Menu;
|
|
40
|
+
export const MenubarGroup = MenubarPrimitive.Group;
|
|
41
|
+
export const MenubarSub = MenubarPrimitive.Sub;
|
|
42
|
+
export const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
|
43
|
+
/** The top-level button ("File"). */
|
|
44
|
+
export const MenubarTrigger = React.forwardRef(function MenubarTrigger({ className, ...props }, ref) {
|
|
45
|
+
return (_jsx(MenubarPrimitive.Trigger, { ref: ref, "data-slot": "menubar-trigger", className: cn("inline-flex h-[calc(var(--control-h)-0.5rem)] shrink-0 cursor-default select-none items-center gap-1.5 rounded-ui-sm px-2.5 text-[13px] font-medium text-fg outline-none", "transition-colors duration-100 hover:bg-hover focus-visible:bg-hover focus-ring", "data-highlighted:bg-hover data-[state=open]:bg-selected", "disabled:pointer-events-none disabled:opacity-45 data-disabled:pointer-events-none data-disabled:opacity-45", "[&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-fg-muted", className), ...props }));
|
|
46
|
+
});
|
|
47
|
+
/** A menu panel under its trigger. Portals into the nearest `ThemeScope`; only this first layer blurs in glass mode. */
|
|
48
|
+
export const MenubarContent = React.forwardRef(function MenubarContent({ className, size: sizeProp, sideOffset = 6, alignOffset = -4, collisionPadding = 12, align = "start", ...props }, ref) {
|
|
49
|
+
const container = usePortalContainer();
|
|
50
|
+
const barSize = React.useContext(MenuSizeContext);
|
|
51
|
+
const size = sizeProp ?? barSize;
|
|
52
|
+
return (_jsx(MenubarPrimitive.Portal, { container: container, children: _jsx(MenuSizeContext.Provider, { value: size, children: _jsx(Layer, { asChild: true, children: _jsx(MenubarPrimitive.Content, { ref: ref, "data-slot": "menubar-content", sideOffset: sideOffset, alignOffset: alignOffset, collisionPadding: collisionPadding, align: align, className: cn(menuContentVariants({ size }), "max-h-(--radix-menubar-content-available-height) origin-(--radix-menubar-content-transform-origin)", className), ...props }) }) }) }));
|
|
53
|
+
});
|
|
54
|
+
/** One command. `onSelect` fires on click, Enter and Space, then the menu closes (`event.preventDefault()` keeps it open). */
|
|
55
|
+
export const MenubarItem = React.forwardRef(function MenubarItem({ className, icon, description, shortcut, badge, tone = "default", inset, asChild, children, ...props }, ref) {
|
|
56
|
+
const size = React.useContext(MenuSizeContext);
|
|
57
|
+
return (_jsx(MenubarPrimitive.Item, { ref: ref, "data-slot": "menubar-item", "data-tone": tone, asChild: asChild, className: menuItemClass({ size, tone, inset, className }), ...props, children: asChild ? (children) : (_jsx(MenuItemContent, { icon: icon, description: description, shortcut: shortcut, badge: badge, children: children })) }));
|
|
58
|
+
});
|
|
59
|
+
/** A toggle (`menuitemcheckbox`) — "Show ruler", "Word wrap". */
|
|
60
|
+
export const MenubarCheckboxItem = React.forwardRef(function MenubarCheckboxItem({ className, shortcut, description, children, ...props }, ref) {
|
|
61
|
+
const size = React.useContext(MenuSizeContext);
|
|
62
|
+
return (_jsxs(MenubarPrimitive.CheckboxItem, { ref: ref, "data-slot": "menubar-checkbox-item", className: menuItemClass({ size, inset: true, className }), ...props, children: [_jsx("span", { className: menuIndicatorClass, children: _jsx(MenubarPrimitive.ItemIndicator, { children: _jsx(MenuCheckIcon, {}) }) }), _jsx(MenuItemContent, { shortcut: shortcut, description: description, children: children })] }));
|
|
63
|
+
});
|
|
64
|
+
/** One choice inside a `MenubarRadioGroup` (`menuitemradio`) — zoom level, sort order. */
|
|
65
|
+
export const MenubarRadioItem = React.forwardRef(function MenubarRadioItem({ className, shortcut, description, children, ...props }, ref) {
|
|
66
|
+
const size = React.useContext(MenuSizeContext);
|
|
67
|
+
return (_jsxs(MenubarPrimitive.RadioItem, { ref: ref, "data-slot": "menubar-radio-item", className: menuItemClass({ size, inset: true, className }), ...props, children: [_jsx("span", { className: menuIndicatorClass, children: _jsx(MenubarPrimitive.ItemIndicator, { children: _jsx(MenuRadioDot, {}) }) }), _jsx(MenuItemContent, { shortcut: shortcut, description: description, children: children })] }));
|
|
68
|
+
});
|
|
69
|
+
export const MenubarLabel = React.forwardRef(function MenubarLabel({ className, inset, ...props }, ref) {
|
|
70
|
+
return _jsx(MenubarPrimitive.Label, { ref: ref, "data-slot": "menubar-label", className: cn(menuLabelClass, inset && "ps-8", className), ...props });
|
|
71
|
+
});
|
|
72
|
+
export const MenubarSeparator = React.forwardRef(function MenubarSeparator({ className, ...props }, ref) {
|
|
73
|
+
return _jsx(MenubarPrimitive.Separator, { ref: ref, "data-slot": "menubar-separator", className: cn(menuSeparatorClass, className), ...props });
|
|
74
|
+
});
|
|
75
|
+
/** Trailing text hint (when you don't want keycaps). */
|
|
76
|
+
export function MenubarShortcut({ className, ...props }) {
|
|
77
|
+
return _jsx("span", { "data-slot": "menubar-shortcut", className: cn("ms-auto ps-4 font-mono text-[11px] tracking-wide text-fg-subtle", className), ...props });
|
|
78
|
+
}
|
|
79
|
+
/** Opens a submenu on hover, Enter, Space or → (← in RTL). */
|
|
80
|
+
export const MenubarSubTrigger = React.forwardRef(function MenubarSubTrigger({ className, icon, inset, children, ...props }, ref) {
|
|
81
|
+
const size = React.useContext(MenuSizeContext);
|
|
82
|
+
return (_jsx(MenubarPrimitive.SubTrigger, { ref: ref, "data-slot": "menubar-sub-trigger", className: menuItemClass({ size, inset, className: cn("data-[state=open]:bg-hover", className) }), ...props, children: _jsx(MenuItemContent, { icon: icon, chevron: true, children: children }) }));
|
|
83
|
+
});
|
|
84
|
+
/** A submenu panel. One layer deeper, so it is opaque in glass mode. */
|
|
85
|
+
export const MenubarSubContent = React.forwardRef(function MenubarSubContent({ className, size: sizeProp, sideOffset = 4, alignOffset = -5, collisionPadding = 12, ...props }, ref) {
|
|
86
|
+
const container = usePortalContainer();
|
|
87
|
+
const parentSize = React.useContext(MenuSizeContext);
|
|
88
|
+
const size = sizeProp ?? parentSize;
|
|
89
|
+
return (_jsx(MenubarPrimitive.Portal, { container: container, children: _jsx(Layer, { asChild: true, children: _jsx(MenubarPrimitive.SubContent, { ref: ref, "data-slot": "menubar-sub-content", sideOffset: sideOffset, alignOffset: alignOffset, collisionPadding: collisionPadding, className: cn(menuContentVariants({ size }), "max-h-(--radix-menubar-content-available-height)", className), ...props }) }) }));
|
|
90
|
+
});
|
|
91
|
+
/* ------------------------------------------------------------------ */
|
|
92
|
+
/* Data-driven */
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
const menubarParts = {
|
|
95
|
+
Item: MenubarItem,
|
|
96
|
+
CheckboxItem: MenubarCheckboxItem,
|
|
97
|
+
RadioGroup: MenubarRadioGroup,
|
|
98
|
+
RadioItem: MenubarRadioItem,
|
|
99
|
+
Label: MenubarLabel,
|
|
100
|
+
Separator: MenubarSeparator,
|
|
101
|
+
Group: MenubarGroup,
|
|
102
|
+
Sub: MenubarSub,
|
|
103
|
+
SubTrigger: MenubarSubTrigger,
|
|
104
|
+
SubContent: MenubarSubContent,
|
|
105
|
+
};
|
|
106
|
+
/** Renders `MenuEntry[]` as menubar items (inside your own `MenubarContent`). */
|
|
107
|
+
export function MenubarEntries({ items }) {
|
|
108
|
+
const visible = useVisibleEntries(items);
|
|
109
|
+
return _jsx(MenuEntries, { entries: visible, parts: menubarParts });
|
|
110
|
+
}
|
|
111
|
+
function MenubarDataMenu({ menu }) {
|
|
112
|
+
const visible = useVisibleEntries(menu.items);
|
|
113
|
+
if (visible.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
return (_jsxs(MenubarMenu, { value: menu.id, children: [_jsx(MenubarTrigger, { disabled: menu.disabled, children: menu.label }), _jsx(MenubarContent, { children: _jsx(MenuEntries, { entries: visible, parts: menubarParts }) })] }));
|
|
116
|
+
}
|
|
117
|
+
/** Renders `MenubarMenuEntry[]` (what `Menubar menus` uses). Menus with no visible entries are dropped. */
|
|
118
|
+
export function MenubarMenus({ menus }) {
|
|
119
|
+
const can = useCan();
|
|
120
|
+
return (_jsx(_Fragment, { children: menus
|
|
121
|
+
.filter((m) => !m.permission || can(m.permission))
|
|
122
|
+
.map((m) => (_jsx(MenubarDataMenu, { menu: m }, m.id))) }));
|
|
123
|
+
}
|