@stealthscale/component-screen 0.0.1 → 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,283 @@
1
+ import { below, defineSlotRecipe, dense, interactive, lookVariants, onSlot, onSlots, paletteVariants, sizeVariants, truncate } from "@stealthscale/theme/authoring";
2
+ //#region src/folding/folding.ts
3
+ /**
4
+ * Styles an action that folds to its icon while its row is narrow, for the page, the toolbar and
5
+ * the section.
6
+ *
7
+ * @remarks
8
+ * A secondary action shows its icon alone, and its words remain for a screen reader. A tertiary
9
+ * action renders nothing on a narrow row and runs from the row's menu, which the action registers
10
+ * with. The row measures its own width, so a row beside an open sidebar folds on its own width
11
+ * and the caller does not write a breakpoint.
12
+ */
13
+ /**
14
+ * Attribute an action sets on itself while its own row is narrow.
15
+ *
16
+ * @remarks
17
+ * The action reads the state from its row's context, so a narrow ancestor folds no action of a
18
+ * wider row inside it. A bar fixed to the window is wider than the page around it in the
19
+ * document.
20
+ */
21
+ const NARROW$1 = "data-narrow";
22
+ /**
23
+ * Attribute a toolbar control sets to the id it registers its menu row under, while a narrow row
24
+ * can move it into the row's menu.
25
+ *
26
+ * @remarks
27
+ * The row reads the attribute in document order. It moves the control nearest its end first, and
28
+ * its menu lists the moved controls in the row's order.
29
+ */
30
+ const FOLDS = "data-folds";
31
+ /**
32
+ * Attribute an action sets to its priority.
33
+ *
34
+ * @remarks
35
+ * The priority is an attribute and not an axis, because a slot recipe resolves its variants once,
36
+ * at the root, and each action in a row folds on its own priority.
37
+ */
38
+ const PRIORITY = "data-priority";
39
+ /**
40
+ * Styles an action by its priority while its row is narrow.
41
+ *
42
+ * @remarks
43
+ * Every action keeps its words on one line at every width, because a wrapped control would make
44
+ * the row taller. A secondary action hides its element children other than its icon with
45
+ * `srOnly`, so it keeps its accessible name, and renders as a square around its icon. Its words
46
+ * must be in an element, because a CSS selector cannot select a bare text node. A primary action
47
+ * is unchanged.
48
+ */
49
+ const FOLDING = {
50
+ [`&[${PRIORITY}=secondary]`]: { [`&[${NARROW$1}]`]: {
51
+ "& > :not(svg)": { srOnly: true },
52
+ aspectRatio: "square",
53
+ justifyContent: "center",
54
+ paddingInline: "0"
55
+ } },
56
+ whiteSpace: "nowrap"
57
+ };
58
+ //#endregion
59
+ //#region src/switcher/recipe.ts
60
+ /**
61
+ * Declares the switcher's slot recipe, which styles the control that shows the current workspace,
62
+ * project or environment and opens the menu of the others.
63
+ *
64
+ * @remarks
65
+ * The recipe has six slots. The root slot is the trigger, and the mark, the label, the name, the
66
+ * detail and the indicator are inside it. `Switcher.Root` is the disclosure package's menu, so
67
+ * the menu's recipe styles the panel and its rows. The trigger takes the button's looks and
68
+ * palettes from the same layer styles, so an outlined switcher has the button's light edge. The
69
+ * mark renders an initial, an icon or an avatar in the palette's ink, without a fill. The name
70
+ * and the detail truncate to keep the control at one height. A pair of chevrons marks a control
71
+ * that switches and not a direction, so the indicator does not rotate while the menu is open. In
72
+ * a sidebar closed to icons, and in a narrow toolbar for a switcher with a mark, the control is
73
+ * its mark alone, square. The words remain in the accessible name in both.
74
+ */
75
+ /**
76
+ * Selects a part of a switcher drawn as its mark alone, in a sidebar closed to icons.
77
+ */
78
+ const ICONIC = ".switcher__root[data-iconic] &";
79
+ /**
80
+ * Selects a part of a switcher with a mark in a toolbar too narrow for the name. A switcher
81
+ * without a mark keeps its name, because the name is the only content that identifies it.
82
+ */
83
+ const NARROW = ".switcher__root[data-narrow]:has(.switcher__mark) &";
84
+ /**
85
+ * Custom property the size axis sets to the height of a button at the switcher's size.
86
+ */
87
+ const HEIGHT = "--switcher-height";
88
+ /**
89
+ * Styles a control that is one line as wide as its words: in a toolbar and on its own.
90
+ *
91
+ * @remarks
92
+ * `inlineSize: fit` keeps the control as wide as its words, because a flex container fills its
93
+ * line otherwise. The detail is hidden, because the control is one line tall. The control is at
94
+ * least as tall as a button of its size, so it lines up with the buttons in a toolbar: 36px at
95
+ * `sm` and 40px at `md`. In a narrow toolbar a control with a mark is a square of that height
96
+ * around the mark, as an action with an icon is.
97
+ */
98
+ const INLINE = {
99
+ detail: { display: "none" },
100
+ root: {
101
+ "&[data-narrow]:has(.switcher__mark)": {
102
+ aspectRatio: "square",
103
+ inlineSize: "auto",
104
+ justifyContent: "center",
105
+ paddingInline: "0"
106
+ },
107
+ inlineSize: "fit",
108
+ minBlockSize: `var(${HEIGHT})`
109
+ }
110
+ };
111
+ /**
112
+ * Styles the control in a sidebar: a row as wide as the column, and the mark alone on a rail.
113
+ *
114
+ * @remarks
115
+ * On a rail the control has no padding and no edge, so it is the mark's square, as tall as the
116
+ * rows under it: 32px at `md`.
117
+ */
118
+ const ROW = { root: {
119
+ "&[data-iconic]": {
120
+ alignSelf: "center",
121
+ aspectRatio: "square",
122
+ borderWidth: "0",
123
+ inlineSize: "auto",
124
+ justifyContent: "center",
125
+ padding: "0"
126
+ },
127
+ inlineSize: "full"
128
+ } };
129
+ /**
130
+ * Styles a ghost switcher at the middle size, alone.
131
+ */
132
+ const recipe = defineSlotRecipe({
133
+ base: {
134
+ detail: {
135
+ ...truncate(),
136
+ textStyle: "caption",
137
+ color: "fg.subtle"
138
+ },
139
+ indicator: {
140
+ _open: { rotate: "0deg" },
141
+ alignItems: "center",
142
+ color: "fg.subtle",
143
+ display: "inline-flex",
144
+ flexShrink: "0",
145
+ [ICONIC]: { display: "none" },
146
+ justifyContent: "center",
147
+ marginInlineStart: "auto",
148
+ [NARROW]: { display: "none" },
149
+ transitionDuration: "press",
150
+ transitionProperty: "common",
151
+ transitionTimingFunction: "press"
152
+ },
153
+ label: {
154
+ display: "flex",
155
+ flex: "1",
156
+ flexDirection: "column",
157
+ [ICONIC]: { srOnly: true },
158
+ minInlineSize: "0",
159
+ [NARROW]: { srOnly: true },
160
+ textAlign: "start"
161
+ },
162
+ mark: {
163
+ alignItems: "center",
164
+ color: "colorPalette.fg",
165
+ display: "inline-flex",
166
+ flexShrink: "0",
167
+ fontWeight: "semibold",
168
+ justifyContent: "center",
169
+ lineHeight: "tight",
170
+ overflow: "clip"
171
+ },
172
+ name: {
173
+ ...truncate(),
174
+ fontWeight: "medium"
175
+ },
176
+ root: {
177
+ ...interactive(),
178
+ alignItems: "center",
179
+ appearance: "none",
180
+ borderColor: "transparent",
181
+ borderWidth: "control",
182
+ colorPalette: "neutral",
183
+ display: "flex",
184
+ minInlineSize: "0"
185
+ }
186
+ },
187
+ className: "switcher",
188
+ compoundVariants: [{
189
+ css: { mark: {
190
+ boxSize: dense("{sizes.icon.lg}"),
191
+ fontSize: "xs"
192
+ } },
193
+ name: "marked",
194
+ placement: ["alone", "toolbar"]
195
+ }, (
196
+ /**
197
+ * Sets the detail and the indicator in the trigger's own ink on the solid look, where the
198
+ * page's subtle ink is not read against the fill.
199
+ */
200
+ {
201
+ css: {
202
+ detail: { color: "inherit" },
203
+ indicator: { color: "inherit" }
204
+ },
205
+ name: "inked",
206
+ variant: "solid"
207
+ })],
208
+ defaultVariants: {
209
+ placement: "alone",
210
+ size: "md",
211
+ variant: "ghost"
212
+ },
213
+ jsx: [/^Switcher(\.\w+)?$/u],
214
+ slots: [
215
+ "root",
216
+ "mark",
217
+ "label",
218
+ "name",
219
+ "detail",
220
+ "indicator"
221
+ ],
222
+ variants: {
223
+ /**
224
+ * The palette of the look and the mark.
225
+ */
226
+ palette: onSlot("root", paletteVariants()),
227
+ /**
228
+ * Where the control is placed, which sets its width and its lines.
229
+ *
230
+ * @remarks
231
+ * `Switcher.Root` reads the placement from the sidebar or the toolbar around it unless a
232
+ * caller states one. The `marked` compound sizes the mark to `icon.lg` on one line, because
233
+ * the size axis sizes it for a sidebar and a compound applies over an axis.
234
+ */
235
+ placement: {
236
+ alone: INLINE,
237
+ sidebar: ROW,
238
+ toolbar: INLINE
239
+ },
240
+ size: onSlots({
241
+ indicator: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), [
242
+ "sm",
243
+ "md",
244
+ "lg"
245
+ ]),
246
+ mark: sizeVariants((size) => ({
247
+ boxSize: dense(`{sizes.control.${below(below(size))}}`),
248
+ fontSize: below(size)
249
+ }), [
250
+ "sm",
251
+ "md",
252
+ "lg"
253
+ ]),
254
+ name: sizeVariants((size) => ({ textStyle: `body.${below(size)}` }), [
255
+ "sm",
256
+ "md",
257
+ "lg"
258
+ ]),
259
+ root: sizeVariants((size) => ({
260
+ textStyle: `body.${below(size)}`,
261
+ borderRadius: "l2",
262
+ gap: dense(`{spacing.gap.${size}}`),
263
+ [HEIGHT]: dense(`{sizes.control.${size}}`),
264
+ paddingBlock: dense(`{spacing.gap.${below(size)}}`),
265
+ paddingInline: dense(`{spacing.gap.${size}}`)
266
+ }), [
267
+ "sm",
268
+ "md",
269
+ "lg"
270
+ ])
271
+ }),
272
+ /**
273
+ * The button's six looks, each one of the theme's layer styles.
274
+ *
275
+ * @remarks
276
+ * `ghost`, the default, has no fill at rest, so a switcher at the head of a sidebar shows the
277
+ * sidebar's own ground and fills under a pointer.
278
+ */
279
+ variant: onSlot("root", lookVariants())
280
+ }
281
+ });
282
+ //#endregion
283
+ export { PRIORITY as a, NARROW$1 as i, FOLDING as n, FOLDS as r, recipe as t };