lostcoords-ui 0.5.0 → 0.7.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -49
  3. data/app/assets/tailwind/lostcoords/ui/card.css +5 -2
  4. data/app/assets/tailwind/lostcoords/ui/{disclosure.css → collapsible.css} +1 -1
  5. data/app/assets/tailwind/lostcoords/ui/components/code-block.css +5 -0
  6. data/app/assets/tailwind/lostcoords/ui/components/collapsible.css +5 -0
  7. data/app/assets/tailwind/lostcoords/ui/components/dialog.css +9 -0
  8. data/app/assets/tailwind/lostcoords/ui/components/emoji-picker.css +5 -0
  9. data/app/assets/tailwind/lostcoords/ui/components/menu.css +5 -0
  10. data/app/assets/tailwind/lostcoords/ui/components/prose.css +3 -0
  11. data/app/assets/tailwind/lostcoords/ui/components/table.css +3 -0
  12. data/app/assets/tailwind/lostcoords/ui/components/toast.css +5 -0
  13. data/app/assets/tailwind/lostcoords/ui/components/tooltip.css +4 -0
  14. data/app/assets/tailwind/lostcoords/ui/components.css +12 -25
  15. data/app/assets/tailwind/lostcoords/ui/core.css +21 -0
  16. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +45 -47
  17. data/app/assets/tailwind/lostcoords/ui/popover.css +0 -52
  18. data/app/assets/tailwind/lostcoords/ui/{reading.css → prose.css} +6 -0
  19. data/app/assets/tailwind/lostcoords/ui/sheet.css +3 -64
  20. data/app/assets/tailwind/lostcoords/ui/theme.css +16 -1
  21. data/app/assets/tailwind/lostcoords/ui/toast.css +23 -0
  22. data/app/assets/tailwind/lostcoords/ui/tooltip.css +12 -11
  23. data/app/assets/tailwind/lostcoords/ui/typography.css +1 -7
  24. data/app/helpers/lostcoords/ui/badge_helper.rb +2 -2
  25. data/app/helpers/lostcoords/ui/button_helper.rb +12 -6
  26. data/app/helpers/lostcoords/ui/choice_helper.rb +11 -7
  27. data/app/helpers/lostcoords/ui/collapsible_helper.rb +9 -0
  28. data/app/helpers/lostcoords/ui/dialog_helper.rb +10 -5
  29. data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +6 -0
  30. data/app/helpers/lostcoords/ui/feedback_helper.rb +2 -2
  31. data/app/helpers/lostcoords/ui/form_helper.rb +4 -3
  32. data/app/helpers/lostcoords/ui/glass_helper.rb +1 -1
  33. data/app/helpers/lostcoords/ui/menu_helper.rb +37 -2
  34. data/app/helpers/lostcoords/ui/navigation_helper.rb +17 -4
  35. data/app/helpers/lostcoords/ui/render_attributes_helper.rb +17 -1
  36. data/app/helpers/lostcoords/ui/sheet_helper.rb +4 -0
  37. data/app/helpers/lostcoords/ui/structures_helper.rb +7 -95
  38. data/app/helpers/lostcoords/ui/toast_helper.rb +22 -0
  39. data/app/helpers/lostcoords/ui/{overlay_helper.rb → tooltip_helper.rb} +3 -3
  40. data/app/helpers/lostcoords/ui/typography_helper.rb +30 -0
  41. data/app/javascript/lostcoords/ui/controllers/lc_collapsible_controller.js +1 -1
  42. data/app/javascript/lostcoords/ui/controllers/lc_emoji_picker_controller.js +34 -6
  43. data/app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js +12 -0
  44. data/app/javascript/lostcoords/ui/controllers/lc_menu_controller.js +23 -1
  45. data/app/javascript/lostcoords/ui/controllers/lc_selection_controller.js +8 -16
  46. data/app/javascript/lostcoords/ui/controllers/lc_sheet_controller.js +15 -3
  47. data/app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js +45 -0
  48. data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +18 -15
  49. data/app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js +62 -0
  50. data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +46 -6
  51. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +72 -20
  52. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +23 -0
  53. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +6 -7
  54. data/app/javascript/lostcoords/ui/helpers/menu_keys.js +19 -18
  55. data/app/javascript/lostcoords/ui/helpers/roving_focus.js +175 -0
  56. data/app/javascript/lostcoords/ui/helpers/shortcuts.js +162 -0
  57. data/app/javascript/lostcoords/ui/helpers/top_layer.js +48 -10
  58. data/app/javascript/lostcoords/ui/index.js +6 -0
  59. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +27 -27
  60. data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +3 -3
  61. data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +1 -1
  62. data/lib/lostcoords/ui/action_view.rb +1 -0
  63. data/lib/lostcoords/ui/builders.rb +2 -2
  64. data/lib/lostcoords/ui/form_builder.rb +55 -10
  65. data/lib/lostcoords/ui/stylesheets.rb +25 -0
  66. data/lib/lostcoords/ui/version.rb +1 -1
  67. data/lib/tasks/lostcoords_ui.rake +11 -0
  68. metadata +23 -6
  69. data/config/helper_renames.json +0 -111
  70. data/script/migrate_helpers.rb +0 -136
@@ -1,18 +1,16 @@
1
1
  @layer components {
2
- .emoji-picker {
2
+ .lc-emoji-picker {
3
3
  display: flex;
4
4
  gap: 6px;
5
5
  flex-wrap: wrap;
6
6
  align-items: center;
7
7
 
8
- /* Sit above the mobile app nav (z-100 + 25) so the sheet covers it. */
9
- --lc-emoji-sheet-z: calc(100 + 30);
10
8
  --lc-emoji-cell: 2.25rem;
11
9
  }
12
10
 
13
11
  /* ---- Trigger + quick choices -------------------------------------- */
14
- .emoji-picker__choice,
15
- .emoji-picker__trigger {
12
+ .lc-emoji-picker__choice,
13
+ .lc-emoji-picker__trigger {
16
14
  display: inline-grid;
17
15
  place-items: center;
18
16
  inline-size: 44px;
@@ -38,12 +36,12 @@
38
36
  }
39
37
  }
40
38
 
41
- .emoji-picker__trigger {
39
+ .lc-emoji-picker__trigger {
42
40
  color: var(--muted-foreground);
43
41
  font-weight: var(--font-weight-normal, 400);
44
42
  }
45
43
 
46
- .emoji-picker__more {
44
+ .lc-emoji-picker__more {
47
45
  position: relative;
48
46
  display: inline-flex;
49
47
  }
@@ -58,7 +56,7 @@
58
56
  dims everything beneath it — including the app nav. The allow-discrete
59
57
  transitions keep it in the top layer while it eases out, so it fades
60
58
  both ways instead of snapping away on close. */
61
- .emoji-picker__popover::backdrop {
59
+ .lc-emoji-picker__popover::backdrop {
62
60
  background: var(--lc-scrim);
63
61
  opacity: 0;
64
62
  transition:
@@ -72,7 +70,7 @@
72
70
  /* Only the phone sheet dims the page; the desktop popover rides the top
73
71
  layer too (so nothing paints over it) but floats over an undimmed page. */
74
72
  @media (width < 40rem) {
75
- .emoji-picker__popover:popover-open::backdrop {
73
+ .lc-emoji-picker__popover:popover-open::backdrop {
76
74
  opacity: 1;
77
75
 
78
76
  @starting-style { opacity: 0; }
@@ -82,7 +80,7 @@
82
80
  /* Phones get a solid sheet; on desktop the host paints the popover as a
83
81
  floating surface (glass in wariwari), so the gem leaves it unpainted. */
84
82
  @media (width < 40rem) {
85
- .emoji-picker__popover {
83
+ .lc-emoji-picker__popover {
86
84
  border: 1px solid var(--lc-glass-edge, var(--border));
87
85
  border-block-end: 0;
88
86
  background: var(--background);
@@ -90,11 +88,11 @@
90
88
  }
91
89
  }
92
90
 
93
- .emoji-picker__popover {
91
+ .lc-emoji-picker__popover {
94
92
  position: fixed;
95
93
  inset: auto 0 0;
96
94
  margin: 0;
97
- z-index: var(--lc-emoji-sheet-z);
95
+ z-index: var(--lc-z-overlay);
98
96
  display: none;
99
97
  max-inline-size: none;
100
98
  color: var(--foreground);
@@ -125,7 +123,7 @@
125
123
 
126
124
  /* Fallback for browsers without the Popover API: class-driven display. */
127
125
  @supports not selector(:popover-open) {
128
- .emoji-picker__popover[data-open] {
126
+ .lc-emoji-picker__popover[data-open] {
129
127
  display: flex;
130
128
  transform: translateY(0);
131
129
  }
@@ -135,7 +133,7 @@
135
133
  is large; the host's sheet draws the pill.
136
134
  touch-action: none lets a vertical drag pull the sheet down rather than
137
135
  scroll. Drag-to-dismiss is wired by the sheet controller. */
138
- .emoji-picker__handle {
136
+ .lc-emoji-picker__handle {
139
137
  flex: 0 0 auto;
140
138
  display: grid;
141
139
  place-items: center;
@@ -145,10 +143,10 @@
145
143
  cursor: grab;
146
144
  }
147
145
 
148
- .emoji-picker__handle:active { cursor: grabbing; }
146
+ .lc-emoji-picker__handle:active { cursor: grabbing; }
149
147
 
150
148
  /* ---- Search row --------------------------------------------------- */
151
- .emoji-picker__search {
149
+ .lc-emoji-picker__search {
152
150
  flex: 0 0 auto;
153
151
  display: flex;
154
152
  gap: 0.5rem;
@@ -156,14 +154,14 @@
156
154
  padding: 0.75rem 0.75rem 0.5rem;
157
155
  }
158
156
 
159
- .emoji-picker__search-field {
157
+ .lc-emoji-picker__search-field {
160
158
  position: relative;
161
159
  flex: 1 1 auto;
162
160
  display: flex;
163
161
  align-items: center;
164
162
  }
165
163
 
166
- .emoji-picker__search-icon {
164
+ .lc-emoji-picker__search-icon {
167
165
  position: absolute;
168
166
  inset-inline-start: 14px;
169
167
  inline-size: 18px;
@@ -172,7 +170,7 @@
172
170
  pointer-events: none;
173
171
  }
174
172
 
175
- .emoji-picker__search-input {
173
+ .lc-emoji-picker__search-input {
176
174
  inline-size: 100%;
177
175
  min-block-size: 44px;
178
176
  padding-inline: 40px 1rem;
@@ -198,12 +196,12 @@
198
196
  }
199
197
 
200
198
  /* ---- Skin-tone control ------------------------------------------- */
201
- .emoji-picker__tones {
199
+ .lc-emoji-picker__tones {
202
200
  position: relative;
203
201
  flex: 0 0 auto;
204
202
  }
205
203
 
206
- .emoji-picker__tone-toggle {
204
+ .lc-emoji-picker__tone-toggle {
207
205
  display: inline-grid;
208
206
  place-items: center;
209
207
  inline-size: 44px;
@@ -221,7 +219,7 @@
221
219
  &[aria-expanded="true"] { background: color-mix(in oklab, var(--primary) 12%, var(--background)); }
222
220
  }
223
221
 
224
- .emoji-picker__tone-menu {
222
+ .lc-emoji-picker__tone-menu {
225
223
  position: absolute;
226
224
  inset-block-end: calc(100% + 6px);
227
225
  inset-inline-end: 0;
@@ -234,7 +232,7 @@
234
232
  &[hidden] { display: none; }
235
233
  }
236
234
 
237
- .emoji-picker__tone {
235
+ .lc-emoji-picker__tone {
238
236
  display: inline-grid;
239
237
  place-items: center;
240
238
  inline-size: 36px;
@@ -255,7 +253,7 @@
255
253
 
256
254
  /* Category tabs: a segmented track, like the app's tab strips — a paper-2
257
255
  track with the active tab raised on a paper thumb. */
258
- .emoji-picker__tabs {
256
+ .lc-emoji-picker__tabs {
259
257
  flex: 0 0 auto;
260
258
  display: flex;
261
259
  gap: 2px;
@@ -269,7 +267,7 @@
269
267
  &::-webkit-scrollbar { display: none; }
270
268
  }
271
269
 
272
- .emoji-picker__tab {
270
+ .lc-emoji-picker__tab {
273
271
  flex: 0 0 auto;
274
272
  display: inline-grid;
275
273
  place-items: center;
@@ -294,7 +292,7 @@
294
292
  }
295
293
 
296
294
  /* ---- Scrollable body --------------------------------------------- */
297
- .emoji-picker__body {
295
+ .lc-emoji-picker__body {
298
296
  position: relative;
299
297
  flex: 1 1 auto;
300
298
  overflow-y: auto;
@@ -309,15 +307,15 @@
309
307
  @media (prefers-reduced-motion: reduce) { scroll-behavior: auto; }
310
308
  }
311
309
 
312
- .emoji-picker__category + .emoji-picker__category,
313
- .emoji-picker__recent + .emoji-picker__catalog,
314
- .emoji-picker__results + * {
310
+ .lc-emoji-picker__category + .lc-emoji-picker__category,
311
+ .lc-emoji-picker__recent + .lc-emoji-picker__catalog,
312
+ .lc-emoji-picker__results + * {
315
313
  margin-block-start: 0.5rem;
316
314
  }
317
315
 
318
316
  /* Heads scroll with their section: the tab track already marks the current
319
317
  category, and a sticky band would sit as a stripe across the panel. */
320
- .emoji-picker__category-head {
318
+ .lc-emoji-picker__category-head {
321
319
  display: flex;
322
320
  align-items: baseline;
323
321
  justify-content: space-between;
@@ -338,7 +336,7 @@
338
336
  }
339
337
  }
340
338
 
341
- .emoji-picker__grid {
339
+ .lc-emoji-picker__grid {
342
340
  display: grid;
343
341
  grid-template-columns: repeat(auto-fill, minmax(var(--lc-emoji-cell), 1fr));
344
342
  gap: 2px;
@@ -370,13 +368,13 @@
370
368
  }
371
369
 
372
370
  /* 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 {
371
+ .lc-emoji-picker__category,
372
+ .lc-emoji-picker__recent,
373
+ .lc-emoji-picker__results {
376
374
  scroll-margin-block-start: 0;
377
375
  }
378
376
 
379
- .emoji-picker__empty {
377
+ .lc-emoji-picker__empty {
380
378
  margin: 0;
381
379
  padding: 1rem 0.5rem;
382
380
  color: var(--muted-foreground);
@@ -385,16 +383,16 @@
385
383
  }
386
384
 
387
385
  /* ---- Search mode: swap catalog for a single ranked result list ---- */
388
- .emoji-picker__popover:not([data-searching]) .emoji-picker__results { display: none; }
386
+ .lc-emoji-picker__popover:not([data-searching]) .lc-emoji-picker__results { display: none; }
389
387
 
390
- .emoji-picker__popover[data-searching] {
391
- .emoji-picker__tabs,
392
- .emoji-picker__recent,
393
- .emoji-picker__catalog { display: none; }
388
+ .lc-emoji-picker__popover[data-searching] {
389
+ .lc-emoji-picker__tabs,
390
+ .lc-emoji-picker__recent,
391
+ .lc-emoji-picker__catalog { display: none; }
394
392
  }
395
393
 
396
394
  /* ---- Preview footer ---------------------------------------------- */
397
- .emoji-picker__preview {
395
+ .lc-emoji-picker__preview {
398
396
  flex: 0 0 auto;
399
397
  display: flex;
400
398
  gap: 0.5rem;
@@ -403,12 +401,12 @@
403
401
  padding: 0.25rem 1rem 0.75rem;
404
402
  }
405
403
 
406
- .emoji-picker__preview-emoji {
404
+ .lc-emoji-picker__preview-emoji {
407
405
  font-size: 1.5rem;
408
406
  line-height: 1;
409
407
  }
410
408
 
411
- .emoji-picker__preview-name {
409
+ .lc-emoji-picker__preview-name {
412
410
  overflow: hidden;
413
411
  font-size: 1rem;
414
412
  color: var(--muted-foreground);
@@ -422,7 +420,7 @@
422
420
 
423
421
  /* ==================================================================== */
424
422
  @media (width >= 40rem) {
425
- .emoji-picker__popover {
423
+ .lc-emoji-picker__popover {
426
424
  position: absolute;
427
425
  inset-block: calc(100% + 8px) auto;
428
426
  inset-inline: 0 auto;
@@ -452,10 +450,10 @@
452
450
  @media (prefers-reduced-motion: reduce) { transition: none; }
453
451
  }
454
452
 
455
- .emoji-picker__handle { display: none; }
453
+ .lc-emoji-picker__handle { display: none; }
456
454
 
457
- main:has(.emoji-picker__popover[data-open]),
458
- .emoji-picker:has(.emoji-picker__popover[data-open]) {
455
+ main:has(.lc-emoji-picker__popover[data-open]),
456
+ .lc-emoji-picker:has(.lc-emoji-picker__popover[data-open]) {
459
457
  z-index: var(--lc-z-overlay);
460
458
  }
461
459
  }
@@ -21,55 +21,3 @@
21
21
  }
22
22
  }
23
23
  }
24
-
25
- @layer components {
26
- /* Desktop: the .lc-sheet becomes an anchored popover instead of a bottom sheet. */
27
- @media (width >= 40rem) {
28
- .lc-sheet--anchored {
29
- @apply lc-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. --lc-pop-y sets the
52
- direction: popovers drop from their trigger, dialogs rise. */
53
- .lc-popover-pop {
54
- --lc-pop-from: translateY(var(--lc-pop-y, -0.25rem)) scale(0.96);
55
-
56
- transform-origin: var(--lc-pop-origin, top);
57
- transition:
58
- opacity 160ms var(--lc-ease-out-expo),
59
- transform 240ms var(--lc-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(--lc-pop-from);
66
- }
67
-
68
- &:is(:not([hidden], [popover], dialog), :popover-open, dialog[open]) {
69
- @starting-style {
70
- opacity: 0;
71
- transform: var(--lc-pop-from);
72
- }
73
- }
74
- }
75
- }
@@ -58,3 +58,9 @@
58
58
  }
59
59
  }
60
60
  }
61
+
62
+ @layer utilities {
63
+ .lc-prose :where(code):not(:where(pre code)) {
64
+ @apply lc-inline-code;
65
+ }
66
+ }
@@ -1,66 +1,5 @@
1
1
  @layer components {
2
2
  @media (width < 40rem) {
3
- .lc-sheet {
4
- position: fixed;
5
-
6
- /* Anchored from the top edge at the screen height it opened at
7
- (sheet_controller), then lifted by its own height. In-app browsers
8
- (Brave, Chrome) resize the page under the keyboard; anything hung
9
- from the bottom edge jumps a frame as they do, but the top holds. */
10
- inset: var(--lc-sheet-floor, 100dvh) 0 auto;
11
- translate: 0 -100%;
12
- margin: 0;
13
- z-index: var(--lc-z-sheet);
14
- inline-size: 100%;
15
- max-block-size: calc(var(--lc-sheet-floor, 100dvh) * 0.9);
16
- overflow-y: auto;
17
- overscroll-behavior: contain;
18
- padding-block-end: max(env(safe-area-inset-bottom), 1rem);
19
- border: 1px solid var(--lc-glass-edge);
20
- border-start-start-radius: var(--radius-3xl);
21
- border-start-end-radius: var(--radius-3xl);
22
-
23
- /* Published as a variable, not just applied, because the sticky chrome
24
- that rides on top of the sheet — the grabber bar, the pinned action
25
- row — has to paint the *same* surface or it reads as a seam. In dark
26
- mode the sheet is a lightened paper-2/paper-3 mix while --background is
27
- 0.045 lightness darker, which is exactly the band you could see. */
28
- --lc-sheet-bg: var(--background);
29
- @supports (background: light-dark(white, black)) {
30
- --lc-sheet-bg: light-dark(var(--background), color-mix(in oklab, var(--muted) 92%, var(--accent)));
31
- }
32
- background: var(--lc-sheet-bg);
33
- color: var(--foreground);
34
- box-shadow: 0 -12px 40px -12px color-mix(in oklab, var(--foreground) 45%, transparent);
35
- transform: translateY(var(--lc-sheet-offscreen));
36
- transition:
37
- transform var(--lc-sheet-duration) var(--lc-ease-sheet),
38
- display var(--lc-sheet-duration) allow-discrete,
39
- overlay var(--lc-sheet-duration) allow-discrete;
40
- }
41
-
42
- /* Popover-API reveal (currency): top layer drives display, so showPopover()/
43
- hidePopover() animate the slide both ways, with a native ::backdrop. */
44
- .lc-sheet[popover] { display: none; flex-direction: column; max-block-size: min(calc(var(--lc-sheet-floor, 100svh) * 0.72), 34rem); }
45
- .lc-sheet[popover]:popover-open {
46
- display: flex;
47
- transform: translateY(0);
48
-
49
- @starting-style { transform: translateY(var(--lc-sheet-offscreen)); }
50
- }
51
- .lc-sheet[popover]::backdrop {
52
- background: var(--lc-scrim);
53
- backdrop-filter: blur(2px);
54
- opacity: 0;
55
- transition:
56
- opacity var(--lc-sheet-duration) var(--lc-ease-sheet),
57
- display var(--lc-sheet-duration) allow-discrete,
58
- overlay var(--lc-sheet-duration) allow-discrete;
59
-
60
- @media (prefers-reduced-motion: reduce) { transition: none; }
61
- }
62
- .lc-sheet[popover]:popover-open::backdrop { opacity: 1; @starting-style { opacity: 0; } }
63
-
64
3
  /* UIKit's 36×5 grabber. */
65
4
  :is([data-lc-sheet-target~="handle"], dialog[data-controller~="lc-sheet"])::before {
66
5
  content: "";
@@ -74,7 +13,7 @@
74
13
  }
75
14
 
76
15
  @layer modules {
77
- /* Phones: every modal dialog is an iOS sheet (sheet_controller). It floats
16
+ /* Phones: every modal dialog is an iOS sheet (lc-sheet). It floats
78
17
  inset with all corners round while it is short, and docks to the screen
79
18
  edges as it reaches the large detent: --lc-sheet-p runs 0 → 1 between them.
80
19
  Lives in this layer so it out-ranks a dialog's own frame. */
@@ -88,7 +27,7 @@
88
27
 
89
28
  position: fixed;
90
29
 
91
- /* Anchored from the top edge, like .lc-sheet: the keyboard covers it and
30
+ /* Anchored from the top edge: the keyboard covers it and
92
31
  nothing moves. */
93
32
  inset: calc(var(--lc-sheet-floor, 100dvh) - var(--lc-sheet-gap)) 0 auto;
94
33
  translate: 0 -100%;
@@ -151,7 +90,7 @@
151
90
  }
152
91
 
153
92
  /* Behind a large sheet the page shrinks back and rounds off, over black,
154
- as UIKit's page sheet does; sheet_controller drives the variables. */
93
+ as UIKit's page sheet does; lc-sheet drives the variables. */
155
94
  html[data-lc-sheet-page] {
156
95
  background: #000;
157
96
 
@@ -15,6 +15,11 @@
15
15
  --color-secondary-foreground: var(--secondary-foreground);
16
16
  --color-muted: var(--muted);
17
17
  --color-muted-foreground: var(--muted-foreground);
18
+
19
+ /* Muted fills that a card switches to stay visible on it (card.css): bg-tile for choice tiles,
20
+ alerts, neutral badges, skeletons and key hints; bg-track for toggle group tracks. */
21
+ --color-tile: var(--lc-tile-bg, var(--muted));
22
+ --color-track: var(--lc-track-bg, var(--muted));
18
23
  --color-accent: var(--accent);
19
24
  --color-accent-foreground: var(--accent-foreground);
20
25
  --color-destructive: var(--destructive);
@@ -56,6 +61,11 @@
56
61
  --spacing-control-sm: var(--lc-control-sm);
57
62
  --spacing-control-md: var(--lc-control-md);
58
63
  --spacing-control-lg: var(--lc-control-lg);
64
+
65
+ /* In-page layers: z-raised lifts content over its neighbours, z-sticky keeps
66
+ app chrome above content. Overlays need no z-index; see --lc-z-popover. */
67
+ --z-index-raised: var(--lc-z-raised);
68
+ --z-index-sticky: var(--lc-z-sticky);
59
69
  }
60
70
 
61
71
  @theme {
@@ -116,8 +126,13 @@
116
126
  --lc-sheet-duration: 500ms;
117
127
  --lc-sheet-offscreen: calc(100% + 2rem);
118
128
 
129
+ --lc-z-raised: 1;
130
+ --lc-z-sticky: 20;
131
+
132
+ /* Dialogs, sheets, menus, popovers, tooltips and toasts open in the
133
+ browser's top layer, above every z-index. These order them only where
134
+ the Popover API is missing. */
119
135
  --lc-z-popover: 30;
120
- --lc-z-sheet: 50;
121
136
  --lc-z-overlay: 60;
122
137
  --lc-z-toast: 70;
123
138
 
@@ -0,0 +1,23 @@
1
+ @layer components {
2
+ /* lc-toast-region opens it as a popover; these rules also override the
3
+ popover defaults, which would centre it and hide it while closed. */
4
+ .lc-toast-region {
5
+ position: fixed;
6
+ inset: auto var(--spacing-gutter) var(--spacing-gutter) auto;
7
+ z-index: var(--lc-z-toast);
8
+ display: flex;
9
+ flex-direction: column;
10
+ align-items: flex-end;
11
+ gap: var(--spacing-related);
12
+ max-inline-size: calc(100vw - var(--spacing-gutter) * 2);
13
+ margin: 0;
14
+ padding: 0;
15
+ overflow: visible;
16
+ border: 0;
17
+ background: none;
18
+ color: inherit;
19
+
20
+ &:dir(rtl) { inset: auto auto var(--spacing-gutter) var(--spacing-gutter); }
21
+ &:empty { display: none; }
22
+ }
23
+ }
@@ -1,4 +1,6 @@
1
1
  @layer components {
2
+ /* lc-tooltip lifts the content into the top layer while it shows. Without
3
+ JavaScript or the Popover API it shows in place, over its neighbours. */
2
4
  .lc-tooltip__content {
3
5
  position: absolute;
4
6
  inset-block-end: calc(100% + var(--spacing-related));
@@ -6,7 +8,9 @@
6
8
  z-index: var(--lc-z-popover);
7
9
  inline-size: max-content;
8
10
  max-inline-size: min(16rem, calc(100vw - 2rem));
11
+ overflow: visible;
9
12
  padding: var(--spacing-tight) var(--spacing-related);
13
+ border: 0;
10
14
  border-radius: var(--radius-md);
11
15
  background: var(--foreground);
12
16
  color: var(--background);
@@ -17,31 +21,28 @@
17
21
  pointer-events: none;
18
22
  opacity: 0;
19
23
  visibility: hidden;
20
- translate: -50% 0.125rem;
24
+ translate: -50% 0;
25
+
26
+ /* allow-discrete keeps a lifted tooltip in the top layer while it fades out. */
21
27
  transition:
22
28
  opacity 120ms var(--lc-ease-out),
23
- translate 120ms var(--lc-ease-out),
24
- visibility 120ms;
29
+ visibility 120ms,
30
+ display 120ms allow-discrete,
31
+ overlay 120ms allow-discrete;
25
32
 
26
- &:dir(rtl) { translate: 50% 0.125rem; }
33
+ &:dir(rtl) { translate: 50% 0; }
27
34
  }
28
35
 
29
36
  .lc-tooltip[data-side="bottom"] > .lc-tooltip__content {
30
37
  inset-block: calc(100% + var(--spacing-related)) auto;
31
- translate: -50% -0.125rem;
32
38
  }
33
39
 
34
- .lc-tooltip:is(:hover, :has(:focus-visible)) > .lc-tooltip__content {
40
+ .lc-tooltip:is([data-open], :hover, :has(:focus-visible)) > .lc-tooltip__content {
35
41
  opacity: 1;
36
42
  visibility: visible;
37
- translate: -50% 0;
38
43
  transition-delay: 300ms;
39
44
  }
40
45
 
41
- .lc-tooltip:is(:hover, :has(:focus-visible)) > .lc-tooltip__content:dir(rtl) {
42
- translate: 50% 0;
43
- }
44
-
45
46
  .lc-tooltip[data-dismissed] > .lc-tooltip__content {
46
47
  opacity: 0;
47
48
  visibility: hidden;
@@ -31,7 +31,7 @@
31
31
  text-box: trim-both cap alphabetic; /* browser-compat: enhancement */
32
32
  }
33
33
 
34
- /* Inline code inside running text. base.css applies it to every <code>; prose
34
+ /* Inline code inside running text. base.css applies it to every <code>; prose.css
35
35
  applies it inside .lc-prose. */
36
36
  @utility lc-inline-code {
37
37
  font-family: var(--font-mono);
@@ -49,9 +49,3 @@
49
49
  content: none;
50
50
  }
51
51
  }
52
-
53
- @layer utilities {
54
- .lc-prose :where(code):not(:where(pre code)) {
55
- @apply lc-inline-code;
56
- }
57
- }
@@ -58,7 +58,7 @@ module Lostcoords::Ui
58
58
  "font-sans text-caption font-medium whitespace-nowrap"
59
59
 
60
60
  BADGE_TONES = {
61
- neutral: "bg-muted text-muted-foreground",
61
+ neutral: "bg-tile text-muted-foreground",
62
62
  success: "bg-success-soft text-success",
63
63
  warning: "bg-warning-soft text-warning",
64
64
  destructive: "bg-destructive-soft text-destructive",
@@ -86,7 +86,7 @@ module Lostcoords::Ui
86
86
  shape = lc_option!(:shape, shape, ICON_BADGE_SHAPES.keys)
87
87
  tone = lc_option!(:tone, tone, BADGE_TONES.keys)
88
88
  lc_tw(ICON_BADGE_BASE, ICON_BADGE_SIZES.fetch(size), ICON_BADGE_SHAPES.fetch(shape),
89
- (tone == :neutral ? "bg-muted text-foreground" : BADGE_TONES.fetch(tone)), extra)
89
+ (tone == :neutral ? "bg-tile text-foreground" : BADGE_TONES.fetch(tone)), extra)
90
90
  end
91
91
 
92
92
  def lc_avatar_class(size: :md, ink: false, linked: false, extra: nil)
@@ -1,10 +1,16 @@
1
1
  module Lostcoords::Ui
2
2
  module ButtonHelper
3
+ # Disabled controls drop their variant colours for a faint tint and dim ink, so they read as unavailable on any surface.
4
+ BUTTON_DISABLED =
5
+ "disabled:cursor-not-allowed disabled:shadow-none " \
6
+ "disabled:[--lc-button-bg:color-mix(in_oklab,var(--foreground)_10%,transparent)] " \
7
+ "disabled:text-[color-mix(in_oklab,var(--foreground)_38%,transparent)]"
8
+
3
9
  BUTTON_BASE =
4
10
  "inline-flex items-center justify-center gap-related rounded-pill font-sans " \
5
11
  "whitespace-nowrap no-underline cursor-pointer " \
6
12
  "transition-[background-color,border-color,color,box-shadow,transform,translate,scale,filter] duration-(--lc-motion-control) ease-(--lc-ease-out) " \
7
- "motion-safe:hover:-translate-y-px motion-safe:active:translate-y-px active:shadow-none disabled:opacity-40 disabled:pointer-events-none " \
13
+ "motion-safe:not-disabled:hover:-translate-y-px motion-safe:not-disabled:active:translate-y-px active:shadow-none #{BUTTON_DISABLED} " \
8
14
  "[&_svg]:block [&_svg:not([class*='size-'])]:size-[1.1em] [&_svg]:shrink-0"
9
15
 
10
16
  # Height comes from the shared control sizes; an icon trims the padding on its side.
@@ -15,15 +21,15 @@ module Lostcoords::Ui
15
21
  }.freeze
16
22
 
17
23
  BUTTON_HOVER =
18
- "hover:bg-[color-mix(in_oklab,var(--lc-button-bg,var(--secondary))_94%,var(--lc-button-ink,var(--foreground)))] " \
19
- "active:bg-[color-mix(in_oklab,var(--lc-button-bg,var(--secondary))_94%,var(--lc-button-ink,var(--foreground)))]"
24
+ "not-disabled:hover:bg-[color-mix(in_oklab,var(--lc-button-bg,var(--secondary))_94%,var(--lc-button-ink,var(--foreground)))] " \
25
+ "not-disabled:active:bg-[color-mix(in_oklab,var(--lc-button-bg,var(--secondary))_94%,var(--lc-button-ink,var(--foreground)))]"
20
26
 
21
27
  BUTTON_VARIANTS = {
22
28
  primary: "border border-transparent [--lc-button-bg:var(--primary)] [--lc-button-ink:var(--primary-foreground)] bg-(--lc-button-bg) text-(--lc-button-ink) #{BUTTON_HOVER}",
23
29
  secondary: "border border-transparent [--lc-button-bg:var(--secondary)] [--lc-button-ink:var(--secondary-foreground)] bg-(--lc-button-bg) text-(--lc-button-ink) #{BUTTON_HOVER}",
24
30
  outline: "border border-border bg-transparent text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
25
31
  glass: "[--lc-button-bg:var(--_lc-glass-fill)] text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
26
- ghost: "border border-transparent bg-transparent text-muted-foreground hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
32
+ ghost: "border border-transparent bg-transparent text-muted-foreground not-disabled:hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
27
33
  destructive: "border border-transparent [--lc-button-bg:color-mix(in_oklab,var(--destructive)_60%,black)] [--lc-button-ink:#fff] bg-(--lc-button-bg) text-(--lc-button-ink) #{BUTTON_HOVER}"
28
34
  }.freeze
29
35
 
@@ -31,8 +37,8 @@ module Lostcoords::Ui
31
37
  "lc-press inline-grid place-items-center shrink-0 border-0 bg-transparent " \
32
38
  "text-muted-foreground cursor-pointer no-underline " \
33
39
  "transition-[background-color,color,transform,scale] duration-(--lc-motion-quick) ease-(--lc-ease-out) " \
34
- "hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER} " \
35
- "disabled:opacity-40 disabled:pointer-events-none " \
40
+ "not-disabled:hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER} " \
41
+ "#{BUTTON_DISABLED} " \
36
42
  "[&_svg]:block [&_svg]:shrink-0"
37
43
 
38
44
  ICON_BUTTON_SHAPES = {