@domicile-desktop/component-library 0.0.0-alpha-e42226f4af44 → 0.0.0-alpha-fff3e247bb02

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-e42226f4af44",
6
- "@domicile-desktop/system-apps": "0.0.0-alpha-e42226f4af44",
5
+ "@domicile-desktop/sdk": "0.0.0-alpha-fff3e247bb02",
6
+ "@domicile-desktop/system-apps": "0.0.0-alpha-fff3e247bb02",
7
7
  "@pandacss/dev": "^1.12.1",
8
8
  "@phosphor-icons/react": "^2.1.10"
9
9
  },
@@ -52,6 +52,7 @@
52
52
  "./folders": "./src/FilePicker/folders.ts",
53
53
  "./Input": "./src/Input/Input.tsx",
54
54
  "./Kbd": "./src/Kbd/Kbd.tsx",
55
+ "./LevelToast": "./src/LevelToast/LevelToast.tsx",
55
56
  "./list-walk": "./src/list-walk.ts",
56
57
  "./ModalDialog": "./src/ModalDialog/ModalDialog.tsx",
57
58
  "./media-theme-source": "./src/ThemeSwitch/media-theme-source.ts",
@@ -115,5 +116,5 @@
115
116
  "test:unit": "bun test"
116
117
  },
117
118
  "type": "module",
118
- "version": "0.0.0-alpha-e42226f4af44"
119
+ "version": "0.0.0-alpha-fff3e247bb02"
119
120
  }
@@ -0,0 +1,152 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { useState } from "react";
3
+ import { css } from "../../styled-system/css";
4
+ import { circle, hstack } from "../../styled-system/patterns";
5
+
6
+ type Props = {
7
+ /**
8
+ * How many times the level changed. A new count keeps the toast up for
9
+ * another full hold, or shows it again once it has gone.
10
+ */
11
+ changes: number;
12
+ /** Drawn large before the label. */
13
+ icon: ReactNode;
14
+ /** What the level is of, such as "Volume". */
15
+ label: string;
16
+ /** From 0 to 1. */
17
+ level: number;
18
+ /** Says "Muted" and grays the meter, such as for muted audio. */
19
+ muted: boolean;
20
+ };
21
+
22
+ /**
23
+ * A large toast showing a level, such as volume or brightness, that fades out
24
+ * a moment after its last change.
25
+ *
26
+ * A change while it is up updates it in place and starts the hold over, so
27
+ * holding a key does not replay its entrance. It hides on its own
28
+ * `animationend`, not a timer, so the duration lives only in the stylesheet.
29
+ * The caller positions it; it ignores the pointer.
30
+ */
31
+ export const LevelToast = ({ changes, icon, label, level, muted }: Props) => {
32
+ const [goneAfter, setGoneAfter] = useState<number | undefined>();
33
+
34
+ return goneAfter === changes ? undefined : (
35
+ <output
36
+ className={toastStyles}
37
+ // Alternates the fade-out between two copies of one keyframe, so the
38
+ // browser restarts it while the entrance, unchanged, does not replay.
39
+ data-again={changes % 2 === 0 ? "" : undefined}
40
+ data-muted={muted ? "" : undefined}
41
+ onAnimationEnd={(event) => {
42
+ if (event.target === event.currentTarget) {
43
+ setGoneAfter(changes);
44
+ }
45
+ }}
46
+ >
47
+ <span className={panelStyles}>
48
+ <span className={iconStyles}>{icon}</span>
49
+ <span className={readoutStyles}>
50
+ <span className={labelRowStyles}>
51
+ <span>{label}</span>
52
+ <span className={valueStyles}>
53
+ {muted ? "Muted" : `${String(Math.round(level * 100))}%`}
54
+ </span>
55
+ </span>
56
+ <span
57
+ aria-valuemax={100}
58
+ aria-valuemin={0}
59
+ aria-valuenow={Math.round(level * 100)}
60
+ className={meterStyles}
61
+ data-muted={muted ? "" : undefined}
62
+ role="meter"
63
+ // Inline because the level is a runtime value Panda cannot extract.
64
+ style={{ "--level-toast-level": level } as CSSProperties}
65
+ />
66
+ </span>
67
+ </span>
68
+ </output>
69
+ );
70
+ };
71
+
72
+ // Holds, then fades the whole toast out.
73
+ const toastStyles = css({
74
+ "&[data-again]": {
75
+ animationName: "levelToastHoldAgain",
76
+ },
77
+ animation: "levelToastHold {durations.levelToast} {easings.linear} forwards",
78
+ display: "block",
79
+ pointerEvents: "none",
80
+ });
81
+
82
+ // Rises in once per showing. A frosted card like `Toaster`'s.
83
+ const panelStyles = hstack({
84
+ animation: "levelToastEnter {durations.normal} {easings.outQuart}",
85
+ backdropFilter: "blur({spacing.4}) saturate(160%)",
86
+ backgroundColor: "color-mix(in oklab, {colors.card} 85%, transparent)",
87
+ border: "1px solid color-mix(in oklab, {colors.foreground} 12%, transparent)",
88
+ borderRadius: "3xl",
89
+ boxShadow: "lifted",
90
+ color: "foreground",
91
+ gap: 4,
92
+ inlineSize: 88,
93
+ paddingBlock: 4,
94
+ paddingInline: 5,
95
+ });
96
+
97
+ const iconStyles = circle({
98
+ "[data-muted] &": {
99
+ backgroundColor:
100
+ "color-mix(in oklab, {colors.foreground} 10%, transparent)",
101
+ color: "muted",
102
+ },
103
+ backgroundColor: "color-mix(in oklab, {colors.accent} 18%, transparent)",
104
+ color: "accent",
105
+ flexShrink: 0,
106
+ size: 12,
107
+ transition:
108
+ "color {durations.fast} {easings.out}, background-color {durations.fast} {easings.out}",
109
+ });
110
+
111
+ const readoutStyles = css({
112
+ display: "flex",
113
+ flex: 1,
114
+ flexDirection: "column",
115
+ gap: 2.5,
116
+ minInlineSize: 0,
117
+ });
118
+
119
+ const labelRowStyles = hstack({
120
+ fontSize: "md",
121
+ fontWeight: "medium",
122
+ justify: "space-between",
123
+ });
124
+
125
+ const valueStyles = css({
126
+ fontVariantNumeric: "tabular-nums",
127
+ fontWeight: "semibold",
128
+ });
129
+
130
+ // A track filled from the start edge. The fill eases to each new level, which
131
+ // shows while a key is held.
132
+ const meterStyles = css({
133
+ "&::after": {
134
+ backgroundColor: "accent",
135
+ borderRadius: "full",
136
+ content: '""',
137
+ inlineSize: "calc(var(--level-toast-level) * 100%)",
138
+ insetBlock: 0,
139
+ insetInlineStart: 0,
140
+ position: "absolute",
141
+ transition:
142
+ "inline-size {durations.fast} {easings.out}, background-color {durations.fast} {easings.out}",
143
+ },
144
+ "&[data-muted]::after": {
145
+ backgroundColor: "muted",
146
+ },
147
+ backgroundColor: "color-mix(in oklab, {colors.foreground} 14%, transparent)",
148
+ blockSize: 2,
149
+ borderRadius: "full",
150
+ overflow: "hidden",
151
+ position: "relative",
152
+ });
@@ -25,6 +25,13 @@ const quarterStepSpacing = Object.fromEntries(
25
25
  ),
26
26
  );
27
27
 
28
+ // `LevelToast` holds, then fades out over the last fifth.
29
+ const levelToastHold = {
30
+ "0%": { opacity: "1" },
31
+ "80%": { opacity: "1" },
32
+ "100%": { opacity: "0" },
33
+ };
34
+
28
35
  export const domicilePreset = definePreset({
29
36
  conditions: {
30
37
  extend: {
@@ -153,6 +160,18 @@ export const domicilePreset = definePreset({
153
160
  theme: {
154
161
  extend: {
155
162
  keyframes: {
163
+ // `LevelToast`'s entrance, once per showing.
164
+ levelToastEnter: {
165
+ "0%": {
166
+ opacity: "0",
167
+ transform: "translateY({spacing.3}) scale(0.96)",
168
+ },
169
+ "100%": { opacity: "1", transform: "none" },
170
+ },
171
+ // `LevelToast`'s hold and fade-out. Two copies so a change can
172
+ // restart it by switching names.
173
+ levelToastHold,
174
+ levelToastHoldAgain: levelToastHold,
156
175
  // `Notice`: appears quickly, holds long enough to read, then fades
157
176
  // out on its own.
158
177
  noticeShown: {
@@ -405,6 +424,8 @@ export const domicilePreset = definePreset({
405
424
  durations: {
406
425
  // Wallpaper crossfade. Long because nothing waits on it.
407
426
  crossfade: { value: "2s" },
427
+ // How long a `LevelToast` stays up after its last change.
428
+ levelToast: { value: "2s" },
408
429
  // How long a `Notice` stays up. Longer than the scale above, which
409
430
  // is for control feedback, not reading.
410
431
  notice: { value: "1.5s" },
@@ -1307,6 +1307,10 @@ const tokens = {
1307
1307
  "value": "2s",
1308
1308
  "variable": "var(--gXQUBH)"
1309
1309
  },
1310
+ "durations.levelToast": {
1311
+ "value": "2s",
1312
+ "variable": "var(--gbbHhs)"
1313
+ },
1310
1314
  "durations.notice": {
1311
1315
  "value": "1.5s",
1312
1316
  "variable": "var(--infmmp)"
@@ -27,7 +27,7 @@ export type AnimationToken = "spin" | "ping" | "pulse" | "bounce"
27
27
 
28
28
  export type BorderWidthToken = "fauxBold"
29
29
 
30
- export type DurationToken = "fastest" | "faster" | "fast" | "normal" | "slow" | "slower" | "slowest" | "crossfade" | "notice" | "pulse" | "spin" | "wipe"
30
+ export type DurationToken = "fastest" | "faster" | "fast" | "normal" | "slow" | "slower" | "slowest" | "crossfade" | "levelToast" | "notice" | "pulse" | "spin" | "wipe"
31
31
 
32
32
  export type EasingToken = "default" | "linear" | "in" | "out" | "in-out" | "emphasized" | "outBack" | "outQuart"
33
33
 
@@ -169,7 +169,7 @@ export interface UtilityValues {
169
169
  transitionProperty: "common" | "colors" | "size" | "position" | "background";
170
170
  transition: "all" | "common" | "size" | "position" | "background" | "colors" | "opacity" | "shadow" | "transform";
171
171
  animation: Tokens["animations"];
172
- animationName: "ping" | "bounce" | "noticeShown" | "pulse" | "spin" | "themeSwipeFromBottom" | "themeSwipeFromTop" | "toastCountdown";
172
+ animationName: "ping" | "bounce" | "levelToastEnter" | "levelToastHold" | "levelToastHoldAgain" | "noticeShown" | "pulse" | "spin" | "themeSwipeFromBottom" | "themeSwipeFromTop" | "toastCountdown";
173
173
  animationTimingFunction: Tokens["easings"];
174
174
  animationDuration: Tokens["durations"];
175
175
  animationDelay: Tokens["durations"];