@domicile-desktop/component-library 0.0.0-alpha-75b2a946029c → 0.0.0-alpha-d8c1903a165f
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/package.json +3 -1
- package/src/Accordion/Accordion.tsx +135 -0
- package/src/Drilldown/Drilldown.tsx +147 -0
- package/src/Popover/Popover.tsx +4 -2
- package/src/Slider/Slider.tsx +49 -1
package/package.json
CHANGED
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
"vite": "^8.2.2"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
|
+
"./Accordion": "./src/Accordion/Accordion.tsx",
|
|
29
30
|
"./Autocomplete": "./src/Autocomplete/Autocomplete.tsx",
|
|
30
31
|
"./Avatar": "./src/Avatar/Avatar.tsx",
|
|
31
32
|
"./Button": "./src/Button/Button.tsx",
|
|
32
33
|
"./Card": "./src/Card/Card.tsx",
|
|
33
34
|
"./control-sizes": "./src/control-sizes.ts",
|
|
34
35
|
"./DisplayProvider": "./src/Screen/DisplayProvider.tsx",
|
|
36
|
+
"./Drilldown": "./src/Drilldown/Drilldown.tsx",
|
|
35
37
|
"./display-source": "./src/Screen/display-source.ts",
|
|
36
38
|
"./Field": "./src/Field/Field.tsx",
|
|
37
39
|
"./Input": "./src/Input/Input.tsx",
|
|
@@ -86,5 +88,5 @@
|
|
|
86
88
|
"test:unit": "bun test"
|
|
87
89
|
},
|
|
88
90
|
"type": "module",
|
|
89
|
-
"version": "0.0.0-alpha-
|
|
91
|
+
"version": "0.0.0-alpha-d8c1903a165f"
|
|
90
92
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Accordion as BaseAccordion } from "@base-ui/react/accordion";
|
|
2
|
+
import { CaretDownIcon } from "@phosphor-icons/react/dist/ssr/CaretDown";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { css } from "../../styled-system/css";
|
|
6
|
+
import { flex, hstack } from "../../styled-system/patterns";
|
|
7
|
+
import type { ExtendProps } from "../extend-props";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One section: the `value` that names it, the `label` on its button, and the
|
|
11
|
+
* `content` it opens to.
|
|
12
|
+
*/
|
|
13
|
+
export type AccordionItem = {
|
|
14
|
+
value: string;
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
content: ReactNode;
|
|
17
|
+
disabled?: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Props = ExtendProps<
|
|
21
|
+
typeof BaseAccordion.Root<string>,
|
|
22
|
+
{
|
|
23
|
+
items: readonly AccordionItem[];
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sections stacked under their labels, each opening in place with the rest
|
|
29
|
+
* pushed down: more of a panel without leaving it.
|
|
30
|
+
*
|
|
31
|
+
* Wraps the @base-ui/react Accordion primitive, which owns the behavior — the
|
|
32
|
+
* buttons' `aria-expanded`, arrow keys between them, the panels' heights — so
|
|
33
|
+
* this layer is the look: a label and a caret that turns, a panel that grows
|
|
34
|
+
* to its content. One section open at a time unless `multiple`; closed panels
|
|
35
|
+
* unmount, so what is inside holds no state while it is shut.
|
|
36
|
+
*
|
|
37
|
+
* **Drawn in `currentcolor`**, as `Slider` is, so it reads in a card and over
|
|
38
|
+
* a photograph alike.
|
|
39
|
+
*/
|
|
40
|
+
export const Accordion = ({ items, ...rootProps }: Props) => (
|
|
41
|
+
<BaseAccordion.Root className={rootStyles} {...rootProps}>
|
|
42
|
+
{items.map((item) => (
|
|
43
|
+
<BaseAccordion.Item
|
|
44
|
+
className={itemStyles}
|
|
45
|
+
disabled={item.disabled ?? false}
|
|
46
|
+
key={item.value}
|
|
47
|
+
value={item.value}
|
|
48
|
+
>
|
|
49
|
+
<BaseAccordion.Header className={headerStyles}>
|
|
50
|
+
<BaseAccordion.Trigger className={triggerStyles}>
|
|
51
|
+
<span className={labelStyles}>{item.label}</span>
|
|
52
|
+
<CaretDownIcon className={caretStyles} size={12} />
|
|
53
|
+
</BaseAccordion.Trigger>
|
|
54
|
+
</BaseAccordion.Header>
|
|
55
|
+
<BaseAccordion.Panel className={panelStyles}>
|
|
56
|
+
<div className={contentStyles}>{item.content}</div>
|
|
57
|
+
</BaseAccordion.Panel>
|
|
58
|
+
</BaseAccordion.Item>
|
|
59
|
+
))}
|
|
60
|
+
</BaseAccordion.Root>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const rootStyles = flex({
|
|
64
|
+
direction: "column",
|
|
65
|
+
inlineSize: "100%",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// A hairline between sections, in the color of whatever holds them.
|
|
69
|
+
const itemStyles = css({
|
|
70
|
+
"& + &": {
|
|
71
|
+
borderBlockStart:
|
|
72
|
+
"1px solid color-mix(in oklab, currentcolor 15%, transparent)",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const headerStyles = css({
|
|
77
|
+
margin: 0,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const triggerStyles = hstack({
|
|
81
|
+
_disabled: {
|
|
82
|
+
cursor: "not-allowed",
|
|
83
|
+
opacity: "disabled",
|
|
84
|
+
},
|
|
85
|
+
_focusVisible: {
|
|
86
|
+
backgroundColor: "color-mix(in oklab, currentcolor 12%, transparent)",
|
|
87
|
+
outlineColor: "transparent",
|
|
88
|
+
},
|
|
89
|
+
_hoverEnabled: {
|
|
90
|
+
backgroundColor: "color-mix(in oklab, currentcolor 8%, transparent)",
|
|
91
|
+
},
|
|
92
|
+
backgroundColor: "transparent",
|
|
93
|
+
borderRadius: "sm",
|
|
94
|
+
borderStyle: "none",
|
|
95
|
+
color: "inherit",
|
|
96
|
+
cursor: "pointer",
|
|
97
|
+
font: "inherit",
|
|
98
|
+
gap: 2,
|
|
99
|
+
inlineSize: "100%",
|
|
100
|
+
justify: "space-between",
|
|
101
|
+
paddingBlock: 1.5,
|
|
102
|
+
paddingInline: 1,
|
|
103
|
+
textAlign: "start",
|
|
104
|
+
transition: "background-color {durations.fast} {easings.out}",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const labelStyles = css({
|
|
108
|
+
flexGrow: 1,
|
|
109
|
+
fontWeight: "medium",
|
|
110
|
+
minInlineSize: 0,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Turned over while its section is open: base-ui marks the trigger.
|
|
114
|
+
const caretStyles = css({
|
|
115
|
+
"[data-panel-open] > &": {
|
|
116
|
+
transform: "rotate(180deg)",
|
|
117
|
+
},
|
|
118
|
+
flexShrink: 0,
|
|
119
|
+
transition: "transform {durations.normal} {easings.out}",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Grows to its content and back, off the height base-ui measures.
|
|
123
|
+
const panelStyles = css({
|
|
124
|
+
"&[data-ending-style], &[data-starting-style]": {
|
|
125
|
+
blockSize: 0,
|
|
126
|
+
},
|
|
127
|
+
blockSize: "var(--accordion-panel-height)",
|
|
128
|
+
overflow: "hidden",
|
|
129
|
+
transition: "block-size {durations.normal} {easings.out}",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const contentStyles = css({
|
|
133
|
+
paddingBlockEnd: 2,
|
|
134
|
+
paddingInline: 1,
|
|
135
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { CaretLeftIcon } from "@phosphor-icons/react/dist/ssr/CaretLeft";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { css, cva } from "../../styled-system/css";
|
|
6
|
+
import { hstack } from "../../styled-system/patterns";
|
|
7
|
+
|
|
8
|
+
/** What slides in: a title over its content. */
|
|
9
|
+
export type DrilldownDetail = {
|
|
10
|
+
title: string;
|
|
11
|
+
content: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
/** The main view, shown while there is no detail. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
/** What slides in over the main view, or `undefined` for none. */
|
|
18
|
+
detail: DrilldownDetail | undefined;
|
|
19
|
+
/** The back button was pressed: drop the detail. */
|
|
20
|
+
onBack: () => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A view that something slides in over from the side and back out of: a list
|
|
25
|
+
* of choices for one row of the main view, with a back button above it — the
|
|
26
|
+
* way a phone's settings go one level deeper without leaving the screen.
|
|
27
|
+
*
|
|
28
|
+
* For a panel that has no room for a second panel, or no business opening
|
|
29
|
+
* one: a popover that a `Select`'s own popup would close, say.
|
|
30
|
+
*
|
|
31
|
+
* **The view that is not shown is not there**: `inert` and `aria-hidden`, so a
|
|
32
|
+
* keyboard and a screen reader skip it, and out of the flow, so the panel is as tall as what
|
|
33
|
+
* it shows. The detail that slid out is kept until the next one replaces it,
|
|
34
|
+
* so it can slide out rather than vanish.
|
|
35
|
+
*
|
|
36
|
+
* Drawn in `currentcolor`, as `Slider` is.
|
|
37
|
+
*/
|
|
38
|
+
export const Drilldown = ({ children, detail, onBack }: Props) => {
|
|
39
|
+
const [shown, setShown] = useState(detail);
|
|
40
|
+
if (detail !== undefined && detail !== shown) {
|
|
41
|
+
setShown(detail);
|
|
42
|
+
}
|
|
43
|
+
const open = detail !== undefined;
|
|
44
|
+
return (
|
|
45
|
+
<div className={rootStyles}>
|
|
46
|
+
<div
|
|
47
|
+
aria-hidden={open}
|
|
48
|
+
className={paneStyles({ place: open ? "before" : "here" })}
|
|
49
|
+
inert={open}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
<div
|
|
54
|
+
aria-hidden={!open}
|
|
55
|
+
className={paneStyles({ place: open ? "here" : "after" })}
|
|
56
|
+
inert={!open}
|
|
57
|
+
>
|
|
58
|
+
{shown !== undefined && (
|
|
59
|
+
<>
|
|
60
|
+
<div className={headerStyles}>
|
|
61
|
+
<button
|
|
62
|
+
aria-label="Back"
|
|
63
|
+
className={backStyles}
|
|
64
|
+
onClick={onBack}
|
|
65
|
+
type="button"
|
|
66
|
+
>
|
|
67
|
+
<CaretLeftIcon size={14} />
|
|
68
|
+
</button>
|
|
69
|
+
<h3 className={titleStyles}>{shown.title}</h3>
|
|
70
|
+
</div>
|
|
71
|
+
{shown.content}
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const rootStyles = css({
|
|
80
|
+
overflow: "hidden",
|
|
81
|
+
position: "relative",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// The pane shown is in the flow; the other is laid over it, slid off to its
|
|
85
|
+
// side and hidden once it has gone — `visibility` waits out the slide.
|
|
86
|
+
const paneStyles = cva({
|
|
87
|
+
base: {
|
|
88
|
+
transition:
|
|
89
|
+
"transform {durations.normal} {easings.out}, visibility {durations.normal}",
|
|
90
|
+
},
|
|
91
|
+
variants: {
|
|
92
|
+
place: {
|
|
93
|
+
after: {
|
|
94
|
+
insetBlockStart: 0,
|
|
95
|
+
insetInline: 0,
|
|
96
|
+
position: "absolute",
|
|
97
|
+
transform: "translateX(100%)",
|
|
98
|
+
visibility: "hidden",
|
|
99
|
+
},
|
|
100
|
+
before: {
|
|
101
|
+
insetBlockStart: 0,
|
|
102
|
+
insetInline: 0,
|
|
103
|
+
position: "absolute",
|
|
104
|
+
transform: "translateX(-100%)",
|
|
105
|
+
visibility: "hidden",
|
|
106
|
+
},
|
|
107
|
+
here: {
|
|
108
|
+
position: "relative",
|
|
109
|
+
transform: "translateX(0)",
|
|
110
|
+
visibility: "visible",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const headerStyles = hstack({
|
|
117
|
+
gap: 1,
|
|
118
|
+
paddingBlockEnd: 1.5,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const backStyles = css({
|
|
122
|
+
_focusVisible: {
|
|
123
|
+
backgroundColor: "color-mix(in oklab, currentcolor 16%, transparent)",
|
|
124
|
+
outlineColor: "transparent",
|
|
125
|
+
},
|
|
126
|
+
_hover: {
|
|
127
|
+
backgroundColor: "color-mix(in oklab, currentcolor 12%, transparent)",
|
|
128
|
+
},
|
|
129
|
+
alignItems: "center",
|
|
130
|
+
backgroundColor: "transparent",
|
|
131
|
+
blockSize: 6,
|
|
132
|
+
borderRadius: "full",
|
|
133
|
+
borderStyle: "none",
|
|
134
|
+
color: "inherit",
|
|
135
|
+
cursor: "pointer",
|
|
136
|
+
display: "inline-flex",
|
|
137
|
+
flexShrink: 0,
|
|
138
|
+
inlineSize: 6,
|
|
139
|
+
justifyContent: "center",
|
|
140
|
+
padding: 0,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const titleStyles = css({
|
|
144
|
+
fontSize: "sm",
|
|
145
|
+
fontWeight: "semibold",
|
|
146
|
+
margin: 0,
|
|
147
|
+
});
|
package/src/Popover/Popover.tsx
CHANGED
|
@@ -147,12 +147,14 @@ const popupStyles = flex({
|
|
|
147
147
|
},
|
|
148
148
|
// A frosted pill lettered white over any photo: the bar's language rather
|
|
149
149
|
// than a card's, and the same in both themes, since the wallpaper does not
|
|
150
|
-
// flip with them.
|
|
150
|
+
// flip with them. A pill while it is one row — the radius is half a row's
|
|
151
|
+
// height, as the bar's own chips are — and a rounded panel once its content
|
|
152
|
+
// is taller, rather than a stadium.
|
|
151
153
|
"&[data-tone=overPhoto]": {
|
|
152
154
|
backdropFilter: "blur({spacing.3})",
|
|
153
155
|
backgroundColor: "panelOverPhoto",
|
|
154
156
|
border: "none",
|
|
155
|
-
borderRadius: "
|
|
157
|
+
borderRadius: "2xl",
|
|
156
158
|
color: "onPhoto",
|
|
157
159
|
paddingBlock: 1.5,
|
|
158
160
|
paddingInline: 3,
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -8,6 +8,11 @@ type Props = Omit<
|
|
|
8
8
|
> & {
|
|
9
9
|
/** What the slider sets, for a screen reader: the thumb's name. */
|
|
10
10
|
label: string;
|
|
11
|
+
/**
|
|
12
|
+
* How loud, or how full, what the slider sets is right now, 0 through 1 —
|
|
13
|
+
* a meter drawn under the track, named for the slider. Absent for no meter.
|
|
14
|
+
*/
|
|
15
|
+
level?: number | undefined;
|
|
11
16
|
};
|
|
12
17
|
|
|
13
18
|
/**
|
|
@@ -21,17 +26,38 @@ type Props = Omit<
|
|
|
21
26
|
* asks for one. Every interaction — pointer, keyboard, the hidden range input a
|
|
22
27
|
* screen reader drives — is base-ui's.
|
|
23
28
|
*/
|
|
24
|
-
export const Slider = ({ label, ...rootProps }: Props) => (
|
|
29
|
+
export const Slider = ({ label, level, ...rootProps }: Props) => (
|
|
25
30
|
<BaseSlider.Root className={rootStyles} {...rootProps}>
|
|
26
31
|
<BaseSlider.Control className={controlStyles}>
|
|
27
32
|
<BaseSlider.Track className={trackStyles}>
|
|
28
33
|
<BaseSlider.Indicator className={indicatorStyles} />
|
|
29
34
|
<BaseSlider.Thumb aria-label={label} className={thumbStyles} />
|
|
30
35
|
</BaseSlider.Track>
|
|
36
|
+
{level !== undefined && <Meter label={label} level={level} />}
|
|
31
37
|
</BaseSlider.Control>
|
|
32
38
|
</BaseSlider.Root>
|
|
33
39
|
);
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The level, as a hairline under the track: thin enough not to read as a
|
|
43
|
+
* second slider, and in the slider's own color.
|
|
44
|
+
*/
|
|
45
|
+
const Meter = ({ label, level }: { label: string; level: number }) => {
|
|
46
|
+
const percent = Math.round(Math.min(1, Math.max(0, level)) * 100);
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
aria-label={`${label} level`}
|
|
50
|
+
aria-valuemax={100}
|
|
51
|
+
aria-valuemin={0}
|
|
52
|
+
aria-valuenow={percent}
|
|
53
|
+
className={meterStyles}
|
|
54
|
+
role="meter"
|
|
55
|
+
>
|
|
56
|
+
<span className={meterFillStyles} style={{ inlineSize: `${percent}%` }} />
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
35
61
|
// As wide as whatever holds it, and a flex item that takes what is left of a
|
|
36
62
|
// row rather than shrinking to its own empty content.
|
|
37
63
|
const rootStyles = css({
|
|
@@ -45,10 +71,32 @@ const controlStyles = css({
|
|
|
45
71
|
blockSize: 5,
|
|
46
72
|
cursor: "pointer",
|
|
47
73
|
display: "flex",
|
|
74
|
+
position: "relative",
|
|
48
75
|
touchAction: "none",
|
|
49
76
|
userSelect: "none",
|
|
50
77
|
});
|
|
51
78
|
|
|
79
|
+
// Pinned to the foot of the control, under the track and out of its way.
|
|
80
|
+
const meterStyles = css({
|
|
81
|
+
backgroundColor: "color-mix(in oklab, currentcolor 12%, transparent)",
|
|
82
|
+
blockSize: "2px",
|
|
83
|
+
borderRadius: "full",
|
|
84
|
+
insetBlockEnd: 0,
|
|
85
|
+
insetInline: 0,
|
|
86
|
+
overflow: "hidden",
|
|
87
|
+
pointerEvents: "none",
|
|
88
|
+
position: "absolute",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Quick to rise and quick to fall: the levels arrive twenty times a second,
|
|
92
|
+
// and a slower ease would draw a level that had already gone.
|
|
93
|
+
const meterFillStyles = css({
|
|
94
|
+
backgroundColor: "color-mix(in oklab, currentcolor 70%, transparent)",
|
|
95
|
+
blockSize: "100%",
|
|
96
|
+
display: "block",
|
|
97
|
+
transition: "inline-size {durations.fastest} {easings.linear}",
|
|
98
|
+
});
|
|
99
|
+
|
|
52
100
|
const trackStyles = css({
|
|
53
101
|
backgroundColor: "color-mix(in oklab, currentcolor 25%, transparent)",
|
|
54
102
|
blockSize: 1,
|