@domicile-desktop/component-library 0.0.0-alpha-0bd0d2a4d04f → 0.0.0-alpha-a2ee8eaa5f91

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 CHANGED
@@ -2,8 +2,8 @@
2
2
  "dependencies": {
3
3
  "@base-ui/react": "^1.8.0",
4
4
  "@cprussin/option-result": "^2.0.1",
5
- "@domicile-desktop/sdk": "0.0.0-alpha-0bd0d2a4d04f",
6
- "@domicile-desktop/system-apps": "0.0.0-alpha-0bd0d2a4d04f",
5
+ "@domicile-desktop/sdk": "0.0.0-alpha-a2ee8eaa5f91",
6
+ "@domicile-desktop/system-apps": "0.0.0-alpha-a2ee8eaa5f91",
7
7
  "@pandacss/dev": "^1.12.1",
8
8
  "@phosphor-icons/react": "^2.1.10"
9
9
  },
@@ -159,5 +159,5 @@
159
159
  "test:unit": "bun test"
160
160
  },
161
161
  "type": "module",
162
- "version": "0.0.0-alpha-0bd0d2a4d04f"
162
+ "version": "0.0.0-alpha-a2ee8eaa5f91"
163
163
  }
@@ -36,6 +36,11 @@ type CommonProps<V> = Partial<ControlVariant> & {
36
36
  disabled?: boolean | undefined;
37
37
  /** Shown in place of the list when `suggestions` is empty. */
38
38
  emptyMessage?: ReactNode | undefined;
39
+ /**
40
+ * A tinted field with no edge until hovered or focused, as in an address
41
+ * bar, which sits on a toolbar rather than on a page.
42
+ */
43
+ filled?: boolean | undefined;
39
44
  onSuggestionTaken?: ((value: V) => void) | undefined;
40
45
  onValueChange?: ((value: string) => void) | undefined;
41
46
  rounded?: boolean | undefined;
@@ -74,6 +79,7 @@ export const Autocomplete = <V,>({
74
79
  autoHighlight = false,
75
80
  disabled,
76
81
  emptyMessage,
82
+ filled = false,
77
83
  onSuggestionTaken,
78
84
  onValueChange,
79
85
  prefixButtons,
@@ -107,7 +113,7 @@ export const Autocomplete = <V,>({
107
113
  className={cx(
108
114
  control({ size }),
109
115
  wrapperBase,
110
- wrapperStyles({ rounded, size }),
116
+ wrapperStyles({ filled, rounded, size }),
111
117
  )}
112
118
  onMouseDown={focusControlOnMouseDown}
113
119
  ref={attach}
@@ -211,6 +217,15 @@ const wrapperStyles = cva({
211
217
  { css: { paddingInline: 11.5 }, rounded: true, size: "4xl" },
212
218
  ],
213
219
  variants: {
220
+ // `wrapperBase` still draws the hover and focus edges.
221
+ filled: {
222
+ false: {},
223
+ true: {
224
+ backgroundColor: "card",
225
+ borderColor: "transparent",
226
+ borderRadius: "lg",
227
+ },
228
+ },
214
229
  rounded: {
215
230
  true: { borderRadius: "full" },
216
231
  },
@@ -820,13 +820,16 @@ const sidebarStyles = flex({
820
820
 
821
821
  const placesStyles = flex({
822
822
  "& > button": {
823
+ // 13px, the desktop's text size, as the folder's rows.
824
+ fontSize: "0.8125rem",
823
825
  gap: 2.5,
824
826
  inlineSize: "100%",
825
827
  justifyContent: "flex-start",
826
828
  },
827
829
  "& > button[aria-current=location]": {
828
- backgroundColor: "color-mix(in oklab, {colors.accent} 16%, transparent)",
829
- color: "accent",
830
+ backgroundColor:
831
+ "color-mix(in oklab, {colors.foreground} 10%, transparent)",
832
+ color: "foreground",
830
833
  fontWeight: "semibold",
831
834
  },
832
835
  direction: "column",
@@ -883,7 +886,11 @@ const crumbSeparatorStyles = css({
883
886
  });
884
887
 
885
888
  const resultsStyles = css({
886
- backgroundColor: "color-mix(in oklab, {colors.background} 30%, transparent)",
889
+ // White in light mode, as a document is.
890
+ backgroundColor: {
891
+ _light: "color-mix(in oklab, {colors.white} 80%, transparent)",
892
+ base: "color-mix(in oklab, {colors.background} 30%, transparent)",
893
+ },
887
894
  border: "1px solid {colors.border}",
888
895
  borderRadius: "lg",
889
896
  flex: "1 1 auto",
@@ -902,7 +909,8 @@ const columnsStyles = grid({
902
909
  backgroundColor: "color-mix(in oklab, {colors.card} 70%, transparent)",
903
910
  color: "textTertiary",
904
911
  columnGap: 3,
905
- fontSize: "2xs",
912
+ // 11px, which has no scale token.
913
+ fontSize: "0.6875rem",
906
914
  fontWeight: "semibold",
907
915
  gridTemplateColumns: "minmax(0, 1fr) {spacing.32}",
908
916
  insetBlockStart: 0,
@@ -927,21 +935,14 @@ const rowStyles = grid({
927
935
  _hoverEnabled: {
928
936
  backgroundColor: "color-mix(in oklab, {colors.foreground} 5%, transparent)",
929
937
  },
930
- // The tile color shows the file kind. Tones that blend two hues mix in
931
- // OKLCH, since OKLab mixing passes through gray.
932
- "& [data-row-tile]": {
933
- backgroundColor: "color-mix(in oklab, var(--tone) 16%, transparent)",
934
- borderColor: "color-mix(in oklab, var(--tone) 26%, transparent)",
935
- color: "var(--tone)",
936
- },
938
+ "--tone": "{colors.muted}",
939
+ // A plain glyph, teal for a folder and gray for anything else, so folders
940
+ // stand out without a tile behind every row.
941
+ "& [data-row-tile]": { color: "var(--tone)" },
942
+ // A neutral wash: color is kept for focus and the main action.
937
943
  "&[data-highlighted]": {
938
- backgroundImage:
939
- "linear-gradient(to right, color-mix(in oklab, {colors.accent} 30%, transparent), color-mix(in oklab, {colors.accent} 8%, transparent))",
940
- },
941
- "&[data-highlighted] [data-row-tile]": {
942
- backgroundColor: "color-mix(in oklab, {colors.accent} 28%, transparent)",
943
- borderColor: "color-mix(in oklab, {colors.accent} 45%, transparent)",
944
- color: "accent",
944
+ backgroundColor:
945
+ "color-mix(in oklab, {colors.foreground} 10%, transparent)",
945
946
  },
946
947
  "&[data-marked] [data-row-tile]": {
947
948
  backgroundColor: "accent",
@@ -952,24 +953,8 @@ const rowStyles = grid({
952
953
  "&[data-replaced]": {
953
954
  backgroundColor: "color-mix(in oklab, {colors.warning} 14%, transparent)",
954
955
  },
955
- "&[data-tone=archive]": { "--tone": "{colors.warning}" },
956
- "&[data-tone=audio]": {
957
- "--tone": "color-mix(in oklch, {colors.warning} 50%, {colors.danger})",
958
- },
959
- "&[data-tone=code]": { "--tone": "{colors.success}" },
960
- "&[data-tone=document]": {
961
- "--tone": "color-mix(in oklab, {colors.accent} 45%, {colors.foreground})",
962
- },
963
956
  "&[data-tone=folder]": { "--tone": "{colors.accent}" },
964
- "&[data-tone=image]": {
965
- "--tone": "color-mix(in oklch, {colors.danger} 55%, {colors.accent})",
966
- },
967
- "&[data-tone=other]": { "--tone": "{colors.muted}" },
968
- "&[data-tone=parent]": { "--tone": "{colors.muted}", color: "muted" },
969
- "&[data-tone=pdf]": { "--tone": "{colors.danger}" },
970
- "&[data-tone=video]": {
971
- "--tone": "color-mix(in oklch, {colors.danger} 80%, {colors.warning})",
972
- },
957
+ "&[data-tone=parent]": { color: "muted" },
973
958
  alignItems: "center",
974
959
  borderRadius: "md",
975
960
  color: "foreground",
@@ -994,7 +979,8 @@ const rowTileStyles = css({
994
979
  });
995
980
 
996
981
  const rowNameStyles = css({
997
- fontSize: "sm",
982
+ // 13px, the desktop's text size, between the scale's 12px and 14px.
983
+ fontSize: "0.8125rem",
998
984
  fontWeight: "medium",
999
985
  overflow: "hidden",
1000
986
  textOverflow: "ellipsis",
@@ -17,6 +17,11 @@ export { SIZES, type Size } from "../control-sizes";
17
17
 
18
18
  type CommonProps = Partial<ControlVariant> & {
19
19
  clearable?: boolean | undefined;
20
+ /**
21
+ * No box: just the text and its icons, for a field that heads a panel. It
22
+ * draws no edge, fill or focus ring.
23
+ */
24
+ flush?: boolean | undefined;
20
25
  prefixIcon?: ReactNode | undefined;
21
26
  // Narrower than base-ui's `Ref<HTMLElement>` since this always renders an
22
27
  // `<input>`. Ref objects are invariant, so the wider type would need a cast.
@@ -36,6 +41,7 @@ type Props = ExtendProps<
36
41
 
37
42
  export const Input = ({
38
43
  clearable = false,
44
+ flush = false,
39
45
  prefixIcon,
40
46
  rounded = false,
41
47
  size = "md",
@@ -74,8 +80,9 @@ export const Input = ({
74
80
  className={cx(
75
81
  control({ size }),
76
82
  wrapperBase,
77
- wrapperStyles({ rounded, size }),
83
+ wrapperStyles({ flush, rounded, size }),
78
84
  )}
85
+ data-flush={flush ? "" : undefined}
79
86
  onMouseDown={focusControlOnMouseDown}
80
87
  style={controlSizingStyle({ width })}
81
88
  >
@@ -133,6 +140,20 @@ const wrapperStyles = cva({
133
140
  { css: { paddingInline: 11.5 }, rounded: true, size: "4xl" },
134
141
  ],
135
142
  variants: {
143
+ // `wrapperBase` and glass surfaces skip their edges and fills for
144
+ // `[data-flush]`.
145
+ // Under `[data-flush]`, to outrank `wrapperBase`'s own fill and edge.
146
+ flush: {
147
+ false: {},
148
+ true: {
149
+ "&[data-flush]": {
150
+ backgroundColor: "transparent",
151
+ borderColor: "transparent",
152
+ borderRadius: 0,
153
+ outlineStyle: "none",
154
+ },
155
+ },
156
+ },
136
157
  rounded: {
137
158
  true: { borderRadius: "full" },
138
159
  },
@@ -31,8 +31,8 @@ export const SURFACES = ["card", "glass"] as const;
31
31
  export type Surface = (typeof SURFACES)[number];
32
32
 
33
33
  /**
34
- * The popup's width. `lg` fits rows with several columns; `xl` fits a list
35
- * beside a preview.
34
+ * The popup's width. `lg` fits rows with several columns, or a compact list
35
+ * beside a preview; `xl` fits a roomy list beside a preview.
36
36
  */
37
37
  export const SIZES = ["md", "lg", "xl"] as const;
38
38
  export type Size = (typeof SIZES)[number];
@@ -68,6 +68,11 @@ type Props = ExtendProps<
68
68
  size?: Size | undefined;
69
69
  surface?: Surface | undefined;
70
70
  title?: ReactNode | undefined;
71
+ /**
72
+ * Whether `title` is for screen readers only, as when the popup's first
73
+ * control already says what it is for.
74
+ */
75
+ titleHidden?: boolean | undefined;
71
76
  trigger?: ReactElement | undefined;
72
77
  }
73
78
  >;
@@ -83,6 +88,7 @@ const ModalDialogComponent = ({
83
88
  size = "md",
84
89
  surface = "card",
85
90
  title,
91
+ titleHidden = false,
86
92
  trigger,
87
93
  ...rootProps
88
94
  }: Props) => {
@@ -119,7 +125,12 @@ const ModalDialogComponent = ({
119
125
  finalFocus={returnFocus}
120
126
  onKeyDown={moveFooterFocus}
121
127
  >
122
- {title !== undefined && (
128
+ {title !== undefined && titleHidden && (
129
+ <BaseDialog.Title className={hiddenTitleStyles}>
130
+ {title}
131
+ </BaseDialog.Title>
132
+ )}
133
+ {title !== undefined && !titleHidden && (
123
134
  <header
124
135
  className={headerStyles({ hasCloseButton: closeButton })}
125
136
  >
@@ -143,7 +154,7 @@ const ModalDialogComponent = ({
143
154
  className={bodyStyles({
144
155
  hasCloseButton: closeButton,
145
156
  hasFooter: footer !== undefined,
146
- hasTitle: title !== undefined,
157
+ hasTitle: title !== undefined && !titleHidden,
147
158
  })}
148
159
  >
149
160
  {children}
@@ -225,7 +236,7 @@ const popupStyles = flex({
225
236
  marginBlockStart: "8cqh",
226
237
  },
227
238
  "&[data-size=lg]": {
228
- inlineSize: "min({spacing.180}, 92cqw)",
239
+ inlineSize: "min({spacing.220}, 92cqw)",
229
240
  },
230
241
  "&[data-size=xl]": {
231
242
  inlineSize: "min({spacing.320}, 92cqw)",
@@ -241,20 +252,24 @@ const popupStyles = flex({
241
252
  position: "absolute",
242
253
  },
243
254
  backdropFilter: "blur({spacing.5}) saturate(180%)",
244
- backgroundColor: "color-mix(in oklab, {colors.card} 62%, transparent)",
255
+ // Near white in light mode: the light card through blur reads as gray.
256
+ backgroundColor: {
257
+ _light: "color-mix(in oklab, {colors.white} 86%, transparent)",
258
+ base: "color-mix(in oklab, {colors.card} 62%, transparent)",
259
+ },
245
260
  borderColor: "color-mix(in oklab, {colors.foreground} 16%, transparent)",
246
261
  },
247
262
  // Makes form controls translucent on glass. Controls mark their element
248
263
  // with `data-control`, so its parent is the wrapper that paints the
249
264
  // background. The descendant selector outranks the wrapper's own class
250
265
  // regardless of order (see `_control/wrapperBase.ts`).
251
- "&[data-surface=glass] :has(> [data-control])": {
266
+ "&[data-surface=glass] :has(> [data-control]):not([data-flush])": {
252
267
  backgroundColor:
253
268
  "color-mix(in oklab, {colors.background} 45%, transparent)",
254
269
  },
255
270
  backgroundColor: "card",
256
271
  border: "1px solid {colors.border}",
257
- borderRadius: "lg",
272
+ borderRadius: "0.875rem",
258
273
  boxShadow: "modal",
259
274
  direction: "column",
260
275
  inlineSize: "min({spacing.120}, 92cqw)",
@@ -293,6 +308,8 @@ const titleStyles = css({
293
308
  margin: 0,
294
309
  });
295
310
 
311
+ const hiddenTitleStyles = css({ srOnly: true });
312
+
296
313
  const closeStyles = css({
297
314
  insetBlockStart: 3,
298
315
  insetInlineEnd: 3,
@@ -13,44 +13,56 @@ type Props = {
13
13
  };
14
14
 
15
15
  /** A yes/no question from an application. Dismissing it denies. */
16
- export const AccessDialog = ({ answer, asker, body, screen }: Props) => (
17
- <ModalDialog
18
- closeButton={false}
19
- footer={
20
- <>
21
- <Button
22
- onClick={() => {
23
- answer(Answer.Canceled());
24
- }}
25
- variant="outline"
26
- >
27
- {body.denyLabel ?? "Deny"}
28
- </Button>
29
- <Button
30
- onClick={() => {
31
- answer(Answer.Access());
32
- }}
33
- variant={body.destructive ? "danger" : "primary"}
34
- >
35
- {body.grantLabel ?? "Allow"}
36
- </Button>
37
- </>
38
- }
39
- onOpenChange={(open) => {
40
- if (!open) {
41
- answer(Answer.Canceled());
16
+ export const AccessDialog = ({ answer, asker, body, screen }: Props) => {
17
+ // One line under the title. The subtitle mostly restates the title, so the
18
+ // body wins when there is one.
19
+ const detail = [body.body, body.subtitle].find((text) => text !== "");
20
+ return (
21
+ <ModalDialog
22
+ closeButton={false}
23
+ footer={
24
+ <>
25
+ {asker !== undefined && (
26
+ <p className={askerStyles}>{`${asker} asks`}</p>
27
+ )}
28
+ <Button
29
+ onClick={() => {
30
+ answer(Answer.Canceled());
31
+ }}
32
+ variant="outline"
33
+ >
34
+ {body.denyLabel ?? "Deny"}
35
+ </Button>
36
+ <Button
37
+ onClick={() => {
38
+ answer(Answer.Access());
39
+ }}
40
+ variant={body.destructive ? "danger" : "accent"}
41
+ >
42
+ {body.grantLabel ?? "Allow"}
43
+ </Button>
44
+ </>
42
45
  }
43
- }}
44
- open
45
- screen={screen}
46
- title={body.title}
47
- >
48
- {asker !== undefined && <p className={askerStyles}>{`${asker} asks`}</p>}
49
- {body.subtitle !== "" && <p className={textStyles}>{body.subtitle}</p>}
50
- {body.body !== "" && <p className={textStyles}>{body.body}</p>}
51
- </ModalDialog>
52
- );
46
+ onOpenChange={(open) => {
47
+ if (!open) {
48
+ answer(Answer.Canceled());
49
+ }
50
+ }}
51
+ open
52
+ screen={screen}
53
+ title={body.title}
54
+ >
55
+ {detail !== undefined && <p className={detailStyles}>{detail}</p>}
56
+ </ModalDialog>
57
+ );
58
+ };
53
59
 
54
- const askerStyles = css({ color: "muted", fontSize: "sm", margin: 0 });
60
+ // Who asks sits opposite the buttons, leaving the body one line.
61
+ const askerStyles = css({
62
+ color: "muted",
63
+ fontSize: "0.8125rem",
64
+ margin: 0,
65
+ marginInlineEnd: "auto",
66
+ });
55
67
 
56
- const textStyles = css({ color: "foreground", margin: 0 });
68
+ const detailStyles = css({ color: "muted", margin: 0 });
@@ -7,7 +7,8 @@ import { css } from "../../styled-system/css";
7
7
  * it, marked `data-control`. Selectors always target `[data-control]` so
8
8
  * suffix buttons can't affect the wrapper; a disabled Send button must not
9
9
  * gray out the textarea. Precedence is disabled, invalid, focused, hovered,
10
- * enforced by the `:not(...)` chains.
10
+ * enforced by the `:not(...)` chains. A `[data-flush]` wrapper (a flush
11
+ * `Input`) draws no hover or focus edge.
11
12
  *
12
13
  * The wrapper looks focused when the control has `:focus`,
13
14
  * `[data-popup-open]` (a Select popup holds focus), or `[data-active]`
@@ -31,7 +32,7 @@ export const wrapperBase = css({
31
32
  "&:has([data-control]:is(:focus, [data-popup-open], [data-active]))": {
32
33
  color: "foreground",
33
34
  },
34
- "&:has([data-control]:is(:focus, [data-popup-open], [data-active])):not(:has([data-control][data-invalid]))":
35
+ "&:has([data-control]:is(:focus, [data-popup-open], [data-active])):not(:has([data-control][data-invalid])):not([data-flush])":
35
36
  {
36
37
  borderColor: "accent",
37
38
  outlineColor: "accent",
@@ -65,7 +66,7 @@ export const wrapperBase = css({
65
66
  "&:has([data-control][data-resizing]) [data-resize-handle]": {
66
67
  color: "muted",
67
68
  },
68
- "&:is(:hover, :has([data-control][data-resizing])):not(:has([data-control]:disabled)):not(:has([data-control]:is(:focus, [data-popup-open], [data-active]))):not(:has([data-control][data-invalid]))":
69
+ "&:is(:hover, :has([data-control][data-resizing])):not(:has([data-control]:disabled)):not(:has([data-control]:is(:focus, [data-popup-open], [data-active]))):not(:has([data-control][data-invalid])):not([data-flush])":
69
70
  {
70
71
  borderColor: "muted",
71
72
  },