@giddaa-housing/ui 3.3.0 → 3.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/css/generated/shared.css +1 -1
- package/css/theme.css +20 -0
- package/dist/badge.js +5 -0
- package/dist/price-tag.d.ts +80 -12
- package/dist/price-tag.js +98 -57
- package/dist/scroll-spy.d.ts +16 -1
- package/dist/scroll-spy.js +25 -2
- package/dist/sonar.d.ts +51 -0
- package/dist/sonar.js +117 -0
- package/dist/styles.css +43 -0
- package/package.json +5 -1
package/css/theme.css
CHANGED
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
|
142
142
|
--animate-sonar-glow: sonar-glow 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
|
|
143
143
|
--animate-round-sonar-glow: round-sonar-glow 1.5s ease-out infinite;
|
|
144
|
+
--animate-sonar-wave: sonar-wave 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;
|
|
144
145
|
--animate-spin-slow: spin 3s linear infinite;
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -175,6 +176,7 @@
|
|
|
175
176
|
--animate-bouncing-loader: none;
|
|
176
177
|
--animate-sonar-glow: none;
|
|
177
178
|
--animate-round-sonar-glow: none;
|
|
179
|
+
--animate-sonar-wave: none;
|
|
178
180
|
--animate-spin-slow: none;
|
|
179
181
|
}
|
|
180
182
|
}
|
|
@@ -280,3 +282,21 @@
|
|
|
280
282
|
opacity: 0;
|
|
281
283
|
}
|
|
282
284
|
}
|
|
285
|
+
|
|
286
|
+
/* Drives `Sonar`. Spread rather than `transform: scale()` on purpose: spread
|
|
287
|
+
grows the ring by the same number of pixels on every side and traces the
|
|
288
|
+
element's own border-radius, so one keyframe fits a 8px dot and a wide
|
|
289
|
+
button alike. Scaling would expand a wide element far more horizontally
|
|
290
|
+
than vertically — which is why sonar-glow above needs its per-axis
|
|
291
|
+
--sonar-glow-scale-x/y knobs. */
|
|
292
|
+
@keyframes sonar-wave {
|
|
293
|
+
from {
|
|
294
|
+
box-shadow: 0 0 0 0 currentColor;
|
|
295
|
+
opacity: 0.55;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
to {
|
|
299
|
+
box-shadow: 0 0 0 var(--sonar-spread, 0.625rem) currentColor;
|
|
300
|
+
opacity: 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
package/dist/badge.js
CHANGED
|
@@ -36,6 +36,11 @@ const badgeVariants = cva("group/badge inline-flex w-fit shrink-0 items-center j
|
|
|
36
36
|
false: ""
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
+
compoundVariants: [{
|
|
40
|
+
variant: "outline",
|
|
41
|
+
border: false,
|
|
42
|
+
class: "border-line"
|
|
43
|
+
}],
|
|
39
44
|
defaultVariants: {
|
|
40
45
|
variant: "default",
|
|
41
46
|
size: "md",
|
package/dist/price-tag.d.ts
CHANGED
|
@@ -3,32 +3,100 @@ import * as React from "react";
|
|
|
3
3
|
//#region src/price-tag.d.ts
|
|
4
4
|
declare const priceTagVariants: (props?: ({
|
|
5
5
|
size?: "lg" | "md" | "sm" | "xs" | "xxs" | null | undefined;
|
|
6
|
+
hasDescription?: boolean | null | undefined;
|
|
6
7
|
hasMonthlyPayment?: boolean | null | undefined;
|
|
7
8
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
9
|
type PriceTagSize = NonNullable<VariantProps<typeof priceTagVariants>["size"]>;
|
|
10
|
+
/**
|
|
11
|
+
* A currency in the dropdown.
|
|
12
|
+
*
|
|
13
|
+
* `currency`, `value` and `description` are all PriceTag needs, and none of
|
|
14
|
+
* them are interpreted — `description` in particular is printed as given, so
|
|
15
|
+
* the wording is yours. The rest are legacy and go in the next major.
|
|
16
|
+
*/
|
|
9
17
|
type PriceTagCurrencyOption = {
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
/** Shown at the leading edge, and matched against `selectedCurrency`. */
|
|
19
|
+
currency?: string;
|
|
20
|
+
/** The price, exactly as it should read. */
|
|
21
|
+
value?: React.ReactNode;
|
|
22
|
+
/** A second line under the price. Your words, printed as given. */
|
|
23
|
+
description?: React.ReactNode;
|
|
24
|
+
/** @deprecated Use `currency`. Removed in the next major. */
|
|
25
|
+
code?: string;
|
|
26
|
+
/** @deprecated Use `value`. Removed in the next major. */
|
|
27
|
+
amount?: string;
|
|
28
|
+
/** @deprecated Format before you pass it. Removed in the next major. */
|
|
12
29
|
shortAmount?: string;
|
|
30
|
+
/** @deprecated Use `description`. Removed in the next major. */
|
|
13
31
|
monthlyPayment?: string;
|
|
32
|
+
/** @deprecated Use `description`. Removed in the next major. */
|
|
14
33
|
monthlyPaymentLabel?: string;
|
|
15
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* A currency option after the legacy fields have been folded in — what
|
|
37
|
+
* `onCurrencyChange` and `renderMenuOption` receive. The original object is
|
|
38
|
+
* spread in, so code still reading `code` or `amount` off it keeps working.
|
|
39
|
+
*
|
|
40
|
+
* Generic over your own option type, which `PriceTag` infers from `currencies`.
|
|
41
|
+
* Anything extra you hang on an option — a rate, an id, a flag — arrives here
|
|
42
|
+
* typed, so `renderMenuOption` can use it without a cast.
|
|
43
|
+
*/
|
|
44
|
+
type PriceTagMenuOption<TOption extends PriceTagCurrencyOption = PriceTagCurrencyOption> = TOption & {
|
|
45
|
+
currency: string;
|
|
46
|
+
value: React.ReactNode;
|
|
47
|
+
description?: React.ReactNode;
|
|
48
|
+
/** Whether this is the row `selectedCurrency` points at. */
|
|
49
|
+
selected: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** @deprecated Format before you pass it. Removed in the next major. */
|
|
16
52
|
type PriceTagFormat = "full" | "shortened";
|
|
17
|
-
type PriceTagProps = Omit<React.ComponentProps<"button">, "children"> & {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
53
|
+
type PriceTagProps<TOption extends PriceTagCurrencyOption = PriceTagCurrencyOption> = Omit<React.ComponentProps<"button">, "children" | "value"> & {
|
|
54
|
+
/** The price, exactly as it should read. */
|
|
55
|
+
value?: React.ReactNode;
|
|
56
|
+
/** A second line under the price. Your words, printed as given. */
|
|
57
|
+
description?: React.ReactNode;
|
|
21
58
|
size?: PriceTagSize;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Providing this turns the tag into a dropdown trigger. Options carry
|
|
61
|
+
* whatever else you need — `TOption` is inferred from here, and the
|
|
62
|
+
* callbacks below receive it intact.
|
|
63
|
+
*/
|
|
64
|
+
currencies?: TOption[];
|
|
25
65
|
selectedCurrency?: string;
|
|
26
|
-
onCurrencyChange?: (
|
|
66
|
+
onCurrencyChange?: (option: PriceTagMenuOption<NoInfer<TOption>>) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Replace a row's contents. PriceTag keeps the button, its selected state
|
|
69
|
+
* and its keyboard behaviour, so a custom row can't quietly lose them.
|
|
70
|
+
*/
|
|
71
|
+
renderMenuOption?: (option: PriceTagMenuOption<NoInfer<TOption>>) => React.ReactNode;
|
|
27
72
|
open?: boolean;
|
|
28
73
|
defaultOpen?: boolean;
|
|
29
74
|
onOpenChange?: (open: boolean) => void;
|
|
30
75
|
popoverClassName?: string;
|
|
76
|
+
/** @deprecated Use `value`. Removed in the next major. */
|
|
77
|
+
amount?: string;
|
|
78
|
+
/** @deprecated Format before you pass it. Removed in the next major. */
|
|
79
|
+
shortAmount?: string;
|
|
80
|
+
/** @deprecated Format before you pass it. Removed in the next major. */
|
|
81
|
+
format?: PriceTagFormat;
|
|
82
|
+
/** @deprecated Use `description`. Removed in the next major. */
|
|
83
|
+
monthlyPayment?: string;
|
|
84
|
+
/** @deprecated Use `description`. Removed in the next major. */
|
|
85
|
+
monthlyPaymentLabel?: string;
|
|
31
86
|
};
|
|
32
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Pill-shaped price display, optionally a trigger for a currency dropdown.
|
|
89
|
+
*
|
|
90
|
+
* Display only: it prints `value` and `description` as given and never
|
|
91
|
+
* composes a sentence, picks a format, or decides that a line is too small to
|
|
92
|
+
* be worth showing. Wording, currency conversion and number formatting all
|
|
93
|
+
* belong to the app, which is the only place that knows the locale, the
|
|
94
|
+
* product's voice, and what the second line is actually for.
|
|
95
|
+
*
|
|
96
|
+
* The legacy `amount` / `shortAmount` / `format` / `monthlyPayment` props still
|
|
97
|
+
* work and keep their old behaviour exactly, including the size threshold that
|
|
98
|
+
* hid the monthly line below `md`. They go in the next major.
|
|
99
|
+
*/
|
|
100
|
+
declare function PriceTag<TOption extends PriceTagCurrencyOption = PriceTagCurrencyOption>({ value, description, size, currencies, selectedCurrency, onCurrencyChange, renderMenuOption, open, defaultOpen, onOpenChange, className, popoverClassName, type, amount, shortAmount, format, monthlyPayment, monthlyPaymentLabel, ...props }: PriceTagProps<TOption>): React.JSX.Element;
|
|
33
101
|
//#endregion
|
|
34
|
-
export { PriceTag, type PriceTagCurrencyOption, type PriceTagFormat, type PriceTagProps, type PriceTagSize, priceTagVariants };
|
|
102
|
+
export { PriceTag, type PriceTagCurrencyOption, type PriceTagFormat, type PriceTagMenuOption, type PriceTagProps, type PriceTagSize, priceTagVariants };
|
package/dist/price-tag.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Check, ChevronDown } from "./icons.js";
|
|
3
3
|
import { t as cn } from "./cn-BI_4DMBf.js";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
6
|
import { Popover } from "@base-ui/react/popover";
|
|
7
7
|
//#region src/price-tag.tsx
|
|
@@ -14,6 +14,12 @@ const priceTagVariants = cva("group/price-tag inline-flex w-fit shrink-0 items-c
|
|
|
14
14
|
md: "min-h-10.5 gap-1.5 px-5 py-2 text-gdt-md [&>svg]:size-4",
|
|
15
15
|
lg: "min-h-13 gap-2 px-7 py-3 text-gdt-lg [&>svg]:size-4.5"
|
|
16
16
|
},
|
|
17
|
+
/** Two lines sit top-aligned against the chevron rather than centred. */
|
|
18
|
+
hasDescription: {
|
|
19
|
+
true: "items-start",
|
|
20
|
+
false: ""
|
|
21
|
+
},
|
|
22
|
+
/** @deprecated Use `hasDescription`. Removed in the next major. */
|
|
17
23
|
hasMonthlyPayment: {
|
|
18
24
|
true: "items-start",
|
|
19
25
|
false: ""
|
|
@@ -21,37 +27,102 @@ const priceTagVariants = cva("group/price-tag inline-flex w-fit shrink-0 items-c
|
|
|
21
27
|
},
|
|
22
28
|
defaultVariants: {
|
|
23
29
|
size: "md",
|
|
24
|
-
|
|
30
|
+
hasDescription: false
|
|
25
31
|
}
|
|
26
32
|
});
|
|
33
|
+
/** The description is quieter than the price, and quietest at the largest size. */
|
|
34
|
+
const descriptionSizeClassName = {
|
|
35
|
+
xxs: "text-gdt-subtext",
|
|
36
|
+
xs: "text-gdt-subtext",
|
|
37
|
+
sm: "text-gdt-subtext",
|
|
38
|
+
md: "text-gdt-subtext",
|
|
39
|
+
lg: "text-gdt-xs"
|
|
40
|
+
};
|
|
27
41
|
function getDisplayAmount({ amount, shortAmount, format }) {
|
|
28
42
|
return format === "shortened" && shortAmount ? shortAmount : amount;
|
|
29
43
|
}
|
|
30
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Folds the legacy fields into the current three. New fields win outright, so
|
|
46
|
+
* an option can be migrated one at a time.
|
|
47
|
+
*/
|
|
48
|
+
function resolveMenuOption(option, format, selectedCurrency) {
|
|
49
|
+
const currency = option.currency ?? option.code ?? "";
|
|
50
|
+
const value = option.value ?? getDisplayAmount({
|
|
51
|
+
amount: option.amount,
|
|
52
|
+
shortAmount: option.shortAmount,
|
|
53
|
+
format
|
|
54
|
+
});
|
|
55
|
+
const description = option.description ?? (option.monthlyPayment ? `${option.monthlyPaymentLabel ?? "As low as"} ${option.monthlyPayment} per month` : void 0);
|
|
56
|
+
return {
|
|
57
|
+
...option,
|
|
58
|
+
currency,
|
|
59
|
+
value,
|
|
60
|
+
description,
|
|
61
|
+
selected: currency === selectedCurrency
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function DefaultMenuOption({ option }) {
|
|
65
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
66
|
+
/* @__PURE__ */ jsx("span", {
|
|
67
|
+
className: "min-w-10 text-gdt-sm font-semibold text-fg-brand",
|
|
68
|
+
children: option.currency
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ jsxs("span", {
|
|
71
|
+
className: "ml-auto flex min-w-0 flex-col items-end",
|
|
72
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
73
|
+
className: "text-gdt-sm font-bold text-fg-primary",
|
|
74
|
+
children: option.value
|
|
75
|
+
}), option.description ? /* @__PURE__ */ jsx("span", {
|
|
76
|
+
className: "text-gdt-xs text-fg-secondary",
|
|
77
|
+
children: option.description
|
|
78
|
+
}) : null]
|
|
79
|
+
}),
|
|
80
|
+
option.selected ? /* @__PURE__ */ jsx(Check, {
|
|
81
|
+
"aria-hidden": "true",
|
|
82
|
+
className: "size-4 shrink-0 text-fg-brand"
|
|
83
|
+
}) : null
|
|
84
|
+
] });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pill-shaped price display, optionally a trigger for a currency dropdown.
|
|
88
|
+
*
|
|
89
|
+
* Display only: it prints `value` and `description` as given and never
|
|
90
|
+
* composes a sentence, picks a format, or decides that a line is too small to
|
|
91
|
+
* be worth showing. Wording, currency conversion and number formatting all
|
|
92
|
+
* belong to the app, which is the only place that knows the locale, the
|
|
93
|
+
* product's voice, and what the second line is actually for.
|
|
94
|
+
*
|
|
95
|
+
* The legacy `amount` / `shortAmount` / `format` / `monthlyPayment` props still
|
|
96
|
+
* work and keep their old behaviour exactly, including the size threshold that
|
|
97
|
+
* hid the monthly line below `md`. They go in the next major.
|
|
98
|
+
*/
|
|
99
|
+
function PriceTag({ value, description, size = "md", currencies, selectedCurrency, onCurrencyChange, renderMenuOption, open, defaultOpen, onOpenChange, className, popoverClassName, type = "button", amount, shortAmount, format = "full", monthlyPayment, monthlyPaymentLabel = "Low as", ...props }) {
|
|
31
100
|
const hasDropdown = Boolean(currencies?.length);
|
|
32
|
-
const
|
|
101
|
+
const resolvedValue = value ?? getDisplayAmount({
|
|
102
|
+
amount,
|
|
103
|
+
shortAmount,
|
|
104
|
+
format
|
|
105
|
+
});
|
|
106
|
+
const legacyDescription = monthlyPayment && (size === "md" || size === "lg") ? `${monthlyPaymentLabel} ${monthlyPayment} monthly` : void 0;
|
|
107
|
+
const resolvedDescription = description ?? legacyDescription;
|
|
33
108
|
const pill = /* @__PURE__ */ jsxs("span", {
|
|
34
109
|
className: "flex min-w-0 flex-col gap-1",
|
|
35
|
-
children: [/* @__PURE__ */ jsx("span", { children:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
format
|
|
39
|
-
}) }), displayMonthlyPayment ? /* @__PURE__ */ jsx("span", {
|
|
40
|
-
className: cn("hidden text-gdt-subtext font-medium leading-none opacity-90", {
|
|
41
|
-
"inline-block": size === "md",
|
|
42
|
-
"inline-block text-gdt-xs": size === "lg"
|
|
43
|
-
}),
|
|
44
|
-
children: displayMonthlyPayment
|
|
110
|
+
children: [/* @__PURE__ */ jsx("span", { children: resolvedValue }), resolvedDescription ? /* @__PURE__ */ jsx("span", {
|
|
111
|
+
className: cn("font-medium leading-none opacity-90", descriptionSizeClassName[size]),
|
|
112
|
+
children: resolvedDescription
|
|
45
113
|
}) : null]
|
|
46
114
|
});
|
|
115
|
+
const pillClassName = cn(priceTagVariants({
|
|
116
|
+
size,
|
|
117
|
+
hasDescription: Boolean(resolvedDescription)
|
|
118
|
+
}), className);
|
|
47
119
|
if (!hasDropdown) return /* @__PURE__ */ jsx("span", {
|
|
48
120
|
"data-slot": "price-tag",
|
|
49
|
-
className:
|
|
50
|
-
|
|
51
|
-
hasMonthlyPayment: Boolean(displayMonthlyPayment)
|
|
52
|
-
}), className),
|
|
121
|
+
className: pillClassName,
|
|
122
|
+
...props,
|
|
53
123
|
children: pill
|
|
54
124
|
});
|
|
125
|
+
const options = (currencies ?? []).map((currency) => resolveMenuOption(currency, format, selectedCurrency));
|
|
55
126
|
return /* @__PURE__ */ jsxs(Popover.Root, {
|
|
56
127
|
open,
|
|
57
128
|
defaultOpen,
|
|
@@ -60,10 +131,7 @@ function PriceTag({ amount, shortAmount, format = "full", size = "md", monthlyPa
|
|
|
60
131
|
"data-slot": "price-tag",
|
|
61
132
|
"data-interactive": "true",
|
|
62
133
|
type,
|
|
63
|
-
className:
|
|
64
|
-
size,
|
|
65
|
-
hasMonthlyPayment: Boolean(displayMonthlyPayment)
|
|
66
|
-
}), className),
|
|
134
|
+
className: pillClassName,
|
|
67
135
|
...props,
|
|
68
136
|
children: [pill, /* @__PURE__ */ jsx(ChevronDown, {
|
|
69
137
|
"aria-hidden": "true",
|
|
@@ -75,41 +143,14 @@ function PriceTag({ amount, shortAmount, format = "full", size = "md", monthlyPa
|
|
|
75
143
|
children: /* @__PURE__ */ jsx(Popover.Popup, {
|
|
76
144
|
"data-slot": "price-tag-popover",
|
|
77
145
|
className: cn("z-50 flex w-[17.25rem] max-w-[calc(100vw-2rem)] origin-(--transform-origin) flex-col rounded-xl border border-line-subtle bg-surface-overlay p-2 text-fg-primary shadow-3 outline-hidden transition-[opacity,scale,translate] duration-[var(--duration-motion-popup)] ease-gdt-out data-starting-style:opacity-0 data-starting-style:scale-95 data-ending-style:opacity-0 data-ending-style:scale-95 data-[side=bottom]:data-starting-style:translate-y-2 data-[side=bottom]:data-ending-style:translate-y-2 data-[side=top]:data-starting-style:-translate-y-2 data-[side=top]:data-ending-style:-translate-y-2 motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100 motion-reduce:data-[side=bottom]:data-starting-style:translate-y-0 motion-reduce:data-[side=bottom]:data-ending-style:translate-y-0 motion-reduce:data-[side=top]:data-starting-style:translate-y-0 motion-reduce:data-[side=top]:data-ending-style:translate-y-0", popoverClassName),
|
|
78
|
-
children:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
87
|
-
type: "button",
|
|
88
|
-
"data-active": active ? "true" : void 0,
|
|
89
|
-
className: "flex min-h-14 w-full items-center gap-3 rounded-lg px-3 text-start outline-none transition-colors hover:bg-surface-brand-subtle focus-visible:bg-surface-brand-subtle focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] data-[active=true]:bg-surface-brand-subtle",
|
|
90
|
-
onClick: () => onCurrencyChange?.(currency),
|
|
91
|
-
children: [
|
|
92
|
-
/* @__PURE__ */ jsx("span", {
|
|
93
|
-
className: "min-w-10 text-gdt-sm font-semibold text-fg-brand",
|
|
94
|
-
children: currency.code
|
|
95
|
-
}),
|
|
96
|
-
/* @__PURE__ */ jsxs("span", {
|
|
97
|
-
className: "ml-auto flex min-w-0 flex-col items-end",
|
|
98
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
99
|
-
className: "text-gdt-sm font-bold text-fg-primary",
|
|
100
|
-
children: optionAmount
|
|
101
|
-
}), optionMonthlyPayment ? /* @__PURE__ */ jsx("span", {
|
|
102
|
-
className: "text-gdt-xs text-fg-secondary",
|
|
103
|
-
children: optionMonthlyPayment
|
|
104
|
-
}) : null]
|
|
105
|
-
}),
|
|
106
|
-
active ? /* @__PURE__ */ jsx(Check, {
|
|
107
|
-
"aria-hidden": "true",
|
|
108
|
-
className: "size-4 shrink-0 text-fg-brand"
|
|
109
|
-
}) : null
|
|
110
|
-
]
|
|
111
|
-
}, currency.code);
|
|
112
|
-
})
|
|
146
|
+
children: options.map((option) => /* @__PURE__ */ jsx("button", {
|
|
147
|
+
type: "button",
|
|
148
|
+
"data-slot": "price-tag-option",
|
|
149
|
+
"data-active": option.selected ? "true" : void 0,
|
|
150
|
+
className: "flex min-h-14 w-full items-center gap-3 rounded-lg px-3 text-start outline-none transition-colors hover:bg-surface-brand-subtle focus-visible:bg-surface-brand-subtle focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] data-[active=true]:bg-surface-brand-subtle",
|
|
151
|
+
onClick: () => onCurrencyChange?.(option),
|
|
152
|
+
children: renderMenuOption ? renderMenuOption(option) : /* @__PURE__ */ jsx(DefaultMenuOption, { option })
|
|
153
|
+
}, option.currency))
|
|
113
154
|
})
|
|
114
155
|
}) })]
|
|
115
156
|
});
|
package/dist/scroll-spy.d.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
|
|
|
2
2
|
import { TabsListVariant } from "./tabs.js";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
//#region src/scroll-spy.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Read and drive the spy from anywhere inside a `ScrollSpy` — an overflow
|
|
7
|
+
* "More" menu, a floating progress rail, a heading that echoes the section
|
|
8
|
+
* being read.
|
|
9
|
+
*
|
|
10
|
+
* Context reaches through portals, so a consumer inside an open `DropdownMenu`
|
|
11
|
+
* or `Popover` works without threading anything down by hand. `registerSection`
|
|
12
|
+
* is deliberately not exposed: sections are `ScrollSpyContent`'s to own.
|
|
13
|
+
*/
|
|
14
|
+
declare function useScrollSpy(): {
|
|
15
|
+
activeValue: string | undefined;
|
|
16
|
+
getSectionId: (value: string) => string;
|
|
17
|
+
getTriggerId: (value: string) => string;
|
|
18
|
+
scrollToSection: (value: string) => void;
|
|
19
|
+
};
|
|
5
20
|
type ScrollSpyProps = Omit<React.ComponentProps<"div">, "onChange"> & {
|
|
6
21
|
orientation?: "horizontal" | "vertical";
|
|
7
22
|
/** Controlled active section. */
|
|
@@ -58,4 +73,4 @@ type ScrollSpyContentProps = Omit<React.ComponentProps<"section">, "value"> & {
|
|
|
58
73
|
};
|
|
59
74
|
declare function ScrollSpyContent({ className, value, style, tabIndex, ...props }: ScrollSpyContentProps): React.JSX.Element;
|
|
60
75
|
//#endregion
|
|
61
|
-
export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName };
|
|
76
|
+
export { ScrollSpy, ScrollSpyContent, type ScrollSpyContentProps, ScrollSpyList, type ScrollSpyListProps, type ScrollSpyProps, ScrollSpyTrigger, type ScrollSpyTriggerProps, scrollSpyTriggerClassName, useScrollSpy };
|
package/dist/scroll-spy.js
CHANGED
|
@@ -37,10 +37,33 @@ function getScrollport(element) {
|
|
|
37
37
|
}
|
|
38
38
|
function useScrollSpyContext(part) {
|
|
39
39
|
const context = React.useContext(ScrollSpyContext);
|
|
40
|
-
if (!context) throw new Error(`\`${part}\` must be
|
|
40
|
+
if (!context) throw new Error(`\`${part}\` must be used inside \`ScrollSpy\`.`);
|
|
41
41
|
return context;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
+
* Read and drive the spy from anywhere inside a `ScrollSpy` — an overflow
|
|
45
|
+
* "More" menu, a floating progress rail, a heading that echoes the section
|
|
46
|
+
* being read.
|
|
47
|
+
*
|
|
48
|
+
* Context reaches through portals, so a consumer inside an open `DropdownMenu`
|
|
49
|
+
* or `Popover` works without threading anything down by hand. `registerSection`
|
|
50
|
+
* is deliberately not exposed: sections are `ScrollSpyContent`'s to own.
|
|
51
|
+
*/
|
|
52
|
+
function useScrollSpy() {
|
|
53
|
+
const { activeValue, getSectionId, getTriggerId, scrollToSection } = useScrollSpyContext("useScrollSpy");
|
|
54
|
+
return React.useMemo(() => ({
|
|
55
|
+
activeValue,
|
|
56
|
+
getSectionId,
|
|
57
|
+
getTriggerId,
|
|
58
|
+
scrollToSection
|
|
59
|
+
}), [
|
|
60
|
+
activeValue,
|
|
61
|
+
getSectionId,
|
|
62
|
+
getTriggerId,
|
|
63
|
+
scrollToSection
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
44
67
|
* `Tabs` for a page that shows everything at once: every section stays mounted
|
|
45
68
|
* and visible, the triggers scroll to their section, and the active trigger
|
|
46
69
|
* tracks whichever section the reader is currently on.
|
|
@@ -292,4 +315,4 @@ function ScrollSpyContent({ className, value, style, tabIndex = -1, ...props })
|
|
|
292
315
|
});
|
|
293
316
|
}
|
|
294
317
|
//#endregion
|
|
295
|
-
export { ScrollSpy, ScrollSpyContent, ScrollSpyList, ScrollSpyTrigger, scrollSpyTriggerClassName };
|
|
318
|
+
export { ScrollSpy, ScrollSpyContent, ScrollSpyList, ScrollSpyTrigger, scrollSpyTriggerClassName, useScrollSpy };
|
package/dist/sonar.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { t as ComponentSize } from "./size-context-BX6kAdVj.js";
|
|
2
|
+
import { VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
//#region src/sonar.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Kept in step with `--animate-sonar-wave` in `css/theme.css`; the waves are
|
|
7
|
+
* staggered by a fraction of one cycle, which only reads as a continuous sonar
|
|
8
|
+
* if this matches the keyframe's duration. `sonar.test.tsx` asserts they agree.
|
|
9
|
+
*/
|
|
10
|
+
declare const SONAR_DURATION_MS = 1500;
|
|
11
|
+
/**
|
|
12
|
+
* Colour rides on `currentColor` so the keyframe stays tone-agnostic: one set
|
|
13
|
+
* of keyframes, one class per tone.
|
|
14
|
+
*/
|
|
15
|
+
declare const sonarWaveVariants: (props?: ({
|
|
16
|
+
tone?: "brand" | "danger" | "info" | "success" | "warning" | null | undefined;
|
|
17
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
18
|
+
type SonarProps = React.ComponentProps<"span"> & VariantProps<typeof sonarWaveVariants> & {
|
|
19
|
+
/** Stop pulsing without unmounting — the child renders on its own. */
|
|
20
|
+
active?: boolean;
|
|
21
|
+
/** Overlapping rings. More reads as a faster, more urgent sonar. */
|
|
22
|
+
waves?: 1 | 2 | 3;
|
|
23
|
+
/** How far each ring travels. Inherits from `SizeProvider`. */
|
|
24
|
+
size?: ComponentSize;
|
|
25
|
+
/**
|
|
26
|
+
* Skip measurement and use this `border-radius` verbatim. Only needed
|
|
27
|
+
* when the child's shape can't be read off the DOM — an SVG using
|
|
28
|
+
* geometry rather than CSS, say.
|
|
29
|
+
*/
|
|
30
|
+
radius?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Draws attention to a dot, badge or button by pulsing rings outward from it.
|
|
34
|
+
*
|
|
35
|
+
* The rings trace the child's own shape: `Sonar` reads the child's computed
|
|
36
|
+
* `border-radius` and hands it to the rings, so a pill badge pulses a pill and
|
|
37
|
+
* a rounded button pulses a rounded rectangle, with nothing to keep in sync by
|
|
38
|
+
* hand. That matters because radii in this library are often size-dependent —
|
|
39
|
+
* `Tag` alone moves through `rounded-md`/`lg`/`xl` across its three sizes.
|
|
40
|
+
*
|
|
41
|
+
* Purely decorative: the rings are `aria-hidden` and the child is untouched, so
|
|
42
|
+
* whatever the child announces is what assistive tech hears. A pulse is not a
|
|
43
|
+
* label — if the attention it draws carries meaning, put that meaning in the
|
|
44
|
+
* child.
|
|
45
|
+
*
|
|
46
|
+
* The rings are painted with `box-shadow` spread, which lives outside the
|
|
47
|
+
* element's box, so an ancestor with `overflow: hidden` will clip them.
|
|
48
|
+
*/
|
|
49
|
+
declare function Sonar({ className, children, active, waves, size, tone, radius, style, ...props }: SonarProps): React.JSX.Element;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { SONAR_DURATION_MS, Sonar, type SonarProps, sonarWaveVariants };
|
package/dist/sonar.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as cn } from "./cn-BI_4DMBf.js";
|
|
3
|
+
import { useComponentSize } from "./size-context.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
//#region src/sonar.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Kept in step with `--animate-sonar-wave` in `css/theme.css`; the waves are
|
|
10
|
+
* staggered by a fraction of one cycle, which only reads as a continuous sonar
|
|
11
|
+
* if this matches the keyframe's duration. `sonar.test.tsx` asserts they agree.
|
|
12
|
+
*/
|
|
13
|
+
const SONAR_DURATION_MS = 1500;
|
|
14
|
+
const SQUARE = {
|
|
15
|
+
borderTopLeftRadius: "0px",
|
|
16
|
+
borderTopRightRadius: "0px",
|
|
17
|
+
borderBottomRightRadius: "0px",
|
|
18
|
+
borderBottomLeftRadius: "0px"
|
|
19
|
+
};
|
|
20
|
+
function sameCorners(a, b) {
|
|
21
|
+
return a !== null && a.borderTopLeftRadius === b.borderTopLeftRadius && a.borderTopRightRadius === b.borderTopRightRadius && a.borderBottomRightRadius === b.borderBottomRightRadius && a.borderBottomLeftRadius === b.borderBottomLeftRadius;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Colour rides on `currentColor` so the keyframe stays tone-agnostic: one set
|
|
25
|
+
* of keyframes, one class per tone.
|
|
26
|
+
*/
|
|
27
|
+
const sonarWaveVariants = cva(cn("pointer-events-none absolute inset-0 animate-sonar-wave", "motion-reduce:animate-none motion-reduce:opacity-40 motion-reduce:shadow-[0_0_0_2px_currentColor]"), {
|
|
28
|
+
variants: { tone: {
|
|
29
|
+
brand: "text-fg-brand",
|
|
30
|
+
info: "text-status-info",
|
|
31
|
+
success: "text-status-success",
|
|
32
|
+
warning: "text-status-warning",
|
|
33
|
+
danger: "text-status-danger"
|
|
34
|
+
} },
|
|
35
|
+
defaultVariants: { tone: "brand" }
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Rings evenly spaced across one cycle, so the last finishes just as the first
|
|
39
|
+
* comes round again. Doubles as each ring's key — the offsets are distinct by
|
|
40
|
+
* construction, which an index would only pretend to be.
|
|
41
|
+
*/
|
|
42
|
+
function waveDelays(waves) {
|
|
43
|
+
return Array.from({ length: waves }, (_, index) => Math.round(index * SONAR_DURATION_MS / waves));
|
|
44
|
+
}
|
|
45
|
+
/** How far the ring travels before it fades out. */
|
|
46
|
+
const sonarSpread = {
|
|
47
|
+
sm: "[--sonar-spread:0.375rem]",
|
|
48
|
+
md: "[--sonar-spread:0.625rem]",
|
|
49
|
+
lg: "[--sonar-spread:0.875rem]"
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Draws attention to a dot, badge or button by pulsing rings outward from it.
|
|
53
|
+
*
|
|
54
|
+
* The rings trace the child's own shape: `Sonar` reads the child's computed
|
|
55
|
+
* `border-radius` and hands it to the rings, so a pill badge pulses a pill and
|
|
56
|
+
* a rounded button pulses a rounded rectangle, with nothing to keep in sync by
|
|
57
|
+
* hand. That matters because radii in this library are often size-dependent —
|
|
58
|
+
* `Tag` alone moves through `rounded-md`/`lg`/`xl` across its three sizes.
|
|
59
|
+
*
|
|
60
|
+
* Purely decorative: the rings are `aria-hidden` and the child is untouched, so
|
|
61
|
+
* whatever the child announces is what assistive tech hears. A pulse is not a
|
|
62
|
+
* label — if the attention it draws carries meaning, put that meaning in the
|
|
63
|
+
* child.
|
|
64
|
+
*
|
|
65
|
+
* The rings are painted with `box-shadow` spread, which lives outside the
|
|
66
|
+
* element's box, so an ancestor with `overflow: hidden` will clip them.
|
|
67
|
+
*/
|
|
68
|
+
function Sonar({ className, children, active = true, waves = 2, size, tone, radius, style, ...props }) {
|
|
69
|
+
const resolvedSize = useComponentSize(size);
|
|
70
|
+
const ref = React.useRef(null);
|
|
71
|
+
const [corners, setCorners] = React.useState(null);
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (radius !== void 0 || !active) return;
|
|
74
|
+
const target = ref.current?.querySelector(":scope > :not([data-slot=\"sonar-wave\"])");
|
|
75
|
+
if (!target) {
|
|
76
|
+
setCorners(SQUARE);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const measure = () => {
|
|
80
|
+
const computed = getComputedStyle(target);
|
|
81
|
+
const next = {
|
|
82
|
+
borderTopLeftRadius: computed.borderTopLeftRadius,
|
|
83
|
+
borderTopRightRadius: computed.borderTopRightRadius,
|
|
84
|
+
borderBottomRightRadius: computed.borderBottomRightRadius,
|
|
85
|
+
borderBottomLeftRadius: computed.borderBottomLeftRadius
|
|
86
|
+
};
|
|
87
|
+
setCorners((current) => sameCorners(current, next) ? current : next);
|
|
88
|
+
};
|
|
89
|
+
measure();
|
|
90
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
91
|
+
const observer = new ResizeObserver(measure);
|
|
92
|
+
observer.observe(target);
|
|
93
|
+
return () => observer.disconnect();
|
|
94
|
+
}, [active, radius]);
|
|
95
|
+
const shape = radius !== void 0 ? { borderRadius: radius } : corners;
|
|
96
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
97
|
+
ref,
|
|
98
|
+
"data-slot": "sonar",
|
|
99
|
+
"data-tone": tone ?? "brand",
|
|
100
|
+
"data-size": resolvedSize,
|
|
101
|
+
"data-active": active ? "" : void 0,
|
|
102
|
+
className: cn("relative inline-flex w-fit shrink-0", sonarSpread[resolvedSize], className),
|
|
103
|
+
style,
|
|
104
|
+
...props,
|
|
105
|
+
children: [children, active && shape ? waveDelays(waves).map((delay) => /* @__PURE__ */ jsx("span", {
|
|
106
|
+
"aria-hidden": "true",
|
|
107
|
+
"data-slot": "sonar-wave",
|
|
108
|
+
style: {
|
|
109
|
+
...shape,
|
|
110
|
+
animationDelay: delay === 0 ? void 0 : `${delay}ms`
|
|
111
|
+
},
|
|
112
|
+
className: cn(sonarWaveVariants({ tone }), delay > 0 && "motion-reduce:hidden")
|
|
113
|
+
}, `sonar-wave-${delay}`)) : null]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { SONAR_DURATION_MS, Sonar, sonarWaveVariants };
|