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.
- checksums.yaml +4 -4
- data/README.md +169 -23
- data/app/assets/javascripts/hakumi_components.js +12 -12
- data/app/assets/stylesheets/hakumi_components.css +1 -1
- data/app/components/hakumi/alert/component.html.erb +12 -8
- data/app/components/hakumi/alert/component.rb +18 -62
- data/app/components/hakumi/base_component.rb +13 -0
- data/app/components/hakumi/card/component.html.erb +14 -22
- data/app/components/hakumi/card/component.rb +38 -31
- data/app/components/hakumi/checkbox/component.html.erb +39 -21
- data/app/components/hakumi/checkbox/component.rb +12 -2
- data/app/components/hakumi/collapse/component.html.erb +2 -2
- data/app/components/hakumi/collapse/component.rb +1 -1
- data/app/components/hakumi/collapse/panel/component.rb +9 -0
- data/app/components/hakumi/color_picker/component.rb +0 -4
- data/app/components/hakumi/drawer/component.html.erb +7 -7
- data/app/components/hakumi/drawer/component.rb +12 -19
- data/app/components/hakumi/input/component.rb +0 -2
- data/app/components/hakumi/input/text_area/component.rb +0 -2
- data/app/components/hakumi/input_number/component.rb +3 -4
- data/app/components/hakumi/mentions/component.rb +0 -1
- data/app/components/hakumi/modal/component.html.erb +40 -0
- data/app/components/hakumi/modal/component.rb +24 -102
- data/app/components/hakumi/modal/confirm/component.html.erb +23 -0
- data/app/components/hakumi/modal/confirm/component.rb +23 -41
- data/app/components/hakumi/modal/error/component.rb +12 -11
- data/app/components/hakumi/modal/info/component.rb +12 -11
- data/app/components/hakumi/modal/success/component.rb +12 -11
- data/app/components/hakumi/modal/warning/component.rb +15 -10
- data/app/components/hakumi/popconfirm/component.html.erb +25 -25
- data/app/components/hakumi/popconfirm/component.rb +11 -27
- data/app/components/hakumi/rate/component.rb +0 -1
- data/app/components/hakumi/segmented/component.rb +0 -4
- data/app/components/hakumi/slider/component.rb +2 -6
- data/app/components/hakumi/statistic/component.rb +0 -4
- data/app/components/hakumi/switch/component.html.erb +4 -0
- data/app/components/hakumi/switch/component.rb +1 -2
- data/app/components/hakumi/table/component.rb +3 -229
- data/app/components/hakumi/table/concerns/columns.rb +1 -1
- data/app/components/hakumi/table/concerns/editable.rb +121 -0
- data/app/components/hakumi/table/concerns/ellipsis.rb +63 -0
- data/app/components/hakumi/table/concerns/fixed_columns.rb +87 -0
- data/app/components/hakumi/transfer/component.rb +0 -4
- data/app/controllers/{hakumi_components → hakumi}/components_controller.rb +2 -2
- data/app/form_builders/hakumi/form_builder.rb +217 -175
- data/app/helpers/hakumi/form_helper.rb +39 -0
- data/app/javascript/hakumi_components/controllers/base/registry_controller.js +83 -3
- data/app/javascript/hakumi_components/controllers/hakumi/affix_controller.js +0 -23
- data/app/javascript/hakumi_components/controllers/hakumi/alert_controller.js +2 -1
- data/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +0 -7
- data/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +1 -6
- data/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +28 -34
- data/app/javascript/hakumi_components/controllers/hakumi/drawer_controller.js +2 -1
- data/app/javascript/hakumi_components/controllers/hakumi/form_item_controller.js +9 -63
- data/app/javascript/hakumi_components/controllers/hakumi/mentions_controller.js +4 -11
- data/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +1 -1
- data/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +4 -20
- data/app/javascript/hakumi_components/controllers/hakumi/notification_controller.js +1 -1
- data/app/javascript/hakumi_components/controllers/hakumi/popconfirm_controller.js +33 -27
- data/app/javascript/hakumi_components/controllers/hakumi/popover_controller.js +2 -23
- data/app/javascript/hakumi_components/controllers/hakumi/qr_code_controller.js +0 -20
- data/app/javascript/hakumi_components/controllers/hakumi/segmented_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/spin_controller.js +1 -19
- data/app/javascript/hakumi_components/controllers/hakumi/statistic_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +48 -74
- data/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +15 -14
- data/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +14 -13
- data/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +24 -1
- data/app/javascript/hakumi_components/controllers/hakumi/time_picker_controller.js +3 -7
- data/app/javascript/hakumi_components/controllers/hakumi/timeline_controller.js +0 -16
- data/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +2 -2
- data/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/tree_select_controller.js +3 -3
- data/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +12 -26
- data/app/javascript/hakumi_components/core/persistence.js +3 -3
- data/app/javascript/hakumi_components/core/render_component.js +3 -1
- data/app/javascript/lib/validation_manager.js +101 -0
- data/app/javascript/stylesheets/_theme-tokens.scss +2 -1
- data/app/javascript/stylesheets/components/_modal.scss +13 -0
- data/app/services/{hakumi_components → hakumi}/component_handler.rb +1 -1
- data/app/services/hakumi/icon/loader.rb +2 -2
- data/app/services/hakumi/illustrations/loader.rb +3 -3
- data/app/views/hakumi/_drawer.html.erb +21 -0
- data/app/views/hakumi/_modal.html.erb +18 -0
- data/lib/hakumi_components/documentation.rb +127 -0
- data/lib/hakumi_components/engine.rb +13 -4
- data/lib/hakumi_components/rails/attribute_introspection.rb +1 -1
- data/lib/hakumi_components/rails/validation_introspection.rb +5 -5
- data/lib/hakumi_components/rails/validation_mapper.rb +484 -0
- data/lib/hakumi_components/rails.rb +2 -1
- data/lib/hakumi_components/version.rb +2 -2
- data/lib/hakumi_components.rb +3 -1
- data/lib/tasks/coverage.rake +37 -0
- data/sig/hakumi/base_component.rbs +5 -0
- data/sig/hakumi/checkbox/component.rbs +10 -0
- data/sig/hakumi/color_picker/component.rbs +0 -1
- data/sig/hakumi/form_builder.rbs +9 -1
- data/sig/{hakumi_components → hakumi}/rails/attribute_introspection.rbs +1 -1
- data/sig/{hakumi_components → hakumi}/rails/validation_introspection.rbs +1 -1
- data/sig/hakumi/rails/validation_mapper.rbs +53 -0
- data/sig/{hakumi_components → hakumi}/rails.rbs +1 -1
- data/sig/hakumi/segmented/component.rbs +0 -1
- data/sig/hakumi/slider/component.rbs +0 -1
- data/sig/hakumi/statistic/component.rbs +0 -2
- data/sig/hakumi/table/component.rbs +3 -4
- data/sig/hakumi/table/concerns/columns.rbs +2 -1
- data/sig/hakumi/table/concerns/editable.rbs +40 -0
- data/sig/hakumi/table/concerns/ellipsis.rbs +27 -0
- data/sig/hakumi/table/concerns/fixed_columns.rbs +33 -0
- data/sig/hakumi/transfer/component.rbs +0 -1
- data/sig/{hakumi_components.rbs → hakumi.rbs} +20 -3
- data/sig/rails/active_model/validations/comparison_validator.rbs +6 -0
- metadata +44 -29
- data/app/views/hakumi_components/_drawer.html.erb +0 -3
- data/app/views/hakumi_components/_modal.html.erb +0 -3
- /data/app/views/{hakumi_components → hakumi}/_admin_panel.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_affix.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_alert.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_confirm.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_message.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_notification.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_popconfirm.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_popover.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_qr_code.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_result.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_segmented.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_skeleton.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_spin.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_statistic.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_table.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_tag.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_timeline.html.erb +0 -0
- /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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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?.
|
|
271
|
+
return this.treeTarget?.hakumiComponent?.api
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
dispatchSelect(detail) {
|
|
275
|
-
this.
|
|
275
|
+
this.dispatch("select", { detail })
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
dispatchChange(detail) {
|
|
279
|
-
this.
|
|
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.
|
|
746
|
-
|
|
747
|
-
|
|
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.
|
|
755
|
-
|
|
756
|
-
|
|
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.
|
|
764
|
-
|
|
765
|
-
|
|
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.
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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?(
|
|
8
|
-
|
|
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?(
|
|
8
|
-
|
|
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
|
|
5
|
+
module Hakumi
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
|
-
isolate_namespace
|
|
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
|
-
|
|
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 "
|
|
34
|
+
initializer "hakumi.views" do |app|
|
|
26
35
|
app.config.paths["app/views"] << root.join("app/views")
|
|
27
36
|
end
|
|
28
37
|
end
|