@dodlhuat/basix 1.1.1 → 1.2.1

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 (352) hide show
  1. package/README.md +706 -482
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/badge.scss +104 -0
  5. package/css/bottom-sheet.scss +192 -0
  6. package/css/breadcrumb.scss +158 -0
  7. package/css/button.scss +48 -0
  8. package/css/calendar.scss +957 -0
  9. package/css/card.scss +65 -65
  10. package/css/chart.scss +270 -157
  11. package/css/chat-bubbles.scss +134 -68
  12. package/css/chips.scss +109 -19
  13. package/css/colors.scss +32 -32
  14. package/css/context-menu.scss +182 -0
  15. package/css/datepicker.scss +336 -336
  16. package/css/defaults.scss +90 -90
  17. package/css/docs.scss +529 -0
  18. package/css/editor.scss +664 -461
  19. package/css/file-uploader.scss +1 -1
  20. package/css/flyout-menu.scss +361 -361
  21. package/css/form.scss +124 -0
  22. package/css/gallery.scss +65 -6
  23. package/css/grid.scss +41 -40
  24. package/css/group-picker.scss +345 -0
  25. package/css/guitar-chords.css +250 -250
  26. package/css/icons.scss +330 -330
  27. package/css/parameters.scss +3 -3
  28. package/css/placeholder.scss +33 -33
  29. package/css/popover.scss +206 -0
  30. package/css/progress.scss +76 -32
  31. package/css/properties.scss +51 -36
  32. package/css/push-menu.scss +302 -174
  33. package/css/reset.scss +39 -39
  34. package/css/scrollbar.scss +62 -5
  35. package/css/sidebar-nav.scss +92 -0
  36. package/css/spinner.scss +65 -65
  37. package/css/stepper.scss +248 -0
  38. package/css/style.css +4603 -273
  39. package/css/style.css.map +1 -1
  40. package/css/style.min.css +1 -1
  41. package/css/style.scss +51 -39
  42. package/css/table.scss +199 -199
  43. package/css/tabs.scss +154 -123
  44. package/css/timeline.scss +83 -38
  45. package/css/timepicker.scss +100 -5
  46. package/css/toast.scss +81 -81
  47. package/css/typography.scss +194 -161
  48. package/css/virtual-dropdown.scss +35 -29
  49. package/js/bottom-sheet.js +173 -0
  50. package/js/bottom-sheet.ts +222 -0
  51. package/js/calendar.js +532 -0
  52. package/js/calendar.ts +706 -0
  53. package/js/carousel.js +26 -13
  54. package/js/chart.js +573 -257
  55. package/js/chart.ts +692 -0
  56. package/js/code-viewer.js +10 -10
  57. package/js/code-viewer.ts +188 -188
  58. package/js/context-menu.js +212 -0
  59. package/js/context-menu.ts +252 -0
  60. package/js/datepicker.ts +627 -627
  61. package/js/docs-nav.js +204 -0
  62. package/js/dropdown.ts +179 -179
  63. package/js/editor.js +96 -38
  64. package/js/editor.ts +483 -425
  65. package/js/file-uploader.js +1 -0
  66. package/js/file-uploader.ts +1 -0
  67. package/js/flyout-menu.js +14 -14
  68. package/js/flyout-menu.ts +249 -249
  69. package/js/form-builder.js +106 -106
  70. package/js/gallery.js +13 -6
  71. package/js/gallery.ts +245 -236
  72. package/js/group-picker.js +342 -0
  73. package/js/group-picker.ts +447 -0
  74. package/js/guitar-chords.js +268 -268
  75. package/js/lazy-loader.js +121 -121
  76. package/js/modal.ts +166 -166
  77. package/js/popover.js +163 -0
  78. package/js/popover.ts +219 -0
  79. package/js/position.js +108 -0
  80. package/js/position.ts +111 -0
  81. package/js/push-menu.js +226 -113
  82. package/js/push-menu.ts +284 -145
  83. package/js/request.js +50 -50
  84. package/js/scroll.ts +47 -47
  85. package/js/scrollbar.js +13 -0
  86. package/js/scrollbar.ts +324 -307
  87. package/js/select.ts +216 -216
  88. package/js/sidebar-nav.js +41 -0
  89. package/js/sidebar-nav.ts +66 -0
  90. package/js/stepper.js +80 -0
  91. package/js/stepper.ts +104 -0
  92. package/js/table.ts +452 -452
  93. package/js/tabs.ts +279 -279
  94. package/js/theme.js +17 -6
  95. package/js/theme.ts +234 -224
  96. package/js/timepicker.js +21 -8
  97. package/js/toast.ts +137 -137
  98. package/js/tooltip.js +6 -60
  99. package/js/tooltip.ts +184 -251
  100. package/js/tsconfig.json +18 -18
  101. package/js/utils.ts +83 -83
  102. package/js/virtual-dropdown.js +25 -25
  103. package/js/virtual-dropdown.ts +365 -365
  104. package/package.json +39 -39
  105. package/fonts/Outfit-VariableFont_wght.woff +0 -0
  106. package/fonts/material-icons.woff2 +0 -0
  107. package/icons/activity-outline.svg +0 -1
  108. package/icons/alert-circle-outline.svg +0 -1
  109. package/icons/alert-triangle-outline.svg +0 -1
  110. package/icons/archive-outline.svg +0 -1
  111. package/icons/arrow-back-outline.svg +0 -1
  112. package/icons/arrow-circle-down-outline.svg +0 -1
  113. package/icons/arrow-circle-left-outline.svg +0 -1
  114. package/icons/arrow-circle-right-outline.svg +0 -1
  115. package/icons/arrow-circle-up-outline.svg +0 -1
  116. package/icons/arrow-down-outline.svg +0 -1
  117. package/icons/arrow-downward-outline.svg +0 -1
  118. package/icons/arrow-forward-outline.svg +0 -1
  119. package/icons/arrow-ios-back-outline.svg +0 -1
  120. package/icons/arrow-ios-downward-outline.svg +0 -1
  121. package/icons/arrow-ios-forward-outline.svg +0 -1
  122. package/icons/arrow-ios-upward-outline.svg +0 -1
  123. package/icons/arrow-left-outline.svg +0 -1
  124. package/icons/arrow-right-outline.svg +0 -1
  125. package/icons/arrow-up-outline.svg +0 -1
  126. package/icons/arrow-upward-outline.svg +0 -1
  127. package/icons/arrowhead-down-outline.svg +0 -1
  128. package/icons/arrowhead-left-outline.svg +0 -1
  129. package/icons/arrowhead-right-outline.svg +0 -1
  130. package/icons/arrowhead-up-outline.svg +0 -1
  131. package/icons/at-outline.svg +0 -1
  132. package/icons/attach-2-outline.svg +0 -1
  133. package/icons/attach-outline.svg +0 -1
  134. package/icons/award-outline.svg +0 -1
  135. package/icons/backspace-outline.svg +0 -1
  136. package/icons/bar-chart-2-outline.svg +0 -1
  137. package/icons/bar-chart-outline.svg +0 -1
  138. package/icons/battery-outline.svg +0 -1
  139. package/icons/behance-outline.svg +0 -1
  140. package/icons/bell-off-outline.svg +0 -1
  141. package/icons/bell-outline.svg +0 -1
  142. package/icons/bluetooth-outline.svg +0 -1
  143. package/icons/book-open-outline.svg +0 -1
  144. package/icons/book-outline.svg +0 -1
  145. package/icons/bookmark-outline.svg +0 -1
  146. package/icons/briefcase-outline.svg +0 -1
  147. package/icons/browser-outline.svg +0 -1
  148. package/icons/brush-outline.svg +0 -1
  149. package/icons/bulb-outline.svg +0 -1
  150. package/icons/calendar-outline.svg +0 -1
  151. package/icons/camera-outline.svg +0 -1
  152. package/icons/car-outline.svg +0 -1
  153. package/icons/cast-outline.svg +0 -1
  154. package/icons/charging-outline.svg +0 -1
  155. package/icons/checkmark-circle-2-outline.svg +0 -1
  156. package/icons/checkmark-circle-outline.svg +0 -1
  157. package/icons/checkmark-outline.svg +0 -1
  158. package/icons/checkmark-square-2-outline.svg +0 -1
  159. package/icons/checkmark-square-outline.svg +0 -1
  160. package/icons/chevron-down-outline.svg +0 -1
  161. package/icons/chevron-left-outline.svg +0 -1
  162. package/icons/chevron-right-outline.svg +0 -1
  163. package/icons/chevron-up-outline.svg +0 -1
  164. package/icons/clipboard-outline.svg +0 -1
  165. package/icons/clock-outline.svg +0 -1
  166. package/icons/close-circle-outline.svg +0 -1
  167. package/icons/close-outline.svg +0 -1
  168. package/icons/close-square-outline.svg +0 -1
  169. package/icons/cloud-download-outline.svg +0 -1
  170. package/icons/cloud-upload-outline.svg +0 -1
  171. package/icons/code-download-outline.svg +0 -1
  172. package/icons/code-outline.svg +0 -1
  173. package/icons/collapse-outline.svg +0 -1
  174. package/icons/color-palette-outline.svg +0 -1
  175. package/icons/color-picker-outline.svg +0 -1
  176. package/icons/compass-outline.svg +0 -1
  177. package/icons/copy-outline.svg +0 -1
  178. package/icons/corner-down-left-outline.svg +0 -1
  179. package/icons/corner-down-right-outline.svg +0 -1
  180. package/icons/corner-left-down-outline.svg +0 -1
  181. package/icons/corner-left-up-outline.svg +0 -1
  182. package/icons/corner-right-down-outline.svg +0 -1
  183. package/icons/corner-right-up-outline.svg +0 -1
  184. package/icons/corner-up-left-outline.svg +0 -1
  185. package/icons/corner-up-right-outline.svg +0 -1
  186. package/icons/credit-card-outline.svg +0 -1
  187. package/icons/crop-outline.svg +0 -1
  188. package/icons/cube-outline.svg +0 -1
  189. package/icons/diagonal-arrow-left-down-outline.svg +0 -1
  190. package/icons/diagonal-arrow-left-up-outline.svg +0 -1
  191. package/icons/diagonal-arrow-right-down-outline.svg +0 -1
  192. package/icons/diagonal-arrow-right-up-outline.svg +0 -1
  193. package/icons/done-all-outline.svg +0 -1
  194. package/icons/download-outline.svg +0 -1
  195. package/icons/droplet-off-outline.svg +0 -1
  196. package/icons/droplet-outline.svg +0 -1
  197. package/icons/edit-2-outline.svg +0 -1
  198. package/icons/edit-outline.svg +0 -1
  199. package/icons/email-outline.svg +0 -1
  200. package/icons/expand-outline.svg +0 -1
  201. package/icons/external-link-outline.svg +0 -1
  202. package/icons/eye-off-2-outline.svg +0 -1
  203. package/icons/eye-off-outline.svg +0 -1
  204. package/icons/eye-outline.svg +0 -1
  205. package/icons/facebook-outline.svg +0 -1
  206. package/icons/file-add-outline.svg +0 -1
  207. package/icons/file-outline.svg +0 -1
  208. package/icons/file-remove-outline.svg +0 -1
  209. package/icons/file-text-outline.svg +0 -1
  210. package/icons/film-outline.svg +0 -1
  211. package/icons/flag-outline.svg +0 -1
  212. package/icons/flash-off-outline.svg +0 -1
  213. package/icons/flash-outline.svg +0 -1
  214. package/icons/flip-2-outline.svg +0 -1
  215. package/icons/flip-outline.svg +0 -1
  216. package/icons/folder-add-outline.svg +0 -1
  217. package/icons/folder-outline.svg +0 -1
  218. package/icons/folder-remove-outline.svg +0 -1
  219. package/icons/funnel-outline.svg +0 -1
  220. package/icons/gift-outline.svg +0 -1
  221. package/icons/github-outline.svg +0 -1
  222. package/icons/globe-2-outline.svg +0 -1
  223. package/icons/globe-outline.svg +0 -1
  224. package/icons/google-outline.svg +0 -1
  225. package/icons/grid-outline.svg +0 -1
  226. package/icons/hard-drive-outline.svg +0 -1
  227. package/icons/hash-outline.svg +0 -1
  228. package/icons/headphones-outline.svg +0 -1
  229. package/icons/heart-outline.svg +0 -1
  230. package/icons/home-outline.svg +0 -1
  231. package/icons/image-outline.svg +0 -1
  232. package/icons/inbox-outline.svg +0 -1
  233. package/icons/info-outline.svg +0 -1
  234. package/icons/keypad-outline.svg +0 -1
  235. package/icons/layers-outline.svg +0 -1
  236. package/icons/layout-outline.svg +0 -1
  237. package/icons/link-2-outline.svg +0 -1
  238. package/icons/link-outline.svg +0 -1
  239. package/icons/linkedin-outline.svg +0 -1
  240. package/icons/list-outline.svg +0 -1
  241. package/icons/loader-outline.svg +0 -1
  242. package/icons/lock-outline.svg +0 -1
  243. package/icons/log-in-outline.svg +0 -1
  244. package/icons/log-out-outline.svg +0 -1
  245. package/icons/map-outline.svg +0 -1
  246. package/icons/maximize-outline.svg +0 -1
  247. package/icons/menu-2-outline.svg +0 -1
  248. package/icons/menu-arrow-outline.svg +0 -1
  249. package/icons/menu-outline.svg +0 -1
  250. package/icons/message-circle-outline.svg +0 -1
  251. package/icons/message-square-outline.svg +0 -1
  252. package/icons/mic-off-outline.svg +0 -1
  253. package/icons/mic-outline.svg +0 -1
  254. package/icons/minimize-outline.svg +0 -1
  255. package/icons/minus-circle-outline.svg +0 -1
  256. package/icons/minus-outline.svg +0 -1
  257. package/icons/minus-square-outline.svg +0 -1
  258. package/icons/monitor-outline.svg +0 -1
  259. package/icons/moon-outline.svg +0 -1
  260. package/icons/more-horizontal-outline.svg +0 -1
  261. package/icons/more-vertical-outline.svg +0 -1
  262. package/icons/move-outline.svg +0 -1
  263. package/icons/music-outline.svg +0 -1
  264. package/icons/navigation-2-outline.svg +0 -1
  265. package/icons/navigation-outline.svg +0 -1
  266. package/icons/npm-outline.svg +0 -1
  267. package/icons/options-2-outline.svg +0 -1
  268. package/icons/options-outline.svg +0 -1
  269. package/icons/pantone-outline.svg +0 -1
  270. package/icons/paper-plane-outline.svg +0 -1
  271. package/icons/pause-circle-outline.svg +0 -1
  272. package/icons/people-outline.svg +0 -1
  273. package/icons/percent-outline.svg +0 -1
  274. package/icons/person-add-outline.svg +0 -1
  275. package/icons/person-delete-outline.svg +0 -1
  276. package/icons/person-done-outline.svg +0 -1
  277. package/icons/person-outline.svg +0 -1
  278. package/icons/person-remove-outline.svg +0 -1
  279. package/icons/phone-call-outline.svg +0 -1
  280. package/icons/phone-missed-outline.svg +0 -1
  281. package/icons/phone-off-outline.svg +0 -1
  282. package/icons/phone-outline.svg +0 -1
  283. package/icons/pie-chart-outline.svg +0 -1
  284. package/icons/pin-outline.svg +0 -1
  285. package/icons/play-circle-outline.svg +0 -1
  286. package/icons/plus-circle-outline.svg +0 -1
  287. package/icons/plus-outline.svg +0 -1
  288. package/icons/plus-square-outline.svg +0 -1
  289. package/icons/power-outline.svg +0 -1
  290. package/icons/pricetags-outline.svg +0 -1
  291. package/icons/printer-outline.svg +0 -1
  292. package/icons/question-mark-circle-outline.svg +0 -1
  293. package/icons/question-mark-outline.svg +0 -1
  294. package/icons/radio-button-off-outline.svg +0 -1
  295. package/icons/radio-button-on-outline.svg +0 -1
  296. package/icons/radio-outline.svg +0 -1
  297. package/icons/recording-outline.svg +0 -1
  298. package/icons/refresh-outline.svg +0 -1
  299. package/icons/repeat-outline.svg +0 -1
  300. package/icons/rewind-left-outline.svg +0 -1
  301. package/icons/rewind-right-outline.svg +0 -1
  302. package/icons/save-outline.svg +0 -1
  303. package/icons/scissors-outline.svg +0 -1
  304. package/icons/search-outline.svg +0 -1
  305. package/icons/settings-2-outline.svg +0 -1
  306. package/icons/settings-outline.svg +0 -1
  307. package/icons/shake-outline.svg +0 -1
  308. package/icons/share-outline.svg +0 -1
  309. package/icons/shield-off-outline.svg +0 -1
  310. package/icons/shield-outline.svg +0 -1
  311. package/icons/shopping-bag-outline.svg +0 -1
  312. package/icons/shopping-cart-outline.svg +0 -1
  313. package/icons/shuffle-2-outline.svg +0 -1
  314. package/icons/shuffle-outline.svg +0 -1
  315. package/icons/skip-back-outline.svg +0 -1
  316. package/icons/skip-forward-outline.svg +0 -1
  317. package/icons/slash-outline.svg +0 -1
  318. package/icons/smartphone-outline.svg +0 -1
  319. package/icons/smiling-face-outline.svg +0 -1
  320. package/icons/speaker-outline.svg +0 -1
  321. package/icons/square-outline.svg +0 -1
  322. package/icons/star-outline.svg +0 -1
  323. package/icons/stop-circle-outline.svg +0 -1
  324. package/icons/sun-outline.svg +0 -1
  325. package/icons/swap-outline.svg +0 -1
  326. package/icons/sync-outline.svg +0 -1
  327. package/icons/text-outline.svg +0 -1
  328. package/icons/thermometer-minus-outline.svg +0 -1
  329. package/icons/thermometer-outline.svg +0 -1
  330. package/icons/thermometer-plus-outline.svg +0 -1
  331. package/icons/toggle-left-outline.svg +0 -1
  332. package/icons/toggle-right-outline.svg +0 -1
  333. package/icons/trash-2-outline.svg +0 -1
  334. package/icons/trash-outline.svg +0 -1
  335. package/icons/trending-down-outline.svg +0 -1
  336. package/icons/trending-up-outline.svg +0 -1
  337. package/icons/tv-outline.svg +0 -1
  338. package/icons/twitter-outline.svg +0 -1
  339. package/icons/umbrella-outline.svg +0 -1
  340. package/icons/undo-outline.svg +0 -1
  341. package/icons/unlock-outline.svg +0 -1
  342. package/icons/upload-outline.svg +0 -1
  343. package/icons/video-off-outline.svg +0 -1
  344. package/icons/video-outline.svg +0 -1
  345. package/icons/volume-down-outline.svg +0 -1
  346. package/icons/volume-mute-outline.svg +0 -1
  347. package/icons/volume-off-outline.svg +0 -1
  348. package/icons/volume-up-outline.svg +0 -1
  349. package/icons/wifi-off-outline.svg +0 -1
  350. package/icons/wifi-outline.svg +0 -1
  351. package/js/index.js +0 -718
  352. package/js/index.ts +0 -873
package/js/calendar.js ADDED
@@ -0,0 +1,532 @@
1
+ // ============================================================
2
+ // calendar.ts — Basix Calendar Component
3
+ // Integrates with @dodlhuat/basix design tokens & conventions
4
+ // ============================================================
5
+ // -----------------------------------------------------------
6
+ // Date Logic (pure functions, no side effects)
7
+ // -----------------------------------------------------------
8
+ export const CalendarLogic = {
9
+ /**
10
+ * Returns all days to render for a month grid (including leading/trailing
11
+ * days from adjacent months to fill the 7-column grid).
12
+ */
13
+ getMonthGrid(year, month, firstDayOfWeek) {
14
+ const firstOfMonth = new Date(year, month, 1);
15
+ const lastOfMonth = new Date(year, month + 1, 0);
16
+ // Leading days from previous month
17
+ let startDow = firstOfMonth.getDay() - firstDayOfWeek;
18
+ if (startDow < 0)
19
+ startDow += 7;
20
+ const days = [];
21
+ for (let i = startDow; i > 0; i--) {
22
+ days.push(new Date(year, month, 1 - i));
23
+ }
24
+ for (let d = 1; d <= lastOfMonth.getDate(); d++) {
25
+ days.push(new Date(year, month, d));
26
+ }
27
+ // Trailing days to fill remaining cells (always complete the row)
28
+ const remaining = 7 - (days.length % 7);
29
+ if (remaining < 7) {
30
+ for (let i = 1; i <= remaining; i++) {
31
+ days.push(new Date(year, month + 1, i));
32
+ }
33
+ }
34
+ return days;
35
+ },
36
+ /** Returns the 7 dates of the week containing `date`. */
37
+ getWeekDays(date, firstDayOfWeek) {
38
+ const d = new Date(date);
39
+ const dow = d.getDay();
40
+ let diff = dow - firstDayOfWeek;
41
+ if (diff < 0)
42
+ diff += 7;
43
+ d.setDate(d.getDate() - diff);
44
+ return Array.from({ length: 7 }, (_, i) => {
45
+ const day = new Date(d);
46
+ day.setDate(d.getDate() + i);
47
+ return day;
48
+ });
49
+ },
50
+ isSameDay(a, b) {
51
+ return (a.getFullYear() === b.getFullYear() &&
52
+ a.getMonth() === b.getMonth() &&
53
+ a.getDate() === b.getDate());
54
+ },
55
+ isToday(date) {
56
+ return CalendarLogic.isSameDay(date, new Date());
57
+ },
58
+ isCurrentMonth(date, year, month) {
59
+ return date.getFullYear() === year && date.getMonth() === month;
60
+ },
61
+ /** Returns all events that fall (fully or partially) on a given day. */
62
+ getEventsForDay(events, day) {
63
+ const dayStart = new Date(day);
64
+ dayStart.setHours(0, 0, 0, 0);
65
+ const dayEnd = new Date(day);
66
+ dayEnd.setHours(23, 59, 59, 999);
67
+ return events.filter((e) => e.start <= dayEnd && e.end >= dayStart);
68
+ },
69
+ /** Returns only allDay events for a day. */
70
+ getAllDayEvents(events, day) {
71
+ return CalendarLogic.getEventsForDay(events, day).filter((e) => e.allDay);
72
+ },
73
+ /** Returns only timed events for a day. */
74
+ getTimedEvents(events, day) {
75
+ return CalendarLogic.getEventsForDay(events, day).filter((e) => !e.allDay);
76
+ },
77
+ /** Returns top-offset % and height % for a timed event within a day column. */
78
+ getEventPosition(event, day) {
79
+ const dayStart = new Date(day);
80
+ dayStart.setHours(0, 0, 0, 0);
81
+ const dayEnd = new Date(day);
82
+ dayEnd.setHours(24, 0, 0, 0);
83
+ const totalMs = 24 * 60 * 60 * 1000;
84
+ const startMs = Math.max(event.start.getTime(), dayStart.getTime()) - dayStart.getTime();
85
+ const endMs = Math.min(event.end.getTime(), dayEnd.getTime()) - dayStart.getTime();
86
+ return {
87
+ top: (startMs / totalMs) * 100,
88
+ height: Math.max(((endMs - startMs) / totalMs) * 100, 2), // min 2%
89
+ };
90
+ },
91
+ formatTime(date) {
92
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
93
+ },
94
+ };
95
+ // -----------------------------------------------------------
96
+ // Default Locale
97
+ // -----------------------------------------------------------
98
+ const DEFAULT_LOCALE = {
99
+ monthNames: [
100
+ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
101
+ 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
102
+ ],
103
+ dayNamesShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
104
+ dayNamesFull: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
105
+ firstDayOfWeek: 1,
106
+ today: 'Heute',
107
+ month: 'Monat',
108
+ week: 'Woche',
109
+ agenda: 'Agenda',
110
+ allDay: 'Ganztägig',
111
+ noEvents: 'Keine Termine',
112
+ };
113
+ // -----------------------------------------------------------
114
+ // Renderer — builds DOM from CalendarLogic output
115
+ // -----------------------------------------------------------
116
+ export class CalendarRenderer {
117
+ constructor(locale) {
118
+ this.locale = locale;
119
+ }
120
+ /** Ordered day-name headers respecting firstDayOfWeek */
121
+ renderWeekdayHeaders() {
122
+ const { dayNamesShort, firstDayOfWeek } = this.locale;
123
+ const ordered = [
124
+ ...dayNamesShort.slice(firstDayOfWeek),
125
+ ...dayNamesShort.slice(0, firstDayOfWeek),
126
+ ];
127
+ return ordered
128
+ .map((name) => `<div class="cal__weekday" aria-label="${name}">${name}</div>`)
129
+ .join('');
130
+ }
131
+ renderEvent(event, compact = false) {
132
+ const extraClass = event.className ?? '';
133
+ if (compact) {
134
+ return `<div class="cal__event-pill ${extraClass}"
135
+ data-event-id="${event.id}"
136
+ role="button"
137
+ tabindex="0"
138
+ aria-label="${event.title}"
139
+ title="${event.title}">${event.title}</div>`;
140
+ }
141
+ return `<div class="cal__event-pill ${extraClass}"
142
+ data-event-id="${event.id}"
143
+ role="button"
144
+ tabindex="0"
145
+ aria-label="${event.title}, ${CalendarLogic.formatTime(event.start)} – ${CalendarLogic.formatTime(event.end)}"
146
+ title="${event.title}">
147
+ <span class="cal__event-time">${CalendarLogic.formatTime(event.start)}</span>
148
+ ${event.title}
149
+ </div>`;
150
+ }
151
+ renderMonthDay(date, currentMonth, currentYear, events, selectedDate, showOutsideDays) {
152
+ const dayEvents = CalendarLogic.getEventsForDay(events, date);
153
+ const isToday = CalendarLogic.isToday(date);
154
+ const isSelected = selectedDate ? CalendarLogic.isSameDay(date, selectedDate) : false;
155
+ const isOutside = !CalendarLogic.isCurrentMonth(date, currentYear, currentMonth);
156
+ if (isOutside && !showOutsideDays) {
157
+ return `<div class="cal__day cal__day--empty" aria-hidden="true"></div>`;
158
+ }
159
+ const classes = [
160
+ 'cal__day',
161
+ isToday ? 'is-today' : '',
162
+ isSelected ? 'is-selected' : '',
163
+ isOutside ? 'cal__day--outside' : '',
164
+ dayEvents.length > 0 ? 'has-events' : '',
165
+ ]
166
+ .filter(Boolean)
167
+ .join(' ');
168
+ const eventsHtml = dayEvents
169
+ .slice(0, 3)
170
+ .map((e) => this.renderEvent(e, true))
171
+ .join('');
172
+ const moreCount = dayEvents.length - 3;
173
+ const moreHtml = moreCount > 0
174
+ ? `<div class="cal__event-more">+${moreCount}</div>`
175
+ : '';
176
+ return `<div class="${classes}"
177
+ role="gridcell"
178
+ tabindex="0"
179
+ aria-label="${date.toLocaleDateString()}"
180
+ aria-selected="${isSelected}"
181
+ data-date="${date.toISOString()}">
182
+ <span class="cal__day-num">${date.getDate()}</span>
183
+ <div class="cal__day-events">${eventsHtml}${moreHtml}</div>
184
+ </div>`;
185
+ }
186
+ renderMonthView(year, month, events, selectedDate, showOutsideDays, firstDayOfWeek) {
187
+ const days = CalendarLogic.getMonthGrid(year, month, firstDayOfWeek);
188
+ const cells = days
189
+ .map((d) => this.renderMonthDay(d, month, year, events, selectedDate, showOutsideDays))
190
+ .join('');
191
+ return `<div class="cal__month-grid" role="grid" aria-label="${this.locale.monthNames[month]} ${year}">
192
+ ${this.renderWeekdayHeaders()}
193
+ ${cells}
194
+ </div>`;
195
+ }
196
+ renderWeekView(date, events, selectedDate, firstDayOfWeek) {
197
+ const days = CalendarLogic.getWeekDays(date, firstDayOfWeek);
198
+ // All-day row
199
+ const allDayCols = days
200
+ .map((d) => {
201
+ const adEvents = CalendarLogic.getAllDayEvents(events, d);
202
+ const pills = adEvents.map((e) => this.renderEvent(e, true)).join('');
203
+ return `<div class="cal__allday-col">${pills}</div>`;
204
+ })
205
+ .join('');
206
+ // Day column headers
207
+ const headCols = days
208
+ .map((d) => {
209
+ const isToday = CalendarLogic.isToday(d);
210
+ const isSelected = selectedDate ? CalendarLogic.isSameDay(d, selectedDate) : false;
211
+ const classes = [
212
+ 'cal__week-head-day',
213
+ isToday ? 'is-today' : '',
214
+ isSelected ? 'is-selected' : '',
215
+ ]
216
+ .filter(Boolean)
217
+ .join(' ');
218
+ const dow = this.locale.dayNamesShort[(d.getDay() + 7) % 7];
219
+ return `<div class="${classes}" data-date="${d.toISOString()}">
220
+ ${dow}<span>${d.getDate()}</span>
221
+ </div>`;
222
+ })
223
+ .join('');
224
+ // Hour slots + events
225
+ const hourLabels = Array.from({ length: 24 }, (_, h) => {
226
+ const label = h === 0 ? '' : `${String(h).padStart(2, '0')}:00`;
227
+ return `<div class="cal__time-slot">${label}</div>`;
228
+ }).join('');
229
+ const dayCols = days
230
+ .map((d) => {
231
+ const timedEvents = CalendarLogic.getTimedEvents(events, d);
232
+ const hourCells = Array.from({ length: 24 }, () => `<div class="cal__day-col-hour"></div>`).join('');
233
+ const eventOverlays = timedEvents
234
+ .map((e) => {
235
+ const { top, height } = CalendarLogic.getEventPosition(e, d);
236
+ const extraClass = e.className ?? '';
237
+ return `<div class="cal__week-event ${extraClass}"
238
+ style="top:${top.toFixed(2)}%;height:${height.toFixed(2)}%"
239
+ data-event-id="${e.id}"
240
+ role="button"
241
+ tabindex="0"
242
+ aria-label="${e.title}">
243
+ <span class="cal__event-time">${CalendarLogic.formatTime(e.start)}</span>
244
+ ${e.title}
245
+ </div>`;
246
+ })
247
+ .join('');
248
+ return `<div class="cal__day-col" data-date="${d.toISOString()}">${hourCells}${eventOverlays}</div>`;
249
+ })
250
+ .join('');
251
+ return `<div class="cal__week" role="grid">
252
+ <div class="cal__week-head">
253
+ <div class="cal__week-head-time"></div>
254
+ ${headCols}
255
+ </div>
256
+ <div class="cal__allday">
257
+ <div class="cal__allday-label">${this.locale.allDay}</div>
258
+ ${allDayCols}
259
+ </div>
260
+ <div class="cal__week-body">
261
+ <div class="cal__week-grid">
262
+ <div class="cal__time-col">${hourLabels}</div>
263
+ ${dayCols}
264
+ </div>
265
+ </div>
266
+ </div>`;
267
+ }
268
+ renderAgendaView(year, month, events) {
269
+ // Collect all days in this month that have events
270
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
271
+ let html = '';
272
+ for (let d = 1; d <= daysInMonth; d++) {
273
+ const day = new Date(year, month, d);
274
+ const dayEvents = CalendarLogic.getEventsForDay(events, day);
275
+ if (dayEvents.length === 0)
276
+ continue;
277
+ const isToday = CalendarLogic.isToday(day);
278
+ const dow = this.locale.dayNamesFull[day.getDay()];
279
+ html += `<div class="cal__agenda-day ${isToday ? 'is-today' : ''}">
280
+ <div class="cal__agenda-date">
281
+ <span class="cal__agenda-dow">${dow}</span>
282
+ <span class="cal__agenda-num ${isToday ? 'is-today' : ''}">${d}</span>
283
+ </div>
284
+ <div class="cal__agenda-events">
285
+ ${dayEvents.map((e) => `
286
+ <div class="cal__agenda-event ${e.className ?? ''}"
287
+ data-event-id="${e.id}"
288
+ role="button"
289
+ tabindex="0">
290
+ <span class="cal__agenda-event-time">
291
+ ${e.allDay ? this.locale.allDay : CalendarLogic.formatTime(e.start) + ' – ' + CalendarLogic.formatTime(e.end)}
292
+ </span>
293
+ <span class="cal__agenda-event-title">${e.title}</span>
294
+ </div>`).join('')}
295
+ </div>
296
+ </div>`;
297
+ }
298
+ if (!html) {
299
+ html = `<div class="cal__agenda-empty">${this.locale.noEvents}</div>`;
300
+ }
301
+ return `<div class="cal__agenda">${html}</div>`;
302
+ }
303
+ }
304
+ // -----------------------------------------------------------
305
+ // Calendar — main controller class
306
+ // -----------------------------------------------------------
307
+ export class Calendar {
308
+ constructor(options) {
309
+ this.selectedDate = null;
310
+ this.events = [];
311
+ // Resolve container
312
+ if (typeof options.container === 'string') {
313
+ const el = document.querySelector(options.container);
314
+ if (!el)
315
+ throw new Error(`Calendar: container "${options.container}" not found.`);
316
+ this.container = el;
317
+ }
318
+ else {
319
+ this.container = options.container;
320
+ }
321
+ this.locale = { ...DEFAULT_LOCALE, ...(options.locale ?? {}) };
322
+ this.renderer = new CalendarRenderer(this.locale);
323
+ this.options = {
324
+ container: this.container,
325
+ events: options.events ?? [],
326
+ view: options.view ?? 'month',
327
+ locale: options.locale ?? {},
328
+ showOutsideDays: options.showOutsideDays ?? true,
329
+ onDayClick: options.onDayClick ?? (() => { }),
330
+ onEventClick: options.onEventClick ?? (() => { }),
331
+ onChange: options.onChange ?? (() => { }),
332
+ className: options.className ?? '',
333
+ };
334
+ this.events = [...this.options.events];
335
+ this.currentView = this.options.view;
336
+ this.currentDate = new Date();
337
+ this.render();
338
+ this.attachEvents();
339
+ }
340
+ // ----------------------------------------------------------
341
+ // Public API
342
+ // ----------------------------------------------------------
343
+ setView(view) {
344
+ this.currentView = view;
345
+ this.render();
346
+ this.options.onChange(this.currentDate, this.currentView);
347
+ }
348
+ next() {
349
+ if (this.currentView === 'month' || this.currentView === 'agenda') {
350
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 1);
351
+ }
352
+ else {
353
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), this.currentDate.getDate() + 7);
354
+ }
355
+ this.render();
356
+ this.options.onChange(this.currentDate, this.currentView);
357
+ }
358
+ prev() {
359
+ if (this.currentView === 'month' || this.currentView === 'agenda') {
360
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 1);
361
+ }
362
+ else {
363
+ this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), this.currentDate.getDate() - 7);
364
+ }
365
+ this.render();
366
+ this.options.onChange(this.currentDate, this.currentView);
367
+ }
368
+ today() {
369
+ this.currentDate = new Date();
370
+ this.render();
371
+ this.options.onChange(this.currentDate, this.currentView);
372
+ }
373
+ addEvent(event) {
374
+ this.events.push(event);
375
+ this.render();
376
+ }
377
+ removeEvent(id) {
378
+ this.events = this.events.filter((e) => e.id !== id);
379
+ this.render();
380
+ }
381
+ setEvents(events) {
382
+ this.events = [...events];
383
+ this.render();
384
+ }
385
+ getEvents() {
386
+ return [...this.events];
387
+ }
388
+ destroy() {
389
+ this.container.innerHTML = '';
390
+ this.container.removeAttribute('data-cal');
391
+ }
392
+ // ----------------------------------------------------------
393
+ // Internal rendering
394
+ // ----------------------------------------------------------
395
+ getTitle() {
396
+ const { monthNames } = this.locale;
397
+ const y = this.currentDate.getFullYear();
398
+ const m = this.currentDate.getMonth();
399
+ if (this.currentView === 'week') {
400
+ const days = CalendarLogic.getWeekDays(this.currentDate, this.locale.firstDayOfWeek);
401
+ const first = days[0];
402
+ const last = days[6];
403
+ if (first.getMonth() === last.getMonth()) {
404
+ return `${monthNames[first.getMonth()]} ${y}`;
405
+ }
406
+ return `${monthNames[first.getMonth()]} – ${monthNames[last.getMonth()]} ${y}`;
407
+ }
408
+ return `${monthNames[m]} ${y}`;
409
+ }
410
+ buildHeader() {
411
+ const activeMonth = this.currentView === 'month' || this.currentView === 'agenda' ? 'cal__btn--active' : '';
412
+ const activeWeek = this.currentView === 'week' ? 'cal__btn--active' : '';
413
+ const activeAgenda = this.currentView === 'agenda' ? 'cal__btn--active' : '';
414
+ return `<div class="cal__header">
415
+ <div class="cal__nav">
416
+ <button class="cal__btn cal__btn--today" data-action="today" aria-label="${this.locale.today}">${this.locale.today}</button>
417
+ <button class="cal__btn" data-action="prev" aria-label="Zurück">
418
+ <svg class="icon-svg" aria-hidden="true"><use href="svg-icons/icons.svg#chevron_left"/></svg>
419
+ </button>
420
+ <button class="cal__btn" data-action="next" aria-label="Vor">
421
+ <svg class="icon-svg" aria-hidden="true"><use href="svg-icons/icons.svg#chevron_right"/></svg>
422
+ </button>
423
+ </div>
424
+ <h2 class="cal__title" aria-live="polite">${this.getTitle()}</h2>
425
+ <div class="cal__view-toggle" role="group" aria-label="Ansicht wählen">
426
+ <button class="cal__btn ${activeMonth}" data-action="view-month" aria-pressed="${this.currentView === 'month'}">${this.locale.month}</button>
427
+ <button class="cal__btn ${activeWeek}" data-action="view-week" aria-pressed="${this.currentView === 'week'}">${this.locale.week}</button>
428
+ <button class="cal__btn ${activeAgenda}" data-action="view-agenda" aria-pressed="${this.currentView === 'agenda'}">${this.locale.agenda}</button>
429
+ </div>
430
+ </div>`;
431
+ }
432
+ buildBody() {
433
+ const { firstDayOfWeek } = this.locale;
434
+ const y = this.currentDate.getFullYear();
435
+ const m = this.currentDate.getMonth();
436
+ switch (this.currentView) {
437
+ case 'month':
438
+ return this.renderer.renderMonthView(y, m, this.events, this.selectedDate, this.options.showOutsideDays, firstDayOfWeek);
439
+ case 'week':
440
+ return this.renderer.renderWeekView(this.currentDate, this.events, this.selectedDate, firstDayOfWeek);
441
+ case 'agenda':
442
+ return this.renderer.renderAgendaView(y, m, this.events);
443
+ }
444
+ }
445
+ render() {
446
+ const rootClass = ['cal', this.options.className].filter(Boolean).join(' ');
447
+ this.container.setAttribute('data-cal', this.currentView);
448
+ this.container.innerHTML = `<div class="${rootClass}" role="application" aria-label="Kalender">
449
+ ${this.buildHeader()}
450
+ <div class="cal__body">
451
+ ${this.buildBody()}
452
+ </div>
453
+ </div>`;
454
+ }
455
+ // ----------------------------------------------------------
456
+ // Event delegation
457
+ // ----------------------------------------------------------
458
+ attachEvents() {
459
+ this.container.addEventListener('click', (e) => this.handleClick(e));
460
+ this.container.addEventListener('keydown', (e) => this.handleKeydown(e));
461
+ }
462
+ handleClick(e) {
463
+ const target = e.target;
464
+ // Nav / view buttons
465
+ const btn = target.closest('[data-action]');
466
+ if (btn) {
467
+ const action = btn.dataset.action;
468
+ if (action === 'prev')
469
+ this.prev();
470
+ else if (action === 'next')
471
+ this.next();
472
+ else if (action === 'today')
473
+ this.today();
474
+ else if (action === 'view-month')
475
+ this.setView('month');
476
+ else if (action === 'view-week')
477
+ this.setView('week');
478
+ else if (action === 'view-agenda')
479
+ this.setView('agenda');
480
+ return;
481
+ }
482
+ // Event click
483
+ const eventEl = target.closest('[data-event-id]');
484
+ if (eventEl) {
485
+ const id = eventEl.dataset.eventId;
486
+ const event = this.events.find((ev) => ev.id === id);
487
+ if (event) {
488
+ e.stopPropagation();
489
+ this.options.onEventClick(event);
490
+ }
491
+ return;
492
+ }
493
+ // Day click
494
+ const dayEl = target.closest('[data-date]');
495
+ if (dayEl && dayEl.dataset.date) {
496
+ const date = new Date(dayEl.dataset.date);
497
+ this.selectedDate = date;
498
+ this.options.onDayClick(date);
499
+ // Re-render to update selection state
500
+ this.render();
501
+ }
502
+ }
503
+ handleKeydown(e) {
504
+ const target = e.target;
505
+ // Allow Enter/Space to trigger click on focused interactive elements
506
+ if (e.key === 'Enter' || e.key === ' ') {
507
+ if (target.closest('[data-date], [data-event-id], [data-action]')) {
508
+ e.preventDefault();
509
+ target.click();
510
+ }
511
+ }
512
+ // Arrow key navigation within month grid
513
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key))
514
+ return;
515
+ const dayEl = target.closest('.cal__day[data-date]');
516
+ if (!dayEl)
517
+ return;
518
+ e.preventDefault();
519
+ const all = Array.from(this.container.querySelectorAll('.cal__day[data-date]:not(.cal__day--empty)'));
520
+ const idx = all.indexOf(dayEl);
521
+ let next = idx;
522
+ if (e.key === 'ArrowRight')
523
+ next = idx + 1;
524
+ else if (e.key === 'ArrowLeft')
525
+ next = idx - 1;
526
+ else if (e.key === 'ArrowDown')
527
+ next = idx + 7;
528
+ else if (e.key === 'ArrowUp')
529
+ next = idx - 7;
530
+ all[Math.max(0, Math.min(next, all.length - 1))]?.focus();
531
+ }
532
+ }