picocrank 1.25.2 → 1.26.0

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 (40) hide show
  1. package/eslint.config.js +68 -0
  2. package/package.json +13 -3
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +20 -11
  5. package/styles/picocrank-extensions.css +77 -30
  6. package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
  7. package/supplemental-themes/dracula-alucard/theme.css +10 -24
  8. package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
  9. package/supplemental-themes/waffles/theme.css +2 -16
  10. package/vue/components/Breadcrumbs.vue +192 -45
  11. package/vue/components/Calendar.vue +45 -25
  12. package/vue/components/DangerZone.vue +186 -0
  13. package/vue/components/Header.vue +22 -6
  14. package/vue/components/NavigationGrid.vue +26 -180
  15. package/vue/components/NotificationBlock.vue +120 -0
  16. package/vue/components/NotificationPopup.vue +44 -41
  17. package/vue/components/ReadOnlyTextArea.vue +1 -1
  18. package/vue/components/SidebarNavLink.vue +3 -3
  19. package/vue/components/Table.vue +107 -79
  20. package/vue/components/TableColumnOptionsPopover.vue +9 -0
  21. package/vue/components/Tabs.vue +363 -79
  22. package/vue/components/TopBar.vue +2 -2
  23. package/vue/composables/tableColumnPriorities.js +33 -0
  24. package/vue/composables/tableFilters.js +1 -1
  25. package/vue/data/navSectionHubs.js +196 -0
  26. package/vue/data/routeBreadcrumbs.js +90 -0
  27. package/vue/examples/Admin.vue +12 -12
  28. package/vue/examples/App.vue +27 -36
  29. package/vue/examples/CalendarExample.vue +32 -22
  30. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  31. package/vue/examples/Login.vue +0 -3
  32. package/vue/examples/LoginExample.vue +2 -14
  33. package/vue/examples/NavSectionHub.vue +36 -0
  34. package/vue/examples/PageStructure.vue +6 -3
  35. package/vue/examples/StatusExample.vue +248 -129
  36. package/vue/examples/TableExample.vue +68 -67
  37. package/vue/examples/TabsExample.vue +200 -255
  38. package/vue/examples/ThemePreview.vue +21 -23
  39. package/vue/examples/ViewItem.vue +32 -5
  40. package/vue/examples/Welcome.vue +3 -40
@@ -32,7 +32,7 @@ html[data-theme="dark"] {
32
32
  --dt-warning-fg: var(--dt-surface);
33
33
 
34
34
  --body-bg-color: var(--dt-background);
35
- --surface-bg-color: var(--dt-surface);
35
+ --section-bg-color: var(--dt-surface);
36
36
  --standout-bg-color: var(--dt-surface);
37
37
  --section-header-bg-color: var(--dt-surface);
38
38
  --shadow-color: var(--dt-shadow-strong);
@@ -41,8 +41,6 @@ html[data-theme="dark"] {
41
41
  --muted-text-color: var(--dt-muted);
42
42
  --annotation-key-color: var(--dt-muted);
43
43
  --link-color: var(--dt-accent);
44
- --nav-grid-accent-fg: var(--link-color);
45
- --nav-grid-accent-bg: var(--link-color);
46
44
  --border-color: var(--dt-border);
47
45
 
48
46
  --hover-background-color: var(--dt-selection);
@@ -65,6 +63,9 @@ html[data-theme="dark"] {
65
63
  --input-fg-color: var(--dt-foreground);
66
64
  --control-option-bg-color: var(--dt-background);
67
65
  --control-hover-bg-color: var(--dt-overlay);
66
+ --control-checked-bg: color-mix(in srgb, var(--dt-green) 35%, var(--standout-bg-color));
67
+ --control-checked-hover-bg: color-mix(in srgb, var(--dt-green) 45%, var(--standout-bg-color));
68
+ --control-checked-fg: var(--dt-foreground);
68
69
  --menu-hover-bg-color: var(--dt-selection);
69
70
 
70
71
  --table-row-hover-bg: var(--dt-selection);
@@ -87,19 +88,11 @@ html[data-theme="dark"] {
87
88
  --karma-bad: var(--dt-red);
88
89
  --karma-warning: var(--dt-yellow);
89
90
  --karma-severe: var(--dt-orange);
90
- --karma-important: var(--dt-purple);
91
91
  --karma-note: var(--dt-cyan);
92
92
  --karma-info: var(--dt-overlay);
93
93
  --karma-old: var(--dt-muted);
94
94
 
95
- --karma-good-fg: var(--dt-green);
96
- --karma-bad-fg: var(--dt-red);
97
- --karma-warning-fg: var(--dt-yellow);
98
- --karma-severe-fg: var(--dt-orange);
99
- --karma-important-fg: var(--dt-purple);
100
- --karma-note-fg: var(--dt-cyan);
101
- --karma-info-fg: var(--dt-muted);
102
- --karma-old-fg: var(--dt-muted);
95
+ --karma-on-bg-fg: var(--dt-good-fg);
103
96
  }
104
97
 
105
98
  html[data-theme="light"] {
@@ -126,7 +119,7 @@ html[data-theme="light"] {
126
119
  --dt-warning-fg: var(--dt-background);
127
120
 
128
121
  --body-bg-color: var(--dt-background);
129
- --surface-bg-color: var(--dt-surface);
122
+ --section-bg-color: var(--dt-surface);
130
123
  --standout-bg-color: var(--dt-surface);
131
124
  --section-header-bg-color: var(--dt-surface);
132
125
  --shadow-color: var(--dt-shadow-strong);
@@ -135,8 +128,6 @@ html[data-theme="light"] {
135
128
  --muted-text-color: var(--dt-muted);
136
129
  --annotation-key-color: var(--dt-muted);
137
130
  --link-color: var(--dt-accent);
138
- --nav-grid-accent-fg: var(--link-color);
139
- --nav-grid-accent-bg: var(--link-color);
140
131
  --border-color: var(--dt-border);
141
132
 
142
133
  --hover-background-color: var(--dt-selection);
@@ -159,6 +150,9 @@ html[data-theme="light"] {
159
150
  --input-fg-color: var(--dt-foreground);
160
151
  --control-option-bg-color: var(--dt-overlay);
161
152
  --control-hover-bg-color: var(--dt-selection);
153
+ --control-checked-bg: color-mix(in srgb, var(--dt-green) 20%, var(--standout-bg-color));
154
+ --control-checked-hover-bg: color-mix(in srgb, var(--dt-green) 28%, var(--standout-bg-color));
155
+ --control-checked-fg: var(--dt-foreground);
162
156
  --menu-hover-bg-color: var(--dt-selection);
163
157
 
164
158
  --table-row-hover-bg: var(--dt-selection);
@@ -181,17 +175,9 @@ html[data-theme="light"] {
181
175
  --karma-bad: var(--dt-red);
182
176
  --karma-warning: var(--dt-yellow);
183
177
  --karma-severe: var(--dt-orange);
184
- --karma-important: var(--dt-purple);
185
178
  --karma-note: var(--dt-cyan);
186
179
  --karma-info: var(--dt-overlay);
187
180
  --karma-old: var(--dt-muted);
188
181
 
189
- --karma-good-fg: var(--dt-green);
190
- --karma-bad-fg: var(--dt-red);
191
- --karma-warning-fg: var(--dt-yellow);
192
- --karma-severe-fg: var(--dt-orange);
193
- --karma-important-fg: var(--dt-purple);
194
- --karma-note-fg: var(--dt-cyan);
195
- --karma-info-fg: var(--dt-muted);
196
- --karma-old-fg: var(--dt-muted);
182
+ --karma-on-bg-fg: var(--dt-good-fg);
197
183
  }
@@ -35,7 +35,7 @@ html[data-theme="dark"] {
35
35
  --gb-good-fg: var(--gb-bg0-hard);
36
36
 
37
37
  --body-bg-color: var(--gb-bg0);
38
- --surface-bg-color: var(--gb-bg1);
38
+ --section-bg-color: var(--gb-bg1);
39
39
  --standout-bg-color: var(--gb-bg1);
40
40
  --section-header-bg-color: var(--gb-bg1);
41
41
  --shadow-color: var(--gb-shadow-strong);
@@ -44,8 +44,6 @@ html[data-theme="dark"] {
44
44
  --muted-text-color: var(--gb-fg3);
45
45
  --annotation-key-color: var(--gb-fg3);
46
46
  --link-color: var(--gb-accent);
47
- --nav-grid-accent-fg: var(--link-color);
48
- --nav-grid-accent-bg: var(--link-color);
49
47
  --border-color: var(--gb-bg3);
50
48
 
51
49
  --hover-background-color: var(--gb-bg2);
@@ -68,6 +66,9 @@ html[data-theme="dark"] {
68
66
  --input-fg-color: var(--gb-fg0);
69
67
  --control-option-bg-color: var(--gb-bg2);
70
68
  --control-hover-bg-color: var(--gb-bg3);
69
+ --control-checked-bg: color-mix(in srgb, var(--gb-green) 35%, var(--standout-bg-color));
70
+ --control-checked-hover-bg: color-mix(in srgb, var(--gb-green) 45%, var(--standout-bg-color));
71
+ --control-checked-fg: var(--gb-fg0);
71
72
  --menu-hover-bg-color: var(--gb-bg2);
72
73
 
73
74
  --table-row-hover-bg: var(--gb-bg2);
@@ -90,19 +91,11 @@ html[data-theme="dark"] {
90
91
  --karma-bad: var(--gb-red);
91
92
  --karma-warning: var(--gb-yellow);
92
93
  --karma-severe: var(--gb-orange);
93
- --karma-important: var(--gb-purple);
94
94
  --karma-note: var(--gb-aqua);
95
95
  --karma-info: var(--gb-bg2);
96
96
  --karma-old: var(--gb-gray);
97
97
 
98
- --karma-good-fg: var(--gb-green);
99
- --karma-bad-fg: var(--gb-red);
100
- --karma-warning-fg: var(--gb-yellow);
101
- --karma-severe-fg: var(--gb-orange);
102
- --karma-important-fg: var(--gb-purple);
103
- --karma-note-fg: var(--gb-aqua);
104
- --karma-info-fg: var(--gb-fg3);
105
- --karma-old-fg: var(--gb-fg4);
98
+ --karma-on-bg-fg: var(--gb-good-fg);
106
99
  }
107
100
 
108
101
  html[data-theme="light"] {
@@ -132,7 +125,7 @@ html[data-theme="light"] {
132
125
  --gb-good-fg: var(--gb-bg0);
133
126
 
134
127
  --body-bg-color: var(--gb-bg0);
135
- --surface-bg-color: var(--gb-bg1);
128
+ --section-bg-color: var(--gb-bg1);
136
129
  --standout-bg-color: var(--gb-bg1);
137
130
  --section-header-bg-color: var(--gb-bg1);
138
131
  --shadow-color: var(--gb-shadow-strong);
@@ -141,8 +134,6 @@ html[data-theme="light"] {
141
134
  --muted-text-color: var(--gb-fg3);
142
135
  --annotation-key-color: var(--gb-fg3);
143
136
  --link-color: var(--gb-accent);
144
- --nav-grid-accent-fg: var(--link-color);
145
- --nav-grid-accent-bg: var(--link-color);
146
137
  --border-color: var(--gb-bg3);
147
138
 
148
139
  --hover-background-color: var(--gb-bg2);
@@ -165,6 +156,9 @@ html[data-theme="light"] {
165
156
  --input-fg-color: var(--gb-fg0);
166
157
  --control-option-bg-color: var(--gb-bg2);
167
158
  --control-hover-bg-color: var(--gb-bg3);
159
+ --control-checked-bg: color-mix(in srgb, var(--gb-green) 20%, var(--standout-bg-color));
160
+ --control-checked-hover-bg: color-mix(in srgb, var(--gb-green) 28%, var(--standout-bg-color));
161
+ --control-checked-fg: var(--gb-fg0);
168
162
  --menu-hover-bg-color: var(--gb-bg2);
169
163
 
170
164
  --table-row-hover-bg: var(--gb-bg2);
@@ -187,17 +181,9 @@ html[data-theme="light"] {
187
181
  --karma-bad: var(--gb-red);
188
182
  --karma-warning: var(--gb-yellow);
189
183
  --karma-severe: var(--gb-orange);
190
- --karma-important: var(--gb-purple);
191
184
  --karma-note: var(--gb-aqua);
192
185
  --karma-info: var(--gb-bg2);
193
186
  --karma-old: var(--gb-gray);
194
187
 
195
- --karma-good-fg: var(--gb-green);
196
- --karma-bad-fg: var(--gb-red);
197
- --karma-warning-fg: var(--gb-yellow);
198
- --karma-severe-fg: var(--gb-orange);
199
- --karma-important-fg: var(--gb-purple);
200
- --karma-note-fg: var(--gb-aqua);
201
- --karma-info-fg: var(--gb-fg3);
202
- --karma-old-fg: var(--gb-fg4);
188
+ --karma-on-bg-fg: var(--gb-good-fg);
203
189
  }
@@ -18,7 +18,7 @@ html {
18
18
  --waffle-accent: #a05020;
19
19
 
20
20
  --body-bg-color: var(--waffle-cream);
21
- --surface-bg-color: #fffdf6;
21
+ --section-bg-color: #fffdf6;
22
22
  --standout-bg-color: var(--waffle-batter);
23
23
  --section-header-bg-color: var(--waffle-batter);
24
24
  --shadow-color: rgba(139, 69, 19, 0.12);
@@ -26,8 +26,6 @@ html {
26
26
  --text-color: var(--waffle-syrup);
27
27
  --muted-text-color: #a88860;
28
28
  --link-color: var(--waffle-accent);
29
- --nav-grid-accent-fg: var(--link-color);
30
- --nav-grid-accent-bg: var(--link-color);
31
29
  --border-color: #e0c896;
32
30
 
33
31
  --hover-background-color: #f0d9a8;
@@ -61,19 +59,9 @@ html {
61
59
  --karma-bad: #f0a0a8;
62
60
  --karma-warning: var(--waffle-butter);
63
61
  --karma-severe: #e08a6a;
64
- --karma-important: #ffe8a3;
65
62
  --karma-note: #a8d4e8;
66
63
  --karma-info: var(--waffle-batter);
67
64
  --karma-old: #d4c4a0;
68
-
69
- --karma-good-fg: #4a7a28;
70
- --karma-bad-fg: var(--waffle-berry);
71
- --karma-warning-fg: #9a6b00;
72
- --karma-severe-fg: #a04020;
73
- --karma-important-fg: #8a6800;
74
- --karma-note-fg: #2a6a88;
75
- --karma-info-fg: #7a6540;
76
- --karma-old-fg: #6a5840;
77
65
  }
78
66
 
79
67
  body {
@@ -140,8 +128,6 @@ html[data-theme="dark"] {
140
128
  --text-color: var(--waffle-foreground);
141
129
  --muted-text-color: var(--waffle-foreground-muted);
142
130
  --link-color: var(--waffle-accent-dark);
143
- --nav-grid-accent-fg: var(--link-color);
144
- --nav-grid-accent-bg: var(--link-color);
145
131
  --border-color: #c9a66a;
146
132
  --standout-bg-color: var(--waffle-batter);
147
133
  --hover-background-color: #f0d9a8;
@@ -149,7 +135,7 @@ html[data-theme="dark"] {
149
135
  --disabled-text-color: #7a6548;
150
136
 
151
137
  --body-bg-color: var(--waffle-cream);
152
- --surface-bg-color: var(--waffle-batter);
138
+ --section-bg-color: var(--waffle-batter);
153
139
  --section-header-bg-color: var(--waffle-section);
154
140
  --sidebar-bg-color: var(--waffle-sidebar);
155
141
  --sidebar-link-color: var(--waffle-foreground);
@@ -1,58 +1,205 @@
1
1
  <template>
2
- <div id = "breadcrumbs">
3
- <template v-for="(link, index) in links" :key="link.name">
4
- <router-link :to="link.href">{{ link.name }}</router-link>
5
- <span v-if="index < links.length - 1" class="separator">
6
- &raquo;
7
- </span>
8
- </template>
9
- </div>
2
+ <nav id="breadcrumbs" class="breadcrumbs" aria-label="Breadcrumb">
3
+ <div class="breadcrumbs-links flex-row">
4
+ <template v-for="(link, index) in links" :key="`${link.name}-${index}`">
5
+ <HugeiconsIcon
6
+ v-if="index > 0"
7
+ :icon="ArrowRight01Icon"
8
+ class="separator"
9
+ width="0.85em"
10
+ height="0.85em"
11
+ aria-hidden="true"
12
+ />
13
+
14
+ <nav
15
+ v-if="hasMenu(link)"
16
+ class="menu breadcrumb-menu"
17
+ :aria-label="`${link.name} sections`"
18
+ >
19
+ <ul>
20
+ <li>
21
+ <span class="submenu">
22
+ <router-link
23
+ v-if="link.to"
24
+ :to="link.to"
25
+ class="button breadcrumb-link"
26
+ :aria-current="isLast(index) ? 'page' : undefined"
27
+ >
28
+ <span>{{ link.name }}</span>
29
+ </router-link>
30
+ <span
31
+ v-else
32
+ class="button breadcrumb-link"
33
+ :aria-current="isLast(index) ? 'page' : undefined"
34
+ >
35
+ {{ link.name }}
36
+ </span>
37
+ </span>
38
+ <div>
39
+ <ul>
40
+ <li v-for="menuItem in link.menu" :key="menuItem.name">
41
+ <router-link
42
+ :to="menuItem.to"
43
+ :class="{ active: isMenuItemActive(link, menuItem) }"
44
+ >
45
+ {{ menuItem.name }}
46
+ </router-link>
47
+ </li>
48
+ </ul>
49
+ </div>
50
+ </li>
51
+ </ul>
52
+ </nav>
53
+
54
+ <router-link
55
+ v-else-if="!isLast(index)"
56
+ :to="link.to ?? link.href"
57
+ class="button breadcrumb-link"
58
+ >
59
+ <span>{{ link.name }}</span>
60
+ </router-link>
61
+
62
+ <span
63
+ v-else
64
+ class="button breadcrumb-link active"
65
+ aria-current="page"
66
+ >
67
+ <span>{{ link.name }}</span>
68
+ </span>
69
+ </template>
70
+ </div>
71
+ </nav>
10
72
  </template>
11
73
 
74
+ <script setup>
75
+ import { ref, watch } from 'vue'
76
+ import { useRoute } from 'vue-router'
77
+ import { HugeiconsIcon } from '@hugeicons/vue'
78
+ import { ArrowRight01Icon } from '@hugeicons/core-free-icons'
79
+
80
+ const route = useRoute()
81
+ const links = ref([])
82
+
83
+ function hasMenu(link) {
84
+ return Array.isArray(link.menu) && link.menu.length > 0
85
+ }
86
+
87
+ function isLast(index) {
88
+ return index === links.value.length - 1
89
+ }
90
+
91
+ function isMenuItemActive(link, menuItem) {
92
+ const activeRoute = link.to?.name
93
+ const itemRoute = menuItem.to?.name
94
+ return activeRoute != null && activeRoute === itemRoute
95
+ }
96
+
97
+ watch(() => route.matched, (matched) => {
98
+ links.value = []
99
+ matched.forEach((record) => {
100
+ if (record.meta && record.meta.breadcrumbs) {
101
+ record.meta.breadcrumbs(route).forEach((item) => {
102
+ links.value.push({
103
+ name: item.name,
104
+ to: item.to,
105
+ href: item.href || record.path || '/',
106
+ menu: item.menu,
107
+ })
108
+ })
109
+ } else if (record.name) {
110
+ links.value.push({
111
+ name: record.meta?.title || record.name,
112
+ to: { name: record.name, params: route.params, query: route.query },
113
+ href: route.fullPath || record.path || '/',
114
+ })
115
+ }
116
+ })
117
+ }, { immediate: true })
118
+ </script>
119
+
12
120
  <style scoped>
13
- .separator {
14
- color: var(--breadcrumbs-separator-fg);
121
+ .breadcrumbs {
122
+ flex: 1 1 auto;
123
+ min-width: 0;
124
+ }
125
+
126
+ .breadcrumbs-links {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 0.0625rem;
130
+ flex-wrap: wrap;
131
+ }
132
+
133
+ .breadcrumb-menu {
134
+ display: inline-flex;
135
+ flex-shrink: 0;
136
+ }
137
+
138
+ .breadcrumb-menu > ul {
139
+ list-style: none;
140
+ padding: 0;
141
+ margin: 0;
142
+ }
143
+
144
+ .breadcrumb-link {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ gap: 0.375rem;
148
+ padding: 0.5em 0.375em;
149
+ border-radius: 0.25em;
150
+ text-decoration: none;
151
+ color: var(--header-fg-color);
152
+ transition: background-color 0.15s ease, color 0.15s ease;
153
+ white-space: nowrap;
154
+ border: 0;
155
+ font-weight: normal;
156
+ background-color: transparent;
157
+ cursor: default;
15
158
  }
16
159
 
17
- a {
18
- text-decoration: none;
19
- padding: 0.4em;
20
- border-radius: 0.2em;
160
+ a.breadcrumb-link {
161
+ cursor: pointer;
21
162
  }
22
163
 
23
- a:hover {
24
- text-decoration: underline;
25
- background-color: var(--breadcrumbs-link-hover-bg);
164
+ .breadcrumb-link.active {
165
+ text-decoration: underline;
166
+ font-weight: 500;
26
167
  }
27
168
 
169
+ .separator {
170
+ flex-shrink: 0;
171
+ color: var(--header-fg-color);
172
+ opacity: 0.4;
173
+ }
28
174
  </style>
29
175
 
176
+ <style>
177
+ header .breadcrumbs a.breadcrumb-link:hover,
178
+ header .breadcrumbs a.breadcrumb-link:focus-visible,
179
+ header .breadcrumbs .breadcrumb-menu.menu > ul > li:hover,
180
+ header .breadcrumbs .breadcrumb-menu.menu > ul > li:focus-within,
181
+ header .breadcrumbs .breadcrumb-menu.menu > ul > li:hover > span > a,
182
+ header .breadcrumbs .breadcrumb-menu.menu > ul > li:focus-within > span > a,
183
+ header .breadcrumbs .breadcrumb-menu.menu > ul div ul a:hover,
184
+ header .breadcrumbs .breadcrumb-menu.menu > ul div ul a:focus-visible {
185
+ background-color: var(--header-hover-background-color);
186
+ color: var(--header-hover-text-color);
187
+ }
30
188
 
31
- <script setup>
32
- import { ref } from 'vue';
33
- import { watch } from 'vue';
34
- import { useRoute } from 'vue-router';
35
-
36
- const route = useRoute();
37
- const links = ref([]);
38
-
39
- watch(() => route.matched, (matched) => {
40
-
41
- links.value = [];
42
- matched.forEach((record) => {
43
- if (record.meta && record.meta.breadcrumbs) {
44
- record.meta.breadcrumbs(route).forEach((item) => {
45
- links.value.push({
46
- name: item.name,
47
- href: item.href || record.path || '/'
48
- });
49
- });
50
- } else if (record.name) {
51
- links.value.push({
52
- name: record.name,
53
- href: record.path || '/'
54
- });
55
- }
56
- });
57
- }, { immediate: true });
58
- </script>
189
+ header .breadcrumbs a.breadcrumb-link:focus-visible {
190
+ outline-color: var(--header-focus-outline-color);
191
+ }
192
+
193
+ header .breadcrumb-menu.menu > ul > li {
194
+ border-radius: 0.25em;
195
+ }
196
+
197
+ header .breadcrumb-menu.menu > ul li span.submenu .breadcrumb-link {
198
+ text-decoration: inherit;
199
+ }
200
+
201
+ header .breadcrumb-menu.menu > ul li span.submenu .breadcrumb-link[aria-current="page"] {
202
+ text-decoration: underline;
203
+ font-weight: 500;
204
+ }
205
+ </style>
@@ -12,6 +12,8 @@ export interface CalendarEvent {
12
12
  icon?: unknown
13
13
  /** Optional CSS color overriding the default event background. */
14
14
  color?: string
15
+ /** Optional CSS color for event title/time when using a custom background. */
16
+ foregroundColor?: string
15
17
  [key: string]: any
16
18
  }
17
19
 
@@ -23,7 +25,7 @@ export interface CalendarEventMoveRequest {
23
25
  /** Calendar cell where the event was dropped. */
24
26
  targetDate: Date
25
27
  /** Invoke exactly once with the outcome. If omitted until timeout, the move is treated as rejected. */
26
- respond: (accepted: boolean) => void
28
+ respond: (_accepted: boolean) => void
27
29
  }
28
30
 
29
31
  export interface CalendarProps {
@@ -41,9 +43,9 @@ export interface CalendarProps {
41
43
  */
42
44
  eventMoveResponseTimeoutMs?: number
43
45
  // Formatter functions
44
- getEventDate?: (event: CalendarEvent) => Date | null
45
- getEventDateRange?: (event: CalendarEvent) => { start: Date | null; end: Date | null }
46
- formatEventTime?: (event: CalendarEvent, date: Date) => string
46
+ getEventDate?: (_event: CalendarEvent) => Date | null
47
+ getEventDateRange?: (_event: CalendarEvent) => { start: Date | null; end: Date | null }
48
+ formatEventTime?: (_event: CalendarEvent, _date: Date) => string
47
49
  // Customization
48
50
  showNavigation?: boolean
49
51
  /** When true, day titles show ordinal + full month name (e.g. "21st July") instead of the day number alone. */
@@ -87,8 +89,8 @@ const currentDate = computed(() => {
87
89
  return internalCurrentDate.value
88
90
  })
89
91
 
90
- const currentMonth = computed(() => currentDate.value.getMonth())
91
- const currentYear = computed(() => currentDate.value.getFullYear())
92
+ const viewMonth = computed(() => currentDate.value.getMonth())
93
+ const viewYear = computed(() => currentDate.value.getFullYear())
92
94
 
93
95
  // Compute the first visible day (Monday) for the current month view
94
96
  function getStartOfGrid(date: Date): Date {
@@ -130,7 +132,7 @@ function isPastDay(date: Date): boolean {
130
132
  }
131
133
 
132
134
  // Get event date range
133
- function getEventDateRange(event: CalendarEvent): { start: Date | null; end: Date | null } {
135
+ function resolveEventDateRange(event: CalendarEvent): { start: Date | null; end: Date | null } {
134
136
  if (props.getEventDateRange) {
135
137
  return props.getEventDateRange(event)
136
138
  }
@@ -158,7 +160,7 @@ function getEventsForDate(date: Date): CalendarEvent[] {
158
160
  targetDate.setHours(0, 0, 0, 0)
159
161
 
160
162
  return props.events.filter(event => {
161
- const { start, end } = getEventDateRange(event)
163
+ const { start, end } = resolveEventDateRange(event)
162
164
 
163
165
  if (!start) return false
164
166
 
@@ -177,7 +179,7 @@ function getEventsForDate(date: Date): CalendarEvent[] {
177
179
 
178
180
  // Check if an event is a multi-day event
179
181
  function isMultiDayEvent(event: CalendarEvent): boolean {
180
- const { start, end } = getEventDateRange(event)
182
+ const { start, end } = resolveEventDateRange(event)
181
183
 
182
184
  if (!start || !end) return false
183
185
 
@@ -192,7 +194,7 @@ function isMultiDayEvent(event: CalendarEvent): boolean {
192
194
 
193
195
  // Get the position of an event within a multi-day range for a specific date
194
196
  function getMultiDayPosition(event: CalendarEvent, date: Date): 'start' | 'middle' | 'end' | 'single' {
195
- const { start, end } = getEventDateRange(event)
197
+ const { start, end } = resolveEventDateRange(event)
196
198
 
197
199
  if (!start || !end) return 'single'
198
200
 
@@ -218,7 +220,7 @@ function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
218
220
  return props.formatEventTime(event, date)
219
221
  }
220
222
 
221
- const { start, end } = getEventDateRange(event)
223
+ const { start, end } = resolveEventDateRange(event)
222
224
  const position = getMultiDayPosition(event, date)
223
225
 
224
226
  if (!start || !end) return 'No time'
@@ -237,8 +239,14 @@ function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
237
239
  }
238
240
 
239
241
  function getEventStyle(event: CalendarEvent): Record<string, string> | undefined {
240
- if (!event.color) return undefined
241
- return { '--event-color': event.color }
242
+ const style: Record<string, string> = {}
243
+ if (event.color) {
244
+ style['--event-color'] = event.color
245
+ }
246
+ if (event.foregroundColor) {
247
+ style['--event-foreground-color'] = event.foregroundColor
248
+ }
249
+ return Object.keys(style).length > 0 ? style : undefined
242
250
  }
243
251
 
244
252
  // Get ordinal suffix for a number (1st, 2nd, 3rd, 4th, etc.)
@@ -275,25 +283,25 @@ const calendarDays = computed(() => {
275
283
  // Navigation functions
276
284
  function previousMonth() {
277
285
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
278
- const newMonth = currentMonth.value === 0 ? 11 : currentMonth.value - 1
279
- const newYear = currentMonth.value === 0 ? currentYear.value - 1 : currentYear.value
286
+ const newMonth = viewMonth.value === 0 ? 11 : viewMonth.value - 1
287
+ const newYear = viewMonth.value === 0 ? viewYear.value - 1 : viewYear.value
280
288
  emit('month-change', newMonth, newYear)
281
289
  // Optimistically update grid anchor for smooth transition
282
290
  gridStartDate.value = getStartOfGrid(new Date(newYear, newMonth, 1))
283
291
  } else {
284
- internalCurrentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
292
+ internalCurrentDate.value = new Date(viewYear.value, viewMonth.value - 1, 1)
285
293
  gridStartDate.value = getStartOfGrid(internalCurrentDate.value)
286
294
  }
287
295
  }
288
296
 
289
297
  function nextMonth() {
290
298
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
291
- const newMonth = currentMonth.value === 11 ? 0 : currentMonth.value + 1
292
- const newYear = currentMonth.value === 11 ? currentYear.value + 1 : currentYear.value
299
+ const newMonth = viewMonth.value === 11 ? 0 : viewMonth.value + 1
300
+ const newYear = viewMonth.value === 11 ? viewYear.value + 1 : viewYear.value
293
301
  emit('month-change', newMonth, newYear)
294
302
  gridStartDate.value = getStartOfGrid(new Date(newYear, newMonth, 1))
295
303
  } else {
296
- internalCurrentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
304
+ internalCurrentDate.value = new Date(viewYear.value, viewMonth.value + 1, 1)
297
305
  gridStartDate.value = getStartOfGrid(internalCurrentDate.value)
298
306
  }
299
307
  }
@@ -325,7 +333,7 @@ function adjustByWeeks(weeks: number): void {
325
333
 
326
334
  if (props.currentMonth !== undefined && props.currentYear !== undefined) {
327
335
  const { month, year } = getVisibleMonthYear(newStart)
328
- if (month !== currentMonth.value || year !== currentYear.value) {
336
+ if (month !== viewMonth.value || year !== viewYear.value) {
329
337
  emit('month-change', month, year)
330
338
  // Set flag to prevent watcher from resetting our scroll position
331
339
  isScrolling.value = true
@@ -589,9 +597,9 @@ function onDayDrop(dayDate: Date, ev: DragEvent): void {
589
597
  }
590
598
 
591
599
  // Sync grid start when controlled month/year props change (but not during scrolling)
592
- watch([currentMonth, currentYear], () => {
600
+ watch([viewMonth, viewYear], () => {
593
601
  if (isScrolling.value) return
594
- const anchor = new Date(currentYear.value, currentMonth.value, 1)
602
+ const anchor = new Date(viewYear.value, viewMonth.value, 1)
595
603
  gridStartDate.value = getStartOfGrid(anchor)
596
604
  })
597
605
 
@@ -600,7 +608,7 @@ watch([currentMonth, currentYear], () => {
600
608
  <template>
601
609
  <div class="calendar-wrapper">
602
610
  <div v-if="showNavigation" class="calendar-header-nav">
603
- <h2 class="calendar-title">{{ monthNames[currentMonth] }} {{ currentYear }}</h2>
611
+ <h2 class="calendar-title">{{ monthNames[viewMonth] }} {{ viewYear }}</h2>
604
612
  <div class="calendar-nav-buttons">
605
613
  <slot name="nav-buttons">
606
614
  <button @click="previousMonth" class="button neutral">‹</button>
@@ -638,8 +646,8 @@ watch([currentMonth, currentYear], () => {
638
646
  :class="{
639
647
  'today': day.date.toDateString() === new Date().toDateString(),
640
648
  'weekend': day.date.getDay() === 0 || day.date.getDay() === 6,
641
- 'prev-month': day.date.getMonth() !== currentMonth && day.date.getMonth() !== (currentMonth + 1) % 12,
642
- 'next-month': day.date.getMonth() !== currentMonth && day.date.getMonth() === (currentMonth + 1) % 12,
649
+ 'prev-month': day.date.getMonth() !== viewMonth && day.date.getMonth() !== (viewMonth + 1) % 12,
650
+ 'next-month': day.date.getMonth() !== viewMonth && day.date.getMonth() === (viewMonth + 1) % 12,
643
651
  'past': isPastDay(day.date),
644
652
  'range-single': rangeHighlightForDate(day.date) === 'single',
645
653
  'range-start': rangeHighlightForDate(day.date) === 'start',
@@ -980,6 +988,18 @@ watch([currentMonth, currentYear], () => {
980
988
  filter: brightness(0.92);
981
989
  }
982
990
 
991
+ .calendar-event.has-custom-color .event-title {
992
+ color: var(--event-foreground-color, var(--calendar-event-title-fg));
993
+ }
994
+
995
+ .calendar-event.has-custom-color .event-time {
996
+ color: var(--event-foreground-color, var(--calendar-muted-fg));
997
+ }
998
+
999
+ .calendar-event.has-custom-color .multi-day-indicator {
1000
+ color: var(--event-foreground-color, var(--calendar-accent-fg));
1001
+ }
1002
+
983
1003
  .event-icon {
984
1004
  flex-shrink: 0;
985
1005
  }