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
@@ -0,0 +1,196 @@
1
+ import { Pin02Icon } from '@hugeicons/core-free-icons'
2
+
3
+ export const topBarHubRoutes = [
4
+ 'NavLayoutsHub',
5
+ 'NavDataDisplayHub',
6
+ 'NavFormsInputHub',
7
+ 'NavFeedbackStatusHub',
8
+ 'NavPatternsHub',
9
+ ]
10
+
11
+ export function helloWorld() {
12
+ alert('Hello World')
13
+ }
14
+
15
+ export const navSectionHubs = {
16
+ NavLayoutsHub: {
17
+ title: 'Layouts',
18
+ subtitle: 'Page structure, navigation, and application shell',
19
+ sectionId: 'nav-layouts',
20
+ links: [
21
+ {
22
+ name: 'PageStructure',
23
+ options: { description: 'Configure the example app shell' },
24
+ },
25
+ {
26
+ name: 'ThemePreview',
27
+ title: 'Theme Switcher',
28
+ options: { description: 'Drop-in themes on Femtocrank' },
29
+ },
30
+ {
31
+ name: 'ViewItem',
32
+ title: 'View item',
33
+ options: { params: { id: 1 }, description: 'Detail view with breadcrumbs' },
34
+ },
35
+ {
36
+ name: 'NavigationGridExample',
37
+ title: 'Navigation Grid',
38
+ options: { description: 'Icon grid for settings hubs' },
39
+ },
40
+ {
41
+ name: 'Admin',
42
+ options: { description: 'Admin-style section layout' },
43
+ },
44
+ {
45
+ name: 'TabsExample',
46
+ title: 'Tabs',
47
+ options: { description: 'Tabbed content regions' },
48
+ },
49
+ ],
50
+ },
51
+ NavDataDisplayHub: {
52
+ title: 'Data display',
53
+ subtitle: 'Tables, calendars, and read-only output',
54
+ sectionId: 'nav-data',
55
+ links: [
56
+ {
57
+ name: 'TableExample',
58
+ title: 'Table',
59
+ options: { description: 'Sorting, pagination, and filters' },
60
+ },
61
+ {
62
+ name: 'TableRemoteExample',
63
+ title: 'Remote table',
64
+ options: { description: 'Server-style fetchRows pagination and filters' },
65
+ },
66
+ {
67
+ name: 'CalendarExample',
68
+ title: 'Calendar',
69
+ options: { description: 'Month view with events' },
70
+ },
71
+ {
72
+ name: 'ReadOnlyTextAreaExample',
73
+ title: 'Read-only output',
74
+ options: { description: 'Copyable read-only text areas' },
75
+ },
76
+ ],
77
+ },
78
+ NavFormsInputHub: {
79
+ title: 'Forms & input',
80
+ subtitle: 'Buttons, forms, and authentication',
81
+ sectionId: 'nav-forms',
82
+ links: [
83
+ {
84
+ name: 'ButtonsExample',
85
+ title: 'Buttons',
86
+ options: { description: 'Button variants and toolbars' },
87
+ },
88
+ {
89
+ name: 'FormExample',
90
+ title: 'Forms',
91
+ options: { description: 'Labels, inputs, and fieldsets' },
92
+ },
93
+ {
94
+ name: 'LoginExample',
95
+ title: 'Login',
96
+ options: { description: 'Auth form with OAuth tabs' },
97
+ },
98
+ ],
99
+ },
100
+ NavFeedbackStatusHub: {
101
+ title: 'Feedback & status',
102
+ subtitle: 'Notifications, dialogs, and inline status',
103
+ sectionId: 'nav-feedback',
104
+ links: [
105
+ {
106
+ name: 'StatusExample',
107
+ title: 'Status & notifications',
108
+ options: { description: 'Block, inline, and popup notifications' },
109
+ },
110
+ {
111
+ name: 'DialogExample',
112
+ title: 'Dialog',
113
+ options: { description: 'Modal dialogs and confirmations' },
114
+ },
115
+ ],
116
+ },
117
+ NavPatternsHub: {
118
+ title: 'Patterns',
119
+ subtitle: 'Reusable UI patterns and interactions',
120
+ sectionId: 'nav-patterns',
121
+ links: [
122
+ {
123
+ type: 'callback',
124
+ title: 'Callback example',
125
+ callback: helloWorld,
126
+ options: {
127
+ icon: Pin02Icon,
128
+ description: 'Navigation item that runs a function instead of routing',
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ }
134
+
135
+ function registerNavLink(nav, link, { onHelloWorld = helloWorld } = {}) {
136
+ if (link.type === 'callback') {
137
+ const callback = link.callback ?? onHelloWorld
138
+ nav.addCallback(link.title, callback, link.options ?? {})
139
+ return
140
+ }
141
+
142
+ nav.addRouterLink(link.name, link.title ?? null, link.options ?? {})
143
+ }
144
+
145
+ /** Flat example list for the sidebar — hub routes are not registered here. */
146
+ export function registerSidebarNavigation(nav, { onHelloWorld = helloWorld } = {}) {
147
+ if (!nav) {
148
+ return
149
+ }
150
+
151
+ nav.addRouterLink('Welcome')
152
+
153
+ for (const routeName of topBarHubRoutes) {
154
+ const hub = navSectionHubs[routeName]
155
+ nav.addSection(hub.title, { name: hub.sectionId })
156
+
157
+ for (const link of hub.links) {
158
+ registerNavLink(nav, link, { onHelloWorld })
159
+ }
160
+ }
161
+ }
162
+
163
+ /** Hub category links for the header top bar. */
164
+ export function registerTopBarNavigation(nav) {
165
+ if (!nav) {
166
+ return
167
+ }
168
+
169
+ for (const routeName of topBarHubRoutes) {
170
+ nav.addRouterLink(routeName)
171
+ }
172
+ }
173
+
174
+ /** Example links for a single hub page (NavigationGrid). */
175
+ export function registerHubNavigation(nav, hubRouteName, { onHelloWorld = helloWorld } = {}) {
176
+ const hub = navSectionHubs[hubRouteName]
177
+ if (!nav || !hub) {
178
+ return
179
+ }
180
+
181
+ for (const link of hub.links) {
182
+ registerNavLink(nav, link, { onHelloWorld })
183
+ }
184
+ }
185
+
186
+ /** Hub overview grid for the welcome page. */
187
+ export function registerWelcomeHubGrid(nav) {
188
+ if (!nav) {
189
+ return
190
+ }
191
+
192
+ for (const routeName of topBarHubRoutes) {
193
+ const hub = navSectionHubs[routeName]
194
+ nav.addRouterLink(routeName, hub.title, { description: hub.subtitle })
195
+ }
196
+ }
@@ -0,0 +1,90 @@
1
+ import { navSectionHubs, topBarHubRoutes } from './navSectionHubs.js'
2
+
3
+ /** Example route name → hub route name (from navSectionHubs). */
4
+ const routeToHub = Object.fromEntries(
5
+ topBarHubRoutes.flatMap((hubRouteName) =>
6
+ navSectionHubs[hubRouteName].links
7
+ .filter((link) => link.name)
8
+ .map((link) => [link.name, hubRouteName]),
9
+ ),
10
+ )
11
+
12
+ export function welcomeCrumb() {
13
+ return { name: 'Welcome', to: { name: 'Welcome' } }
14
+ }
15
+
16
+ export function hubMenuItems() {
17
+ return topBarHubRoutes.map((hubRouteName) => {
18
+ const hub = navSectionHubs[hubRouteName]
19
+ return {
20
+ name: hub.title,
21
+ to: { name: hubRouteName },
22
+ }
23
+ })
24
+ }
25
+
26
+ /** Hub section picker — all example categories in one dropdown. */
27
+ export function hubMenuCrumb(activeHubRouteName) {
28
+ const hub = navSectionHubs[activeHubRouteName]
29
+ return {
30
+ name: hub.title,
31
+ to: { name: activeHubRouteName },
32
+ menu: hubMenuItems(),
33
+ }
34
+ }
35
+
36
+ export function routeCrumb(routeName, title, params = undefined) {
37
+ return {
38
+ name: title,
39
+ to: params ? { name: routeName, params } : { name: routeName },
40
+ }
41
+ }
42
+
43
+ /** Welcome → hub section (hub landing pages). */
44
+ export function hubSectionBreadcrumbs(hubRouteName) {
45
+ return () => [welcomeCrumb(), hubMenuCrumb(hubRouteName)]
46
+ }
47
+
48
+ /** Welcome → hub menu → example page (most grid-linked examples). */
49
+ export function hubChildBreadcrumbs(hubRouteName, routeName, title) {
50
+ return () => [
51
+ welcomeCrumb(),
52
+ hubMenuCrumb(hubRouteName),
53
+ routeCrumb(routeName, title),
54
+ ]
55
+ }
56
+
57
+ /** Resolve hub for a route name when only the child is known. */
58
+ export function hubForRoute(routeName) {
59
+ return routeToHub[routeName] ?? null
60
+ }
61
+
62
+ const viewItemLabels = {
63
+ 1: 'Acme deployment',
64
+ Alice: 'Alice',
65
+ Bob: 'Bob',
66
+ }
67
+
68
+ export function viewItemLabel(id) {
69
+ const key = String(id)
70
+ if (viewItemLabels[key]) {
71
+ return viewItemLabels[key]
72
+ }
73
+ return key.charAt(0).toUpperCase() + key.slice(1)
74
+ }
75
+
76
+ /** Table row detail: Welcome → hub menu → Table → record. */
77
+ export function viewItemBreadcrumbs(route) {
78
+ const id = route.params.id
79
+ return [
80
+ welcomeCrumb(),
81
+ hubMenuCrumb('NavDataDisplayHub'),
82
+ routeCrumb('TableExample', 'Table'),
83
+ routeCrumb('ViewItem', viewItemLabel(id), { id }),
84
+ ]
85
+ }
86
+
87
+ /** Standalone pages with no hub parent. */
88
+ export function standaloneBreadcrumbs(routeName, title) {
89
+ return () => [welcomeCrumb(), routeCrumb(routeName, title)]
90
+ }
@@ -10,28 +10,28 @@
10
10
  <p>Welcome to the administrative panel. This is a placeholder for administrative functionality.</p>
11
11
 
12
12
  <div class="grid-boxed">
13
- <div class="stat-display">
13
+ <div class="stat-display good">
14
14
  <h4>Total Users</h4>
15
- <div class="stat fg-good">1,247</div>
16
- <div class="subtle">+12% from last month</div>
15
+ <span class="stat">1,247</span>
16
+ <span class="subtle">+12% from last month</span>
17
17
  </div>
18
18
 
19
- <div class="stat-display">
19
+ <div class="stat-display bad">
20
20
  <h4>Active Sessions</h4>
21
- <div class="stat fg-bad">89</div>
22
- <div class="subtle">-3% from last hour</div>
21
+ <span class="stat">89</span>
22
+ <span class="subtle">-3% from last hour</span>
23
23
  </div>
24
24
 
25
- <div class="stat-display">
25
+ <div class="stat-display warning">
26
26
  <h4>System Load</h4>
27
- <div class="stat fg-warning">67%</div>
28
- <div class="subtle">Moderate</div>
27
+ <span class="stat">67%</span>
28
+ <span class="subtle">Moderate</span>
29
29
  </div>
30
30
 
31
- <div class="stat-display">
31
+ <div class="stat-display good">
32
32
  <h4>Storage Used</h4>
33
- <div class="stat fg-good">2.4 GB</div>
34
- <div class="subtle">45% of total</div>
33
+ <span class="stat">2.4 GB</span>
34
+ <span class="subtle">45% of total</span>
35
35
  </div>
36
36
  </div>
37
37
  </div>
@@ -1,6 +1,21 @@
1
1
  <template>
2
2
  <Navigation ref="navigation">
3
- <Header username = "Guest" @toggleSidebar="toggleSidebar" @logoClick="goToIndex" @userClick="goToUserDetails" title = "PicoCrank" :logoUrl="logoUrl" :sidebarEnabled="sidebarEnabled" :navigation="navigation" :topBarEnabled="topBarEnabled" :showBranding="brandingEnabled" :breadcrumbs="breadcrumbsEnabled" :themeToggleEnabled="themeToggleEnabled">
3
+ <Navigation ref="topBarNavigation">
4
+ <Header
5
+ username="Guest"
6
+ @toggleSidebar="toggleSidebar"
7
+ @logoClick="goToIndex"
8
+ @userClick="goToUserDetails"
9
+ title="PicoCrank"
10
+ :logoUrl="logoUrl"
11
+ :sidebarEnabled="sidebarEnabled"
12
+ :navigation="navigation"
13
+ :topBarNavigation="topBarNavigation"
14
+ :topBarEnabled="topBarEnabled"
15
+ :showBranding="brandingEnabled"
16
+ :breadcrumbs="breadcrumbsEnabled"
17
+ :themeToggleEnabled="themeToggleEnabled"
18
+ >
4
19
  <template #toolbar>
5
20
  <ExampleHeaderThemeSwitcher
6
21
  v-if="themeSwitcherEnabled"
@@ -16,6 +31,7 @@
16
31
  />
17
32
  </template>
18
33
  </Header>
34
+ </Navigation>
19
35
 
20
36
  <div id = "layout">
21
37
  <Sidebar v-if="sidebarEnabled" ref = "sidebar" />
@@ -38,7 +54,7 @@
38
54
  <script setup>
39
55
  import { ref, onMounted, provide, watch } from 'vue';
40
56
  import { useRouter } from 'vue-router';
41
- import { Pin02Icon, UserIcon } from '@hugeicons/core-free-icons'
57
+ import { UserIcon } from '@hugeicons/core-free-icons'
42
58
 
43
59
  import QuickSearch from './../components/QuickSearch.vue'
44
60
  import ExampleHeaderThemeSwitcher from './ExampleHeaderThemeSwitcher.vue'
@@ -48,12 +64,18 @@
48
64
  import '../composables/useTheme.js'
49
65
  import { initCustomTheme } from '../composables/useCustomTheme.js'
50
66
  import { examplePeople } from '../data/examplePeople.js'
67
+ import {
68
+ helloWorld,
69
+ registerSidebarNavigation,
70
+ registerTopBarNavigation,
71
+ } from '../data/navSectionHubs.js'
51
72
  import logoUrl from '/logo.png';
52
73
 
53
74
  const router = useRouter();
54
75
  const quickSearchRef = ref(null)
55
76
  const sidebar = ref(null);
56
77
  const navigation = ref(null);
78
+ const topBarNavigation = ref(null);
57
79
  const sidebarEnabled = ref(true);
58
80
  const brandingEnabled = ref(true);
59
81
  const quickSearchEnabled = ref(true);
@@ -62,7 +84,7 @@
62
84
  const themeSwitcherEnabled = ref(true);
63
85
  const includeSupplementalThemes = ref(true);
64
86
  const topBarEnabled = ref(false);
65
- const breadcrumbsEnabled = ref(false);
87
+ const breadcrumbsEnabled = ref(true);
66
88
 
67
89
  const { discoverThemes } = initCustomTheme({ includeSupplementalThemes: true });
68
90
 
@@ -188,35 +210,8 @@
188
210
  onMounted(() => {
189
211
  discoverThemes()
190
212
 
191
- if (navigation.value) {
192
- navigation.value.addRouterLink('Welcome')
193
-
194
- navigation.value.addSection('Layouts', { name: 'nav-layouts' })
195
- navigation.value.addRouterLink('PageStructure', 'Page structure')
196
- navigation.value.addRouterLink('ThemePreview', 'Theme Switcher')
197
- navigation.value.addRouterLink('ViewItem', 'View item', { params: { id: 1 } })
198
- navigation.value.addRouterLink('NavigationGridExample', 'Navigation Grid')
199
- navigation.value.addRouterLink('Admin')
200
- navigation.value.addRouterLink('TabsExample', 'Tabs')
201
-
202
- navigation.value.addSection('Data display', { name: 'nav-data' })
203
- navigation.value.addRouterLink('TableExample', 'Table')
204
- navigation.value.addRouterLink('TableRemoteExample', 'Remote table')
205
- navigation.value.addRouterLink('CalendarExample', 'Calendar')
206
- navigation.value.addRouterLink('ReadOnlyTextAreaExample', 'Read-only output')
207
-
208
- navigation.value.addSection('Forms & input', { name: 'nav-forms' })
209
- navigation.value.addRouterLink('ButtonsExample', 'Buttons')
210
- navigation.value.addRouterLink('FormExample', 'Forms')
211
- navigation.value.addRouterLink('LoginExample', 'Login')
212
-
213
- navigation.value.addSection('Feedback & status', { name: 'nav-feedback' })
214
- navigation.value.addRouterLink('StatusExample', 'Status & notifications')
215
- navigation.value.addRouterLink('DialogExample', 'Dialog')
216
-
217
- navigation.value.addSection('Patterns', { name: 'nav-patterns' })
218
- navigation.value.addCallback('Callback example', helloWorld, { icon: Pin02Icon })
219
- }
213
+ registerSidebarNavigation(navigation.value, { onHelloWorld: helloWorld })
214
+ registerTopBarNavigation(topBarNavigation.value)
220
215
 
221
216
  if (sidebar.value) {
222
217
  sidebar.value.open();
@@ -226,8 +221,4 @@
226
221
  registerQuickSearchItems()
227
222
  });
228
223
 
229
- function helloWorld() {
230
- alert('Hello World')
231
- }
232
-
233
224
  </script>
@@ -53,6 +53,20 @@ import {
53
53
  AlertCircleIcon,
54
54
  } from '@hugeicons/core-free-icons'
55
55
 
56
+ /** Static event palette — background + accessible dark foreground for light pastels in any theme. */
57
+ const EVENT_COLORS = {
58
+ weekend: { color: '#e8d5f5', foregroundColor: '#4a2d6b' },
59
+ teamMeeting: { color: '#d1fae5', foregroundColor: '#14532d' },
60
+ clientCall: { color: '#e0f2fe', foregroundColor: '#075985' },
61
+ projectReview: { color: '#ede9fe', foregroundColor: '#5b21b6' },
62
+ lunchBreak: { color: '#ffedd5', foregroundColor: '#9a3412' },
63
+ codeReview: { color: '#ccfbf1', foregroundColor: '#115e59' },
64
+ conference: { color: '#d6e8ff', foregroundColor: '#1e40af' },
65
+ vacation: { color: '#fff0d6', foregroundColor: '#92400e' },
66
+ holiday: { color: '#ffe0e0', foregroundColor: '#991b1b' },
67
+ deadline: { color: '#fde8e8', foregroundColor: '#7f1d1d' },
68
+ }
69
+
56
70
  const events = ref([])
57
71
  const loading = ref(false)
58
72
  const error = ref(null)
@@ -99,30 +113,32 @@ function generateDummyEventsForMonth(monthIndex, year) {
99
113
  date: new Date(year, monthIndex, day, 10, 0),
100
114
  description: 'Relaxing weekend activity',
101
115
  icon: PartyIcon,
102
- color: '#e8d5f5',
116
+ ...EVENT_COLORS.weekend,
103
117
  })
104
118
  }
105
119
 
106
120
  // Add some random events
107
121
  if (random() > 0.7) {
108
122
  const eventTypes = [
109
- { title: 'Team Meeting', time: [9, 0], duration: 1 },
110
- { title: 'Client Call', time: [14, 30], duration: 0.5 },
111
- { title: 'Project Review', time: [16, 0], duration: 2 },
112
- { title: 'Lunch Break', time: [12, 0], duration: 1 },
113
- { title: 'Code Review', time: [10, 0], duration: 1.5 }
123
+ { title: 'Team Meeting', time: [9, 0], duration: 1, ...EVENT_COLORS.teamMeeting },
124
+ { title: 'Client Call', time: [14, 30], duration: 0.5, ...EVENT_COLORS.clientCall },
125
+ { title: 'Project Review', time: [16, 0], duration: 2, ...EVENT_COLORS.projectReview },
126
+ { title: 'Lunch Break', time: [12, 0], duration: 1, ...EVENT_COLORS.lunchBreak },
127
+ { title: 'Code Review', time: [10, 0], duration: 1.5, ...EVENT_COLORS.codeReview },
114
128
  ]
115
129
 
116
130
  const eventType = eventTypes[Math.floor(random() * eventTypes.length)]
117
- const [hours, minutes] = eventType.time
131
+ const { title, time, color, foregroundColor } = eventType
132
+ const [hours, minutes] = time
118
133
 
119
134
  dummyEvents.push({
120
135
  id: `event-${year}-${monthIndex}-${day}-${Math.random().toString(36).substr(2, 9)}`,
121
- title: eventType.title,
136
+ title,
122
137
  date: new Date(year, monthIndex, day, hours, minutes),
123
- description: `Scheduled ${eventType.title.toLowerCase()}`,
124
- icon: eventType.title === 'Team Meeting' ? MeetingRoomIcon : undefined,
125
- color: eventType.title === 'Client Call' ? '#e0f2fe' : undefined,
138
+ description: `Scheduled ${title.toLowerCase()}`,
139
+ icon: title === 'Team Meeting' ? MeetingRoomIcon : undefined,
140
+ color,
141
+ foregroundColor,
126
142
  })
127
143
  }
128
144
  }
@@ -135,7 +151,7 @@ function generateDummyEventsForMonth(monthIndex, year) {
135
151
  endDate: new Date(year, monthIndex, 7, 17, 0),
136
152
  description: 'Annual technology conference',
137
153
  icon: Calendar01Icon,
138
- color: '#d6e8ff',
154
+ ...EVENT_COLORS.conference,
139
155
  })
140
156
 
141
157
  dummyEvents.push({
@@ -145,7 +161,7 @@ function generateDummyEventsForMonth(monthIndex, year) {
145
161
  endDate: new Date(year, monthIndex, 18, 23, 59),
146
162
  description: 'Family vacation time',
147
163
  icon: BeachIcon,
148
- color: '#fff0d6',
164
+ ...EVENT_COLORS.vacation,
149
165
  })
150
166
 
151
167
  // Add some all-day events
@@ -155,7 +171,7 @@ function generateDummyEventsForMonth(monthIndex, year) {
155
171
  date: new Date(year, monthIndex, 1, 0, 0),
156
172
  description: 'National holiday - office closed',
157
173
  icon: PartyIcon,
158
- color: '#ffe0e0',
174
+ ...EVENT_COLORS.holiday,
159
175
  })
160
176
 
161
177
  if (daysInMonth >= 25) {
@@ -165,7 +181,7 @@ function generateDummyEventsForMonth(monthIndex, year) {
165
181
  date: new Date(year, monthIndex, 25, 0, 0),
166
182
  description: 'Important project deadline',
167
183
  icon: AlertCircleIcon,
168
- color: '#fde8e8',
184
+ ...EVENT_COLORS.deadline,
169
185
  })
170
186
  }
171
187
 
@@ -204,12 +220,6 @@ function getEventsForSurroundingMonths(monthIndex, year) {
204
220
  ]
205
221
  }
206
222
 
207
- // Generate dummy events for the current month (backward compatibility wrapper)
208
- function generateDummyEvents() {
209
- const now = new Date()
210
- return getEventsForMonth(now.getMonth(), now.getFullYear())
211
- }
212
-
213
223
  function handleEventClick(event) {
214
224
  console.log('Event clicked:', event)
215
225
  alert(`Event: ${event.title}\nDescription: ${event.description || 'No description'}`)
@@ -232,7 +242,7 @@ function handleDateRangeSelect(start, end) {
232
242
  function handleEventMoveRequest({ respond, event, sourceDate, targetDate }) {
233
243
  window.setTimeout(() => {
234
244
  const ok = window.confirm(
235
- `Move "${event.title}" to ${targetDate.toLocaleDateString()}? (Cancel = decline)`
245
+ `Move "${event.title}" from ${sourceDate.toLocaleDateString()} to ${targetDate.toLocaleDateString()}? (Cancel = decline)`
236
246
  )
237
247
  respond(ok)
238
248
  }, 50)
@@ -56,7 +56,7 @@ header .header-actions .theme-switcher--compact select {
56
56
  border: 0;
57
57
  border-radius: 0;
58
58
  background-color: transparent;
59
- color: var(--header-fg-color, var(--header-text-color, #fff));
59
+ color: var(--header-fg-color);
60
60
  padding-top: 0;
61
61
  padding-bottom: 0;
62
62
  padding-left: 0.6em;
@@ -9,12 +9,9 @@
9
9
  </template>
10
10
 
11
11
  <script setup>
12
- import { useRouter } from 'vue-router';
13
12
  import Section from '../components/Section.vue';
14
13
  import Login from '../components/Login.vue';
15
14
 
16
- const router = useRouter();
17
-
18
15
  // Handle local login
19
16
  function handleLocalLogin(credentials) {
20
17
  console.log('Local login attempt:', {
@@ -8,7 +8,7 @@
8
8
  @tab-change="onTabChange"
9
9
  >
10
10
  <!-- Custom tab: Magic Link Login -->
11
- <template #tab-magic-link="{ tab }">
11
+ <template #tab-magic-link>
12
12
  <div class="login-section">
13
13
  <form @submit.prevent="handleMagicLinkLogin" class="magic-link-form">
14
14
  <input
@@ -30,7 +30,7 @@
30
30
  </template>
31
31
 
32
32
  <!-- Custom tab: SSO Login -->
33
- <template #tab-sso="{ tab }">
33
+ <template #tab-sso>
34
34
  <div class="login-section">
35
35
  <div class="sso-providers">
36
36
  <button
@@ -54,12 +54,9 @@
54
54
 
55
55
  <script setup>
56
56
  import { ref } from 'vue';
57
- import { useRouter } from 'vue-router';
58
57
  import Section from '../components/Section.vue';
59
58
  import Login from '../components/Login.vue';
60
59
 
61
- const router = useRouter();
62
-
63
60
  // Custom tabs configuration
64
61
  const customTabs = ref([
65
62
  { id: 'magic-link', label: 'Magic Link' },
@@ -232,19 +229,10 @@ function handleSSOLogin(provider) {
232
229
  }
233
230
 
234
231
  .magic-link-form input {
235
- padding: 0.75rem;
236
- border-radius: 4px;
237
- font-size: 1em;
238
232
  width: 100%;
239
233
  box-sizing: border-box;
240
234
  }
241
235
 
242
- .magic-link-form input:focus {
243
- outline: none;
244
- border-color: var(--primary-color, #007bff);
245
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
246
- }
247
-
248
236
  .magic-link-form button,
249
237
  .sso-button {
250
238
  width: 100%;
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <Section v-if="hub" :title="hub.title" :subtitle="hub.subtitle">
3
+ <Navigation ref="localNavigation">
4
+ <NavigationGrid />
5
+ </Navigation>
6
+ </Section>
7
+ <Section
8
+ v-else
9
+ title="Unknown section"
10
+ subtitle="This navigation hub is not configured."
11
+ >
12
+ <p class="subtle">
13
+ Return to the welcome page and pick an example category from the top bar or sidebar.
14
+ </p>
15
+ <router-link :to="{ name: 'Welcome' }" class="button neutral">
16
+ Back to welcome
17
+ </router-link>
18
+ </Section>
19
+ </template>
20
+
21
+ <script setup>
22
+ import { ref, onMounted, computed } from 'vue'
23
+ import { useRoute } from 'vue-router'
24
+ import Section from '../components/Section.vue'
25
+ import Navigation from '../components/Navigation.vue'
26
+ import NavigationGrid from '../components/NavigationGrid.vue'
27
+ import { navSectionHubs, registerHubNavigation } from '../data/navSectionHubs.js'
28
+
29
+ const route = useRoute()
30
+ const localNavigation = ref(null)
31
+ const hub = computed(() => navSectionHubs[route.name])
32
+
33
+ onMounted(() => {
34
+ registerHubNavigation(localNavigation.value, route.name)
35
+ })
36
+ </script>