@ultimat3/ui 22.14.0 → 23.0.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/CATALOG.md +43 -12
- package/CLAUDE.md +13 -49
- package/README.md +59 -4
- package/package.json +6 -5
- package/src/catalog/parse-component.ts +85 -12
- package/src/catalog/render-catalog.ts +21 -1
- package/src/components/Avatar.module.scss +1 -1
- package/src/components/BarChart.module.scss +12 -1
- package/src/components/BarChart.tsx +41 -15
- package/src/components/Button.tsx +3 -8
- package/src/components/Card.module.scss +1 -1
- package/src/components/Checkbox.module.scss +4 -4
- package/src/components/Checkbox.tsx +6 -0
- package/src/components/CommandPalette.module.scss +1 -1
- package/src/components/DataTable.module.scss +2 -0
- package/src/components/DataTable.tsx +90 -29
- package/src/components/Dialog.module.scss +2 -2
- package/src/components/Dropzone.module.scss +3 -3
- package/src/components/FileInput.module.scss +1 -1
- package/src/components/Form.module.scss +2 -2
- package/src/components/Input.module.scss +1 -1
- package/src/components/Link.tsx +64 -12
- package/src/components/Pagination.tsx +89 -37
- package/src/components/Radio.module.scss +2 -2
- package/src/components/Select.module.scss +3 -3
- package/src/components/Skeleton.module.scss +1 -1
- package/src/components/Spinner.module.scss +3 -3
- package/src/components/Switch.module.scss +2 -2
- package/src/components/Table.module.scss +2 -2
- package/src/components/Tabs.module.scss +5 -5
- package/src/components/ThemeToggle.module.scss +1 -1
- package/src/components/Toast.module.scss +2 -2
- package/src/components/Tooltip.module.scss +1 -1
- package/src/components/bar-chart-view.ts +29 -3
- package/src/components/button-classes.ts +25 -0
- package/src/components/link-classes.ts +38 -0
- package/src/components/link-target.ts +13 -0
- package/src/index.ts +13 -3
- package/src/sass-probe.ts +55 -0
- package/src/tokens/_index.scss +3 -1
- package/src/tokens/_mixins.scss +56 -8
- package/src/tokens/_motion.scss +4 -0
- package/src/tokens/_stroke.scss +20 -0
- package/src/tokens/_units.scss +47 -0
- package/src/tokens/reset.scss +4 -4
- package/src/tokens/theme.scss +2 -0
- package/src/tokens/tokens.ts +6 -0
|
@@ -1,32 +1,63 @@
|
|
|
1
1
|
// Cursor-first pagination — the only shape a Postgres-backed list should use.
|
|
2
2
|
// Numbered mode exists for prerendered archives where the total is known.
|
|
3
|
+
// One mode per use: callbacks (`onCursor` / `onPage`) for an island, or `hrefFor` for a
|
|
4
|
+
// server-rendered list — anchors the browser follows, with no script on the page.
|
|
3
5
|
|
|
4
6
|
import type { JSX } from 'solid-js';
|
|
5
7
|
import { cx } from '../cx';
|
|
6
8
|
import { UI_KEYS } from '../i18n-keys';
|
|
7
9
|
import { useUi } from '../theme/context';
|
|
8
10
|
import { Button } from './Button';
|
|
11
|
+
import { Link } from './Link';
|
|
9
12
|
import styles from './Pagination.module.scss';
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
type PageDirection = 'next' | 'prev';
|
|
15
|
+
|
|
16
|
+
interface PaginationBaseProps {
|
|
12
17
|
/** Opaque cursor for the following page; absent disables "next". */
|
|
13
18
|
nextCursor?: string | undefined;
|
|
14
19
|
prevCursor?: string | undefined;
|
|
20
|
+
/** Already-translated; falls back to the ui.* catalog keys. */
|
|
21
|
+
labelPrevious?: string | undefined;
|
|
22
|
+
labelNext?: string | undefined;
|
|
23
|
+
class?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Callback mode: the page is an island and pages itself. */
|
|
27
|
+
export interface PaginationCallbackProps extends PaginationBaseProps {
|
|
28
|
+
/** Callback mode. Never beside `hrefFor` — the type refuses the pair. */
|
|
15
29
|
onCursor?: ((cursor: string, direction: 'next' | 'prev') => void) | undefined;
|
|
16
30
|
/** Numbered mode: 1-based page and total. Ignored when cursors are present. */
|
|
17
31
|
page?: number | undefined;
|
|
18
32
|
totalPages?: number | undefined;
|
|
19
33
|
onPage?: ((page: number) => void) | undefined;
|
|
20
|
-
|
|
21
|
-
labelPrevious?: string | undefined;
|
|
22
|
-
labelNext?: string | undefined;
|
|
23
|
-
class?: string | undefined;
|
|
34
|
+
hrefFor?: undefined;
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/** Link mode: each cursor is a URL, so the pager works with scripting off. */
|
|
38
|
+
export interface PaginationLinkProps extends PaginationBaseProps {
|
|
39
|
+
/**
|
|
40
|
+
* Link mode: the URL of the page a cursor names. Renders `<a rel="next|prev">` and attaches no
|
|
41
|
+
* handler, so it needs no island. Cursor paging only — never beside `onCursor`, `onPage`,
|
|
42
|
+
* `page` or `totalPages`.
|
|
43
|
+
*/
|
|
44
|
+
hrefFor: (cursor: string, direction: 'next' | 'prev') => string;
|
|
45
|
+
onCursor?: undefined;
|
|
46
|
+
page?: undefined;
|
|
47
|
+
totalPages?: undefined;
|
|
48
|
+
onPage?: undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type PaginationProps = PaginationCallbackProps | PaginationLinkProps;
|
|
52
|
+
|
|
26
53
|
export function Pagination(props: PaginationProps): JSX.Element {
|
|
27
54
|
const ui = useUi();
|
|
28
|
-
const
|
|
29
|
-
|
|
55
|
+
const label = (direction: PageDirection): string =>
|
|
56
|
+
direction === 'prev'
|
|
57
|
+
? (props.labelPrevious ?? ui.t(UI_KEYS.previous))
|
|
58
|
+
: (props.labelNext ?? ui.t(UI_KEYS.next));
|
|
59
|
+
const cursor = (direction: PageDirection): string | undefined =>
|
|
60
|
+
direction === 'prev' ? props.prevCursor : props.nextCursor;
|
|
30
61
|
// A cursor present means cursor mode, exactly as `page`'s own doc says. The inverted form —
|
|
31
62
|
// "cursor mode when a number is MISSING" — silently dropped the cursor and paged by number the
|
|
32
63
|
// moment a caller passed both, which is the shape a list that knows its total naturally has.
|
|
@@ -36,23 +67,62 @@ export function Pagination(props: PaginationProps): JSX.Element {
|
|
|
36
67
|
props.page === undefined ||
|
|
37
68
|
props.totalPages === undefined;
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
const exhausted = (direction: PageDirection): boolean => {
|
|
71
|
+
if (cursorMode()) return cursor(direction) === undefined;
|
|
72
|
+
return direction === 'prev'
|
|
73
|
+
? (props.page ?? 1) <= 1
|
|
74
|
+
: (props.page ?? 1) >= (props.totalPages ?? 1);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const go = (direction: PageDirection): void => {
|
|
78
|
+
const to = cursor(direction);
|
|
79
|
+
if (cursorMode()) {
|
|
80
|
+
if (to !== undefined) props.onCursor?.(to, direction);
|
|
81
|
+
} else if (direction === 'prev') {
|
|
82
|
+
props.onPage?.(Math.max(1, (props.page ?? 1) - 1));
|
|
83
|
+
} else {
|
|
84
|
+
props.onPage?.(Math.min(props.totalPages ?? 1, (props.page ?? 1) + 1));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One side of the pager. In link mode a side WITH a cursor is an anchor and a side without one
|
|
90
|
+
* is the same disabled button callback mode shows: an anchor with nowhere to go is a dead
|
|
91
|
+
* control that still takes a tab stop, and dropping the side would move the other one between
|
|
92
|
+
* the first page and the second.
|
|
93
|
+
*/
|
|
94
|
+
const side = (direction: PageDirection): JSX.Element => {
|
|
95
|
+
const to = cursor(direction);
|
|
96
|
+
if (props.hrefFor !== undefined && to !== undefined) {
|
|
97
|
+
return (
|
|
98
|
+
<Link
|
|
99
|
+
appearance="button"
|
|
100
|
+
variant="secondary"
|
|
101
|
+
size="sm"
|
|
102
|
+
tone="neutral"
|
|
103
|
+
rel={direction}
|
|
104
|
+
href={props.hrefFor(to, direction)}
|
|
105
|
+
>
|
|
106
|
+
{label(direction)}
|
|
107
|
+
</Link>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
41
111
|
<Button
|
|
42
112
|
variant="secondary"
|
|
43
113
|
size="sm"
|
|
44
114
|
tone="neutral"
|
|
45
|
-
disabled={
|
|
46
|
-
onClick={() =>
|
|
47
|
-
if (cursorMode()) {
|
|
48
|
-
if (props.prevCursor !== undefined) props.onCursor?.(props.prevCursor, 'prev');
|
|
49
|
-
} else {
|
|
50
|
-
props.onPage?.(Math.max(1, (props.page ?? 1) - 1));
|
|
51
|
-
}
|
|
52
|
-
}}
|
|
115
|
+
disabled={exhausted(direction)}
|
|
116
|
+
onClick={props.hrefFor === undefined ? () => go(direction) : undefined}
|
|
53
117
|
>
|
|
54
|
-
{
|
|
118
|
+
{label(direction)}
|
|
55
119
|
</Button>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<nav class={cx(styles['pagination'], props.class)} aria-label={ui.t(UI_KEYS.page)}>
|
|
125
|
+
{side('prev')}
|
|
56
126
|
|
|
57
127
|
{cursorMode() ? null : (
|
|
58
128
|
<span class={styles['status']} aria-live="polite">
|
|
@@ -60,25 +130,7 @@ export function Pagination(props: PaginationProps): JSX.Element {
|
|
|
60
130
|
</span>
|
|
61
131
|
)}
|
|
62
132
|
|
|
63
|
-
|
|
64
|
-
variant="secondary"
|
|
65
|
-
size="sm"
|
|
66
|
-
tone="neutral"
|
|
67
|
-
disabled={
|
|
68
|
-
cursorMode()
|
|
69
|
-
? props.nextCursor === undefined
|
|
70
|
-
: (props.page ?? 1) >= (props.totalPages ?? 1)
|
|
71
|
-
}
|
|
72
|
-
onClick={() => {
|
|
73
|
-
if (cursorMode()) {
|
|
74
|
-
if (props.nextCursor !== undefined) props.onCursor?.(props.nextCursor, 'next');
|
|
75
|
-
} else {
|
|
76
|
-
props.onPage?.(Math.min(props.totalPages ?? 1, (props.page ?? 1) + 1));
|
|
77
|
-
}
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
{next()}
|
|
81
|
-
</Button>
|
|
133
|
+
{side('next')}
|
|
82
134
|
</nav>
|
|
83
135
|
);
|
|
84
136
|
}
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
inset-inline-end: t.space(3);
|
|
31
31
|
inline-size: 0.4rem;
|
|
32
32
|
block-size: 0.4rem;
|
|
33
|
-
border-inline-end:
|
|
34
|
-
border-block-end:
|
|
35
|
-
transform: translateY(-2px) rotate(45deg);
|
|
33
|
+
border-inline-end: t.stroke(thick) solid t.role('fg-muted');
|
|
34
|
+
border-block-end: t.stroke(thick) solid t.role('fg-muted');
|
|
35
|
+
transform: translateY(t.rem(-2px)) rotate(45deg);
|
|
36
36
|
pointer-events: none;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
.spinner {
|
|
10
10
|
display: inline-block;
|
|
11
|
-
border:
|
|
11
|
+
border: t.stroke(thick) solid t.role('line');
|
|
12
12
|
border-block-start-color: t.role('accent');
|
|
13
13
|
border-radius: t.radius(full);
|
|
14
|
-
animation: ultimate-spin
|
|
14
|
+
animation: ultimate-spin t.duration(slower) linear infinite;
|
|
15
15
|
// Reduced motion is handled globally in reset.scss; the ring still reads as
|
|
16
16
|
// a distinct shape when the animation is neutralised.
|
|
17
17
|
}
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
.size-lg {
|
|
30
30
|
inline-size: 2rem;
|
|
31
31
|
block-size: 2rem;
|
|
32
|
-
border-width:
|
|
32
|
+
border-width: t.stroke(heavy);
|
|
33
33
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
.tab[aria-selected='true'] {
|
|
22
|
-
box-shadow: inset -
|
|
22
|
+
box-shadow: inset calc(-1 * #{t.stroke(thick)}) 0 0 0 t.role('accent');
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.tab {
|
|
35
|
-
@include t.focus-ring(
|
|
35
|
+
@include t.focus-ring('accent', t.stroke(thick), calc(-1 * #{t.stroke(thick)}));
|
|
36
36
|
@include t.transition(color, box-shadow, background);
|
|
37
37
|
padding-block: t.space(2);
|
|
38
38
|
padding-inline: t.space(3);
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
&[aria-selected='true'] {
|
|
55
55
|
color: t.role('accent');
|
|
56
56
|
// Inset shadow instead of a border, so the tab does not shift on select.
|
|
57
|
-
box-shadow: inset 0 -
|
|
57
|
+
box-shadow: inset 0 calc(-1 * #{t.stroke(thick)}) 0 0 t.role('accent');
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
min-inline-size: 0;
|
|
63
63
|
|
|
64
64
|
&:focus-visible {
|
|
65
|
-
outline:
|
|
66
|
-
outline-offset:
|
|
65
|
+
outline: t.stroke(thick) solid t.role('accent');
|
|
66
|
+
outline-offset: t.stroke(thick);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
align-items: flex-start;
|
|
39
39
|
gap: t.space(3);
|
|
40
40
|
padding: t.space(4);
|
|
41
|
-
border-inline-start:
|
|
41
|
+
border-inline-start: t.stroke(heavy) solid var(--toast-accent);
|
|
42
42
|
color: t.role('fg');
|
|
43
43
|
pointer-events: auto;
|
|
44
44
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
@starting-style {
|
|
58
58
|
opacity: 0;
|
|
59
|
-
translate: 0
|
|
59
|
+
translate: 0 t.space(3);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -5,6 +5,11 @@ export interface ChartPoint {
|
|
|
5
5
|
/** The bar's name — a day, a bucket, a label. Read by `<title>` and the axis. */
|
|
6
6
|
readonly key: string;
|
|
7
7
|
readonly value: number;
|
|
8
|
+
/**
|
|
9
|
+
* A second series, stacked on `value` — failed on top of done. Absent or 0 draws nothing, so a
|
|
10
|
+
* one-series chart is exactly the chart it was.
|
|
11
|
+
*/
|
|
12
|
+
readonly secondary?: number;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
export interface BarRect {
|
|
@@ -24,11 +29,16 @@ export const BAR_CHART = { width: 600, top: 12, plot: 104, height: 128, gap: 3 }
|
|
|
24
29
|
/** Quarter lines. Four is what a reader can use to judge a bar's height without a y-axis. */
|
|
25
30
|
export const GRID_STEPS = [0.25, 0.5, 0.75, 1] as const;
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
const finite = (value: number | undefined): number =>
|
|
33
|
+
value !== undefined && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
34
|
+
|
|
35
|
+
/** The busiest point — its whole stack — and never below 1, so an all-zero series still divides. */
|
|
28
36
|
export function maxOf(points: readonly ChartPoint[]): number {
|
|
29
37
|
let max = 1;
|
|
30
|
-
for (const point of points)
|
|
31
|
-
|
|
38
|
+
for (const point of points) {
|
|
39
|
+
const total = finite(point.value) + finite(point.secondary);
|
|
40
|
+
if (total > max) max = total;
|
|
41
|
+
}
|
|
32
42
|
return max;
|
|
33
43
|
}
|
|
34
44
|
|
|
@@ -53,3 +63,19 @@ export function barRects(points: readonly ChartPoint[]): readonly BarRect[] {
|
|
|
53
63
|
};
|
|
54
64
|
});
|
|
55
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The second series' rect per point, stacked on that point's primary bar; `null` where it is 0.
|
|
69
|
+
* Same scale as `barRects`, so a stack is read against the same grid lines.
|
|
70
|
+
*/
|
|
71
|
+
export function secondaryRects(points: readonly ChartPoint[]): readonly (BarRect | null)[] {
|
|
72
|
+
const primary = barRects(points);
|
|
73
|
+
const max = maxOf(points);
|
|
74
|
+
return points.map((point, index) => {
|
|
75
|
+
const value = finite(point.secondary);
|
|
76
|
+
const below = primary[index];
|
|
77
|
+
if (value === 0 || below === undefined) return null;
|
|
78
|
+
const height = (value / max) * BAR_CHART.plot;
|
|
79
|
+
return { x: below.x, y: below.y - height, width: below.width, height };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The classes a button's look is made of — asked by `<Button>` and by `<Link appearance="button">`,
|
|
2
|
+
// so a control that submits and a control that navigates cannot drift into two looks. Keys into
|
|
3
|
+
// `Button.module.scss`, never class names: the scoped name is the stylesheet's to give.
|
|
4
|
+
|
|
5
|
+
import type { ButtonVariant, Size, Tone } from './variants';
|
|
6
|
+
|
|
7
|
+
export interface ButtonLook {
|
|
8
|
+
variant?: ButtonVariant | undefined;
|
|
9
|
+
tone?: Tone | undefined;
|
|
10
|
+
size?: Size | undefined;
|
|
11
|
+
fullWidth?: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The classes of the element's CONTENT, which both elements wrap their label in. */
|
|
15
|
+
export const BUTTON_CONTENT_KEYS = ['label', 'spinner'] as const;
|
|
16
|
+
|
|
17
|
+
export function buttonClassKeys(look: ButtonLook): readonly string[] {
|
|
18
|
+
return [
|
|
19
|
+
'button',
|
|
20
|
+
`variant-${look.variant ?? 'primary'}`,
|
|
21
|
+
`tone-${look.tone ?? 'accent'}`,
|
|
22
|
+
`size-${look.size ?? 'md'}`,
|
|
23
|
+
...(look.fullWidth === true ? ['full'] : []),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Which stylesheet a `<Link>` reads and which of its classes — the one decision `appearance`
|
|
2
|
+
// makes. Pure, so it is testable with no renderer: the `.tsx` only maps the answer onto a sheet.
|
|
3
|
+
|
|
4
|
+
import { type ButtonLook, buttonClassKeys } from './button-classes';
|
|
5
|
+
import type { Tone } from './variants';
|
|
6
|
+
|
|
7
|
+
export interface LinkLook extends Omit<ButtonLook, 'tone'> {
|
|
8
|
+
appearance?: 'link' | 'button' | undefined;
|
|
9
|
+
underline?: 'always' | 'hover' | 'none' | undefined;
|
|
10
|
+
/** A text link's own two tones, or — as a button — any `Tone`. */
|
|
11
|
+
tone?: Tone | 'inherit' | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LinkClasses {
|
|
15
|
+
/** `button` is `Button.module.scss`: one set of button styles, whichever element wears them. */
|
|
16
|
+
readonly sheet: 'link' | 'button';
|
|
17
|
+
readonly keys: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function linkClasses(look: LinkLook): LinkClasses {
|
|
21
|
+
if (look.appearance === 'button') {
|
|
22
|
+
return {
|
|
23
|
+
sheet: 'button',
|
|
24
|
+
// `inherit` is a text link's tone and no button has it; the type refuses the pairing, and a
|
|
25
|
+
// caller that got past the type gets the default tone rather than a class nothing declares.
|
|
26
|
+
keys: buttonClassKeys({
|
|
27
|
+
variant: look.variant,
|
|
28
|
+
tone: look.tone === 'inherit' ? undefined : look.tone,
|
|
29
|
+
size: look.size,
|
|
30
|
+
fullWidth: look.fullWidth,
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
sheet: 'link',
|
|
36
|
+
keys: ['link', `underline-${look.underline ?? 'hover'}`, `tone-${look.tone ?? 'accent'}`],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -21,3 +21,16 @@ export function linkTarget(href: string, declaredExternal?: boolean | undefined)
|
|
|
21
21
|
if (safe === null) return { href: undefined, external: false };
|
|
22
22
|
return { href: safe, external: declaredExternal === true || /^https?:\/\//.test(safe) };
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The `rel` an anchor carries. A relation is ADDED to the external hardening, never traded for
|
|
27
|
+
* it — which is why `<Link>` takes the relation as a closed pair and not a free `rel` string a
|
|
28
|
+
* caller could use to write the hardening away.
|
|
29
|
+
*/
|
|
30
|
+
export function linkRel(
|
|
31
|
+
external: boolean,
|
|
32
|
+
relation?: 'next' | 'prev' | undefined,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
if (!external) return relation;
|
|
35
|
+
return relation === undefined ? 'noopener noreferrer' : `${relation} noopener noreferrer`;
|
|
36
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -89,7 +89,12 @@ export {
|
|
|
89
89
|
settleActive,
|
|
90
90
|
stepActive,
|
|
91
91
|
} from './components/command-palette-view';
|
|
92
|
-
export type {
|
|
92
|
+
export type {
|
|
93
|
+
Column,
|
|
94
|
+
DataTableCallbackProps,
|
|
95
|
+
DataTableLinkProps,
|
|
96
|
+
DataTableProps,
|
|
97
|
+
} from './components/DataTable';
|
|
93
98
|
export { DataTable } from './components/DataTable';
|
|
94
99
|
export type { DateTimeProps } from './components/DateTime';
|
|
95
100
|
export { DateTime } from './components/DateTime';
|
|
@@ -160,7 +165,7 @@ export { boxFor, reservedRatio, sourceSetsFor, srcsetFor } from './components/im
|
|
|
160
165
|
export type { LoadMoreInput, LoadMoreState } from './components/infinite-scroll-view';
|
|
161
166
|
export type { KbdProps } from './components/Kbd';
|
|
162
167
|
export { Kbd } from './components/Kbd';
|
|
163
|
-
export type { LinkProps } from './components/Link';
|
|
168
|
+
export type { ButtonLinkProps, LinkProps, TextLinkProps } from './components/Link';
|
|
164
169
|
export { Link } from './components/Link';
|
|
165
170
|
export type { LocaleSwitcherProps } from './components/LocaleSwitcher';
|
|
166
171
|
export { LocaleSwitcher, localeLabel } from './components/LocaleSwitcher';
|
|
@@ -176,7 +181,11 @@ export type { MoneyFormatter, MoneyInput, MoneyViewOptions } from './components/
|
|
|
176
181
|
export { moneyText, toMoney } from './components/money-view';
|
|
177
182
|
export type { PageHeaderProps } from './components/PageHeader';
|
|
178
183
|
export { PageHeader } from './components/PageHeader';
|
|
179
|
-
export type {
|
|
184
|
+
export type {
|
|
185
|
+
PaginationCallbackProps,
|
|
186
|
+
PaginationLinkProps,
|
|
187
|
+
PaginationProps,
|
|
188
|
+
} from './components/Pagination';
|
|
180
189
|
export { Pagination } from './components/Pagination';
|
|
181
190
|
export type { Placement, PopoverProps } from './components/Popover';
|
|
182
191
|
export { Popover } from './components/Popover';
|
|
@@ -378,5 +387,6 @@ export {
|
|
|
378
387
|
radiusTokens,
|
|
379
388
|
shadowTokens,
|
|
380
389
|
spaceTokens,
|
|
390
|
+
strokeTokens,
|
|
381
391
|
zTokens,
|
|
382
392
|
} from './tokens/tokens';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// TEST-ONLY. Compiles a stylesheet with the compiler an app's build uses, so a test can read what
|
|
2
|
+
// Sass EMITS — a media query, a `clamp()`, the class list a mixin generates, an `@error` — instead
|
|
3
|
+
// of the source it was written as. Never exported from `index.ts`, like `jsx-probe.ts`.
|
|
4
|
+
|
|
5
|
+
/** The two members of Sass's API this file calls; the package itself is not a dependency of `ui`. */
|
|
6
|
+
interface SassApi {
|
|
7
|
+
compileString(source: string, options: { readonly url: URL }): { readonly css: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let loading: Promise<SassApi> | undefined;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `sass` is `@ultimat3/render`'s dependency — the package that compiles an app's sheets — and `ui`
|
|
14
|
+
* may not import `render` (tier 4, both). Resolved FROM render's directory, so a test compiles with
|
|
15
|
+
* the one compiler at the one version the lockfile pins and `ui` declares no second copy. Only a
|
|
16
|
+
* specifier is read from render; no module of it loads. Lazy, so importing this file has no effect.
|
|
17
|
+
*/
|
|
18
|
+
function sassCompiler(): Promise<SassApi> {
|
|
19
|
+
loading ??= import(
|
|
20
|
+
Bun.resolveSync('sass', new URL('../../render', import.meta.url).pathname)
|
|
21
|
+
) as Promise<SassApi>;
|
|
22
|
+
return loading;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Compile `source` as though it were the file at `path`, so its relative `@use`s resolve. */
|
|
26
|
+
export async function compileScss(source: string, path: string): Promise<string> {
|
|
27
|
+
const sass = await sassCompiler();
|
|
28
|
+
return sass.compileString(source, { url: new URL(`file://${path}`) }).css;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Compile a stylesheet that exists on disk. */
|
|
32
|
+
export async function compileScssFile(path: string): Promise<string> {
|
|
33
|
+
return compileScss(await Bun.file(path).text(), path);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The `@error` text Sass refused `source` with, or `undefined` when it compiled. A refusal that is
|
|
38
|
+
* not Sass's own — the compiler failing to load — is rethrown: it is not an answer about `source`.
|
|
39
|
+
*/
|
|
40
|
+
export async function scssRefusal(source: string, path: string): Promise<string | undefined> {
|
|
41
|
+
try {
|
|
42
|
+
await compileScss(source, path);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (typeof error === 'object' && error !== null && 'sassMessage' in error) {
|
|
45
|
+
return String(error.sassMessage);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every class a compiled stylesheet declares. */
|
|
53
|
+
export function declaredClasses(css: string): ReadonlySet<string> {
|
|
54
|
+
return new Set([...css.matchAll(/\.([A-Za-z][\w-]*)/g)].map((match) => match[1] as string));
|
|
55
|
+
}
|
package/src/tokens/_index.scss
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Single entry point for authors: `@use '../tokens' as t;` then `t.role('bg')`,
|
|
2
|
-
// `t.space(4)`, `@include t.focus-ring`. Forwards maps, functions, and mixins —
|
|
2
|
+
// `t.space(4)`, `t.rem(24px)`, `@include t.focus-ring`. Forwards maps, functions, and mixins —
|
|
3
3
|
// emits no CSS. Global custom properties come from `theme.scss`.
|
|
4
4
|
|
|
5
5
|
@forward 'breakpoints';
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
@forward 'radius' hide emit;
|
|
9
9
|
@forward 'shadow' hide emit, levels;
|
|
10
10
|
@forward 'space' hide emit;
|
|
11
|
+
@forward 'stroke' hide emit;
|
|
11
12
|
@forward 'typography' hide emit;
|
|
13
|
+
@forward 'units';
|
|
12
14
|
@forward 'z' hide emit;
|
|
13
15
|
@forward 'mixins';
|