lostcoords-ui 0.4.1 → 0.5.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +303 -89
  3. data/app/assets/tailwind/lostcoords/ui/avatar.css +6 -6
  4. data/app/assets/tailwind/lostcoords/ui/base.css +45 -0
  5. data/app/assets/tailwind/lostcoords/ui/card.css +20 -22
  6. data/app/assets/tailwind/lostcoords/ui/choice.css +39 -0
  7. data/app/assets/tailwind/lostcoords/ui/code-block.css +10 -0
  8. data/app/assets/tailwind/lostcoords/ui/components.css +25 -0
  9. data/app/assets/tailwind/lostcoords/ui/copy-button.css +9 -9
  10. data/app/assets/tailwind/lostcoords/ui/dialog.css +17 -17
  11. data/app/assets/tailwind/lostcoords/ui/disclosure.css +5 -5
  12. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +43 -43
  13. data/app/assets/tailwind/lostcoords/ui/field.css +2 -2
  14. data/app/assets/tailwind/lostcoords/ui/glass.css +9 -8
  15. data/app/assets/tailwind/lostcoords/ui/layout.css +2 -2
  16. data/app/assets/tailwind/lostcoords/ui/popover.css +14 -14
  17. data/app/assets/tailwind/lostcoords/ui/reading.css +24 -24
  18. data/app/assets/tailwind/lostcoords/ui/sheet.css +60 -60
  19. data/app/assets/tailwind/lostcoords/ui/squircle.css +7 -27
  20. data/app/assets/tailwind/lostcoords/ui/table.css +51 -0
  21. data/app/assets/tailwind/lostcoords/ui/theme.css +194 -225
  22. data/app/assets/tailwind/lostcoords/ui/tooltip.css +50 -0
  23. data/app/assets/tailwind/lostcoords/ui/typography.css +37 -61
  24. data/app/assets/tailwind/lostcoords/ui/ui.css +2 -21
  25. data/app/assets/tailwind/lostcoords/ui/utilities.css +14 -22
  26. data/app/helpers/lostcoords/ui/badge_helper.rb +67 -110
  27. data/app/helpers/lostcoords/ui/button_helper.rb +36 -33
  28. data/app/helpers/lostcoords/ui/card_helper.rb +9 -52
  29. data/app/helpers/lostcoords/ui/choice_helper.rb +83 -0
  30. data/app/helpers/lostcoords/ui/code_block_helper.rb +22 -0
  31. data/app/helpers/lostcoords/ui/copy_button_helper.rb +7 -7
  32. data/app/helpers/lostcoords/ui/description_list_helper.rb +12 -0
  33. data/app/helpers/lostcoords/ui/dialog_helper.rb +6 -13
  34. data/app/helpers/lostcoords/ui/feedback_helper.rb +25 -6
  35. data/app/helpers/lostcoords/ui/form_helper.rb +36 -59
  36. data/app/helpers/lostcoords/ui/glass_helper.rb +7 -7
  37. data/app/helpers/lostcoords/ui/layout_helper.rb +13 -11
  38. data/app/helpers/lostcoords/ui/menu_helper.rb +8 -8
  39. data/app/helpers/lostcoords/ui/navigation_helper.rb +131 -0
  40. data/app/helpers/lostcoords/ui/overlay_helper.rb +17 -0
  41. data/app/helpers/lostcoords/ui/primitives_helper.rb +39 -61
  42. data/app/helpers/lostcoords/ui/reading_helper.rb +2 -2
  43. data/app/helpers/lostcoords/ui/sheet_helper.rb +3 -45
  44. data/app/helpers/lostcoords/ui/structures_helper.rb +55 -64
  45. data/app/helpers/lostcoords/ui/table_helper.rb +11 -0
  46. data/app/helpers/lostcoords/ui/tailwind_helper.rb +13 -4
  47. data/app/helpers/lostcoords/ui/typography_helper.rb +11 -11
  48. data/app/javascript/lostcoords/ui/controllers/lc_code_block_controller.js +23 -0
  49. data/app/javascript/lostcoords/ui/controllers/{disclosure_controller.js → lc_collapsible_controller.js} +3 -3
  50. data/app/javascript/lostcoords/ui/controllers/{confirm_dialog_controller.js → lc_confirm_dialog_controller.js} +4 -4
  51. data/app/javascript/lostcoords/ui/controllers/{modal_controller.js → lc_dialog_controller.js} +1 -1
  52. data/app/javascript/lostcoords/ui/controllers/{emoji_picker_controller.js → lc_emoji_picker_controller.js} +4 -4
  53. data/app/javascript/lostcoords/ui/controllers/{row_menu_controller.js → lc_menu_controller.js} +3 -3
  54. data/app/javascript/lostcoords/ui/controllers/{ui_popover_controller.js → lc_popover_controller.js} +1 -1
  55. data/app/javascript/lostcoords/ui/controllers/{ui_selection_controller.js → lc_selection_controller.js} +1 -1
  56. data/app/javascript/lostcoords/ui/controllers/{sheet_controller.js → lc_sheet_controller.js} +15 -15
  57. data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +39 -0
  58. data/app/javascript/lostcoords/ui/controllers/{toast_controller.js → lc_toast_controller.js} +2 -2
  59. data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +14 -0
  60. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +5 -5
  61. data/app/javascript/lostcoords/ui/helpers/context_dialog.js +6 -6
  62. data/app/javascript/lostcoords/ui/helpers/dialog.js +2 -2
  63. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +7 -7
  64. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +2 -2
  65. data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +4 -4
  66. data/app/javascript/lostcoords/ui/index.js +51 -0
  67. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +30 -30
  68. data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +18 -0
  69. data/app/views/lostcoords/ui/components/_lc_collapsible.html.erb +5 -0
  70. data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +11 -8
  71. data/app/views/lostcoords/ui/components/_lc_floating_panel.html.erb +8 -0
  72. data/config/helper_renames.json +23 -25
  73. data/config/importmap.rb +10 -2
  74. data/config/locales/en.yml +23 -0
  75. data/config/locales/es.yml +23 -0
  76. data/config/locales/ja.yml +23 -0
  77. data/lib/lostcoords/ui/action_view.rb +5 -3
  78. data/lib/lostcoords/ui/builders.rb +1 -1
  79. data/lib/lostcoords/ui/form_builder.rb +113 -0
  80. data/lib/lostcoords/ui/parts.rb +154 -0
  81. data/lib/lostcoords/ui/tokens.rb +24 -1
  82. data/lib/lostcoords/ui/version.rb +1 -1
  83. metadata +33 -17
  84. data/app/views/lostcoords/ui/components/_form_alert.html.erb +0 -4
  85. data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +0 -5
  86. data/app/views/lostcoords/ui/components/_skeleton.html.erb +0 -4
  87. data/app/views/lostcoords/ui/components/_spinner.html.erb +0 -10
  88. data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +0 -8
  89. /data/app/javascript/lostcoords/ui/controllers/{ui_copy_controller.js → lc_copy_controller.js} +0 -0
  90. /data/app/javascript/lostcoords/ui/controllers/{ui_toggle_controller.js → lc_toggle_controller.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { CatalogPicker } from "helpers/catalog_picker"
2
- import { prefersReducedMotion } from "helpers/viewport"
1
+ import { CatalogPicker } from "lostcoords/ui/helpers/catalog_picker"
2
+ import { prefersReducedMotion } from "lostcoords/ui/helpers/viewport"
3
3
 
4
4
  const TONE_MODIFIERS = ["", "\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"]
5
- const TONE_STORAGE_KEY = "lostcoords_ui:emoji-picker:tone"
5
+ const TONE_STORAGE_KEY = "lostcoords_ui:lc-emoji-picker:tone"
6
6
 
7
7
  function markCurrent(element, current) {
8
8
  element.setAttribute("aria-current", String(current))
@@ -322,7 +322,7 @@ export default class extends CatalogPicker {
322
322
  return {
323
323
  duration,
324
324
  settle: duration && 280,
325
- transition: "transform 260ms var(--ease-out-expo, ease-out)",
325
+ transition: "transform 260ms var(--lc-ease-out-expo, ease-out)",
326
326
  offscreen: "translateY(100%)",
327
327
  }
328
328
  }
@@ -1,5 +1,5 @@
1
- import { FloatingPanel } from "helpers/floating_panel"
2
- import { focusFirstMenuItem, navigateMenu } from "helpers/menu_keys"
1
+ import { FloatingPanel } from "lostcoords/ui/helpers/floating_panel"
2
+ import { focusFirstMenuItem, navigateMenu } from "lostcoords/ui/helpers/menu_keys"
3
3
 
4
4
  // The phone dock's action stack lays itself out over the fixed nav, so it
5
5
  // passes lift: false and stays in place instead of lifting into the top layer.
@@ -29,7 +29,7 @@ export default class extends FloatingPanel {
29
29
 
30
30
  toggleFromRow(event) {
31
31
  if (this.popoverTarget.contains(event.target)) return
32
- if (event.target.closest("a, button, input, select, textarea, [data-row-menu-ignore]")) return
32
+ if (event.target.closest("a, button, input, select, textarea, [data-lc-menu-ignore]")) return
33
33
 
34
34
  event.preventDefault()
35
35
  this.isOpen ? this.close() : this.open()
@@ -1,4 +1,4 @@
1
- import { FloatingPanel } from "helpers/floating_panel"
1
+ import { FloatingPanel } from "lostcoords/ui/helpers/floating_panel"
2
2
 
3
3
  export default class extends FloatingPanel {
4
4
  toggle(event) {
@@ -22,7 +22,7 @@ export default class extends Controller {
22
22
  }
23
23
 
24
24
  select(event) {
25
- const option = event.target.closest('[data-ui-selection-target~="option"]')
25
+ const option = event.target.closest('[data-lc-selection-target~="option"]')
26
26
  if (!this.options.includes(option)) return
27
27
  this.apply(option)
28
28
  this.changed(option)
@@ -1,13 +1,13 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
- import { holdDocumentScroll } from "helpers/dismiss_stack"
3
- import { SHEET_TIMING, sheetDuration } from "helpers/sheet_motion"
4
- import { isSheetViewport, prefersReducedMotion, TEXT_ENTRY } from "helpers/viewport"
2
+ import { holdDocumentScroll } from "lostcoords/ui/helpers/dismiss_stack"
3
+ import { SHEET_TIMING, sheetDuration } from "lostcoords/ui/helpers/sheet_motion"
4
+ import { isSheetViewport, prefersReducedMotion, TEXT_ENTRY } from "lostcoords/ui/helpers/viewport"
5
5
 
6
6
  const PROJECTION_MS = 180
7
7
  const MEDIUM_FRACTION = 0.5
8
8
  const PAGE_SCALE_LOSS = 0.06
9
- const NO_DRAG = "input[type=range], [data-sheet-no-drag]"
10
- const HANDLE = "[data-sheet-target~='handle']"
9
+ const NO_DRAG = "input[type=range], [data-lc-sheet-no-drag]"
10
+ const HANDLE = "[data-lc-sheet-target~='handle']"
11
11
  const GRABBER_ZONE = 28
12
12
  const KEYBOARD_FREE = 0.5
13
13
 
@@ -71,9 +71,9 @@ export default class extends Controller {
71
71
  // The keyboard rises over the sheet. Safari and in-app browsers shrink the
72
72
  // page under it, which would drag a bottom-pinned sheet up and squash it, so
73
73
  // the sheet is placed from the top at the height the screen had when it
74
- // opened (see --sheet-floor in the CSS).
74
+ // opened (see --lc-sheet-floor in the CSS).
75
75
  pinFloor() {
76
- if (this.isSheet) this.element.style.setProperty("--sheet-floor", `${window.innerHeight}px`)
76
+ if (this.isSheet) this.element.style.setProperty("--lc-sheet-floor", `${window.innerHeight}px`)
77
77
  }
78
78
 
79
79
  closed() {
@@ -330,7 +330,7 @@ export default class extends Controller {
330
330
  }
331
331
 
332
332
  setLargeness(p) {
333
- this.element.style.setProperty("--sheet-p", p.toFixed(3))
333
+ this.element.style.setProperty("--lc-sheet-p", p.toFixed(3))
334
334
  }
335
335
 
336
336
  setPage(p) {
@@ -339,12 +339,12 @@ export default class extends Controller {
339
339
  if (p <= 0) return
340
340
  this.releaseScroll = holdDocumentScroll()
341
341
  const lockedTop = -parseFloat(document.body.style.top || "0")
342
- root.style.setProperty("--sheet-page-origin", `${window.scrollY + lockedTop}px`)
342
+ root.style.setProperty("--lc-sheet-page-origin", `${window.scrollY + lockedTop}px`)
343
343
  root.dataset.sheetPage = ""
344
344
  this.ownsPage = true
345
345
  }
346
- root.style.setProperty("--sheet-page-p", p.toFixed(3))
347
- root.style.setProperty("--sheet-page-scale", (1 - PAGE_SCALE_LOSS * p).toFixed(4))
346
+ root.style.setProperty("--lc-sheet-page-p", p.toFixed(3))
347
+ root.style.setProperty("--lc-sheet-page-scale", (1 - PAGE_SCALE_LOSS * p).toFixed(4))
348
348
  }
349
349
 
350
350
  animatePage(p) {
@@ -363,13 +363,13 @@ export default class extends Controller {
363
363
  const root = document.documentElement
364
364
  delete root.dataset.sheetPage
365
365
  for (const name of ["p", "scale", "origin", "transition"])
366
- root.style.removeProperty(`--sheet-page-${name}`)
366
+ root.style.removeProperty(`--lc-sheet-page-${name}`)
367
367
  this.releaseScroll()
368
368
  }
369
369
 
370
370
  pageTransition(value = prefersReducedMotion() ? "0s" : SHEET_TIMING) {
371
371
  if (this.ownsPage || value !== "0s")
372
- document.documentElement.style.setProperty("--sheet-page-transition", value)
372
+ document.documentElement.style.setProperty("--lc-sheet-page-transition", value)
373
373
  }
374
374
 
375
375
  transition() {
@@ -396,8 +396,8 @@ export default class extends Controller {
396
396
  style.transform = ""
397
397
  style.willChange = ""
398
398
  style.blockSize = ""
399
- style.removeProperty("--sheet-p")
400
- style.removeProperty("--sheet-floor")
399
+ style.removeProperty("--lc-sheet-p")
400
+ style.removeProperty("--lc-sheet-floor")
401
401
  }
402
402
 
403
403
  scrollerFor(target) {
@@ -0,0 +1,39 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Tabs with automatic activation: arrow keys, Home and End move focus and select.
4
+ export default class extends Controller {
5
+ static targets = ["tab", "panel"]
6
+
7
+ select(event) {
8
+ this.show(event.currentTarget)
9
+ }
10
+
11
+ navigate(event) {
12
+ const tabs = this.tabTargets.filter((tab) => !tab.disabled)
13
+ const index = tabs.indexOf(event.currentTarget)
14
+ const step = getComputedStyle(this.element).direction === "rtl" ? -1 : 1
15
+ const next = {
16
+ ArrowRight: index + step,
17
+ ArrowLeft: index - step,
18
+ Home: 0,
19
+ End: tabs.length - 1,
20
+ }[event.key]
21
+ if (next === undefined) return
22
+
23
+ event.preventDefault()
24
+ const tab = tabs[(next + tabs.length) % tabs.length]
25
+ tab.focus()
26
+ this.show(tab)
27
+ }
28
+
29
+ show(tab) {
30
+ for (const candidate of this.tabTargets) {
31
+ const selected = candidate === tab
32
+ candidate.setAttribute("aria-selected", String(selected))
33
+ candidate.tabIndex = selected ? 0 : -1
34
+ }
35
+ const panelId = tab.getAttribute("aria-controls")
36
+ for (const panel of this.panelTargets) panel.hidden = panel.id !== panelId
37
+ this.dispatch("change", { detail: { value: tab.dataset.value } })
38
+ }
39
+ }
@@ -1,5 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
- import { prefersReducedMotion } from "helpers/viewport"
2
+ import { prefersReducedMotion } from "lostcoords/ui/helpers/viewport"
3
3
 
4
4
  export default class extends Controller {
5
5
  static values = { timeout: { type: Number, default: 4000 } }
@@ -7,7 +7,7 @@ export default class extends Controller {
7
7
  connect() {
8
8
  for (const sibling of this.element.parentElement?.children || []) {
9
9
  if (sibling === this.element) continue
10
- this.application.getControllerForElementAndIdentifier(sibling, "toast")?.dismiss()
10
+ this.application.getControllerForElementAndIdentifier(sibling, "lc-toast")?.dismiss()
11
11
  }
12
12
 
13
13
  this.startedAt = performance.now()
@@ -0,0 +1,14 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Escape hides a visible tooltip without moving the pointer or focus. It shows
4
+ // again after the pointer leaves or focus moves away.
5
+ export default class extends Controller {
6
+ dismiss(event) {
7
+ if (event.key !== "Escape" || !this.element.matches(":hover, :focus-within")) return
8
+ this.element.dataset.dismissed = ""
9
+ }
10
+
11
+ reset() {
12
+ delete this.element.dataset.dismissed
13
+ }
14
+ }
@@ -1,7 +1,7 @@
1
- import { holdDocumentScroll } from "helpers/dismiss_stack"
2
- import { FloatingPanel } from "helpers/floating_panel"
3
- import { SHEET_TIMING, sheetDuration } from "helpers/sheet_motion"
4
- import { isSheetViewport } from "helpers/viewport"
1
+ import { holdDocumentScroll } from "lostcoords/ui/helpers/dismiss_stack"
2
+ import { FloatingPanel } from "lostcoords/ui/helpers/floating_panel"
3
+ import { SHEET_TIMING, sheetDuration } from "lostcoords/ui/helpers/sheet_motion"
4
+ import { isSheetViewport } from "lostcoords/ui/helpers/viewport"
5
5
 
6
6
  const catalogs = new Map()
7
7
  const MAX_RESULTS = 250
@@ -433,7 +433,7 @@ export class CatalogPicker extends FloatingPanel {
433
433
  duration,
434
434
  settle: duration,
435
435
  transition: `transform ${SHEET_TIMING}`,
436
- offscreen: "translateY(var(--sheet-offscreen))",
436
+ offscreen: "translateY(var(--lc-sheet-offscreen))",
437
437
  }
438
438
  }
439
439
 
@@ -1,4 +1,4 @@
1
- import { isSheetViewport } from "helpers/viewport"
1
+ import { isSheetViewport } from "lostcoords/ui/helpers/viewport"
2
2
 
3
3
  const EDGE = 16
4
4
 
@@ -10,19 +10,19 @@ export function showContextDialog(dialog, menu) {
10
10
  const corner = rect?.height
11
11
  ? { top: Math.max(EDGE, rect.bottom - dialog.offsetHeight), right: rect.right }
12
12
  : null
13
- if (anchorDialog(dialog, corner)) dialog.style.setProperty("--dialog-origin", "bottom right")
13
+ if (anchorDialog(dialog, corner)) dialog.style.setProperty("--lc-dialog-origin", "bottom right")
14
14
  }
15
15
 
16
16
  export function anchorDialog(dialog, corner) {
17
17
  delete dialog.dataset.anchored
18
- dialog.style.removeProperty("--dialog-top")
19
- dialog.style.removeProperty("--dialog-right")
18
+ dialog.style.removeProperty("--lc-dialog-top")
19
+ dialog.style.removeProperty("--lc-dialog-right")
20
20
  if (!corner || isSheetViewport()) return false
21
21
 
22
22
  dialog.dataset.anchored = ""
23
- dialog.style.setProperty("--dialog-top", `${corner.top}px`)
23
+ dialog.style.setProperty("--lc-dialog-top", `${corner.top}px`)
24
24
  dialog.style.setProperty(
25
- "--dialog-right",
25
+ "--lc-dialog-right",
26
26
  `${Math.max(EDGE, window.innerWidth - corner.right)}px`,
27
27
  )
28
28
  return true
@@ -1,4 +1,4 @@
1
- import { registerDismissable } from "helpers/dismiss_stack"
1
+ import { registerDismissable } from "lostcoords/ui/helpers/dismiss_stack"
2
2
 
3
3
  const registrations = new WeakMap()
4
4
 
@@ -8,7 +8,7 @@ export function registerDialog(dialog, { close = () => dialog.close(), lockScrol
8
8
 
9
9
  const unregisterDismissable = registerDismissable({
10
10
  element: dialog,
11
- type: "modal",
11
+ type: "lc-dialog",
12
12
  lockScroll,
13
13
  close,
14
14
  isOpen: () => dialog.open,
@@ -3,9 +3,8 @@ let nextOrder = 0
3
3
  let lockedScrollY = 0
4
4
  let scrollHolds = 0
5
5
 
6
- document.addEventListener("keydown", dismissOnEscape, true)
7
-
8
6
  export function registerDismissable(options) {
7
+ if (stack.length === 0) document.addEventListener("keydown", dismissOnEscape, true)
9
8
  const entry = {
10
9
  type: "panel",
11
10
  isOpen: defaultIsOpen,
@@ -17,7 +16,7 @@ export function registerDismissable(options) {
17
16
  stack.push(entry)
18
17
  syncModalScrollLock()
19
18
 
20
- if (entry.type === "modal" && entry.element instanceof HTMLDialogElement) {
19
+ if (entry.type === "lc-dialog" && entry.element instanceof HTMLDialogElement) {
21
20
  entry.cancel = (event) => cancelDialog(event, entry)
22
21
  entry.element.addEventListener("cancel", entry.cancel)
23
22
  }
@@ -81,7 +80,7 @@ function topDismissable(scope = topModal()?.element) {
81
80
 
82
81
  function topModal() {
83
82
  return openEntries()
84
- .filter((entry) => entry.type === "modal")
83
+ .filter((entry) => entry.type === "lc-dialog")
85
84
  .at(-1)
86
85
  }
87
86
 
@@ -92,6 +91,7 @@ function openEntries() {
92
91
  function unregisterDismissable(entry) {
93
92
  const index = stack.indexOf(entry)
94
93
  if (index >= 0) stack.splice(index, 1)
94
+ if (stack.length === 0) document.removeEventListener("keydown", dismissOnEscape, true)
95
95
 
96
96
  if (entry.cancel) {
97
97
  entry.element.removeEventListener("cancel", entry.cancel)
@@ -116,7 +116,7 @@ function priorityFor(type) {
116
116
  function syncModalScrollLock() {
117
117
  if (
118
118
  scrollHolds > 0 ||
119
- openEntries().some((entry) => entry.type === "modal" && entry.lockScroll !== false)
119
+ openEntries().some((entry) => entry.type === "lc-dialog" && entry.lockScroll !== false)
120
120
  ) {
121
121
  lockDocumentScroll()
122
122
  return
@@ -136,7 +136,7 @@ function lockDocumentScroll() {
136
136
  body.style.top = `-${lockedScrollY}px`
137
137
  // Pinning the body drags the sticky nav off with the scrolled-away page;
138
138
  // the nav offsets itself by this to stay where it was.
139
- body.style.setProperty("--scroll-lock-y", `${lockedScrollY}px`)
139
+ body.style.setProperty("--lc-scroll-lock-y", `${lockedScrollY}px`)
140
140
  body.style.inlineSize = "100%"
141
141
  }
142
142
 
@@ -147,7 +147,7 @@ function unlockDocumentScroll() {
147
147
  body.style.position = ""
148
148
  body.style.inset = ""
149
149
  body.style.top = ""
150
- body.style.removeProperty("--scroll-lock-y")
150
+ body.style.removeProperty("--lc-scroll-lock-y")
151
151
  body.style.inlineSize = ""
152
152
  delete body.dataset.modalScrollLock
153
153
  window.scrollTo({ top: lockedScrollY, behavior: "instant" })
@@ -1,6 +1,6 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
- import { registerDismissable } from "helpers/dismiss_stack"
3
- import { liftToTopLayer } from "helpers/top_layer"
2
+ import { registerDismissable } from "lostcoords/ui/helpers/dismiss_stack"
3
+ import { liftToTopLayer } from "lostcoords/ui/helpers/top_layer"
4
4
 
5
5
  const BEFORE_OPEN = "overlay:before-open"
6
6
  const PAGE_LEAVES = ["turbo:before-cache", "turbo:before-visit", "turbo:before-render"]
@@ -1,13 +1,13 @@
1
- import { prefersReducedMotion } from "helpers/viewport"
2
- // The sheet beat lives in CSS (--sheet-duration, --ease-sheet) so CSS- and
1
+ import { prefersReducedMotion } from "lostcoords/ui/helpers/viewport"
2
+ // The sheet beat lives in CSS (--lc-sheet-duration, --lc-ease-sheet) so CSS- and
3
3
  // JS-driven slides can't drift apart. Inline transitions use the variables
4
4
  // directly; timers need the duration in milliseconds.
5
- export const SHEET_TIMING = "var(--sheet-duration) var(--ease-sheet)"
5
+ export const SHEET_TIMING = "var(--lc-sheet-duration) var(--lc-ease-sheet)"
6
6
 
7
7
  export function sheetDuration() {
8
8
  if (prefersReducedMotion()) return 0
9
9
  const value = getComputedStyle(document.documentElement)
10
- .getPropertyValue("--sheet-duration")
10
+ .getPropertyValue("--lc-sheet-duration")
11
11
  .trim()
12
12
  return value.endsWith("ms") ? parseFloat(value) : parseFloat(value) * 1000
13
13
  }
@@ -0,0 +1,51 @@
1
+ import CodeBlock from "lostcoords/ui/controllers/lc_code_block_controller"
2
+ import Collapsible from "lostcoords/ui/controllers/lc_collapsible_controller"
3
+ import ConfirmDialog from "lostcoords/ui/controllers/lc_confirm_dialog_controller"
4
+ import Copy from "lostcoords/ui/controllers/lc_copy_controller"
5
+ import Dialog from "lostcoords/ui/controllers/lc_dialog_controller"
6
+ import EmojiPicker from "lostcoords/ui/controllers/lc_emoji_picker_controller"
7
+ import Menu from "lostcoords/ui/controllers/lc_menu_controller"
8
+ import Popover from "lostcoords/ui/controllers/lc_popover_controller"
9
+ import Selection from "lostcoords/ui/controllers/lc_selection_controller"
10
+ import Sheet from "lostcoords/ui/controllers/lc_sheet_controller"
11
+ import Tabs from "lostcoords/ui/controllers/lc_tabs_controller"
12
+ import Toast from "lostcoords/ui/controllers/lc_toast_controller"
13
+ import Toggle from "lostcoords/ui/controllers/lc_toggle_controller"
14
+ import Tooltip from "lostcoords/ui/controllers/lc_tooltip_controller"
15
+
16
+ const controllers = {
17
+ "lc-confirm-dialog": ConfirmDialog,
18
+ "lc-collapsible": Collapsible,
19
+ "lc-code-block": CodeBlock,
20
+ "lc-copy": Copy,
21
+ "lc-dialog": Dialog,
22
+ "lc-emoji-picker": EmojiPicker,
23
+ "lc-menu": Menu,
24
+ "lc-popover": Popover,
25
+ "lc-selection": Selection,
26
+ "lc-sheet": Sheet,
27
+ "lc-tabs": Tabs,
28
+ "lc-toast": Toast,
29
+ "lc-toggle": Toggle,
30
+ "lc-tooltip": Tooltip,
31
+ }
32
+ const registrations = new WeakMap()
33
+
34
+ export function registerControllers(application, { only = Object.keys(controllers) } = {}) {
35
+ if (!Array.isArray(only)) throw new TypeError("only must be an array of controller identifiers")
36
+ for (const identifier of only) {
37
+ if (!Object.hasOwn(controllers, identifier)) {
38
+ throw new Error(`Unknown lostcoords-ui controller: ${identifier}`)
39
+ }
40
+ }
41
+ let registered = registrations.get(application)
42
+ if (!registered) {
43
+ registered = new Set()
44
+ registrations.set(application, registered)
45
+ }
46
+ for (const identifier of only) {
47
+ if (registered.has(identifier)) continue
48
+ application.register(identifier, controllers[identifier])
49
+ registered.add(identifier)
50
+ }
51
+ }
@@ -7,10 +7,10 @@
7
7
  tone_modifiers = [ "", "🏻", "🏼", "🏽", "🏾", "🏿" ]
8
8
  tone_names = %w[default light medium_light medium medium_dark dark] %>
9
9
 
10
- <%= tag.div(**lc_attributes({ class: lc_tw("emoji-picker", picker_class), data: { controller: "emoji-picker", emoji_picker_auto_submit_value: auto_submit, emoji_picker_catalog_url_value: catalog_url, emoji_picker_recent_scope_value: recent_scope } }, attributes)) do %>
10
+ <%= tag.div(**lc_attributes({ class: lc_tw("lc-emoji-picker", picker_class), data: { controller: "lc-emoji-picker", lc_emoji_picker_auto_submit_value: auto_submit, lc_emoji_picker_catalog_url_value: catalog_url, lc_emoji_picker_recent_scope_value: recent_scope } }, attributes)) do %>
11
11
  <%= form.hidden_field field,
12
12
  value: selected,
13
- data: { emoji_picker_target: "input" } %>
13
+ data: { lc_emoji_picker_target: "input" } %>
14
14
 
15
15
  <% quick.each do |emoji| %>
16
16
  <button
@@ -18,7 +18,7 @@
18
18
  class="emoji-picker__choice"
19
19
  aria-current="<%= emoji == selected %>"
20
20
  data-emoji="<%= emoji %>"
21
- data-action="click->emoji-picker#select"
21
+ data-action="click->lc-emoji-picker#select"
22
22
  >
23
23
  <%= emoji %>
24
24
  </button>
@@ -29,8 +29,8 @@
29
29
  type="button"
30
30
  class="emoji-picker__trigger <%= trigger_class %>"
31
31
  aria-current="<%= !selected_is_quick %>"
32
- data-emoji-picker-target="trigger"
33
- data-action="click->emoji-picker#toggle"
32
+ data-lc-emoji-picker-target="trigger"
33
+ data-action="click->lc-emoji-picker#toggle"
34
34
  data-default-label="+"
35
35
  data-value="<%= selected %>"
36
36
  aria-label="<%= label %>"
@@ -41,14 +41,14 @@
41
41
 
42
42
  <div
43
43
  class="emoji-picker__popover <%= popover_class %>"
44
- data-emoji-picker-target="popover"
45
- data-controller="sheet"
46
- data-action="click->emoji-picker#select pointerover->emoji-picker#preview focusin->emoji-picker#preview sheet:dismiss->emoji-picker#close"
44
+ data-lc-emoji-picker-target="popover"
45
+ data-controller="lc-sheet"
46
+ data-action="click->lc-emoji-picker#select pointerover->lc-emoji-picker#preview focusin->lc-emoji-picker#preview lc-sheet:dismiss->lc-emoji-picker#close"
47
47
  role="dialog"
48
48
  aria-label="<%= label %>"
49
49
  hidden
50
50
  >
51
- <span class="emoji-picker__handle" data-sheet-target="handle" aria-hidden="true"></span>
51
+ <span class="emoji-picker__handle" data-lc-sheet-target="handle" aria-hidden="true"></span>
52
52
 
53
53
  <div class="emoji-picker__search">
54
54
  <div class="emoji-picker__search-field">
@@ -67,30 +67,30 @@
67
67
  enterkeyhint="search"
68
68
  aria-label="<%= lc_t("lostcoords.ui.emoji_picker.search_label") %>"
69
69
  placeholder="<%= lc_t("lostcoords.ui.emoji_picker.search_placeholder") %>"
70
- data-emoji-picker-target="searchInput"
71
- data-action="input->emoji-picker#filter keydown->emoji-picker#onSearchKeydown"
70
+ data-lc-emoji-picker-target="searchInput"
71
+ data-action="input->lc-emoji-picker#filter keydown->lc-emoji-picker#onSearchKeydown"
72
72
  >
73
73
  </div>
74
74
 
75
- <div class="emoji-picker__tones" data-emoji-picker-target="tones">
75
+ <div class="emoji-picker__tones" data-lc-emoji-picker-target="tones">
76
76
  <button
77
77
  type="button"
78
78
  class="emoji-picker__tone-toggle"
79
- data-emoji-picker-target="toneToggle"
80
- data-action="click->emoji-picker#toggleTones"
79
+ data-lc-emoji-picker-target="toneToggle"
80
+ data-action="click->lc-emoji-picker#toggleTones"
81
81
  aria-haspopup="true"
82
82
  aria-expanded="false"
83
83
  aria-label="<%= lc_t("lostcoords.ui.emoji_picker.skin_tone") %>"
84
84
  >✋</button>
85
85
 
86
- <div class="emoji-picker__tone-menu <%= popover_class %>" data-emoji-picker-target="toneMenu" role="menu" hidden>
86
+ <div class="emoji-picker__tone-menu <%= popover_class %>" data-lc-emoji-picker-target="toneMenu" role="menu" hidden>
87
87
  <% tone_modifiers.each_with_index do |modifier, index| %>
88
88
  <button
89
89
  type="button"
90
90
  class="emoji-picker__tone"
91
91
  role="menuitemradio"
92
92
  data-tone-index="<%= index %>"
93
- data-action="click->emoji-picker#selectTone"
93
+ data-action="click->lc-emoji-picker#selectTone"
94
94
  aria-label="<%= lc_t("lostcoords.ui.emoji_picker.skin_tones.#{tone_names[index]}") %>"
95
95
  >✋<%= modifier %></button>
96
96
  <% end %>
@@ -98,14 +98,14 @@
98
98
  </div>
99
99
  </div>
100
100
 
101
- <nav class="emoji-picker__tabs" data-emoji-picker-target="tabs" aria-label="<%= lc_t("lostcoords.ui.emoji_picker.categories_nav") %>">
101
+ <nav class="emoji-picker__tabs" data-lc-emoji-picker-target="tabs" aria-label="<%= lc_t("lostcoords.ui.emoji_picker.categories_nav") %>">
102
102
  <% if recent.any? %>
103
103
  <button
104
104
  type="button"
105
105
  class="emoji-picker__tab"
106
- data-emoji-picker-target="tab"
106
+ data-lc-emoji-picker-target="tab"
107
107
  data-target-section="recent"
108
- data-action="click->emoji-picker#jumpTo"
108
+ data-action="click->lc-emoji-picker#jumpTo"
109
109
  aria-label="<%= lc_t("lostcoords.ui.emoji_picker.recently_used") %>"
110
110
  >🕘</button>
111
111
  <% end %>
@@ -114,21 +114,21 @@
114
114
  <button
115
115
  type="button"
116
116
  class="emoji-picker__tab"
117
- data-emoji-picker-target="tab"
117
+ data-lc-emoji-picker-target="tab"
118
118
  data-target-section="<%= group[:key] %>"
119
- data-action="click->emoji-picker#jumpTo"
119
+ data-action="click->lc-emoji-picker#jumpTo"
120
120
  aria-label="<%= group[:name] %>"
121
121
  ><%= group[:icon] %></button>
122
122
  <% end %>
123
123
  </nav>
124
124
 
125
- <div class="emoji-picker__body" data-emoji-picker-target="body">
125
+ <div class="emoji-picker__body" data-lc-emoji-picker-target="body">
126
126
  <% if recent.any? %>
127
127
  <section
128
128
  class="emoji-picker__recent"
129
129
  data-section="recent"
130
130
  aria-label="<%= lc_t('lostcoords.ui.emoji_picker.recently_used') %>"
131
- data-emoji-picker-target="recent"
131
+ data-lc-emoji-picker-target="recent"
132
132
  >
133
133
  <header class="emoji-picker__category-head">
134
134
  <h3><%= lc_t("lostcoords.ui.emoji_picker.recently_used") %></h3>
@@ -153,23 +153,23 @@
153
153
  </section>
154
154
  <% end %>
155
155
 
156
- <section class="emoji-picker__results" data-emoji-picker-target="results" aria-label="<%= lc_t('lostcoords.ui.emoji_picker.search_results') %>" hidden>
156
+ <section class="emoji-picker__results" data-lc-emoji-picker-target="results" aria-label="<%= lc_t('lostcoords.ui.emoji_picker.search_results') %>" hidden>
157
157
  <header class="emoji-picker__category-head">
158
158
  <h3><%= lc_t("lostcoords.ui.emoji_picker.search_results") %></h3>
159
- <span class="count" data-emoji-picker-target="resultsCount">0</span>
159
+ <span class="count" data-lc-emoji-picker-target="resultsCount">0</span>
160
160
  </header>
161
161
 
162
- <div class="emoji-picker__grid" data-emoji-picker-target="resultsGrid"></div>
162
+ <div class="emoji-picker__grid" data-lc-emoji-picker-target="resultsGrid"></div>
163
163
  </section>
164
164
 
165
- <p class="emoji-picker__empty" data-emoji-picker-target="empty" hidden>
165
+ <p class="emoji-picker__empty" data-lc-emoji-picker-target="empty" hidden>
166
166
  <%= lc_t("lostcoords.ui.emoji_picker.no_results") %>
167
167
  </p>
168
168
  </div>
169
169
 
170
- <footer class="emoji-picker__preview" data-emoji-picker-target="preview">
171
- <span class="emoji-picker__preview-emoji" data-emoji-picker-target="previewEmoji"><%= selected %></span>
172
- <span class="emoji-picker__preview-name" data-emoji-picker-target="previewName"><%= lc_emoji_picker_search_name(selected) %></span>
170
+ <footer class="emoji-picker__preview" data-lc-emoji-picker-target="preview">
171
+ <span class="emoji-picker__preview-emoji" data-lc-emoji-picker-target="previewEmoji"><%= selected %></span>
172
+ <span class="emoji-picker__preview-name" data-lc-emoji-picker-target="previewName"><%= lc_emoji_picker_search_name(selected) %></span>
173
173
  </footer>
174
174
  </div>
175
175
  </div>
@@ -0,0 +1,18 @@
1
+ <%# locals: (id:, language:, title:, label:, content:, truncated:, max_lines:, attributes:) %>
2
+ <%= tag.div(**attributes) do %>
3
+ <% if title %>
4
+ <div class="flex h-10 items-center ps-inset pe-16 pt-inset">
5
+ <span class="truncate font-mono text-xs text-muted-foreground"><%= title %></span>
6
+ </div>
7
+ <% end %>
8
+ <div class="lc-code-block-copy absolute end-2 top-2 z-10 rounded-full bg-muted">
9
+ <%= lc_copy_button source: "#{id}-content", icon_only: true, variant: :ghost, size: :sm, aria: { label: lc_t("lostcoords.ui.code_block.copy", label: label) } %>
10
+ </div>
11
+ <div class="relative">
12
+ <pre id="<%= id %>-content" class="m-0 overflow-x-auto ps-inset pe-16 pb-inset font-mono text-[0.8125rem] text-muted-foreground [tab-size:2] <%= title ? 'pt-2 [--lc-code-block-padding:--spacing(7)]' : 'pt-inset [--lc-code-block-padding:--spacing(10)]' %> <%= 'max-h-[calc(var(--lc-code-block-lines)*1lh+var(--lc-code-block-padding))] overflow-y-hidden group-data-expanded/code-block:max-h-none' if truncated %>" style="<%= "--lc-code-block-lines: #{max_lines}" if truncated %>" tabindex="0" role="region" aria-label="<%= label %>"><%= tag.code(content, data: { language: language }) %></pre>
13
+ <% if truncated %><div class="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-linear-to-t from-muted to-transparent group-data-expanded/code-block:hidden" aria-hidden="true"></div><% end %>
14
+ </div>
15
+ <% if truncated %>
16
+ <%= lc_button lc_t("lostcoords.ui.code_block.expand"), variant: :ghost, size: :sm, class: "w-full rounded-none", aria: { expanded: false, controls: "#{id}-content" }, data: { action: "lc-code-block#toggle", lc_code_block_target: "toggle", expand_label: lc_t("lostcoords.ui.code_block.expand"), collapse_label: lc_t("lostcoords.ui.code_block.collapse") } %>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%# locals: (title:, attributes:, content:) %>
2
+ <%= tag.details(**attributes) do %>
3
+ <summary class="cursor-pointer text-label font-semibold"><%= title %></summary>
4
+ <div class="pt-heading"><%= content %></div>
5
+ <% end %>
@@ -1,12 +1,15 @@
1
- <%# locals: (id:, title:, trigger:, attributes:, content:) %>
2
- <div data-controller="modal">
3
- <button type="button" class="<%= lc_button_class(variant: :secondary) %>" aria-haspopup="dialog" aria-controls="<%= id %>" data-action="modal#open"><%= trigger %></button>
1
+ <%# locals: (id:, title:, description:, trigger:, attributes:, content:) %>
2
+ <div data-controller="lc-dialog">
3
+ <button type="button" class="<%= lc_button_class(variant: :secondary) %>" aria-haspopup="dialog" aria-controls="<%= id %>" data-action="lc-dialog#open" data-slot="dialog-trigger"><%= trigger %></button>
4
4
  <%= tag.dialog(**attributes) do %>
5
- <div class="flex flex-col gap-5 p-5">
6
- <div class="flex items-center justify-between gap-4">
7
- <h2 id="<%= id %>-title" class="text-xl font-semibold"><%= title %></h2>
8
- <button type="button" class="<%= lc_icon_button_class(size: :sm) %>" aria-label="Close <%= title %>" data-action="modal#close"><%= lc_icon "x-mark", options: { "aria-hidden": true } %></button>
9
- </div>
5
+ <div class="flex flex-col gap-inset p-inset" data-slot="dialog-content">
6
+ <header class="flex items-start justify-between gap-content" data-slot="dialog-header">
7
+ <div class="flex min-w-0 flex-col gap-tight">
8
+ <h2 id="<%= id %>-title" class="<%= lc_heading_class(size: :subheading) %>" data-slot="dialog-title"><%= title %></h2>
9
+ <% if description.present? %><p id="<%= id %>-description" class="<%= lc_text_class(size: :small, tone: :muted) %>" data-slot="dialog-description"><%= description %></p><% end %>
10
+ </div>
11
+ <button type="button" class="<%= lc_icon_button_class(size: :sm, shape: :circle, extra: "-me-related -mt-tight") %>" aria-label="<%= lc_t("lostcoords.ui.dialog.close", title: title) %>" data-action="lc-dialog#close"><%= lc_icon "x-mark", options: { "aria-hidden": true } %></button>
12
+ </header>
10
13
  <%= content %>
11
14
  </div>
12
15
  <% end %>