kiso 0.5.2.pre → 0.6.0.pre

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/kiso/button.css +12 -3
  3. data/app/assets/tailwind/kiso/checkbox.css +13 -2
  4. data/app/assets/tailwind/kiso/color-mode.css +15 -3
  5. data/app/assets/tailwind/kiso/dashboard.css +97 -44
  6. data/app/assets/tailwind/kiso/dialog.css +39 -5
  7. data/app/assets/tailwind/kiso/engine.css +117 -34
  8. data/app/assets/tailwind/kiso/input-otp.css +24 -4
  9. data/app/assets/tailwind/kiso/palettes/blue.css +14 -5
  10. data/app/assets/tailwind/kiso/palettes/green.css +9 -5
  11. data/app/assets/tailwind/kiso/palettes/orange.css +9 -5
  12. data/app/assets/tailwind/kiso/palettes/violet.css +9 -5
  13. data/app/assets/tailwind/kiso/palettes/zinc.css +11 -7
  14. data/app/assets/tailwind/kiso/radio-group.css +11 -4
  15. data/app/assets/tailwind/kiso/slider.css +25 -6
  16. data/app/assets/tailwind/kiso/tooltip.css +37 -11
  17. data/app/helpers/kiso/app_component_helper.rb +83 -34
  18. data/app/helpers/kiso/component_helper.rb +227 -70
  19. data/app/helpers/kiso/icon_helper.rb +101 -39
  20. data/app/helpers/kiso/theme_helper.rb +50 -9
  21. data/app/helpers/kiso/ui_context_helper.rb +87 -35
  22. data/app/javascript/controllers/kiso/combobox_controller.js +10 -2
  23. data/app/javascript/controllers/kiso/command_controller.js +2 -0
  24. data/app/javascript/controllers/kiso/command_dialog_controller.js +4 -0
  25. data/app/javascript/controllers/kiso/dialog_controller.js +6 -1
  26. data/app/javascript/controllers/kiso/dialog_trigger_controller.js +1 -1
  27. data/app/javascript/controllers/kiso/dropdown_menu_controller.js +23 -5
  28. data/app/javascript/controllers/kiso/index.js +25 -0
  29. data/app/javascript/controllers/kiso/input_otp_controller.js +5 -3
  30. data/app/javascript/controllers/kiso/popover_controller.js +18 -4
  31. data/app/javascript/controllers/kiso/select_controller.js +10 -2
  32. data/app/javascript/controllers/kiso/sidebar_controller.js +26 -4
  33. data/app/javascript/controllers/kiso/slider_controller.js +3 -3
  34. data/app/javascript/controllers/kiso/theme_controller.js +2 -1
  35. data/app/javascript/controllers/kiso/toggle_controller.js +2 -0
  36. data/app/javascript/controllers/kiso/toggle_group_controller.js +3 -0
  37. data/app/javascript/controllers/kiso/tooltip_controller.js +3 -0
  38. data/app/javascript/kiso/utils/focusable.js +14 -0
  39. data/app/javascript/kiso/utils/highlight.js +15 -1
  40. data/app/views/kiso/components/_alert.html.erb +2 -0
  41. data/app/views/kiso/components/_alert_dialog.html.erb +5 -2
  42. data/app/views/kiso/components/_app.html.erb +2 -0
  43. data/app/views/kiso/components/_aspect_ratio.html.erb +1 -0
  44. data/app/views/kiso/components/_avatar.html.erb +6 -2
  45. data/app/views/kiso/components/_button.html.erb +3 -0
  46. data/app/views/kiso/components/_checkbox.html.erb +1 -0
  47. data/app/views/kiso/components/_color_mode_button.html.erb +2 -0
  48. data/app/views/kiso/components/_color_mode_select.html.erb +2 -0
  49. data/app/views/kiso/components/_combobox.html.erb +3 -0
  50. data/app/views/kiso/components/_command.html.erb +2 -0
  51. data/app/views/kiso/components/_dashboard_group.html.erb +4 -0
  52. data/app/views/kiso/components/_dashboard_navbar.html.erb +2 -0
  53. data/app/views/kiso/components/_dashboard_panel.html.erb +1 -0
  54. data/app/views/kiso/components/_dashboard_sidebar.html.erb +2 -0
  55. data/app/views/kiso/components/_dashboard_toolbar.html.erb +2 -0
  56. data/app/views/kiso/components/_dialog.html.erb +3 -0
  57. data/app/views/kiso/components/_dropdown_menu.html.erb +2 -0
  58. data/app/views/kiso/components/_empty.html.erb +2 -0
  59. data/app/views/kiso/components/_field.html.erb +2 -0
  60. data/app/views/kiso/components/_field_group.html.erb +1 -0
  61. data/app/views/kiso/components/_field_set.html.erb +1 -0
  62. data/app/views/kiso/components/_input_group.html.erb +1 -0
  63. data/app/views/kiso/components/_input_otp.html.erb +3 -0
  64. data/app/views/kiso/components/_nav.html.erb +2 -0
  65. data/app/views/kiso/components/_page_card.html.erb +3 -0
  66. data/app/views/kiso/components/_page_header.html.erb +3 -0
  67. data/app/views/kiso/components/_page_section.html.erb +2 -0
  68. data/app/views/kiso/components/_pagination.html.erb +2 -0
  69. data/app/views/kiso/components/_popover.html.erb +3 -0
  70. data/app/views/kiso/components/_select.html.erb +3 -0
  71. data/app/views/kiso/components/_select_native.html.erb +2 -0
  72. data/app/views/kiso/components/_separator.html.erb +2 -0
  73. data/app/views/kiso/components/_skeleton.html.erb +1 -0
  74. data/app/views/kiso/components/_slider.html.erb +4 -0
  75. data/app/views/kiso/components/_spinner.html.erb +2 -0
  76. data/app/views/kiso/components/_stats_card.html.erb +2 -0
  77. data/app/views/kiso/components/_stats_grid.html.erb +1 -0
  78. data/app/views/kiso/components/_switch.html.erb +2 -0
  79. data/app/views/kiso/components/_table.html.erb +2 -0
  80. data/app/views/kiso/components/_textarea.html.erb +3 -0
  81. data/app/views/kiso/components/_toggle.html.erb +2 -0
  82. data/app/views/kiso/components/_toggle_group.html.erb +2 -0
  83. data/app/views/kiso/components/_tooltip.html.erb +3 -0
  84. data/app/views/kiso/components/alert_dialog/_action.html.erb +1 -0
  85. data/app/views/kiso/components/alert_dialog/_cancel.html.erb +1 -0
  86. data/app/views/kiso/components/alert_dialog/_description.html.erb +1 -0
  87. data/app/views/kiso/components/alert_dialog/_title.html.erb +1 -0
  88. data/app/views/kiso/components/avatar/_image.html.erb +1 -0
  89. data/app/views/kiso/components/breadcrumb/_separator.html.erb +3 -0
  90. data/app/views/kiso/components/combobox/_chips.html.erb +3 -0
  91. data/app/views/kiso/components/command/_dialog.html.erb +2 -0
  92. data/app/views/kiso/components/dashboard_sidebar/_collapse.html.erb +2 -0
  93. data/app/views/kiso/components/dialog/_close.html.erb +1 -0
  94. data/app/views/kiso/components/field/_error.html.erb +4 -0
  95. data/app/views/kiso/components/field/_label.html.erb +2 -0
  96. data/app/views/kiso/components/field/_separator.html.erb +3 -0
  97. data/app/views/kiso/components/input_otp/_separator.html.erb +2 -0
  98. data/app/views/kiso/components/input_otp/_slot.html.erb +2 -0
  99. data/app/views/kiso/components/nav/_section.html.erb +4 -0
  100. data/app/views/kiso/components/tooltip/_content.html.erb +2 -0
  101. data/lib/generators/kiso/install/USAGE +23 -0
  102. data/lib/generators/kiso/install/install_generator.rb +91 -0
  103. data/lib/generators/kiso/install/templates/design_system.md.tt +190 -0
  104. data/lib/generators/kiso/install/templates/initializer.rb.tt +40 -0
  105. data/lib/kiso/cli/make.rb +6 -3
  106. data/lib/kiso/cli.rb +10 -0
  107. data/lib/kiso/color_utils.rb +31 -8
  108. data/lib/kiso/configuration.rb +11 -0
  109. data/lib/kiso/engine.rb +9 -2
  110. data/lib/kiso/propshaft_tailwind_stub_filter.rb +9 -2
  111. data/lib/kiso/themes/avatar.rb +40 -6
  112. data/lib/kiso/themes/badge.rb +5 -1
  113. data/lib/kiso/themes/color_mode_button.rb +11 -0
  114. data/lib/kiso/themes/color_mode_select.rb +7 -0
  115. data/lib/kiso/themes/dashboard.rb +28 -0
  116. data/lib/kiso/themes/dropdown_menu.rb +2 -2
  117. data/lib/kiso/themes/input_otp.rb +6 -3
  118. data/lib/kiso/themes/nav.rb +17 -0
  119. data/lib/kiso/themes/pagination.rb +9 -4
  120. data/lib/kiso/themes/shared.rb +27 -7
  121. data/lib/kiso/version.rb +5 -2
  122. metadata +5 -1
@@ -4,52 +4,91 @@ module Kiso
4
4
  # View helpers for rendering host app components.
5
5
  #
6
6
  # Mirrors {ComponentHelper#kui} but resolves partials from
7
- # +app/views/components/+ and themes from +AppThemes::+.
8
- # No global config layer — host apps own the source directly.
7
+ # +app/views/components/+ and themes from the +AppThemes::+ namespace
8
+ # (loaded from +app/themes/<theme_name>/+).
9
9
  #
10
- # Included in all views automatically by {Engine}.
10
+ # == Key difference from +kui()+
11
+ #
12
+ # +appui()+ does *not* merge global config overrides (Layer 2). Host apps
13
+ # own their component source directly, so there is no need for a
14
+ # boot-time override layer. The three layers for +appui()+ are:
15
+ #
16
+ # 1. **Theme default** -- the +ClassVariants+ definition in
17
+ # +app/themes/<theme_name>/+.
18
+ # 2. **Instance +ui:+** -- per-render slot overrides.
19
+ # 3. **Instance +css_classes:+** -- per-render root-element overrides.
20
+ #
21
+ # == Generating host app components
22
+ #
23
+ # Use the generator to scaffold theme and partial files:
24
+ #
25
+ # bin/rails generate kiso:component pricing_card --sub-parts header body footer
26
+ #
27
+ # This creates files in +app/themes/default/+ and +app/views/components/+.
28
+ #
29
+ # Included in all views automatically by {Kiso::Engine}.
30
+ #
31
+ # @see ComponentHelper#kui the equivalent helper for engine-shipped components
32
+ # @see ComponentHelper#kui_tag the themed +content_tag+ shorthand (aliased as {#appui_tag})
11
33
  module AppComponentHelper
12
34
  # Renders a host app component partial.
13
35
  #
14
36
  # Components live in +app/views/components/+. Sub-parts are nested
15
- # in a directory matching the parent component name.
37
+ # in a directory matching the parent component name (e.g.
38
+ # +components/pricing_card/_header.html.erb+).
16
39
  #
17
- # @param component [Symbol] the component name (e.g. +:pricing_card+)
18
- # @param part [Symbol, nil] optional sub-part name (e.g. +:header+, +:footer+)
19
- # @param collection [Array, nil] renders the partial once per item when present
20
- # @param ui [Hash{Symbol => String}, nil] per-slot class overrides keyed by sub-part name.
21
- # For parent components, the hash is pushed onto a context stack so sub-parts
22
- # inherit overrides automatically. For self-rendering components, the hash is
23
- # also passed as a local so the partial can apply overrides to internally
24
- # rendered elements.
25
- # @param scope [Hash, nil] domain locals shared from parent to sub-parts via context stack.
26
- # Sub-parts receive scope values as kwargs automatically. Explicit kwargs on sub-part
27
- # calls override scope values. One level deep only — no ancestor resolution.
28
- # @param kwargs [Hash] locals passed to the partial (e.g. +css_classes:+)
40
+ # Behaves identically to {ComponentHelper#kui} except:
41
+ # - Partials resolve from +app/views/components/+ instead of
42
+ # +app/views/kiso/components/+.
43
+ # - Global config +ui:+ overrides are *not* merged (host apps own the
44
+ # source and can edit themes directly).
45
+ #
46
+ # @param component [Symbol] the component name (e.g. +:pricing_card+).
47
+ # Must match a partial at +app/views/components/_<name>.html.erb+.
48
+ # @param part [Symbol, nil] optional sub-part name (e.g. +:header+,
49
+ # +:footer+). Resolves to
50
+ # +app/views/components/<component>/_<part>.html.erb+.
51
+ # @param collection [Array, nil] when present, renders the partial once
52
+ # per item using Rails collection rendering.
53
+ # @param ui [Hash{Symbol => String}, nil] per-slot class overrides keyed
54
+ # by sub-part name. Pushed onto the context stack for composed
55
+ # sub-parts, and also passed as a +ui:+ local to self-rendering
56
+ # partials.
57
+ # @param scope [Hash, nil] domain locals shared from parent to sub-parts
58
+ # via context stack. Sub-parts receive scope values as kwargs
59
+ # automatically. Explicit kwargs on sub-part calls override scope
60
+ # values. One level deep only -- no ancestor resolution.
61
+ # @param kwargs [Hash] locals forwarded to the partial (e.g.
62
+ # +css_classes:+, +plan:+, +featured:+). Must match the partial's
63
+ # strict locals declaration.
29
64
  # @yield optional block for component content
30
- # @return [ActiveSupport::SafeBuffer] rendered HTML
65
+ # @return [ActiveSupport::SafeBuffer] rendered HTML string
31
66
  #
32
67
  # @example Render a pricing card
33
- # appui(:pricing_card) { "Content" }
68
+ # <%= appui(:pricing_card, plan: @plan) { "Content" } %>
34
69
  #
35
- # @example Render a pricing card with sub-parts
36
- # appui(:pricing_card) do
37
- # appui(:pricing_card, :header) { "Header" }
38
- # end
70
+ # @example Render a pricing card with composed sub-parts
71
+ # <%= appui(:pricing_card) do %>
72
+ # <%= appui(:pricing_card, :header) { "Pro Plan" } %>
73
+ # <%= appui(:pricing_card, :body) do %>
74
+ # <p>Everything you need to get started.</p>
75
+ # <% end %>
76
+ # <%= appui(:pricing_card, :footer) { "Sign up" } %>
77
+ # <% end %>
39
78
  #
40
79
  # @example Render with per-slot overrides
41
- # appui(:pricing_card, ui: { header: "p-8" }) do
42
- # appui(:pricing_card, :header) { "Header" }
43
- # end
80
+ # <%= appui(:pricing_card, ui: { header: "p-8 bg-muted" }) do %>
81
+ # <%= appui(:pricing_card, :header) { "Enterprise" } %>
82
+ # <% end %>
44
83
  #
45
- # @example Share domain locals with sub-parts
46
- # appui(:room_card, scope: { room: room }) do
47
- # appui(:room_card, :status)
48
- # appui(:room_card, :meta)
49
- # end
84
+ # @example Share domain locals with sub-parts via scope
85
+ # <%= appui(:room_card, scope: { room: room }) do %>
86
+ # <%= appui(:room_card, :status) %>
87
+ # <%= appui(:room_card, :meta) %>
88
+ # <% end %>
50
89
  #
51
90
  # @example Render a collection
52
- # appui(:pricing_card, collection: @plans)
91
+ # <%= appui(:pricing_card, collection: @plans) %>
53
92
  def appui(component, part = nil, collection: nil, ui: nil, scope: nil, **kwargs, &block)
54
93
  kiso_render_component(
55
94
  component, part,
@@ -61,10 +100,20 @@ module Kiso
61
100
 
62
101
  # Renders a themed HTML element for host app components.
63
102
  #
64
- # Identical to {ComponentHelper#kui_tag} provided as a naming
65
- # convenience so host app partials use +appui_tag+ alongside +appui()+.
103
+ # This is an alias for {ComponentHelper#kui_tag}, provided as a naming
104
+ # convenience so host app partials use +appui_tag+ alongside +appui()+
105
+ # for visual consistency.
106
+ #
107
+ # @see ComponentHelper#kui_tag for full parameter documentation
66
108
  #
67
- # @see ComponentHelper#kui_tag
109
+ # @example In a host app component partial
110
+ # <%# app/views/components/_pricing_card.html.erb %>
111
+ # <%= appui_tag :div, theme: AppThemes::Default::PricingCard,
112
+ # slot: "pricing-card", css_classes: css_classes,
113
+ # variants: { featured: featured },
114
+ # **component_options do %>
115
+ # <%= yield %>
116
+ # <% end %>
68
117
  def appui_tag(...)
69
118
  kui_tag(...)
70
119
  end
@@ -1,48 +1,111 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kiso
2
- # View helpers for rendering Kiso components.
4
+ # View helpers for rendering Kiso UI components in ERB templates.
5
+ #
6
+ # This module is the primary public API for the Kiso component library.
7
+ # It provides {#kui} for rendering engine-shipped components,
8
+ # {#kiso_prepare_options} for building +data-slot+ attributes in partials,
9
+ # and {#kui_tag} for collapsing the common +content_tag+ boilerplate.
10
+ #
11
+ # Included in all views automatically by {Kiso::Engine}.
12
+ #
13
+ # == Override layers
3
14
  #
4
- # Included in all views automatically by {Engine}.
15
+ # Kiso resolves Tailwind classes through four layers (lowest to highest
16
+ # priority):
17
+ #
18
+ # 1. **Theme default** -- the +ClassVariants+ definition in
19
+ # +lib/kiso/themes/+.
20
+ # 2. **Global config** -- +Kiso.configure { |c| c.theme[:button] = ... }+,
21
+ # applied once at boot via +ClassVariants::Instance#merge+.
22
+ # 3. **Instance +ui:+** -- per-render slot overrides passed to +kui()+.
23
+ # 4. **Instance +css_classes:+** -- per-render root-element overrides,
24
+ # merged via +tailwind_merge+ at render time.
25
+ #
26
+ # @see AppComponentHelper#appui the equivalent helper for host app components
27
+ # @see UiContextHelper the request-scoped context stack that powers +ui:+
28
+ # @see Kiso::Configuration global theme and icon configuration
5
29
  module ComponentHelper
6
30
  # Renders a Kiso component partial.
7
31
  #
32
+ # This is the main entry point for rendering any Kiso UI component.
8
33
  # Components live in +app/views/kiso/components/+. Sub-parts are nested
9
- # in a directory matching the parent component name.
10
- #
11
- # @param component [Symbol] the component name (e.g. +:badge+, +:card+)
12
- # @param part [Symbol, nil] optional sub-part name (e.g. +:header+, +:footer+)
13
- # @param collection [Array, nil] renders the partial once per item when present
14
- # @param ui [Hash{Symbol => String}, nil] per-slot class overrides keyed by sub-part name.
15
- # For parent components, the hash is pushed onto a context stack so sub-parts
16
- # inherit overrides automatically. For self-rendering components, the hash is
17
- # also passed as a local so the partial can apply overrides to internally
18
- # rendered elements.
19
- # @param scope [Hash, nil] domain locals shared from parent to sub-parts via context stack.
20
- # Sub-parts receive scope values as kwargs automatically. Explicit kwargs on sub-part
21
- # calls override scope values. One level deep only — no ancestor resolution.
22
- # @param kwargs [Hash] locals passed to the partial (e.g. +color:+, +variant:+, +css_classes:+)
23
- # @yield optional block for component content
24
- # @return [ActiveSupport::SafeBuffer] rendered HTML
34
+ # in a directory matching the parent component name (e.g.
35
+ # +card/_header.html.erb+).
25
36
  #
26
- # @example Render a badge
27
- # kui(:badge, color: :success, variant: :soft) { "Active" }
37
+ # Internally wraps Rails +render+ with Kiso-specific behavior:
38
+ # - Injects an empty +proc+ when no block is given, preventing +yield+
39
+ # from bubbling up the ERB rendering chain (see implementation notes)
40
+ # - Pushes +ui:+ and +scope:+ onto request-scoped context stacks so
41
+ # sub-parts inherit them automatically
42
+ # - Merges global config +ui:+ overrides (Layer 2) with instance +ui:+
43
+ # (Layer 3)
44
+ #
45
+ # @param component [Symbol] the component name (e.g. +:badge+, +:card+,
46
+ # +:button+). Must match a partial at
47
+ # +app/views/kiso/components/_<name>.html.erb+.
48
+ # @param part [Symbol, nil] optional sub-part name (e.g. +:header+,
49
+ # +:footer+, +:title+). Resolves to
50
+ # +app/views/kiso/components/<component>/_<part>.html.erb+.
51
+ # @param collection [Array, nil] when present, renders the partial once
52
+ # per item using Rails collection rendering.
53
+ # @param ui [Hash{Symbol => String}, nil] per-slot class overrides keyed
54
+ # by sub-part name. For parent components, the hash is pushed onto a
55
+ # context stack so composed sub-parts inherit overrides automatically.
56
+ # For self-rendering components (Alert, Dialog, Switch, etc.), the
57
+ # hash is also passed as a +ui:+ local so the partial can apply
58
+ # overrides to internally rendered elements via
59
+ # +Kiso::Themes::SubPart.render(class: ui[:slot_name])+.
60
+ # @param scope [Hash, nil] domain locals shared from parent to sub-parts
61
+ # via context stack. Sub-parts receive scope values as kwargs
62
+ # automatically. Explicit kwargs on sub-part calls override scope
63
+ # values. One level deep only -- no ancestor resolution.
64
+ # @param kwargs [Hash] locals forwarded to the partial (e.g. +color:+,
65
+ # +variant:+, +size:+, +css_classes:+, +icon:+). These must match the
66
+ # partial's +strict locals+ declaration.
67
+ # @yield optional block for component content. When omitted, an empty
68
+ # proc is injected so partials can safely use
69
+ # +capture { yield }.presence+ for optional block overrides.
70
+ # @return [ActiveSupport::SafeBuffer] rendered HTML string
71
+ #
72
+ # @example Render a simple badge
73
+ # <%= kui(:badge, color: :success, variant: :soft) { "Active" } %>
74
+ #
75
+ # @example Render a card with composed sub-parts
76
+ # <%= kui(:card) do %>
77
+ # <%= kui(:card, :header) do %>
78
+ # <%= kui(:card, :title) { "Dashboard" } %>
79
+ # <%= kui(:card, :description) { "Overview of your projects" } %>
80
+ # <% end %>
81
+ # <%= kui(:card, :content) do %>
82
+ # <p>Card body content here.</p>
83
+ # <% end %>
84
+ # <% end %>
28
85
  #
29
86
  # @example Render a card with per-slot overrides
30
- # kui(:card, ui: { header: "p-8", title: "text-xl" }) do
31
- # kui(:card, :header) do
32
- # kui(:card, :title) { "Dashboard" }
33
- # end
34
- # end
87
+ # <%= kui(:card, ui: { header: "p-8", title: "text-xl" }) do %>
88
+ # <%= kui(:card, :header) do %>
89
+ # <%= kui(:card, :title) { "Dashboard" } %>
90
+ # <% end %>
91
+ # <% end %>
35
92
  #
36
93
  # @example Render an alert with inner element overrides
37
- # kui(:alert, icon: "info", ui: { close: "opacity-50" })
94
+ # <%= kui(:alert, icon: "info", ui: { close: "opacity-50" }) %>
95
+ #
96
+ # @example Override root element classes
97
+ # <%= kui(:badge, css_classes: "uppercase tracking-wide") { "New" } %>
38
98
  #
39
- # @example Share domain locals with sub-parts
40
- # kui(:card, scope: { project: @project }) do
41
- # kui(:card, :header) # sub-part receives project: automatically
42
- # end
99
+ # @example Pass HTML attributes through to the root element
100
+ # <%= kui(:button, id: "submit-btn", data: { turbo: false }) { "Submit" } %>
101
+ #
102
+ # @example Share domain locals with sub-parts via scope
103
+ # <%= kui(:card, scope: { project: @project }) do %>
104
+ # <%= kui(:card, :header) %> <%# receives project: automatically %>
105
+ # <% end %>
43
106
  #
44
107
  # @example Render a collection
45
- # kui(:badge, collection: @tags)
108
+ # <%= kui(:badge, collection: @tags) %>
46
109
  def kui(component, part = nil, collection: nil, ui: nil, scope: nil, **kwargs, &block)
47
110
  kiso_render_component(
48
111
  component, part,
@@ -52,55 +115,125 @@ module Kiso
52
115
  )
53
116
  end
54
117
 
55
- # Prepares +component_options+ for use with +content_tag+.
118
+ # Prepares +component_options+ for use with +content_tag+ in component
119
+ # partials.
56
120
  #
57
- # Sets +data-slot+ for component identity (shadcn v4 convention) and
58
- # merges any additional data attributes. Raises if +class:+ is passed
59
- # (use +css_classes:+ instead).
121
+ # Extracts the caller's +data:+ hash from +component_options+, merges it
122
+ # with the mandatory +data-slot+ attribute (shadcn v4 convention) and any
123
+ # additional data attributes (typically Stimulus bindings). Returns the
124
+ # merged data hash ready for +content_tag+.
60
125
  #
61
- # @param component_options [Hash] the +**component_options+ splat from the partial.
62
- # Any +data:+ key is extracted and merged with +slot+ and +data_attrs+.
63
- # @param slot [String] the +data-slot+ value (kebab-case, e.g. +"card-header"+)
64
- # @param data_attrs [Hash] additional data attributes (e.g. +controller: "kiso--toggle"+)
65
- # @return [Hash] merged data attributes hash for +content_tag+
66
- # @raise [ArgumentError] if +component_options+ contains a +class:+ key
126
+ # This method mutates +component_options+ by deleting the +data:+ key so
127
+ # it is not double-passed when the partial splats +**component_options+
128
+ # onto +content_tag+.
67
129
  #
68
- # @example In a component partial
69
- # data: kiso_prepare_options(component_options, slot: "badge")
130
+ # @param component_options [Hash] the +**component_options+ splat from
131
+ # the partial's strict locals. Callers pass arbitrary HTML attributes
132
+ # here (e.g. +id:+, +aria:+, +data:+). The +data:+ key is extracted
133
+ # and merged; all other keys pass through to +content_tag+.
134
+ # @param slot [String] the +data-slot+ value in kebab-case
135
+ # (e.g. +"badge"+, +"card-header"+, +"alert-title"+). Every component
136
+ # and sub-part must have a unique slot name.
137
+ # @param data_attrs [Hash] additional data attributes merged into the
138
+ # result (e.g. +controller: "kiso--toggle"+, +action: "click->..."+).
139
+ # For +action:+ and +controller:+, user and component values are
140
+ # concatenated (space-separated) so both Stimulus bindings apply.
141
+ # All other keys use standard merge (component wins on conflict).
142
+ # @return [Hash] merged data attributes hash suitable for the +data:+
143
+ # kwarg of +content_tag+.
144
+ # @raise [ArgumentError] if +component_options+ contains a +class:+ key.
145
+ # Kiso uses +css_classes:+ to avoid conflicts with Ruby's +class+
146
+ # method and to make the override intent explicit.
147
+ #
148
+ # @example Basic usage in a component partial
149
+ # <%# _badge.html.erb %>
150
+ # <%= content_tag :span,
151
+ # class: Kiso::Themes::Badge.render(color: color, class: css_classes),
152
+ # data: kiso_prepare_options(component_options, slot: "badge"),
153
+ # **component_options do %>
154
+ # <%= yield %>
155
+ # <% end %>
70
156
  #
71
157
  # @example With a Stimulus controller
72
- # data: kiso_prepare_options(component_options, slot: "toggle", controller: "kiso--toggle")
158
+ # data: kiso_prepare_options(component_options, slot: "toggle",
159
+ # controller: "kiso--toggle")
160
+ #
161
+ # @example Caller passes data attributes through
162
+ # <%# In the view: %>
163
+ # <%= kui(:badge, data: { turbo_frame: "results" }) { "Active" } %>
164
+ # <%# Inside the partial, kiso_prepare_options merges caller's data
165
+ # with { slot: "badge" }, producing:
166
+ # { slot: "badge", turbo_frame: "results" } %>
73
167
  def kiso_prepare_options(component_options, slot:, **data_attrs)
74
168
  if component_options.key?(:class)
75
169
  raise ArgumentError, "Use css_classes: instead of class: for Kiso components"
76
170
  end
77
171
 
78
- (component_options.delete(:data) || {}).merge(slot: slot, **data_attrs)
172
+ user_data = component_options.delete(:data) || {}
173
+ component_data = {slot: slot, **data_attrs}
174
+
175
+ # Stimulus data-action and data-controller are space-separated and
176
+ # additive — concatenate rather than overwrite so both the component's
177
+ # and user's bindings apply.
178
+ CONCATENABLE_DATA_KEYS.each do |key|
179
+ if user_data.key?(key) && component_data.key?(key)
180
+ component_data[key] = "#{user_data.delete(key)} #{component_data[key]}"
181
+ end
182
+ end
183
+
184
+ user_data.merge(component_data)
79
185
  end
80
186
 
187
+ # Data attribute keys whose values are space-separated lists in Stimulus.
188
+ # When both the user and the component provide these, values are
189
+ # concatenated rather than overwritten.
190
+ CONCATENABLE_DATA_KEYS = %i[action controller].freeze
191
+
81
192
  # Renders a themed HTML element with Kiso conventions.
82
193
  #
83
- # Collapses the common +content_tag+ + +kiso_prepare_options+ + theme
84
- # rendering boilerplate into a single call. Use in component partials
85
- # instead of manually wiring +content_tag+.
194
+ # A convenience method that collapses the common three-step boilerplate
195
+ # in component partials:
196
+ #
197
+ # 1. +theme.render(**variants, class: css_classes)+ -- compute classes
198
+ # 2. +kiso_prepare_options(component_options, slot: ...)+ -- build data attrs
199
+ # 3. +content_tag(tag, class: ..., data: ..., **component_options)+ -- render
200
+ #
201
+ # Into a single call. Prefer this over manual +content_tag+ wiring in
202
+ # component partials for consistency.
86
203
  #
87
204
  # @param tag [Symbol] HTML element name (e.g. +:div+, +:span+, +:button+)
88
- # @param theme [ClassVariants::Instance] the theme module to render classes from
89
- # @param slot [String] the +data-slot+ value (kebab-case)
90
- # @param css_classes [String] caller's class overrides, merged via +tailwind_merge+
91
- # @param variants [Hash] variant values forwarded to +theme.render+
92
- # (e.g. +{ size: :md, color: :primary }+)
93
- # @param component_options [Hash] HTML attributes forwarded to +content_tag+
94
- # (e.g. +id:+, +aria:+). A +data:+ key is extracted and merged with slot.
205
+ # @param theme [ClassVariants::Instance] the theme module to render
206
+ # classes from (e.g. +Kiso::Themes::Badge+, +Kiso::Themes::Card+).
207
+ # Must respond to +#render(**variants, class:)+.
208
+ # @param slot [String] the +data-slot+ value in kebab-case
209
+ # (e.g. +"badge"+, +"card-header"+).
210
+ # @param css_classes [String] caller's class overrides, merged via
211
+ # +tailwind_merge+ inside the theme's +#render+ method. Defaults to
212
+ # an empty string.
213
+ # @param variants [Hash] variant key-value pairs forwarded to
214
+ # +theme.render+ (e.g. +{ size: :md, color: :primary, variant: :soft }+).
215
+ # @param component_options [Hash] HTML attributes forwarded to
216
+ # +content_tag+ (e.g. +id:+, +aria:+, +type:+). A +data:+ key is
217
+ # extracted and merged with the slot and any Stimulus bindings.
95
218
  # @yield optional block for element content
96
219
  # @return [ActiveSupport::SafeBuffer] rendered HTML
97
220
  #
98
221
  # @example In a component partial
99
- # kui_tag :div, theme: Kiso::Themes::Badge, slot: "badge",
100
- # css_classes: css_classes, variants: { color: color, variant: variant },
101
- # **component_options do
102
- # yield
103
- # end
222
+ # <%# _badge.html.erb %>
223
+ # <%= kui_tag :span, theme: Kiso::Themes::Badge, slot: "badge",
224
+ # css_classes: css_classes,
225
+ # variants: { color: color, variant: variant, size: size },
226
+ # **component_options do %>
227
+ # <%= yield %>
228
+ # <% end %>
229
+ #
230
+ # @example Button with type attribute
231
+ # <%= kui_tag :button, theme: Kiso::Themes::Button, slot: "button",
232
+ # css_classes: css_classes,
233
+ # variants: { color: color, variant: variant, size: size },
234
+ # type: "button", **component_options do %>
235
+ # <%= yield %>
236
+ # <% end %>
104
237
  def kui_tag(tag, theme:, slot:, css_classes: "", variants: {}, **component_options, &block)
105
238
  html_options = {
106
239
  class: theme.render(**variants, class: css_classes),
@@ -117,15 +250,33 @@ module Kiso
117
250
 
118
251
  private
119
252
 
120
- # Shared rendering pipeline for both kui() and appui().
253
+ # Shared rendering pipeline for both {#kui} and {AppComponentHelper#appui}.
254
+ #
255
+ # Handles two distinct code paths:
256
+ #
257
+ # - **Parent components** (+part+ is +nil+): merges global and instance
258
+ # +ui:+ layers, pushes +ui:+ and +scope:+ onto their respective
259
+ # context stacks, renders the partial, then pops the stacks in an
260
+ # +ensure+ block.
261
+ # - **Sub-parts** (+part+ is present): reads the parent's context stacks
262
+ # to inherit +ui:+ slot overrides and +scope:+ domain locals, then
263
+ # renders the partial with the merged kwargs.
264
+ #
265
+ # The empty +proc {}+ guard is critical: without it, partials that call
266
+ # +capture { yield }.presence+ for optional block overrides would have
267
+ # their +yield+ bubble through nested +content_tag+ blocks all the way
268
+ # to the layout's +<%= yield %>+, capturing the entire page template.
121
269
  #
122
270
  # @param component [Symbol] the component name
123
271
  # @param part [Symbol, nil] optional sub-part name
124
- # @param path_prefix [String] partial path prefix (e.g. "kiso/components" or "components")
125
- # @param collection [Array, nil] renders the partial once per item when present
126
- # @param ui [Hash, nil] per-slot class overrides
272
+ # @param path_prefix [String] partial path prefix
273
+ # (+"kiso/components"+ for engine, +"components"+ for host app)
274
+ # @param collection [Array, nil] renders the partial once per item
275
+ # @param ui [Hash{Symbol => String}, nil] per-slot class overrides
127
276
  # @param scope [Hash, nil] domain locals shared from parent to sub-parts
128
- # @param merge_global_ui [Boolean] whether to merge global config ui layer
277
+ # @param merge_global_ui [Boolean] whether to merge global config ui
278
+ # layer. +true+ for {#kui} (engine components have global config),
279
+ # +false+ for {AppComponentHelper#appui} (host apps own the source).
129
280
  # @param kwargs [Hash] locals passed to the partial
130
281
  # @param block [Proc] optional block for component content
131
282
  # @return [ActiveSupport::SafeBuffer] rendered HTML
@@ -198,12 +349,18 @@ module Kiso
198
349
  end
199
350
  end
200
351
 
201
- # Merge global config ui overrides with instance ui overrides.
202
- # Global config is Layer 2, instance +ui:+ is Layer 3.
352
+ # Merges global config +ui:+ overrides (Layer 2) with per-instance
353
+ # +ui:+ overrides (Layer 3).
203
354
  #
204
- # @param component [Symbol] the component name
205
- # @param instance_ui [Hash, nil] per-instance ui overrides
206
- # @return [Hash{Symbol => String}] merged ui hash
355
+ # When both layers define the same slot, their class strings are
356
+ # concatenated (space-separated). Conflicts are resolved later by
357
+ # +tailwind_merge+ when the sub-part calls +theme.render(class:)+.
358
+ #
359
+ # @param component [Symbol] the component name (e.g. +:card+)
360
+ # @param instance_ui [Hash{Symbol => String}, nil] per-instance ui
361
+ # overrides passed to +kui(:card, ui: { ... })+
362
+ # @return [Hash{Symbol => String}] merged ui hash with all slot
363
+ # overrides from both layers
207
364
  def kiso_merge_ui_layers(component, instance_ui)
208
365
  global_ui = Kiso.config.theme.dig(component, :ui)
209
366
  return instance_ui || {} if global_ui.nil? || global_ui.empty?