lostcoords-ui 0.4.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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +352 -0
  4. data/app/assets/tailwind/lostcoords/ui/avatar.css +11 -0
  5. data/app/assets/tailwind/lostcoords/ui/card.css +26 -0
  6. data/app/assets/tailwind/lostcoords/ui/copy-button.css +33 -0
  7. data/app/assets/tailwind/lostcoords/ui/dialog.css +60 -0
  8. data/app/assets/tailwind/lostcoords/ui/disclosure.css +43 -0
  9. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +462 -0
  10. data/app/assets/tailwind/lostcoords/ui/field.css +6 -0
  11. data/app/assets/tailwind/lostcoords/ui/glass.css +37 -0
  12. data/app/assets/tailwind/lostcoords/ui/layout.css +5 -0
  13. data/app/assets/tailwind/lostcoords/ui/popover.css +75 -0
  14. data/app/assets/tailwind/lostcoords/ui/reading.css +60 -0
  15. data/app/assets/tailwind/lostcoords/ui/sheet.css +170 -0
  16. data/app/assets/tailwind/lostcoords/ui/squircle.css +32 -0
  17. data/app/assets/tailwind/lostcoords/ui/theme.css +255 -0
  18. data/app/assets/tailwind/lostcoords/ui/typography.css +81 -0
  19. data/app/assets/tailwind/lostcoords/ui/ui.css +21 -0
  20. data/app/assets/tailwind/lostcoords/ui/utilities.css +45 -0
  21. data/app/assets/tailwind/lostcoords_ui/engine.css +1 -0
  22. data/app/helpers/lostcoords/ui/badge_helper.rb +162 -0
  23. data/app/helpers/lostcoords/ui/button_helper.rb +70 -0
  24. data/app/helpers/lostcoords/ui/card_helper.rb +60 -0
  25. data/app/helpers/lostcoords/ui/copy_button_helper.rb +40 -0
  26. data/app/helpers/lostcoords/ui/dialog_helper.rb +33 -0
  27. data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +245 -0
  28. data/app/helpers/lostcoords/ui/feedback_helper.rb +30 -0
  29. data/app/helpers/lostcoords/ui/form_helper.rb +93 -0
  30. data/app/helpers/lostcoords/ui/glass_helper.rb +25 -0
  31. data/app/helpers/lostcoords/ui/icon_helper.rb +15 -0
  32. data/app/helpers/lostcoords/ui/layout_helper.rb +60 -0
  33. data/app/helpers/lostcoords/ui/menu_helper.rb +41 -0
  34. data/app/helpers/lostcoords/ui/primitives_helper.rb +152 -0
  35. data/app/helpers/lostcoords/ui/reading_helper.rb +15 -0
  36. data/app/helpers/lostcoords/ui/render_attributes_helper.rb +46 -0
  37. data/app/helpers/lostcoords/ui/sheet_helper.rb +54 -0
  38. data/app/helpers/lostcoords/ui/structures_helper.rb +159 -0
  39. data/app/helpers/lostcoords/ui/tailwind_helper.rb +14 -0
  40. data/app/helpers/lostcoords/ui/translation_helper.rb +13 -0
  41. data/app/helpers/lostcoords/ui/typography_helper.rb +28 -0
  42. data/app/javascript/lostcoords/ui/controllers/confirm_dialog_controller.js +39 -0
  43. data/app/javascript/lostcoords/ui/controllers/disclosure_controller.js +163 -0
  44. data/app/javascript/lostcoords/ui/controllers/emoji_picker_controller.js +366 -0
  45. data/app/javascript/lostcoords/ui/controllers/modal_controller.js +27 -0
  46. data/app/javascript/lostcoords/ui/controllers/row_menu_controller.js +83 -0
  47. data/app/javascript/lostcoords/ui/controllers/sheet_controller.js +422 -0
  48. data/app/javascript/lostcoords/ui/controllers/toast_controller.js +69 -0
  49. data/app/javascript/lostcoords/ui/controllers/ui_copy_controller.js +56 -0
  50. data/app/javascript/lostcoords/ui/controllers/ui_popover_controller.js +17 -0
  51. data/app/javascript/lostcoords/ui/controllers/ui_selection_controller.js +69 -0
  52. data/app/javascript/lostcoords/ui/controllers/ui_toggle_controller.js +13 -0
  53. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +455 -0
  54. data/app/javascript/lostcoords/ui/helpers/context_dialog.js +29 -0
  55. data/app/javascript/lostcoords/ui/helpers/dialog.js +31 -0
  56. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +155 -0
  57. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +108 -0
  58. data/app/javascript/lostcoords/ui/helpers/menu_keys.js +27 -0
  59. data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +13 -0
  60. data/app/javascript/lostcoords/ui/helpers/top_layer.js +102 -0
  61. data/app/javascript/lostcoords/ui/helpers/viewport.js +13 -0
  62. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +176 -0
  63. data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +29 -0
  64. data/app/views/lostcoords/ui/components/_form_alert.html.erb +4 -0
  65. data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +13 -0
  66. data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +5 -0
  67. data/app/views/lostcoords/ui/components/_skeleton.html.erb +4 -0
  68. data/app/views/lostcoords/ui/components/_spinner.html.erb +10 -0
  69. data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +8 -0
  70. data/config/emoji_search_aliases.yml +146 -0
  71. data/config/emoji_search_names.yml +1917 -0
  72. data/config/emoji_search_names_ja.yml +6902 -0
  73. data/config/helper_renames.json +113 -0
  74. data/config/importmap.rb +3 -0
  75. data/config/locales/en.yml +33 -0
  76. data/config/locales/es.yml +33 -0
  77. data/config/locales/ja.yml +33 -0
  78. data/lib/lostcoords/ui/action_view.rb +10 -0
  79. data/lib/lostcoords/ui/builders.rb +42 -0
  80. data/lib/lostcoords/ui/components.rb +13 -0
  81. data/lib/lostcoords/ui/engine.rb +22 -0
  82. data/lib/lostcoords/ui/helpers.rb +11 -0
  83. data/lib/lostcoords/ui/icons.rb +24 -0
  84. data/lib/lostcoords/ui/rails.rb +3 -0
  85. data/lib/lostcoords/ui/resources.rb +17 -0
  86. data/lib/lostcoords/ui/tokens.rb +14 -0
  87. data/lib/lostcoords/ui/version.rb +5 -0
  88. data/lib/lostcoords/ui.rb +2 -0
  89. data/lib/tasks/lostcoords_ui.rake +15 -0
  90. data/script/migrate_helpers.rb +136 -0
  91. data/vendor/heroicons/LICENSE +21 -0
  92. data/vendor/heroicons/icons.json +1303 -0
  93. metadata +203 -0
@@ -0,0 +1,462 @@
1
+ @layer components {
2
+ .emoji-picker {
3
+ display: flex;
4
+ gap: 6px;
5
+ flex-wrap: wrap;
6
+ align-items: center;
7
+
8
+ /* Sit above the mobile app nav (z-100 + 25) so the sheet covers it. */
9
+ --emoji-sheet-z: calc(100 + 30);
10
+ --emoji-cell: 2.25rem;
11
+ }
12
+
13
+ /* ---- Trigger + quick choices -------------------------------------- */
14
+ .emoji-picker__choice,
15
+ .emoji-picker__trigger {
16
+ display: inline-grid;
17
+ place-items: center;
18
+ inline-size: 44px;
19
+ block-size: 44px;
20
+ border-radius: var(--radius-2xl, 1rem);
21
+ border: 1px solid var(--hairline);
22
+ background: var(--paper);
23
+ color: var(--ink);
24
+ cursor: pointer;
25
+ font-size: var(--text-xl, 1.25rem);
26
+ line-height: 1;
27
+ transition:
28
+ border-color 0.15s,
29
+ background 0.15s,
30
+ transform 0.15s var(--ease-out),
31
+ box-shadow 0.15s;
32
+
33
+ &:hover { border-color: var(--ink-3); }
34
+
35
+ &[aria-current="true"] {
36
+ border-color: var(--sky);
37
+ background: var(--sky-soft);
38
+ }
39
+ }
40
+
41
+ .emoji-picker__trigger {
42
+ color: var(--ink-3);
43
+ font-weight: var(--font-weight-normal, 400);
44
+ }
45
+
46
+ .emoji-picker__more {
47
+ position: relative;
48
+ display: inline-flex;
49
+ }
50
+
51
+ /* ==================================================================== */
52
+
53
+ /* Mobile-first: the picker opens as a bottom half-sheet. */
54
+
55
+ /* ==================================================================== */
56
+
57
+ /* The sheet renders in the top layer (Popover API), so this ::backdrop
58
+ dims everything beneath it — including the app nav. The allow-discrete
59
+ transitions keep it in the top layer while it eases out, so it fades
60
+ both ways instead of snapping away on close. */
61
+ .emoji-picker__popover::backdrop {
62
+ background: var(--scrim);
63
+ opacity: 0;
64
+ transition:
65
+ opacity 220ms var(--ease-out),
66
+ display 220ms allow-discrete,
67
+ overlay 220ms allow-discrete;
68
+
69
+ @media (prefers-reduced-motion: reduce) { transition: none; }
70
+ }
71
+
72
+ /* Only the phone sheet dims the page; the desktop popover rides the top
73
+ layer too (so nothing paints over it) but floats over an undimmed page. */
74
+ @media (width < 40rem) {
75
+ .emoji-picker__popover:popover-open::backdrop {
76
+ opacity: 1;
77
+
78
+ @starting-style { opacity: 0; }
79
+ }
80
+ }
81
+
82
+ /* Phones get a solid sheet; on desktop the host paints the popover as a
83
+ floating surface (glass in wariwari), so the gem leaves it unpainted. */
84
+ @media (width < 40rem) {
85
+ .emoji-picker__popover {
86
+ border: 1px solid var(--glass-edge, var(--hairline));
87
+ border-block-end: 0;
88
+ background: var(--paper);
89
+ box-shadow: 0 -12px 40px -12px color-mix(in oklab, var(--ink) 45%, transparent);
90
+ }
91
+ }
92
+
93
+ .emoji-picker__popover {
94
+ position: fixed;
95
+ inset: auto 0 0;
96
+ margin: 0;
97
+ z-index: var(--emoji-sheet-z);
98
+ display: none;
99
+ max-inline-size: none;
100
+ color: var(--ink);
101
+ flex-direction: column;
102
+ inline-size: 100%;
103
+ max-block-size: min(72svh, 34rem);
104
+ padding-block-end: max(env(safe-area-inset-bottom), 8px);
105
+ border-start-start-radius: var(--radius-3xl, 1.5rem);
106
+ border-start-end-radius: var(--radius-3xl, 1.5rem);
107
+ overscroll-behavior: contain;
108
+ transform: translateY(100%);
109
+ transition:
110
+ transform 260ms var(--ease-out-expo),
111
+ display 260ms allow-discrete,
112
+ overlay 260ms allow-discrete;
113
+
114
+ /* Top-layer (Popover API) drives display, so showPopover()/hidePopover()
115
+ animate the slide in AND out via the allow-discrete transitions. */
116
+ &:popover-open {
117
+ display: flex;
118
+ transform: translateY(0);
119
+
120
+ @starting-style { transform: translateY(100%); }
121
+ }
122
+
123
+ @media (prefers-reduced-motion: reduce) { transition: none; }
124
+ }
125
+
126
+ /* Fallback for browsers without the Popover API: class-driven display. */
127
+ @supports not selector(:popover-open) {
128
+ .emoji-picker__popover[data-open] {
129
+ display: flex;
130
+ transform: translateY(0);
131
+ }
132
+ }
133
+
134
+ /* Drag handle for swipe-to-dismiss. Full-width grab strip, so the touch target
135
+ is large; the host's sheet draws the pill.
136
+ touch-action: none lets a vertical drag pull the sheet down rather than
137
+ scroll. Drag-to-dismiss is wired by the sheet controller. */
138
+ .emoji-picker__handle {
139
+ flex: 0 0 auto;
140
+ display: grid;
141
+ place-items: center;
142
+ inline-size: 100%;
143
+ min-block-size: 1.75rem;
144
+ touch-action: none;
145
+ cursor: grab;
146
+ }
147
+
148
+ .emoji-picker__handle:active { cursor: grabbing; }
149
+
150
+ /* ---- Search row --------------------------------------------------- */
151
+ .emoji-picker__search {
152
+ flex: 0 0 auto;
153
+ display: flex;
154
+ gap: 0.5rem;
155
+ align-items: center;
156
+ padding: 0.75rem 0.75rem 0.5rem;
157
+ }
158
+
159
+ .emoji-picker__search-field {
160
+ position: relative;
161
+ flex: 1 1 auto;
162
+ display: flex;
163
+ align-items: center;
164
+ }
165
+
166
+ .emoji-picker__search-icon {
167
+ position: absolute;
168
+ inset-inline-start: 14px;
169
+ inline-size: 18px;
170
+ block-size: 18px;
171
+ color: var(--ink-3);
172
+ pointer-events: none;
173
+ }
174
+
175
+ .emoji-picker__search-input {
176
+ inline-size: 100%;
177
+ min-block-size: 44px;
178
+ padding-inline: 40px 1rem;
179
+ border: 1px solid transparent;
180
+ border-radius: 999px;
181
+ background: var(--paper-2);
182
+ color: var(--ink);
183
+ font: inherit;
184
+ font-size: 1rem;
185
+ transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
186
+
187
+ &::placeholder { color: var(--ink-3); }
188
+ &::-webkit-search-cancel-button {
189
+ /* stylelint-disable-next-line property-no-vendor-prefix -- WebKit search decoration */
190
+ -webkit-appearance: none;
191
+ appearance: none;
192
+ }
193
+
194
+ &:focus {
195
+ outline: none;
196
+ box-shadow: var(--focus-halo, 0 0 0 2px color-mix(in oklab, var(--sky) 50%, transparent));
197
+ }
198
+ }
199
+
200
+ /* ---- Skin-tone control ------------------------------------------- */
201
+ .emoji-picker__tones {
202
+ position: relative;
203
+ flex: 0 0 auto;
204
+ }
205
+
206
+ .emoji-picker__tone-toggle {
207
+ display: inline-grid;
208
+ place-items: center;
209
+ inline-size: 44px;
210
+ block-size: 44px;
211
+ border: none;
212
+ border-radius: 999px;
213
+ background: var(--paper-2);
214
+ cursor: pointer;
215
+ font-size: var(--text-lg, 1.125rem);
216
+ line-height: 1;
217
+ transition: background 150ms ease-out;
218
+
219
+ &:hover { background: var(--paper-3); }
220
+
221
+ &[aria-expanded="true"] { background: var(--sky-soft); }
222
+ }
223
+
224
+ .emoji-picker__tone-menu {
225
+ position: absolute;
226
+ inset-block-end: calc(100% + 6px);
227
+ inset-inline-end: 0;
228
+ z-index: 4;
229
+ display: flex;
230
+ gap: 2px;
231
+ padding: 4px;
232
+ border-radius: 999px;
233
+
234
+ &[hidden] { display: none; }
235
+ }
236
+
237
+ .emoji-picker__tone {
238
+ display: inline-grid;
239
+ place-items: center;
240
+ inline-size: 36px;
241
+ block-size: 36px;
242
+ border: none;
243
+ border-radius: 999px;
244
+ background: transparent;
245
+ cursor: pointer;
246
+ font-size: var(--text-lg, 1.125rem);
247
+ line-height: 1;
248
+
249
+ &:hover { background: var(--paper-2); }
250
+
251
+ &[aria-current="true"] { background: var(--sky-soft); }
252
+ }
253
+
254
+ /* ---- Category tab navigation ------------------------------------- */
255
+
256
+ /* Category tabs: a segmented track, like the app's tab strips — a paper-2
257
+ track with the active tab raised on a paper thumb. */
258
+ .emoji-picker__tabs {
259
+ flex: 0 0 auto;
260
+ display: flex;
261
+ gap: 2px;
262
+ margin: 0 0.75rem 0.25rem;
263
+ padding: 4px;
264
+ overflow-x: auto;
265
+ scrollbar-width: none;
266
+ border-radius: var(--radius-2xl, 1rem);
267
+ background: var(--paper-2);
268
+
269
+ &::-webkit-scrollbar { display: none; }
270
+ }
271
+
272
+ .emoji-picker__tab {
273
+ flex: 0 0 auto;
274
+ display: inline-grid;
275
+ place-items: center;
276
+ inline-size: 36px;
277
+ block-size: 36px;
278
+ border: none;
279
+ border-radius: var(--radius-xl, 0.75rem);
280
+ background: transparent;
281
+ cursor: pointer;
282
+ font-size: var(--text-lg, 1.125rem);
283
+ line-height: 1;
284
+ opacity: 0.55;
285
+ transition: opacity 0.12s, background 0.12s, box-shadow 0.12s;
286
+
287
+ &:hover { opacity: 0.85; }
288
+
289
+ &[aria-current="true"] {
290
+ opacity: 1;
291
+ background: var(--paper);
292
+ box-shadow: var(--shadow-sm);
293
+ }
294
+ }
295
+
296
+ /* ---- Scrollable body --------------------------------------------- */
297
+ .emoji-picker__body {
298
+ position: relative;
299
+ flex: 1 1 auto;
300
+ overflow-y: auto;
301
+ overscroll-behavior: contain;
302
+ padding: 0 0.75rem 0.75rem;
303
+ scroll-behavior: smooth;
304
+
305
+ /* Emoji melt under the tabs and into the footer rather than cutting off
306
+ against a divider. */
307
+ mask-image: linear-gradient(to bottom, transparent, #000 0.5rem, #000 calc(100% - 1.25rem), transparent);
308
+
309
+ @media (prefers-reduced-motion: reduce) { scroll-behavior: auto; }
310
+ }
311
+
312
+ .emoji-picker__category + .emoji-picker__category,
313
+ .emoji-picker__recent + .emoji-picker__catalog,
314
+ .emoji-picker__results + * {
315
+ margin-block-start: 0.5rem;
316
+ }
317
+
318
+ /* Heads scroll with their section: the tab track already marks the current
319
+ category, and a sticky band would sit as a stripe across the panel. */
320
+ .emoji-picker__category-head {
321
+ display: flex;
322
+ align-items: baseline;
323
+ justify-content: space-between;
324
+ gap: 0.5rem;
325
+ padding: 0.625rem 0.25rem 0.25rem;
326
+
327
+ h3 {
328
+ margin: 0;
329
+ font-size: var(--text-sm, 0.875rem);
330
+ font-weight: var(--font-weight-semibold, 600);
331
+ color: var(--ink-2);
332
+ }
333
+
334
+ .count {
335
+ font-family: var(--font-body, system-ui);
336
+ font-size: var(--text-xs, 0.75rem);
337
+ color: var(--ink-3);
338
+ }
339
+ }
340
+
341
+ .emoji-picker__grid {
342
+ display: grid;
343
+ grid-template-columns: repeat(auto-fill, minmax(var(--emoji-cell), 1fr));
344
+ gap: 2px;
345
+ padding-block-start: 4px;
346
+
347
+ button {
348
+ display: inline-grid;
349
+ place-items: center;
350
+ aspect-ratio: 1;
351
+ min-inline-size: 0;
352
+ border: none;
353
+ border-radius: var(--radius-xl, 0.75rem);
354
+ background: transparent;
355
+ cursor: pointer;
356
+ font-size: var(--text-xl, 1.25rem);
357
+ line-height: 1;
358
+ padding: 0;
359
+ transition: background 0.1s, transform 0.1s var(--ease-out);
360
+
361
+ &:hover,
362
+ &:focus-visible {
363
+ background: var(--paper-2);
364
+ transform: scale(1.12);
365
+ outline: none;
366
+ }
367
+
368
+ &[aria-current="true"] { background: var(--sky-soft); }
369
+ }
370
+ }
371
+
372
+ /* Section that owns the sticky header needs the scroll anchor, not the grid */
373
+ .emoji-picker__category,
374
+ .emoji-picker__recent,
375
+ .emoji-picker__results {
376
+ scroll-margin-block-start: 0;
377
+ }
378
+
379
+ .emoji-picker__empty {
380
+ margin: 0;
381
+ padding: 1rem 0.5rem;
382
+ color: var(--ink-3);
383
+ font-size: 1rem;
384
+ text-align: center;
385
+ }
386
+
387
+ /* ---- Search mode: swap catalog for a single ranked result list ---- */
388
+ .emoji-picker__popover:not([data-searching]) .emoji-picker__results { display: none; }
389
+
390
+ .emoji-picker__popover[data-searching] {
391
+ .emoji-picker__tabs,
392
+ .emoji-picker__recent,
393
+ .emoji-picker__catalog { display: none; }
394
+ }
395
+
396
+ /* ---- Preview footer ---------------------------------------------- */
397
+ .emoji-picker__preview {
398
+ flex: 0 0 auto;
399
+ display: flex;
400
+ gap: 0.5rem;
401
+ align-items: center;
402
+ min-block-size: 2.75rem;
403
+ padding: 0.25rem 1rem 0.75rem;
404
+ }
405
+
406
+ .emoji-picker__preview-emoji {
407
+ font-size: 1.5rem;
408
+ line-height: 1;
409
+ }
410
+
411
+ .emoji-picker__preview-name {
412
+ overflow: hidden;
413
+ font-size: 1rem;
414
+ color: var(--ink-3);
415
+ text-overflow: ellipsis;
416
+ white-space: nowrap;
417
+ }
418
+
419
+ /* ==================================================================== */
420
+
421
+ /* Desktop: anchored popover instead of a sheet. */
422
+
423
+ /* ==================================================================== */
424
+ @media (width >= 40rem) {
425
+ .emoji-picker__popover {
426
+ position: absolute;
427
+ inset-block: calc(100% + 8px) auto;
428
+ inset-inline: 0 auto;
429
+ inline-size: min(22rem, calc(100vw - 32px));
430
+ max-block-size: min(28rem, calc(100vh - 96px));
431
+ padding-block-end: 0;
432
+ border-radius: var(--radius-3xl, 1.5rem);
433
+ transform: translateY(-4px) scale(0.98);
434
+ opacity: 0;
435
+ transition:
436
+ opacity 160ms var(--ease-out-expo),
437
+ transform 160ms var(--ease-out-expo),
438
+ display 160ms allow-discrete,
439
+ overlay 160ms allow-discrete;
440
+
441
+ &[data-open] {
442
+ display: flex;
443
+ opacity: 1;
444
+ transform: translateY(0) scale(1);
445
+
446
+ @starting-style {
447
+ opacity: 0;
448
+ transform: translateY(-4px) scale(0.98);
449
+ }
450
+ }
451
+
452
+ @media (prefers-reduced-motion: reduce) { transition: none; }
453
+ }
454
+
455
+ .emoji-picker__handle { display: none; }
456
+
457
+ main:has(.emoji-picker__popover[data-open]),
458
+ .emoji-picker:has(.emoji-picker__popover[data-open]) {
459
+ z-index: 60;
460
+ }
461
+ }
462
+ }
@@ -0,0 +1,6 @@
1
+ @layer components {
2
+ /* A legend sits outside the fieldset's internal grid. */
3
+ :where(fieldset.ui-field > legend) {
4
+ margin-block-end: var(--ui-space-related, 0.5rem);
5
+ }
6
+ }
@@ -0,0 +1,37 @@
1
+ /* Glass — the floating control layer, used the way iOS uses it: only on
2
+ chrome that floats over content (the nav pills, the phone CTA dock, menus
3
+ and popovers), never on cards or sections. Menus lift further, on
4
+ --shadow-lg. Written once here because Safari
5
+ drops custom properties inside -webkit-backdrop-filter, so the blur needs
6
+ literal values, and because the solid fallbacks need @supports and
7
+ prefers-reduced-transparency. glass_class is the only writer of the name. */
8
+ @layer components {
9
+ /* The emoji and currency pickers are glass only as desktop popovers; on
10
+ phones they're bottom sheets, whose rules come later in the cascade and repaint them solid. */
11
+ .glass {
12
+ background: var(--glass-fill);
13
+ border: 1px solid var(--glass-edge);
14
+ box-shadow: var(--glass-shadow);
15
+ -webkit-backdrop-filter: blur(24px) saturate(160%);
16
+ backdrop-filter: blur(24px) saturate(160%);
17
+
18
+ /* Phones take a tighter blur and richer saturation, as iOS materials
19
+ do. At 3x density Chromium renders a wide blur over a short bar as the
20
+ sharp backdrop faded out, so 24px let nav text read straight through. */
21
+ @media (width < 48rem) {
22
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
23
+ backdrop-filter: blur(12px) saturate(180%);
24
+ }
25
+
26
+ /* --glass-solid lets inverted (HUD) glass fall back to its own colour. */
27
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
28
+ background: var(--glass-solid, var(--paper));
29
+ }
30
+
31
+ @media (prefers-reduced-transparency: reduce) {
32
+ background: var(--glass-solid, var(--paper));
33
+ -webkit-backdrop-filter: none;
34
+ backdrop-filter: none;
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,5 @@
1
+ @layer components {
2
+ .ui-container-page {
3
+ padding-inline: var(--ui-gutter, clamp(1rem, 3vw, 1.5rem));
4
+ }
5
+ }
@@ -0,0 +1,75 @@
1
+ /* Closed is the exit pose, so closing animates too; allow-discrete holds
2
+ display/overlay until it ends. FloatingPanel sets [data-open]. */
3
+ @utility popover-shell {
4
+ opacity: 0;
5
+ transform: translateY(--spacing(-1)) scale(0.98);
6
+ pointer-events: none;
7
+ transition:
8
+ opacity 160ms var(--ease-out-expo),
9
+ transform 160ms var(--ease-out-expo),
10
+ display 160ms allow-discrete,
11
+ overlay 160ms allow-discrete;
12
+
13
+ &[data-open] {
14
+ opacity: 1;
15
+ transform: translateY(0) scale(1);
16
+ pointer-events: auto;
17
+
18
+ @starting-style {
19
+ opacity: 0;
20
+ transform: translateY(--spacing(-1)) scale(0.98);
21
+ }
22
+ }
23
+ }
24
+
25
+ @layer components {
26
+ /* Desktop: the .sheet becomes an anchored popover instead of a bottom sheet. */
27
+ @media (width >= 40rem) {
28
+ .sheet--anchored {
29
+ @apply popover-shell;
30
+
31
+ display: none;
32
+ flex-direction: column;
33
+ position: absolute;
34
+ inset-block: calc(100% + --spacing(2)) auto;
35
+ inset-inline: auto 0;
36
+ inline-size: min(24rem, calc(100vw - --spacing(8)));
37
+ max-block-size: min(28rem, calc(100vh - --spacing(24)));
38
+ padding-block-end: 0;
39
+ border-radius: var(--radius-3xl);
40
+ pointer-events: auto;
41
+
42
+ &[data-open] {
43
+ display: flex;
44
+ }
45
+ }
46
+ }
47
+
48
+ /* Enter/exit for small overlays, whichever way they open: a [hidden] toggle
49
+ (amount calculator, colour picker), the Popover API (phone nav menus) or a
50
+ modal <dialog>. The closed state is the exit pose, so close animates too;
51
+ allow-discrete holds display/overlay until it ends. --pop-y sets the
52
+ direction: popovers drop from their trigger, dialogs rise. */
53
+ .popover-pop {
54
+ --pop-from: translateY(var(--pop-y, -0.25rem)) scale(0.96);
55
+
56
+ transform-origin: var(--pop-origin, top);
57
+ transition:
58
+ opacity 160ms var(--ease-out-expo),
59
+ transform 240ms var(--ease-out-expo),
60
+ display 240ms allow-discrete,
61
+ overlay 240ms allow-discrete;
62
+
63
+ &:is([hidden], [popover]:not(:popover-open), dialog:not([open])) {
64
+ opacity: 0;
65
+ transform: var(--pop-from);
66
+ }
67
+
68
+ &:is(:not([hidden], [popover], dialog), :popover-open, dialog[open]) {
69
+ @starting-style {
70
+ opacity: 0;
71
+ transform: var(--pop-from);
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,60 @@
1
+ @layer utilities {
2
+ .ui-prose {
3
+ --tw-prose-body: var(--ink-2);
4
+ --tw-prose-headings: var(--ink);
5
+ --tw-prose-lead: var(--ink-2);
6
+ --tw-prose-links: var(--accent, var(--sky));
7
+ --tw-prose-bold: var(--ink);
8
+ --tw-prose-counters: var(--ink-3);
9
+ --tw-prose-bullets: var(--ink-3);
10
+ --tw-prose-hr: var(--hairline);
11
+ --tw-prose-quotes: var(--ink);
12
+ --tw-prose-quote-borders: var(--hairline);
13
+ --tw-prose-captions: var(--ink-3);
14
+ --tw-prose-code: var(--ink);
15
+ --tw-prose-pre-code: var(--ink);
16
+ --tw-prose-pre-bg: var(--paper-2);
17
+ --tw-prose-th-borders: var(--hairline);
18
+ --tw-prose-td-borders: var(--hairline);
19
+
20
+ & :where(p, ul, ol, dl, blockquote, pre, table, figure, hr):not(:where(.not-prose, .not-prose *)) {
21
+ margin-block: var(--ui-reading-flow, 1.25em) 0;
22
+ }
23
+
24
+ & :where(h1, h2, h3, h4):not(:where(.not-prose, .not-prose *)) {
25
+ margin-block: calc(var(--ui-reading-flow, 1.25em) * 1.6) 0;
26
+ font-family: var(--font-display, inherit);
27
+ line-height: var(--ui-reading-heading-leading, 1.25);
28
+ text-wrap: balance;
29
+ }
30
+
31
+ & :where(h1, h2, h3, h4) + :where(p, ul, ol):not(:where(.not-prose, .not-prose *)) {
32
+ margin-block-start: calc(var(--ui-reading-flow, 1.25em) * 0.5);
33
+ }
34
+
35
+ & :where(li > p, li > ul, li > ol):not(:where(.not-prose, .not-prose *)) {
36
+ margin-block-start: calc(var(--ui-reading-flow, 1.25em) * 0.5);
37
+ }
38
+
39
+ & > :where(:first-child):not(:where(.not-prose, .not-prose *)) {
40
+ margin-block-start: 0;
41
+ }
42
+
43
+ &:lang(ja) {
44
+ line-break: strict;
45
+ word-break: normal;
46
+ overflow-wrap: anywhere;
47
+ }
48
+
49
+ &:lang(ja) :where(h1, h2, h3, h4):not(:where(.not-prose, .not-prose *)) {
50
+ line-height: var(--ui-reading-heading-leading, 1.4);
51
+ letter-spacing: normal;
52
+ }
53
+ }
54
+
55
+ @supports (word-break: auto-phrase) {
56
+ .ui-prose:lang(ja) :where(h1, h2, h3, h4):not(:where(.not-prose, .not-prose *)) {
57
+ word-break: auto-phrase;
58
+ }
59
+ }
60
+ }