@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.ts ADDED
@@ -0,0 +1,706 @@
1
+ // ============================================================
2
+ // calendar.ts — Basix Calendar Component
3
+ // Integrates with @dodlhuat/basix design tokens & conventions
4
+ // ============================================================
5
+
6
+ // -----------------------------------------------------------
7
+ // Types & Interfaces
8
+ // -----------------------------------------------------------
9
+
10
+ export interface CalendarEvent {
11
+ id: string;
12
+ title: string;
13
+ start: Date;
14
+ end: Date;
15
+ allDay?: boolean;
16
+ /** Extra CSS class — use Basix badge/alert classes e.g. "badge-success" */
17
+ className?: string;
18
+ }
19
+
20
+ export type CalendarView = 'month' | 'week' | 'agenda';
21
+
22
+ export interface CalendarLocale {
23
+ monthNames: string[];
24
+ dayNamesShort: string[];
25
+ dayNamesFull: string[];
26
+ /** 0 = Sunday, 1 = Monday */
27
+ firstDayOfWeek: number;
28
+ today: string;
29
+ month: string;
30
+ week: string;
31
+ agenda: string;
32
+ allDay: string;
33
+ noEvents: string;
34
+ }
35
+
36
+ export interface CalendarOptions {
37
+ /** Target container element or CSS selector */
38
+ container: HTMLElement | string;
39
+ events?: CalendarEvent[];
40
+ view?: CalendarView;
41
+ locale?: Partial<CalendarLocale>;
42
+ /** Show days outside the current month in month view */
43
+ showOutsideDays?: boolean;
44
+ /** Callback when a day cell is clicked */
45
+ onDayClick?: (date: Date) => void;
46
+ /** Callback when an event is clicked */
47
+ onEventClick?: (event: CalendarEvent) => void;
48
+ /** Callback when view or date changes */
49
+ onChange?: (date: Date, view: CalendarView) => void;
50
+ /** Extra CSS class injected on the root .cal element */
51
+ className?: string;
52
+ }
53
+
54
+ // -----------------------------------------------------------
55
+ // Date Logic (pure functions, no side effects)
56
+ // -----------------------------------------------------------
57
+
58
+ export const CalendarLogic = {
59
+ /**
60
+ * Returns all days to render for a month grid (including leading/trailing
61
+ * days from adjacent months to fill the 7-column grid).
62
+ */
63
+ getMonthGrid(year: number, month: number, firstDayOfWeek: number): Date[] {
64
+ const firstOfMonth = new Date(year, month, 1);
65
+ const lastOfMonth = new Date(year, month + 1, 0);
66
+
67
+ // Leading days from previous month
68
+ let startDow = firstOfMonth.getDay() - firstDayOfWeek;
69
+ if (startDow < 0) startDow += 7;
70
+
71
+ const days: Date[] = [];
72
+
73
+ for (let i = startDow; i > 0; i--) {
74
+ days.push(new Date(year, month, 1 - i));
75
+ }
76
+ for (let d = 1; d <= lastOfMonth.getDate(); d++) {
77
+ days.push(new Date(year, month, d));
78
+ }
79
+
80
+ // Trailing days to fill remaining cells (always complete the row)
81
+ const remaining = 7 - (days.length % 7);
82
+ if (remaining < 7) {
83
+ for (let i = 1; i <= remaining; i++) {
84
+ days.push(new Date(year, month + 1, i));
85
+ }
86
+ }
87
+
88
+ return days;
89
+ },
90
+
91
+ /** Returns the 7 dates of the week containing `date`. */
92
+ getWeekDays(date: Date, firstDayOfWeek: number): Date[] {
93
+ const d = new Date(date);
94
+ const dow = d.getDay();
95
+ let diff = dow - firstDayOfWeek;
96
+ if (diff < 0) diff += 7;
97
+ d.setDate(d.getDate() - diff);
98
+
99
+ return Array.from({ length: 7 }, (_, i) => {
100
+ const day = new Date(d);
101
+ day.setDate(d.getDate() + i);
102
+ return day;
103
+ });
104
+ },
105
+
106
+ isSameDay(a: Date, b: Date): boolean {
107
+ return (
108
+ a.getFullYear() === b.getFullYear() &&
109
+ a.getMonth() === b.getMonth() &&
110
+ a.getDate() === b.getDate()
111
+ );
112
+ },
113
+
114
+ isToday(date: Date): boolean {
115
+ return CalendarLogic.isSameDay(date, new Date());
116
+ },
117
+
118
+ isCurrentMonth(date: Date, year: number, month: number): boolean {
119
+ return date.getFullYear() === year && date.getMonth() === month;
120
+ },
121
+
122
+ /** Returns all events that fall (fully or partially) on a given day. */
123
+ getEventsForDay(events: CalendarEvent[], day: Date): CalendarEvent[] {
124
+ const dayStart = new Date(day);
125
+ dayStart.setHours(0, 0, 0, 0);
126
+ const dayEnd = new Date(day);
127
+ dayEnd.setHours(23, 59, 59, 999);
128
+
129
+ return events.filter(
130
+ (e) => e.start <= dayEnd && e.end >= dayStart
131
+ );
132
+ },
133
+
134
+ /** Returns only allDay events for a day. */
135
+ getAllDayEvents(events: CalendarEvent[], day: Date): CalendarEvent[] {
136
+ return CalendarLogic.getEventsForDay(events, day).filter((e) => e.allDay);
137
+ },
138
+
139
+ /** Returns only timed events for a day. */
140
+ getTimedEvents(events: CalendarEvent[], day: Date): CalendarEvent[] {
141
+ return CalendarLogic.getEventsForDay(events, day).filter((e) => !e.allDay);
142
+ },
143
+
144
+ /** Returns top-offset % and height % for a timed event within a day column. */
145
+ getEventPosition(event: CalendarEvent, day: Date): { top: number; height: number } {
146
+ const dayStart = new Date(day);
147
+ dayStart.setHours(0, 0, 0, 0);
148
+ const dayEnd = new Date(day);
149
+ dayEnd.setHours(24, 0, 0, 0);
150
+
151
+ const totalMs = 24 * 60 * 60 * 1000;
152
+ const startMs = Math.max(event.start.getTime(), dayStart.getTime()) - dayStart.getTime();
153
+ const endMs = Math.min(event.end.getTime(), dayEnd.getTime()) - dayStart.getTime();
154
+
155
+ return {
156
+ top: (startMs / totalMs) * 100,
157
+ height: Math.max(((endMs - startMs) / totalMs) * 100, 2), // min 2%
158
+ };
159
+ },
160
+
161
+ formatTime(date: Date): string {
162
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
163
+ },
164
+ };
165
+
166
+ // -----------------------------------------------------------
167
+ // Default Locale
168
+ // -----------------------------------------------------------
169
+
170
+ const DEFAULT_LOCALE: CalendarLocale = {
171
+ monthNames: [
172
+ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
173
+ 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
174
+ ],
175
+ dayNamesShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
176
+ dayNamesFull: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
177
+ firstDayOfWeek: 1,
178
+ today: 'Heute',
179
+ month: 'Monat',
180
+ week: 'Woche',
181
+ agenda: 'Agenda',
182
+ allDay: 'Ganztägig',
183
+ noEvents: 'Keine Termine',
184
+ };
185
+
186
+ // -----------------------------------------------------------
187
+ // Renderer — builds DOM from CalendarLogic output
188
+ // -----------------------------------------------------------
189
+
190
+ export class CalendarRenderer {
191
+ private locale: CalendarLocale;
192
+
193
+ constructor(locale: CalendarLocale) {
194
+ this.locale = locale;
195
+ }
196
+
197
+ /** Ordered day-name headers respecting firstDayOfWeek */
198
+ renderWeekdayHeaders(): string {
199
+ const { dayNamesShort, firstDayOfWeek } = this.locale;
200
+ const ordered = [
201
+ ...dayNamesShort.slice(firstDayOfWeek),
202
+ ...dayNamesShort.slice(0, firstDayOfWeek),
203
+ ];
204
+ return ordered
205
+ .map((name) => `<div class="cal__weekday" aria-label="${name}">${name}</div>`)
206
+ .join('');
207
+ }
208
+
209
+ renderEvent(event: CalendarEvent, compact = false): string {
210
+ const extraClass = event.className ?? '';
211
+ if (compact) {
212
+ return `<div class="cal__event-pill ${extraClass}"
213
+ data-event-id="${event.id}"
214
+ role="button"
215
+ tabindex="0"
216
+ aria-label="${event.title}"
217
+ title="${event.title}">${event.title}</div>`;
218
+ }
219
+ return `<div class="cal__event-pill ${extraClass}"
220
+ data-event-id="${event.id}"
221
+ role="button"
222
+ tabindex="0"
223
+ aria-label="${event.title}, ${CalendarLogic.formatTime(event.start)} – ${CalendarLogic.formatTime(event.end)}"
224
+ title="${event.title}">
225
+ <span class="cal__event-time">${CalendarLogic.formatTime(event.start)}</span>
226
+ ${event.title}
227
+ </div>`;
228
+ }
229
+
230
+ renderMonthDay(
231
+ date: Date,
232
+ currentMonth: number,
233
+ currentYear: number,
234
+ events: CalendarEvent[],
235
+ selectedDate: Date | null,
236
+ showOutsideDays: boolean
237
+ ): string {
238
+ const dayEvents = CalendarLogic.getEventsForDay(events, date);
239
+ const isToday = CalendarLogic.isToday(date);
240
+ const isSelected = selectedDate ? CalendarLogic.isSameDay(date, selectedDate) : false;
241
+ const isOutside = !CalendarLogic.isCurrentMonth(date, currentYear, currentMonth);
242
+
243
+ if (isOutside && !showOutsideDays) {
244
+ return `<div class="cal__day cal__day--empty" aria-hidden="true"></div>`;
245
+ }
246
+
247
+ const classes = [
248
+ 'cal__day',
249
+ isToday ? 'is-today' : '',
250
+ isSelected ? 'is-selected' : '',
251
+ isOutside ? 'cal__day--outside' : '',
252
+ dayEvents.length > 0 ? 'has-events' : '',
253
+ ]
254
+ .filter(Boolean)
255
+ .join(' ');
256
+
257
+ const eventsHtml = dayEvents
258
+ .slice(0, 3)
259
+ .map((e) => this.renderEvent(e, true))
260
+ .join('');
261
+
262
+ const moreCount = dayEvents.length - 3;
263
+ const moreHtml =
264
+ moreCount > 0
265
+ ? `<div class="cal__event-more">+${moreCount}</div>`
266
+ : '';
267
+
268
+ return `<div class="${classes}"
269
+ role="gridcell"
270
+ tabindex="0"
271
+ aria-label="${date.toLocaleDateString()}"
272
+ aria-selected="${isSelected}"
273
+ data-date="${date.toISOString()}">
274
+ <span class="cal__day-num">${date.getDate()}</span>
275
+ <div class="cal__day-events">${eventsHtml}${moreHtml}</div>
276
+ </div>`;
277
+ }
278
+
279
+ renderMonthView(
280
+ year: number,
281
+ month: number,
282
+ events: CalendarEvent[],
283
+ selectedDate: Date | null,
284
+ showOutsideDays: boolean,
285
+ firstDayOfWeek: number
286
+ ): string {
287
+ const days = CalendarLogic.getMonthGrid(year, month, firstDayOfWeek);
288
+ const cells = days
289
+ .map((d) =>
290
+ this.renderMonthDay(d, month, year, events, selectedDate, showOutsideDays)
291
+ )
292
+ .join('');
293
+
294
+ return `<div class="cal__month-grid" role="grid" aria-label="${this.locale.monthNames[month]} ${year}">
295
+ ${this.renderWeekdayHeaders()}
296
+ ${cells}
297
+ </div>`;
298
+ }
299
+
300
+ renderWeekView(
301
+ date: Date,
302
+ events: CalendarEvent[],
303
+ selectedDate: Date | null,
304
+ firstDayOfWeek: number
305
+ ): string {
306
+ const days = CalendarLogic.getWeekDays(date, firstDayOfWeek);
307
+
308
+ // All-day row
309
+ const allDayCols = days
310
+ .map((d) => {
311
+ const adEvents = CalendarLogic.getAllDayEvents(events, d);
312
+ const pills = adEvents.map((e) => this.renderEvent(e, true)).join('');
313
+ return `<div class="cal__allday-col">${pills}</div>`;
314
+ })
315
+ .join('');
316
+
317
+ // Day column headers
318
+ const headCols = days
319
+ .map((d) => {
320
+ const isToday = CalendarLogic.isToday(d);
321
+ const isSelected = selectedDate ? CalendarLogic.isSameDay(d, selectedDate) : false;
322
+ const classes = [
323
+ 'cal__week-head-day',
324
+ isToday ? 'is-today' : '',
325
+ isSelected ? 'is-selected' : '',
326
+ ]
327
+ .filter(Boolean)
328
+ .join(' ');
329
+
330
+ const dow = this.locale.dayNamesShort[(d.getDay() + 7) % 7];
331
+ return `<div class="${classes}" data-date="${d.toISOString()}">
332
+ ${dow}<span>${d.getDate()}</span>
333
+ </div>`;
334
+ })
335
+ .join('');
336
+
337
+ // Hour slots + events
338
+ const hourLabels = Array.from({ length: 24 }, (_, h) => {
339
+ const label = h === 0 ? '' : `${String(h).padStart(2, '0')}:00`;
340
+ return `<div class="cal__time-slot">${label}</div>`;
341
+ }).join('');
342
+
343
+ const dayCols = days
344
+ .map((d) => {
345
+ const timedEvents = CalendarLogic.getTimedEvents(events, d);
346
+ const hourCells = Array.from({ length: 24 }, () => `<div class="cal__day-col-hour"></div>`).join('');
347
+
348
+ const eventOverlays = timedEvents
349
+ .map((e) => {
350
+ const { top, height } = CalendarLogic.getEventPosition(e, d);
351
+ const extraClass = e.className ?? '';
352
+ return `<div class="cal__week-event ${extraClass}"
353
+ style="top:${top.toFixed(2)}%;height:${height.toFixed(2)}%"
354
+ data-event-id="${e.id}"
355
+ role="button"
356
+ tabindex="0"
357
+ aria-label="${e.title}">
358
+ <span class="cal__event-time">${CalendarLogic.formatTime(e.start)}</span>
359
+ ${e.title}
360
+ </div>`;
361
+ })
362
+ .join('');
363
+
364
+ return `<div class="cal__day-col" data-date="${d.toISOString()}">${hourCells}${eventOverlays}</div>`;
365
+ })
366
+ .join('');
367
+
368
+ return `<div class="cal__week" role="grid">
369
+ <div class="cal__week-head">
370
+ <div class="cal__week-head-time"></div>
371
+ ${headCols}
372
+ </div>
373
+ <div class="cal__allday">
374
+ <div class="cal__allday-label">${this.locale.allDay}</div>
375
+ ${allDayCols}
376
+ </div>
377
+ <div class="cal__week-body">
378
+ <div class="cal__week-grid">
379
+ <div class="cal__time-col">${hourLabels}</div>
380
+ ${dayCols}
381
+ </div>
382
+ </div>
383
+ </div>`;
384
+ }
385
+
386
+ renderAgendaView(year: number, month: number, events: CalendarEvent[]): string {
387
+ // Collect all days in this month that have events
388
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
389
+ let html = '';
390
+
391
+ for (let d = 1; d <= daysInMonth; d++) {
392
+ const day = new Date(year, month, d);
393
+ const dayEvents = CalendarLogic.getEventsForDay(events, day);
394
+ if (dayEvents.length === 0) continue;
395
+
396
+ const isToday = CalendarLogic.isToday(day);
397
+ const dow = this.locale.dayNamesFull[day.getDay()];
398
+
399
+ html += `<div class="cal__agenda-day ${isToday ? 'is-today' : ''}">
400
+ <div class="cal__agenda-date">
401
+ <span class="cal__agenda-dow">${dow}</span>
402
+ <span class="cal__agenda-num ${isToday ? 'is-today' : ''}">${d}</span>
403
+ </div>
404
+ <div class="cal__agenda-events">
405
+ ${dayEvents.map((e) => `
406
+ <div class="cal__agenda-event ${e.className ?? ''}"
407
+ data-event-id="${e.id}"
408
+ role="button"
409
+ tabindex="0">
410
+ <span class="cal__agenda-event-time">
411
+ ${e.allDay ? this.locale.allDay : CalendarLogic.formatTime(e.start) + ' – ' + CalendarLogic.formatTime(e.end)}
412
+ </span>
413
+ <span class="cal__agenda-event-title">${e.title}</span>
414
+ </div>`).join('')}
415
+ </div>
416
+ </div>`;
417
+ }
418
+
419
+ if (!html) {
420
+ html = `<div class="cal__agenda-empty">${this.locale.noEvents}</div>`;
421
+ }
422
+
423
+ return `<div class="cal__agenda">${html}</div>`;
424
+ }
425
+ }
426
+
427
+ // -----------------------------------------------------------
428
+ // Calendar — main controller class
429
+ // -----------------------------------------------------------
430
+
431
+ export class Calendar {
432
+ private container: HTMLElement;
433
+ private options: Required<CalendarOptions>;
434
+ private locale: CalendarLocale;
435
+ private renderer: CalendarRenderer;
436
+
437
+ private currentDate: Date;
438
+ private currentView: CalendarView;
439
+ private selectedDate: Date | null = null;
440
+ private events: CalendarEvent[] = [];
441
+
442
+ constructor(options: CalendarOptions) {
443
+ // Resolve container
444
+ if (typeof options.container === 'string') {
445
+ const el = document.querySelector<HTMLElement>(options.container);
446
+ if (!el) throw new Error(`Calendar: container "${options.container}" not found.`);
447
+ this.container = el;
448
+ } else {
449
+ this.container = options.container;
450
+ }
451
+
452
+ this.locale = { ...DEFAULT_LOCALE, ...(options.locale ?? {}) };
453
+ this.renderer = new CalendarRenderer(this.locale);
454
+
455
+ this.options = {
456
+ container: this.container,
457
+ events: options.events ?? [],
458
+ view: options.view ?? 'month',
459
+ locale: options.locale ?? {},
460
+ showOutsideDays: options.showOutsideDays ?? true,
461
+ onDayClick: options.onDayClick ?? (() => {}),
462
+ onEventClick: options.onEventClick ?? (() => {}),
463
+ onChange: options.onChange ?? (() => {}),
464
+ className: options.className ?? '',
465
+ };
466
+
467
+ this.events = [...this.options.events];
468
+ this.currentView = this.options.view;
469
+ this.currentDate = new Date();
470
+
471
+ this.render();
472
+ this.attachEvents();
473
+ }
474
+
475
+ // ----------------------------------------------------------
476
+ // Public API
477
+ // ----------------------------------------------------------
478
+
479
+ setView(view: CalendarView): void {
480
+ this.currentView = view;
481
+ this.render();
482
+ this.options.onChange(this.currentDate, this.currentView);
483
+ }
484
+
485
+ next(): void {
486
+ if (this.currentView === 'month' || this.currentView === 'agenda') {
487
+ this.currentDate = new Date(
488
+ this.currentDate.getFullYear(),
489
+ this.currentDate.getMonth() + 1,
490
+ 1
491
+ );
492
+ } else {
493
+ this.currentDate = new Date(
494
+ this.currentDate.getFullYear(),
495
+ this.currentDate.getMonth(),
496
+ this.currentDate.getDate() + 7
497
+ );
498
+ }
499
+ this.render();
500
+ this.options.onChange(this.currentDate, this.currentView);
501
+ }
502
+
503
+ prev(): void {
504
+ if (this.currentView === 'month' || this.currentView === 'agenda') {
505
+ this.currentDate = new Date(
506
+ this.currentDate.getFullYear(),
507
+ this.currentDate.getMonth() - 1,
508
+ 1
509
+ );
510
+ } else {
511
+ this.currentDate = new Date(
512
+ this.currentDate.getFullYear(),
513
+ this.currentDate.getMonth(),
514
+ this.currentDate.getDate() - 7
515
+ );
516
+ }
517
+ this.render();
518
+ this.options.onChange(this.currentDate, this.currentView);
519
+ }
520
+
521
+ today(): void {
522
+ this.currentDate = new Date();
523
+ this.render();
524
+ this.options.onChange(this.currentDate, this.currentView);
525
+ }
526
+
527
+ addEvent(event: CalendarEvent): void {
528
+ this.events.push(event);
529
+ this.render();
530
+ }
531
+
532
+ removeEvent(id: string): void {
533
+ this.events = this.events.filter((e) => e.id !== id);
534
+ this.render();
535
+ }
536
+
537
+ setEvents(events: CalendarEvent[]): void {
538
+ this.events = [...events];
539
+ this.render();
540
+ }
541
+
542
+ getEvents(): CalendarEvent[] {
543
+ return [...this.events];
544
+ }
545
+
546
+ destroy(): void {
547
+ this.container.innerHTML = '';
548
+ this.container.removeAttribute('data-cal');
549
+ }
550
+
551
+ // ----------------------------------------------------------
552
+ // Internal rendering
553
+ // ----------------------------------------------------------
554
+
555
+ private getTitle(): string {
556
+ const { monthNames } = this.locale;
557
+ const y = this.currentDate.getFullYear();
558
+ const m = this.currentDate.getMonth();
559
+
560
+ if (this.currentView === 'week') {
561
+ const days = CalendarLogic.getWeekDays(this.currentDate, this.locale.firstDayOfWeek);
562
+ const first = days[0];
563
+ const last = days[6];
564
+ if (first.getMonth() === last.getMonth()) {
565
+ return `${monthNames[first.getMonth()]} ${y}`;
566
+ }
567
+ return `${monthNames[first.getMonth()]} – ${monthNames[last.getMonth()]} ${y}`;
568
+ }
569
+
570
+ return `${monthNames[m]} ${y}`;
571
+ }
572
+
573
+ private buildHeader(): string {
574
+ const activeMonth = this.currentView === 'month' || this.currentView === 'agenda' ? 'cal__btn--active' : '';
575
+ const activeWeek = this.currentView === 'week' ? 'cal__btn--active' : '';
576
+ const activeAgenda = this.currentView === 'agenda' ? 'cal__btn--active' : '';
577
+
578
+ return `<div class="cal__header">
579
+ <div class="cal__nav">
580
+ <button class="cal__btn cal__btn--today" data-action="today" aria-label="${this.locale.today}">${this.locale.today}</button>
581
+ <button class="cal__btn" data-action="prev" aria-label="Zurück">
582
+ <svg class="icon-svg" aria-hidden="true"><use href="svg-icons/icons.svg#chevron_left"/></svg>
583
+ </button>
584
+ <button class="cal__btn" data-action="next" aria-label="Vor">
585
+ <svg class="icon-svg" aria-hidden="true"><use href="svg-icons/icons.svg#chevron_right"/></svg>
586
+ </button>
587
+ </div>
588
+ <h2 class="cal__title" aria-live="polite">${this.getTitle()}</h2>
589
+ <div class="cal__view-toggle" role="group" aria-label="Ansicht wählen">
590
+ <button class="cal__btn ${activeMonth}" data-action="view-month" aria-pressed="${this.currentView === 'month'}">${this.locale.month}</button>
591
+ <button class="cal__btn ${activeWeek}" data-action="view-week" aria-pressed="${this.currentView === 'week'}">${this.locale.week}</button>
592
+ <button class="cal__btn ${activeAgenda}" data-action="view-agenda" aria-pressed="${this.currentView === 'agenda'}">${this.locale.agenda}</button>
593
+ </div>
594
+ </div>`;
595
+ }
596
+
597
+ private buildBody(): string {
598
+ const { firstDayOfWeek } = this.locale;
599
+ const y = this.currentDate.getFullYear();
600
+ const m = this.currentDate.getMonth();
601
+
602
+ switch (this.currentView) {
603
+ case 'month':
604
+ return this.renderer.renderMonthView(
605
+ y, m, this.events, this.selectedDate,
606
+ this.options.showOutsideDays, firstDayOfWeek
607
+ );
608
+ case 'week':
609
+ return this.renderer.renderWeekView(
610
+ this.currentDate, this.events, this.selectedDate, firstDayOfWeek
611
+ );
612
+ case 'agenda':
613
+ return this.renderer.renderAgendaView(y, m, this.events);
614
+ }
615
+ }
616
+
617
+ private render(): void {
618
+ const rootClass = ['cal', this.options.className].filter(Boolean).join(' ');
619
+ this.container.setAttribute('data-cal', this.currentView);
620
+ this.container.innerHTML = `<div class="${rootClass}" role="application" aria-label="Kalender">
621
+ ${this.buildHeader()}
622
+ <div class="cal__body">
623
+ ${this.buildBody()}
624
+ </div>
625
+ </div>`;
626
+ }
627
+
628
+ // ----------------------------------------------------------
629
+ // Event delegation
630
+ // ----------------------------------------------------------
631
+
632
+ private attachEvents(): void {
633
+ this.container.addEventListener('click', (e) => this.handleClick(e));
634
+ this.container.addEventListener('keydown', (e) => this.handleKeydown(e));
635
+ }
636
+
637
+ private handleClick(e: MouseEvent): void {
638
+ const target = e.target as HTMLElement;
639
+
640
+ // Nav / view buttons
641
+ const btn = target.closest<HTMLElement>('[data-action]');
642
+ if (btn) {
643
+ const action = btn.dataset.action!;
644
+ if (action === 'prev') this.prev();
645
+ else if (action === 'next') this.next();
646
+ else if (action === 'today') this.today();
647
+ else if (action === 'view-month') this.setView('month');
648
+ else if (action === 'view-week') this.setView('week');
649
+ else if (action === 'view-agenda') this.setView('agenda');
650
+ return;
651
+ }
652
+
653
+ // Event click
654
+ const eventEl = target.closest<HTMLElement>('[data-event-id]');
655
+ if (eventEl) {
656
+ const id = eventEl.dataset.eventId!;
657
+ const event = this.events.find((ev) => ev.id === id);
658
+ if (event) {
659
+ e.stopPropagation();
660
+ this.options.onEventClick(event);
661
+ }
662
+ return;
663
+ }
664
+
665
+ // Day click
666
+ const dayEl = target.closest<HTMLElement>('[data-date]');
667
+ if (dayEl && dayEl.dataset.date) {
668
+ const date = new Date(dayEl.dataset.date);
669
+ this.selectedDate = date;
670
+ this.options.onDayClick(date);
671
+ // Re-render to update selection state
672
+ this.render();
673
+ }
674
+ }
675
+
676
+ private handleKeydown(e: KeyboardEvent): void {
677
+ const target = e.target as HTMLElement;
678
+
679
+ // Allow Enter/Space to trigger click on focused interactive elements
680
+ if (e.key === 'Enter' || e.key === ' ') {
681
+ if (target.closest('[data-date], [data-event-id], [data-action]')) {
682
+ e.preventDefault();
683
+ target.click();
684
+ }
685
+ }
686
+
687
+ // Arrow key navigation within month grid
688
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
689
+ const dayEl = target.closest<HTMLElement>('.cal__day[data-date]');
690
+ if (!dayEl) return;
691
+
692
+ e.preventDefault();
693
+ const all = Array.from(
694
+ this.container.querySelectorAll<HTMLElement>('.cal__day[data-date]:not(.cal__day--empty)')
695
+ );
696
+ const idx = all.indexOf(dayEl);
697
+ let next = idx;
698
+
699
+ if (e.key === 'ArrowRight') next = idx + 1;
700
+ else if (e.key === 'ArrowLeft') next = idx - 1;
701
+ else if (e.key === 'ArrowDown') next = idx + 7;
702
+ else if (e.key === 'ArrowUp') next = idx - 7;
703
+
704
+ all[Math.max(0, Math.min(next, all.length - 1))]?.focus();
705
+ }
706
+ }