@hakumi-dev/hakumi-components 0.1.17-pre → 0.1.19-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 (56) hide show
  1. package/README.md +218 -369
  2. package/app/javascript/hakumi_components/controllers/hakumi/admin_panel_controller.js +5 -7
  3. package/app/javascript/hakumi_components/controllers/hakumi/back_top_controller.js +1 -1
  4. package/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +108 -2
  5. package/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +183 -95
  6. package/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +23 -285
  7. package/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +274 -262
  8. package/app/javascript/hakumi_components/controllers/hakumi/float_button_group_controller.js +2 -2
  9. package/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +4 -2
  10. package/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +119 -125
  11. package/app/javascript/hakumi_components/controllers/hakumi/table/editable.js +291 -0
  12. package/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +166 -366
  13. package/app/javascript/hakumi_components/controllers/hakumi/tabs_controller.js +8 -2
  14. package/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +27 -25
  15. package/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +19 -18
  16. package/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +116 -117
  17. package/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +17 -1
  18. package/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +363 -78
  19. package/app/javascript/hakumi_components/controllers/hakumi/typography_controller.js +3 -3
  20. package/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +320 -204
  21. package/app/javascript/hakumi_components/core/render_component.js +37 -11
  22. package/app/javascript/hakumi_components/utils/color_helper.js +262 -0
  23. package/app/javascript/stylesheets/_base.scss +9 -0
  24. package/app/javascript/stylesheets/_hakumi_components.scss +1 -0
  25. package/app/javascript/stylesheets/components/_breadcrumb.scss +2 -2
  26. package/app/javascript/stylesheets/components/_calendar.scss +13 -13
  27. package/app/javascript/stylesheets/components/_cascader.scss +5 -5
  28. package/app/javascript/stylesheets/components/_checkbox.scss +9 -11
  29. package/app/javascript/stylesheets/components/_color_picker.scss +11 -11
  30. package/app/javascript/stylesheets/components/_date_picker.scss +4 -4
  31. package/app/javascript/stylesheets/components/_descriptions.scss +2 -2
  32. package/app/javascript/stylesheets/components/_drawer.scss +3 -3
  33. package/app/javascript/stylesheets/components/_dropdown.scss +2 -2
  34. package/app/javascript/stylesheets/components/_flex.scss +1 -1
  35. package/app/javascript/stylesheets/components/_float_button.scss +5 -5
  36. package/app/javascript/stylesheets/components/_form_item.scss +92 -0
  37. package/app/javascript/stylesheets/components/_image.scss +15 -15
  38. package/app/javascript/stylesheets/components/_input.scss +23 -113
  39. package/app/javascript/stylesheets/components/_layout.scss +27 -26
  40. package/app/javascript/stylesheets/components/_menu.scss +15 -15
  41. package/app/javascript/stylesheets/components/_modal.scss +13 -13
  42. package/app/javascript/stylesheets/components/_notification.scss +3 -3
  43. package/app/javascript/stylesheets/components/_popover.scss +1 -1
  44. package/app/javascript/stylesheets/components/_segmented.scss +3 -3
  45. package/app/javascript/stylesheets/components/_select.scss +6 -6
  46. package/app/javascript/stylesheets/components/_slider.scss +1 -1
  47. package/app/javascript/stylesheets/components/_spin.scss +2 -2
  48. package/app/javascript/stylesheets/components/_steps.scss +10 -10
  49. package/app/javascript/stylesheets/components/_switch.scss +11 -10
  50. package/app/javascript/stylesheets/components/_table.scss +6 -6
  51. package/app/javascript/stylesheets/components/_tag.scss +2 -2
  52. package/app/javascript/stylesheets/components/_tooltip.scss +4 -4
  53. package/app/javascript/stylesheets/components/_tree_select.scss +3 -3
  54. package/app/javascript/stylesheets/components/_typography.scss +3 -3
  55. package/app/javascript/stylesheets/components/_upload.scss +4 -4
  56. package/package.json +2 -2
@@ -335,8 +335,14 @@ export default class extends RegistryController {
335
335
 
336
336
 
337
337
  if (wasActive && this._tabs.length > 0) {
338
- const newIndex = Math.min(tabIndex, this._tabs.length - 1)
339
- this.#activateTabByKey(this._tabs[newIndex].key)
338
+ const candidates = [
339
+ ...this._tabs.slice(tabIndex),
340
+ ...this._tabs.slice(0, tabIndex)
341
+ ]
342
+ const nextTab = candidates.find(tab => !tab.disabled)
343
+ if (nextTab) {
344
+ this.#activateTabByKey(nextTab.key)
345
+ }
340
346
  }
341
347
 
342
348
  this.#syncOverflow()
@@ -1,12 +1,26 @@
1
- import { Controller } from "@hotwired/stimulus"
1
+ import RegistryController from "../base/registry_controller.js"
2
2
 
3
- export default class extends Controller {
3
+ export default class extends RegistryController {
4
4
  static values = {
5
5
  checkable: { type: Boolean, default: false },
6
6
  checked: { type: Boolean, default: false }
7
7
  }
8
8
 
9
- connect() {
9
+ setup() {
10
+ if (this.checkableValue) {
11
+ this.boundHandleClick = this.handleClick.bind(this)
12
+ this.element.addEventListener("click", this.boundHandleClick)
13
+ }
14
+ }
15
+
16
+ teardown() {
17
+ if (this.boundHandleClick) {
18
+ this.element.removeEventListener("click", this.boundHandleClick)
19
+ this.boundHandleClick = null
20
+ }
21
+ }
22
+
23
+ registerApi() {
10
24
  this.element.hakumiComponent = {
11
25
  name: "tag",
12
26
  version: 1,
@@ -19,18 +33,6 @@ export default class extends Controller {
19
33
  isChecked: () => this.isChecked()
20
34
  }
21
35
  }
22
-
23
- if (this.checkableValue) {
24
- this.element.addEventListener("click", this.#handleClick)
25
- }
26
- }
27
-
28
- disconnect() {
29
- delete this.element.hakumiComponent
30
-
31
- if (this.checkableValue) {
32
- this.element.removeEventListener("click", this.#handleClick)
33
- }
34
36
  }
35
37
 
36
38
  close(event) {
@@ -60,8 +62,8 @@ export default class extends Controller {
60
62
  if (!this.checkableValue) return
61
63
 
62
64
  this.checkedValue = !this.checkedValue
63
- this.#updateCheckedState()
64
-
65
+ this.updateCheckedState()
66
+
65
67
  this.dispatch("change", {
66
68
  detail: { checked: this.checkedValue, element: this.element }
67
69
  })
@@ -69,10 +71,10 @@ export default class extends Controller {
69
71
 
70
72
  check() {
71
73
  if (!this.checkableValue || this.checkedValue) return
72
-
74
+
73
75
  this.checkedValue = true
74
- this.#updateCheckedState()
75
-
76
+ this.updateCheckedState()
77
+
76
78
  this.dispatch("change", {
77
79
  detail: { checked: true, element: this.element }
78
80
  })
@@ -80,10 +82,10 @@ export default class extends Controller {
80
82
 
81
83
  uncheck() {
82
84
  if (!this.checkableValue || !this.checkedValue) return
83
-
85
+
84
86
  this.checkedValue = false
85
- this.#updateCheckedState()
86
-
87
+ this.updateCheckedState()
88
+
87
89
  this.dispatch("change", {
88
90
  detail: { checked: false, element: this.element }
89
91
  })
@@ -93,12 +95,12 @@ export default class extends Controller {
93
95
  return this.checkedValue
94
96
  }
95
97
 
96
- #handleClick = (event) => {
98
+ handleClick(event) {
97
99
  if (event.target.closest(".hakumi-tag-close-icon")) return
98
100
  this.toggle()
99
101
  }
100
102
 
101
- #updateCheckedState() {
103
+ updateCheckedState() {
102
104
  this.element.classList.toggle("hakumi-tag-checkable-checked", this.checkedValue)
103
105
  }
104
106
  }
@@ -1,19 +1,25 @@
1
- import { Controller } from "@hotwired/stimulus"
1
+ import RegistryController from "../base/registry_controller.js"
2
2
  import Sortable from "sortablejs"
3
3
 
4
- export default class extends Controller {
4
+ export default class extends RegistryController {
5
5
  static values = {
6
6
  sortable: { type: Boolean, default: false },
7
7
  animation: { type: Number, default: 150 }
8
8
  }
9
9
 
10
- connect() {
10
+ setup() {
11
11
  this._sortable = null
12
12
 
13
13
  if (this.sortableValue) {
14
- this.#setupSortable()
14
+ this.setupSortable()
15
15
  }
16
+ }
17
+
18
+ teardown() {
19
+ this.destroySortable()
20
+ }
16
21
 
22
+ registerApi() {
17
23
  this.element.hakumiComponent = {
18
24
  name: "tag_group",
19
25
  version: 1,
@@ -28,16 +34,11 @@ export default class extends Controller {
28
34
  }
29
35
  }
30
36
 
31
- disconnect() {
32
- this.#destroySortable()
33
- delete this.element.hakumiComponent
34
- }
35
-
36
37
  sortableValueChanged() {
37
38
  if (this.sortableValue) {
38
- this.#setupSortable()
39
+ this.setupSortable()
39
40
  } else {
40
- this.#destroySortable()
41
+ this.destroySortable()
41
42
  }
42
43
  }
43
44
 
@@ -61,7 +62,7 @@ export default class extends Controller {
61
62
  return this.getTags().map(tag => tag.textContent.trim())
62
63
  }
63
64
 
64
- #setupSortable() {
65
+ setupSortable() {
65
66
  if (this._sortable) return
66
67
 
67
68
  this._sortable = Sortable.create(this.element, {
@@ -74,19 +75,19 @@ export default class extends Controller {
74
75
  this.dispatch("sortStart", { detail: { item: evt.item, oldIndex: evt.oldIndex } })
75
76
  },
76
77
  onEnd: (evt) => {
77
- this.dispatch("sortEnd", {
78
- detail: {
79
- item: evt.item,
80
- oldIndex: evt.oldIndex,
78
+ this.dispatch("sortEnd", {
79
+ detail: {
80
+ item: evt.item,
81
+ oldIndex: evt.oldIndex,
81
82
  newIndex: evt.newIndex,
82
83
  values: this.getTagValues()
83
- }
84
+ }
84
85
  })
85
86
  }
86
87
  })
87
88
  }
88
89
 
89
- #destroySortable() {
90
+ destroySortable() {
90
91
  if (this._sortable) {
91
92
  this._sortable.destroy()
92
93
  this._sortable = null
@@ -2,136 +2,135 @@ import RegistryController from "../base/registry_controller.js"
2
2
  import { Persistence } from "../../core/persistence.js"
3
3
 
4
4
  export default class extends RegistryController {
5
- static values = {
6
- default: { type: String, default: "light" },
7
- storageKey: { type: String, default: "theme" },
8
- syncSandbox: { type: Boolean, default: true },
5
+ static values = {
6
+ default: { type: String, default: "light" },
7
+ storageKey: { type: String, default: "theme" },
8
+ syncSandbox: { type: Boolean, default: true },
9
+ }
10
+
11
+ static targets = ["sandboxFrame"]
12
+
13
+ static mount() {
14
+ Persistence.ensureHost({ id: "theme", controllerName: "hakumi--theme" })
15
+ }
16
+
17
+ setup() {
18
+ if (window.__hakumiThemeActive && window.__hakumiThemeActive !== this.element.id) return
19
+ window.__hakumiThemeActive = this.element.id
20
+
21
+ this._mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
22
+
23
+ // Check if there's an existing explicit theme set in HTML
24
+ const existingTheme = document.documentElement.getAttribute("data-theme")
25
+
26
+ this.boundHandleSystemThemeChange = (e) => {
27
+ const currentTheme = document.documentElement.getAttribute("data-theme")
28
+ // If theme is "auto", don't change the attribute - let CSS media query handle it
29
+ // Only dispatch event for sync purposes
30
+ if (currentTheme === "auto") {
31
+ const effectiveTheme = e.matches ? "dark" : "light"
32
+ window.dispatchEvent(new CustomEvent("theme-changed", { detail: { theme: effectiveTheme } }))
33
+ if (this.syncSandboxValue) this.syncSandboxFrames(effectiveTheme)
34
+ return
35
+ }
36
+ // For non-auto themes, only update if no stored preference
37
+ if (!Persistence.storage.get(this.storageKeyValue)) {
38
+ this.setTheme(e.matches ? "dark" : "light")
39
+ }
9
40
  }
10
41
 
11
- static targets = ["sandboxFrame"]
42
+ this._mediaQuery.addEventListener("change", this.boundHandleSystemThemeChange)
12
43
 
13
- static mount() {
14
- Persistence.ensureHost({ id: "theme", controllerName: "hakumi--theme" })
44
+ if (this.syncSandboxValue) {
45
+ this.sandboxFrameTargets.forEach((frame) => {
46
+ frame.addEventListener("load", this.handleSandboxLoad)
47
+ })
15
48
  }
16
49
 
17
- setup() {
18
- if (window.__hakumiThemeActive && window.__hakumiThemeActive !== this.element.id) return
19
- window.__hakumiThemeActive = this.element.id
20
-
21
- this._mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
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
-
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
38
- if (!Persistence.storage.get(this.storageKeyValue)) {
39
- this.setTheme(e.matches ? "dark" : "light")
40
- }
41
- }
42
-
43
- this._mediaQuery.addEventListener("change", this._handleSystemThemeChange)
44
-
45
- if (this.syncSandboxValue) {
46
- this.sandboxFrameTargets.forEach((frame) => {
47
- frame.addEventListener("load", this.handleSandboxLoad)
48
- })
49
- }
50
-
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
- }
50
+ // Only set theme if there's no explicit theme
51
+ // If "auto", don't change the attribute - CSS media query handles it
52
+ if (!existingTheme) {
53
+ this.setTheme(this.detectInitialTheme())
54
+ } else if (existingTheme === "auto") {
55
+ // For "auto", just dispatch event and sync sandboxes without changing attribute
56
+ const effectiveTheme = this._mediaQuery.matches ? "dark" : "light"
57
+ window.dispatchEvent(new CustomEvent("theme-changed", { detail: { theme: effectiveTheme } }))
58
+ if (this.syncSandboxValue) this.syncSandboxFrames(effectiveTheme)
61
59
  }
60
+ }
62
61
 
63
- teardown() {
64
- if (window.__hakumiThemeActive === this.element.id) delete window.__hakumiThemeActive
62
+ teardown() {
63
+ if (window.__hakumiThemeActive === this.element.id) delete window.__hakumiThemeActive
65
64
 
66
- if (this._mediaQuery && this._handleSystemThemeChange) {
67
- this._mediaQuery.removeEventListener("change", this._handleSystemThemeChange)
68
- }
69
-
70
- if (this.syncSandboxValue) {
71
- this.sandboxFrameTargets.forEach((frame) => {
72
- frame.removeEventListener("load", this.handleSandboxLoad)
73
- })
74
- }
75
- }
76
-
77
- registerApi() {
78
- const api = {
79
- get: () => this.getTheme(),
80
- set: (theme) => this.setTheme(theme),
81
- toggle: () => this.toggle(),
82
- clearStored: () => Persistence.storage.remove(this.storageKeyValue),
83
- detect: () => this.detectInitialTheme(),
84
- }
85
-
86
- this.element.hakumiComponent = {
87
- name: "theme",
88
- version: 1,
89
- singleton: true,
90
- api,
91
- apply: (params = {}) => {
92
- if (params.theme) this.setTheme(params.theme)
93
- },
94
- }
65
+ if (this._mediaQuery && this.boundHandleSystemThemeChange) {
66
+ this._mediaQuery.removeEventListener("change", this.boundHandleSystemThemeChange)
95
67
  }
96
68
 
97
- toggle() {
98
- const current = this.getTheme()
99
- const next = current === "dark" ? "light" : "dark"
100
- this.setTheme(next)
101
- Persistence.storage.set(this.storageKeyValue, next)
69
+ if (this.syncSandboxValue) {
70
+ this.sandboxFrameTargets.forEach((frame) => {
71
+ frame.removeEventListener("load", this.handleSandboxLoad)
72
+ })
102
73
  }
103
-
104
- getTheme() {
105
- return document.documentElement.getAttribute("data-theme") || this.defaultValue
106
- }
107
-
108
- setTheme(theme) {
109
- document.documentElement.setAttribute("data-theme", theme)
110
- window.dispatchEvent(new CustomEvent("theme-changed", { detail: { theme } }))
111
- if (this.syncSandboxValue) this.syncSandboxFrames(theme)
74
+ }
75
+
76
+ registerApi() {
77
+ const api = {
78
+ get: () => this.getTheme(),
79
+ set: (theme) => this.setTheme(theme),
80
+ toggle: () => this.toggle(),
81
+ clearStored: () => Persistence.storage.remove(this.storageKeyValue),
82
+ detect: () => this.detectInitialTheme(),
112
83
  }
113
84
 
114
- detectInitialTheme() {
115
- const savedTheme = Persistence.storage.get(this.storageKeyValue)
116
- if (savedTheme) return savedTheme
117
- if (this._mediaQuery?.matches) return "dark"
118
- return this.defaultValue
85
+ this.element.hakumiComponent = {
86
+ name: "theme",
87
+ version: 1,
88
+ singleton: true,
89
+ api,
90
+ apply: (params = {}) => {
91
+ if (params.theme) this.setTheme(params.theme)
92
+ },
119
93
  }
120
-
121
- syncSandboxFrames(theme) {
122
- this._currentTheme = theme
123
- this.sandboxFrameTargets.forEach((frame) => this.postThemeToFrame(frame, theme))
124
- }
125
-
126
- handleSandboxLoad = (event) => {
127
- const frame = event.currentTarget
128
- const theme = this._currentTheme || this.getTheme()
129
- this.postThemeToFrame(frame, theme)
130
- }
131
-
132
- postThemeToFrame(frame, theme) {
133
- if (frame?.contentWindow) {
134
- frame.contentWindow.postMessage({ theme }, "*")
135
- }
94
+ }
95
+
96
+ toggle() {
97
+ const current = this.getTheme()
98
+ const next = current === "dark" ? "light" : "dark"
99
+ this.setTheme(next)
100
+ Persistence.storage.set(this.storageKeyValue, next)
101
+ }
102
+
103
+ getTheme() {
104
+ return document.documentElement.getAttribute("data-theme") || this.defaultValue
105
+ }
106
+
107
+ setTheme(theme) {
108
+ document.documentElement.setAttribute("data-theme", theme)
109
+ window.dispatchEvent(new CustomEvent("theme-changed", { detail: { theme } }))
110
+ if (this.syncSandboxValue) this.syncSandboxFrames(theme)
111
+ }
112
+
113
+ detectInitialTheme() {
114
+ const savedTheme = Persistence.storage.get(this.storageKeyValue)
115
+ if (savedTheme) return savedTheme
116
+ if (this._mediaQuery?.matches) return "dark"
117
+ return this.defaultValue
118
+ }
119
+
120
+ syncSandboxFrames(theme) {
121
+ this._currentTheme = theme
122
+ this.sandboxFrameTargets.forEach((frame) => this.postThemeToFrame(frame, theme))
123
+ }
124
+
125
+ handleSandboxLoad = (event) => {
126
+ const frame = event.currentTarget
127
+ const theme = this._currentTheme || this.getTheme()
128
+ this.postThemeToFrame(frame, theme)
129
+ }
130
+
131
+ postThemeToFrame(frame, theme) {
132
+ if (frame?.contentWindow) {
133
+ frame.contentWindow.postMessage({ theme }, "*")
136
134
  }
135
+ }
137
136
  }
@@ -27,6 +27,7 @@ export default class extends RegistryController {
27
27
  }
28
28
 
29
29
  setup() {
30
+ this.#syncHiddenInputs()
30
31
  this.#updateCounts()
31
32
  this.#syncSelectAll("source")
32
33
  this.#syncSelectAll("target")
@@ -71,6 +72,7 @@ export default class extends RegistryController {
71
72
  if (item.parentElement !== destination) {
72
73
  destination.appendChild(item)
73
74
  }
75
+ this.#syncHiddenInput(item, targetSet.has(key))
74
76
  this.#setItemSelected(item, false)
75
77
  })
76
78
 
@@ -176,6 +178,7 @@ export default class extends RegistryController {
176
178
  items.forEach((item) => {
177
179
  this.#setItemSelected(item, false)
178
180
  this.#listContainer(to).appendChild(item)
181
+ this.#syncHiddenInput(item, to === "target")
179
182
  })
180
183
 
181
184
  this.targetKeysValue = this.getTargetKeys()
@@ -211,7 +214,8 @@ export default class extends RegistryController {
211
214
  }
212
215
 
213
216
  #isItemDisabled(item) {
214
- return item.dataset.disabled === "true" || item.querySelector("input")?.disabled
217
+ const checkbox = item.querySelector(".hakumi-checkbox-input")
218
+ return item.dataset.disabled === "true" || checkbox?.disabled
215
219
  }
216
220
 
217
221
  #isItemSelected(item) {
@@ -279,6 +283,18 @@ export default class extends RegistryController {
279
283
  return Array.from(container.querySelectorAll(".hakumi-transfer-list-item"))
280
284
  }
281
285
 
286
+ #syncHiddenInputs() {
287
+ this.#listItems("source").forEach((item) => this.#syncHiddenInput(item, false))
288
+ this.#listItems("target").forEach((item) => this.#syncHiddenInput(item, true))
289
+ }
290
+
291
+ #syncHiddenInput(item, enabled) {
292
+ const hiddenInput = item.querySelector("[data-hakumi--transfer-hidden-input='true']")
293
+ if (!hiddenInput) return
294
+
295
+ hiddenInput.disabled = !enabled
296
+ }
297
+
282
298
  #listKeys(list) {
283
299
  return this.#listItems(list).map((item) => item.dataset.key)
284
300
  }