@domicile-desktop/component-library 0.0.0-alpha-f2862631a576
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/README.md +76 -0
- package/package.json +90 -0
- package/src/Autocomplete/Autocomplete.tsx +339 -0
- package/src/Avatar/Avatar.tsx +124 -0
- package/src/Avatar/gradient.ts +17 -0
- package/src/Avatar/initials.ts +19 -0
- package/src/Avatar/slowImageDecorator.tsx +92 -0
- package/src/Button/Button.tsx +285 -0
- package/src/Card/Card.tsx +55 -0
- package/src/Field/Field.tsx +136 -0
- package/src/Field/FieldErrorPopover.tsx +144 -0
- package/src/Input/Input.tsx +157 -0
- package/src/Kbd/Kbd.tsx +25 -0
- package/src/ModalDialog/ModalDialog.tsx +367 -0
- package/src/Popover/Popover.tsx +236 -0
- package/src/Provider/Provider.tsx +29 -0
- package/src/Screen/DisplayProvider.tsx +105 -0
- package/src/Screen/Screen.tsx +122 -0
- package/src/Screen/display-source.ts +64 -0
- package/src/Select/Select.tsx +576 -0
- package/src/SlideOver/SlideOver.tsx +176 -0
- package/src/Slider/Slider.tsx +91 -0
- package/src/TabRail/TabRail.tsx +355 -0
- package/src/Tabs/Tabs.tsx +163 -0
- package/src/Textarea/Textarea.tsx +270 -0
- package/src/ThemeSwitch/ThemeProvider.tsx +130 -0
- package/src/ThemeSwitch/ThemeSwitch.tsx +174 -0
- package/src/ThemeSwitch/standalone-theme-source.ts +42 -0
- package/src/ThemeSwitch/theme-core.ts +172 -0
- package/src/ThemeSwitch/theme-source.ts +65 -0
- package/src/Toaster/Toaster.tsx +250 -0
- package/src/__test__/AnyComponent.ts +11 -0
- package/src/__test__/Variants.tsx +89 -0
- package/src/__test__/adornmentVariations.tsx +202 -0
- package/src/__test__/iconControl.tsx +20 -0
- package/src/__test__/invalidDecorator.tsx +14 -0
- package/src/__test__/runControlSuite.tsx +212 -0
- package/src/__test__/wrapperOf.ts +14 -0
- package/src/_control/PrefixIconStack.tsx +153 -0
- package/src/_control/ResizeHandle.tsx +136 -0
- package/src/_control/TrailingGroup.tsx +204 -0
- package/src/_control/clearControl.ts +20 -0
- package/src/_control/controlSizingStyle.ts +37 -0
- package/src/_control/focus.ts +34 -0
- package/src/_control/multilineSizeStyles.ts +35 -0
- package/src/_control/useControlValue.ts +31 -0
- package/src/_control/useStableRef.ts +71 -0
- package/src/_control/wrapperBase.ts +163 -0
- package/src/control-sizes.ts +46 -0
- package/src/extend-props.ts +9 -0
- package/src/pandacss-preset.ts +492 -0
- package/src/spacing.ts +17 -0
- package/src/vite-shell.ts +118 -0
- package/styled-system/css/conditions.mjs +36 -0
- package/styled-system/css/css.d.ts +22 -0
- package/styled-system/css/css.mjs +45 -0
- package/styled-system/css/cva.d.ts +6 -0
- package/styled-system/css/cva.mjs +87 -0
- package/styled-system/css/cx.d.ts +5 -0
- package/styled-system/css/cx.mjs +15 -0
- package/styled-system/css/index.d.ts +5 -0
- package/styled-system/css/index.mjs +4 -0
- package/styled-system/css/sva.d.ts +4 -0
- package/styled-system/css/sva.mjs +46 -0
- package/styled-system/helpers.mjs +328 -0
- package/styled-system/jsx/aspect-ratio.d.ts +10 -0
- package/styled-system/jsx/aspect-ratio.mjs +14 -0
- package/styled-system/jsx/bleed.d.ts +10 -0
- package/styled-system/jsx/bleed.mjs +14 -0
- package/styled-system/jsx/box.d.ts +10 -0
- package/styled-system/jsx/box.mjs +14 -0
- package/styled-system/jsx/center.d.ts +10 -0
- package/styled-system/jsx/center.mjs +14 -0
- package/styled-system/jsx/circle.d.ts +10 -0
- package/styled-system/jsx/circle.mjs +14 -0
- package/styled-system/jsx/container.d.ts +10 -0
- package/styled-system/jsx/container.mjs +14 -0
- package/styled-system/jsx/cq.d.ts +10 -0
- package/styled-system/jsx/cq.mjs +14 -0
- package/styled-system/jsx/create-style-context.d.ts +54 -0
- package/styled-system/jsx/create-style-context.mjs +121 -0
- package/styled-system/jsx/divider.d.ts +10 -0
- package/styled-system/jsx/divider.mjs +14 -0
- package/styled-system/jsx/factory-helper.mjs +22 -0
- package/styled-system/jsx/factory.d.ts +3 -0
- package/styled-system/jsx/factory.mjs +89 -0
- package/styled-system/jsx/flex.d.ts +10 -0
- package/styled-system/jsx/flex.mjs +14 -0
- package/styled-system/jsx/float.d.ts +10 -0
- package/styled-system/jsx/float.mjs +14 -0
- package/styled-system/jsx/grid-item.d.ts +10 -0
- package/styled-system/jsx/grid-item.mjs +14 -0
- package/styled-system/jsx/grid.d.ts +10 -0
- package/styled-system/jsx/grid.mjs +14 -0
- package/styled-system/jsx/hstack.d.ts +10 -0
- package/styled-system/jsx/hstack.mjs +14 -0
- package/styled-system/jsx/index.d.ts +25 -0
- package/styled-system/jsx/index.mjs +23 -0
- package/styled-system/jsx/is-valid-prop.d.ts +11 -0
- package/styled-system/jsx/is-valid-prop.mjs +17 -0
- package/styled-system/jsx/link-overlay.d.ts +10 -0
- package/styled-system/jsx/link-overlay.mjs +14 -0
- package/styled-system/jsx/spacer.d.ts +10 -0
- package/styled-system/jsx/spacer.mjs +14 -0
- package/styled-system/jsx/square.d.ts +10 -0
- package/styled-system/jsx/square.mjs +14 -0
- package/styled-system/jsx/stack.d.ts +10 -0
- package/styled-system/jsx/stack.mjs +14 -0
- package/styled-system/jsx/visually-hidden.d.ts +10 -0
- package/styled-system/jsx/visually-hidden.mjs +14 -0
- package/styled-system/jsx/vstack.d.ts +10 -0
- package/styled-system/jsx/vstack.mjs +14 -0
- package/styled-system/jsx/wrap.d.ts +10 -0
- package/styled-system/jsx/wrap.mjs +14 -0
- package/styled-system/patterns/aspect-ratio.d.ts +20 -0
- package/styled-system/patterns/aspect-ratio.mjs +38 -0
- package/styled-system/patterns/bleed.d.ts +21 -0
- package/styled-system/patterns/bleed.mjs +24 -0
- package/styled-system/patterns/box.d.ts +20 -0
- package/styled-system/patterns/box.mjs +15 -0
- package/styled-system/patterns/center.d.ts +20 -0
- package/styled-system/patterns/center.mjs +21 -0
- package/styled-system/patterns/circle.d.ts +20 -0
- package/styled-system/patterns/circle.mjs +25 -0
- package/styled-system/patterns/container.d.ts +20 -0
- package/styled-system/patterns/container.mjs +21 -0
- package/styled-system/patterns/cq.d.ts +21 -0
- package/styled-system/patterns/cq.mjs +21 -0
- package/styled-system/patterns/divider.d.ts +22 -0
- package/styled-system/patterns/divider.mjs +25 -0
- package/styled-system/patterns/flex.d.ts +26 -0
- package/styled-system/patterns/flex.mjs +26 -0
- package/styled-system/patterns/float.d.ts +23 -0
- package/styled-system/patterns/float.mjs +52 -0
- package/styled-system/patterns/grid-item.d.ts +25 -0
- package/styled-system/patterns/grid-item.mjs +25 -0
- package/styled-system/patterns/grid.d.ts +24 -0
- package/styled-system/patterns/grid.mjs +27 -0
- package/styled-system/patterns/hstack.d.ts +21 -0
- package/styled-system/patterns/hstack.mjs +24 -0
- package/styled-system/patterns/index.d.ts +21 -0
- package/styled-system/patterns/index.mjs +20 -0
- package/styled-system/patterns/link-overlay.d.ts +20 -0
- package/styled-system/patterns/link-overlay.mjs +24 -0
- package/styled-system/patterns/spacer.d.ts +20 -0
- package/styled-system/patterns/spacer.mjs +25 -0
- package/styled-system/patterns/square.d.ts +20 -0
- package/styled-system/patterns/square.mjs +24 -0
- package/styled-system/patterns/stack.d.ts +23 -0
- package/styled-system/patterns/stack.mjs +24 -0
- package/styled-system/patterns/visually-hidden.d.ts +20 -0
- package/styled-system/patterns/visually-hidden.mjs +18 -0
- package/styled-system/patterns/vstack.d.ts +21 -0
- package/styled-system/patterns/vstack.mjs +24 -0
- package/styled-system/patterns/wrap.d.ts +24 -0
- package/styled-system/patterns/wrap.mjs +25 -0
- package/styled-system/recipes/control.d.ts +31 -0
- package/styled-system/recipes/control.mjs +35 -0
- package/styled-system/recipes/create-recipe.mjs +82 -0
- package/styled-system/recipes/index.d.ts +2 -0
- package/styled-system/recipes/index.mjs +1 -0
- package/styled-system/tokens/index.d.ts +9 -0
- package/styled-system/tokens/index.mjs +49636 -0
- package/styled-system/tokens/tokens.d.ts +72 -0
- package/styled-system/types/composition.d.ts +227 -0
- package/styled-system/types/conditions.d.ts +320 -0
- package/styled-system/types/csstype.d.ts +22570 -0
- package/styled-system/types/global.d.ts +20 -0
- package/styled-system/types/index.d.ts +8 -0
- package/styled-system/types/jsx.d.ts +69 -0
- package/styled-system/types/parts.d.ts +8 -0
- package/styled-system/types/pattern.d.ts +78 -0
- package/styled-system/types/prop-type.d.ts +268 -0
- package/styled-system/types/recipe.d.ts +181 -0
- package/styled-system/types/selectors.d.ts +59 -0
- package/styled-system/types/static-css.d.ts +56 -0
- package/styled-system/types/style-props.d.ts +8089 -0
- package/styled-system/types/system-types.d.ts +151 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Decorator } from "@storybook/react-vite";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Storybook decorator that monkey-patches `window.Image` so the next image
|
|
6
|
+
* created while a story is mounted defers its load callback by 5 seconds.
|
|
7
|
+
* Lets the Avatar stories show the loading skeleton long enough to inspect
|
|
8
|
+
* before the image fades in (or fails). Reverts the patch on unmount.
|
|
9
|
+
*
|
|
10
|
+
* The patch is installed during render (via `useState` lazy init) rather
|
|
11
|
+
* than from `useEffect`, because base-ui's `useImageLoadingStatus` calls
|
|
12
|
+
* `new window.Image()` from inside `useIsoLayoutEffect` — a *layout* effect
|
|
13
|
+
* that fires before our decorator's passive `useEffect`. Installing the
|
|
14
|
+
* patch later would miss the probe Image's construction and the loading
|
|
15
|
+
* delay would never apply. Uninstall still runs from `useEffect` cleanup,
|
|
16
|
+
* which is fine because the patch only needs to be removed on unmount.
|
|
17
|
+
*/
|
|
18
|
+
export const slowImageDecorator: Decorator = (Story) => {
|
|
19
|
+
useState(installSlowImage);
|
|
20
|
+
useEffect(() => uninstallSlowImage, []);
|
|
21
|
+
return <Story />;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SLOW_LOAD_MS = 5000;
|
|
25
|
+
const PATCH_FLAG = "__avatarSlowImageOriginal";
|
|
26
|
+
|
|
27
|
+
type PatchedWindow = typeof window & { [PATCH_FLAG]?: typeof window.Image };
|
|
28
|
+
|
|
29
|
+
const installSlowImage = () => {
|
|
30
|
+
if (typeof window === "undefined") {
|
|
31
|
+
return;
|
|
32
|
+
} else {
|
|
33
|
+
const w = window as PatchedWindow;
|
|
34
|
+
if (w[PATCH_FLAG] === undefined) {
|
|
35
|
+
const OriginalImage = window.Image;
|
|
36
|
+
const srcDescriptor = Object.getOwnPropertyDescriptor(
|
|
37
|
+
HTMLImageElement.prototype,
|
|
38
|
+
"src",
|
|
39
|
+
);
|
|
40
|
+
const completeDescriptor = Object.getOwnPropertyDescriptor(
|
|
41
|
+
HTMLImageElement.prototype,
|
|
42
|
+
"complete",
|
|
43
|
+
);
|
|
44
|
+
if (srcDescriptor === undefined || completeDescriptor === undefined) {
|
|
45
|
+
return;
|
|
46
|
+
} else {
|
|
47
|
+
// Must be a `function` (not arrow) because consumers call
|
|
48
|
+
// `new window.Image(...)` and arrow functions are not constructable.
|
|
49
|
+
const SlowImage = function (
|
|
50
|
+
...args: ConstructorParameters<typeof OriginalImage>
|
|
51
|
+
) {
|
|
52
|
+
const img = new OriginalImage(...args);
|
|
53
|
+
let pending = false;
|
|
54
|
+
Object.defineProperty(img, "complete", {
|
|
55
|
+
configurable: true,
|
|
56
|
+
get: () =>
|
|
57
|
+
pending === true ? false : completeDescriptor.get?.call(img),
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(img, "src", {
|
|
60
|
+
configurable: true,
|
|
61
|
+
get: () => srcDescriptor.get?.call(img),
|
|
62
|
+
set: (value: string) => {
|
|
63
|
+
pending = true;
|
|
64
|
+
window.setTimeout(() => {
|
|
65
|
+
pending = false;
|
|
66
|
+
srcDescriptor.set?.call(img, value);
|
|
67
|
+
}, SLOW_LOAD_MS);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return img;
|
|
71
|
+
} as unknown as typeof window.Image;
|
|
72
|
+
w[PATCH_FLAG] = OriginalImage;
|
|
73
|
+
window.Image = SlowImage;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const uninstallSlowImage = () => {
|
|
82
|
+
if (typeof window === "undefined") {
|
|
83
|
+
return;
|
|
84
|
+
} else {
|
|
85
|
+
const w = window as PatchedWindow;
|
|
86
|
+
const original = w[PATCH_FLAG];
|
|
87
|
+
if (original !== undefined) {
|
|
88
|
+
window.Image = original;
|
|
89
|
+
delete w[PATCH_FLAG];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Button as BaseButton } from "@base-ui/react/button";
|
|
2
|
+
import type { ComponentProps, ReactNode, Ref } from "react";
|
|
3
|
+
import { useLayoutEffect, useState } from "react";
|
|
4
|
+
import { css, cva, cx } from "../../styled-system/css";
|
|
5
|
+
import type { ControlVariant } from "../../styled-system/recipes";
|
|
6
|
+
import { control } from "../../styled-system/recipes";
|
|
7
|
+
import type { ColorToken } from "../../styled-system/tokens";
|
|
8
|
+
import { useStableRef } from "../_control/useStableRef";
|
|
9
|
+
import type { ExtendProps } from "../extend-props";
|
|
10
|
+
|
|
11
|
+
export { SIZES, type Size } from "../control-sizes";
|
|
12
|
+
|
|
13
|
+
export const VARIANTS = [
|
|
14
|
+
"primary",
|
|
15
|
+
"solid",
|
|
16
|
+
"ghost",
|
|
17
|
+
"outline",
|
|
18
|
+
"success",
|
|
19
|
+
"danger",
|
|
20
|
+
"warning",
|
|
21
|
+
"accent",
|
|
22
|
+
] as const;
|
|
23
|
+
export type Variant = (typeof VARIANTS)[number];
|
|
24
|
+
|
|
25
|
+
type CommonProps = Partial<ControlVariant> & {
|
|
26
|
+
disabled?: boolean | undefined;
|
|
27
|
+
loading?: boolean | undefined;
|
|
28
|
+
rounded?: boolean | undefined;
|
|
29
|
+
variant?: Variant | undefined;
|
|
30
|
+
} & (
|
|
31
|
+
| {
|
|
32
|
+
afterIcon?: ReactNode | undefined;
|
|
33
|
+
beforeIcon?: ReactNode | undefined;
|
|
34
|
+
children: string;
|
|
35
|
+
label?: undefined;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
afterIcon?: undefined;
|
|
39
|
+
beforeIcon?: undefined;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
label: string;
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
type Props =
|
|
45
|
+
| ExtendProps<
|
|
46
|
+
typeof BaseButton,
|
|
47
|
+
CommonProps & {
|
|
48
|
+
href?: undefined;
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
| ExtendProps<
|
|
52
|
+
"a",
|
|
53
|
+
CommonProps & {
|
|
54
|
+
href: NonNullable<ComponentProps<"a">["href"]>;
|
|
55
|
+
}
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
export const Button = ({
|
|
59
|
+
disabled = false,
|
|
60
|
+
loading = false,
|
|
61
|
+
rounded = false,
|
|
62
|
+
size = "md",
|
|
63
|
+
variant = "primary",
|
|
64
|
+
beforeIcon,
|
|
65
|
+
afterIcon,
|
|
66
|
+
children,
|
|
67
|
+
label,
|
|
68
|
+
...passthroughProps
|
|
69
|
+
}: Props) => {
|
|
70
|
+
// The caller's ref as well as this button's own. `sharedProps` below sets
|
|
71
|
+
// `ref` after `passthroughProps` is spread, so a button that did not pass it
|
|
72
|
+
// on here would replace the caller's ref with its own and hand the element
|
|
73
|
+
// back to nobody — see `useStableRef` for what that breaks.
|
|
74
|
+
//
|
|
75
|
+
// THE CAST IS THE POLYMORPHISM. `Props` is a union, so the ref arrives as a
|
|
76
|
+
// ref for an anchor or a ref for a button, and a ref's element is a
|
|
77
|
+
// parameter — contravariant — so the two do not unify into a ref for either.
|
|
78
|
+
// What TypeScript cannot see is that the `href` which picked the member of
|
|
79
|
+
// the union is the same `href` that picks the element below: this is the
|
|
80
|
+
// anchor's ref exactly when an anchor is what gets drawn.
|
|
81
|
+
const forwarded = passthroughProps.ref as
|
|
82
|
+
| Ref<HTMLAnchorElement | HTMLButtonElement>
|
|
83
|
+
| undefined;
|
|
84
|
+
const [elementRef, setElementRef] = useStableRef<
|
|
85
|
+
HTMLAnchorElement | HTMLButtonElement
|
|
86
|
+
>(forwarded);
|
|
87
|
+
const [renderedLoading, setRenderedLoading] = useState(loading);
|
|
88
|
+
|
|
89
|
+
// Smooth handoff from the `_loading` pulse animation back to the resting
|
|
90
|
+
// opacity. The pulse is keyframe-based, so removing the `data-loading`
|
|
91
|
+
// attribute would snap opacity to the rest value instantly. Instead we
|
|
92
|
+
// (1) snapshot the live computed opacity into an inline `style`, freezing
|
|
93
|
+
// the in-flight pulse where it was; (2) drop the loading attribute on the
|
|
94
|
+
// next render; (3) clear the inline style next frame so the control
|
|
95
|
+
// recipe's `opacity` transition takes over from that frozen starting value
|
|
96
|
+
// and animates smoothly back to 1.
|
|
97
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `elementRef` is a stable RefObject from useStableRef; biome doesn't see through the helper
|
|
98
|
+
useLayoutEffect(() => {
|
|
99
|
+
if (loading === true && renderedLoading === false) {
|
|
100
|
+
setRenderedLoading(true);
|
|
101
|
+
} else if (renderedLoading === true && loading === false) {
|
|
102
|
+
if (elementRef.current !== null) {
|
|
103
|
+
elementRef.current.style.opacity = getComputedStyle(
|
|
104
|
+
elementRef.current,
|
|
105
|
+
).opacity;
|
|
106
|
+
}
|
|
107
|
+
// setRenderedLoading triggers a re-render that removes the `data-loading`
|
|
108
|
+
// attribute; the inline opacity we just set persists into that render and
|
|
109
|
+
// is cleared on the next frame so the control recipe's `opacity`
|
|
110
|
+
// transition takes over from the frozen starting value.
|
|
111
|
+
setRenderedLoading(false);
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
if (elementRef.current !== null) {
|
|
114
|
+
elementRef.current.style.opacity = "";
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}, [loading, renderedLoading]);
|
|
119
|
+
|
|
120
|
+
const iconOnly = label !== undefined;
|
|
121
|
+
const isDisabled = loading === true || disabled === true;
|
|
122
|
+
const sharedProps = {
|
|
123
|
+
"aria-busy": loading === true ? true : undefined,
|
|
124
|
+
"aria-label": label,
|
|
125
|
+
children: iconOnly ? (
|
|
126
|
+
<span className={iconOnlyStyles}>{children}</span>
|
|
127
|
+
) : (
|
|
128
|
+
<>
|
|
129
|
+
{beforeIcon}
|
|
130
|
+
<span className={labelStyles}>{children}</span>
|
|
131
|
+
{afterIcon}
|
|
132
|
+
</>
|
|
133
|
+
),
|
|
134
|
+
className: cx(
|
|
135
|
+
"group",
|
|
136
|
+
control({ size }),
|
|
137
|
+
styles({ iconOnly, rounded, variant }),
|
|
138
|
+
),
|
|
139
|
+
"data-loading": renderedLoading === true ? "" : undefined,
|
|
140
|
+
ref: setElementRef,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (passthroughProps.href === undefined) {
|
|
144
|
+
// `<button disabled>` is the native disabled mechanism — fires no click.
|
|
145
|
+
return (
|
|
146
|
+
<BaseButton
|
|
147
|
+
{...passthroughProps}
|
|
148
|
+
{...sharedProps}
|
|
149
|
+
disabled={isDisabled}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
// Anchors have no native `disabled` — that attribute would render as
|
|
154
|
+
// invalid HTML and the link would still navigate. Emulate it with
|
|
155
|
+
// `aria-disabled`, `data-disabled` (so the control recipe's `_disabled`
|
|
156
|
+
// condition matches: `:is(:disabled, [data-disabled])`), and an
|
|
157
|
+
// onClick guard.
|
|
158
|
+
return (
|
|
159
|
+
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: anchor with href is interactive; onClick is the disabled-state guard that responds to keyboard via the native anchor behavior
|
|
160
|
+
<a
|
|
161
|
+
{...passthroughProps}
|
|
162
|
+
{...sharedProps}
|
|
163
|
+
aria-disabled={isDisabled === true ? true : undefined}
|
|
164
|
+
data-disabled={isDisabled === true ? "" : undefined}
|
|
165
|
+
// biome-ignore lint/a11y/useValidAnchor: anchor is for navigation (has href); onClick only intercepts to block when disabled
|
|
166
|
+
onClick={(event) => {
|
|
167
|
+
if (isDisabled === true) {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
} else if (passthroughProps.onClick !== undefined) {
|
|
170
|
+
passthroughProps.onClick(event);
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
tabIndex={isDisabled === true ? -1 : passthroughProps.tabIndex}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const iconOnlyStyles = css({
|
|
180
|
+
// Match both `:disabled` (button) and `[data-disabled]` (anchor) so the
|
|
181
|
+
// icon-only hover/active scale doesn't fire on a "disabled" anchor
|
|
182
|
+
// (anchors don't get the `:disabled` pseudo-class).
|
|
183
|
+
".group:not([data-disabled]):not(:disabled):active &": { scale: 1.1 },
|
|
184
|
+
".group:not([data-disabled]):not(:disabled):hover &": { scale: 1.2 },
|
|
185
|
+
// `inline-flex` + center alignment so the icon sits at the geometric
|
|
186
|
+
// center of the wrapper rather than on the line-height baseline. A
|
|
187
|
+
// bare inline wrapper sizes its line box to the font's line-height,
|
|
188
|
+
// and the inline icon child (img or mask span) settles on the
|
|
189
|
+
// baseline of that line — pushing the icon visibly down relative to
|
|
190
|
+
// the button's flex-centered wrapper.
|
|
191
|
+
alignItems: "center",
|
|
192
|
+
display: "inline-flex",
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
scale: 1,
|
|
195
|
+
transition: "scale {durations.faster} {easings.out}",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const labelStyles = css({ paddingInline: "0.25em" });
|
|
199
|
+
|
|
200
|
+
const tinted = (color: ColorToken) => ({
|
|
201
|
+
backgroundColor: {
|
|
202
|
+
_activeEnabled: `color-mix(in oklab, {colors.${color}} 80%, {colors.background})`,
|
|
203
|
+
_hoverEnabled: `color-mix(in oklab, {colors.${color}} 70%, {colors.foreground})`,
|
|
204
|
+
base: color,
|
|
205
|
+
},
|
|
206
|
+
borderColor: "transparent",
|
|
207
|
+
color: "background",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const ghost = {
|
|
211
|
+
backgroundColor: {
|
|
212
|
+
_activeEnabled: "color-mix(in oklab, {colors.foreground} 10%, transparent)",
|
|
213
|
+
_hoverEnabled: "color-mix(in oklab, {colors.foreground} 5%, transparent)",
|
|
214
|
+
base: "transparent",
|
|
215
|
+
},
|
|
216
|
+
borderColor: "transparent",
|
|
217
|
+
color: {
|
|
218
|
+
_activeEnabled: "color-mix(in oklab, {colors.foreground} 85%, white)",
|
|
219
|
+
_hoverEnabled: "foreground",
|
|
220
|
+
base: "muted",
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const styles = cva({
|
|
225
|
+
base: {
|
|
226
|
+
animation: {
|
|
227
|
+
_loading:
|
|
228
|
+
"pulse {durations.pulse} {easings.in-out} infinite {durations.slow}",
|
|
229
|
+
},
|
|
230
|
+
border: "1px solid transparent",
|
|
231
|
+
cursor: { _disabled: "not-allowed", _loading: "wait", base: "pointer" },
|
|
232
|
+
filter: { _disabled: "saturate(0.6)", base: "none" },
|
|
233
|
+
gap: 0,
|
|
234
|
+
opacity: { _disabled: "disabled", _loading: "pulseMin", base: 1 },
|
|
235
|
+
},
|
|
236
|
+
variants: {
|
|
237
|
+
iconOnly: {
|
|
238
|
+
true: {
|
|
239
|
+
aspectRatio: "square",
|
|
240
|
+
inlineSize: "auto",
|
|
241
|
+
paddingInline: 0,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
rounded: {
|
|
245
|
+
true: { borderRadius: "full" },
|
|
246
|
+
},
|
|
247
|
+
variant: {
|
|
248
|
+
accent: tinted("accent"),
|
|
249
|
+
danger: tinted("danger"),
|
|
250
|
+
ghost,
|
|
251
|
+
outline: {
|
|
252
|
+
...ghost,
|
|
253
|
+
borderColor: {
|
|
254
|
+
_activeEnabled: "foreground",
|
|
255
|
+
_hoverEnabled: "muted",
|
|
256
|
+
base: "border",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
primary: {
|
|
260
|
+
backgroundColor: {
|
|
261
|
+
_activeEnabled:
|
|
262
|
+
"color-mix(in oklab, {colors.background} 85%, {colors.foreground})",
|
|
263
|
+
_hoverEnabled:
|
|
264
|
+
"color-mix(in oklab, {colors.background} 70%, {colors.foreground})",
|
|
265
|
+
base: "color-mix(in oklab, {colors.background} 80%, {colors.foreground})",
|
|
266
|
+
},
|
|
267
|
+
borderColor: "transparent",
|
|
268
|
+
color: "foreground",
|
|
269
|
+
},
|
|
270
|
+
solid: {
|
|
271
|
+
backgroundColor: {
|
|
272
|
+
_activeEnabled:
|
|
273
|
+
"color-mix(in oklab, {colors.foreground} 60%, {colors.background})",
|
|
274
|
+
_hoverEnabled:
|
|
275
|
+
"color-mix(in oklab, {colors.foreground} 80%, {colors.background})",
|
|
276
|
+
base: "foreground",
|
|
277
|
+
},
|
|
278
|
+
borderColor: "transparent",
|
|
279
|
+
color: "background",
|
|
280
|
+
},
|
|
281
|
+
success: tinted("success"),
|
|
282
|
+
warning: tinted("warning"),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
|
|
3
|
+
import { css } from "../../styled-system/css";
|
|
4
|
+
import { stack } from "../../styled-system/patterns";
|
|
5
|
+
import type { ExtendProps } from "../extend-props";
|
|
6
|
+
|
|
7
|
+
type Props = ExtendProps<"section", { title?: string | undefined }>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A surface: a padded, bordered container that stacks its children vertically,
|
|
11
|
+
* with an optional `title` rendered as its heading. The base building block for
|
|
12
|
+
* the app's chrome panels — settings, sidebars, and the like frame their content
|
|
13
|
+
* in one rather than each re-deriving the same card treatment. Styling is fixed
|
|
14
|
+
* (it's a design primitive, not a `className` sink); compose layout inside it.
|
|
15
|
+
*
|
|
16
|
+
* A card is a thematic grouping, not a styling wrapper, so it renders a
|
|
17
|
+
* `<section>`. When it has a `title`, the heading names the section
|
|
18
|
+
* (`aria-labelledby`), promoting it to a landmark `region` a screen reader can
|
|
19
|
+
* jump to; an untitled card stays a plain, unnamed section.
|
|
20
|
+
*/
|
|
21
|
+
export const Card = ({ children, title, ...props }: Props) => {
|
|
22
|
+
const headingId = useId();
|
|
23
|
+
return (
|
|
24
|
+
<section
|
|
25
|
+
aria-labelledby={title === undefined ? undefined : headingId}
|
|
26
|
+
{...props}
|
|
27
|
+
className={cardStyles}
|
|
28
|
+
>
|
|
29
|
+
{title !== undefined && (
|
|
30
|
+
<h2 className={headingStyles} id={headingId}>
|
|
31
|
+
{title}
|
|
32
|
+
</h2>
|
|
33
|
+
)}
|
|
34
|
+
{children}
|
|
35
|
+
</section>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// A vertical `stack` carries the flex column + gap; the surface tokens ride
|
|
40
|
+
// alongside. `stack` defaults to `direction: column` and leaves `alignItems`
|
|
41
|
+
// unset, so children stretch to the card's width.
|
|
42
|
+
const cardStyles = stack({
|
|
43
|
+
backgroundColor: "card",
|
|
44
|
+
border: "1px solid {colors.border}",
|
|
45
|
+
borderRadius: "lg",
|
|
46
|
+
gap: 3,
|
|
47
|
+
padding: 4,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const headingStyles = css({
|
|
51
|
+
color: "foreground",
|
|
52
|
+
fontSize: "sm",
|
|
53
|
+
fontWeight: "semibold",
|
|
54
|
+
margin: 0,
|
|
55
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Field as BaseField } from "@base-ui/react/field";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { css } from "../../styled-system/css";
|
|
5
|
+
import { flex, grid } from "../../styled-system/patterns";
|
|
6
|
+
import type { ExtendProps } from "../extend-props";
|
|
7
|
+
|
|
8
|
+
import { FieldErrorPopover } from "./FieldErrorPopover";
|
|
9
|
+
|
|
10
|
+
type Props = ExtendProps<
|
|
11
|
+
typeof BaseField.Root,
|
|
12
|
+
{
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
error?: ReactNode | undefined;
|
|
15
|
+
hint?: ReactNode | undefined;
|
|
16
|
+
label?: ReactNode | undefined;
|
|
17
|
+
}
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export const Field = ({
|
|
21
|
+
children,
|
|
22
|
+
error,
|
|
23
|
+
hint,
|
|
24
|
+
invalid,
|
|
25
|
+
label,
|
|
26
|
+
...props
|
|
27
|
+
}: Props) => {
|
|
28
|
+
const [controlElement, setControlElement] = useState<
|
|
29
|
+
HTMLDivElement | undefined
|
|
30
|
+
>(undefined);
|
|
31
|
+
// React passes `null` to ref callbacks on unmount; map that to `undefined`
|
|
32
|
+
// to keep the codebase's no-`null` convention.
|
|
33
|
+
const setControlElementRef = useCallback((el: HTMLDivElement | null) => {
|
|
34
|
+
setControlElement(el ?? undefined);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<BaseField.Root
|
|
39
|
+
{...props}
|
|
40
|
+
className={rootStyles}
|
|
41
|
+
// Stable hook for wrapperBase's "label-hover → control hover" rule
|
|
42
|
+
// (`[data-domicile-field]:has(> label:hover) > div > &`). base-ui doesn't
|
|
43
|
+
// emit a discriminating attribute on the Field root, and a generic
|
|
44
|
+
// `label:hover ~ div *` selector would leak hover state into unrelated
|
|
45
|
+
// controls that happen to follow a hovered label on the page.
|
|
46
|
+
data-domicile-field=""
|
|
47
|
+
invalid={error === undefined ? invalid : true}
|
|
48
|
+
>
|
|
49
|
+
{label !== undefined && (
|
|
50
|
+
<BaseField.Label className={labelStyles}>{label}</BaseField.Label>
|
|
51
|
+
)}
|
|
52
|
+
<div className={controlRowStyles} ref={setControlElementRef}>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
{hint !== undefined && (
|
|
56
|
+
<BaseField.Description className={hintStyles}>
|
|
57
|
+
{hint}
|
|
58
|
+
</BaseField.Description>
|
|
59
|
+
)}
|
|
60
|
+
<BaseField.Validity>
|
|
61
|
+
{({ error: nativeError, validity }) => (
|
|
62
|
+
<FieldErrorPopover
|
|
63
|
+
anchor={controlElement}
|
|
64
|
+
error={resolveError(
|
|
65
|
+
error,
|
|
66
|
+
validity.valid ?? undefined,
|
|
67
|
+
nativeError,
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</BaseField.Validity>
|
|
72
|
+
</BaseField.Root>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const resolveError = (
|
|
77
|
+
explicit: ReactNode,
|
|
78
|
+
valid: boolean | undefined,
|
|
79
|
+
nativeMessage: string,
|
|
80
|
+
): ReactNode =>
|
|
81
|
+
explicit ??
|
|
82
|
+
(valid === false && nativeMessage !== "" ? nativeMessage : undefined);
|
|
83
|
+
|
|
84
|
+
// No flex `gap` on the root — the label↔control and control↔hint gaps
|
|
85
|
+
// are set independently via directional margins on the label and hint
|
|
86
|
+
// (`marginBlockEnd` / `marginBlockStart` below) so the hint can sit
|
|
87
|
+
// tighter under the control than the label does above it.
|
|
88
|
+
const rootStyles = flex({
|
|
89
|
+
cursor: { _disabled: "not-allowed", base: "auto" },
|
|
90
|
+
direction: "column",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const controlRowStyles = grid({
|
|
94
|
+
alignItems: "stretch",
|
|
95
|
+
gap: 1.5,
|
|
96
|
+
gridAutoColumns: "auto",
|
|
97
|
+
gridAutoFlow: "column",
|
|
98
|
+
gridTemplateColumns: "minmax(0, 1fr)",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const labelStyles = css({
|
|
102
|
+
_disabled: { color: "muted", opacity: "disabled" },
|
|
103
|
+
_invalid: { color: "danger" },
|
|
104
|
+
color: "foreground",
|
|
105
|
+
// Pointer cursor on the label signals "clicking me focuses the
|
|
106
|
+
// control" — the conventional affordance for a form label, and
|
|
107
|
+
// matches the wrapped control's own cursor: when the control is
|
|
108
|
+
// disabled the label switches to not-allowed alongside it.
|
|
109
|
+
cursor: { _disabled: "not-allowed", base: "pointer" },
|
|
110
|
+
// `display: block` makes the label fill the field's full inline width
|
|
111
|
+
// so clicks anywhere on the row — not just on the text glyphs — hit
|
|
112
|
+
// the label and forward to the control. The space between the label
|
|
113
|
+
// and the control is `paddingBlockEnd` (rather than `marginBlockEnd`
|
|
114
|
+
// or the parent's flex `gap`) for the same reason: it counts as part
|
|
115
|
+
// of the label's hit region instead of being inert space below it.
|
|
116
|
+
display: "block",
|
|
117
|
+
fontSize: "xs",
|
|
118
|
+
fontVariantNumeric: "tabular-nums",
|
|
119
|
+
fontWeight: "medium",
|
|
120
|
+
letterSpacing: "normal",
|
|
121
|
+
lineHeight: "snug",
|
|
122
|
+
margin: 0,
|
|
123
|
+
paddingBlockEnd: 2,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const hintStyles = css({
|
|
127
|
+
_disabled: { opacity: "disabled" },
|
|
128
|
+
color: "muted",
|
|
129
|
+
fontSize: "xs",
|
|
130
|
+
fontVariantNumeric: "tabular-nums",
|
|
131
|
+
fontWeight: "normal",
|
|
132
|
+
letterSpacing: "normal",
|
|
133
|
+
lineHeight: "normal",
|
|
134
|
+
margin: 0,
|
|
135
|
+
marginBlockStart: 0.5,
|
|
136
|
+
});
|