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.
- checksums.yaml +4 -4
- data/README.md +303 -89
- data/app/assets/tailwind/lostcoords/ui/avatar.css +6 -6
- data/app/assets/tailwind/lostcoords/ui/base.css +45 -0
- data/app/assets/tailwind/lostcoords/ui/card.css +20 -22
- data/app/assets/tailwind/lostcoords/ui/choice.css +39 -0
- data/app/assets/tailwind/lostcoords/ui/code-block.css +10 -0
- data/app/assets/tailwind/lostcoords/ui/components.css +25 -0
- data/app/assets/tailwind/lostcoords/ui/copy-button.css +9 -9
- data/app/assets/tailwind/lostcoords/ui/dialog.css +17 -17
- data/app/assets/tailwind/lostcoords/ui/disclosure.css +5 -5
- data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +43 -43
- data/app/assets/tailwind/lostcoords/ui/field.css +2 -2
- data/app/assets/tailwind/lostcoords/ui/glass.css +9 -8
- data/app/assets/tailwind/lostcoords/ui/layout.css +2 -2
- data/app/assets/tailwind/lostcoords/ui/popover.css +14 -14
- data/app/assets/tailwind/lostcoords/ui/reading.css +24 -24
- data/app/assets/tailwind/lostcoords/ui/sheet.css +60 -60
- data/app/assets/tailwind/lostcoords/ui/squircle.css +7 -27
- data/app/assets/tailwind/lostcoords/ui/table.css +51 -0
- data/app/assets/tailwind/lostcoords/ui/theme.css +194 -225
- data/app/assets/tailwind/lostcoords/ui/tooltip.css +50 -0
- data/app/assets/tailwind/lostcoords/ui/typography.css +37 -61
- data/app/assets/tailwind/lostcoords/ui/ui.css +2 -21
- data/app/assets/tailwind/lostcoords/ui/utilities.css +14 -22
- data/app/helpers/lostcoords/ui/badge_helper.rb +67 -110
- data/app/helpers/lostcoords/ui/button_helper.rb +36 -33
- data/app/helpers/lostcoords/ui/card_helper.rb +9 -52
- data/app/helpers/lostcoords/ui/choice_helper.rb +83 -0
- data/app/helpers/lostcoords/ui/code_block_helper.rb +22 -0
- data/app/helpers/lostcoords/ui/copy_button_helper.rb +7 -7
- data/app/helpers/lostcoords/ui/description_list_helper.rb +12 -0
- data/app/helpers/lostcoords/ui/dialog_helper.rb +6 -13
- data/app/helpers/lostcoords/ui/feedback_helper.rb +25 -6
- data/app/helpers/lostcoords/ui/form_helper.rb +36 -59
- data/app/helpers/lostcoords/ui/glass_helper.rb +7 -7
- data/app/helpers/lostcoords/ui/layout_helper.rb +13 -11
- data/app/helpers/lostcoords/ui/menu_helper.rb +8 -8
- data/app/helpers/lostcoords/ui/navigation_helper.rb +131 -0
- data/app/helpers/lostcoords/ui/overlay_helper.rb +17 -0
- data/app/helpers/lostcoords/ui/primitives_helper.rb +39 -61
- data/app/helpers/lostcoords/ui/reading_helper.rb +2 -2
- data/app/helpers/lostcoords/ui/sheet_helper.rb +3 -45
- data/app/helpers/lostcoords/ui/structures_helper.rb +55 -64
- data/app/helpers/lostcoords/ui/table_helper.rb +11 -0
- data/app/helpers/lostcoords/ui/tailwind_helper.rb +13 -4
- data/app/helpers/lostcoords/ui/typography_helper.rb +11 -11
- data/app/javascript/lostcoords/ui/controllers/lc_code_block_controller.js +23 -0
- data/app/javascript/lostcoords/ui/controllers/{disclosure_controller.js → lc_collapsible_controller.js} +3 -3
- data/app/javascript/lostcoords/ui/controllers/{confirm_dialog_controller.js → lc_confirm_dialog_controller.js} +4 -4
- data/app/javascript/lostcoords/ui/controllers/{modal_controller.js → lc_dialog_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{emoji_picker_controller.js → lc_emoji_picker_controller.js} +4 -4
- data/app/javascript/lostcoords/ui/controllers/{row_menu_controller.js → lc_menu_controller.js} +3 -3
- data/app/javascript/lostcoords/ui/controllers/{ui_popover_controller.js → lc_popover_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{ui_selection_controller.js → lc_selection_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{sheet_controller.js → lc_sheet_controller.js} +15 -15
- data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +39 -0
- data/app/javascript/lostcoords/ui/controllers/{toast_controller.js → lc_toast_controller.js} +2 -2
- data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +14 -0
- data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +5 -5
- data/app/javascript/lostcoords/ui/helpers/context_dialog.js +6 -6
- data/app/javascript/lostcoords/ui/helpers/dialog.js +2 -2
- data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +7 -7
- data/app/javascript/lostcoords/ui/helpers/floating_panel.js +2 -2
- data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +4 -4
- data/app/javascript/lostcoords/ui/index.js +51 -0
- data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +30 -30
- data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +18 -0
- data/app/views/lostcoords/ui/components/_lc_collapsible.html.erb +5 -0
- data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +11 -8
- data/app/views/lostcoords/ui/components/_lc_floating_panel.html.erb +8 -0
- data/config/helper_renames.json +23 -25
- data/config/importmap.rb +10 -2
- data/config/locales/en.yml +23 -0
- data/config/locales/es.yml +23 -0
- data/config/locales/ja.yml +23 -0
- data/lib/lostcoords/ui/action_view.rb +5 -3
- data/lib/lostcoords/ui/builders.rb +1 -1
- data/lib/lostcoords/ui/form_builder.rb +113 -0
- data/lib/lostcoords/ui/parts.rb +154 -0
- data/lib/lostcoords/ui/tokens.rb +24 -1
- data/lib/lostcoords/ui/version.rb +1 -1
- metadata +33 -17
- data/app/views/lostcoords/ui/components/_form_alert.html.erb +0 -4
- data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +0 -5
- data/app/views/lostcoords/ui/components/_skeleton.html.erb +0 -4
- data/app/views/lostcoords/ui/components/_spinner.html.erb +0 -10
- data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +0 -8
- /data/app/javascript/lostcoords/ui/controllers/{ui_copy_controller.js → lc_copy_controller.js} +0 -0
- /data/app/javascript/lostcoords/ui/controllers/{ui_toggle_controller.js → lc_toggle_controller.js} +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# locals: (id:, label:, title:, controller:, role:, trigger_variant:, attributes:, content:) %>
|
|
2
|
+
<%= tag.div(class: "relative inline-flex", data: { controller: controller }) do %>
|
|
3
|
+
<%= lc_button label, variant: trigger_variant, id: "#{id}-trigger", aria: { haspopup: role, expanded: false, controls: id }, data: { "#{controller}_target" => "trigger", action: "#{controller}#toggle" } %>
|
|
4
|
+
<%= tag.div(**attributes) do %>
|
|
5
|
+
<% if title %><h2 id="<%= id %>-title" class="<%= lc_heading_class(size: :subheading) %>"><%= title %></h2><% end %>
|
|
6
|
+
<%= content %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
data/config/helper_renames.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"button_class": "lc_button_class",
|
|
7
7
|
"button_variants": "lc_button_variants",
|
|
8
8
|
"card_class": "lc_card_class",
|
|
9
|
-
"card_surface_class": "
|
|
9
|
+
"card_surface_class": "lc_card_class",
|
|
10
10
|
"container_class": "lc_container_class",
|
|
11
11
|
"dialog_class": "lc_dialog_class",
|
|
12
12
|
"dialog_kinds": "lc_dialog_kinds",
|
|
@@ -20,30 +20,29 @@
|
|
|
20
20
|
"emoji_picker_toneable?": "lc_emoji_picker_toneable?",
|
|
21
21
|
"emoji_picker_version": "lc_emoji_picker_version",
|
|
22
22
|
"empty_state_class": "lc_empty_state_class",
|
|
23
|
-
"eyebrow_class": "lc_eyebrow_class",
|
|
24
23
|
"field_class": "lc_field_class",
|
|
25
24
|
"field_label_class": "lc_field_label_class",
|
|
26
|
-
"form_actions_class": "
|
|
27
|
-
"form_alert_class": "
|
|
28
|
-
"form_alert_icon": "
|
|
29
|
-
"form_card_class": "
|
|
25
|
+
"form_actions_class": "lc_inline_class",
|
|
26
|
+
"form_alert_class": "lc_alert_class",
|
|
27
|
+
"form_alert_icon": "lc_alert_icon",
|
|
28
|
+
"form_card_class": "lc_card_class",
|
|
30
29
|
"glass_class": "lc_glass_class",
|
|
31
30
|
"heading_class": "lc_heading_class",
|
|
32
31
|
"heroicon": "lc_icon",
|
|
33
32
|
"icon_badge_class": "lc_icon_badge_class",
|
|
34
33
|
"icon_button_class": "lc_icon_button_class",
|
|
35
|
-
"icon_medallion_class": "
|
|
34
|
+
"icon_medallion_class": "lc_icon_badge_class",
|
|
36
35
|
"inline_class": "lc_inline_class",
|
|
37
36
|
"input_class": "lc_input_class",
|
|
38
37
|
"menu_option_class": "lc_menu_option_class",
|
|
39
38
|
"menu_popover_class": "lc_menu_popover_class",
|
|
40
|
-
"pill_class": "
|
|
41
|
-
"pill_row_class": "
|
|
39
|
+
"pill_class": "lc_toggle_class",
|
|
40
|
+
"pill_row_class": "lc_inline_class",
|
|
42
41
|
"popover_class": "lc_popover_class",
|
|
43
42
|
"prose_class": "lc_prose_class",
|
|
44
43
|
"section_header_class": "lc_section_header_class",
|
|
45
|
-
"segmented_class": "
|
|
46
|
-
"segmented_tab_class": "
|
|
44
|
+
"segmented_class": "lc_toggle_group_class",
|
|
45
|
+
"segmented_tab_class": "lc_toggle_group_item_class",
|
|
47
46
|
"select_class": "lc_select_class",
|
|
48
47
|
"sheet_chip_class": "lc_sheet_chip_class",
|
|
49
48
|
"sheet_data": "lc_sheet_data",
|
|
@@ -54,7 +53,7 @@
|
|
|
54
53
|
"skeleton_class": "lc_skeleton_class",
|
|
55
54
|
"spinner_class": "lc_spinner_class",
|
|
56
55
|
"stack_class": "lc_stack_class",
|
|
57
|
-
"status_pill_class": "
|
|
56
|
+
"status_pill_class": "lc_badge_class",
|
|
58
57
|
"text_class": "lc_text_class",
|
|
59
58
|
"textarea_class": "lc_textarea_class",
|
|
60
59
|
"tw": "lc_tw",
|
|
@@ -68,45 +67,44 @@
|
|
|
68
67
|
"ui_button_link_to": "lc_button_link_to",
|
|
69
68
|
"ui_button_to": "lc_button_to",
|
|
70
69
|
"ui_card": "lc_card",
|
|
71
|
-
"ui_card_surface": "
|
|
70
|
+
"ui_card_surface": "lc_card",
|
|
72
71
|
"ui_container": "lc_container",
|
|
73
72
|
"ui_copy_button": "lc_copy_button",
|
|
74
73
|
"ui_copy_content": "lc_copy_content",
|
|
75
74
|
"ui_dialog": "lc_dialog",
|
|
76
|
-
"ui_disclosure": "
|
|
75
|
+
"ui_disclosure": "lc_collapsible",
|
|
77
76
|
"ui_emoji_picker": "lc_emoji_picker",
|
|
78
77
|
"ui_empty_state": "lc_empty_state",
|
|
79
|
-
"ui_eyebrow": "lc_eyebrow",
|
|
80
78
|
"ui_field": "lc_field",
|
|
81
|
-
"ui_form_actions": "
|
|
82
|
-
"ui_form_alert": "
|
|
83
|
-
"ui_form_card": "
|
|
79
|
+
"ui_form_actions": "lc_inline",
|
|
80
|
+
"ui_form_alert": "lc_alert",
|
|
81
|
+
"ui_form_card": "lc_card",
|
|
84
82
|
"ui_heading": "lc_heading",
|
|
85
83
|
"ui_icon_badge": "lc_icon_badge",
|
|
86
84
|
"ui_icon_button": "lc_icon_button",
|
|
87
85
|
"ui_inline": "lc_inline",
|
|
88
86
|
"ui_input": "lc_input",
|
|
89
87
|
"ui_label!": "lc_label!",
|
|
90
|
-
"ui_medallion": "
|
|
88
|
+
"ui_medallion": "lc_icon_badge",
|
|
91
89
|
"ui_menu": "lc_menu",
|
|
92
90
|
"ui_menu_option": "lc_menu_option",
|
|
93
91
|
"ui_normalize_attributes": "lc_normalize_attributes",
|
|
94
92
|
"ui_option!": "lc_option!",
|
|
95
|
-
"ui_pill": "
|
|
96
|
-
"ui_pill_row": "
|
|
93
|
+
"ui_pill": "lc_toggle",
|
|
94
|
+
"ui_pill_row": "lc_inline",
|
|
97
95
|
"ui_popover": "lc_popover",
|
|
98
96
|
"ui_prose": "lc_prose",
|
|
99
97
|
"ui_render_dialog": "lc_render_dialog",
|
|
100
98
|
"ui_render_panel": "lc_render_panel",
|
|
101
99
|
"ui_section": "lc_section",
|
|
102
|
-
"ui_segmented": "
|
|
103
|
-
"ui_segmented_tab": "
|
|
100
|
+
"ui_segmented": "lc_toggle_group",
|
|
101
|
+
"ui_segmented_tab": "lc_toggle_group_item",
|
|
104
102
|
"ui_select": "lc_select",
|
|
105
|
-
"ui_sheet": "
|
|
103
|
+
"ui_sheet": "lc_dialog",
|
|
106
104
|
"ui_skeleton": "lc_skeleton",
|
|
107
105
|
"ui_spinner": "lc_spinner",
|
|
108
106
|
"ui_stack": "lc_stack",
|
|
109
|
-
"ui_status_pill": "
|
|
107
|
+
"ui_status_pill": "lc_badge",
|
|
110
108
|
"ui_text": "lc_text",
|
|
111
109
|
"ui_textarea": "lc_textarea",
|
|
112
110
|
"ui_toast": "lc_toast"
|
data/config/importmap.rb
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Everything lives under lostcoords/ui/*. Controllers are also pinned under the host's controllers/* key,
|
|
2
|
+
# so eagerLoadControllersFrom("controllers", application) registers them as lc-* identifiers. A host file
|
|
3
|
+
# with the same name, such as app/javascript/controllers/lc_dialog_controller.js, replaces the gem's.
|
|
4
|
+
pin "lostcoords/ui", to: "lostcoords/ui/index.js", preload: false
|
|
2
5
|
pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/controllers"), under: "controllers", to: "lostcoords/ui/controllers", preload: false
|
|
3
|
-
pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/helpers"), under: "helpers", to: "lostcoords/ui/helpers", preload: false
|
|
6
|
+
pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/helpers"), under: "lostcoords/ui/helpers", to: "lostcoords/ui/helpers", preload: false
|
|
7
|
+
# pin_all_from keeps one mapping per directory, so the namespaced controller keys are pinned one by one.
|
|
8
|
+
Dir[Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/controllers/*.js")].each do |path|
|
|
9
|
+
name = File.basename(path, ".js")
|
|
10
|
+
pin "lostcoords/ui/controllers/#{name}", to: "lostcoords/ui/controllers/#{name}.js", preload: false
|
|
11
|
+
end
|
data/config/locales/en.yml
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
en:
|
|
2
2
|
lostcoords:
|
|
3
3
|
ui:
|
|
4
|
+
pager:
|
|
5
|
+
label: "Pages"
|
|
6
|
+
previous: "Previous"
|
|
7
|
+
next: "Next"
|
|
8
|
+
skip_link:
|
|
9
|
+
label: "Skip to content"
|
|
10
|
+
spinner:
|
|
11
|
+
label: "Loading…"
|
|
12
|
+
dialog:
|
|
13
|
+
trigger: "Open dialog"
|
|
14
|
+
cancel: "Cancel"
|
|
15
|
+
close: "Close %{title}"
|
|
16
|
+
menu:
|
|
17
|
+
label: "Actions"
|
|
18
|
+
popover:
|
|
19
|
+
trigger: "Open popover"
|
|
20
|
+
breadcrumb:
|
|
21
|
+
label: "Breadcrumb"
|
|
22
|
+
code_block:
|
|
23
|
+
label: "Source code"
|
|
24
|
+
copy: "Copy %{label}"
|
|
25
|
+
expand: "Show all code"
|
|
26
|
+
collapse: "Show less code"
|
|
4
27
|
copy_button:
|
|
5
28
|
label: "Copy"
|
|
6
29
|
copied: "Copied"
|
data/config/locales/es.yml
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
es:
|
|
2
2
|
lostcoords:
|
|
3
3
|
ui:
|
|
4
|
+
pager:
|
|
5
|
+
label: "Páginas"
|
|
6
|
+
previous: "Anterior"
|
|
7
|
+
next: "Siguiente"
|
|
8
|
+
skip_link:
|
|
9
|
+
label: "Saltar al contenido"
|
|
10
|
+
spinner:
|
|
11
|
+
label: "Cargando…"
|
|
12
|
+
dialog:
|
|
13
|
+
trigger: "Abrir diálogo"
|
|
14
|
+
cancel: "Cancelar"
|
|
15
|
+
close: "Cerrar %{title}"
|
|
16
|
+
menu:
|
|
17
|
+
label: "Acciones"
|
|
18
|
+
popover:
|
|
19
|
+
trigger: "Abrir panel"
|
|
20
|
+
breadcrumb:
|
|
21
|
+
label: "Ruta de navegación"
|
|
22
|
+
code_block:
|
|
23
|
+
label: "Código fuente"
|
|
24
|
+
copy: "Copiar %{label}"
|
|
25
|
+
expand: "Mostrar todo el código"
|
|
26
|
+
collapse: "Mostrar menos código"
|
|
4
27
|
copy_button:
|
|
5
28
|
label: "Copiar"
|
|
6
29
|
copied: "Copiado"
|
data/config/locales/ja.yml
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
ja:
|
|
2
2
|
lostcoords:
|
|
3
3
|
ui:
|
|
4
|
+
pager:
|
|
5
|
+
label: "ページ"
|
|
6
|
+
previous: "前へ"
|
|
7
|
+
next: "次へ"
|
|
8
|
+
skip_link:
|
|
9
|
+
label: "本文へ移動"
|
|
10
|
+
spinner:
|
|
11
|
+
label: "読み込み中…"
|
|
12
|
+
dialog:
|
|
13
|
+
trigger: "ダイアログを開く"
|
|
14
|
+
cancel: "キャンセル"
|
|
15
|
+
close: "%{title}を閉じる"
|
|
16
|
+
menu:
|
|
17
|
+
label: "操作"
|
|
18
|
+
popover:
|
|
19
|
+
trigger: "パネルを開く"
|
|
20
|
+
breadcrumb:
|
|
21
|
+
label: "パンくずリスト"
|
|
22
|
+
code_block:
|
|
23
|
+
label: "ソースコード"
|
|
24
|
+
copy: "%{label}をコピー"
|
|
25
|
+
expand: "コードをすべて表示"
|
|
26
|
+
collapse: "コードを折りたたむ"
|
|
4
27
|
copy_button:
|
|
5
28
|
label: "コピー"
|
|
6
29
|
copied: "コピー済み"
|
|
@@ -3,8 +3,10 @@ require "action_view"
|
|
|
3
3
|
require "active_support/core_ext/hash"
|
|
4
4
|
require "active_support/core_ext/object/blank"
|
|
5
5
|
require "tailwind_merge"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
require_relative "resources"
|
|
7
|
+
require_relative "icons"
|
|
8
|
+
require_relative "parts"
|
|
9
|
+
require_relative "form_builder"
|
|
10
|
+
require_relative "helpers"
|
|
9
11
|
|
|
10
12
|
Lostcoords::Ui.load_translations!
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
# Rails form builder that renders the library's controls and connects model errors to them.
|
|
3
|
+
#
|
|
4
|
+
# <%= lc_form_with model: @project do |form| %>
|
|
5
|
+
# <%= form.field :name, hint: "Shown to members" %>
|
|
6
|
+
# <%= form.field :email, as: :email_field %>
|
|
7
|
+
# <%= form.switch :public %>
|
|
8
|
+
# <%= form.submit %>
|
|
9
|
+
# <% end %>
|
|
10
|
+
#
|
|
11
|
+
# Every standard helper keeps its Rails signature. A symbol `size:` (:sm, :md, :lg) picks the control
|
|
12
|
+
# size; an integer still sets the HTML size attribute.
|
|
13
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
14
|
+
INPUTS = %i[
|
|
15
|
+
text_field email_field password_field telephone_field phone_field url_field search_field number_field
|
|
16
|
+
date_field time_field datetime_field datetime_local_field month_field week_field
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
INPUTS.each do |selector|
|
|
20
|
+
define_method(selector) do |method, options = {}|
|
|
21
|
+
size = control_size!(options)
|
|
22
|
+
super(method, control_options(method, options, "input", @template.lc_input_class(size: size)))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def text_area(method, options = {})
|
|
27
|
+
super(method, control_options(method, options, "textarea", @template.lc_textarea_class))
|
|
28
|
+
end
|
|
29
|
+
alias textarea text_area
|
|
30
|
+
|
|
31
|
+
def select(method, choices = nil, options = {}, html_options = {}, &block)
|
|
32
|
+
size = control_size!(html_options)
|
|
33
|
+
super(method, choices, options, control_options(method, html_options, "select", @template.lc_select_class(size: size)), &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
|
|
37
|
+
super(method, control_options(method, options, "checkbox-input", @template.lc_checkbox_class), checked_value, unchecked_value)
|
|
38
|
+
end
|
|
39
|
+
alias checkbox check_box
|
|
40
|
+
|
|
41
|
+
def radio_button(method, tag_value, options = {})
|
|
42
|
+
super(method, tag_value, control_options(method, options, "radio-input", @template.lc_radio_class))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A checkbox drawn as a switch, with its label beside it.
|
|
46
|
+
def switch(method, label: nil, hint: nil, checked_value: "1", unchecked_value: "0", **options)
|
|
47
|
+
input = check_box(method, { role: "switch", class: @template.lc_switch_class(extra: options.delete(:class)), data: { slot: "switch-input" } }.merge(options), checked_value, unchecked_value)
|
|
48
|
+
choice_label(method, label, hint, "switch", input)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A checkbox with its label beside it.
|
|
52
|
+
def choice(method, label: nil, hint: nil, checked_value: "1", unchecked_value: "0", **options)
|
|
53
|
+
choice_label(method, label, hint, "checkbox", check_box(method, options, checked_value, unchecked_value))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def label(method, text = nil, options = {}, &block)
|
|
57
|
+
if text.is_a?(Hash)
|
|
58
|
+
options = text
|
|
59
|
+
text = nil
|
|
60
|
+
end
|
|
61
|
+
super(method, text, options.merge(class: @template.lc_field_label_class(extra: options[:class])), &block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def submit(value = nil, options = {})
|
|
65
|
+
if value.is_a?(Hash)
|
|
66
|
+
options = value
|
|
67
|
+
value = nil
|
|
68
|
+
end
|
|
69
|
+
variant = options.delete(:variant) || :primary
|
|
70
|
+
size = options.delete(:size) || :md
|
|
71
|
+
super(value, options.merge(class: @template.lc_button_class(variant: variant, size: size, extra: options[:class])))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Label, control, hint and the attribute's errors in one field. `as:` names the control helper;
|
|
75
|
+
# a block renders a custom control and receives its attributes to splat with `**control`.
|
|
76
|
+
def field(method, label: nil, hint: nil, as: :text_field, **options, &block)
|
|
77
|
+
@template.lc_field(label: label || human_name(method), id: field_id(method), hint: hint, error: errors_for(method)) do |control|
|
|
78
|
+
if block
|
|
79
|
+
@template.capture(control, &block)
|
|
80
|
+
else
|
|
81
|
+
public_send(as, method, **options, **control.to_hash.except(:id))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
def control_size!(options)
|
|
88
|
+
options[:size].is_a?(Symbol) || options[:size].is_a?(String) && options[:size].match?(/\A[a-z]+\z/) ? options.delete(:size).to_sym : :md
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def control_options(method, options, slot, classes)
|
|
92
|
+
options = options.dup
|
|
93
|
+
aria = (options.delete(:aria) || {}).dup
|
|
94
|
+
aria[:invalid] = true if errors_for(method).any? && !aria.key?(:invalid)
|
|
95
|
+
data = { slot: slot }.merge(options.delete(:data) || {})
|
|
96
|
+
options.merge(class: @template.lc_tw(classes, options[:class]), aria: aria, data: data)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def choice_label(method, text, hint, slot, input)
|
|
100
|
+
@template.send(:lc_choice_label, slot, text || human_name(method), hint, input, for: field_id(method))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def errors_for(method)
|
|
104
|
+
return [] unless object.respond_to?(:errors)
|
|
105
|
+
|
|
106
|
+
object.errors.full_messages_for(method)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def human_name(method)
|
|
110
|
+
object.class.respond_to?(:human_attribute_name) ? object.class.human_attribute_name(method) : method.to_s.humanize
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
# Builders yielded by compound components, so their parts are scoped and discoverable:
|
|
3
|
+
#
|
|
4
|
+
# <%= lc_card do |card| %>
|
|
5
|
+
# <%= card.header title: "Members", description: "People in this workspace" %>
|
|
6
|
+
# <%= card.content do %>…<% end %>
|
|
7
|
+
# <%= card.footer do %>…<% end %>
|
|
8
|
+
# <% end %>
|
|
9
|
+
#
|
|
10
|
+
# Every part carries data-slot, so a context can style it without matching classes.
|
|
11
|
+
module Parts
|
|
12
|
+
class Base
|
|
13
|
+
def initialize(view)
|
|
14
|
+
@view = view
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
attr_reader :view
|
|
19
|
+
|
|
20
|
+
def merge(defaults, attributes)
|
|
21
|
+
view.send(:lc_attributes, defaults, attributes)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def slot(name, attributes, defaults = {})
|
|
25
|
+
merge(defaults.merge(data: { slot: name }), attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def part(tag_name, name, classes, attributes, &block)
|
|
29
|
+
view.content_tag(tag_name, slot(name, attributes, class: classes), &block)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Card < Base
|
|
34
|
+
def header(title: nil, description: nil, level: 3, **attributes, &actions)
|
|
35
|
+
heading = [ (self.title(title, level: level) if title), (self.description(description) if description) ].compact
|
|
36
|
+
action = view.capture(&actions) if actions
|
|
37
|
+
part(:header, "card-header", "flex min-w-0 items-start justify-between gap-content", attributes) do
|
|
38
|
+
view.safe_join([
|
|
39
|
+
(view.tag.div(view.safe_join(heading), class: "flex min-w-0 flex-col gap-tight", data: { slot: "card-heading" }) if heading.any?),
|
|
40
|
+
(view.tag.div(action, class: "flex shrink-0 items-center gap-related", data: { slot: "card-action" }) if action.present?)
|
|
41
|
+
].compact)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def title(content = nil, level: 3, **attributes, &block)
|
|
46
|
+
view.lc_heading(content, level: level, size: :subheading, **slot("card-title", attributes), &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def description(content = nil, **attributes, &block)
|
|
50
|
+
view.lc_text(content, size: :small, tone: :muted, **slot("card-description", attributes), &block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def content(**attributes, &block)
|
|
54
|
+
part(:div, "card-content", "flex min-w-0 flex-col gap-content", attributes, &block)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def footer(**attributes, &block)
|
|
58
|
+
part(:div, "card-footer", "flex min-w-0 flex-wrap items-center justify-end gap-related", attributes, &block)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Splats into a control as `**field`, so the control gets the field's id and validation attributes.
|
|
63
|
+
class Field < Base
|
|
64
|
+
attr_reader :id
|
|
65
|
+
|
|
66
|
+
def initialize(view, id, control)
|
|
67
|
+
super(view)
|
|
68
|
+
@id = id
|
|
69
|
+
@control = control
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_hash = @control
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Dialog < Base
|
|
76
|
+
def initialize(view, id)
|
|
77
|
+
super(view)
|
|
78
|
+
@id = id
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def footer(**attributes, &block)
|
|
82
|
+
part(:div, "dialog-footer", "flex flex-wrap items-center justify-end gap-related", attributes, &block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def close(label = nil, variant: :secondary, **attributes)
|
|
86
|
+
label ||= view.send(:lc_t, "lostcoords.ui.dialog.cancel")
|
|
87
|
+
view.lc_button(label, variant: variant, **merge({ data: { action: "lc-dialog#close", slot: "dialog-close" } }, attributes))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Alert < Base
|
|
92
|
+
def actions(**attributes, &block)
|
|
93
|
+
part(:div, "alert-actions", "flex flex-wrap items-center gap-related pt-tight", attributes, &block)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class Tabs < Base
|
|
98
|
+
Panel = Data.define(:value, :label, :content, :attributes)
|
|
99
|
+
|
|
100
|
+
attr_reader :panels
|
|
101
|
+
|
|
102
|
+
def initialize(view)
|
|
103
|
+
super
|
|
104
|
+
@panels = []
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Records a panel; lc_tabs renders the tab list and panels after the block.
|
|
108
|
+
def panel(label, value: nil, **attributes, &block)
|
|
109
|
+
value = (value || label).to_s
|
|
110
|
+
raise ArgumentError, "tab label must not be blank" if label.blank?
|
|
111
|
+
raise ArgumentError, "duplicate tab value: #{value}" if @panels.any? { |panel| panel.value == value }
|
|
112
|
+
|
|
113
|
+
@panels << Panel.new(value, label, (view.capture(&block) if block), attributes)
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class NavList < Base
|
|
119
|
+
def initialize(view, compact)
|
|
120
|
+
super(view)
|
|
121
|
+
@compact = compact
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# current: marks the page the reader is on with aria-current="page".
|
|
125
|
+
def link(label, url, current: false, indent: false, **attributes)
|
|
126
|
+
view.tag.li(class: ("ps-content" if indent), data: { slot: "nav-item" }) do
|
|
127
|
+
view.link_to(label, url, **merge({ class: view.lc_nav_link_class(compact: @compact), aria: { current: ("page" if current) }, data: { slot: "nav-link" } }, attributes))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The list is named with aria-label rather than an id, so the same navigation can render twice,
|
|
132
|
+
# as in a sidebar and a phone menu.
|
|
133
|
+
def group(title, **attributes, &block)
|
|
134
|
+
part(:li, "nav-group", "flex flex-col gap-tight not-first:pt-section", attributes) do
|
|
135
|
+
view.safe_join([
|
|
136
|
+
view.tag.span(title, class: "px-3 pb-tight text-caption font-semibold text-foreground", aria: { hidden: true }, data: { slot: "nav-group-title" }),
|
|
137
|
+
view.tag.ul(view.capture(&block), class: "m-0 flex list-none flex-col gap-px p-0", aria: { label: title })
|
|
138
|
+
])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class DescriptionList < Base
|
|
144
|
+
def item(term, detail = nil, **attributes, &block)
|
|
145
|
+
part(:div, "description-item", "grid min-w-0 gap-tight py-heading sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] sm:gap-content", attributes) do
|
|
146
|
+
view.safe_join([
|
|
147
|
+
view.tag.dt(term, class: "text-label text-muted-foreground", data: { slot: "description-term" }),
|
|
148
|
+
view.tag.dd(block ? view.capture(&block) : detail, class: "m-0 min-w-0 text-body text-foreground [overflow-wrap:anywhere]", data: { slot: "description-detail" })
|
|
149
|
+
])
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/lostcoords/ui/tokens.rb
CHANGED
|
@@ -4,9 +4,32 @@ module Lostcoords::Ui
|
|
|
4
4
|
module Tokens
|
|
5
5
|
THEME_PATH = File.expand_path("../../../app/assets/tailwind/lostcoords/ui/theme.css", __dir__)
|
|
6
6
|
DECLARATION = /(?<![\w-])(--[\w-]+)\s*:/
|
|
7
|
+
COLORS = {
|
|
8
|
+
"--background" => "Page background",
|
|
9
|
+
"--foreground" => "Main text and headings",
|
|
10
|
+
"--card" => "Cards and grouped content",
|
|
11
|
+
"--card-foreground" => "Text on cards",
|
|
12
|
+
"--popover" => "Menus, popovers and dialogs",
|
|
13
|
+
"--popover-foreground" => "Text on menus, popovers and dialogs",
|
|
14
|
+
"--primary" => "Primary actions, links, selected states and keyboard focus",
|
|
15
|
+
"--primary-foreground" => "Text and icons on a solid primary background",
|
|
16
|
+
"--secondary" => "Secondary buttons",
|
|
17
|
+
"--secondary-foreground" => "Text on secondary buttons",
|
|
18
|
+
"--muted" => "Subtle backgrounds: badges, code, skeletons, table headers",
|
|
19
|
+
"--muted-foreground" => "Secondary text, hints and placeholders",
|
|
20
|
+
"--accent" => "Hovered rows, menu items and inline code",
|
|
21
|
+
"--accent-foreground" => "Text on accent backgrounds",
|
|
22
|
+
"--destructive" => "Errors and destructive actions",
|
|
23
|
+
"--success" => "Successful actions and positive feedback",
|
|
24
|
+
"--warning" => "Warnings and items needing attention",
|
|
25
|
+
"--border" => "Dividers and outlines",
|
|
26
|
+
"--input" => "Input and toggle fills",
|
|
27
|
+
"--ring" => "Keyboard focus ring; unset, it follows --primary"
|
|
28
|
+
}.freeze
|
|
29
|
+
FONTS = %w[--font-sans --font-display --font-mono --font-japanese].freeze
|
|
7
30
|
|
|
8
31
|
def self.names
|
|
9
|
-
|
|
32
|
+
(COLORS.keys + FONTS).sort
|
|
10
33
|
end
|
|
11
34
|
end
|
|
12
35
|
|