@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.
Files changed (178) hide show
  1. package/README.md +76 -0
  2. package/package.json +90 -0
  3. package/src/Autocomplete/Autocomplete.tsx +339 -0
  4. package/src/Avatar/Avatar.tsx +124 -0
  5. package/src/Avatar/gradient.ts +17 -0
  6. package/src/Avatar/initials.ts +19 -0
  7. package/src/Avatar/slowImageDecorator.tsx +92 -0
  8. package/src/Button/Button.tsx +285 -0
  9. package/src/Card/Card.tsx +55 -0
  10. package/src/Field/Field.tsx +136 -0
  11. package/src/Field/FieldErrorPopover.tsx +144 -0
  12. package/src/Input/Input.tsx +157 -0
  13. package/src/Kbd/Kbd.tsx +25 -0
  14. package/src/ModalDialog/ModalDialog.tsx +367 -0
  15. package/src/Popover/Popover.tsx +236 -0
  16. package/src/Provider/Provider.tsx +29 -0
  17. package/src/Screen/DisplayProvider.tsx +105 -0
  18. package/src/Screen/Screen.tsx +122 -0
  19. package/src/Screen/display-source.ts +64 -0
  20. package/src/Select/Select.tsx +576 -0
  21. package/src/SlideOver/SlideOver.tsx +176 -0
  22. package/src/Slider/Slider.tsx +91 -0
  23. package/src/TabRail/TabRail.tsx +355 -0
  24. package/src/Tabs/Tabs.tsx +163 -0
  25. package/src/Textarea/Textarea.tsx +270 -0
  26. package/src/ThemeSwitch/ThemeProvider.tsx +130 -0
  27. package/src/ThemeSwitch/ThemeSwitch.tsx +174 -0
  28. package/src/ThemeSwitch/standalone-theme-source.ts +42 -0
  29. package/src/ThemeSwitch/theme-core.ts +172 -0
  30. package/src/ThemeSwitch/theme-source.ts +65 -0
  31. package/src/Toaster/Toaster.tsx +250 -0
  32. package/src/__test__/AnyComponent.ts +11 -0
  33. package/src/__test__/Variants.tsx +89 -0
  34. package/src/__test__/adornmentVariations.tsx +202 -0
  35. package/src/__test__/iconControl.tsx +20 -0
  36. package/src/__test__/invalidDecorator.tsx +14 -0
  37. package/src/__test__/runControlSuite.tsx +212 -0
  38. package/src/__test__/wrapperOf.ts +14 -0
  39. package/src/_control/PrefixIconStack.tsx +153 -0
  40. package/src/_control/ResizeHandle.tsx +136 -0
  41. package/src/_control/TrailingGroup.tsx +204 -0
  42. package/src/_control/clearControl.ts +20 -0
  43. package/src/_control/controlSizingStyle.ts +37 -0
  44. package/src/_control/focus.ts +34 -0
  45. package/src/_control/multilineSizeStyles.ts +35 -0
  46. package/src/_control/useControlValue.ts +31 -0
  47. package/src/_control/useStableRef.ts +71 -0
  48. package/src/_control/wrapperBase.ts +163 -0
  49. package/src/control-sizes.ts +46 -0
  50. package/src/extend-props.ts +9 -0
  51. package/src/pandacss-preset.ts +492 -0
  52. package/src/spacing.ts +17 -0
  53. package/src/vite-shell.ts +118 -0
  54. package/styled-system/css/conditions.mjs +36 -0
  55. package/styled-system/css/css.d.ts +22 -0
  56. package/styled-system/css/css.mjs +45 -0
  57. package/styled-system/css/cva.d.ts +6 -0
  58. package/styled-system/css/cva.mjs +87 -0
  59. package/styled-system/css/cx.d.ts +5 -0
  60. package/styled-system/css/cx.mjs +15 -0
  61. package/styled-system/css/index.d.ts +5 -0
  62. package/styled-system/css/index.mjs +4 -0
  63. package/styled-system/css/sva.d.ts +4 -0
  64. package/styled-system/css/sva.mjs +46 -0
  65. package/styled-system/helpers.mjs +328 -0
  66. package/styled-system/jsx/aspect-ratio.d.ts +10 -0
  67. package/styled-system/jsx/aspect-ratio.mjs +14 -0
  68. package/styled-system/jsx/bleed.d.ts +10 -0
  69. package/styled-system/jsx/bleed.mjs +14 -0
  70. package/styled-system/jsx/box.d.ts +10 -0
  71. package/styled-system/jsx/box.mjs +14 -0
  72. package/styled-system/jsx/center.d.ts +10 -0
  73. package/styled-system/jsx/center.mjs +14 -0
  74. package/styled-system/jsx/circle.d.ts +10 -0
  75. package/styled-system/jsx/circle.mjs +14 -0
  76. package/styled-system/jsx/container.d.ts +10 -0
  77. package/styled-system/jsx/container.mjs +14 -0
  78. package/styled-system/jsx/cq.d.ts +10 -0
  79. package/styled-system/jsx/cq.mjs +14 -0
  80. package/styled-system/jsx/create-style-context.d.ts +54 -0
  81. package/styled-system/jsx/create-style-context.mjs +121 -0
  82. package/styled-system/jsx/divider.d.ts +10 -0
  83. package/styled-system/jsx/divider.mjs +14 -0
  84. package/styled-system/jsx/factory-helper.mjs +22 -0
  85. package/styled-system/jsx/factory.d.ts +3 -0
  86. package/styled-system/jsx/factory.mjs +89 -0
  87. package/styled-system/jsx/flex.d.ts +10 -0
  88. package/styled-system/jsx/flex.mjs +14 -0
  89. package/styled-system/jsx/float.d.ts +10 -0
  90. package/styled-system/jsx/float.mjs +14 -0
  91. package/styled-system/jsx/grid-item.d.ts +10 -0
  92. package/styled-system/jsx/grid-item.mjs +14 -0
  93. package/styled-system/jsx/grid.d.ts +10 -0
  94. package/styled-system/jsx/grid.mjs +14 -0
  95. package/styled-system/jsx/hstack.d.ts +10 -0
  96. package/styled-system/jsx/hstack.mjs +14 -0
  97. package/styled-system/jsx/index.d.ts +25 -0
  98. package/styled-system/jsx/index.mjs +23 -0
  99. package/styled-system/jsx/is-valid-prop.d.ts +11 -0
  100. package/styled-system/jsx/is-valid-prop.mjs +17 -0
  101. package/styled-system/jsx/link-overlay.d.ts +10 -0
  102. package/styled-system/jsx/link-overlay.mjs +14 -0
  103. package/styled-system/jsx/spacer.d.ts +10 -0
  104. package/styled-system/jsx/spacer.mjs +14 -0
  105. package/styled-system/jsx/square.d.ts +10 -0
  106. package/styled-system/jsx/square.mjs +14 -0
  107. package/styled-system/jsx/stack.d.ts +10 -0
  108. package/styled-system/jsx/stack.mjs +14 -0
  109. package/styled-system/jsx/visually-hidden.d.ts +10 -0
  110. package/styled-system/jsx/visually-hidden.mjs +14 -0
  111. package/styled-system/jsx/vstack.d.ts +10 -0
  112. package/styled-system/jsx/vstack.mjs +14 -0
  113. package/styled-system/jsx/wrap.d.ts +10 -0
  114. package/styled-system/jsx/wrap.mjs +14 -0
  115. package/styled-system/patterns/aspect-ratio.d.ts +20 -0
  116. package/styled-system/patterns/aspect-ratio.mjs +38 -0
  117. package/styled-system/patterns/bleed.d.ts +21 -0
  118. package/styled-system/patterns/bleed.mjs +24 -0
  119. package/styled-system/patterns/box.d.ts +20 -0
  120. package/styled-system/patterns/box.mjs +15 -0
  121. package/styled-system/patterns/center.d.ts +20 -0
  122. package/styled-system/patterns/center.mjs +21 -0
  123. package/styled-system/patterns/circle.d.ts +20 -0
  124. package/styled-system/patterns/circle.mjs +25 -0
  125. package/styled-system/patterns/container.d.ts +20 -0
  126. package/styled-system/patterns/container.mjs +21 -0
  127. package/styled-system/patterns/cq.d.ts +21 -0
  128. package/styled-system/patterns/cq.mjs +21 -0
  129. package/styled-system/patterns/divider.d.ts +22 -0
  130. package/styled-system/patterns/divider.mjs +25 -0
  131. package/styled-system/patterns/flex.d.ts +26 -0
  132. package/styled-system/patterns/flex.mjs +26 -0
  133. package/styled-system/patterns/float.d.ts +23 -0
  134. package/styled-system/patterns/float.mjs +52 -0
  135. package/styled-system/patterns/grid-item.d.ts +25 -0
  136. package/styled-system/patterns/grid-item.mjs +25 -0
  137. package/styled-system/patterns/grid.d.ts +24 -0
  138. package/styled-system/patterns/grid.mjs +27 -0
  139. package/styled-system/patterns/hstack.d.ts +21 -0
  140. package/styled-system/patterns/hstack.mjs +24 -0
  141. package/styled-system/patterns/index.d.ts +21 -0
  142. package/styled-system/patterns/index.mjs +20 -0
  143. package/styled-system/patterns/link-overlay.d.ts +20 -0
  144. package/styled-system/patterns/link-overlay.mjs +24 -0
  145. package/styled-system/patterns/spacer.d.ts +20 -0
  146. package/styled-system/patterns/spacer.mjs +25 -0
  147. package/styled-system/patterns/square.d.ts +20 -0
  148. package/styled-system/patterns/square.mjs +24 -0
  149. package/styled-system/patterns/stack.d.ts +23 -0
  150. package/styled-system/patterns/stack.mjs +24 -0
  151. package/styled-system/patterns/visually-hidden.d.ts +20 -0
  152. package/styled-system/patterns/visually-hidden.mjs +18 -0
  153. package/styled-system/patterns/vstack.d.ts +21 -0
  154. package/styled-system/patterns/vstack.mjs +24 -0
  155. package/styled-system/patterns/wrap.d.ts +24 -0
  156. package/styled-system/patterns/wrap.mjs +25 -0
  157. package/styled-system/recipes/control.d.ts +31 -0
  158. package/styled-system/recipes/control.mjs +35 -0
  159. package/styled-system/recipes/create-recipe.mjs +82 -0
  160. package/styled-system/recipes/index.d.ts +2 -0
  161. package/styled-system/recipes/index.mjs +1 -0
  162. package/styled-system/tokens/index.d.ts +9 -0
  163. package/styled-system/tokens/index.mjs +49636 -0
  164. package/styled-system/tokens/tokens.d.ts +72 -0
  165. package/styled-system/types/composition.d.ts +227 -0
  166. package/styled-system/types/conditions.d.ts +320 -0
  167. package/styled-system/types/csstype.d.ts +22570 -0
  168. package/styled-system/types/global.d.ts +20 -0
  169. package/styled-system/types/index.d.ts +8 -0
  170. package/styled-system/types/jsx.d.ts +69 -0
  171. package/styled-system/types/parts.d.ts +8 -0
  172. package/styled-system/types/pattern.d.ts +78 -0
  173. package/styled-system/types/prop-type.d.ts +268 -0
  174. package/styled-system/types/recipe.d.ts +181 -0
  175. package/styled-system/types/selectors.d.ts +59 -0
  176. package/styled-system/types/static-css.d.ts +56 -0
  177. package/styled-system/types/style-props.d.ts +8089 -0
  178. 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
+ });