lostcoords-ui 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +352 -0
  4. data/app/assets/tailwind/lostcoords/ui/avatar.css +11 -0
  5. data/app/assets/tailwind/lostcoords/ui/card.css +26 -0
  6. data/app/assets/tailwind/lostcoords/ui/copy-button.css +33 -0
  7. data/app/assets/tailwind/lostcoords/ui/dialog.css +60 -0
  8. data/app/assets/tailwind/lostcoords/ui/disclosure.css +43 -0
  9. data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +462 -0
  10. data/app/assets/tailwind/lostcoords/ui/field.css +6 -0
  11. data/app/assets/tailwind/lostcoords/ui/glass.css +37 -0
  12. data/app/assets/tailwind/lostcoords/ui/layout.css +5 -0
  13. data/app/assets/tailwind/lostcoords/ui/popover.css +75 -0
  14. data/app/assets/tailwind/lostcoords/ui/reading.css +60 -0
  15. data/app/assets/tailwind/lostcoords/ui/sheet.css +170 -0
  16. data/app/assets/tailwind/lostcoords/ui/squircle.css +32 -0
  17. data/app/assets/tailwind/lostcoords/ui/theme.css +255 -0
  18. data/app/assets/tailwind/lostcoords/ui/typography.css +81 -0
  19. data/app/assets/tailwind/lostcoords/ui/ui.css +21 -0
  20. data/app/assets/tailwind/lostcoords/ui/utilities.css +45 -0
  21. data/app/assets/tailwind/lostcoords_ui/engine.css +1 -0
  22. data/app/helpers/lostcoords/ui/badge_helper.rb +162 -0
  23. data/app/helpers/lostcoords/ui/button_helper.rb +70 -0
  24. data/app/helpers/lostcoords/ui/card_helper.rb +60 -0
  25. data/app/helpers/lostcoords/ui/copy_button_helper.rb +40 -0
  26. data/app/helpers/lostcoords/ui/dialog_helper.rb +33 -0
  27. data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +245 -0
  28. data/app/helpers/lostcoords/ui/feedback_helper.rb +30 -0
  29. data/app/helpers/lostcoords/ui/form_helper.rb +93 -0
  30. data/app/helpers/lostcoords/ui/glass_helper.rb +25 -0
  31. data/app/helpers/lostcoords/ui/icon_helper.rb +15 -0
  32. data/app/helpers/lostcoords/ui/layout_helper.rb +60 -0
  33. data/app/helpers/lostcoords/ui/menu_helper.rb +41 -0
  34. data/app/helpers/lostcoords/ui/primitives_helper.rb +152 -0
  35. data/app/helpers/lostcoords/ui/reading_helper.rb +15 -0
  36. data/app/helpers/lostcoords/ui/render_attributes_helper.rb +46 -0
  37. data/app/helpers/lostcoords/ui/sheet_helper.rb +54 -0
  38. data/app/helpers/lostcoords/ui/structures_helper.rb +159 -0
  39. data/app/helpers/lostcoords/ui/tailwind_helper.rb +14 -0
  40. data/app/helpers/lostcoords/ui/translation_helper.rb +13 -0
  41. data/app/helpers/lostcoords/ui/typography_helper.rb +28 -0
  42. data/app/javascript/lostcoords/ui/controllers/confirm_dialog_controller.js +39 -0
  43. data/app/javascript/lostcoords/ui/controllers/disclosure_controller.js +163 -0
  44. data/app/javascript/lostcoords/ui/controllers/emoji_picker_controller.js +366 -0
  45. data/app/javascript/lostcoords/ui/controllers/modal_controller.js +27 -0
  46. data/app/javascript/lostcoords/ui/controllers/row_menu_controller.js +83 -0
  47. data/app/javascript/lostcoords/ui/controllers/sheet_controller.js +422 -0
  48. data/app/javascript/lostcoords/ui/controllers/toast_controller.js +69 -0
  49. data/app/javascript/lostcoords/ui/controllers/ui_copy_controller.js +56 -0
  50. data/app/javascript/lostcoords/ui/controllers/ui_popover_controller.js +17 -0
  51. data/app/javascript/lostcoords/ui/controllers/ui_selection_controller.js +69 -0
  52. data/app/javascript/lostcoords/ui/controllers/ui_toggle_controller.js +13 -0
  53. data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +455 -0
  54. data/app/javascript/lostcoords/ui/helpers/context_dialog.js +29 -0
  55. data/app/javascript/lostcoords/ui/helpers/dialog.js +31 -0
  56. data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +155 -0
  57. data/app/javascript/lostcoords/ui/helpers/floating_panel.js +108 -0
  58. data/app/javascript/lostcoords/ui/helpers/menu_keys.js +27 -0
  59. data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +13 -0
  60. data/app/javascript/lostcoords/ui/helpers/top_layer.js +102 -0
  61. data/app/javascript/lostcoords/ui/helpers/viewport.js +13 -0
  62. data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +176 -0
  63. data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +29 -0
  64. data/app/views/lostcoords/ui/components/_form_alert.html.erb +4 -0
  65. data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +13 -0
  66. data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +5 -0
  67. data/app/views/lostcoords/ui/components/_skeleton.html.erb +4 -0
  68. data/app/views/lostcoords/ui/components/_spinner.html.erb +10 -0
  69. data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +8 -0
  70. data/config/emoji_search_aliases.yml +146 -0
  71. data/config/emoji_search_names.yml +1917 -0
  72. data/config/emoji_search_names_ja.yml +6902 -0
  73. data/config/helper_renames.json +113 -0
  74. data/config/importmap.rb +3 -0
  75. data/config/locales/en.yml +33 -0
  76. data/config/locales/es.yml +33 -0
  77. data/config/locales/ja.yml +33 -0
  78. data/lib/lostcoords/ui/action_view.rb +10 -0
  79. data/lib/lostcoords/ui/builders.rb +42 -0
  80. data/lib/lostcoords/ui/components.rb +13 -0
  81. data/lib/lostcoords/ui/engine.rb +22 -0
  82. data/lib/lostcoords/ui/helpers.rb +11 -0
  83. data/lib/lostcoords/ui/icons.rb +24 -0
  84. data/lib/lostcoords/ui/rails.rb +3 -0
  85. data/lib/lostcoords/ui/resources.rb +17 -0
  86. data/lib/lostcoords/ui/tokens.rb +14 -0
  87. data/lib/lostcoords/ui/version.rb +5 -0
  88. data/lib/lostcoords/ui.rb +2 -0
  89. data/lib/tasks/lostcoords_ui.rake +15 -0
  90. data/script/migrate_helpers.rb +136 -0
  91. data/vendor/heroicons/LICENSE +21 -0
  92. data/vendor/heroicons/icons.json +1303 -0
  93. metadata +203 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7b432f8fc71804447fa32cf3290c7db2556243ef42c6f6849fa7604ed5081870
4
+ data.tar.gz: 5cf53096cc4479889f98f4764d6dac7391e787b0a3fe4bb2c21f23e5201d2918
5
+ SHA512:
6
+ metadata.gz: aa29b884d7e07cc1204088de38ace739805f0b5d7ff853bdd2196f9787c65ad71c80005e653180840c877e880d21b66159b8c660fcab0846dc9a34715236d8b4
7
+ data.tar.gz: 4da2f287536853733eac9ee133fd16b2627d0f54e4c2cfc2bdd2d254588b4ab51f9631191b1558f35ca52b6fbb85ec2caade6674d1987c8eb51cc4ebc07989ec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lost Coordinates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,352 @@
1
+ # lostcoords-ui
2
+
3
+ Render complete UI components from Rails or standalone ActionView ERB. Helpers include semantic HTML,
4
+ accessible attributes and Stimulus wiring. Tailwind v4 supplies the styles. Rails integrates Propshaft
5
+ and importmap automatically; standalone applications own their asset build.
6
+
7
+ ```erb
8
+ <%= lc_button "Save changes", type: :submit, icon: "check" %>
9
+ <%= lc_button_link_to "Projects", projects_path, variant: :secondary %>
10
+
11
+ <%= lc_card do %>
12
+ <%= lc_field label: "Project name", id: "project-name" do |field| %>
13
+ <%= lc_input name: "project[name]", **field %>
14
+ <% end %>
15
+ <% end %>
16
+ ```
17
+
18
+ The supported stack is ActionView, ERB, Tailwind and Stimulus. Each host application's lockfile pins
19
+ its gem version, so publishing a library update does not upgrade consumers.
20
+
21
+ ## Install
22
+
23
+ ```ruby
24
+ gem "lostcoords-ui", "~> 0.4.0"
25
+ ```
26
+
27
+ The engine adds its rendering helpers to host views, its JavaScript to the asset paths and its pins to
28
+ the importmap. Translations live under `lostcoords.ui.*`.
29
+
30
+ ### Standalone ActionView
31
+
32
+ ```ruby
33
+ require "lostcoords/ui/action_view"
34
+
35
+ class ViewContext < ActionView::Base
36
+ include Lostcoords::Ui::Helpers
37
+ end
38
+
39
+ view = ViewContext.with_empty_template_cache.with_view_paths(Lostcoords::Ui.view_paths)
40
+ view.lc_button("Save", icon: "check")
41
+ ```
42
+
43
+ For an existing view context, include the helpers and append `Lostcoords::Ui.view_paths` to its
44
+ lookup paths after application paths. The loader registers the library's translations without changing
45
+ the default locale. If the host has its own locale handling, override private `lc_locale` in the view
46
+ context; components do not call the host's `t` helper.
47
+
48
+ Standalone applications compile Tailwind and register Stimulus controllers through their own asset
49
+ pipeline. `Lostcoords::Ui.root` locates the gem's source assets. Rails applications receive automatic
50
+ registration; use `require "lostcoords/ui/rails"` explicitly if Rails loads after the components.
51
+
52
+ ### Stylesheet
53
+
54
+ Tailwind CSS v4 is required. Use `tailwindcss-rails` 4.4 or later to compile the component styles and
55
+ your app's utilities. Add the gem's entry to `app/assets/tailwind/application.css`:
56
+
57
+ ```css
58
+ @layer theme, base, layout, components, modules, utilities;
59
+
60
+ @import "tailwindcss/theme.css" layer(theme);
61
+ @import "tailwindcss/preflight.css" layer(base);
62
+ @import "tailwindcss/utilities.css" layer(utilities);
63
+
64
+ @plugin "@tailwindcss/typography";
65
+
66
+ @import "../builds/tailwind/lostcoords_ui.css";
67
+ ```
68
+
69
+ Tailwind resolves `@import` by path, and Bundler decides where the gem lives. tailwindcss-rails' `tailwindcss:engines`
70
+ task, which runs before every `tailwindcss:build` and `tailwindcss:watch` (and so before `assets:precompile`), writes
71
+ `app/assets/builds/tailwind/lostcoords_ui.css` with the absolute path to the gem's
72
+ `app/assets/tailwind/lostcoords_ui/engine.css`. `app/assets/builds` is already ignored by a Rails app's `.gitignore`
73
+ and `.dockerignore`, so there is nothing to commit. The file does not exist until the first Tailwind build, so a lint
74
+ that checks imports should skip it.
75
+
76
+ The gem's stylesheets write into the `components` and `modules` layers, so the host declares that layer order.
77
+ The theme defines the `dark:` variant with `html[data-theme="dark"]`. `ui.css` carries its own `@source` lines, so Tailwind
78
+ scans the gem's helpers, views and JavaScript wherever the gem is installed.
79
+
80
+ ### Theming
81
+
82
+ The engine includes a complete light and dark theme, system fonts, shadows, glass effects and motion
83
+ settings. Components work without an app-owned token file. To activate dark mode, set
84
+ `data-theme="dark"` on `<html>`. Your app controls the switch and saved preference.
85
+
86
+ To customize the theme, add CSS variable overrides after the engine import:
87
+
88
+ ```css
89
+ :root {
90
+ --sky: oklch(52% 0.14 155);
91
+ --sky-deep: oklch(46% 0.14 155);
92
+ --sky-soft: oklch(96% 0.02 155);
93
+ }
94
+
95
+ html[data-theme="dark"] {
96
+ --sky: oklch(75% 0.14 155);
97
+ --sky-deep: oklch(70% 0.14 155);
98
+ --sky-soft: color-mix(in oklab, var(--sky) 18%, var(--paper));
99
+ }
100
+ ```
101
+
102
+ Override only the values you need. The [theming guide](https://ui.lostcoords.com/theming.html) covers
103
+ colors, fonts, corners, shadows and advanced variables. Defaults live in
104
+ `app/assets/tailwind/lostcoords/ui/theme.css`. `Lostcoords::Ui::TOKENS` lists the variables in that file
105
+ for reference; apps do not have to define them.
106
+
107
+ ### Importmap
108
+
109
+ The engine pins its modules under the host's own keys, `controllers/*` and `helpers/*`, so
110
+ `eagerLoadControllersFrom("controllers", application)` registers the gem's controllers with no extra wiring.
111
+ A host file with the same name, such as `app/javascript/controllers/sheet_controller.js`, shadows the gem's.
112
+
113
+ ## Components
114
+
115
+ [Installation and setup](https://ui.lostcoords.com/installation.html) · [Live examples](https://ui.lostcoords.com). Each example renders the same ERB source shown
116
+ in its code panel. The API reference reads signatures from the helper definitions.
117
+ [API JSON](https://ui.lostcoords.com/api.json) and [llms.txt](https://ui.lostcoords.com/llms.txt)
118
+ include those signatures and the runnable examples.
119
+
120
+ Buttons use `primary`, `secondary`, `ghost` or `destructive`, with `sm`, `base` or `lg` sizes.
121
+ `lc_button_to` preserves Rails' form, HTTP method and CSRF behavior. Block helpers capture ERB content.
122
+ Use `icon: "plus"` for a leading Heroicon or `icon_position: :end` for a trailing one. Custom icons and
123
+ spinners use `data-icon="inline-start"` or `data-icon="inline-end"`; buttons adjust that edge's padding.
124
+
125
+ ```erb
126
+ <%= lc_button_to "Archive", project_path(@project), method: :delete, variant: :destructive %>
127
+ <%= lc_icon_button "Settings", icon: "cog-6-tooth" %>
128
+
129
+ <%= lc_button class: "w-full", data: { action: "editor#save" }, aria: { busy: true } do %>
130
+ <%= lc_spinner size: :sm %>
131
+ Saving…
132
+ <% end %>
133
+ ```
134
+
135
+ Use native attributes for application behavior, `class:` to merge additional Tailwind utilities, and
136
+ theme tokens to restyle the library. `lc_field` yields the input's `id` and validation attributes so
137
+ labels, hints and errors stay connected. Linked cards use `lc_card(href: ...)`; cards containing
138
+ buttons or fields remain ordinary containers.
139
+
140
+ ## Copy feedback and motion
141
+
142
+ ```erb
143
+ <%= lc_copy_button value: "https://example.com/invite" %>
144
+ <%= lc_copy_button "Copy invite link", value: invite_url, copied_label: "Link copied" %>
145
+ <%= lc_copy_button "Copy link", value: invite_url, icon_only: true, variant: :ghost %>
146
+ <%= lc_copy_button "Copy code", source: "code-snippet", reset_after: 2500 %>
147
+ ```
148
+
149
+ Pass exactly one of `value:` (literal text) or `source:` (an element ID). The source is read at click time:
150
+ inputs and textareas use their current value; other elements use their text content. Text is copied as supplied,
151
+ including whitespace. Clipboard access requires HTTPS or localhost and browser permission.
152
+
153
+ After a successful copy, the button transitions to a check and "Copied", then resets after `reset_after:`
154
+ milliseconds (2000 by default). Failure shows a retry icon and label with an accessible explanation.
155
+ The button reserves space for all labels, preserves focus, and announces the outcome through a live region.
156
+ Default labels are translated in English, Japanese and Spanish. Use `label` (the first argument),
157
+ `copied_label:` and `error_label:` to customize visible text, and `aria: { label: ... }` for a contextual name.
158
+ Normal button variants, sizes, classes, disabled state and attributes are supported.
159
+
160
+ The component emits bubbling `ui-copy:copied` (`detail.text`) and `ui-copy:error` (`detail.error`) events
161
+ from its wrapper. Listen on an ancestor to connect application behavior. The documentation uses the error
162
+ event to reveal clipped source code for manual copying.
163
+
164
+ Buttons include hover and press feedback. Override `--ui-motion-quick` (140ms), `--ui-motion-control` (180ms),
165
+ `--ui-motion-feedback` (160ms), `--ui-press-scale` (0.96) and `--ease-out` in your theme or on a container.
166
+ Reduced motion removes control movement and makes copy feedback immediate. Your application owns the
167
+ pending, success and failure states of network actions such as saving a form.
168
+
169
+ [Try the copy button](https://ui.lostcoords.com/components/copy_button.html) ·
170
+ [Motion reference](https://ui.lostcoords.com/components/motion.html)
171
+
172
+ ### Icons
173
+
174
+ `lc_icon` renders Heroicons v2.2.0 SVGs. Variants are `outline` (the default), `solid`, `mini` and `micro`.
175
+ Pass SVG attributes through `options:` and existing path attributes through `path_options:`.
176
+
177
+ ```erb
178
+ <%= lc_icon "check", options: { class: "size-5" } %>
179
+ <%= lc_icon "check", variant: :mini, options: {
180
+ class: "size-4", role: "img", aria: { label: "Complete", hidden: false }
181
+ } %>
182
+ ```
183
+
184
+ ## Advanced style composition
185
+
186
+ ### Typography and layout
187
+
188
+ The engine stylesheet includes shared text styles and layout defaults. Heading level controls document
189
+ structure independently of its visual size. Layout parents own the spacing between children, including fields.
190
+ Inline `<code>` elements get a shared monospace style with a theme-aware background. `<pre><code>`
191
+ blocks keep their block styling.
192
+
193
+ ```erb
194
+ <%= lc_stack gap: :section do %>
195
+ <%= lc_heading "Workspace", level: 1, size: :page %>
196
+ <%= lc_section title: "Members" do %>
197
+ <%= lc_card do %>
198
+ <%= lc_stack gap: :related do %>
199
+ <%= lc_heading "Design team", level: 3, size: :card %>
200
+ <%= lc_text "Three people share this workspace.", tone: :muted %>
201
+ <% end %>
202
+ <% end %>
203
+ <% end %>
204
+ <% end %>
205
+ ```
206
+
207
+ Heading sizes are `page`, `section` and `card`. Text sizes are `body`, `small` and `caption`,
208
+ with `default`, `muted` and `subtle` tones. Stacks and inline groups accept `tight` (4px), `related`
209
+ (8px), `heading` (12px), `content` (16px), `inset` (20px) and `section` (32px) gaps at the default
210
+ browser font size. `lc_section` leaves 12px below its heading and 16px between content blocks.
211
+ `lc_container` provides `page`, `reading` and `form` widths. Only the page container adds gutters.
212
+
213
+ Fonts and colours come from the shared theme and your app's overrides. Optional custom properties override the shared defaults:
214
+ `--ui-space-section`, `--ui-heading-page-size`, `--ui-heading-page-line-height`, `--ui-page-max`,
215
+ `--ui-gutter` and `--ui-reading-measure`. Set them in the host theme or on a container. Japanese headings
216
+ inherit `lang="ja"` and use natural letter spacing.
217
+
218
+ [Browse the foundations examples](https://ui.lostcoords.com/components/foundations.html).
219
+
220
+ ### Reading content
221
+
222
+ For articles or rendered Markdown, enable Tailwind Typography in the host input:
223
+
224
+ ```css
225
+ @plugin "@tailwindcss/typography";
226
+ ```
227
+
228
+ The standalone Tailwind CLI used by `tailwindcss-rails` includes this plugin. `lc_prose` supplies the host
229
+ theme colours, a 65ch maximum width, 1.75 line height and proportional block spacing. Its block follows
230
+ Rails' normal escaping rules; sanitise rendered user content before marking it safe.
231
+
232
+ ```erb
233
+ <%= lc_prose do %>
234
+ <h2>Share a workspace</h2>
235
+ <p>Invite your team and keep your work together.</p>
236
+ <div class="not-prose"><%= lc_button "Invite someone" %></div>
237
+ <% end %>
238
+ ```
239
+
240
+ Override `--ui-reading-size`, `--ui-reading-leading`, `--ui-reading-flow` and `--ui-reading-measure`
241
+ to tune a reading context. Use `not-prose` for embedded components.
242
+
243
+ ### Class helpers
244
+
245
+ Class helpers remain available for custom markup. The application owns its semantics and behavior.
246
+ `lc_glass_class` supplies translucent surfaces; `lc_tw` merges utilities with the last class winning.
247
+
248
+ ```erb
249
+ <nav class="<%= lc_glass_class(shape: :bar) %>" aria-label="Project navigation">
250
+ <%= link_to "Overview", project_path(@project), class: lc_button_class(variant: :ghost) %>
251
+ </nav>
252
+ ```
253
+
254
+ Generated by `bin/rails lostcoords:ui:builders`; `bin/lint` checks this list against the style helpers.
255
+
256
+ <!-- builders -->
257
+ - `lc_icon_badge_class(size: :base, extra: nil)` (BadgeHelper)
258
+ - `lc_icon_medallion_class(size: :sm, tone: :accent, extra: nil)` (BadgeHelper)
259
+ - `lc_avatar_class(size: :base, ink: false, linked: false, extra: nil)` (BadgeHelper)
260
+ - `lc_avatar_stack_class(compact: false, extra: nil)` (BadgeHelper)
261
+ - `lc_avatar_stack_row_class(extra: nil)` (BadgeHelper)
262
+ - `lc_eyebrow_class(extra: nil)` (BadgeHelper)
263
+ - `lc_badge_class(tone: :neutral, extra: nil)` (BadgeHelper)
264
+ - `lc_status_pill_class(tone: :accent, extra: nil)` (BadgeHelper)
265
+ - `lc_pill_class(active: false, ghost: false, size: :base, extra: nil)` (BadgeHelper)
266
+ - `lc_pill_row_class(extra: nil)` (BadgeHelper)
267
+ - `lc_button_class(variant: :default, size: :base, block: false, extra: nil)` (ButtonHelper)
268
+ - `lc_icon_button_class(shape: :square, size: :base, extra: nil)` (ButtonHelper)
269
+ - `lc_card_class(interactive: false, dense: false, extra: nil)` (CardHelper)
270
+ - `lc_card_surface_class(extra: nil)` (CardHelper)
271
+ - `lc_form_card_class(padding: :default, extra: nil)` (CardHelper)
272
+ - `lc_dialog_class(kind = :modal, extra: nil)` (DialogHelper)
273
+ - `lc_dialog_surface_class(extra: nil)` (DialogHelper)
274
+ - `lc_emoji_picker_groups` (EmojiPickerHelper)
275
+ - `lc_emoji_picker_count` (EmojiPickerHelper)
276
+ - `lc_emoji_picker_version` (EmojiPickerHelper)
277
+ - `lc_emoji_picker_search_text(emoji)` (EmojiPickerHelper)
278
+ - `lc_emoji_picker_search_name(emoji)` (EmojiPickerHelper)
279
+ - `lc_emoji_picker_search_labels(emoji)` (EmojiPickerHelper)
280
+ - `lc_emoji_picker_toneable?(emoji)` (EmojiPickerHelper)
281
+ - `lc_spinner_class(size: :base, extra: nil)` (FeedbackHelper)
282
+ - `lc_skeleton_class(extra: nil)` (FeedbackHelper)
283
+ - `lc_empty_state_class(extra: nil)` (FeedbackHelper)
284
+ - `lc_segmented_class(extra: nil)` (FormHelper)
285
+ - `lc_segmented_tab_class(active: false, extra: nil)` (FormHelper)
286
+ - `lc_input_class(size: :base, extra: nil)` (FormHelper)
287
+ - `lc_dropdown_trigger_class(size: :base, extra: nil)` (FormHelper)
288
+ - `lc_select_class(size: :base, extra: nil)` (FormHelper)
289
+ - `lc_textarea_class(extra: nil)` (FormHelper)
290
+ - `lc_field_class(extra: nil)` (FormHelper)
291
+ - `lc_field_label_class(extra: nil)` (FormHelper)
292
+ - `lc_form_actions_class(extra: nil)` (FormHelper)
293
+ - `lc_form_alert_class(extra: nil)` (FormHelper)
294
+ - `lc_form_alert_icon(tone: :error)` (FormHelper)
295
+ - `lc_glass_class(shape: :bar, thick: false, extra: nil)` (GlassHelper)
296
+ - `lc_stack_class(gap: :content, extra: nil)` (LayoutHelper)
297
+ - `lc_inline_class(gap: :related, extra: nil)` (LayoutHelper)
298
+ - `lc_container_class(size: :page, extra: nil)` (LayoutHelper)
299
+ - `lc_section_header_class(extra: nil)` (LayoutHelper)
300
+ - `lc_popover_class(align: :end, extra: nil)` (MenuHelper)
301
+ - `lc_menu_popover_class(align: :end, extra: nil)` (MenuHelper)
302
+ - `lc_menu_option_class(tone: :default, extra: nil)` (MenuHelper)
303
+ - `lc_prose_class(extra: nil)` (ReadingHelper)
304
+ - `lc_sheet_data(data = {}, detents: "fit")` (SheetHelper)
305
+ - `lc_sheet_row_class(extra: nil)` (SheetHelper)
306
+ - `lc_sheet_row_label_class(extra: nil)` (SheetHelper)
307
+ - `lc_sheet_chip_class(active: false, member: false, extra: nil)` (SheetHelper)
308
+ - `lc_sheet_datetime_input_class(extra: nil)` (SheetHelper)
309
+ - `lc_sheet_text_input_class(extra: nil)` (SheetHelper)
310
+ - `lc_tw(*classes)` (TailwindHelper)
311
+ - `lc_heading_class(size: :section, extra: nil)` (TypographyHelper)
312
+ - `lc_text_class(size: :body, tone: :default, extra: nil)` (TypographyHelper)
313
+ <!-- /builders -->
314
+
315
+ ## Blocks
316
+
317
+ [Browse the blocks](https://ui.lostcoords.com/blocks.html) for complete layouts assembled from the
318
+ existing components. Each block has an interactive preview and the ERB, CSS and JavaScript that
319
+ render it. Copy the files into your app and adapt the content, routes and actions.
320
+
321
+ Blocks live in the docs app. They add no rendering helpers or installable block APIs to the gem.
322
+
323
+ ## Docs
324
+
325
+ The docs app lives in `test/dummy`. Run `parcela up --detach` and open https://ui.localhost.
326
+ Parcela starts Rails and the Tailwind watcher. `parcela down` stops this environment.
327
+
328
+ `bin/rails lostcoords:ui:showcase` exports the home, component pages, blocks, token reference, machine-readable
329
+ API and assets to `docs/` for GitHub Pages. CI checks that the export matches the source and that every
330
+ public rendering helper has a runnable example.
331
+
332
+ ## Development
333
+
334
+ ```sh
335
+ bundle install && npm ci
336
+ bin/rails server # the dummy app in test/dummy
337
+ bin/rails tailwindcss:build
338
+ bin/test
339
+ bin/lint
340
+ ```
341
+
342
+ The helper migration tool updates Ruby calls, ERB code, and Ruby/ERB Markdown examples to the `lc_`
343
+ namespace. Run it in a consumer checkout to preview changes, then add `--write` to apply them.
344
+ Review dynamic method names stored in strings separately.
345
+
346
+ ```sh
347
+ ruby "$(bundle show lostcoords-ui)/script/migrate_helpers.rb" .
348
+ ruby "$(bundle show lostcoords-ui)/script/migrate_helpers.rb" . --write
349
+ ```
350
+
351
+ The icon catalog and its MIT license live in `vendor/heroicons`. Regenerate them from the pinned
352
+ upstream archive with `ruby bin/update-icons`.
@@ -0,0 +1,11 @@
1
+ /* A linked avatar's ring draws inside its own footprint, so it takes exactly the space of an
2
+ unlinked one: the outline sits on the edge, over any cutout border, and the inset shadow
3
+ paints the gap in the surface colour. Contexts that already cut the avatar out with a
4
+ border set --avatar-ring-inset (or --avatar-ring-gap) so the gap doesn't double up. */
5
+ @utility avatar-ring {
6
+ --avatar-ring: clamp(1.5px, calc(var(--avatar-size, var(--avatar-size-default, --spacing(8))) * 0.06), 3px);
7
+
8
+ outline: var(--avatar-ring) solid color-mix(in oklab, var(--sky) 74%, var(--paper));
9
+ outline-offset: calc(var(--avatar-ring) * -1);
10
+ box-shadow: inset 0 0 0 var(--avatar-ring-inset, calc(var(--avatar-ring) * var(--avatar-ring-gap, 2))) var(--avatar-cutout, var(--paper));
11
+ }
@@ -0,0 +1,26 @@
1
+ @layer components {
2
+ /* Shared paper-card chrome. Used as a bare `.card-surface` hook in ~17
3
+ places across many feature partials (group lists, transactions, overview
4
+ cards and currency tools. Kept authored — not folded into
5
+ card_surface_class — because several call sites override the border
6
+ (`border-transparent`), background or radius with utilities that must
7
+ out-cascade this base; a utilities-layer helper emitting a `[border:…]`
8
+ shorthand would defeat those overrides. Semantic classes set the
9
+ --card-surface-* vars for local tone. (Form cards use form_card_class,
10
+ which carries this same chrome plus padding/container/error border.) */
11
+ .card-surface {
12
+ --avatar-cutout: var(--card-surface-bg, var(--paper-2));
13
+ background: var(--card-surface-bg, var(--paper-2));
14
+ border: var(--card-surface-border, 1px solid transparent);
15
+ border-radius: var(--card-surface-radius, var(--radius-3xl));
16
+ box-shadow: var(--card-surface-shadow, none);
17
+ transition:
18
+ background 0.18s var(--ease-out),
19
+ border-color 0.18s var(--ease-out),
20
+ box-shadow 0.18s var(--ease-out);
21
+ }
22
+
23
+ .form-card + .form-card { margin-block-start: 1rem; }
24
+
25
+ .form-submit { margin-block-start: 2rem; }
26
+ }
@@ -0,0 +1,33 @@
1
+ @layer components {
2
+ .ui-copy { display: inline-grid; }
3
+
4
+ .ui-copy-content {
5
+ grid-area: 1 / 1;
6
+ display: inline-flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ gap: inherit;
10
+ opacity: 0;
11
+ visibility: hidden;
12
+ transform: translateY(0.2rem) scale(0.96);
13
+ transition:
14
+ opacity var(--ui-motion-feedback) var(--ease-out),
15
+ transform var(--ui-motion-feedback) var(--ease-out),
16
+ visibility var(--ui-motion-feedback);
17
+ }
18
+
19
+ .ui-copy[data-state="idle"] [data-copy-state="idle"],
20
+ .ui-copy[data-state="copied"] [data-copy-state="copied"],
21
+ .ui-copy[data-state="error"] [data-copy-state="error"] {
22
+ opacity: 1;
23
+ visibility: visible;
24
+ transform: none;
25
+ }
26
+
27
+ @media (prefers-reduced-motion: reduce) {
28
+ .ui-copy-content {
29
+ transform: none;
30
+ transition: none;
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,60 @@
1
+ @layer components {
2
+ .app-dialog {
3
+ --dialog-from: translateY(0.4rem) scale(0.98);
4
+ --dialog-fade: 0.2s;
5
+ --dialog-duration: 0.2s;
6
+
7
+ /* Preflight's `margin: 0` clobbers the UA `margin: auto` that centres a modal dialog. */
8
+ margin: auto;
9
+ overflow: hidden;
10
+ overscroll-behavior: contain;
11
+ opacity: 0;
12
+ transform: var(--dialog-from);
13
+ transition:
14
+ opacity var(--dialog-fade) var(--ease-out-expo),
15
+ transform var(--dialog-duration) var(--ease-out-expo),
16
+ display var(--dialog-duration) allow-discrete,
17
+ overlay var(--dialog-duration) allow-discrete;
18
+
19
+ &[open] {
20
+ opacity: 1;
21
+ transform: none;
22
+
23
+ @starting-style {
24
+ opacity: 0;
25
+ transform: var(--dialog-from);
26
+ }
27
+ }
28
+
29
+ /* Fades the tint and the blur, not opacity: Chrome applies a backdrop-filter
30
+ under opacity < 1 to only some of the page's layers. */
31
+ &::backdrop {
32
+ background: transparent;
33
+ backdrop-filter: blur(0);
34
+ transition:
35
+ background-color var(--dialog-fade) var(--ease-out),
36
+ backdrop-filter var(--dialog-fade) var(--ease-out),
37
+ display var(--dialog-fade) allow-discrete,
38
+ overlay var(--dialog-fade) allow-discrete;
39
+ }
40
+
41
+ &[open]::backdrop {
42
+ background: var(--scrim);
43
+ backdrop-filter: blur(2px);
44
+
45
+ @starting-style {
46
+ background: transparent;
47
+ backdrop-filter: blur(0);
48
+ }
49
+ }
50
+
51
+ &.context-dialog[open]::backdrop {
52
+ background: var(--scrim-soft);
53
+ backdrop-filter: none;
54
+ }
55
+
56
+ @media (prefers-reduced-motion: reduce) {
57
+ &::backdrop { transition: none; }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,43 @@
1
+ /* Removing [open] hides the content at once, so disclosure_controller holds the
2
+ panel open under [data-closing] for the exit, then closes it. Transitioning
3
+ content-visibility makes WebKit keep the panel hidden for part of the opening. */
4
+ @layer components {
5
+ .disclosure-animated {
6
+ interpolate-size: allow-keywords; /* browser-compat: enhancement */
7
+
8
+ &::details-content {
9
+ block-size: 0;
10
+ overflow-y: clip;
11
+ opacity: 0;
12
+ translate: 0 -0.375rem;
13
+ transition:
14
+ block-size 420ms var(--ease-out-expo),
15
+ opacity 220ms var(--ease-out),
16
+ translate 320ms var(--ease-out-expo);
17
+ }
18
+
19
+ &[open]::details-content {
20
+ block-size: auto;
21
+ opacity: 1;
22
+ translate: 0;
23
+ }
24
+
25
+ /* Where `auto` cannot ease, a 0 here would snap shut before the fade
26
+ shows; the controller shrinks the <details> instead. */
27
+ &[data-closing]::details-content {
28
+ opacity: 0;
29
+ translate: 0 -0.375rem;
30
+ transition-duration: 260ms, 160ms, 200ms;
31
+ transition-timing-function: var(--ease-out);
32
+ }
33
+
34
+ @supports (interpolate-size: allow-keywords) {
35
+ &[data-closing]::details-content { block-size: 0; }
36
+ }
37
+
38
+ /* The global reduced-motion reset's *::after does not reach this pseudo. */
39
+ @media (prefers-reduced-motion: reduce) {
40
+ &::details-content { transition-duration: 0.01ms; }
41
+ }
42
+ }
43
+ }