@stealthscale/component-actions 0.1.0 → 0.2.1

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.
@@ -0,0 +1,317 @@
1
+ import { PALETTES, axis, below, controlSizes, defineRecipe, defineSlotRecipe, dense, interactive, liftVariants, lookVariants, motion, onSlot, onSlots, paletteVariants, sizeVariants, touchTarget } from "@stealthscale/theme/authoring";
2
+ //#region src/button/recipe.ts
3
+ /**
4
+ * Declares the button recipe: the base styles and the axes a caller sets.
5
+ *
6
+ * @remarks
7
+ * Every value is a token, so a theme moves all of them. The border is transparent at the control
8
+ * width in every look, so the outline look is the same size as the others. A press changes the
9
+ * fill, spreads the ripple and lowers the elevation, and never moves the box, because a target
10
+ * that shifts under the pointer is easy to miss. `staticCss` lists `shape="square"` and every
11
+ * palette. The icon button sets the shape through a default prop, and callers set the palette
12
+ * through a provider or from data, so the compiler extracts neither from source.
13
+ */
14
+ /**
15
+ * Returns the fill of a button that is on under forced colors: `Highlight` behind `HighlightText`.
16
+ *
17
+ * @remarks
18
+ * Forced colors replace every background and remove every shadow, so without it a button that is
19
+ * on reads the same as one that is off.
20
+ */
21
+ function forcedOn() {
22
+ return { _highContrast: {
23
+ background: "Highlight",
24
+ color: "HighlightText",
25
+ forcedColorAdjust: "none"
26
+ } };
27
+ }
28
+ /**
29
+ * Returns the `_currentPage` and `_pressed` styles of a look with no fill of its own.
30
+ *
31
+ * @remarks
32
+ * A button is on when it has `aria-pressed="true"` or `aria-current="page"`, so the fill matches
33
+ * what a screen reader announces. The styles go in a compound variant, not the base, because the
34
+ * compiler emits a look's background in a later cascade layer than the base, and a later layer
35
+ * applies over an earlier one at any specificity. Pass `colorPalette.subtle` for a look with no
36
+ * resting fill and `colorPalette.muted` for a look that rests on the subtle fill, so the on state
37
+ * is one step deeper in both. A pressed button's edge is the palette's emphasized role, one step
38
+ * darker than the outline look's muted edge.
39
+ */
40
+ function on(background) {
41
+ return {
42
+ _currentPage: {
43
+ background,
44
+ color: "colorPalette.fg",
45
+ fontWeight: "semibold",
46
+ ...forcedOn()
47
+ },
48
+ _pressed: {
49
+ background,
50
+ borderColor: "colorPalette.emphasized",
51
+ color: "colorPalette.fg",
52
+ ...forcedOn()
53
+ }
54
+ };
55
+ }
56
+ /**
57
+ * Returns the `_currentPage` and `_pressed` styles of the solid look.
58
+ *
59
+ * @remarks
60
+ * The solid look has no deeper fill, and any other fill matches its hover fill. The on state is
61
+ * the theme's inset line and the semibold weight. `:active` is the momentary press.
62
+ */
63
+ function marked() {
64
+ return {
65
+ _currentPage: {
66
+ boxShadow: "inset",
67
+ fontWeight: "semibold",
68
+ ...forcedOn()
69
+ },
70
+ _pressed: {
71
+ boxShadow: "inset",
72
+ fontWeight: "semibold",
73
+ ...forcedOn()
74
+ }
75
+ };
76
+ }
77
+ /**
78
+ * Styles a button, defaulting to the solid look at the md size in the primary palette.
79
+ */
80
+ const recipe$2 = defineRecipe({
81
+ base: {
82
+ ...interactive(),
83
+ ...touchTarget(),
84
+ layerStyle: "ripple",
85
+ alignItems: "center",
86
+ appearance: "none",
87
+ borderColor: "transparent",
88
+ borderRadius: "l2",
89
+ borderWidth: "control",
90
+ colorPalette: "primary",
91
+ display: "inline-flex",
92
+ flexShrink: "0",
93
+ fontWeight: "medium",
94
+ justifyContent: "center",
95
+ verticalAlign: "middle",
96
+ whiteSpace: "nowrap"
97
+ },
98
+ className: "button",
99
+ compoundVariants: [
100
+ (
101
+ /**
102
+ * Zeroes the inline padding of a square button.
103
+ *
104
+ * @remarks
105
+ * The compiler emits the size axis after the shape axis, and the size axis sets the padding,
106
+ * so the reset is a compound. `aspect-ratio` fixes the width, so padding only narrows the
107
+ * content box: at md, 16px each side leaves 6px for a 16px icon.
108
+ */
109
+ {
110
+ css: { paddingInline: "0" },
111
+ name: "squared",
112
+ shape: "square"
113
+ }),
114
+ {
115
+ css: on("colorPalette.subtle"),
116
+ name: "on",
117
+ variant: [
118
+ "ghost",
119
+ "glass",
120
+ "outline",
121
+ "plain"
122
+ ]
123
+ },
124
+ {
125
+ css: on("colorPalette.muted"),
126
+ name: "on-deeper",
127
+ variant: ["subtle", "surface"]
128
+ },
129
+ {
130
+ css: marked(),
131
+ name: "on-marked",
132
+ variant: ["solid"]
133
+ }
134
+ ],
135
+ defaultVariants: {
136
+ size: "md",
137
+ variant: "solid"
138
+ },
139
+ jsx: [/Button$/u],
140
+ staticCss: [{ shape: ["square"] }, { palette: [...PALETTES] }],
141
+ variants: {
142
+ /**
143
+ * The halo around the button, in the palette's solid at half opacity.
144
+ *
145
+ * @remarks
146
+ * `pulse` sets `boxShadowColor` because its keyframe writes the whole `box-shadow`, which
147
+ * replaces a static glow. The axis offers no moving border. `border.moving` paints over the
148
+ * padding box and hides the look's fill, and a ring without it needs a pseudo-element, but
149
+ * `::after` renders the ripple and `::before` the touch target.
150
+ */
151
+ effect: {
152
+ glow: { layerStyle: "glow.md" },
153
+ pulse: {
154
+ animationStyle: "pulse-glow",
155
+ boxShadowColor: "colorPalette.solid/50"
156
+ }
157
+ },
158
+ elevation: liftVariants(),
159
+ /**
160
+ * Palette of every look. The base sets `primary`.
161
+ *
162
+ * @remarks
163
+ * `neutral` renders the control in the surrounding text ink, for a button in a toolbar.
164
+ */
165
+ palette: paletteVariants(),
166
+ shape: { square: { aspectRatio: "square" } },
167
+ size: controlSizes(),
168
+ variant: {
169
+ ...lookVariants(),
170
+ glass: { layerStyle: "glass" }
171
+ }
172
+ }
173
+ });
174
+ //#endregion
175
+ //#region src/clipboard/recipe.ts
176
+ /**
177
+ * Declares the slot recipe of the clipboard: layout and spacing for its seven parts.
178
+ *
179
+ * @remarks
180
+ * The trigger has no control styles. A caller renders it as the library's `Button` or
181
+ * `IconButton` through `as`, so it matches every other button in a theme. The clipboard has no
182
+ * colour or surface of its own, so it offers no `palette` or `effect` axis. The button it renders
183
+ * through `as` carries both. The root aligns its children at the start, so a trigger on its own
184
+ * keeps its button width. The control row stretches to the width of the root, so a field inside
185
+ * it fills the row.
186
+ */
187
+ /**
188
+ * Sizes the label text and the gaps step through.
189
+ */
190
+ const STEPS = [
191
+ "sm",
192
+ "md",
193
+ "lg"
194
+ ];
195
+ /**
196
+ * Slot styles and the size axis. Defaults to `md`.
197
+ */
198
+ const recipe$1 = defineSlotRecipe({
199
+ base: {
200
+ control: {
201
+ alignItems: "center",
202
+ alignSelf: "stretch",
203
+ display: "flex"
204
+ },
205
+ indicator: {
206
+ alignItems: "center",
207
+ display: "inline-flex",
208
+ flexShrink: "0",
209
+ justifyContent: "center"
210
+ },
211
+ label: {
212
+ color: "fg",
213
+ display: "block"
214
+ },
215
+ root: {
216
+ alignItems: "start",
217
+ display: "flex",
218
+ flexDirection: "column"
219
+ }
220
+ },
221
+ className: "clipboard",
222
+ defaultVariants: { size: "md" },
223
+ jsx: [/^Clipboard(\.\w+)?$/u],
224
+ slots: [
225
+ "root",
226
+ "label",
227
+ "control",
228
+ "input",
229
+ "trigger",
230
+ "indicator",
231
+ "valueText"
232
+ ],
233
+ variants: {
234
+ /**
235
+ * The label text style and the two gaps.
236
+ *
237
+ * @remarks
238
+ * The gap between the label and the row is one size smaller than the gap between the field
239
+ * and the trigger. The field and the trigger take their own size through their providers.
240
+ */
241
+ size: onSlots({
242
+ control: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${size}}`) }), STEPS),
243
+ label: sizeVariants((size) => ({ textStyle: `label.${size}` }), STEPS),
244
+ root: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(size)}}`) }), STEPS)
245
+ }) }
246
+ });
247
+ //#endregion
248
+ //#region src/swap/recipe.ts
249
+ /**
250
+ * Recipe for a swap: two marks in one place, one of which shows, and the motion that replaces one
251
+ * with the other.
252
+ *
253
+ * @remarks
254
+ * The root is an inline grid and both indicators take its one cell, so the root is as large as
255
+ * the larger mark whichever shows, and nothing beside it moves. An indicator with `data-hidden`
256
+ * keeps its room with `visibility: hidden`, which also takes it out of the accessibility tree and
257
+ * the tab order. The `motion` axis
258
+ * picks the pair of animation styles an indicator enters and leaves with, and the leaving mark
259
+ * and the entering mark run at once. Under reduced motion the animation styles run nothing, so
260
+ * the marks change at once. The recipe has no `palette` and no `size` axis: the marks take their
261
+ * color and their size from the control around them.
262
+ */
263
+ /**
264
+ * Class name of the recipe.
265
+ */
266
+ const CLASS = "swap";
267
+ /**
268
+ * Lists the motions in reading order: the default first, then the others, then none.
269
+ */
270
+ const MOTIONS = [
271
+ "scale",
272
+ "fade",
273
+ "slide",
274
+ "none"
275
+ ];
276
+ /**
277
+ * Maps each motion to the animation styles an indicator enters and leaves with, or to no animation.
278
+ * The scale runs from half a mark's size, so a small icon visibly grows into place.
279
+ */
280
+ const PAIRS = {
281
+ fade: motion("fade.in", "fade.out"),
282
+ none: { animation: "none" },
283
+ scale: {
284
+ ...motion("scale-fade.in", "scale-fade.out"),
285
+ "--scale-distance": "0.5"
286
+ },
287
+ slide: motion("slide-up.in", "slide-up.out")
288
+ };
289
+ /**
290
+ * Defines the swap recipe, which scales the marks in and out by default.
291
+ */
292
+ const recipe = defineSlotRecipe({
293
+ base: {
294
+ indicator: {
295
+ "&[data-hidden]": { visibility: "hidden" },
296
+ alignItems: "center",
297
+ display: "inline-flex",
298
+ gridArea: "1 / 1 / 2 / 2",
299
+ justifyContent: "center"
300
+ },
301
+ root: {
302
+ display: "inline-grid",
303
+ placeItems: "center"
304
+ }
305
+ },
306
+ className: CLASS,
307
+ defaultVariants: { motion: "scale" },
308
+ jsx: [/^Swap(\.\w+)?$/u],
309
+ slots: ["root", "indicator"],
310
+ variants: {
311
+ /**
312
+ * Motion the marks replace each other with.
313
+ */
314
+ motion: onSlot("indicator", axis(MOTIONS, (name) => PAIRS[name])()) }
315
+ });
316
+ //#endregion
317
+ export { recipe$1 as n, recipe$2 as r, recipe as t };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/dist/theme.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  //#region src/theme.d.ts
2
2
  /**
3
- * Publishes the preset that registers every recipe in this package, for an application's compiler
4
- * to install.
3
+ * Publishes the preset an application's style compiler installs to pick up this package's recipes.
5
4
  *
6
5
  * @remarks
7
- * The list is written by hand. The package's own specification reports a recipe file the list
8
- * leaves out, so no generator runs here.
6
+ * The list of recipes is maintained by hand. The package's own specification fails when a recipe
7
+ * file is missing from it, which is why nothing generates the list.
9
8
  */
10
9
  declare const _default: import("@stealthscale/theme/authoring").Preset;
11
10
  //#endregion
package/dist/theme.js CHANGED
@@ -1,17 +1,22 @@
1
- import { t as recipe } from "./recipe-19HSwhAG.js";
1
+ import { n as recipe$1, r as recipe, t as recipe$2 } from "./recipe-BhcK7PNL.js";
2
2
  import { definePreset } from "@stealthscale/theme/authoring";
3
3
  //#region src/theme.ts
4
4
  /**
5
- * Publishes the preset that registers every recipe in this package, for an application's compiler
6
- * to install.
5
+ * Publishes the preset an application's style compiler installs to pick up this package's recipes.
7
6
  *
8
7
  * @remarks
9
- * The list is written by hand. The package's own specification reports a recipe file the list
10
- * leaves out, so no generator runs here.
8
+ * The list of recipes is maintained by hand. The package's own specification fails when a recipe
9
+ * file is missing from it, which is why nothing generates the list.
11
10
  */
12
11
  var theme_default = definePreset({
13
12
  name: "@stealthscale/component-actions",
14
- theme: { extend: { recipes: { button: recipe } } }
13
+ theme: { extend: {
14
+ recipes: { button: recipe },
15
+ slotRecipes: {
16
+ clipboard: recipe$1,
17
+ swap: recipe$2
18
+ }
19
+ } }
15
20
  });
16
21
  //#endregion
17
22
  export { theme_default as default };
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@stealthscale/component-actions",
3
- "version": "0.1.0",
4
- "description": "Draws what a person presses: a button, a toggle, and the triggers that carry an action.",
3
+ "version": "0.2.1",
4
+ "description": "Renders what a person presses: a button, a toggle group, a swap, a color mode toggle, a clipboard and a download trigger.",
5
5
  "keywords": [
6
6
  "actions",
7
7
  "button",
8
8
  "components",
9
9
  "react"
10
10
  ],
11
- "homepage": "https://github.com/stealth-scale/config/tree/main/components/actions#readme",
12
- "bugs": "https://github.com/stealth-scale/config/issues",
11
+ "homepage": "https://github.com/stealth-scale/scale/tree/main/components/actions#readme",
12
+ "bugs": "https://github.com/stealth-scale/scale/issues",
13
13
  "license": "MIT",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "git+https://github.com/stealth-scale/config.git",
16
+ "url": "git+https://github.com/stealth-scale/scale.git",
17
17
  "directory": "components/actions"
18
18
  },
19
19
  "files": [
@@ -34,29 +34,44 @@
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
+ "dependencies": {
38
+ "@stealthscale/component-layout": "^0.2.1",
39
+ "@zag-js/clipboard": "1.45.0",
40
+ "@zag-js/file-utils": "1.45.0",
41
+ "@zag-js/react": "1.45.0",
42
+ "@zag-js/toggle-group": "1.45.0",
43
+ "@zag-js/types": "1.45.0",
44
+ "@zag-js/utils": "1.45.0"
45
+ },
37
46
  "devDependencies": {
38
- "@arethetypeswrong/core": "^0.18.5",
39
- "@rolldown/plugin-babel": "^0.2.4",
40
- "@stealthscale/theme": "^0.3.0",
41
- "@stealthscale/vite-config": "^0.6.0",
42
- "@stealthscale/vite-config-react": "^0.8.0",
43
- "@stealthscale/vite-config-theme": "^0.1.0",
47
+ "@arethetypeswrong/core": "^0.18",
48
+ "@stealthscale/hooks": "^0.3.0",
49
+ "@stealthscale/provider-color-mode": "^0.2.2",
50
+ "@stealthscale/settings": "^0.2.0",
51
+ "@stealthscale/theme": "^0.5.0",
52
+ "@stealthscale/vite-config": "^0.8.0",
53
+ "@stealthscale/vite-config-i18n": "^0.2.1",
54
+ "@stealthscale/vite-config-react": "^0.9.1",
55
+ "@stealthscale/vite-config-specimen": "^0.2.1",
56
+ "@stealthscale/vite-config-theme": "^0.2.1",
44
57
  "@stealthscale/vite-config-typescript": "^0.3.0",
45
- "@testing-library/react": "^16.3.0",
46
- "@types/react": "^19.3.0",
47
- "@types/react-dom": "^19.3.0",
48
- "babel-plugin-react-compiler": "^1.0.0",
49
- "happy-dom": "^20.14.3",
50
- "oxc-transform-react": "^0.149.0",
51
- "publint": "^0.3.24",
52
- "react": "^19.3.0",
53
- "react-dom": "^19.3.0",
54
- "typescript": "^7.0.2",
55
- "vitest": "4.1.11"
58
+ "@testing-library/react": "^16.3",
59
+ "@types/react": "^19.3",
60
+ "@types/react-dom": "^19.3",
61
+ "happy-dom": "^20.14",
62
+ "lucide-react": "^1.52",
63
+ "oxc-transform-react": "^0.153",
64
+ "publint": "^0.3",
65
+ "react": "^19.3",
66
+ "react-dom": "^19.3",
67
+ "typescript": "^7.0",
68
+ "vitest": "5.0.3"
56
69
  },
57
70
  "peerDependencies": {
58
- "@stealthscale/theme": "^0.3.0",
59
- "react": "^19.3.0"
71
+ "@stealthscale/hooks": "^0.3.0",
72
+ "@stealthscale/provider-color-mode": "^0.2.2",
73
+ "@stealthscale/theme": "^0.5.0",
74
+ "react": "^19.3"
60
75
  },
61
76
  "engines": {
62
77
  "node": ">=26.0.0"
@@ -1,70 +0,0 @@
1
- import { controlSizes, defineRecipe, interactive, liftVariants, lookVariants, statusVariants, touchTarget } from "@stealthscale/theme/authoring";
2
- //#region src/button/recipe.ts
3
- /**
4
- * States what a button is: the element a person presses, drawn in a look and a size, in the
5
- * palette of its status, square where it holds one glyph, and glowing where a page asks.
6
- *
7
- * @remarks
8
- * Every value is a layer style, a semantic control height, a semantic inset, a label role or a
9
- * palette, so a theme moves all of them. The interactive fragment gives the cursor, the focus
10
- * ring, the disabled layer and the transitions every control shares, and the touch target widens
11
- * the hit area on a coarse pointer without moving the box. The border is drawn transparent at the
12
- * small width in every look, so the outline look changes its colour and not its size. A press is
13
- * read from the look's own pressed fill and from the elevation dropping under the pointer. The
14
- * box scales rather than shifting, because a control that shifts under a press is one a reader
15
- * can miss. The square is listed under `staticCss`, because the icon button fixes it through a
16
- * default prop and no JSX literal writes it for the compiler to extract.
17
- */
18
- /**
19
- * Draws a button on the primary palette in the solid look and the middle size until a caller says
20
- * otherwise, set inline so it sits in a line of controls.
21
- */
22
- const recipe = defineRecipe({
23
- base: {
24
- ...interactive(),
25
- ...touchTarget(),
26
- alignItems: "center",
27
- appearance: "none",
28
- borderColor: "transparent",
29
- borderRadius: "l2",
30
- borderWidth: "sm",
31
- colorPalette: "primary",
32
- display: "inline-flex",
33
- flexShrink: "0",
34
- fontWeight: "medium",
35
- justifyContent: "center",
36
- verticalAlign: "middle",
37
- whiteSpace: "nowrap"
38
- },
39
- className: "button",
40
- compoundVariants: [{
41
- css: { "&:has(> svg:first-child)": { paddingInline: "0" } },
42
- name: "squared",
43
- shape: "square"
44
- }],
45
- defaultVariants: {
46
- size: "md",
47
- variant: "solid"
48
- },
49
- jsx: [/Button$/u],
50
- staticCss: [{ shape: ["square"] }],
51
- variants: {
52
- effect: {
53
- glow: { layerStyle: "glow.md" },
54
- ripple: { layerStyle: "ripple" }
55
- },
56
- elevation: liftVariants(),
57
- shape: { square: {
58
- aspectRatio: "square",
59
- paddingInline: "0"
60
- } },
61
- size: controlSizes(),
62
- status: statusVariants(),
63
- variant: {
64
- ...lookVariants(),
65
- glass: { layerStyle: "glass" }
66
- }
67
- }
68
- });
69
- //#endregion
70
- export { recipe as t };