lostcoords-ui 0.4.1 → 0.6.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +364 -96
  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 +23 -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/{disclosure.css → collapsible.css} +6 -6
  9. data/app/assets/tailwind/lostcoords/ui/components/code-block.css +5 -0
  10. data/app/assets/tailwind/lostcoords/ui/components/collapsible.css +5 -0
  11. data/app/assets/tailwind/lostcoords/ui/components/dialog.css +9 -0
  12. data/app/assets/tailwind/lostcoords/ui/components/emoji-picker.css +5 -0
  13. data/app/assets/tailwind/lostcoords/ui/components/menu.css +5 -0
  14. data/app/assets/tailwind/lostcoords/ui/components/prose.css +3 -0
  15. data/app/assets/tailwind/lostcoords/ui/components/table.css +3 -0
  16. data/app/assets/tailwind/lostcoords/ui/components/toast.css +5 -0
  17. data/app/assets/tailwind/lostcoords/ui/components/tooltip.css +4 -0
  18. data/app/assets/tailwind/lostcoords/ui/components.css +12 -0
  19. data/app/assets/tailwind/lostcoords/ui/copy-button.css +9 -9
  20. data/app/assets/tailwind/lostcoords/ui/core.css +21 -0
  21. data/app/assets/tailwind/lostcoords/ui/dialog.css +17 -17
  22. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +86 -88
  23. data/app/assets/tailwind/lostcoords/ui/field.css +2 -2
  24. data/app/assets/tailwind/lostcoords/ui/glass.css +9 -8
  25. data/app/assets/tailwind/lostcoords/ui/layout.css +2 -2
  26. data/app/assets/tailwind/lostcoords/ui/popover.css +14 -14
  27. data/app/assets/tailwind/lostcoords/ui/prose.css +66 -0
  28. data/app/assets/tailwind/lostcoords/ui/sheet.css +62 -63
  29. data/app/assets/tailwind/lostcoords/ui/squircle.css +7 -27
  30. data/app/assets/tailwind/lostcoords/ui/table.css +51 -0
  31. data/app/assets/tailwind/lostcoords/ui/theme.css +209 -225
  32. data/app/assets/tailwind/lostcoords/ui/toast.css +23 -0
  33. data/app/assets/tailwind/lostcoords/ui/tooltip.css +51 -0
  34. data/app/assets/tailwind/lostcoords/ui/typography.css +31 -61
  35. data/app/assets/tailwind/lostcoords/ui/ui.css +2 -21
  36. data/app/assets/tailwind/lostcoords/ui/utilities.css +14 -22
  37. data/app/helpers/lostcoords/ui/badge_helper.rb +67 -110
  38. data/app/helpers/lostcoords/ui/button_helper.rb +44 -35
  39. data/app/helpers/lostcoords/ui/card_helper.rb +9 -52
  40. data/app/helpers/lostcoords/ui/choice_helper.rb +87 -0
  41. data/app/helpers/lostcoords/ui/code_block_helper.rb +22 -0
  42. data/app/helpers/lostcoords/ui/collapsible_helper.rb +9 -0
  43. data/app/helpers/lostcoords/ui/copy_button_helper.rb +7 -7
  44. data/app/helpers/lostcoords/ui/description_list_helper.rb +12 -0
  45. data/app/helpers/lostcoords/ui/dialog_helper.rb +15 -17
  46. data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +6 -0
  47. data/app/helpers/lostcoords/ui/feedback_helper.rb +25 -6
  48. data/app/helpers/lostcoords/ui/form_helper.rb +36 -59
  49. data/app/helpers/lostcoords/ui/glass_helper.rb +7 -7
  50. data/app/helpers/lostcoords/ui/layout_helper.rb +13 -11
  51. data/app/helpers/lostcoords/ui/menu_helper.rb +45 -10
  52. data/app/helpers/lostcoords/ui/navigation_helper.rb +144 -0
  53. data/app/helpers/lostcoords/ui/primitives_helper.rb +39 -61
  54. data/app/helpers/lostcoords/ui/reading_helper.rb +2 -2
  55. data/app/helpers/lostcoords/ui/render_attributes_helper.rb +17 -1
  56. data/app/helpers/lostcoords/ui/sheet_helper.rb +7 -45
  57. data/app/helpers/lostcoords/ui/structures_helper.rb +26 -125
  58. data/app/helpers/lostcoords/ui/table_helper.rb +11 -0
  59. data/app/helpers/lostcoords/ui/tailwind_helper.rb +13 -4
  60. data/app/helpers/lostcoords/ui/toast_helper.rb +22 -0
  61. data/app/helpers/lostcoords/ui/tooltip_helper.rb +17 -0
  62. data/app/helpers/lostcoords/ui/typography_helper.rb +41 -11
  63. data/app/javascript/lostcoords/ui/controllers/lc_code_block_controller.js +23 -0
  64. data/app/javascript/lostcoords/ui/controllers/{disclosure_controller.js → lc_collapsible_controller.js} +4 -4
  65. data/app/javascript/lostcoords/ui/controllers/{confirm_dialog_controller.js → lc_confirm_dialog_controller.js} +4 -4
  66. data/app/javascript/lostcoords/ui/controllers/{modal_controller.js → lc_dialog_controller.js} +1 -1
  67. data/app/javascript/lostcoords/ui/controllers/{emoji_picker_controller.js → lc_emoji_picker_controller.js} +38 -10
  68. data/app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js +12 -0
  69. data/app/javascript/lostcoords/ui/controllers/{row_menu_controller.js → lc_menu_controller.js} +25 -3
  70. data/app/javascript/lostcoords/ui/controllers/{ui_popover_controller.js → lc_popover_controller.js} +1 -1
  71. data/app/javascript/lostcoords/ui/controllers/{ui_selection_controller.js → lc_selection_controller.js} +7 -15
  72. data/app/javascript/lostcoords/ui/controllers/{sheet_controller.js → lc_sheet_controller.js} +17 -17
  73. data/app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js +45 -0
  74. data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +42 -0
  75. data/app/javascript/lostcoords/ui/controllers/{toast_controller.js → lc_toast_controller.js} +2 -2
  76. data/app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js +62 -0
  77. data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +54 -0
  78. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +77 -25
  79. data/app/javascript/lostcoords/ui/helpers/context_dialog.js +6 -6
  80. data/app/javascript/lostcoords/ui/helpers/dialog.js +2 -2
  81. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +30 -7
  82. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +7 -8
  83. data/app/javascript/lostcoords/ui/helpers/menu_keys.js +19 -18
  84. data/app/javascript/lostcoords/ui/helpers/roving_focus.js +175 -0
  85. data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +4 -4
  86. data/app/javascript/lostcoords/ui/helpers/shortcuts.js +162 -0
  87. data/app/javascript/lostcoords/ui/helpers/top_layer.js +48 -10
  88. data/app/javascript/lostcoords/ui/index.js +57 -0
  89. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +46 -46
  90. data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +3 -3
  91. data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +18 -0
  92. data/app/views/lostcoords/ui/components/_lc_collapsible.html.erb +5 -0
  93. data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +11 -8
  94. data/app/views/lostcoords/ui/components/_lc_floating_panel.html.erb +8 -0
  95. data/config/importmap.rb +10 -2
  96. data/config/locales/en.yml +23 -0
  97. data/config/locales/es.yml +23 -0
  98. data/config/locales/ja.yml +23 -0
  99. data/lib/lostcoords/ui/action_view.rb +6 -3
  100. data/lib/lostcoords/ui/builders.rb +2 -2
  101. data/lib/lostcoords/ui/form_builder.rb +126 -0
  102. data/lib/lostcoords/ui/parts.rb +154 -0
  103. data/lib/lostcoords/ui/stylesheets.rb +25 -0
  104. data/lib/lostcoords/ui/tokens.rb +24 -1
  105. data/lib/lostcoords/ui/version.rb +1 -1
  106. data/lib/tasks/lostcoords_ui.rake +11 -0
  107. metadata +54 -21
  108. data/app/assets/tailwind/lostcoords/ui/reading.css +0 -60
  109. data/app/views/lostcoords/ui/components/_form_alert.html.erb +0 -4
  110. data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +0 -5
  111. data/app/views/lostcoords/ui/components/_skeleton.html.erb +0 -4
  112. data/app/views/lostcoords/ui/components/_spinner.html.erb +0 -10
  113. data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +0 -8
  114. data/config/helper_renames.json +0 -113
  115. data/script/migrate_helpers.rb +0 -136
  116. /data/app/javascript/lostcoords/ui/controllers/{ui_copy_controller.js → lc_copy_controller.js} +0 -0
  117. /data/app/javascript/lostcoords/ui/controllers/{ui_toggle_controller.js → lc_toggle_controller.js} +0 -0
@@ -1,21 +1,2 @@
1
- @import "./theme.css";
2
- @source "../../../../helpers/**/*.rb";
3
- @source "../../../../javascript/**/*.js";
4
- @source "../../../../views/**/*.erb";
5
- @source not "../../../../../lib";
6
-
7
- @import "./glass.css";
8
- @import "./copy-button.css";
9
- @import "./disclosure.css";
10
- @import "./emoji-picker.css";
11
- @import "./popover.css";
12
- @import "./sheet.css";
13
- @import "./card.css";
14
- @import "./dialog.css";
15
- @import "./avatar.css";
16
- @import "./squircle.css";
17
- @import "./utilities.css";
18
- @import "./typography.css";
19
- @import "./layout.css";
20
- @import "./reading.css";
21
- @import "./field.css";
1
+ @import "./components.css";
2
+ @import "./base.css";
@@ -1,45 +1,37 @@
1
- /* The rim is the border box painted with --pill-rim under the surface, so
2
- one element carries both layers. */
3
- @utility pill-3d {
4
- border: calc(var(--spacing) * 0.375) solid transparent;
5
- background: var(--pill-surface) padding-box, var(--pill-rim) border-box;
6
- box-shadow: var(--pill-light), var(--pill-depth);
7
- }
8
-
9
1
  /* A control gives a little as it is pressed, unless it is disabled. */
10
- @utility press {
2
+ @utility lc-press {
11
3
  @media (prefers-reduced-motion: no-preference) {
12
4
  &:not([disabled], :disabled, [aria-disabled="true"]):active {
13
- transform: scale(var(--press-scale, var(--ui-press-scale)));
5
+ transform: scale(var(--lc-press-scale));
14
6
  }
15
7
  }
16
8
  }
17
9
 
18
10
  @layer components {
19
11
  /* A data-URI image cannot read CSS variables, so the chevron's ink is baked in. */
20
- select.select-control {
12
+ select.lc-select {
21
13
  appearance: none;
22
14
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%237b808a' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 8 4 4 4-4'/%3E%3C/svg%3E");
23
15
  background-size: calc(var(--spacing) * 5) calc(var(--spacing) * 5);
24
16
  background-repeat: no-repeat;
25
- background-position: right calc(var(--spacing) * 4) center;
17
+ background-position: right calc(var(--spacing) * 3) center;
26
18
  }
27
19
  }
28
20
 
29
- @property --scroll-fade-start { syntax: "<length>"; inherits: false; initial-value: 0; }
30
- @property --scroll-fade-end { syntax: "<length>"; inherits: false; initial-value: 0; }
21
+ @property --lc-scroll-fade-start { syntax: "<length>"; inherits: false; initial-value: 0; }
22
+ @property --lc-scroll-fade-end { syntax: "<length>"; inherits: false; initial-value: 0; }
31
23
 
32
- @keyframes scroll-fade-x {
33
- 0% { --scroll-fade-start: 0; --scroll-fade-end: 2rem; }
34
- 15% { --scroll-fade-start: 2rem; }
35
- 85% { --scroll-fade-end: 2rem; }
36
- 100% { --scroll-fade-start: 2rem; --scroll-fade-end: 0; }
24
+ @keyframes lc-scroll-fade-x {
25
+ 0% { --lc-scroll-fade-start: 0; --lc-scroll-fade-end: 2rem; }
26
+ 15% { --lc-scroll-fade-start: 2rem; }
27
+ 85% { --lc-scroll-fade-end: 2rem; }
28
+ 100% { --lc-scroll-fade-start: 2rem; --lc-scroll-fade-end: 0; }
37
29
  }
38
30
 
39
31
  @supports (animation-timeline: scroll(self inline)) {
40
- .scroll-fade-x {
41
- mask-image: linear-gradient(to right, transparent, black var(--scroll-fade-start), black calc(100% - var(--scroll-fade-end)), transparent);
42
- animation: scroll-fade-x linear both;
32
+ .lc-scroll-fade-x {
33
+ mask-image: linear-gradient(to right, transparent, black var(--lc-scroll-fade-start), black calc(100% - var(--lc-scroll-fade-end)), transparent);
34
+ animation: lc-scroll-fade-x linear both;
43
35
  animation-timeline: scroll(self inline);
44
36
  }
45
37
  }
@@ -1,128 +1,96 @@
1
1
  module Lostcoords::Ui
2
2
  module BadgeHelper
3
3
  ICON_BADGE_BASE =
4
- "icon-badge squircle grid place-items-center shrink-0 " \
5
- "size-[var(--icon-badge-size,var(--icon-badge-size-default,--spacing(10)))] " \
6
- "[border:var(--icon-badge-border,1px_solid_transparent)] " \
7
- "rounded-[var(--icon-badge-radius,var(--radius-2xl))] " \
8
- "bg-[var(--icon-badge-bg,var(--paper-2))] " \
9
- "text-[length:var(--icon-badge-font-size,var(--icon-badge-font-size-default,1.125rem))] leading-none"
10
-
11
- # Sizes set the default, so a context that pins --icon-badge-size still wins.
12
- ICON_BADGE_SIZES = {
13
- base: "",
14
- lg: "[--icon-badge-size-default:--spacing(11)] [--icon-badge-font-size-default:1.25rem]",
15
- xl: "[--icon-badge-size-default:--spacing(14)] [--icon-badge-font-size-default:1.5rem]"
16
- }.freeze
17
-
18
- ICON_MEDALLION_BASE = "grid place-items-center shrink-0 rounded-full"
4
+ "lc-icon-badge grid place-items-center shrink-0 " \
5
+ "size-[var(--lc-icon-badge-size,var(--lc-icon-badge-size-default,--spacing(10)))] " \
6
+ "[border:var(--lc-icon-badge-border,1px_solid_transparent)] " \
7
+ "text-[length:var(--lc-icon-badge-font-size,var(--lc-icon-badge-font-size-default,1.125rem))] leading-none"
19
8
 
20
- ICON_MEDALLION_SIZES = {
21
- sm: "size-6.5",
22
- lg: "size-12"
9
+ # Sizes set the default, so a context that pins --lc-icon-badge-size still wins.
10
+ ICON_BADGE_SIZES = {
11
+ sm: "[--lc-icon-badge-size-default:--spacing(6.5)] [--lc-icon-badge-font-size-default:0.8125rem]",
12
+ md: "",
13
+ lg: "[--lc-icon-badge-size-default:--spacing(12)] [--lc-icon-badge-font-size-default:1.5rem]",
14
+ xl: "[--lc-icon-badge-size-default:--spacing(14)] [--lc-icon-badge-font-size-default:1.75rem]"
23
15
  }.freeze
24
16
 
25
- ICON_MEDALLION_TONES = {
26
- accent: "bg-sky-soft text-accent",
27
- neutral: "bg-paper-2",
28
- leaf: "bg-[color-mix(in_oklab,var(--leaf-soft)_72%,var(--paper))] text-leaf"
29
- }.freeze
17
+ ICON_BADGE_SHAPES = { square: "lc-squircle rounded-2xl", circle: "rounded-full" }.freeze
30
18
 
31
19
  AVATAR_BASE =
32
- "avatar grid place-items-center shrink-0 rounded-full font-medium tracking-[var(--avatar-tracking,0.05em)] " \
33
- "size-[var(--avatar-size,var(--avatar-size-default,--spacing(8)))] " \
34
- "text-[length:var(--avatar-font-size,var(--avatar-font-size-default,0.75rem))] " \
35
- "text-[color:var(--avatar-color,var(--ink-on-accent))] " \
36
- "border-[length:var(--avatar-border,0px)] border-[color:var(--avatar-cutout,var(--paper))]"
20
+ "lc-avatar grid place-items-center shrink-0 rounded-full font-medium tracking-[var(--lc-avatar-tracking,0.05em)] " \
21
+ "size-[var(--lc-avatar-size,var(--lc-avatar-size-default,--spacing(8)))] " \
22
+ "text-[length:var(--lc-avatar-font-size,var(--lc-avatar-font-size-default,0.75rem))] " \
23
+ "text-[color:var(--lc-avatar-color,var(--primary-foreground))] " \
24
+ "border-[length:var(--lc-avatar-border,0px)] border-[color:var(--lc-avatar-cutout,var(--background))]"
37
25
 
38
- # Sizes set the default, so a context that pins --avatar-size (a stack, a dialog) still wins.
26
+ # Sizes set the default, so a context that pins --lc-avatar-size (a stack, a dialog) still wins.
39
27
  AVATAR_SIZES = {
40
- base: "",
41
- lg: "",
42
- sm: "[--avatar-size-default:--spacing(6.5)] [--avatar-font-size-default:0.625rem]",
43
- xl: "[--avatar-size-default:--spacing(24)] [--avatar-font-size-default:1.25rem]"
28
+ md: "",
29
+ lg: "[--lc-avatar-size-default:--spacing(12)] [--lc-avatar-font-size-default:1rem]",
30
+ sm: "[--lc-avatar-size-default:--spacing(6.5)] [--lc-avatar-font-size-default:0.625rem]",
31
+ xl: "[--lc-avatar-size-default:--spacing(24)] [--lc-avatar-font-size-default:1.25rem]"
44
32
  }.freeze
45
33
 
46
- AVATAR_INK = "[--avatar-color:var(--ink)]"
34
+ AVATAR_INK = "[--lc-avatar-color:var(--foreground)]"
47
35
 
48
- AVATAR_LINKED = "avatar-ring"
36
+ AVATAR_LINKED = "lc-avatar-ring"
49
37
 
50
38
  # Overlapping avatars with a "+N" chip. The stack sets inherited variables rather than styling
51
- # its avatars, so a context's own `[&_.avatar]:` override still wins. The border cuts each
52
- # avatar out of the one beneath, so it takes the fill it sits on (--avatar-cutout).
39
+ # its avatars, so a context's own `[&_.lc-avatar]:` override still wins. The border cuts each
40
+ # avatar out of the one beneath, so it takes the fill it sits on (--lc-avatar-cutout).
53
41
  AVATAR_STACK_BASE =
54
- "avatar-stack relative inline-flex items-center min-w-0 [&_.avatar]:outline-none " \
55
- "[--avatar-size:--spacing(8)] [--avatar-border:--spacing(0.5)] [--avatar-ring-gap:1] " \
56
- "[--avatar-stack-overlap:--spacing(1.5)]"
42
+ "lc-avatar-stack relative inline-flex items-center min-w-0 [&_.lc-avatar]:outline-none " \
43
+ "[--lc-avatar-size:--spacing(8)] [--lc-avatar-border:--spacing(0.5)] [--lc-avatar-ring-gap:1] " \
44
+ "[--lc-avatar-stack-overlap:--spacing(1.5)]"
57
45
 
58
46
  AVATAR_STACK_COMPACT =
59
- "[--avatar-size:--spacing(4.5)] [--avatar-font-size:0.4375rem] [--avatar-border:0px] [--avatar-ring-gap:2] " \
60
- "@sm:[--avatar-size:--spacing(5.5)] @sm:[--avatar-font-size:0.5rem] " \
61
- "[--avatar-stack-overlap:--spacing(1.25)] [--avatar-stack-padding:0px]"
47
+ "[--lc-avatar-size:--spacing(4.5)] [--lc-avatar-font-size:0.4375rem] [--lc-avatar-border:0px] [--lc-avatar-ring-gap:2] " \
48
+ "@sm:[--lc-avatar-size:--spacing(5.5)] @sm:[--lc-avatar-font-size:0.5rem] " \
49
+ "[--lc-avatar-stack-overlap:--spacing(1.25)] [--lc-avatar-stack-padding:0px]"
62
50
 
63
51
  # dynamic_avatar_stack_controller marks the leading visible avatar with data-first as it narrows the stack.
64
52
  AVATAR_STACK_ROW =
65
- "avatar-stack__row flex shrink-0 p-[var(--avatar-stack-padding,--spacing(1))] " \
66
- "[&>.avatar]:-ms-(--avatar-stack-overlap) [&>.avatar:first-child]:ms-0 [&>.avatar[data-first]]:ms-0"
67
-
68
- EYEBROW_BASE =
69
- "font-mono uppercase tracking-widest text-xs font-medium text-ink-3"
53
+ "lc-avatar-stack__row flex shrink-0 p-[var(--lc-avatar-stack-padding,--spacing(1))] " \
54
+ "[&>.lc-avatar]:-ms-(--lc-avatar-stack-overlap) [&>.lc-avatar:first-child]:ms-0 [&>.lc-avatar[data-first]]:ms-0"
70
55
 
71
56
  BADGE_BASE =
72
- "inline-flex items-center gap-1 rounded-xl px-2.5 py-1 " \
73
- "font-body text-xs font-medium whitespace-nowrap"
57
+ "inline-flex items-center gap-tight rounded-lg lc-squircle px-2 py-0.5 " \
58
+ "font-sans text-caption font-medium whitespace-nowrap"
74
59
 
75
60
  BADGE_TONES = {
76
- neutral: "bg-paper-2 text-ink-2",
77
- success: "bg-leaf-soft text-leaf",
78
- warning: "bg-gold-soft text-gold",
79
- danger: "bg-berry-soft text-berry",
80
- info: "bg-sky-soft text-sky-deep"
61
+ neutral: "bg-tile text-muted-foreground",
62
+ success: "bg-success-soft text-success",
63
+ warning: "bg-warning-soft text-warning",
64
+ destructive: "bg-destructive-soft text-destructive",
65
+ primary: "bg-primary-soft text-primary"
81
66
  }.freeze
82
67
 
83
- STATUS_PILL_BASE =
84
- "inline-flex items-center rounded-full px-[0.6em] py-[0.15em] " \
85
- "font-body text-sm font-medium"
86
-
87
- STATUS_PILL_TONES = {
88
- accent: "bg-sky-soft text-accent",
89
- danger: "bg-berry-soft text-berry"
90
- }.freeze
91
-
92
- PILL_BASE =
93
- "inline-flex items-center justify-center gap-2 rounded-full " \
94
- "border border-transparent bg-paper-2 " \
95
- "font-body text-ink-2 whitespace-nowrap cursor-pointer no-underline " \
96
- "transition-[background-color,border-color,color,transform] duration-[150ms] ease-out " \
97
- "hover:text-ink active:scale-[0.96] " \
98
- "aria-pressed:[background:var(--sky-soft)] aria-pressed:border-accent aria-pressed:text-accent aria-pressed:shadow-none " \
99
- "aria-[current]:not-aria-[current=false]:[background:var(--sky-soft)] aria-[current]:not-aria-[current=false]:border-accent " \
100
- "aria-[current]:not-aria-[current=false]:text-accent aria-[current]:not-aria-[current=false]:shadow-none " \
101
- "has-checked:[background:var(--sky-soft)] has-checked:border-accent has-checked:text-accent has-checked:shadow-none " \
68
+ TOGGLE_BASE =
69
+ "inline-flex items-center justify-center gap-related rounded-pill " \
70
+ "border border-transparent bg-input " \
71
+ "font-sans text-muted-foreground whitespace-nowrap cursor-pointer no-underline " \
72
+ "transition-[background-color,border-color,color,transform] duration-(--lc-motion-quick) ease-(--lc-ease-out) " \
73
+ "lc-press hover:text-foreground " \
74
+ "aria-pressed:bg-primary-soft aria-pressed:border-primary aria-pressed:text-primary aria-pressed:shadow-none " \
75
+ "aria-[current]:not-aria-[current=false]:bg-primary-soft aria-[current]:not-aria-[current=false]:border-primary " \
76
+ "aria-[current]:not-aria-[current=false]:text-primary aria-[current]:not-aria-[current=false]:shadow-none " \
77
+ "has-checked:bg-primary-soft has-checked:border-primary has-checked:text-primary has-checked:shadow-none " \
102
78
  "[&_svg]:size-[1.1em] [&_svg]:shrink-0"
103
79
 
104
- PILL_ACTIVE = "bg-sky-soft border-accent text-accent hover:text-accent shadow-none"
80
+ TOGGLE_ACTIVE = "bg-primary-soft border-primary text-primary hover:text-primary shadow-none"
105
81
 
106
- PILL_GHOST = "border-dashed border-ink-3/40 [background:none] shadow-none text-ink-3"
82
+ TOGGLE_GHOST = "border-dashed border-muted-foreground/40 [background:none] shadow-none text-muted-foreground"
107
83
 
108
- PILL_ROW_BASE =
109
- "flex flex-wrap items-center gap-y-2 gap-x-2"
110
-
111
- def lc_icon_badge_class(size: :base, extra: nil)
112
- lc_tw(ICON_BADGE_BASE, ICON_BADGE_SIZES.fetch(size.to_sym), extra)
84
+ def lc_icon_badge_class(size: :md, shape: :square, tone: :neutral, extra: nil)
85
+ size = lc_option!(:size, size, ICON_BADGE_SIZES.keys)
86
+ shape = lc_option!(:shape, shape, ICON_BADGE_SHAPES.keys)
87
+ tone = lc_option!(:tone, tone, BADGE_TONES.keys)
88
+ lc_tw(ICON_BADGE_BASE, ICON_BADGE_SIZES.fetch(size), ICON_BADGE_SHAPES.fetch(shape),
89
+ (tone == :neutral ? "bg-tile text-foreground" : BADGE_TONES.fetch(tone)), extra)
113
90
  end
114
91
 
115
- def lc_icon_medallion_class(size: :sm, tone: :accent, extra: nil)
116
- lc_tw(
117
- ICON_MEDALLION_BASE,
118
- ICON_MEDALLION_SIZES.fetch(size.to_sym, ICON_MEDALLION_SIZES[:sm]),
119
- ICON_MEDALLION_TONES.fetch(tone.to_sym, ICON_MEDALLION_TONES[:accent]),
120
- extra
121
- )
122
- end
123
-
124
- def lc_avatar_class(size: :base, ink: false, linked: false, extra: nil)
125
- lc_tw(AVATAR_BASE, AVATAR_SIZES.fetch(size.to_sym), (AVATAR_INK if ink), (AVATAR_LINKED if linked), extra)
92
+ def lc_avatar_class(size: :md, ink: false, linked: false, extra: nil)
93
+ lc_tw(AVATAR_BASE, AVATAR_SIZES.fetch(lc_option!(:size, size, AVATAR_SIZES.keys)), (AVATAR_INK if ink), (AVATAR_LINKED if linked), extra)
126
94
  end
127
95
 
128
96
  def lc_avatar_stack_class(compact: false, extra: nil)
@@ -133,30 +101,19 @@ module Lostcoords::Ui
133
101
  lc_tw(AVATAR_STACK_ROW, extra)
134
102
  end
135
103
 
136
- def lc_eyebrow_class(extra: nil)
137
- lc_tw(EYEBROW_BASE, extra)
138
- end
139
-
140
104
  def lc_badge_class(tone: :neutral, extra: nil)
141
- lc_tw(BADGE_BASE, BADGE_TONES.fetch(tone.to_sym, BADGE_TONES[:neutral]), extra)
142
- end
143
-
144
- def lc_status_pill_class(tone: :accent, extra: nil)
145
- lc_tw(STATUS_PILL_BASE, STATUS_PILL_TONES.fetch(tone.to_sym, STATUS_PILL_TONES[:accent]), extra)
105
+ tone = lc_option!(:tone, tone, BADGE_TONES.keys)
106
+ lc_tw(BADGE_BASE, BADGE_TONES.fetch(tone), extra)
146
107
  end
147
108
 
148
- def lc_pill_class(active: false, ghost: false, size: :base, extra: nil)
109
+ def lc_toggle_class(active: false, ghost: false, size: :md, extra: nil)
149
110
  lc_tw(
150
- PILL_BASE,
151
- ButtonHelper::BUTTON_SIZES.fetch(size.to_sym, ButtonHelper::BUTTON_SIZES[:base]),
152
- (PILL_GHOST if ghost),
153
- (PILL_ACTIVE if active),
111
+ TOGGLE_BASE,
112
+ ButtonHelper::BUTTON_SIZES.fetch(lc_option!(:size, size, ButtonHelper::BUTTON_SIZES.keys)),
113
+ (TOGGLE_GHOST if ghost),
114
+ (TOGGLE_ACTIVE if active),
154
115
  extra
155
116
  )
156
117
  end
157
-
158
- def lc_pill_row_class(extra: nil)
159
- lc_tw(PILL_ROW_BASE, extra)
160
- end
161
118
  end
162
119
  end
@@ -1,70 +1,79 @@
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
- "inline-flex items-center justify-center gap-2 rounded-full font-body font-medium " \
10
+ "inline-flex items-center justify-center gap-related rounded-pill font-sans " \
5
11
  "whitespace-nowrap no-underline cursor-pointer " \
6
- "transition-[background-color,border-color,color,box-shadow,transform,translate,scale,filter] duration-(--ui-motion-control) 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 " \
12
+ "transition-[background-color,border-color,color,box-shadow,transform,translate,scale,filter] duration-(--lc-motion-control) ease-(--lc-ease-out) " \
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
 
16
+ # Height comes from the shared control sizes; an icon trims the padding on its side.
10
17
  BUTTON_SIZES = {
11
- sm: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-4 has-[>[data-icon=inline-start]]:ps-3 has-[>[data-icon=inline-end]]:pe-3 text-sm",
12
- base: "#{TailwindHelper::CONTROL_SIZES[:base]} px-5 has-[>[data-icon=inline-start]]:ps-4 has-[>[data-icon=inline-end]]:pe-4 text-base",
13
- lg: "#{TailwindHelper::CONTROL_SIZES[:lg]} px-7 has-[>[data-icon=inline-start]]:ps-6 has-[>[data-icon=inline-end]]:pe-6 text-base",
14
- nav: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-4 has-[>[data-icon=inline-start]]:ps-3 has-[>[data-icon=inline-end]]:pe-3 text-base"
18
+ sm: "#{TailwindHelper::CONTROL_SIZES[:sm]} px-3 has-[>[data-icon=inline-start]]:ps-2.5 has-[>[data-icon=inline-end]]:pe-2.5 text-label",
19
+ md: "#{TailwindHelper::CONTROL_SIZES[:md]} px-4 has-[>[data-icon=inline-start]]:ps-3.5 has-[>[data-icon=inline-end]]:pe-3.5 text-label",
20
+ lg: "#{TailwindHelper::CONTROL_SIZES[:lg]} px-5 has-[>[data-icon=inline-start]]:ps-4 has-[>[data-icon=inline-end]]:pe-4 text-body font-medium"
15
21
  }.freeze
16
22
 
23
+ BUTTON_HOVER =
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)))]"
26
+
17
27
  BUTTON_VARIANTS = {
18
- default: "border border-transparent bg-paper text-ink active:bg-paper-2",
19
- accent: "border border-transparent bg-sky text-ink-on-accent hover:bg-sky-deep active:bg-sky-deep",
20
- secondary: "border border-transparent bg-paper-2 text-ink hover:bg-paper-3 active:bg-paper-3",
21
- glass: "pill-3d text-ink hover:brightness-[0.98] active:brightness-[0.96]",
22
- ghost: "border border-transparent bg-transparent text-ink-2 hover:bg-(--ghost-hover-bg,var(--paper-2)) hover:text-ink active:bg-(--ghost-hover-bg,var(--paper-2))",
23
- danger: "border border-transparent bg-transparent text-berry hover:bg-berry/8 active:bg-berry/8"
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}",
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}",
30
+ outline: "border border-border bg-transparent text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER}",
31
+ glass: "[--lc-button-bg:var(--_lc-glass-fill)] 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}",
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}"
24
34
  }.freeze
25
35
 
26
36
  ICON_BUTTON_BASE =
27
- "inline-grid place-items-center shrink-0 border-0 bg-transparent " \
28
- "text-ink-3 cursor-pointer no-underline " \
29
- "transition-[background-color,color,transform,scale] duration-(--ui-motion-quick) ease-out " \
30
- "hover:bg-paper-2 hover:text-ink motion-safe:active:scale-(--ui-press-scale) " \
31
- "disabled:opacity-40 disabled:pointer-events-none " \
37
+ "lc-press inline-grid place-items-center shrink-0 border-0 bg-transparent " \
38
+ "text-muted-foreground cursor-pointer no-underline " \
39
+ "transition-[background-color,color,transform,scale] duration-(--lc-motion-quick) ease-(--lc-ease-out) " \
40
+ "not-disabled:hover:text-(--lc-button-ink,var(--foreground)) #{BUTTON_HOVER} " \
41
+ "#{BUTTON_DISABLED} " \
32
42
  "[&_svg]:block [&_svg]:shrink-0"
33
43
 
34
44
  ICON_BUTTON_SHAPES = {
35
- square: "rounded-xl",
36
- circle: "rounded-full"
45
+ square: "rounded-xl lc-squircle",
46
+ circle: "rounded-pill"
37
47
  }.freeze
38
48
 
39
49
  ICON_BUTTON_SIZES = {
40
- sm: "size-11 [&_svg]:size-4",
41
- base: "size-12 [&_svg]:size-5",
42
- lg: "size-14 [&_svg]:size-6"
50
+ sm: "size-control-sm [&_svg]:size-4",
51
+ md: "size-control-md [&_svg]:size-5",
52
+ lg: "size-control-lg [&_svg]:size-6"
43
53
  }.freeze
44
54
 
45
- def lc_button_class(variant: :default, size: :base, block: false, extra: nil)
55
+ def lc_button_class(variant: :primary, size: :md, block: false, extra: nil)
56
+ variant = lc_option!(:variant, variant, BUTTON_VARIANTS.keys)
57
+ size = lc_option!(:size, size, BUTTON_SIZES.keys)
46
58
  lc_tw(
47
59
  BUTTON_BASE,
48
- BUTTON_SIZES.fetch(size.to_sym, BUTTON_SIZES[:base]),
49
- lc_button_variants.fetch(variant.to_sym, lc_button_variants[:default]),
60
+ BUTTON_SIZES.fetch(size),
61
+ (lc_glass_class(shape: :bar) if variant == :glass),
62
+ BUTTON_VARIANTS.fetch(variant),
50
63
  ("w-full" if block),
51
64
  extra
52
65
  )
53
66
  end
54
67
 
55
- def lc_icon_button_class(shape: :square, size: :base, extra: nil)
68
+ def lc_icon_button_class(shape: :square, size: :md, extra: nil)
69
+ shape = lc_option!(:shape, shape, ICON_BUTTON_SHAPES.keys)
70
+ size = lc_option!(:size, size, ICON_BUTTON_SIZES.keys)
56
71
  lc_tw(
57
72
  ICON_BUTTON_BASE,
58
- ICON_BUTTON_SHAPES.fetch(shape.to_sym, ICON_BUTTON_SHAPES[:square]),
59
- ICON_BUTTON_SIZES.fetch(size.to_sym, ICON_BUTTON_SIZES[:base]),
73
+ ICON_BUTTON_SHAPES.fetch(shape),
74
+ ICON_BUTTON_SIZES.fetch(size),
60
75
  extra
61
76
  )
62
77
  end
63
-
64
- private
65
-
66
- def lc_button_variants
67
- BUTTON_VARIANTS
68
- end
69
78
  end
70
79
  end
@@ -1,60 +1,17 @@
1
1
  module Lostcoords::Ui
2
2
  module CardHelper
3
- CARD_BASE =
4
- "squircle bg-paper-2 border border-transparent rounded-3xl"
5
-
3
+ # Padding sizes set --lc-inset, so nested content that pads or gaps with inset follows the card.
6
4
  CARD_PADDING = {
7
- default: "p-5",
8
- dense: "py-4 px-3.5 sm:p-5"
9
- }.freeze
10
-
11
- HOVER_LIFT =
12
- "transition-[transform,box-shadow] duration-[180ms] ease-out " \
13
- "hover:-translate-y-0.5 hover:shadow-(--shadow)"
14
-
15
- CARD_SURFACE_CHROME =
16
- "squircle bg-(--card-surface-bg,var(--paper-2)) " \
17
- "[border:var(--card-surface-border,1px_solid_transparent)] " \
18
- "rounded-(--card-surface-radius,var(--radius-3xl)) " \
19
- "shadow-(--card-surface-shadow,none) [--field-bg:var(--paper)] " \
20
- "[--avatar-cutout:var(--card-surface-bg,var(--paper-2))] " \
21
- "transition-[background,border-color,box-shadow] duration-[180ms] ease-(--ease-out)"
22
-
23
- FORM_CARD_BASE =
24
- "form-card @container " \
25
- "#{CARD_SURFACE_CHROME} " \
26
- "has-[[data-tone=error]]:[border-color:color-mix(in_oklab,var(--berry)_30%,var(--hairline))]"
27
-
28
- FORM_CARD_PADDINGS = {
29
- default: "p-(--form-card-padding,1.25rem)",
30
- compact: "p-(--form-card-padding,1rem_14px) sm:p-(--form-card-padding,20px)",
31
- snug: "p-(--form-card-padding,1rem_14px) sm:p-(--form-card-padding,1rem_20px)",
32
- empty: "p-(--form-card-padding,2rem_1.25rem)"
5
+ none: "p-0",
6
+ md: "p-inset",
7
+ sm: "[--lc-inset:var(--spacing-content)] p-inset"
33
8
  }.freeze
9
+ HOVER_LIFT = "transition-[transform,box-shadow] duration-(--lc-motion-control) ease-(--lc-ease-out) hover:-translate-y-0.5 hover:shadow-(--lc-shadow)"
34
10
 
35
- FORM_CARD_EMPTY_CONTENT = "text-center text-ink-3"
36
-
37
- def lc_card_class(interactive: false, dense: false, extra: nil)
38
- lc_tw(
39
- CARD_BASE,
40
- CARD_PADDING.fetch(dense ? :dense : :default),
41
- (HOVER_LIFT if interactive),
42
- ("hover:bg-paper-3" if interactive),
43
- extra
44
- )
45
- end
46
-
47
- def lc_card_surface_class(extra: nil)
48
- lc_tw(CARD_SURFACE_CHROME, extra)
49
- end
50
-
51
- def lc_form_card_class(padding: :default, extra: nil)
52
- lc_tw(
53
- FORM_CARD_BASE,
54
- FORM_CARD_PADDINGS.fetch(padding.to_sym, FORM_CARD_PADDINGS[:default]),
55
- (FORM_CARD_EMPTY_CONTENT if padding.to_sym == :empty),
56
- extra
57
- )
11
+ def lc_card_class(interactive: false, padding: :md, extra: nil)
12
+ padding = lc_option!(:padding, padding, CARD_PADDING.keys)
13
+ lc_tw("lc-card lc-squircle", CARD_PADDING.fetch(padding),
14
+ ("#{HOVER_LIFT} hover:bg-accent" if interactive), extra)
58
15
  end
59
16
  end
60
17
  end
@@ -0,0 +1,87 @@
1
+ module Lostcoords::Ui
2
+ module ChoiceHelper
3
+ CHOICE_BASE = "shrink-0 size-4.5 m-0 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-40"
4
+
5
+ # The label row centers the control on the first line of text, so a hint wraps under the label.
6
+ CHOICE_LABEL = "lc-choice inline-flex min-w-0 items-start gap-related text-body text-foreground cursor-pointer has-disabled:cursor-not-allowed has-disabled:opacity-60"
7
+
8
+ # card: true draws the whole label as a selectable tile, for plan or theme pickers.
9
+ CHOICE_CARD = "flex w-full rounded-2xl lc-squircle border border-transparent bg-tile p-content " \
10
+ "transition-[background-color,border-color] duration-(--lc-motion-quick) ease-(--lc-ease-out) hover:bg-accent " \
11
+ "has-checked:border-primary has-checked:bg-primary-soft has-focus-visible:outline-2 has-focus-visible:outline-offset-2 " \
12
+ "has-focus-visible:outline-(--ring,color-mix(in_oklab,var(--primary)_50%,transparent))"
13
+
14
+ def lc_choice_label_class(card: false, extra: nil)
15
+ lc_tw(CHOICE_LABEL, (CHOICE_CARD if card), extra)
16
+ end
17
+
18
+ def lc_checkbox_class(extra: nil)
19
+ lc_tw("lc-checkbox rounded-sm", CHOICE_BASE, extra)
20
+ end
21
+
22
+ def lc_radio_class(extra: nil)
23
+ lc_tw("lc-radio rounded-full", CHOICE_BASE, extra)
24
+ end
25
+
26
+ def lc_switch_class(extra: nil)
27
+ lc_tw("lc-switch shrink-0 m-0 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40", extra)
28
+ end
29
+
30
+ # A native checkbox. unchecked_value: submits a value when unchecked, like Rails' check_box; pass nil to omit it.
31
+ # A block adds content under the label, such as a preview inside a card: true tile.
32
+ def lc_checkbox(label, name: nil, value: "1", checked: false, unchecked_value: "0", hint: nil, card: false, **attributes, &block)
33
+ lc_choice("checkbox", label, hint, card, name, unchecked_value, { type: "checkbox", name: name, value: value, checked: checked, class: lc_checkbox_class }, attributes, &block)
34
+ end
35
+
36
+ def lc_radio(label, name:, value:, checked: false, hint: nil, card: false, **attributes, &block)
37
+ lc_choice("radio", label, hint, card, name, nil, { type: "radio", name: name, value: value, checked: checked, class: lc_radio_class }, attributes, &block)
38
+ end
39
+
40
+ # A checkbox drawn as a switch. It submits with the form, like a checkbox.
41
+ def lc_switch(label, name: nil, value: "1", checked: false, unchecked_value: "0", hint: nil, **attributes)
42
+ lc_choice("switch", label, hint, false, name, unchecked_value, { type: "checkbox", role: "switch", name: name, value: value, checked: checked, class: lc_switch_class }, attributes)
43
+ end
44
+
45
+ # A fieldset of checkboxes, radios or switches with a legend, hint and error. With an id, the
46
+ # fieldset's aria-describedby points at the hint and error, as lc_field does for its control.
47
+ def lc_choice_group(legend:, id: nil, hint: nil, error: nil, **attributes, &block)
48
+ lc_label!(legend, :legend)
49
+ errors = Array(error).compact_blank
50
+ described = [ ("#{id}-hint" if id && hint.present?), ("#{id}-error" if id && errors.any?) ].compact.presence&.join(" ")
51
+ defaults = { id: id, class: lc_field_class(extra: "m-0 min-w-0 border-0 p-0"), aria: { invalid: (true if errors.any?), describedby: described }, data: { slot: "choice-group", invalid: (true if errors.any?) } }
52
+ tag.fieldset(**lc_attributes(defaults, attributes)) do
53
+ safe_join([
54
+ tag.legend(legend, class: lc_field_label_class(extra: "p-0"), data: { slot: "choice-group-legend" }),
55
+ tag.div(class: "flex flex-col gap-related", &block),
56
+ *lc_field_messages(id, hint, errors)
57
+ ])
58
+ end
59
+ end
60
+
61
+ private
62
+ def lc_choice(slot, label, hint, card, name, unchecked_value, defaults, attributes, &block)
63
+ lc_label!(label, :label)
64
+ input = lc_attributes(defaults.merge(data: { slot: "#{slot}-input" }), attributes)
65
+ hidden = hidden_field_tag(name, unchecked_value, id: nil, disabled: input[:disabled]) if name && !unchecked_value.nil?
66
+ lc_choice_label(slot, label, hint, safe_join([ hidden, tag.input(**input) ].compact), card: card, &block)
67
+ end
68
+
69
+ # Shared with Lostcoords::Ui::FormBuilder, which renders its own inputs.
70
+ def lc_choice_label(slot, label, hint, input, card: false, errors: [], error_id: nil, **attributes, &block)
71
+ extra = capture(&block) if block
72
+ tag.label(**lc_attributes({ class: lc_choice_label_class(card: card), data: { slot: slot, card: (true if card) } }, attributes)) do
73
+ safe_join([
74
+ tag.span(input, class: "flex h-lh items-center"),
75
+ tag.span(class: "flex min-w-0 flex-1 flex-col gap-tight") do
76
+ safe_join([
77
+ tag.span(label, class: ("text-label" if card)),
78
+ (tag.span(hint, class: "text-small text-muted-foreground") if hint.present?),
79
+ (tag.span(safe_join(errors, tag.br), id: error_id, class: "text-small text-destructive", data: { slot: "field-error" }) if errors.any?),
80
+ (tag.span(extra, class: "flex min-w-0 flex-col pt-tight") if extra.present?)
81
+ ].compact)
82
+ end
83
+ ])
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,22 @@
1
+ module Lostcoords::Ui
2
+ module CodeBlockHelper
3
+ def lc_code_block(source, id:, language: nil, title: nil, label: nil, max_lines: 10, **attributes, &block)
4
+ lc_label!(id, :id)
5
+ lc_label!(title, :title) unless title.nil?
6
+ label ||= lc_t("lostcoords.ui.code_block.label")
7
+ lc_label!(label, :label)
8
+ raise ArgumentError, "max_lines must be a positive integer or nil" unless max_lines.nil? || (max_lines.is_a?(Integer) && max_lines.positive?)
9
+
10
+ source = String.new(source.to_s)
11
+ content = block ? capture(source, &block) : source
12
+ truncated = !max_lines.nil? && source.lines.size > max_lines
13
+ defaults = {
14
+ id: id, class: "lc-code-block group/code-block relative min-h-15 overflow-hidden rounded-2xl bg-muted",
15
+ data: { controller: "lc-code-block", action: "lc-copy:error->lc-code-block#copyFailed" }
16
+ }
17
+ render "lostcoords/ui/components/lc_code_block", id: id, language: language, title: title, label: label,
18
+ content: content, truncated: truncated, max_lines: max_lines,
19
+ attributes: lc_attributes(defaults, attributes)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ module Lostcoords::Ui
2
+ module CollapsibleHelper
3
+ def lc_collapsible(title:, open: false, **attributes, &block)
4
+ lc_label!(title, :title)
5
+ defaults = { class: lc_card_class(extra: "lc-collapsible"), open: open, data: { slot: "collapsible", controller: "lc-collapsible", action: "click->lc-collapsible#toggle" } }
6
+ render "lostcoords/ui/components/lc_collapsible", title: title, attributes: lc_attributes(defaults, attributes), content: (capture(&block) if block)
7
+ end
8
+ end
9
+ end