hakumi_components 0.1.16.pre → 0.1.17.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +169 -23
  3. data/app/assets/javascripts/hakumi_components.js +12 -12
  4. data/app/assets/stylesheets/hakumi_components.css +1 -1
  5. data/app/components/hakumi/alert/component.html.erb +12 -8
  6. data/app/components/hakumi/alert/component.rb +18 -62
  7. data/app/components/hakumi/base_component.rb +13 -0
  8. data/app/components/hakumi/card/component.html.erb +14 -22
  9. data/app/components/hakumi/card/component.rb +38 -31
  10. data/app/components/hakumi/checkbox/component.html.erb +39 -21
  11. data/app/components/hakumi/checkbox/component.rb +12 -2
  12. data/app/components/hakumi/collapse/component.html.erb +2 -2
  13. data/app/components/hakumi/collapse/component.rb +1 -1
  14. data/app/components/hakumi/collapse/panel/component.rb +9 -0
  15. data/app/components/hakumi/color_picker/component.rb +0 -4
  16. data/app/components/hakumi/drawer/component.html.erb +7 -7
  17. data/app/components/hakumi/drawer/component.rb +12 -19
  18. data/app/components/hakumi/input/component.rb +0 -2
  19. data/app/components/hakumi/input/text_area/component.rb +0 -2
  20. data/app/components/hakumi/input_number/component.rb +3 -4
  21. data/app/components/hakumi/mentions/component.rb +0 -1
  22. data/app/components/hakumi/modal/component.html.erb +40 -0
  23. data/app/components/hakumi/modal/component.rb +24 -102
  24. data/app/components/hakumi/modal/confirm/component.html.erb +23 -0
  25. data/app/components/hakumi/modal/confirm/component.rb +23 -41
  26. data/app/components/hakumi/modal/error/component.rb +12 -11
  27. data/app/components/hakumi/modal/info/component.rb +12 -11
  28. data/app/components/hakumi/modal/success/component.rb +12 -11
  29. data/app/components/hakumi/modal/warning/component.rb +15 -10
  30. data/app/components/hakumi/popconfirm/component.html.erb +25 -25
  31. data/app/components/hakumi/popconfirm/component.rb +11 -27
  32. data/app/components/hakumi/rate/component.rb +0 -1
  33. data/app/components/hakumi/segmented/component.rb +0 -4
  34. data/app/components/hakumi/slider/component.rb +2 -6
  35. data/app/components/hakumi/statistic/component.rb +0 -4
  36. data/app/components/hakumi/switch/component.html.erb +4 -0
  37. data/app/components/hakumi/switch/component.rb +1 -2
  38. data/app/components/hakumi/table/component.rb +3 -229
  39. data/app/components/hakumi/table/concerns/columns.rb +1 -1
  40. data/app/components/hakumi/table/concerns/editable.rb +121 -0
  41. data/app/components/hakumi/table/concerns/ellipsis.rb +63 -0
  42. data/app/components/hakumi/table/concerns/fixed_columns.rb +87 -0
  43. data/app/components/hakumi/transfer/component.rb +0 -4
  44. data/app/controllers/{hakumi_components → hakumi}/components_controller.rb +2 -2
  45. data/app/form_builders/hakumi/form_builder.rb +217 -175
  46. data/app/helpers/hakumi/form_helper.rb +39 -0
  47. data/app/javascript/hakumi_components/controllers/base/registry_controller.js +83 -3
  48. data/app/javascript/hakumi_components/controllers/hakumi/affix_controller.js +0 -23
  49. data/app/javascript/hakumi_components/controllers/hakumi/alert_controller.js +2 -1
  50. data/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +0 -7
  51. data/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +0 -2
  52. data/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +1 -6
  53. data/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +28 -34
  54. data/app/javascript/hakumi_components/controllers/hakumi/drawer_controller.js +2 -1
  55. data/app/javascript/hakumi_components/controllers/hakumi/form_item_controller.js +9 -63
  56. data/app/javascript/hakumi_components/controllers/hakumi/mentions_controller.js +4 -11
  57. data/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +1 -1
  58. data/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +4 -20
  59. data/app/javascript/hakumi_components/controllers/hakumi/notification_controller.js +1 -1
  60. data/app/javascript/hakumi_components/controllers/hakumi/popconfirm_controller.js +33 -27
  61. data/app/javascript/hakumi_components/controllers/hakumi/popover_controller.js +2 -23
  62. data/app/javascript/hakumi_components/controllers/hakumi/qr_code_controller.js +0 -20
  63. data/app/javascript/hakumi_components/controllers/hakumi/segmented_controller.js +0 -2
  64. data/app/javascript/hakumi_components/controllers/hakumi/spin_controller.js +1 -19
  65. data/app/javascript/hakumi_components/controllers/hakumi/statistic_controller.js +0 -2
  66. data/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +48 -74
  67. data/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +15 -14
  68. data/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +14 -13
  69. data/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +24 -1
  70. data/app/javascript/hakumi_components/controllers/hakumi/time_picker_controller.js +3 -7
  71. data/app/javascript/hakumi_components/controllers/hakumi/timeline_controller.js +0 -16
  72. data/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +2 -2
  73. data/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +0 -2
  74. data/app/javascript/hakumi_components/controllers/hakumi/tree_select_controller.js +3 -3
  75. data/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +12 -26
  76. data/app/javascript/hakumi_components/core/persistence.js +3 -3
  77. data/app/javascript/hakumi_components/core/render_component.js +3 -1
  78. data/app/javascript/lib/validation_manager.js +101 -0
  79. data/app/javascript/stylesheets/_theme-tokens.scss +2 -1
  80. data/app/javascript/stylesheets/components/_modal.scss +13 -0
  81. data/app/services/{hakumi_components → hakumi}/component_handler.rb +1 -1
  82. data/app/services/hakumi/icon/loader.rb +2 -2
  83. data/app/services/hakumi/illustrations/loader.rb +3 -3
  84. data/app/views/hakumi/_drawer.html.erb +21 -0
  85. data/app/views/hakumi/_modal.html.erb +18 -0
  86. data/lib/hakumi_components/documentation.rb +127 -0
  87. data/lib/hakumi_components/engine.rb +13 -4
  88. data/lib/hakumi_components/rails/attribute_introspection.rb +1 -1
  89. data/lib/hakumi_components/rails/validation_introspection.rb +5 -5
  90. data/lib/hakumi_components/rails/validation_mapper.rb +484 -0
  91. data/lib/hakumi_components/rails.rb +2 -1
  92. data/lib/hakumi_components/version.rb +2 -2
  93. data/lib/hakumi_components.rb +3 -1
  94. data/lib/tasks/coverage.rake +37 -0
  95. data/sig/hakumi/base_component.rbs +5 -0
  96. data/sig/hakumi/checkbox/component.rbs +10 -0
  97. data/sig/hakumi/color_picker/component.rbs +0 -1
  98. data/sig/hakumi/form_builder.rbs +9 -1
  99. data/sig/{hakumi_components → hakumi}/rails/attribute_introspection.rbs +1 -1
  100. data/sig/{hakumi_components → hakumi}/rails/validation_introspection.rbs +1 -1
  101. data/sig/hakumi/rails/validation_mapper.rbs +53 -0
  102. data/sig/{hakumi_components → hakumi}/rails.rbs +1 -1
  103. data/sig/hakumi/segmented/component.rbs +0 -1
  104. data/sig/hakumi/slider/component.rbs +0 -1
  105. data/sig/hakumi/statistic/component.rbs +0 -2
  106. data/sig/hakumi/table/component.rbs +3 -4
  107. data/sig/hakumi/table/concerns/columns.rbs +2 -1
  108. data/sig/hakumi/table/concerns/editable.rbs +40 -0
  109. data/sig/hakumi/table/concerns/ellipsis.rbs +27 -0
  110. data/sig/hakumi/table/concerns/fixed_columns.rbs +33 -0
  111. data/sig/hakumi/transfer/component.rbs +0 -1
  112. data/sig/{hakumi_components.rbs → hakumi.rbs} +20 -3
  113. data/sig/rails/active_model/validations/comparison_validator.rbs +6 -0
  114. metadata +44 -29
  115. data/app/views/hakumi_components/_drawer.html.erb +0 -3
  116. data/app/views/hakumi_components/_modal.html.erb +0 -3
  117. /data/app/views/{hakumi_components → hakumi}/_admin_panel.html.erb +0 -0
  118. /data/app/views/{hakumi_components → hakumi}/_affix.html.erb +0 -0
  119. /data/app/views/{hakumi_components → hakumi}/_alert.html.erb +0 -0
  120. /data/app/views/{hakumi_components → hakumi}/_confirm.html.erb +0 -0
  121. /data/app/views/{hakumi_components → hakumi}/_message.html.erb +0 -0
  122. /data/app/views/{hakumi_components → hakumi}/_notification.html.erb +0 -0
  123. /data/app/views/{hakumi_components → hakumi}/_popconfirm.html.erb +0 -0
  124. /data/app/views/{hakumi_components → hakumi}/_popover.html.erb +0 -0
  125. /data/app/views/{hakumi_components → hakumi}/_qr_code.html.erb +0 -0
  126. /data/app/views/{hakumi_components → hakumi}/_result.html.erb +0 -0
  127. /data/app/views/{hakumi_components → hakumi}/_segmented.html.erb +0 -0
  128. /data/app/views/{hakumi_components → hakumi}/_skeleton.html.erb +0 -0
  129. /data/app/views/{hakumi_components → hakumi}/_spin.html.erb +0 -0
  130. /data/app/views/{hakumi_components → hakumi}/_statistic.html.erb +0 -0
  131. /data/app/views/{hakumi_components → hakumi}/_table.html.erb +0 -0
  132. /data/app/views/{hakumi_components → hakumi}/_tag.html.erb +0 -0
  133. /data/app/views/{hakumi_components → hakumi}/_timeline.html.erb +0 -0
  134. /data/app/views/{hakumi_components → hakumi}/_tree.html.erb +0 -0
@@ -20,7 +20,21 @@ export default class extends RegistryController {
20
20
 
21
21
  this._mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
22
22
 
23
+ // Check if there's an existing explicit theme set in HTML
24
+ const existingTheme = document.documentElement.getAttribute("data-theme")
25
+ const shouldAutoDetect = !existingTheme || existingTheme === "auto"
26
+
23
27
  this._handleSystemThemeChange = (e) => {
28
+ const currentTheme = document.documentElement.getAttribute("data-theme")
29
+ // If theme is "auto", don't change the attribute - let CSS media query handle it
30
+ // Only dispatch event for sync purposes
31
+ if (currentTheme === "auto") {
32
+ const effectiveTheme = e.matches ? "dark" : "light"
33
+ window.dispatchEvent(new CustomEvent("theme-changed", { detail: { theme: effectiveTheme } }))
34
+ if (this.syncSandboxValue) this.syncSandboxFrames(effectiveTheme)
35
+ return
36
+ }
37
+ // For non-auto themes, only update if no stored preference
24
38
  if (!Persistence.storage.get(this.storageKeyValue)) {
25
39
  this.setTheme(e.matches ? "dark" : "light")
26
40
  }
@@ -34,7 +48,16 @@ export default class extends RegistryController {
34
48
  })
35
49
  }
36
50
 
37
- this.setTheme(this.detectInitialTheme())
51
+ // Only set theme if there's no explicit theme
52
+ // If "auto", don't change the attribute - CSS media query handles it
53
+ if (!existingTheme) {
54
+ this.setTheme(this.detectInitialTheme())
55
+ } else if (existingTheme === "auto") {
56
+ // For "auto", just dispatch event and sync sandboxes without changing attribute
57
+ const effectiveTheme = this._mediaQuery.matches ? "dark" : "light"
58
+ window.dispatchEvent(new CustomEvent("theme-changed", { detail: { theme: effectiveTheme } }))
59
+ if (this.syncSandboxValue) this.syncSandboxFrames(effectiveTheme)
60
+ }
38
61
  }
39
62
 
40
63
  teardown() {
@@ -270,8 +270,7 @@ export default class extends RegistryController {
270
270
  }
271
271
 
272
272
  dispatchChange(value, parts) {
273
- this.element.dispatchEvent(new CustomEvent("hakumi:time-picker:change", {
274
- bubbles: true,
273
+ this.dispatch("change", {
275
274
  detail: {
276
275
  value,
277
276
  hour: parts.hour,
@@ -279,13 +278,10 @@ export default class extends RegistryController {
279
278
  second: parts.second,
280
279
  period: parts.period
281
280
  }
282
- }))
281
+ })
283
282
  }
284
283
 
285
284
  dispatchOpenChange(open) {
286
- this.element.dispatchEvent(new CustomEvent("hakumi:time-picker:open-change", {
287
- bubbles: true,
288
- detail: { open }
289
- }))
285
+ this.dispatch("openChange", { detail: { open } })
290
286
  }
291
287
  }
@@ -22,13 +22,6 @@ export default class extends RegistryController {
22
22
  toggleReverse: () => this.toggleReverse()
23
23
  }
24
24
  }
25
-
26
- this.#exposeApi()
27
- }
28
-
29
- disconnect() {
30
- delete this.element.hakumiTimeline
31
- super.disconnect()
32
25
  }
33
26
 
34
27
  setReverse(value) {
@@ -76,13 +69,4 @@ export default class extends RegistryController {
76
69
 
77
70
  ordered.forEach((item) => this.itemsContainer.appendChild(item))
78
71
  }
79
-
80
- #exposeApi() {
81
- this.element.hakumiTimeline = {
82
- getItems: () => this.getItems(),
83
- getReverse: () => this.reverseValue,
84
- setReverse: (value) => this.setReverse(value),
85
- toggleReverse: () => this.toggleReverse()
86
- }
87
- }
88
72
  }
@@ -301,7 +301,7 @@ export default class extends RegistryController {
301
301
  direction
302
302
  }
303
303
 
304
- this.element.dispatchEvent(new CustomEvent("hakumi:transfer:change", { detail, bubbles: true }))
304
+ this.dispatch("change", { detail })
305
305
  }
306
306
 
307
307
  #dispatchSelectChange() {
@@ -310,6 +310,6 @@ export default class extends RegistryController {
310
310
  targetSelectedKeys: this.#selectedKeys("target")
311
311
  }
312
312
 
313
- this.element.dispatchEvent(new CustomEvent("hakumi:transfer:select-change", { detail, bubbles: true }))
313
+ this.dispatch("selectChange", { detail })
314
314
  }
315
315
  }
@@ -51,7 +51,6 @@ export default class extends RegistryController {
51
51
  }
52
52
 
53
53
  disconnect() {
54
- delete this.element.hakumiTree
55
54
  super.disconnect()
56
55
  }
57
56
 
@@ -81,7 +80,6 @@ export default class extends RegistryController {
81
80
  api
82
81
  }
83
82
 
84
- this.element.hakumiTree = api
85
83
  }
86
84
 
87
85
  buildNodeMap() {
@@ -268,14 +268,14 @@ export default class extends RegistryController {
268
268
  }
269
269
 
270
270
  treeApi() {
271
- return this.treeTarget?.hakumiTree || this.treeTarget?.hakumiComponent?.api
271
+ return this.treeTarget?.hakumiComponent?.api
272
272
  }
273
273
 
274
274
  dispatchSelect(detail) {
275
- this.element.dispatchEvent(new CustomEvent("hakumi:tree-select:select", { detail, bubbles: true }))
275
+ this.dispatch("select", { detail })
276
276
  }
277
277
 
278
278
  dispatchChange(detail) {
279
- this.element.dispatchEvent(new CustomEvent("hakumi:tree-select:change", { detail, bubbles: true }))
279
+ this.dispatch("change", { detail })
280
280
  }
281
281
  }
@@ -742,31 +742,21 @@ export default class extends RegistryController {
742
742
  }
743
743
 
744
744
  dispatchChange(file) {
745
- this.element.dispatchEvent(
746
- new CustomEvent("hakumi:upload:change", {
747
- bubbles: true,
748
- detail: { file, fileList: this.getFileList() }
749
- })
750
- )
745
+ this.dispatch("change", {
746
+ detail: { file, fileList: this.getFileList() }
747
+ })
751
748
  }
752
749
 
753
750
  dispatchProgress(file, event) {
754
- this.element.dispatchEvent(
755
- new CustomEvent("hakumi:upload:progress", {
756
- bubbles: true,
757
- detail: { file: file ? { ...file } : null, fileList: this.getFileList(), event }
758
- })
759
- )
751
+ this.dispatch("progress", {
752
+ detail: { file: file ? { ...file } : null, fileList: this.getFileList(), event }
753
+ })
760
754
  }
761
755
 
762
756
  dispatchSuccess(file) {
763
- this.element.dispatchEvent(
764
- new CustomEvent("hakumi:upload:success", {
765
- bubbles: true,
766
- detail: { file: file ? { ...file } : null, fileList: this.getFileList() }
767
- })
768
- )
769
-
757
+ this.dispatch("success", {
758
+ detail: { file: file ? { ...file } : null, fileList: this.getFileList() }
759
+ })
770
760
 
771
761
  if (window.HakumiComponents?.renderComponent && file?.name) {
772
762
  window.HakumiComponents.renderComponent("message", {
@@ -781,13 +771,9 @@ export default class extends RegistryController {
781
771
  }
782
772
 
783
773
  dispatchError(file, error) {
784
- this.element.dispatchEvent(
785
- new CustomEvent("hakumi:upload:error", {
786
- bubbles: true,
787
- detail: { file: file ? { ...file } : null, fileList: this.getFileList(), error }
788
- })
789
- )
790
-
774
+ this.dispatch("error", {
775
+ detail: { file: file ? { ...file } : null, fileList: this.getFileList(), error }
776
+ })
791
777
 
792
778
  if (window.HakumiComponents?.renderComponent) {
793
779
  const errorMsg = error || file?.error || "Upload failed"
@@ -21,7 +21,7 @@ const json = {
21
21
  },
22
22
  }
23
23
 
24
- // --- navegación (Turbo + fallbacks), bind único global al módulo ---
24
+ // Navigation events (Turbo + fallbacks), single global binding per module
25
25
  let navBound = false
26
26
  const navCallbacks = new Set()
27
27
 
@@ -51,7 +51,7 @@ const bindNavigationOnce = () => {
51
51
  window.addEventListener("hashchange", scheduleNavCallbacks)
52
52
  }
53
53
 
54
- // Dedupe por key: evita múltiples restores activos si mount() corre más de una vez
54
+ // Dedupe by key: prevents multiple active restores if mount() runs more than once
55
55
  const bootstrapsByKey = new Map() // key -> unsubscribe
56
56
 
57
57
  export const Persistence = {
@@ -80,7 +80,7 @@ export const Persistence = {
80
80
  throw new Error("[HakumiComponents] Persistence.bootstrap(key, fn) expects a function")
81
81
  }
82
82
 
83
- // dedupe
83
+ // Dedupe: return existing unsubscribe if already bootstrapped
84
84
  const existing = bootstrapsByKey.get(key)
85
85
  if (existing) return existing
86
86
 
@@ -149,7 +149,9 @@ export const renderComponent = async (
149
149
  element.remove()
150
150
  unregister(id)
151
151
  }
152
- element.addEventListener("hakumi-component:hidden", handleHidden, { once: true })
152
+ // Listen for component-specific hidden event (e.g., hakumi--modal:hidden)
153
+ const controllerName = name.replace(/_/g, "-")
154
+ element.addEventListener(`hakumi--${controllerName}:hidden`, handleHidden, { once: true })
153
155
  }
154
156
 
155
157
  await nextFrame()
@@ -106,6 +106,107 @@ export default class ValidationManager {
106
106
  }
107
107
  },
108
108
 
109
+ comparison: (value, rule, formData = {}) => {
110
+ if (value === null || value === undefined || value === '') return { valid: true }
111
+
112
+ const config = rule.value || rule.comparison || {}
113
+ const operator = config.operator
114
+ if (!operator) return { valid: true }
115
+
116
+ let targetValue
117
+ if (config.field) {
118
+ targetValue = formData[config.field]
119
+ } else if (config.value !== undefined) {
120
+ targetValue = config.value
121
+ } else {
122
+ return { valid: true }
123
+ }
124
+
125
+ if (targetValue === null || targetValue === undefined || targetValue === '') {
126
+ return { valid: true }
127
+ }
128
+
129
+ const normalize = (raw) => {
130
+ if (raw instanceof Date) {
131
+ return { type: 'date', value: raw.getTime() }
132
+ }
133
+
134
+ if (typeof raw === 'number') {
135
+ return { type: 'number', value: raw }
136
+ }
137
+
138
+ const text = String(raw).trim()
139
+ if (text === '') return { type: 'empty', value: text }
140
+
141
+ const numeric = Number(text)
142
+ if (!Number.isNaN(numeric) && /^-?\d+(\.\d+)?$/.test(text)) {
143
+ return { type: 'number', value: numeric }
144
+ }
145
+
146
+ const parsedDate = Date.parse(text)
147
+ if (!Number.isNaN(parsedDate) && /[-/T:]/.test(text)) {
148
+ return { type: 'date', value: parsedDate }
149
+ }
150
+
151
+ return { type: 'string', value: text }
152
+ }
153
+
154
+ const left = normalize(value)
155
+ const right = normalize(targetValue)
156
+
157
+ if (left.type === 'empty' || right.type === 'empty') return { valid: true }
158
+
159
+ const compareValues = (a, b) => {
160
+ if (a.type === 'number' && b.type === 'number') {
161
+ return a.value === b.value ? 0 : (a.value > b.value ? 1 : -1)
162
+ }
163
+
164
+ if (a.type === 'date' && b.type === 'date') {
165
+ return a.value === b.value ? 0 : (a.value > b.value ? 1 : -1)
166
+ }
167
+
168
+ if (a.value === b.value) return 0
169
+ return a.value > b.value ? 1 : -1
170
+ }
171
+
172
+ const result = compareValues(left, right)
173
+ let valid
174
+
175
+ switch (operator) {
176
+ case 'greater_than':
177
+ valid = result > 0
178
+ break
179
+ case 'greater_than_or_equal_to':
180
+ valid = result >= 0
181
+ break
182
+ case 'less_than':
183
+ valid = result < 0
184
+ break
185
+ case 'less_than_or_equal_to':
186
+ valid = result <= 0
187
+ break
188
+ case 'equal_to':
189
+ valid = result === 0
190
+ break
191
+ case 'other_than':
192
+ valid = result !== 0
193
+ break
194
+ default:
195
+ valid = true
196
+ break
197
+ }
198
+
199
+ const label = config.label || config.field || config.value
200
+ const fallbackMessage = label
201
+ ? `Must be ${operator.replace(/_/g, ' ')} ${label}`
202
+ : 'Comparison failed'
203
+
204
+ return {
205
+ valid,
206
+ message: rule.message || fallbackMessage
207
+ }
208
+ },
209
+
109
210
  custom: (value, rule) => {
110
211
  if (typeof rule.validator !== 'function') {
111
212
  console.error('Custom validator must be a function')
@@ -252,8 +252,9 @@
252
252
 
253
253
 
254
254
 
255
+ // Auto-detect theme from OS preference (opt-in with data-theme="auto")
255
256
  @media (prefers-color-scheme: dark) {
256
- :root:not([data-theme]) {
257
+ :root[data-theme="auto"] {
257
258
 
258
259
  --color-primary: #6366f1;
259
260
  --color-primary-hover: #818cf8;
@@ -207,4 +207,17 @@
207
207
  transform: scale(0.2);
208
208
  transition: opacity 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
209
209
  transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
210
+ }
211
+
212
+ // Modal Confirm variant
213
+ .hakumi-modal-confirm-body-wrapper {
214
+ display: flex;
215
+ align-items: flex-start;
216
+ gap: 12px;
217
+ }
218
+
219
+ .hakumi-modal-confirm-content {
220
+ p {
221
+ margin: 0;
222
+ }
210
223
  }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module HakumiComponents
3
+ module Hakumi
4
4
  class ComponentHandler
5
5
  def initialize(component, params)
6
6
  @component = component.to_sym
@@ -4,8 +4,8 @@ module Hakumi
4
4
  module Icon
5
5
  # Loads Ant Design icons from lib/hakumi_components/app/assets/icons
6
6
  class Loader
7
- ICONS_BASE_PATH = if defined?(HakumiComponents::Engine)
8
- HakumiComponents::Engine.root.join("app", "assets", "icons")
7
+ ICONS_BASE_PATH = if defined?(Hakumi::Engine)
8
+ Hakumi::Engine.root.join("app", "assets", "icons")
9
9
  else
10
10
  Rails.root.join("app", "assets", "icons")
11
11
  end
@@ -4,8 +4,8 @@ module Hakumi
4
4
  module Illustrations
5
5
  # Loads SVG illustrations stored under lib/hakumi_components/app/assets/illustrations
6
6
  class Loader
7
- ILLUSTRATIONS_PATH = if defined?(HakumiComponents::Engine)
8
- HakumiComponents::Engine.root.join("app", "assets", "illustrations")
7
+ ILLUSTRATIONS_PATH = if defined?(Hakumi::Engine)
8
+ Hakumi::Engine.root.join("app", "assets", "illustrations")
9
9
  else
10
10
  Rails.root.join("lib", "hakumi_components", "app", "assets", "illustrations")
11
11
  end
@@ -33,7 +33,7 @@ module Hakumi
33
33
  svg = File.read(path)
34
34
  svg.respond_to?(:html_safe) ? svg.html_safe : svg
35
35
  rescue => e
36
- Rails.logger.error("Error loading illustration #{filename}: #{e.message}")
36
+ ::Rails.logger.error("Error loading illustration #{filename}: #{e.message}")
37
37
  nil
38
38
  end
39
39
  end
@@ -0,0 +1,21 @@
1
+ <%= render Hakumi::Drawer::Component.new(
2
+ id: local_assigns[:id],
3
+ open: local_assigns[:open],
4
+ placement: local_assigns[:placement] || :right,
5
+ size: local_assigns[:size] || :default,
6
+ width: local_assigns[:width],
7
+ height: local_assigns[:height],
8
+ closable: local_assigns.fetch(:closable, true),
9
+ mask: local_assigns.fetch(:mask, true),
10
+ mask_closable: local_assigns.fetch(:mask_closable, true),
11
+ keyboard: local_assigns.fetch(:keyboard, true),
12
+ destroy_on_close: local_assigns.fetch(:destroy_on_close, false)
13
+ ) do |drawer| %>
14
+ <% if local_assigns[:title].present? %>
15
+ <% drawer.with_header { local_assigns[:title] } %>
16
+ <% end %>
17
+
18
+ <% if local_assigns[:message].present? %>
19
+ <p><%= local_assigns[:message] %></p>
20
+ <% end %>
21
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <%
2
+ # Extract modal-specific params (title, message) from locals
3
+ title = local_assigns[:title]
4
+ message = local_assigns[:message]
5
+
6
+ # Pass remaining params to Modal::Component
7
+ component_params = local_assigns.except(:title, :message).compact
8
+ %>
9
+
10
+ <%= render(Hakumi::Modal::Component.new(**component_params)) do |modal| %>
11
+ <% if title.present? %>
12
+ <% modal.with_header { title } %>
13
+ <% end %>
14
+
15
+ <% if message.present? %>
16
+ <p><%= message %></p>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Hakumi
6
+ # Provides access to component documentation and examples.
7
+ # Documentation lives in app/components/hakumi/[component]/docs/ and includes:
8
+ # - meta.yml: Component metadata, API documentation, and example definitions
9
+ # - examples/*.erb: Example partials that demonstrate component usage
10
+ #
11
+ # NOTE: Documentation files are NOT included in the published gem.
12
+ # This module is primarily useful for:
13
+ # - Development playgrounds
14
+ # - Documentation generators
15
+ # - IDE tooling
16
+ #
17
+ # @example List all documented components
18
+ # Hakumi::Documentation.components
19
+ # # => ["button", "alert", "modal", ...]
20
+ #
21
+ # @example Get component metadata
22
+ # Hakumi::Documentation.metadata("button")
23
+ # # => { "name" => "Button", "description" => "...", "api" => [...] }
24
+ #
25
+ # @example Get example content
26
+ # Hakumi::Documentation.example("button", "basic_types")
27
+ # # => "<%= render(Hakumi::Button::Component.new..."
28
+ #
29
+ module Documentation
30
+ class << self
31
+ # Root path for component files
32
+ # @return [Pathname]
33
+ def components_root
34
+ @components_root ||= Pathname.new(File.expand_path("../../app/components/hakumi", __dir__.to_s))
35
+ end
36
+
37
+ # List all components that have documentation
38
+ # @return [Array<String>] component names
39
+ def components
40
+ return [] unless components_root.exist?
41
+
42
+ components_root.children
43
+ .select { |path| path.directory? && (path / "docs" / "meta.yml").exist? }
44
+ .map { |path| path.basename.to_s }
45
+ .sort
46
+ end
47
+
48
+ # Get metadata for a component
49
+ # @param component_name [String] the component name (e.g., "button")
50
+ # @return [Hash, nil] parsed meta.yml content or nil if not found
51
+ def metadata(component_name)
52
+ meta_path = meta_path_for(component_name)
53
+ return nil unless meta_path.exist?
54
+
55
+ YAML.load_file(meta_path)
56
+ end
57
+
58
+ # Get list of examples for a component
59
+ # @param component_name [String] the component name
60
+ # @return [Array<Hash>] example definitions from meta.yml
61
+ def examples(component_name)
62
+ meta = metadata(component_name)
63
+ return [] unless meta
64
+
65
+ meta["examples"] || []
66
+ end
67
+
68
+ # Get the content of an example partial
69
+ # @param component_name [String] the component name
70
+ # @param example_name [String] the example name (without underscore or extension)
71
+ # @return [String, nil] the example ERB content or nil if not found
72
+ def example(component_name, example_name)
73
+ example_path = example_path_for(component_name, example_name)
74
+ return nil unless example_path
75
+
76
+ example_path.read
77
+ end
78
+
79
+ # Get the path to an example partial (for rendering in Rails)
80
+ # @param component_name [String] the component name
81
+ # @param example_name [String] the example name
82
+ # @return [Pathname, nil] the path to the example file or nil if not found
83
+ def example_path_for(component_name, example_name)
84
+ path = components_root / component_name / "docs" / "examples" / "_#{example_name}.html.erb"
85
+ path.exist? ? path : nil
86
+ end
87
+
88
+ # Get the API documentation for a component
89
+ # @param component_name [String] the component name
90
+ # @return [Array<Hash>] API property definitions
91
+ def api(component_name)
92
+ meta = metadata(component_name)
93
+ return [] unless meta
94
+
95
+ meta["api"] || []
96
+ end
97
+
98
+ # Get all documentation as a hash (useful for JSON export)
99
+ # @return [Hash] all component documentation
100
+ def all
101
+ result = Hash.new
102
+ components.each do |name|
103
+ result[name] = {
104
+ metadata: metadata(name),
105
+ examples: examples(name).map do |ex|
106
+ ex.merge("content" => example(name, ex["name"]))
107
+ end
108
+ }
109
+ end
110
+ result
111
+ end
112
+
113
+ # Export all documentation to JSON
114
+ # @return [String] JSON representation
115
+ def to_json(*_args)
116
+ require "json"
117
+ JSON.pretty_generate(all)
118
+ end
119
+
120
+ private
121
+
122
+ def meta_path_for(component_name)
123
+ components_root / component_name / "docs" / "meta.yml"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -2,9 +2,9 @@
2
2
 
3
3
  require "pathname"
4
4
 
5
- module HakumiComponents
5
+ module Hakumi
6
6
  class Engine < ::Rails::Engine
7
- isolate_namespace HakumiComponents
7
+ isolate_namespace Hakumi
8
8
 
9
9
  root = Pathname.new(File.expand_path("../..", __dir__ || "."))
10
10
  config.root = root
@@ -15,14 +15,23 @@ module HakumiComponents
15
15
  config.autoload_paths << root.join("app/form_builders")
16
16
  config.eager_load_paths << root.join("app/form_builders")
17
17
 
18
- initializer "hakumi_components.assets" do |app|
18
+ config.autoload_paths << root.join("app/helpers")
19
+ config.eager_load_paths << root.join("app/helpers")
20
+
21
+ initializer "hakumi.helpers" do
22
+ ActiveSupport.on_load(:action_view) do
23
+ include Hakumi::FormHelper
24
+ end
25
+ end
26
+
27
+ initializer "hakumi.assets" do |app|
19
28
  # Only add assets path if Sprockets is available (Rails < 8 or when explicitly added)
20
29
  if app.config.respond_to?(:assets)
21
30
  app.config.assets.paths << root.join("app/assets")
22
31
  end
23
32
  end
24
33
 
25
- initializer "hakumi_components.views" do |app|
34
+ initializer "hakumi.views" do |app|
26
35
  app.config.paths["app/views"] << root.join("app/views")
27
36
  end
28
37
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module HakumiComponents
3
+ module Hakumi
4
4
  module Rails
5
5
  # Introspects ActiveRecord schema and I18n translations to automatically
6
6
  # configure form field labels, placeholders, and hints