@fluixi-ui/skin-paper 0.1.0-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fluixi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @fluixi-ui/skin-paper
2
+
3
+ A warm, paper-like skin for Fluixi UI: a tinted canvas, white sheets that float on it, and
4
+ outlines drawn as translucent half-pixel rings instead of grey borders.
5
+
6
+ Colour stays theme-driven. Surfaces come from the gray scale and rings from the foreground,
7
+ so `data-theme` (light, dark) and `data-accent` keep working.
8
+
9
+ ## Usage
10
+
11
+ ```ts
12
+ import '@fluixi-ui/styles/all.css';
13
+ import '@fluixi-ui/skin-paper/fonts.css'; // optional: self-hosted Figtree and Bricolage Grotesque
14
+ import '@fluixi-ui/skin-paper/styles.css';
15
+
16
+ document.documentElement.dataset.ui = 'paper';
17
+ ```
18
+
19
+ The attribute can sit on any subtree to scope the skin there.
20
+
21
+ ## Construction
22
+
23
+ Every surface belongs to one family:
24
+
25
+ | Family | Recipe | Used by |
26
+ | --- | --- | --- |
27
+ | field | recessed: inset ring and inset shadow | inputs, search |
28
+ | control | flat: ring and a 1px contact shadow | buttons, icon buttons |
29
+ | card | resting: ring and a soft lift | `Card`, `Surface elevation={1}` |
30
+ | floating | ring, contact and ambient shadow | panels, menus, dialogs (`Surface` 2 to 4) |
31
+
32
+ Inner radii are the outer radius minus the padding. In dark themes surfaces lift with a lighter
33
+ ring and a top highlight, since shadows go unseen.
34
+
35
+ The families are exposed as tokens (`--flx-ui-paper-card-shadow` and the like) for surfaces of
36
+ your own, and fluixi-ui's `--flx-ui-shadow-sm` to `-xl` map onto them.
37
+
38
+ ## Type
39
+
40
+ Figtree for the interface, Bricolage Grotesque from 20px up (`h1` and `[data-display]`). Without
41
+ `fonts.css` both fall back to the system sans.
42
+
43
+ Rules live in the `flx.skin` layer, above `flx.components`; your own unlayered CSS still wins.
package/fonts.css ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The paper skin's typefaces, self-hosted: Figtree for the interface, Bricolage Grotesque
3
+ * for display sizes. Optional; without it the stacks fall back to the system sans.
4
+ */
5
+ @import '@fontsource-variable/figtree';
6
+ @import '@fontsource-variable/bricolage-grotesque';
package/fonts.css.js ADDED
@@ -0,0 +1,8 @@
1
+ const href = new URL('./fonts.css', import.meta.url).href;
2
+ if (typeof document !== 'undefined' && !document.querySelector(`link[data-flx-ui="${href}"]`)) {
3
+ const link = document.createElement('link');
4
+ link.rel = 'stylesheet';
5
+ link.href = href;
6
+ link.dataset.flxUi = href;
7
+ document.head.appendChild(link);
8
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@fluixi-ui/skin-paper",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Paper skin for Fluixi UI: warm canvas, floating white sheets, hairline rings; activate with data-ui=\"paper\"",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "registry": "https://registry.npmjs.org/",
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/fluixi/ui.git",
15
+ "directory": "packages/skin-paper"
16
+ },
17
+ "sideEffects": [
18
+ "*.css"
19
+ ],
20
+ "files": [
21
+ "paper.css",
22
+ "paper.css.js",
23
+ "fonts.css",
24
+ "fonts.css.js",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "exports": {
29
+ "./package.json": "./package.json",
30
+ "./styles.css": "./paper.css",
31
+ "./fonts.css": "./fonts.css"
32
+ },
33
+ "dependencies": {
34
+ "@fontsource-variable/figtree": "^5.3.0",
35
+ "@fontsource-variable/bricolage-grotesque": "^5.3.0"
36
+ },
37
+ "scripts": {
38
+ "build": "node ../../scripts/css-loaders.mjs paper.css fonts.css"
39
+ }
40
+ }
package/paper.css ADDED
@@ -0,0 +1,397 @@
1
+ /**
2
+ * @fluixi-ui/skin-paper: a warm, paper-like skin for Fluixi UI.
3
+ *
4
+ * Activate with `data-ui="paper"` on a root or any subtree. Rules live in the `flx.skin`
5
+ * layer, above `flx.components`; consumer CSS stays unlayered and still wins.
6
+ *
7
+ * Construction, not decoration: outlines are translucent half-pixel rings drawn with
8
+ * box-shadow, never grey borders, so they take the tint of whatever they sit on. Every
9
+ * surface belongs to one family with one recipe:
10
+ *
11
+ * field recessed: inset ring and inset shadow
12
+ * control flat: ring and a 1px contact shadow
13
+ * card resting: ring and a soft lift with negative spread
14
+ * floating panel, menu, dialog: ring, contact and ambient shadow, wider as it rises
15
+ *
16
+ * Inner radii are the outer radius minus the padding. Colour stays theme-driven: surfaces
17
+ * come from the gray scale and rings from the foreground, so `data-theme` and
18
+ * `data-accent` keep working.
19
+ */
20
+ @layer flx.reset, flx.base, flx.tokens, flx.components, flx.skin, flx.fx;
21
+
22
+ @layer flx.skin {
23
+ [data-ui='paper'] {
24
+ /* Type: Figtree for the interface, Bricolage Grotesque from 20px up. fonts.css
25
+ self-hosts both; without it the stacks fall back to the system sans. */
26
+ --flx-ui-font-family-sans: 'Figtree Variable', 'Figtree', ui-sans-serif, system-ui, sans-serif;
27
+ --flx-ui-paper-font-display: 'Bricolage Grotesque Variable', 'Bricolage Grotesque',
28
+ var(--flx-ui-font-family-sans);
29
+ --flx-ui-font-size-xs: 12px;
30
+ --flx-ui-font-size-sm: 13px;
31
+ --flx-ui-font-size-md: 14px;
32
+ --flx-ui-font-size-lg: 15px;
33
+ --flx-ui-line-height-normal: 1.5;
34
+
35
+ --flx-ui-radius-sm: 6px;
36
+ --flx-ui-radius-md: 10px;
37
+ --flx-ui-radius-lg: 16px;
38
+ --flx-ui-radius-xl: 20px;
39
+
40
+ /* Controls and fields that share a bar are 36px high. */
41
+ --flx-ui-size-xs: 24px;
42
+ --flx-ui-size-sm: 30px;
43
+ --flx-ui-size-md: 36px;
44
+ --flx-ui-size-lg: 42px;
45
+ --flx-ui-size-xl: 48px;
46
+
47
+ /* Surfaces from the theme's gray scale. */
48
+ --flx-ui-paper-canvas: var(--flx-ui-gray-3);
49
+ --flx-ui-paper-sheet: var(--flx-ui-color-bg);
50
+ --flx-ui-paper-sunken: var(--flx-ui-gray-2);
51
+ --flx-ui-paper-field: var(--flx-ui-gray-1);
52
+ --flx-ui-paper-control: var(--flx-ui-color-bg);
53
+ --flx-ui-paper-control-hover: var(--flx-ui-gray-2);
54
+ --flx-ui-paper-track: var(--flx-ui-gray-2);
55
+ --flx-ui-paper-floating: var(--flx-ui-color-bg);
56
+ --flx-ui-paper-item-hover: color-mix(in srgb, var(--flx-ui-gray-12) 6%, transparent);
57
+
58
+ /* Rings from the foreground, so they flip with the theme. */
59
+ --flx-ui-paper-ring: color-mix(in srgb, var(--flx-ui-gray-12) 10%, transparent);
60
+ --flx-ui-paper-ring-strong: color-mix(in srgb, var(--flx-ui-gray-12) 16%, transparent);
61
+ --flx-ui-paper-ring-field: color-mix(in srgb, var(--flx-ui-gray-12) 18%, transparent);
62
+ --flx-ui-paper-contact: rgb(24 22 18 / 0.05);
63
+ --flx-ui-paper-ambient: rgb(24 22 18 / 0.08);
64
+ --flx-ui-paper-highlight: transparent;
65
+
66
+ /* The four families. */
67
+ --flx-ui-paper-field-shadow: inset 0 0 0 0.5px var(--flx-ui-paper-ring-field),
68
+ inset 0 1px 2px var(--flx-ui-paper-contact);
69
+ --flx-ui-paper-control-shadow: inset 0 1px 0 var(--flx-ui-paper-highlight),
70
+ 0 0 0 0.5px var(--flx-ui-paper-ring-strong), 0 1px 1px var(--flx-ui-paper-contact);
71
+ --flx-ui-paper-card-shadow: inset 0 1px 0 var(--flx-ui-paper-highlight),
72
+ 0 0 0 0.5px var(--flx-ui-paper-ring), 0 1px 2px var(--flx-ui-paper-contact),
73
+ 0 4px 16px -8px var(--flx-ui-paper-ambient);
74
+ --flx-ui-paper-panel-shadow: inset 0 1px 0 var(--flx-ui-paper-highlight),
75
+ 0 0 0 0.5px var(--flx-ui-paper-ring), 0 1px 2px var(--flx-ui-paper-contact),
76
+ 0 10px 32px var(--flx-ui-paper-ambient);
77
+ --flx-ui-paper-menu-shadow: inset 0 1px 0 var(--flx-ui-paper-highlight),
78
+ 0 0 0 0.5px var(--flx-ui-paper-ring-strong), 0 4px 8px var(--flx-ui-paper-contact),
79
+ 0 16px 40px rgb(24 22 18 / 0.12);
80
+ --flx-ui-paper-action-highlight: rgb(255 255 255 / 0.16);
81
+ --flx-ui-paper-dialog-shadow: inset 0 1px 0 var(--flx-ui-paper-highlight),
82
+ 0 0 0 0.5px var(--flx-ui-paper-ring-strong), 0 8px 16px rgb(24 22 18 / 0.08),
83
+ 0 32px 80px rgb(24 22 18 / 0.2);
84
+
85
+ /* fluixi-ui's elevation scale, mapped onto the families. */
86
+ --flx-ui-shadow-sm: var(--flx-ui-paper-control-shadow);
87
+ --flx-ui-shadow-md: var(--flx-ui-paper-card-shadow);
88
+ --flx-ui-shadow-lg: var(--flx-ui-paper-menu-shadow);
89
+ --flx-ui-shadow-xl: var(--flx-ui-paper-dialog-shadow);
90
+
91
+ font-family: var(--flx-ui-font-family-sans);
92
+ font-size: var(--flx-ui-font-size-md);
93
+ background: var(--flx-ui-paper-canvas);
94
+ color: var(--flx-ui-color-fg);
95
+ -webkit-font-smoothing: antialiased;
96
+ }
97
+
98
+ /* Dark: shadows go unseen, so surfaces lift with a lighter ring and a top highlight. */
99
+ [data-ui='paper']:where([data-theme='dark']),
100
+ :where([data-theme='dark']) [data-ui='paper'] {
101
+ --flx-ui-paper-canvas: var(--flx-ui-gray-1);
102
+ --flx-ui-paper-sheet: var(--flx-ui-gray-2);
103
+ --flx-ui-paper-sunken: var(--flx-ui-gray-3);
104
+ --flx-ui-paper-field: var(--flx-ui-gray-1);
105
+ --flx-ui-paper-control: var(--flx-ui-gray-4);
106
+ --flx-ui-paper-control-hover: var(--flx-ui-gray-5);
107
+ --flx-ui-paper-track: var(--flx-ui-gray-1);
108
+ --flx-ui-paper-floating: var(--flx-ui-gray-3);
109
+ --flx-ui-paper-item-hover: color-mix(in srgb, var(--flx-ui-gray-12) 8%, transparent);
110
+ --flx-ui-paper-contact: rgb(0 0 0 / 0.4);
111
+ --flx-ui-paper-ambient: rgb(0 0 0 / 0.5);
112
+ --flx-ui-paper-highlight: rgb(255 255 255 / 0.04);
113
+ }
114
+
115
+ /* Display face for page and record titles. */
116
+ [data-ui='paper'] :where(h1, [data-display]) {
117
+ font-family: var(--flx-ui-paper-font-display);
118
+ font-weight: 700;
119
+ letter-spacing: -0.035em;
120
+ line-height: 1.1;
121
+ }
122
+
123
+ /* ---- Card: the resting family ------------------------------------------------ */
124
+ [data-ui='paper'] :where([data-card]) {
125
+ background: var(--flx-ui-paper-sheet);
126
+ border: none;
127
+ border-radius: var(--flx-ui-radius-lg);
128
+ box-shadow: var(--flx-ui-paper-card-shadow);
129
+ }
130
+ [data-ui='paper'] :where([data-card-header]) {
131
+ gap: 2px;
132
+ padding: 16px 20px 0;
133
+ }
134
+ [data-ui='paper'] :where([data-card-title]) {
135
+ font-size: var(--flx-ui-font-size-lg);
136
+ font-weight: 600;
137
+ letter-spacing: -0.005em;
138
+ }
139
+ [data-ui='paper'] :where([data-card-description]) {
140
+ font-size: var(--flx-ui-font-size-sm);
141
+ }
142
+ [data-ui='paper'] :where([data-card-body]) {
143
+ padding: 14px 20px 18px;
144
+ font-size: var(--flx-ui-font-size-md);
145
+ }
146
+ [data-ui='paper'] :where([data-card-footer]) {
147
+ padding: 12px 20px;
148
+ border-top: none;
149
+ box-shadow: inset 0 0.5px 0 var(--flx-ui-paper-ring);
150
+ }
151
+
152
+ /* ---- Surface: elevation 0 flat, 1 resting, 2 panel, 3 menu, 4 dialog ------------- */
153
+ [data-ui='paper'] :where([data-surface]) {
154
+ background: var(--flx-ui-paper-sheet);
155
+ border: none;
156
+ }
157
+ [data-ui='paper'] :where([data-surface][data-elevation='0']) { box-shadow: none; }
158
+ [data-ui='paper'] :where([data-surface][data-elevation='0'][data-bordered]) {
159
+ box-shadow: 0 0 0 0.5px var(--flx-ui-paper-ring);
160
+ }
161
+ [data-ui='paper'] :where([data-surface][data-elevation='1']) { box-shadow: var(--flx-ui-paper-card-shadow); }
162
+ [data-ui='paper'] :where([data-surface][data-elevation='2']) { box-shadow: var(--flx-ui-paper-panel-shadow); }
163
+ [data-ui='paper'] :where([data-surface][data-elevation='3']) { box-shadow: var(--flx-ui-paper-menu-shadow); }
164
+ [data-ui='paper'] :where([data-surface][data-elevation='4']) { box-shadow: var(--flx-ui-paper-dialog-shadow); }
165
+
166
+ /* ---- Buttons: the control family -------------------------------------------- */
167
+ /* Rings replace the border; pressing settles the button half a pixel. Focus is an
168
+ outline, offset 2px, so it never replaces the construction shadow. */
169
+ [data-ui='paper'] :where([data-variant]:not([data-badge])) {
170
+ border: none;
171
+ font-weight: 600;
172
+ transition:
173
+ background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
174
+ box-shadow var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
175
+ transform var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
176
+ }
177
+ [data-ui='paper'] :where([data-variant]:not([data-badge])[data-pressed]:not([data-disabled])) {
178
+ transform: translateY(0.5px);
179
+ }
180
+ [data-ui='paper'] :where([data-variant]:not([data-badge]):focus-visible) {
181
+ outline: 2px solid var(--flx-ui-color-focus-ring);
182
+ outline-offset: 2px;
183
+ }
184
+ /* A filled action: top highlight, a ring darker than its fill, contact below. Built
185
+ here, not as a root token, so a data-tone on the button recolours the ring. */
186
+ [data-ui='paper'] :where([data-variant='solid']:not([data-badge])) {
187
+ box-shadow: inset 0 1px 0 var(--flx-ui-paper-action-highlight),
188
+ 0 0 0 0.5px color-mix(in srgb, var(--flx-ui-color-primary) 70%, #000),
189
+ 0 1px 2px var(--flx-ui-paper-contact);
190
+ }
191
+ [data-ui='paper'] :where([data-variant='outline']:not([data-badge])) {
192
+ background: var(--flx-ui-paper-control);
193
+ box-shadow: var(--flx-ui-paper-control-shadow);
194
+ }
195
+ [data-ui='paper'] :where([data-variant='outline']:not([data-badge]):hover:not([data-disabled])) {
196
+ background: var(--flx-ui-paper-control-hover);
197
+ }
198
+ [data-ui='paper'] :where([data-variant='soft']:not([data-badge])) {
199
+ box-shadow: none;
200
+ }
201
+ [data-ui='paper'] :where([data-variant='ghost']:not([data-badge])) {
202
+ box-shadow: none;
203
+ }
204
+ [data-ui='paper'] :where([data-variant='ghost']:not([data-badge]):hover:not([data-disabled])) {
205
+ background: color-mix(in srgb, var(--flx-ui-gray-12) 6%, transparent);
206
+ }
207
+ [data-ui='paper'] :where([data-variant]:not([data-badge])[data-disabled]) {
208
+ box-shadow: none;
209
+ }
210
+ [data-ui='paper'] :where([data-variant='outline']:not([data-badge])[data-disabled]) {
211
+ background: transparent;
212
+ box-shadow: 0 0 0 0.5px var(--flx-ui-paper-ring);
213
+ }
214
+
215
+ /* Icon button: glyphs a step smaller than the default, to sit in a 36px control. */
216
+ [data-ui='paper'] :where([data-icon-button] svg) { width: 18px; height: 18px; }
217
+ [data-ui='paper'] :where([data-icon-button][data-size='sm'] svg) { width: 16px; height: 16px; }
218
+ [data-ui='paper'] :where([data-icon-button][data-size='lg'] svg) { width: 20px; height: 20px; }
219
+
220
+ /* ---- Toggle -------------------------------------------------------------------- */
221
+ [data-ui='paper'] :where([aria-pressed]) {
222
+ border: none;
223
+ background: var(--flx-ui-paper-control);
224
+ box-shadow: var(--flx-ui-paper-control-shadow);
225
+ font-weight: 500;
226
+ }
227
+ [data-ui='paper'] :where([aria-pressed]:hover:not([data-disabled])) {
228
+ background: var(--flx-ui-paper-control-hover);
229
+ }
230
+ [data-ui='paper'] :where([aria-pressed][data-state='on']) {
231
+ background: var(--flx-ui-color-primary-subtle);
232
+ box-shadow: 0 0 0 0.5px var(--flx-ui-color-primary), 0 1px 1px var(--flx-ui-paper-contact);
233
+ }
234
+ [data-ui='paper'] :where([aria-pressed]:focus-visible) {
235
+ outline: 2px solid var(--flx-ui-color-focus-ring);
236
+ outline-offset: 2px;
237
+ }
238
+
239
+ /* Toggle group: a recessed track, the chosen item raised out of it. Items keep the
240
+ track's radius minus its 2px padding, and the track keeps the control height. */
241
+ [data-ui='paper'] :where([role='group'][data-orientation]:has(> [aria-pressed])) {
242
+ gap: 2px;
243
+ padding: 2px;
244
+ border-radius: var(--flx-ui-radius-md);
245
+ background: var(--flx-ui-paper-track);
246
+ box-shadow: inset 0 0 0 0.5px var(--flx-ui-paper-ring);
247
+ }
248
+ [data-ui='paper'] :where([role='group'][data-orientation] > [aria-pressed]) {
249
+ height: calc(var(--flx-ui-size-md) - 4px);
250
+ margin: 0;
251
+ border-radius: calc(var(--flx-ui-radius-md) - 2px);
252
+ background: transparent;
253
+ box-shadow: none;
254
+ color: var(--flx-ui-color-fg-muted);
255
+ }
256
+ [data-ui='paper'] :where([role='group'][data-orientation] > [aria-pressed]:hover:not([data-disabled])) {
257
+ background: transparent;
258
+ color: var(--flx-ui-color-fg);
259
+ }
260
+ [data-ui='paper'] :where([role='group'][data-orientation] > [aria-pressed][data-state='on']) {
261
+ background: var(--flx-ui-paper-control);
262
+ box-shadow: var(--flx-ui-paper-control-shadow);
263
+ color: var(--flx-ui-color-fg);
264
+ }
265
+
266
+ /* ---- Fields: input, input group, select trigger, combobox --------------------- */
267
+ /* Recessed: no border, an inset ring and shadow. Hover deepens the ring; focus is the
268
+ same offset outline as the controls. Outlined and filled variants keep their own look. */
269
+ [data-ui='paper'] :where([data-input]:not([data-field-variant]),
270
+ [data-input-group]:not([data-field-variant]),
271
+ button[role='combobox']:not([data-field-variant]),
272
+ [data-combobox-control]:not([data-field-variant])) {
273
+ border: none;
274
+ min-height: var(--flx-ui-size-md);
275
+ border-radius: var(--flx-ui-radius-md);
276
+ background: var(--flx-ui-paper-field);
277
+ box-shadow: var(--flx-ui-paper-field-shadow);
278
+ }
279
+ [data-ui='paper'] :where(button[role='combobox']:not([data-field-variant])) {
280
+ height: var(--flx-ui-size-md);
281
+ }
282
+ [data-ui='paper'] :where([data-input]:not([data-field-variant]):hover,
283
+ [data-input-group]:not([data-field-variant]):hover,
284
+ button[role='combobox']:not([data-field-variant]):hover,
285
+ [data-combobox-control]:not([data-field-variant]):hover) {
286
+ box-shadow: inset 0 0 0 0.5px var(--flx-ui-gray-8), inset 0 1px 2px var(--flx-ui-paper-contact);
287
+ }
288
+ [data-ui='paper'] :where([data-input]:not([data-field-variant]):focus-visible,
289
+ [data-input-group]:not([data-field-variant]):focus-within,
290
+ button[role='combobox']:not([data-field-variant]):focus-visible,
291
+ [data-combobox-control]:not([data-field-variant]):focus-within) {
292
+ outline: 2px solid var(--flx-ui-color-focus-ring);
293
+ outline-offset: 2px;
294
+ box-shadow: var(--flx-ui-paper-field-shadow);
295
+ }
296
+ [data-ui='paper'] :where([data-input][data-invalid]:not([data-field-variant]),
297
+ [data-input-group][data-invalid]:not([data-field-variant])) {
298
+ box-shadow: inset 0 0 0 1px var(--flx-ui-color-danger), inset 0 1px 2px var(--flx-ui-paper-contact);
299
+ }
300
+ /* The combobox's toggle sits inside the field, a square two pixels short of it. */
301
+ [data-ui='paper'] :where([data-combobox-control] > button) {
302
+ align-self: stretch;
303
+ margin: 3px 0;
304
+ padding: 0 6px;
305
+ border-radius: calc(var(--flx-ui-radius-md) - 3px);
306
+ }
307
+ [data-ui='paper'] :where([data-combobox-control] > button:hover) {
308
+ background: var(--flx-ui-paper-item-hover);
309
+ color: var(--flx-ui-color-fg);
310
+ }
311
+
312
+ /* ---- Popup triggers ------------------------------------------------------------ */
313
+ /* DropdownMenuTrigger and PopoverTrigger render a bare button; given no variant, it
314
+ takes the control recipe. A trigger with a variant is already a Button. */
315
+ [data-ui='paper'] :where(button[aria-haspopup='menu']:not([data-variant]):not([role='menuitem']),
316
+ button[aria-haspopup='dialog']:not([data-variant])) {
317
+ box-sizing: border-box;
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: 6px;
321
+ height: var(--flx-ui-size-md);
322
+ padding: 0 12px;
323
+ font: inherit;
324
+ font-weight: 500;
325
+ color: var(--flx-ui-color-fg);
326
+ background: var(--flx-ui-paper-control);
327
+ border: none;
328
+ border-radius: var(--flx-ui-radius-md);
329
+ box-shadow: var(--flx-ui-paper-control-shadow);
330
+ cursor: pointer;
331
+ transition: background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
332
+ }
333
+ [data-ui='paper'] :where(button[aria-haspopup='menu']:not([data-variant]):not([role='menuitem']),
334
+ button[aria-haspopup='dialog']:not([data-variant])):where(:hover, [data-state='open']) {
335
+ background: var(--flx-ui-paper-control-hover);
336
+ }
337
+ [data-ui='paper'] :where(button[aria-haspopup='menu']:not([data-variant]):not([role='menuitem']),
338
+ button[aria-haspopup='dialog']:not([data-variant])):focus-visible {
339
+ outline: 2px solid var(--flx-ui-color-focus-ring);
340
+ outline-offset: 2px;
341
+ }
342
+
343
+ /* ---- Floating: menus, select and combobox popups, popovers, dialogs ----------- */
344
+ /* Menus and popups pad 4px, so their 14px corner holds items at the 10px control
345
+ radius. Hover and keyboard focus are a neutral tint; the accent marks selection. */
346
+ [data-ui='paper'] :where([role='menu'], [role='listbox'][data-state='open']) {
347
+ padding: 4px;
348
+ border: none;
349
+ border-radius: calc(var(--flx-ui-radius-md) + 4px);
350
+ background: var(--flx-ui-paper-floating);
351
+ box-shadow: var(--flx-ui-paper-menu-shadow);
352
+ }
353
+ [data-ui='paper'] :where([role='menuitem'], [role='option']) {
354
+ min-height: calc(var(--flx-ui-size-md) - 4px);
355
+ padding: 0 10px;
356
+ border-radius: var(--flx-ui-radius-md);
357
+ font-size: var(--flx-ui-font-size-md);
358
+ }
359
+ [data-ui='paper'] :where([role='menu'] [role='menuitem'], [role='listbox'] [role='option']):where(:hover, :focus, [data-active]) {
360
+ background: var(--flx-ui-paper-item-hover);
361
+ color: var(--flx-ui-color-fg);
362
+ }
363
+ [data-ui='paper'] :where([role='menuitem'][aria-haspopup='menu'][data-state='open']) {
364
+ background: var(--flx-ui-paper-item-hover);
365
+ color: var(--flx-ui-color-fg);
366
+ }
367
+ [data-ui='paper'] :where([role='menu'] [role='separator']) {
368
+ height: 0;
369
+ margin: 4px 6px;
370
+ background: none;
371
+ box-shadow: 0 0 0 0.25px var(--flx-ui-paper-ring-strong);
372
+ }
373
+ [data-ui='paper'] :where([data-menu-label], [data-listbox-group-label]) {
374
+ padding: 8px 10px 4px;
375
+ letter-spacing: 0.02em;
376
+ }
377
+ [data-ui='paper'] :where([data-listbox-search], [data-select-search]) {
378
+ min-height: calc(var(--flx-ui-size-md) - 4px);
379
+ margin-bottom: 4px;
380
+ border: none;
381
+ border-radius: var(--flx-ui-radius-md);
382
+ background: var(--flx-ui-paper-field);
383
+ box-shadow: var(--flx-ui-paper-field-shadow);
384
+ }
385
+ [data-ui='paper'] :where([role='dialog'][data-placement]) {
386
+ border: none;
387
+ border-radius: var(--flx-ui-radius-lg);
388
+ background: var(--flx-ui-paper-floating);
389
+ box-shadow: var(--flx-ui-paper-menu-shadow);
390
+ }
391
+ [data-ui='paper'] :where([role='dialog']:not([data-placement]), [role='alertdialog']) {
392
+ border: none;
393
+ border-radius: var(--flx-ui-radius-xl);
394
+ background: var(--flx-ui-paper-floating);
395
+ box-shadow: var(--flx-ui-paper-dialog-shadow);
396
+ }
397
+ }
package/paper.css.js ADDED
@@ -0,0 +1,8 @@
1
+ const href = new URL('./paper.css', import.meta.url).href;
2
+ if (typeof document !== 'undefined' && !document.querySelector(`link[data-flx-ui="${href}"]`)) {
3
+ const link = document.createElement('link');
4
+ link.rel = 'stylesheet';
5
+ link.href = href;
6
+ link.dataset.flxUi = href;
7
+ document.head.appendChild(link);
8
+ }