picocrank 1.25.2 → 1.27.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 (51) hide show
  1. package/eslint.config.js +75 -0
  2. package/package.json +17 -4
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +20 -11
  5. package/styles/picocrank-extensions.css +123 -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/vite.config.js +5 -0
  11. package/vue/components/Breadcrumbs.vue +192 -45
  12. package/vue/components/Calendar.vue +162 -145
  13. package/vue/components/DangerZone.vue +186 -0
  14. package/vue/components/FormField.spec.js +350 -0
  15. package/vue/components/FormField.vue +236 -32
  16. package/vue/components/FormLayout.spec.js +56 -0
  17. package/vue/components/Header.vue +22 -6
  18. package/vue/components/Login.vue +152 -41
  19. package/vue/components/NavigationGrid.vue +26 -180
  20. package/vue/components/NotificationBlock.vue +120 -0
  21. package/vue/components/NotificationPopup.vue +44 -41
  22. package/vue/components/Pagination.vue +5 -4
  23. package/vue/components/ReadOnlyTextArea.vue +1 -1
  24. package/vue/components/SidebarNavLink.vue +3 -3
  25. package/vue/components/Table.vue +107 -79
  26. package/vue/components/TableColumnOptionsPopover.vue +9 -0
  27. package/vue/components/Tabs.spec.js +101 -0
  28. package/vue/components/Tabs.vue +455 -81
  29. package/vue/components/TopBar.vue +2 -2
  30. package/vue/components/form-validation.spec.js +126 -0
  31. package/vue/composables/tableColumnPriorities.js +33 -0
  32. package/vue/composables/tableFilters.js +1 -1
  33. package/vue/data/navSectionHubs.js +196 -0
  34. package/vue/data/routeBreadcrumbs.js +90 -0
  35. package/vue/examples/Admin.vue +12 -12
  36. package/vue/examples/App.vue +27 -36
  37. package/vue/examples/CalendarExample.vue +32 -22
  38. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  39. package/vue/examples/FormExample.vue +76 -48
  40. package/vue/examples/Login.vue +5 -3
  41. package/vue/examples/LoginExample.vue +99 -15
  42. package/vue/examples/NavSectionHub.vue +36 -0
  43. package/vue/examples/PageStructure.vue +15 -12
  44. package/vue/examples/StatusExample.vue +248 -129
  45. package/vue/examples/TableExample.vue +72 -71
  46. package/vue/examples/TabsExample.vue +236 -257
  47. package/vue/examples/ThemePreview.vue +21 -23
  48. package/vue/examples/ViewItem.vue +32 -5
  49. package/vue/examples/Welcome.vue +3 -40
  50. package/vue/test/formTestUtils.js +279 -0
  51. package/vue/test/setup.js +5 -0
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ flushControlState,
4
+ MIN_COMMENTS_LENGTH,
5
+ mountLoginValidationForm,
6
+ mountValidationForm,
7
+ } from '../test/formTestUtils.js'
8
+
9
+ describe('form validation integration', () => {
10
+ it('blocks submit and surfaces native required validation for empty email', async () => {
11
+ const { wrapper, submitted } = mountValidationForm()
12
+
13
+ await wrapper.find('button[type="submit"]').trigger('click')
14
+ await flushControlState()
15
+
16
+ expect(submitted.value).toBe(false)
17
+ expect(wrapper.find('#email').element.validationMessage).not.toBe('')
18
+ })
19
+
20
+ it('shows computed email format errors as the user types', async () => {
21
+ const { wrapper } = mountValidationForm()
22
+
23
+ await wrapper.find('#email').setValue('not-an-email')
24
+ await flushControlState()
25
+
26
+ expect(wrapper.find('#email').element.getAttribute('aria-invalid')).toBe('true')
27
+ expect(wrapper.text()).toContain('Enter a valid email address.')
28
+ })
29
+
30
+ it('clears email errors when a valid address is entered', async () => {
31
+ const { wrapper } = mountValidationForm()
32
+
33
+ await wrapper.find('#email').setValue('not-an-email')
34
+ await flushControlState()
35
+ await wrapper.find('#email').setValue('user@example.com')
36
+ await flushControlState()
37
+
38
+ expect(wrapper.find('#email').element.hasAttribute('aria-invalid')).toBe(false)
39
+ expect(wrapper.findAll('.form-field-error')).toHaveLength(0)
40
+ })
41
+
42
+ it('shows comments length validation only when comments are non-empty', async () => {
43
+ const { wrapper } = mountValidationForm()
44
+
45
+ await wrapper.find('#email').setValue('user@example.com')
46
+ await wrapper.find('#comments').setValue('Too short')
47
+ await flushControlState()
48
+
49
+ expect(wrapper.text()).toContain(`Comments must be at least ${MIN_COMMENTS_LENGTH} characters.`)
50
+ })
51
+
52
+ it('allows submit when email is valid and comments are empty', async () => {
53
+ const { wrapper, submitted } = mountValidationForm()
54
+
55
+ await wrapper.find('#email').setValue('user@example.com')
56
+ await wrapper.find('button[type="submit"]').trigger('click')
57
+ await flushControlState()
58
+
59
+ expect(submitted.value).toBe(true)
60
+ })
61
+
62
+ it('blocks submit for conflicting accessibility selections via validate()', async () => {
63
+ const { wrapper, submitted, formData } = mountValidationForm()
64
+
65
+ await wrapper.find('#email').setValue('user@example.com')
66
+ formData.value.accessibility = ['high-contrast', 'low-contrast']
67
+ await flushControlState()
68
+
69
+ await wrapper.find('button[type="submit"]').trigger('click')
70
+ await flushControlState()
71
+
72
+ expect(submitted.value).toBe(false)
73
+ expect(wrapper.text()).toContain('High contrast and low contrast UI cannot both be selected.')
74
+ })
75
+
76
+ it('submits when accessibility selections are compatible', async () => {
77
+ const { wrapper, submitted, formData } = mountValidationForm()
78
+
79
+ await wrapper.find('#email').setValue('user@example.com')
80
+ formData.value.accessibility = ['high-contrast']
81
+ await flushControlState()
82
+
83
+ await wrapper.find('button[type="submit"]').trigger('click')
84
+ await flushControlState()
85
+
86
+ expect(submitted.value).toBe(true)
87
+ })
88
+ })
89
+
90
+ describe('Login-style server validation', () => {
91
+ it('maps password errors to the password field', async () => {
92
+ const { wrapper } = mountLoginValidationForm()
93
+
94
+ wrapper.vm.applyServerErrors({ password: 'Incorrect password.' })
95
+ await flushControlState()
96
+
97
+ expect(wrapper.find('#password').element.getAttribute('aria-invalid')).toBe('true')
98
+ expect(wrapper.text()).toContain('Incorrect password.')
99
+ expect(wrapper.find('.login-form-error').exists()).toBe(false)
100
+ })
101
+
102
+ it('maps server failures to a form-level NotificationBlock', async () => {
103
+ const { wrapper } = mountLoginValidationForm()
104
+
105
+ wrapper.vm.applyServerErrors({ form: 'Unable to reach authentication service.' })
106
+ await flushControlState()
107
+
108
+ expect(wrapper.find('.login-form-error').exists()).toBe(true)
109
+ expect(wrapper.find('.login-form-error').attributes('role')).toBe('alert')
110
+ expect(wrapper.text()).toContain('Unable to reach authentication service.')
111
+ expect(wrapper.find('#username').element.hasAttribute('aria-invalid')).toBe(false)
112
+ })
113
+
114
+ it('clears field errors when the user edits the control', async () => {
115
+ const { wrapper, passwordError } = mountLoginValidationForm()
116
+
117
+ wrapper.vm.applyServerErrors({ password: 'Incorrect password.' })
118
+ await flushControlState()
119
+
120
+ await wrapper.find('#password').setValue('new-secret')
121
+ await flushControlState()
122
+
123
+ expect(passwordError.value).toBe('')
124
+ expect(wrapper.find('#password').element.hasAttribute('aria-invalid')).toBe(false)
125
+ })
126
+ })
@@ -0,0 +1,33 @@
1
+ export function isValidColPriority(priority) {
2
+ return Number.isInteger(priority) && priority >= 1 && priority <= 5
3
+ }
4
+
5
+ /** Default responsive priority by visible column index (0-based). First column has no auto priority. */
6
+ export function autoColumnPriorityForIndex(index) {
7
+ if (index <= 0) {
8
+ return null
9
+ }
10
+ if (index === 1) {
11
+ return 5
12
+ }
13
+ if (index === 2) {
14
+ return 4
15
+ }
16
+ if (index === 3) {
17
+ return 3
18
+ }
19
+ if (index === 4) {
20
+ return 2
21
+ }
22
+ return 1
23
+ }
24
+
25
+ export function defaultColumnPriorityForIndex(header, index, responsiveColumns = true) {
26
+ if (isValidColPriority(header?.colPriority)) {
27
+ return header.colPriority
28
+ }
29
+ if (!responsiveColumns) {
30
+ return null
31
+ }
32
+ return autoColumnPriorityForIndex(index)
33
+ }
@@ -120,7 +120,7 @@ export function filterEntrySpec(entry) {
120
120
  return null
121
121
  }
122
122
 
123
- const { id, name, enabled, ...filter } = entry
123
+ const { id: _id, name: _name, enabled: _enabled, ...filter } = entry
124
124
  return filter
125
125
  }
126
126
 
@@ -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>