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.
- package/eslint.config.js +75 -0
- package/package.json +17 -4
- package/router.js +74 -0
- package/styles/dark-theme.css +20 -11
- package/styles/picocrank-extensions.css +123 -30
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +10 -24
- package/supplemental-themes/dracula-alucard/theme.css +10 -24
- package/supplemental-themes/gruvbox-dark-light/theme.css +10 -24
- package/supplemental-themes/waffles/theme.css +2 -16
- package/vite.config.js +5 -0
- package/vue/components/Breadcrumbs.vue +192 -45
- package/vue/components/Calendar.vue +162 -145
- package/vue/components/DangerZone.vue +186 -0
- package/vue/components/FormField.spec.js +350 -0
- package/vue/components/FormField.vue +236 -32
- package/vue/components/FormLayout.spec.js +56 -0
- package/vue/components/Header.vue +22 -6
- package/vue/components/Login.vue +152 -41
- package/vue/components/NavigationGrid.vue +26 -180
- package/vue/components/NotificationBlock.vue +120 -0
- package/vue/components/NotificationPopup.vue +44 -41
- package/vue/components/Pagination.vue +5 -4
- package/vue/components/ReadOnlyTextArea.vue +1 -1
- package/vue/components/SidebarNavLink.vue +3 -3
- package/vue/components/Table.vue +107 -79
- package/vue/components/TableColumnOptionsPopover.vue +9 -0
- package/vue/components/Tabs.spec.js +101 -0
- package/vue/components/Tabs.vue +455 -81
- package/vue/components/TopBar.vue +2 -2
- package/vue/components/form-validation.spec.js +126 -0
- package/vue/composables/tableColumnPriorities.js +33 -0
- package/vue/composables/tableFilters.js +1 -1
- package/vue/data/navSectionHubs.js +196 -0
- package/vue/data/routeBreadcrumbs.js +90 -0
- package/vue/examples/Admin.vue +12 -12
- package/vue/examples/App.vue +27 -36
- package/vue/examples/CalendarExample.vue +32 -22
- package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
- package/vue/examples/FormExample.vue +76 -48
- package/vue/examples/Login.vue +5 -3
- package/vue/examples/LoginExample.vue +99 -15
- package/vue/examples/NavSectionHub.vue +36 -0
- package/vue/examples/PageStructure.vue +15 -12
- package/vue/examples/StatusExample.vue +248 -129
- package/vue/examples/TableExample.vue +72 -71
- package/vue/examples/TabsExample.vue +236 -257
- package/vue/examples/ThemePreview.vue +21 -23
- package/vue/examples/ViewItem.vue +32 -5
- package/vue/examples/Welcome.vue +3 -40
- package/vue/test/formTestUtils.js +279 -0
- 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
|
+
}
|
|
@@ -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
|
+
}
|
package/vue/examples/Admin.vue
CHANGED
|
@@ -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
|
-
<
|
|
16
|
-
<
|
|
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
|
-
<
|
|
22
|
-
<
|
|
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
|
-
<
|
|
28
|
-
<
|
|
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
|
-
<
|
|
34
|
-
<
|
|
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>
|
package/vue/examples/App.vue
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Navigation ref="navigation">
|
|
3
|
-
<
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
192
|
-
|
|
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>
|