kiso 0.3.0.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -1
  3. data/README.md +16 -3
  4. data/app/assets/tailwind/kiso/engine.css +4 -3
  5. data/app/assets/tailwind/kiso/palettes/blue.css +65 -0
  6. data/app/assets/tailwind/kiso/palettes/green.css +65 -0
  7. data/app/assets/tailwind/kiso/palettes/orange.css +65 -0
  8. data/app/assets/tailwind/kiso/palettes/violet.css +65 -0
  9. data/app/assets/tailwind/kiso/palettes/zinc.css +65 -0
  10. data/app/helpers/kiso/app_component_helper.rb +53 -0
  11. data/app/helpers/kiso/component_helper.rb +110 -24
  12. data/app/helpers/kiso/ui_context_helper.rb +59 -0
  13. data/app/javascript/controllers/kiso/alert_controller.js +33 -0
  14. data/app/javascript/controllers/kiso/index.js +3 -0
  15. data/app/views/kiso/components/_alert.html.erb +19 -3
  16. data/app/views/kiso/components/_alert_dialog.html.erb +2 -2
  17. data/app/views/kiso/components/_app.html.erb +7 -0
  18. data/app/views/kiso/components/_avatar.html.erb +3 -3
  19. data/app/views/kiso/components/_breadcrumb.html.erb +1 -1
  20. data/app/views/kiso/components/_color_mode_button.html.erb +1 -1
  21. data/app/views/kiso/components/_color_mode_select.html.erb +4 -4
  22. data/app/views/kiso/components/_container.html.erb +7 -0
  23. data/app/views/kiso/components/_dashboard_sidebar.html.erb +1 -1
  24. data/app/views/kiso/components/_dialog.html.erb +2 -2
  25. data/app/views/kiso/components/_footer.html.erb +7 -0
  26. data/app/views/kiso/components/_header.html.erb +7 -0
  27. data/app/views/kiso/components/_main.html.erb +7 -0
  28. data/app/views/kiso/components/_page.html.erb +7 -0
  29. data/app/views/kiso/components/_page_body.html.erb +7 -0
  30. data/app/views/kiso/components/_page_card.html.erb +40 -0
  31. data/app/views/kiso/components/_page_grid.html.erb +7 -0
  32. data/app/views/kiso/components/_page_header.html.erb +38 -0
  33. data/app/views/kiso/components/_page_section.html.erb +11 -0
  34. data/app/views/kiso/components/_pagination.html.erb +1 -1
  35. data/app/views/kiso/components/_select_native.html.erb +3 -3
  36. data/app/views/kiso/components/_skeleton.html.erb +5 -0
  37. data/app/views/kiso/components/_slider.html.erb +4 -4
  38. data/app/views/kiso/components/_switch.html.erb +2 -2
  39. data/app/views/kiso/components/alert/_actions.html.erb +7 -0
  40. data/app/views/kiso/components/breadcrumb/_ellipsis.html.erb +1 -1
  41. data/app/views/kiso/components/combobox/_input.html.erb +1 -1
  42. data/app/views/kiso/components/combobox/_item.html.erb +2 -2
  43. data/app/views/kiso/components/combobox/_list.html.erb +2 -1
  44. data/app/views/kiso/components/command/_group.html.erb +2 -2
  45. data/app/views/kiso/components/command/_input.html.erb +3 -2
  46. data/app/views/kiso/components/command/_list.html.erb +2 -1
  47. data/app/views/kiso/components/dashboard_navbar/_toggle.html.erb +1 -1
  48. data/app/views/kiso/components/dashboard_sidebar/_collapse.html.erb +1 -1
  49. data/app/views/kiso/components/dashboard_sidebar/_toggle.html.erb +1 -1
  50. data/app/views/kiso/components/dialog/_close.html.erb +1 -1
  51. data/app/views/kiso/components/nav/_item.html.erb +2 -2
  52. data/app/views/kiso/components/nav/_section.html.erb +5 -5
  53. data/app/views/kiso/components/page/_center.html.erb +7 -0
  54. data/app/views/kiso/components/page/_left.html.erb +7 -0
  55. data/app/views/kiso/components/page/_right.html.erb +7 -0
  56. data/app/views/kiso/components/page_card/_body.html.erb +7 -0
  57. data/app/views/kiso/components/page_card/_description.html.erb +7 -0
  58. data/app/views/kiso/components/page_card/_footer.html.erb +7 -0
  59. data/app/views/kiso/components/page_card/_header.html.erb +7 -0
  60. data/app/views/kiso/components/page_card/_icon.html.erb +7 -0
  61. data/app/views/kiso/components/page_card/_title.html.erb +7 -0
  62. data/app/views/kiso/components/page_header/_description.html.erb +7 -0
  63. data/app/views/kiso/components/page_header/_headline.html.erb +7 -0
  64. data/app/views/kiso/components/page_header/_links.html.erb +7 -0
  65. data/app/views/kiso/components/page_header/_title.html.erb +7 -0
  66. data/app/views/kiso/components/page_section/_body.html.erb +7 -0
  67. data/app/views/kiso/components/page_section/_description.html.erb +7 -0
  68. data/app/views/kiso/components/page_section/_header.html.erb +7 -0
  69. data/app/views/kiso/components/page_section/_headline.html.erb +7 -0
  70. data/app/views/kiso/components/page_section/_links.html.erb +7 -0
  71. data/app/views/kiso/components/page_section/_title.html.erb +7 -0
  72. data/app/views/kiso/components/page_section/_wrapper.html.erb +7 -0
  73. data/app/views/kiso/components/pagination/_ellipsis.html.erb +1 -1
  74. data/app/views/kiso/components/pagination/_next.html.erb +2 -2
  75. data/app/views/kiso/components/pagination/_previous.html.erb +2 -2
  76. data/app/views/kiso/components/select/_item.html.erb +2 -2
  77. data/config/locales/en.yml +33 -0
  78. data/lib/generators/kiso/component/USAGE +35 -0
  79. data/lib/generators/kiso/component/component_generator.rb +104 -0
  80. data/lib/generators/kiso/component/templates/partial.html.erb.tt +7 -0
  81. data/lib/generators/kiso/component/templates/sub_part_partial.html.erb.tt +7 -0
  82. data/lib/generators/kiso/component/templates/sub_part_theme.rb.tt +21 -0
  83. data/lib/generators/kiso/component/templates/theme.rb.tt +21 -0
  84. data/lib/kiso/configuration.rb +40 -0
  85. data/lib/kiso/engine.rb +105 -1
  86. data/lib/kiso/presets/rounded.rb +136 -0
  87. data/lib/kiso/presets/sharp.rb +178 -0
  88. data/lib/kiso/presets.rb +49 -0
  89. data/lib/kiso/theme_overrides.rb +5 -1
  90. data/lib/kiso/themes/alert.rb +24 -11
  91. data/lib/kiso/themes/dashboard.rb +1 -1
  92. data/lib/kiso/themes/layout.rb +69 -0
  93. data/lib/kiso/themes/page.rb +295 -0
  94. data/lib/kiso/themes/skeleton.rb +16 -0
  95. data/lib/kiso/version.rb +1 -1
  96. data/lib/kiso.rb +7 -0
  97. metadata +63 -1
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
@@ -5,7 +5,7 @@ module Kiso
5
5
  )
6
6
 
7
7
  DashboardNavbar = ClassVariants.build(
8
- base: "flex items-center gap-3 px-4 border-b border-border bg-background shrink-0 z-[--z-topbar]"
8
+ base: "flex items-center gap-3 px-4 border-b border-border bg-background shrink-0 z-(--z-topbar)"
9
9
  )
10
10
 
11
11
  DashboardNavbarToggle = ClassVariants.build(
@@ -0,0 +1,69 @@
1
+ module Kiso
2
+ module Themes
3
+ # Root application wrapper. Provides base structure and sets
4
+ # `text-foreground` for dark mode inheritance.
5
+ #
6
+ # @example
7
+ # App.render
8
+ #
9
+ # No variants — purely structural.
10
+ App = ClassVariants.build(
11
+ base: "bg-background text-foreground antialiased"
12
+ )
13
+
14
+ # Content containment with consistent max-width and padding.
15
+ #
16
+ # @example
17
+ # Container.render(size: :default)
18
+ #
19
+ # Variants:
20
+ # - +size+ — :narrow, :default, :wide, :full
21
+ #
22
+ # Nuxt UI base: w-full max-w-(--ui-container) mx-auto px-4 sm:px-6 lg:px-8
23
+ Container = ClassVariants.build(
24
+ base: "mx-auto w-full px-4 sm:px-6 lg:px-8",
25
+ variants: {
26
+ size: {
27
+ narrow: "max-w-3xl",
28
+ default: "max-w-7xl",
29
+ wide: "max-w-screen-2xl",
30
+ full: "max-w-full"
31
+ }
32
+ },
33
+ defaults: {size: :default}
34
+ )
35
+
36
+ # Site/app header. Semantic `<header>` element.
37
+ #
38
+ # @example
39
+ # Header.render
40
+ #
41
+ # No variants — purely structural.
42
+ #
43
+ # Nuxt UI base: bg-default/75 backdrop-blur border-b border-default
44
+ # h-(--ui-header-height) sticky top-0 z-50
45
+ Header = ClassVariants.build(
46
+ base: "bg-background/75 backdrop-blur border-b border-border sticky top-0 z-50"
47
+ )
48
+
49
+ # Site/app footer. Semantic `<footer>` element.
50
+ #
51
+ # @example
52
+ # Footer.render
53
+ #
54
+ # No variants — purely structural.
55
+ Footer = ClassVariants.build(
56
+ base: ""
57
+ )
58
+
59
+ # Primary content area wrapper. Semantic `<main>` element.
60
+ #
61
+ # @example
62
+ # Main.render
63
+ #
64
+ # No variants — purely structural.
65
+ Main = ClassVariants.build(
66
+ base: "flex-1"
67
+ )
68
+ end
69
+ end