kiso 0.2.2.pre → 0.4.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -1
  3. data/README.md +16 -3
  4. data/app/assets/tailwind/kiso/dialog.css +46 -0
  5. data/app/assets/tailwind/kiso/engine.css +6 -3
  6. data/app/assets/tailwind/kiso/input-otp.css +17 -0
  7. data/app/assets/tailwind/kiso/palettes/blue.css +65 -0
  8. data/app/assets/tailwind/kiso/palettes/green.css +65 -0
  9. data/app/assets/tailwind/kiso/palettes/orange.css +65 -0
  10. data/app/assets/tailwind/kiso/palettes/violet.css +65 -0
  11. data/app/assets/tailwind/kiso/palettes/zinc.css +65 -0
  12. data/app/assets/tailwind/kiso/slider.css +27 -0
  13. data/app/helpers/kiso/app_component_helper.rb +53 -0
  14. data/app/helpers/kiso/component_helper.rb +110 -24
  15. data/app/helpers/kiso/ui_context_helper.rb +59 -0
  16. data/app/javascript/controllers/kiso/alert_controller.js +33 -0
  17. data/app/javascript/controllers/kiso/dialog_controller.js +140 -0
  18. data/app/javascript/controllers/kiso/index.js +6 -0
  19. data/app/javascript/controllers/kiso/slider_controller.js +276 -0
  20. data/app/views/kiso/components/_alert.html.erb +19 -3
  21. data/app/views/kiso/components/_alert_dialog.html.erb +28 -0
  22. data/app/views/kiso/components/_app.html.erb +7 -0
  23. data/app/views/kiso/components/_aspect_ratio.html.erb +8 -0
  24. data/app/views/kiso/components/_avatar.html.erb +3 -3
  25. data/app/views/kiso/components/_breadcrumb.html.erb +1 -1
  26. data/app/views/kiso/components/_button.html.erb +31 -17
  27. data/app/views/kiso/components/_color_mode_button.html.erb +1 -1
  28. data/app/views/kiso/components/_color_mode_select.html.erb +4 -4
  29. data/app/views/kiso/components/_container.html.erb +7 -0
  30. data/app/views/kiso/components/_dashboard_sidebar.html.erb +1 -1
  31. data/app/views/kiso/components/_dialog.html.erb +11 -0
  32. data/app/views/kiso/components/_footer.html.erb +7 -0
  33. data/app/views/kiso/components/_header.html.erb +7 -0
  34. data/app/views/kiso/components/_main.html.erb +7 -0
  35. data/app/views/kiso/components/_page.html.erb +7 -0
  36. data/app/views/kiso/components/_page_body.html.erb +7 -0
  37. data/app/views/kiso/components/_page_card.html.erb +40 -0
  38. data/app/views/kiso/components/_page_grid.html.erb +7 -0
  39. data/app/views/kiso/components/_page_header.html.erb +38 -0
  40. data/app/views/kiso/components/_page_section.html.erb +11 -0
  41. data/app/views/kiso/components/_pagination.html.erb +1 -1
  42. data/app/views/kiso/components/_select_native.html.erb +3 -3
  43. data/app/views/kiso/components/_skeleton.html.erb +5 -0
  44. data/app/views/kiso/components/_slider.html.erb +42 -0
  45. data/app/views/kiso/components/_switch.html.erb +2 -2
  46. data/app/views/kiso/components/alert/_actions.html.erb +7 -0
  47. data/app/views/kiso/components/alert_dialog/_action.html.erb +8 -0
  48. data/app/views/kiso/components/alert_dialog/_cancel.html.erb +8 -0
  49. data/app/views/kiso/components/alert_dialog/_description.html.erb +8 -0
  50. data/app/views/kiso/components/alert_dialog/_footer.html.erb +7 -0
  51. data/app/views/kiso/components/alert_dialog/_header.html.erb +7 -0
  52. data/app/views/kiso/components/alert_dialog/_media.html.erb +7 -0
  53. data/app/views/kiso/components/alert_dialog/_title.html.erb +8 -0
  54. data/app/views/kiso/components/breadcrumb/_ellipsis.html.erb +1 -1
  55. data/app/views/kiso/components/combobox/_input.html.erb +1 -1
  56. data/app/views/kiso/components/combobox/_item.html.erb +2 -2
  57. data/app/views/kiso/components/combobox/_list.html.erb +2 -1
  58. data/app/views/kiso/components/command/_group.html.erb +2 -2
  59. data/app/views/kiso/components/command/_input.html.erb +3 -2
  60. data/app/views/kiso/components/command/_list.html.erb +2 -1
  61. data/app/views/kiso/components/dashboard_navbar/_toggle.html.erb +1 -1
  62. data/app/views/kiso/components/dashboard_sidebar/_collapse.html.erb +1 -1
  63. data/app/views/kiso/components/dashboard_sidebar/_toggle.html.erb +1 -1
  64. data/app/views/kiso/components/dialog/_body.html.erb +7 -0
  65. data/app/views/kiso/components/dialog/_close.html.erb +10 -0
  66. data/app/views/kiso/components/dialog/_description.html.erb +7 -0
  67. data/app/views/kiso/components/dialog/_footer.html.erb +7 -0
  68. data/app/views/kiso/components/dialog/_header.html.erb +7 -0
  69. data/app/views/kiso/components/dialog/_title.html.erb +7 -0
  70. data/app/views/kiso/components/empty/_actions.html.erb +7 -0
  71. data/app/views/kiso/components/nav/_item.html.erb +2 -2
  72. data/app/views/kiso/components/nav/_section.html.erb +5 -5
  73. data/app/views/kiso/components/page/_center.html.erb +7 -0
  74. data/app/views/kiso/components/page/_left.html.erb +7 -0
  75. data/app/views/kiso/components/page/_right.html.erb +7 -0
  76. data/app/views/kiso/components/page_card/_body.html.erb +7 -0
  77. data/app/views/kiso/components/page_card/_description.html.erb +7 -0
  78. data/app/views/kiso/components/page_card/_footer.html.erb +7 -0
  79. data/app/views/kiso/components/page_card/_header.html.erb +7 -0
  80. data/app/views/kiso/components/page_card/_icon.html.erb +7 -0
  81. data/app/views/kiso/components/page_card/_title.html.erb +7 -0
  82. data/app/views/kiso/components/page_header/_description.html.erb +7 -0
  83. data/app/views/kiso/components/page_header/_headline.html.erb +7 -0
  84. data/app/views/kiso/components/page_header/_links.html.erb +7 -0
  85. data/app/views/kiso/components/page_header/_title.html.erb +7 -0
  86. data/app/views/kiso/components/page_section/_body.html.erb +7 -0
  87. data/app/views/kiso/components/page_section/_description.html.erb +7 -0
  88. data/app/views/kiso/components/page_section/_header.html.erb +7 -0
  89. data/app/views/kiso/components/page_section/_headline.html.erb +7 -0
  90. data/app/views/kiso/components/page_section/_links.html.erb +7 -0
  91. data/app/views/kiso/components/page_section/_title.html.erb +7 -0
  92. data/app/views/kiso/components/page_section/_wrapper.html.erb +7 -0
  93. data/app/views/kiso/components/pagination/_ellipsis.html.erb +1 -1
  94. data/app/views/kiso/components/pagination/_next.html.erb +2 -2
  95. data/app/views/kiso/components/pagination/_previous.html.erb +2 -2
  96. data/app/views/kiso/components/select/_item.html.erb +2 -2
  97. data/config/locales/en.yml +33 -0
  98. data/lib/generators/kiso/component/USAGE +35 -0
  99. data/lib/generators/kiso/component/component_generator.rb +104 -0
  100. data/lib/generators/kiso/component/templates/partial.html.erb.tt +7 -0
  101. data/lib/generators/kiso/component/templates/sub_part_partial.html.erb.tt +7 -0
  102. data/lib/generators/kiso/component/templates/sub_part_theme.rb.tt +21 -0
  103. data/lib/generators/kiso/component/templates/theme.rb.tt +21 -0
  104. data/lib/kiso/configuration.rb +40 -0
  105. data/lib/kiso/engine.rb +105 -1
  106. data/lib/kiso/presets/rounded.rb +136 -0
  107. data/lib/kiso/presets/sharp.rb +178 -0
  108. data/lib/kiso/presets.rb +49 -0
  109. data/lib/kiso/theme_overrides.rb +5 -1
  110. data/lib/kiso/themes/alert.rb +24 -11
  111. data/lib/kiso/themes/alert_dialog.rb +78 -0
  112. data/lib/kiso/themes/aspect_ratio.rb +16 -0
  113. data/lib/kiso/themes/dashboard.rb +3 -3
  114. data/lib/kiso/themes/dialog.rb +57 -0
  115. data/lib/kiso/themes/empty.rb +6 -1
  116. data/lib/kiso/themes/input_otp.rb +2 -2
  117. data/lib/kiso/themes/layout.rb +69 -0
  118. data/lib/kiso/themes/page.rb +295 -0
  119. data/lib/kiso/themes/skeleton.rb +16 -0
  120. data/lib/kiso/themes/slider.rb +53 -0
  121. data/lib/kiso/version.rb +1 -1
  122. data/lib/kiso.rb +11 -0
  123. metadata +89 -1
@@ -23,9 +23,49 @@ module Kiso
23
23
  # Applied once at boot by {ThemeOverrides.apply!}.
24
24
  attr_reader :theme
25
25
 
26
+ # @return [Symbol] the active app theme directory name.
27
+ # Theme files are loaded from +app/themes/<name>/+. Defaults to
28
+ # +:default+, meaning +app/themes/default/+.
29
+ #
30
+ # @example Switch to a custom theme
31
+ # Kiso.configure do |config|
32
+ # config.app_theme = :modern
33
+ # end
34
+ attr_accessor :app_theme
35
+
26
36
  def initialize
27
37
  @icons = default_icons
28
38
  @theme = {}
39
+ @app_theme = :default
40
+ end
41
+
42
+ # Resolves the active app theme directory path relative to the given root.
43
+ #
44
+ # @param app_root [Pathname] the Rails application root
45
+ # @return [Pathname] the active theme directory (e.g. +app/themes/default+)
46
+ def app_theme_path(app_root)
47
+ app_root.join("app/themes", app_theme.to_s)
48
+ end
49
+
50
+ # Applies a pre-built style preset to all components.
51
+ # Presets populate +@theme+ before {ThemeOverrides.apply!} runs,
52
+ # so host-app overrides set after this call take priority.
53
+ #
54
+ # @param name [Symbol, String] the preset name (e.g. +:rounded+, +:sharp+)
55
+ # @raise [ArgumentError] if the preset does not exist
56
+ # @return [void]
57
+ #
58
+ # @example
59
+ # Kiso.configure do |config|
60
+ # config.apply_preset(:rounded)
61
+ # # Per-component overrides still work on top of the preset:
62
+ # config.theme[:button] = { base: "shadow-lg" }
63
+ # end
64
+ def apply_preset(name)
65
+ preset = Kiso::Presets.load(name)
66
+ preset.each do |component, overrides|
67
+ @theme[component] = (@theme[component] || {}).merge(overrides)
68
+ end
29
69
  end
30
70
 
31
71
  private
data/lib/kiso/engine.rb CHANGED
@@ -10,6 +10,11 @@ module Kiso
10
10
  class Engine < ::Rails::Engine
11
11
  isolate_namespace Kiso
12
12
 
13
+ # Loads Kiso's locale files so host apps can override translations.
14
+ initializer "kiso.i18n" do
15
+ config.i18n.load_path += Dir[root.join("config/locales/**/*.yml")]
16
+ end
17
+
13
18
  # Configures ClassVariants to use TailwindMerge for class deduplication.
14
19
  # This ensures conflicting Tailwind utilities are resolved correctly
15
20
  # when merging base, variant, and override classes.
@@ -27,10 +32,12 @@ module Kiso
27
32
  Kiso::ThemeOverrides.apply!
28
33
  end
29
34
 
30
- # Makes {ComponentHelper} and {IconHelper} available in all views.
35
+ # Makes {ComponentHelper}, {AppComponentHelper}, and {IconHelper} available in all views.
31
36
  initializer "kiso.helpers" do
32
37
  ActiveSupport.on_load(:action_view) do
38
+ include Kiso::UiContextHelper
33
39
  include Kiso::ComponentHelper
40
+ include Kiso::AppComponentHelper
34
41
  include Kiso::IconHelper
35
42
  include Kiso::ThemeHelper
36
43
  end
@@ -96,6 +103,103 @@ module Kiso
96
103
  end
97
104
  end
98
105
 
106
+ # Watches lib/kiso/themes/ in development and reloads changed theme
107
+ # constants so you don't have to restart the server after every tweak.
108
+ # Uses Rails' built-in FileUpdateChecker (same mechanism as route reloading).
109
+ initializer "kiso.theme_reloading" do |app|
110
+ if Rails.env.development? || Rails.env.test?
111
+ theme_dir = root.join("lib/kiso/themes")
112
+ theme_files = Dir[theme_dir.join("*.rb")]
113
+
114
+ reloader = app.config.file_watcher.new(theme_files) do
115
+ # Suppress "already initialized constant" warnings during reload.
116
+ # Theme files assign to constants (Badge = ClassVariants.build(...))
117
+ # which is intentional — we want to replace the old instance.
118
+ verbose, $VERBOSE = $VERBOSE, nil
119
+ begin
120
+ theme_files.each { |file| load file }
121
+ ensure
122
+ $VERBOSE = verbose
123
+ end
124
+ Kiso::ThemeOverrides.reset!
125
+ Kiso::ThemeOverrides.apply!
126
+ end
127
+
128
+ app.reloaders << reloader
129
+
130
+ ActiveSupport::Reloader.to_prepare do
131
+ reloader.execute_if_updated
132
+ end
133
+ end
134
+ end
135
+
136
+ # Loads the active app theme from +app/themes/<name>/+.
137
+ #
138
+ # Theme files define constants under the +AppThemes::+ namespace
139
+ # (e.g. +AppThemes::StatusBadge+) and are loaded via +require+ at
140
+ # boot. The active theme is determined by {Configuration#app_theme}
141
+ # (defaults to +:default+).
142
+ #
143
+ # No-op when +app/themes/+ doesn't exist. Raises a helpful error
144
+ # when the configured theme directory is missing.
145
+ initializer "kiso.app_themes", after: :load_config_initializers do |app|
146
+ themes_root = app.root.join("app/themes")
147
+ next unless themes_root.directory?
148
+
149
+ # Tell Zeitwerk to ignore app/themes/ — we manage loading ourselves.
150
+ # Rails auto-discovers app/* subdirectories and adds them to Zeitwerk,
151
+ # which would expect Default::StatusBadge (matching the directory name)
152
+ # instead of AppThemes::StatusBadge.
153
+ Rails.autoloaders.main.ignore(themes_root.to_s)
154
+
155
+ active_path = Kiso.config.app_theme_path(app.root)
156
+
157
+ unless active_path.directory?
158
+ available = Dir.children(themes_root.to_s)
159
+ .select { |d| File.directory?(themes_root.join(d)) }
160
+ .sort
161
+
162
+ msg = "Kiso app theme :#{Kiso.config.app_theme} not found. " \
163
+ "Expected directory: #{active_path}"
164
+ msg += if available.any?
165
+ "\nAvailable themes: #{available.map { |d| ":#{d}" }.join(", ")}"
166
+ else
167
+ "\nNo theme directories found in #{themes_root}. " \
168
+ "Run: bin/rails generate kiso:component your_component"
169
+ end
170
+ raise Kiso::Error, msg
171
+ end
172
+
173
+ Object.const_set(:AppThemes, Module.new) unless Object.const_defined?(:AppThemes)
174
+
175
+ Dir[active_path.join("**/*.rb")].sort.each { |file| load file }
176
+ end
177
+
178
+ # Watches the active app theme directory in development and reloads
179
+ # changed theme constants. Uses directory-based watching so new files
180
+ # added after boot are picked up automatically.
181
+ initializer "kiso.app_theme_reloading" do |app|
182
+ next unless Rails.env.development? || Rails.env.test?
183
+
184
+ active_path = Kiso.config.app_theme_path(app.root)
185
+ next unless active_path.directory?
186
+
187
+ reloader = app.config.file_watcher.new([], {active_path.to_s => ["rb"]}) do
188
+ verbose, $VERBOSE = $VERBOSE, nil
189
+ begin
190
+ Dir[active_path.join("**/*.rb")].sort.each { |file| load file }
191
+ ensure
192
+ $VERBOSE = verbose
193
+ end
194
+ end
195
+
196
+ app.reloaders << reloader
197
+
198
+ ActiveSupport::Reloader.to_prepare do
199
+ reloader.execute_if_updated
200
+ end
201
+ end
202
+
99
203
  # Registers Kiso's component previews with Lookbook when available.
100
204
  initializer "kiso.lookbook", after: :load_config_initializers do
101
205
  if defined?(Lookbook)
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiso
4
+ module Presets
5
+ # Pill-shaped buttons, fully rounded badges, more rounded cards/inputs.
6
+ # Applies rounded-full where possible, rounded-2xl on containers.
7
+ #
8
+ # Components left unchanged:
9
+ # - Avatar, Switch, Slider, RadioGroup — already rounded-full
10
+ # - Checkbox — uses rounded-[4px] for checkmark alignment
11
+ # - Shared::CHECKABLE_ITEM — uses rounded-sm for menu items (structural)
12
+ ROUNDED = {
13
+ # Buttons: rounded-md → rounded-full (pill shape)
14
+ button: {
15
+ variants: {
16
+ size: {
17
+ xs: "rounded-full",
18
+ sm: "rounded-full",
19
+ md: "rounded-full",
20
+ lg: "rounded-full",
21
+ xl: "rounded-full"
22
+ }
23
+ }
24
+ },
25
+
26
+ # Badges: already rounded-full — no change needed
27
+
28
+ # Card: rounded-xl → rounded-2xl
29
+ card: {base: "rounded-2xl"},
30
+
31
+ # StatsCard: rounded-xl → rounded-2xl
32
+ stats_card: {base: "rounded-2xl"},
33
+
34
+ # Input: rounded-md → rounded-full
35
+ input: {base: "rounded-full"},
36
+
37
+ # Textarea: rounded-md → rounded-xl (full doesn't work for multiline)
38
+ textarea: {base: "rounded-xl"},
39
+
40
+ # Select trigger: rounded-md → rounded-full
41
+ select_trigger: {base: "rounded-full"},
42
+
43
+ # Select content: rounded-md → rounded-xl
44
+ select_content: {base: "rounded-xl"},
45
+
46
+ # SelectNative: rounded-md → rounded-full
47
+ select_native: {base: "rounded-full"},
48
+
49
+ # InputGroup: rounded-md → rounded-full
50
+ input_group: {base: "rounded-full"},
51
+
52
+ # InputOTP slot: first:rounded-l-md last:rounded-r-md → first:rounded-l-xl last:rounded-r-xl
53
+ input_otp_slot: {base: "first:rounded-l-xl last:rounded-r-xl"},
54
+
55
+ # Toggle: rounded-md → rounded-full
56
+ toggle: {base: "rounded-full"},
57
+
58
+ # ToggleGroup: rounded-md → rounded-full
59
+ toggle_group: {base: "rounded-full"},
60
+
61
+ # ToggleGroupItem: first:rounded-l-md last:rounded-r-md → first:rounded-l-full last:rounded-r-full
62
+ toggle_group_item: {base: "first:rounded-l-full last:rounded-r-full"},
63
+
64
+ # Combobox input wrapper: rounded-md → rounded-full
65
+ combobox_input: {base: "rounded-full"},
66
+
67
+ # Combobox content: rounded-md → rounded-xl
68
+ combobox_content: {base: "rounded-xl"},
69
+
70
+ # Combobox chips (multi-select): rounded-md → rounded-xl
71
+ combobox_chips: {base: "rounded-xl"},
72
+
73
+ # Dialog content: rounded-lg → rounded-2xl
74
+ dialog_content: {base: "rounded-2xl"},
75
+
76
+ # AlertDialog content: rounded-lg → rounded-2xl
77
+ alert_dialog_content: {base: "rounded-2xl"},
78
+
79
+ # AlertDialog media: rounded-md → rounded-xl
80
+ alert_dialog_media: {base: "rounded-xl"},
81
+
82
+ # Alert: rounded-lg → rounded-2xl
83
+ alert: {base: "rounded-2xl"},
84
+
85
+ # Command: rounded-md → rounded-xl
86
+ command: {base: "rounded-xl"},
87
+
88
+ # CommandDialog content: rounded-lg → rounded-2xl
89
+ command_dialog_content: {base: "rounded-2xl"},
90
+
91
+ # Popover: rounded-md → rounded-xl
92
+ popover_content: {base: "rounded-xl"},
93
+
94
+ # DropdownMenu content: rounded-md → rounded-xl
95
+ dropdown_menu_content: {base: "rounded-xl"},
96
+
97
+ # DropdownMenu sub-content: rounded-md → rounded-xl
98
+ dropdown_menu_sub_content: {base: "rounded-xl"},
99
+
100
+ # Kbd: rounded-sm → rounded-md
101
+ kbd: {base: "rounded-md"},
102
+
103
+ # Skeleton: rounded-md → rounded-xl
104
+ skeleton: {base: "rounded-xl"},
105
+
106
+ # Pagination: rounded-md → rounded-full
107
+ pagination_link: {base: "rounded-full"},
108
+ pagination_previous: {base: "rounded-full"},
109
+ pagination_next: {base: "rounded-full"},
110
+
111
+ # Empty: rounded-lg → rounded-2xl
112
+ empty: {base: "rounded-2xl"},
113
+
114
+ # Empty media icon variant: rounded-lg → rounded-xl
115
+ empty_media: {
116
+ variants: {
117
+ variant: {
118
+ icon: "rounded-xl"
119
+ }
120
+ }
121
+ },
122
+
123
+ # ColorModeButton: rounded-md → rounded-full
124
+ color_mode_button: {base: "rounded-full"},
125
+
126
+ # Nav section title: rounded-md → rounded-full
127
+ nav_section_title: {base: "rounded-full"},
128
+
129
+ # Nav item: rounded-md → rounded-full
130
+ nav_item: {base: "rounded-full"},
131
+
132
+ # Nav item badge: rounded-md → rounded-full
133
+ nav_item_badge: {base: "rounded-full"}
134
+ }.freeze
135
+ end
136
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiso
4
+ module Presets
5
+ # No border-radius anywhere — geometric, brutalist aesthetic.
6
+ # Applies rounded-none to every component that has border-radius.
7
+ SHARP = {
8
+ # Buttons: rounded-md → rounded-none
9
+ button: {
10
+ variants: {
11
+ size: {
12
+ xs: "rounded-none",
13
+ sm: "rounded-none",
14
+ md: "rounded-none",
15
+ lg: "rounded-none",
16
+ xl: "rounded-none"
17
+ }
18
+ }
19
+ },
20
+
21
+ # Badges: rounded-full → rounded-none
22
+ badge: {
23
+ variants: {
24
+ size: {
25
+ xs: "rounded-none",
26
+ sm: "rounded-none",
27
+ md: "rounded-none",
28
+ lg: "rounded-none",
29
+ xl: "rounded-none"
30
+ }
31
+ }
32
+ },
33
+
34
+ # Card: rounded-xl → rounded-none
35
+ card: {base: "rounded-none"},
36
+
37
+ # StatsCard: rounded-xl → rounded-none
38
+ stats_card: {base: "rounded-none"},
39
+
40
+ # Input: rounded-md → rounded-none
41
+ input: {base: "rounded-none"},
42
+
43
+ # Textarea: rounded-md → rounded-none
44
+ textarea: {base: "rounded-none"},
45
+
46
+ # Select trigger: rounded-md → rounded-none
47
+ select_trigger: {base: "rounded-none"},
48
+
49
+ # Select content: rounded-md → rounded-none
50
+ select_content: {base: "rounded-none"},
51
+
52
+ # SelectNative: rounded-md → rounded-none
53
+ select_native: {base: "rounded-none"},
54
+
55
+ # InputGroup: rounded-md → rounded-none
56
+ input_group: {base: "rounded-none"},
57
+
58
+ # InputOTP slot: rounded-l-md/rounded-r-md → rounded-none
59
+ input_otp_slot: {base: "first:rounded-l-none last:rounded-r-none"},
60
+
61
+ # Toggle: rounded-md → rounded-none
62
+ toggle: {base: "rounded-none"},
63
+
64
+ # ToggleGroup: rounded-md → rounded-none
65
+ toggle_group: {base: "rounded-none"},
66
+
67
+ # ToggleGroupItem: rounded-l-md/rounded-r-md → rounded-none
68
+ toggle_group_item: {base: "first:rounded-l-none last:rounded-r-none"},
69
+
70
+ # Combobox input wrapper: rounded-md → rounded-none
71
+ combobox_input: {base: "rounded-none"},
72
+
73
+ # Combobox content: rounded-md → rounded-none
74
+ combobox_content: {base: "rounded-none"},
75
+
76
+ # Combobox chips (multi-select): rounded-md → rounded-none
77
+ combobox_chips: {base: "rounded-none"},
78
+
79
+ # Dialog content: rounded-lg → rounded-none
80
+ dialog_content: {base: "rounded-none"},
81
+
82
+ # AlertDialog content: rounded-lg → rounded-none
83
+ alert_dialog_content: {base: "rounded-none"},
84
+
85
+ # AlertDialog media: rounded-md → rounded-none
86
+ alert_dialog_media: {base: "rounded-none"},
87
+
88
+ # Alert: rounded-lg → rounded-none
89
+ alert: {base: "rounded-none"},
90
+
91
+ # Command: rounded-md → rounded-none
92
+ command: {base: "rounded-none"},
93
+
94
+ # CommandDialog content: rounded-lg → rounded-none
95
+ command_dialog_content: {base: "rounded-none"},
96
+
97
+ # DashboardNavbarToggle: rounded-md → rounded-none
98
+ dashboard_navbar_toggle: {base: "rounded-none"},
99
+
100
+ # DashboardSidebarCollapse: rounded-md → rounded-none
101
+ dashboard_sidebar_collapse: {base: "rounded-none"},
102
+
103
+ # DashboardSidebarToggle: rounded-md → rounded-none
104
+ dashboard_sidebar_toggle: {base: "rounded-none"},
105
+
106
+ # Popover: rounded-md → rounded-none
107
+ popover_content: {base: "rounded-none"},
108
+
109
+ # DropdownMenu content: rounded-md → rounded-none
110
+ dropdown_menu_content: {base: "rounded-none"},
111
+
112
+ # DropdownMenu sub-content: rounded-md → rounded-none
113
+ dropdown_menu_sub_content: {base: "rounded-none"},
114
+
115
+ # Kbd: rounded-sm → rounded-none
116
+ kbd: {base: "rounded-none"},
117
+
118
+ # Skeleton: rounded-md → rounded-none
119
+ skeleton: {base: "rounded-none"},
120
+
121
+ # Checkbox: rounded-[4px] → rounded-none
122
+ checkbox: {base: "rounded-none"},
123
+
124
+ # Avatar: rounded-full → rounded-none
125
+ avatar: {base: "rounded-none"},
126
+ avatar_badge: {base: "rounded-none"},
127
+ avatar_fallback: {base: "rounded-none"},
128
+ avatar_group_count: {base: "rounded-none"},
129
+ avatar_image: {base: "rounded-none"},
130
+
131
+ # Switch track: rounded-full → rounded-none
132
+ switch_track: {base: "rounded-none"},
133
+ switch_thumb: {base: "rounded-none"},
134
+
135
+ # Slider: rounded-full → rounded-none
136
+ slider_track: {base: "rounded-none"},
137
+ slider_thumb: {base: "rounded-none"},
138
+
139
+ # RadioGroup indicator: rounded-full → rounded-none
140
+ radio_group_item: {base: "rounded-none"},
141
+
142
+ # Pagination: rounded-md → rounded-none
143
+ pagination_link: {base: "rounded-none"},
144
+ pagination_previous: {base: "rounded-none"},
145
+ pagination_next: {base: "rounded-none"},
146
+
147
+ # Empty: rounded-lg → rounded-none
148
+ empty: {base: "rounded-none"},
149
+
150
+ # Empty media icon variant: rounded-lg → rounded-none
151
+ empty_media: {
152
+ variants: {
153
+ variant: {
154
+ icon: "rounded-none"
155
+ }
156
+ }
157
+ },
158
+
159
+ # Alert close: rounded-md → rounded-none
160
+ alert_close: {base: "rounded-none"},
161
+
162
+ # Dialog close: rounded-xs → rounded-none
163
+ dialog_close: {base: "rounded-none"},
164
+
165
+ # ColorModeButton: rounded-md → rounded-none
166
+ color_mode_button: {base: "rounded-none"},
167
+
168
+ # Nav section title: rounded-md → rounded-none
169
+ nav_section_title: {base: "rounded-none"},
170
+
171
+ # Nav item: rounded-md → rounded-none
172
+ nav_item: {base: "rounded-none"},
173
+
174
+ # Nav item badge: rounded-md → rounded-none
175
+ nav_item_badge: {base: "rounded-none"}
176
+ }.freeze
177
+ end
178
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiso
4
+ # Loads pre-built style presets that override ClassVariants for all
5
+ # components at boot time. Presets are monolithic, not composable —
6
+ # each preset is a complete, coherent set of overrides.
7
+ #
8
+ # @example Loading a preset
9
+ # Kiso.configure do |config|
10
+ # config.apply_preset(:rounded)
11
+ # end
12
+ #
13
+ # @see Configuration#apply_preset
14
+ module Presets
15
+ PRESET_DIR = File.expand_path("presets", __dir__).freeze
16
+
17
+ class << self
18
+ # Loads a preset by name and returns its override hash.
19
+ #
20
+ # @param name [Symbol, String] the preset name (e.g. +:rounded+, +:sharp+)
21
+ # @return [Hash{Symbol => Hash}] component overrides keyed by component name
22
+ # @raise [ArgumentError] if the preset does not exist
23
+ def load(name)
24
+ name = name.to_sym
25
+ file = File.join(PRESET_DIR, "#{name}.rb")
26
+
27
+ unless File.exist?(file)
28
+ available = available_presets
29
+ msg = "Unknown preset :#{name}."
30
+ msg += " Available presets: #{available.map { |p| ":#{p}" }.join(", ")}" if available.any?
31
+ raise ArgumentError, msg
32
+ end
33
+
34
+ require file
35
+ const_name = name.to_s.upcase
36
+ const_get(const_name)
37
+ end
38
+
39
+ # Lists all available preset names.
40
+ #
41
+ # @return [Array<Symbol>] sorted list of preset names
42
+ def available_presets
43
+ Dir[File.join(PRESET_DIR, "*.rb")]
44
+ .map { |f| File.basename(f, ".rb").to_sym }
45
+ .sort
46
+ end
47
+ end
48
+ end
49
+ end
@@ -32,7 +32,11 @@ module Kiso
32
32
  validate_keys!(overrides.keys)
33
33
 
34
34
  overrides.each do |key, options|
35
- resolve_constant(key).merge(**options)
35
+ # Extract ui: before passing to ClassVariants#merge (it doesn't
36
+ # understand ui:). The ui: values stay in config for runtime access
37
+ # by ComponentHelper#kiso_merge_ui_layers.
38
+ cv_options = options.except(:ui)
39
+ resolve_constant(key).merge(**cv_options) unless cv_options.empty?
36
40
  end
37
41
 
38
42
  @applied = true
@@ -1,9 +1,10 @@
1
1
  module Kiso
2
2
  module Themes
3
- # Contextual alert banner with optional icon, title, and description.
3
+ # Contextual alert banner with optional icon, title, description,
4
+ # actions, and close button.
4
5
  #
5
- # Uses CSS Grid with +has-[>svg]+ to auto-allocate a column for the icon
6
- # when an SVG is present as a direct child.
6
+ # Uses flexbox layout with an optional icon, a content wrapper, and
7
+ # an optional close button.
7
8
  #
8
9
  # @example
9
10
  # Alert.render(color: :error, variant: :soft)
@@ -12,12 +13,9 @@ module Kiso
12
13
  # - +color+ — :primary (default), :secondary, :success, :info, :warning, :error, :neutral
13
14
  # - +variant+ — :solid, :outline, :soft (default), :subtle
14
15
  #
15
- # Sub-parts: {AlertTitle}, {AlertDescription}
16
+ # Sub-parts: {AlertWrapper}, {AlertTitle}, {AlertDescription}, {AlertActions}, {AlertClose}
16
17
  Alert = ClassVariants.build(
17
- base: "relative w-full rounded-lg px-4 py-3 text-sm " \
18
- "grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] " \
19
- "has-[>svg]:gap-x-3 gap-y-0.5 items-start " \
20
- "[&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
18
+ base: "relative overflow-hidden w-full rounded-lg p-4 flex items-start gap-2.5 text-sm",
21
19
  variants: {
22
20
  variant: {
23
21
  solid: "",
@@ -67,14 +65,29 @@ module Kiso
67
65
  defaults: {color: :primary, variant: :soft}
68
66
  )
69
67
 
70
- # Alert title text. Rendered in the second grid column (after the icon column).
68
+ # Flex wrapper for alert content (title, description, actions).
69
+ AlertWrapper = ClassVariants.build(
70
+ base: "min-w-0 flex-1 flex flex-col"
71
+ )
72
+
73
+ # Alert title text.
71
74
  AlertTitle = ClassVariants.build(
72
- base: "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight"
75
+ base: "line-clamp-1 min-h-4 font-medium tracking-tight"
73
76
  )
74
77
 
75
78
  # Alert body text. Inherits parent text color for contrast on colored backgrounds.
76
79
  AlertDescription = ClassVariants.build(
77
- base: "col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed"
80
+ base: "mt-1 first:mt-0 space-y-1 text-sm [&_p]:leading-relaxed"
81
+ )
82
+
83
+ # Container for action buttons inside an alert.
84
+ AlertActions = ClassVariants.build(
85
+ base: "flex flex-wrap gap-1.5 shrink-0 mt-2.5"
86
+ )
87
+
88
+ # Close button for dismissible alerts.
89
+ AlertClose = ClassVariants.build(
90
+ base: "shrink-0 -m-0.5 p-0.5 rounded-md opacity-70 hover:opacity-100 transition-opacity cursor-pointer"
78
91
  )
79
92
  end
80
93
  end