@hakumi-dev/hakumi-components 0.1.18-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 +208 -381
  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
@@ -13,13 +13,6 @@ const STORAGE_KEY = "hakumi:admin-panel"
13
13
  export default class extends RegistryController {
14
14
  static targets = ["body", "empty", "summary", "search", "typeFilter", "stateFilter"]
15
15
 
16
- static mount() {
17
- Persistence.bootstrap(STORAGE_KEY, ({state}) => {
18
- if(!state.visible) return
19
- window.HakumiComponents.renderComponent("admin_panel")
20
- })
21
- }
22
-
23
16
  setup() {
24
17
  Persistence.save(STORAGE_KEY, { version: 1, state: { visible: true } })
25
18
  this.refresh()
@@ -66,6 +59,11 @@ export default class extends RegistryController {
66
59
  this.applyFilters()
67
60
  }
68
61
 
62
+ toggle(event) {
63
+ if (event) event.preventDefault()
64
+ this.element.classList.toggle("hakumi-admin-panel-collapsed")
65
+ }
66
+
69
67
  close(event) {
70
68
  if (event) event.preventDefault()
71
69
  Persistence.save(STORAGE_KEY, { version: 1, state: { visible: false } })
@@ -51,7 +51,7 @@ export default class extends RegistryController {
51
51
  const scrollTop = this.getScrollTop()
52
52
  const isVisible = scrollTop >= this.visibilityHeightValue
53
53
  this._isVisible = isVisible
54
- this.element.classList.toggle("is-visible", isVisible)
54
+ this.element.classList.toggle("hakumi-float-button-back-top-visible", isVisible)
55
55
  }
56
56
 
57
57
  attachScrollListener() {
@@ -6,15 +6,21 @@ export default class extends RegistryController {
6
6
  }
7
7
 
8
8
  setup() {
9
+ this.originalHref = this.element.getAttribute("href")
10
+ this.initiallyDisabled = this.element.hasAttribute("disabled")
11
+ this.initialAriaDisabled = this.element.getAttribute("aria-disabled")
9
12
  this.boundRipple = this.createRipple.bind(this)
10
- this.element.addEventListener('click', this.boundRipple)
13
+ this.element.addEventListener("click", this.boundRipple)
14
+ this.applyLoadingState()
11
15
  }
12
16
 
13
17
  teardown() {
14
- this.element.removeEventListener('click', this.boundRipple)
18
+ this.element.removeEventListener("click", this.boundRipple)
15
19
  }
16
20
 
17
21
  createRipple(event) {
22
+ if (this.interactionDisabled()) return
23
+
18
24
  const button = event.currentTarget
19
25
  const circle = document.createElement("span")
20
26
  const diameter = Math.max(button.clientWidth, button.clientHeight)
@@ -43,6 +49,7 @@ export default class extends RegistryController {
43
49
  const api = {
44
50
  setLoading: (loading) => {
45
51
  this.loadingValue = Boolean(loading)
52
+ this.applyLoadingState()
46
53
  },
47
54
  isLoading: () => this.loadingValue,
48
55
  ripple: (event) => this.createRipple(event)
@@ -55,4 +62,103 @@ export default class extends RegistryController {
55
62
  api
56
63
  }
57
64
  }
65
+
66
+ loadingValueChanged() {
67
+ this.applyLoadingState()
68
+ }
69
+
70
+ applyLoadingState() {
71
+ if (!this.element) return
72
+
73
+ this.originalHref ??= this.element.getAttribute("href")
74
+ this.initiallyDisabled ??= this.element.hasAttribute("disabled")
75
+ this.initialAriaDisabled ??= this.element.getAttribute("aria-disabled")
76
+
77
+ const loading = this.loadingValue
78
+ this.element.classList.toggle("hakumi-button-loading", loading)
79
+ this.element.setAttribute("aria-busy", String(loading))
80
+
81
+ if (this.element.tagName === "BUTTON") {
82
+ this.element.disabled = this.initiallyDisabled || loading
83
+ } else if (this.element.tagName === "A") {
84
+ this.applyAnchorLoadingState(loading)
85
+ }
86
+
87
+ this.syncLoadingIcon(loading)
88
+ }
89
+
90
+ applyAnchorLoadingState(loading) {
91
+ if (loading) {
92
+ if (this.element.hasAttribute("href")) {
93
+ this.originalHref = this.element.getAttribute("href")
94
+ }
95
+ this.element.removeAttribute("href")
96
+ this.element.setAttribute("role", "button")
97
+ this.element.setAttribute("aria-disabled", "true")
98
+ return
99
+ }
100
+
101
+ if (this.originalHref && !this.initialAriaDisabled) {
102
+ this.element.setAttribute("href", this.originalHref)
103
+ }
104
+
105
+ if (this.initialAriaDisabled) {
106
+ this.element.setAttribute("aria-disabled", this.initialAriaDisabled)
107
+ this.element.setAttribute("role", "button")
108
+ } else {
109
+ this.element.removeAttribute("aria-disabled")
110
+ this.element.removeAttribute("role")
111
+ }
112
+ }
113
+
114
+ syncLoadingIcon(loading) {
115
+ if (loading) {
116
+ this.hideOriginalIcons()
117
+ if (!this.loadingIconElement()) {
118
+ this.element.prepend(this.buildLoadingIcon())
119
+ }
120
+ } else {
121
+ this.loadingIconElement()?.remove()
122
+ this.restoreOriginalIcons()
123
+ }
124
+ }
125
+
126
+ loadingIconElement() {
127
+ return this.element.querySelector(".hakumi-button-loading-icon")
128
+ }
129
+
130
+ buildLoadingIcon() {
131
+ const span = document.createElement("span")
132
+ span.className = "hakumi-icon hakumi-icon-spin hakumi-button-loading-icon"
133
+ span.setAttribute("aria-hidden", "true")
134
+ span.style.color = "currentColor"
135
+ span.innerHTML = `
136
+ <svg viewBox="0 0 1024 1024" focusable="false" width="1em" height="1em" fill="currentColor">
137
+ <path d="M512 64a448 448 0 1 0 448 448h-80a368 368 0 1 1-368-368V64z"></path>
138
+ </svg>
139
+ `
140
+ return span
141
+ }
142
+
143
+ hideOriginalIcons() {
144
+ Array.from(this.element.children).forEach((child) => {
145
+ if (!child.classList.contains("hakumi-icon") || child.classList.contains("hakumi-button-loading-icon")) return
146
+
147
+ child.dataset.hakumiButtonHiddenIcon = "true"
148
+ child.hidden = true
149
+ })
150
+ }
151
+
152
+ restoreOriginalIcons() {
153
+ Array.from(this.element.children).forEach((child) => {
154
+ if (child.dataset.hakumiButtonHiddenIcon !== "true") return
155
+
156
+ child.hidden = false
157
+ delete child.dataset.hakumiButtonHiddenIcon
158
+ })
159
+ }
160
+
161
+ interactionDisabled() {
162
+ return this.loadingValue || this.element.disabled || this.element.getAttribute("aria-disabled") === "true"
163
+ }
58
164
  }
@@ -13,6 +13,7 @@ export default class extends RegistryController {
13
13
  selectedDate: { type: String, default: "" },
14
14
  static: { type: Boolean, default: false },
15
15
  events: { type: Object, default: {} },
16
+ validRange: { type: Array, default: [] },
16
17
  // In week mode, tracks the user's preferred display month
17
18
  // This is set when user clicks a day, and preserved when reopening
18
19
  userPreferredMonth: { type: Number, default: 0 },
@@ -30,36 +31,52 @@ export default class extends RegistryController {
30
31
  monthsFull: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
31
32
  },
32
33
  es: {
33
- daysShort: ["Lu", "Ma", "Mi", "Ju", "Vi", "Sá", "Do"], // ISO week: Lunes primero
34
+ daysShort: ["Lu", "Ma", "Mi", "Ju", "Vi", "Sá", "Do"], // ISO week: Monday first
34
35
  monthsShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"],
35
36
  monthsFull: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
36
37
  }
37
38
  }
38
39
 
39
40
  setup() {
40
- this.#bindNestedEvents()
41
+ this.bindNestedEvents()
41
42
  }
42
43
 
43
44
  teardown() {
45
+ this.unbindNestedEvents()
44
46
  }
45
47
 
46
- #bindNestedEvents() {
48
+ bindNestedEvents() {
47
49
  this.boundChangeYear = this.changeYear.bind(this)
48
50
  this.boundChangeMonth = this.changeMonth.bind(this)
49
51
  this.boundChangeMode = this.changeMode.bind(this)
52
+ this.boundHandleSelectChange = this.handleSelectChange.bind(this)
50
53
 
51
- this.element.addEventListener("hakumi--select:change", (event) => {
52
- const target = event.target
53
- if (target.matches("[data-hakumi--calendar-target='yearSelect']")) {
54
- this.boundChangeYear(event)
55
- } else if (target.matches("[data-hakumi--calendar-target='monthSelect']")) {
56
- this.boundChangeMonth(event)
57
- }
58
- })
59
-
54
+ this.element.addEventListener("hakumi--select:change", this.boundHandleSelectChange)
60
55
  this.element.addEventListener("hakumi--segmented:change", this.boundChangeMode)
61
56
  }
62
57
 
58
+ unbindNestedEvents() {
59
+ if (this.boundHandleSelectChange) {
60
+ this.element.removeEventListener("hakumi--select:change", this.boundHandleSelectChange)
61
+ this.boundHandleSelectChange = null
62
+ }
63
+ if (this.boundChangeMode) {
64
+ this.element.removeEventListener("hakumi--segmented:change", this.boundChangeMode)
65
+ this.boundChangeMode = null
66
+ }
67
+ this.boundChangeYear = null
68
+ this.boundChangeMonth = null
69
+ }
70
+
71
+ handleSelectChange(event) {
72
+ const target = event.target
73
+ if (target.matches("[data-hakumi--calendar-target='yearSelect']")) {
74
+ this.boundChangeYear(event)
75
+ } else if (target.matches("[data-hakumi--calendar-target='monthSelect']")) {
76
+ this.boundChangeMonth(event)
77
+ }
78
+ }
79
+
63
80
  yearValueChanged() {
64
81
  this.#renderCalendar()
65
82
  this.#updateYearSelect()
@@ -447,12 +464,7 @@ export default class extends RegistryController {
447
464
  if (!events[dateStr]) {
448
465
  events[dateStr] = []
449
466
  }
450
- events[dateStr].push({
451
- id: event.id || null,
452
- type: event.type || 'default',
453
- content: event.content || '',
454
- html: event.html || null
455
- })
467
+ events[dateStr].push(this.#normalizeEvent(event))
456
468
 
457
469
  this.eventsValue = events
458
470
  if (!this.staticValue) {
@@ -559,6 +571,10 @@ export default class extends RegistryController {
559
571
  }
560
572
 
561
573
  #formatDateKey(date) {
574
+ if (date === null || date === undefined || date === "") {
575
+ return ""
576
+ }
577
+
562
578
  if (date instanceof Date) {
563
579
  return DateHelper.formatDataDate(date)
564
580
  }
@@ -571,16 +587,20 @@ export default class extends RegistryController {
571
587
  const normalized = {}
572
588
  Object.keys(events).forEach(key => {
573
589
  const dateStr = this.#formatDateKey(key)
574
- normalized[dateStr] = events[key].map(e => ({
575
- id: e.id || null,
576
- type: e.type || 'default',
577
- content: e.content || '',
578
- html: e.html || null
579
- }))
590
+ normalized[dateStr] = events[key].map(e => this.#normalizeEvent(e))
580
591
  })
581
592
  return normalized
582
593
  }
583
594
 
595
+ #normalizeEvent(event) {
596
+ return {
597
+ id: event.id || null,
598
+ type: event.type || 'default',
599
+ content: event.content || '',
600
+ html: event.html || null
601
+ }
602
+ }
603
+
584
604
  #dispatchEventsChange() {
585
605
  this.dispatch("eventsChange", {
586
606
  detail: { events: this.getEvents() }
@@ -623,7 +643,14 @@ export default class extends RegistryController {
623
643
  const today = DateHelper.formatDataDate(new Date())
624
644
 
625
645
  let html = `<table class="hakumi-calendar-table">
626
- <thead class="hakumi-calendar-thead">
646
+ ${this.#renderMonthTableHead(locale, showWeek)}
647
+ ${this.#renderMonthTableBody(weeks, showWeek, today)}
648
+ </table>`
649
+ return html
650
+ }
651
+
652
+ #renderMonthTableHead(locale, showWeek) {
653
+ let html = `<thead class="hakumi-calendar-thead">
627
654
  <tr>`
628
655
 
629
656
  if (showWeek) {
@@ -636,37 +663,62 @@ export default class extends RegistryController {
636
663
  </th>`
637
664
  })
638
665
 
639
- html += `</tr></thead><tbody class="hakumi-calendar-tbody">`
666
+ html += `</tr></thead>`
667
+ return html
668
+ }
669
+
670
+ #renderMonthTableBody(weeks, showWeek, today) {
671
+ let html = `<tbody class="hakumi-calendar-tbody">`
640
672
 
641
673
  weeks.forEach(week => {
642
- const rowClass = week.weekSelected ? "hakumi-calendar-row hakumi-calendar-row-selected" : "hakumi-calendar-row"
643
- html += `<tr class="${rowClass}">`
674
+ html += this.#renderMonthWeekRow(week, showWeek, today)
675
+ })
644
676
 
645
- if (showWeek) {
646
- html += `<td class="hakumi-calendar-cell hakumi-calendar-cell-week" data-action="click->hakumi--calendar#selectWeek">
677
+ html += `</tbody>`
678
+ return html
679
+ }
680
+
681
+ #renderMonthWeekRow(week, showWeek, today) {
682
+ const rowClass = week.weekSelected ? "hakumi-calendar-row hakumi-calendar-row-selected" : "hakumi-calendar-row"
683
+ let html = `<tr class="${rowClass}">`
684
+
685
+ if (showWeek) {
686
+ html += this.#renderWeekNumberCell(week)
687
+ }
688
+
689
+ week.days.forEach(day => {
690
+ html += this.#renderDateCell(day, today)
691
+ })
692
+
693
+ html += `</tr>`
694
+ return html
695
+ }
696
+
697
+ #renderWeekNumberCell(week) {
698
+ return `<td class="hakumi-calendar-cell hakumi-calendar-cell-week" data-action="click->hakumi--calendar#selectWeek">
647
699
  <div class="hakumi-calendar-week-number">${week.weekNumber}</div>
648
700
  </td>`
649
- }
701
+ }
702
+
703
+ #renderDateCell(day, today) {
704
+ const classes = this.#getCellClasses(day, today)
705
+ const eventsHtml = this.#renderEventsForDate(day.dateStr)
706
+ const actions = this.#dateCellActions()
650
707
 
651
- week.days.forEach(day => {
652
- const classes = this.#getCellClasses(day, today)
653
- const eventsHtml = this.#renderEventsForDate(day.dateStr)
654
- const actions = this.rangeStartDateValue && !this.rangeEndDateValue
655
- ? "click->hakumi--calendar#selectDate mouseenter->hakumi--calendar#handleCellHover"
656
- : "click->hakumi--calendar#selectDate"
657
- html += `<td class="${classes}" data-date="${day.dateStr}" data-action="${actions}">
708
+ return `<td class="${classes}" data-date="${day.dateStr}" data-action="${actions}">
658
709
  <div class="hakumi-calendar-date">
659
710
  <div class="hakumi-calendar-date-value">${day.day}</div>
660
711
  ${eventsHtml ? `<div class="hakumi-calendar-date-content">${eventsHtml}</div>` : ''}
661
712
  </div>
662
713
  </td>`
663
- })
714
+ }
664
715
 
665
- html += `</tr>`
666
- })
716
+ #dateCellActions() {
717
+ if (this.rangeStartDateValue && !this.rangeEndDateValue) {
718
+ return "click->hakumi--calendar#selectDate mouseenter->hakumi--calendar#handleCellHover"
719
+ }
667
720
 
668
- html += `</tbody></table>`
669
- return html
721
+ return "click->hakumi--calendar#selectDate"
670
722
  }
671
723
 
672
724
  #renderYearView() {
@@ -710,30 +762,34 @@ export default class extends RegistryController {
710
762
  }
711
763
 
712
764
  #getMonthEventIndicators(year, month) {
713
- const events = this.eventsValue
714
- if (!events || Object.keys(events).length === 0) return ''
765
+ const eventTypes = this.#getMonthEventTypes(year, month)
766
+
767
+ if (eventTypes.size === 0) return ''
768
+
769
+ let html = '<div class="hakumi-calendar-month-events">'
770
+ eventTypes.forEach(type => {
771
+ html += `<span class="hakumi-calendar-month-event-dot hakumi-calendar-month-event-${type}"></span>`
772
+ })
773
+ html += '</div>'
715
774
 
775
+ return html
776
+ }
716
777
 
778
+ #getMonthEventTypes(year, month) {
779
+ const events = this.eventsValue
717
780
  const eventTypes = new Set()
718
-
781
+ if (!events || Object.keys(events).length === 0) return eventTypes
782
+
719
783
  Object.keys(events).forEach(dateStr => {
720
784
  const date = DateHelper.parseDataDate(dateStr)
721
785
  if (date.year() === year && date.month() + 1 === month) {
722
786
  events[dateStr].forEach(event => {
723
- eventTypes.add(event.type || 'default')
787
+ eventTypes.add(this.#eventType(event))
724
788
  })
725
789
  }
726
790
  })
727
791
 
728
- if (eventTypes.size === 0) return ''
729
-
730
- let html = '<div class="hakumi-calendar-month-events">'
731
- eventTypes.forEach(type => {
732
- html += `<span class="hakumi-calendar-month-event-dot hakumi-calendar-month-event-${type}"></span>`
733
- })
734
- html += '</div>'
735
-
736
- return html
792
+ return eventTypes
737
793
  }
738
794
 
739
795
  #renderQuarterView() {
@@ -848,7 +904,8 @@ export default class extends RegistryController {
848
904
  dateStr: DateHelper.formatDataDate(current),
849
905
  day: current.date(),
850
906
  inMonth: current.month() + 1 === month,
851
- weekend: current.day() === 0 || current.day() === 6
907
+ weekend: current.day() === 0 || current.day() === 6,
908
+ disabled: !this.#isDateInValidRange(current)
852
909
  })
853
910
  current = current.add(1, "day")
854
911
  }
@@ -893,59 +950,90 @@ export default class extends RegistryController {
893
950
  classes.push("hakumi-calendar-cell-selected")
894
951
  }
895
952
 
953
+ if (day.disabled) {
954
+ classes.push("hakumi-calendar-cell-disabled")
955
+ }
956
+
896
957
  if (day.weekend && day.dateStr !== this.selectedDateValue) {
897
958
  classes.push("hakumi-calendar-cell-weekend")
898
959
  }
899
960
 
900
- // Range selection support
901
- if (this.rangeStartDateValue || this.rangeEndDateValue) {
902
- const rangeStart = this.rangeStartDateValue ? new Date(this.rangeStartDateValue) : null
903
- const rangeEnd = this.rangeEndDateValue ? new Date(this.rangeEndDateValue) : null
904
- const hovered = this.hoveredDateValue ? new Date(this.hoveredDateValue) : null
905
- const currentDate = new Date(day.dateStr)
906
-
907
- // Mark range start
908
- if (rangeStart && day.dateStr === this.rangeStartDateValue) {
909
- classes.push("hakumi-calendar-cell-range-start")
910
- }
961
+ this.#appendRangeClasses(classes, day.dateStr)
911
962
 
912
- // Mark range end
913
- if (rangeEnd && day.dateStr === this.rangeEndDateValue) {
914
- classes.push("hakumi-calendar-cell-range-end")
915
- }
963
+ return classes.join(" ")
964
+ }
916
965
 
917
- // Mark dates in range (between start and end)
918
- if (rangeStart && rangeEnd) {
919
- if (currentDate > rangeStart && currentDate < rangeEnd) {
920
- classes.push("hakumi-calendar-cell-in-range")
921
- }
922
- }
923
- // Mark hover range (between start and hovered date when no end is selected)
924
- else if (rangeStart && hovered && !rangeEnd) {
925
- if ((currentDate > rangeStart && currentDate <= hovered) ||
926
- (currentDate < rangeStart && currentDate >= hovered)) {
927
- classes.push("hakumi-calendar-cell-in-range")
928
- classes.push("hakumi-calendar-cell-range-hover")
929
- }
930
- }
966
+ #appendRangeClasses(classes, dateStr) {
967
+ if (!this.rangeStartDateValue && !this.rangeEndDateValue) return
968
+
969
+ const range = this.#rangeDates()
970
+ const currentDate = new Date(dateStr)
971
+
972
+ if (range.start && dateStr === this.rangeStartDateValue) {
973
+ classes.push("hakumi-calendar-cell-range-start")
931
974
  }
932
975
 
933
- return classes.join(" ")
976
+ if (range.end && dateStr === this.rangeEndDateValue) {
977
+ classes.push("hakumi-calendar-cell-range-end")
978
+ }
979
+
980
+ if (this.#isDateInSelectedRange(currentDate, range)) {
981
+ classes.push("hakumi-calendar-cell-in-range")
982
+ } else if (this.#isDateInHoverRange(currentDate, range)) {
983
+ classes.push("hakumi-calendar-cell-in-range")
984
+ classes.push("hakumi-calendar-cell-range-hover")
985
+ }
986
+ }
987
+
988
+ #rangeDates() {
989
+ return {
990
+ start: this.rangeStartDateValue ? new Date(this.rangeStartDateValue) : null,
991
+ end: this.rangeEndDateValue ? new Date(this.rangeEndDateValue) : null,
992
+ hovered: this.hoveredDateValue ? new Date(this.hoveredDateValue) : null
993
+ }
994
+ }
995
+
996
+ #isDateInSelectedRange(currentDate, range) {
997
+ return range.start && range.end && currentDate > range.start && currentDate < range.end
998
+ }
999
+
1000
+ #isDateInHoverRange(currentDate, range) {
1001
+ if (!range.start || !range.hovered || range.end) return false
1002
+
1003
+ return (currentDate > range.start && currentDate <= range.hovered) ||
1004
+ (currentDate < range.start && currentDate >= range.hovered)
1005
+ }
1006
+
1007
+ #isDateInValidRange(date) {
1008
+ if (!Array.isArray(this.validRangeValue) || this.validRangeValue.length !== 2) {
1009
+ return true
1010
+ }
1011
+
1012
+ const [startDate, endDate] = this.validRangeValue
1013
+ const current = DateHelper.parseDataDate(DateHelper.formatDataDate(date))
1014
+ const start = startDate ? DateHelper.parseDataDate(startDate) : null
1015
+ const end = endDate ? DateHelper.parseDataDate(endDate) : null
1016
+
1017
+ return (!start || !current.isBefore(start, "day")) && (!end || !current.isAfter(end, "day"))
934
1018
  }
935
1019
 
936
1020
  #renderEventsForDate(dateStr) {
937
1021
  const events = this.eventsValue[dateStr]
938
1022
  if (!events || events.length === 0) return ''
939
1023
 
940
- return events.map(event => {
941
- const type = event.type || 'default'
942
- const content = this.#escapeHtml(event.content)
943
-
1024
+ return events.map(event => this.#renderEvent(event)).join('')
1025
+ }
1026
+
1027
+ #renderEvent(event) {
1028
+ const type = this.#eventType(event)
1029
+ const content = this.#escapeHtml(event.content)
1030
+ const badgeHtml = event.html || this.#renderBadgeHtml(type, content)
1031
+
1032
+ return `<div class="hakumi-calendar-event hakumi-calendar-event-${type}" title="${content}">${badgeHtml}</div>`
1033
+ }
944
1034
 
945
- const badgeHtml = event.html || this.#renderBadgeHtml(type, content)
946
-
947
- return `<div class="hakumi-calendar-event hakumi-calendar-event-${type}" title="${content}">${badgeHtml}</div>`
948
- }).join('')
1035
+ #eventType(event) {
1036
+ return event.type || 'default'
949
1037
  }
950
1038
 
951
1039
  #renderBadgeHtml(type, content) {