picocrank 1.21.5 → 1.22.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/package.json +8 -8
- package/public/themes/README.md +29 -0
- package/public/themes/catppuccin/theme.css +195 -0
- package/public/themes/dracula/theme.css +181 -0
- package/public/themes/index.json +7 -0
- package/public/themes/waffles/theme.css +143 -0
- package/router.js +20 -0
- package/styles/dark-theme.css +15 -1
- package/styles.css +18 -2
- package/vite.config.js +57 -0
- package/vue/components/CheckGroup.vue +156 -0
- package/vue/components/FormField.vue +14 -2
- package/vue/components/Header.vue +139 -33
- package/vue/components/QuickSearch.vue +325 -348
- package/vue/components/RadioGroup.vue +191 -0
- package/vue/components/Sidebar.vue +226 -262
- package/vue/components/SidebarNavLink.vue +161 -0
- package/vue/composables/useCustomTheme.js +162 -0
- package/vue/data/examplePeople.js +45 -0
- package/vue/views/App.vue +85 -15
- package/vue/views/FormExample.vue +64 -29
- package/vue/views/PageStructure.vue +168 -95
- package/vue/views/TableExample.vue +2 -76
- package/vue/views/ThemePreview.vue +172 -0
- package/vue/views/UserDetails.vue +39 -0
- package/vue/views/Welcome.vue +9 -6
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<li
|
|
3
|
+
:title="link.title"
|
|
4
|
+
:class="{ disabled: link.disabled }"
|
|
5
|
+
>
|
|
6
|
+
<div v-if="link.type === 'separator'" class="separator" role="separator" />
|
|
7
|
+
<div v-else-if="link.type === 'html'" v-html="link.html"></div>
|
|
8
|
+
<a
|
|
9
|
+
v-else-if="link.type === 'callback' || link.disabled"
|
|
10
|
+
href="#"
|
|
11
|
+
:class="{ active: active, disabled: link.disabled }"
|
|
12
|
+
:aria-label="accessibleName"
|
|
13
|
+
:aria-disabled="link.disabled || undefined"
|
|
14
|
+
:tabindex="link.disabled ? -1 : undefined"
|
|
15
|
+
@click.prevent="emit('select', link)"
|
|
16
|
+
>
|
|
17
|
+
<HugeiconsIcon :icon="link.icon" aria-hidden="true" />
|
|
18
|
+
<span class="nav-link-title" aria-hidden="true">{{ link.title }}</span>
|
|
19
|
+
<span
|
|
20
|
+
v-if="showCount"
|
|
21
|
+
class="nav-link-count"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>{{ formatCount(link.count) }}</span>
|
|
24
|
+
<span
|
|
25
|
+
v-else-if="showIndicator"
|
|
26
|
+
class="nav-link-indicator"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
/>
|
|
29
|
+
</a>
|
|
30
|
+
<router-link
|
|
31
|
+
v-else
|
|
32
|
+
v-bind="link.props || {}"
|
|
33
|
+
:to="link.to || link.path"
|
|
34
|
+
:class="{ active }"
|
|
35
|
+
:aria-label="needsCustomName ? accessibleName : undefined"
|
|
36
|
+
:aria-current="active ? 'page' : undefined"
|
|
37
|
+
@click="emit('select', link)"
|
|
38
|
+
>
|
|
39
|
+
<HugeiconsIcon :icon="link.icon" aria-hidden="true" />
|
|
40
|
+
<span class="nav-link-title" :aria-hidden="needsCustomName ? 'true' : undefined">{{ link.title }}</span>
|
|
41
|
+
<span
|
|
42
|
+
v-if="showCount"
|
|
43
|
+
class="nav-link-count"
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
>{{ formatCount(link.count) }}</span>
|
|
46
|
+
<span
|
|
47
|
+
v-else-if="showIndicator"
|
|
48
|
+
class="nav-link-indicator"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
</router-link>
|
|
52
|
+
</li>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup>
|
|
56
|
+
import { computed } from 'vue'
|
|
57
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
58
|
+
|
|
59
|
+
const props = defineProps({
|
|
60
|
+
link: {
|
|
61
|
+
type: Object,
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
active: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits(['select'])
|
|
71
|
+
|
|
72
|
+
const showCount = computed(() => (
|
|
73
|
+
!props.link.disabled && props.link.count != null && props.link.count > 0
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
const showIndicator = computed(() => (
|
|
77
|
+
props.link.indicator && !props.link.disabled && !showCount.value
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
const needsCustomName = computed(() => showCount.value || showIndicator.value)
|
|
81
|
+
|
|
82
|
+
const accessibleName = computed(() => {
|
|
83
|
+
const parts = [props.link.title]
|
|
84
|
+
if (showCount.value) {
|
|
85
|
+
parts.push(`${props.link.count} notifications`)
|
|
86
|
+
} else if (showIndicator.value) {
|
|
87
|
+
parts.push('Requires attention')
|
|
88
|
+
}
|
|
89
|
+
if (props.link.disabled) {
|
|
90
|
+
parts.push('unavailable')
|
|
91
|
+
}
|
|
92
|
+
return parts.join(', ')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
function formatCount(count) {
|
|
96
|
+
return count > 99 ? '99+' : String(count)
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<style scoped>
|
|
101
|
+
a {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 0.75rem;
|
|
105
|
+
padding: .75em;
|
|
106
|
+
border-radius: 0;
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
a.active {
|
|
111
|
+
text-decoration: underline;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.nav-link-title {
|
|
115
|
+
flex: 1;
|
|
116
|
+
min-width: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.nav-link-indicator {
|
|
120
|
+
width: 0.5rem;
|
|
121
|
+
height: 0.5rem;
|
|
122
|
+
border-radius: 50%;
|
|
123
|
+
background: var(--indicator-color, #dc3545);
|
|
124
|
+
flex-shrink: 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.nav-link-count {
|
|
128
|
+
min-width: 1.25rem;
|
|
129
|
+
padding: 0.1rem 0.4rem;
|
|
130
|
+
border-radius: 999px;
|
|
131
|
+
background: var(--indicator-color, #dc3545);
|
|
132
|
+
color: #fff;
|
|
133
|
+
font-size: 0.75em;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
line-height: 1.2;
|
|
136
|
+
text-align: center;
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
a.disabled,
|
|
141
|
+
li.disabled a {
|
|
142
|
+
cursor: not-allowed;
|
|
143
|
+
opacity: 0.55;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.separator {
|
|
147
|
+
height: 1px;
|
|
148
|
+
background-color: #eee;
|
|
149
|
+
margin: 0.5rem 0.75rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
html[data-theme="dark"] {
|
|
153
|
+
a {
|
|
154
|
+
color: #f8f9fa;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.separator {
|
|
158
|
+
background-color: #444;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ref, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
const STYLE_ELEMENT_ID = 'theme-style'
|
|
4
|
+
const DEFAULT_STORAGE_KEY = 'picocrank-custom-theme'
|
|
5
|
+
|
|
6
|
+
function resolveDefaultBasePath() {
|
|
7
|
+
try {
|
|
8
|
+
const base = import.meta.env?.BASE_URL || '/'
|
|
9
|
+
const joined = base.endsWith('/') ? `${base}themes` : `${base}/themes`
|
|
10
|
+
return joined.replace(/\/$/, '') || '/themes'
|
|
11
|
+
} catch {
|
|
12
|
+
return '/themes'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_BASE_PATH = resolveDefaultBasePath()
|
|
17
|
+
|
|
18
|
+
const availableThemes = ref([])
|
|
19
|
+
const themePreference = ref('')
|
|
20
|
+
const themesBasePath = ref(DEFAULT_BASE_PATH)
|
|
21
|
+
const storageKey = ref(DEFAULT_STORAGE_KEY)
|
|
22
|
+
let initialized = false
|
|
23
|
+
|
|
24
|
+
function normalizeBasePath(basePath) {
|
|
25
|
+
if (!basePath || typeof basePath !== 'string') {
|
|
26
|
+
return DEFAULT_BASE_PATH
|
|
27
|
+
}
|
|
28
|
+
const trimmed = basePath.replace(/\/$/, '')
|
|
29
|
+
return trimmed || DEFAULT_BASE_PATH
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readStoredTheme(key) {
|
|
33
|
+
if (typeof localStorage === 'undefined') {
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
return localStorage.getItem(key) || ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function persistTheme(key, value) {
|
|
40
|
+
if (typeof localStorage === 'undefined') {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (!value) {
|
|
44
|
+
localStorage.removeItem(key)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
localStorage.setItem(key, value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureStyleElement() {
|
|
51
|
+
if (typeof document === 'undefined') {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
let themeStyle = document.getElementById(STYLE_ELEMENT_ID)
|
|
55
|
+
if (!themeStyle) {
|
|
56
|
+
themeStyle = document.createElement('style')
|
|
57
|
+
themeStyle.id = STYLE_ELEMENT_ID
|
|
58
|
+
themeStyle.type = 'text/css'
|
|
59
|
+
document.head.appendChild(themeStyle)
|
|
60
|
+
}
|
|
61
|
+
return themeStyle
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function applyTheme() {
|
|
65
|
+
const themeStyle = ensureStyleElement()
|
|
66
|
+
if (!themeStyle) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const name = themePreference.value
|
|
71
|
+
if (!name) {
|
|
72
|
+
themeStyle.textContent = ''
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const href = `${themesBasePath.value}/${encodeURIComponent(name)}/theme.css`
|
|
77
|
+
themeStyle.textContent = `@import url('${href}') layer(theme);`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setTheme(name) {
|
|
81
|
+
themePreference.value = name || ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clearTheme() {
|
|
85
|
+
setTheme('')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function discoverThemes() {
|
|
89
|
+
if (typeof fetch === 'undefined') {
|
|
90
|
+
availableThemes.value = []
|
|
91
|
+
return availableThemes.value
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const indexUrl = `${themesBasePath.value}/index.json`
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(indexUrl, { cache: 'no-store' })
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
availableThemes.value = []
|
|
99
|
+
return availableThemes.value
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json()
|
|
102
|
+
const themes = Array.isArray(data?.themes)
|
|
103
|
+
? data.themes.filter((theme) => typeof theme === 'string' && theme.length > 0)
|
|
104
|
+
: []
|
|
105
|
+
availableThemes.value = themes.sort((a, b) => a.localeCompare(b))
|
|
106
|
+
} catch {
|
|
107
|
+
availableThemes.value = []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return availableThemes.value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function configure(options = {}) {
|
|
114
|
+
if (options.themesBasePath !== undefined) {
|
|
115
|
+
themesBasePath.value = normalizeBasePath(options.themesBasePath)
|
|
116
|
+
}
|
|
117
|
+
if (options.storageKey !== undefined && typeof options.storageKey === 'string' && options.storageKey) {
|
|
118
|
+
storageKey.value = options.storageKey
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(options.availableThemes)) {
|
|
121
|
+
availableThemes.value = options.availableThemes
|
|
122
|
+
.filter((theme) => typeof theme === 'string' && theme.length > 0)
|
|
123
|
+
.sort((a, b) => a.localeCompare(b))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function initCustomTheme(options = {}) {
|
|
128
|
+
configure(options)
|
|
129
|
+
|
|
130
|
+
if (!initialized) {
|
|
131
|
+
themePreference.value = readStoredTheme(storageKey.value)
|
|
132
|
+
watch(themePreference, (value) => {
|
|
133
|
+
persistTheme(storageKey.value, value)
|
|
134
|
+
applyTheme()
|
|
135
|
+
})
|
|
136
|
+
initialized = true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
applyTheme()
|
|
140
|
+
return useCustomTheme()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function useCustomTheme(options) {
|
|
144
|
+
if (options) {
|
|
145
|
+
configure(options)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
availableThemes,
|
|
150
|
+
themePreference,
|
|
151
|
+
themesBasePath,
|
|
152
|
+
storageKey,
|
|
153
|
+
applyTheme,
|
|
154
|
+
setTheme,
|
|
155
|
+
clearTheme,
|
|
156
|
+
discoverThemes,
|
|
157
|
+
configure,
|
|
158
|
+
initCustomTheme,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { initCustomTheme }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Example people used by the table example and QuickSearch. */
|
|
2
|
+
export const examplePeople = [
|
|
3
|
+
{ name: 'Alice', age: 30, city: 'New York' },
|
|
4
|
+
{ name: 'Bob', age: 25, city: 'Los Angeles' },
|
|
5
|
+
{ name: 'Charlie', age: 35, city: 'Chicago' },
|
|
6
|
+
{ name: 'David', age: 28, city: 'Houston' },
|
|
7
|
+
{ name: 'Eve', age: 22, city: 'Phoenix' },
|
|
8
|
+
{ name: 'Frank', age: 40, city: 'Philadelphia' },
|
|
9
|
+
{ name: 'Grace', age: 29, city: 'San Antonio' },
|
|
10
|
+
{ name: 'Hannah', age: 32, city: 'San Diego' },
|
|
11
|
+
{ name: 'Ian', age: 27, city: 'Dallas' },
|
|
12
|
+
{ name: 'Jack', age: 31, city: 'San Jose' },
|
|
13
|
+
{ name: 'Kathy', age: 26, city: 'Austin' },
|
|
14
|
+
{ name: 'Leo', age: 33, city: 'Jacksonville' },
|
|
15
|
+
{ name: 'Mia', age: 24, city: 'Fort Worth' },
|
|
16
|
+
{ name: 'Nina', age: 38, city: 'Columbus' },
|
|
17
|
+
{ name: 'Oscar', age: 36, city: 'Charlotte' },
|
|
18
|
+
{ name: 'Paul', age: 34, city: 'San Francisco' },
|
|
19
|
+
{ name: 'Quinn', age: 23, city: 'Indianapolis' },
|
|
20
|
+
{ name: 'Rita', age: 37, city: 'Seattle' },
|
|
21
|
+
{ name: 'Sam', age: 39, city: 'Denver' },
|
|
22
|
+
{ name: 'Tina', age: 21, city: 'Washington D.C.' },
|
|
23
|
+
{ name: 'Uma', age: 28, city: 'Miami' },
|
|
24
|
+
{ name: 'Victor', age: 30, city: 'Portland' },
|
|
25
|
+
{ name: 'Wendy', age: 29, city: 'Nashville' },
|
|
26
|
+
{ name: 'Xavier', age: 31, city: 'Oklahoma City' },
|
|
27
|
+
{ name: 'Yara', age: 27, city: 'Tampa' },
|
|
28
|
+
{ name: 'Zane', age: 32, city: 'Albuquerque' },
|
|
29
|
+
{ name: 'Ava', age: 26, city: 'Las Vegas' },
|
|
30
|
+
{ name: 'Ben', age: 33, city: 'New Orleans' },
|
|
31
|
+
{ name: 'Cara', age: 25, city: 'San Francisco' },
|
|
32
|
+
{ name: 'Drew', age: 34, city: 'Seattle' },
|
|
33
|
+
{ name: 'Ethan', age: 28, city: 'Denver' },
|
|
34
|
+
{ name: 'Fiona', age: 30, city: 'Washington D.C.' },
|
|
35
|
+
{ name: 'George', age: 29, city: 'Miami' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
/** Longer list matching the previous table example volume (with repeats for pagination). */
|
|
39
|
+
export function getExampleTableRows() {
|
|
40
|
+
return [
|
|
41
|
+
...examplePeople,
|
|
42
|
+
...examplePeople.slice(7),
|
|
43
|
+
...examplePeople.slice(7, 22),
|
|
44
|
+
]
|
|
45
|
+
}
|
package/vue/views/App.vue
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Navigation ref="navigation">
|
|
3
|
-
<Header username = "Guest" @toggleSidebar="toggleSidebar" @logoClick="goToIndex" title = "PicoCrank" :logoUrl="logoUrl" :sidebarEnabled="sidebarEnabled" :navigation="navigation" :topBarEnabled="topBarEnabled" :showBranding="brandingEnabled" :breadcrumbs="breadcrumbsEnabled" :themeToggleEnabled="
|
|
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">
|
|
4
4
|
<template #toolbar>
|
|
5
5
|
<QuickSearch
|
|
6
6
|
v-if="quickSearchEnabled"
|
|
7
7
|
ref="quickSearchRef"
|
|
8
8
|
placeholder="Search items..."
|
|
9
|
-
:search-fields="['title', 'name']"
|
|
9
|
+
:search-fields="['title', 'name', 'description', 'category']"
|
|
10
10
|
:max-results="15"
|
|
11
|
+
:fetch-results="dummySearchFetchEnabled ? dummyFetchResults : null"
|
|
11
12
|
/>
|
|
12
13
|
</template>
|
|
13
14
|
</Header>
|
|
@@ -31,15 +32,17 @@
|
|
|
31
32
|
</template>
|
|
32
33
|
|
|
33
34
|
<script setup>
|
|
34
|
-
import { ref, onMounted, provide } from 'vue';
|
|
35
|
+
import { ref, onMounted, provide, watch } from 'vue';
|
|
35
36
|
import { useRouter } from 'vue-router';
|
|
36
|
-
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
37
|
+
import { Pin02Icon, UserIcon } from '@hugeicons/core-free-icons'
|
|
37
38
|
|
|
38
39
|
import QuickSearch from './../components/QuickSearch.vue'
|
|
39
40
|
import Navigation from './../components/Navigation.vue'
|
|
40
41
|
import NotificationPopups from './../components/NotificationPopups.vue'
|
|
41
42
|
import '../../styles.css'
|
|
42
43
|
import '../composables/useTheme.js'
|
|
44
|
+
import { initCustomTheme } from '../composables/useCustomTheme.js'
|
|
45
|
+
import { examplePeople } from '../data/examplePeople.js'
|
|
43
46
|
import logoUrl from '/logo.png';
|
|
44
47
|
|
|
45
48
|
const router = useRouter();
|
|
@@ -49,9 +52,13 @@
|
|
|
49
52
|
const sidebarEnabled = ref(true);
|
|
50
53
|
const brandingEnabled = ref(true);
|
|
51
54
|
const quickSearchEnabled = ref(true);
|
|
55
|
+
const dummySearchFetchEnabled = ref(false);
|
|
56
|
+
const themeToggleEnabled = ref(true);
|
|
52
57
|
const topBarEnabled = ref(false);
|
|
53
58
|
const breadcrumbsEnabled = ref(false);
|
|
54
59
|
|
|
60
|
+
const { discoverThemes } = initCustomTheme();
|
|
61
|
+
|
|
55
62
|
function toggleSidebar() {
|
|
56
63
|
if (sidebar.value) {
|
|
57
64
|
sidebar.value.toggle();
|
|
@@ -62,6 +69,10 @@
|
|
|
62
69
|
router.push({ name: 'Welcome' });
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
function goToUserDetails() {
|
|
73
|
+
router.push({ name: 'UserDetails' });
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
function toggleSidebarEnabled() {
|
|
66
77
|
sidebarEnabled.value = !sidebarEnabled.value;
|
|
67
78
|
}
|
|
@@ -74,6 +85,14 @@
|
|
|
74
85
|
quickSearchEnabled.value = !quickSearchEnabled.value;
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
function toggleDummySearchFetchEnabled() {
|
|
89
|
+
dummySearchFetchEnabled.value = !dummySearchFetchEnabled.value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toggleThemeToggleEnabled() {
|
|
93
|
+
themeToggleEnabled.value = !themeToggleEnabled.value;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
function toggleTopBarEnabled() {
|
|
78
97
|
topBarEnabled.value = !topBarEnabled.value;
|
|
79
98
|
}
|
|
@@ -82,6 +101,54 @@
|
|
|
82
101
|
breadcrumbsEnabled.value = !breadcrumbsEnabled.value;
|
|
83
102
|
}
|
|
84
103
|
|
|
104
|
+
async function dummyFetchResults(_query, { signal } = {}) {
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
const timer = setTimeout(resolve, 1500)
|
|
107
|
+
if (!signal) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const onAbort = () => {
|
|
111
|
+
clearTimeout(timer)
|
|
112
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
113
|
+
}
|
|
114
|
+
if (signal.aborted) {
|
|
115
|
+
onAbort()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
119
|
+
})
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function registerQuickSearchItems() {
|
|
124
|
+
if (!quickSearchRef.value) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
quickSearchRef.value.addItem({
|
|
129
|
+
id: 'hello-world',
|
|
130
|
+
title: 'Hello World',
|
|
131
|
+
name: 'hello-world',
|
|
132
|
+
description: 'This is a test item',
|
|
133
|
+
category: 'Actions',
|
|
134
|
+
type: 'callback',
|
|
135
|
+
callback: helloWorld
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
for (const person of examplePeople) {
|
|
139
|
+
quickSearchRef.value.addItem({
|
|
140
|
+
id: `person-${person.name}`,
|
|
141
|
+
title: person.name,
|
|
142
|
+
name: person.name,
|
|
143
|
+
description: `${person.city} · age ${person.age}`,
|
|
144
|
+
category: 'People',
|
|
145
|
+
type: 'route',
|
|
146
|
+
path: `/view-item/${encodeURIComponent(person.name)}`,
|
|
147
|
+
icon: UserIcon,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
85
152
|
// Provide sidebar, QuickSearch, TopBar, and breadcrumbs state and toggle functions for child components
|
|
86
153
|
provide('sidebarEnabled', sidebarEnabled);
|
|
87
154
|
provide('toggleSidebarEnabled', toggleSidebarEnabled);
|
|
@@ -90,17 +157,30 @@
|
|
|
90
157
|
|
|
91
158
|
provide('quickSearchEnabled', quickSearchEnabled);
|
|
92
159
|
provide('toggleQuickSearchEnabled', toggleQuickSearchEnabled);
|
|
160
|
+
provide('dummySearchFetchEnabled', dummySearchFetchEnabled);
|
|
161
|
+
provide('toggleDummySearchFetchEnabled', toggleDummySearchFetchEnabled);
|
|
162
|
+
provide('themeToggleEnabled', themeToggleEnabled);
|
|
163
|
+
provide('toggleThemeToggleEnabled', toggleThemeToggleEnabled);
|
|
93
164
|
provide('topBarEnabled', topBarEnabled);
|
|
94
165
|
provide('toggleTopBarEnabled', toggleTopBarEnabled);
|
|
95
166
|
provide('breadcrumbsEnabled', breadcrumbsEnabled);
|
|
96
167
|
provide('toggleBreadcrumbsEnabled', toggleBreadcrumbsEnabled);
|
|
97
168
|
|
|
169
|
+
watch(quickSearchRef, (quickSearch) => {
|
|
170
|
+
if (quickSearch) {
|
|
171
|
+
registerQuickSearchItems()
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
98
175
|
onMounted(() => {
|
|
176
|
+
discoverThemes()
|
|
177
|
+
|
|
99
178
|
if (navigation.value) {
|
|
100
179
|
navigation.value.addRouterLink('Welcome')
|
|
101
180
|
|
|
102
181
|
navigation.value.addSection('Layouts', { name: 'nav-layouts' })
|
|
103
182
|
navigation.value.addRouterLink('PageStructure', 'Page structure')
|
|
183
|
+
navigation.value.addRouterLink('ThemePreview', 'Theme Switcher')
|
|
104
184
|
navigation.value.addRouterLink('ViewItem', 'View item', { params: { id: 1 } })
|
|
105
185
|
navigation.value.addRouterLink('NavigationGridExample', 'Navigation Grid')
|
|
106
186
|
navigation.value.addRouterLink('Admin')
|
|
@@ -128,17 +208,7 @@
|
|
|
128
208
|
sidebar.value.stick();
|
|
129
209
|
}
|
|
130
210
|
|
|
131
|
-
|
|
132
|
-
quickSearchRef.value.addItem({
|
|
133
|
-
id: 'hello-world',
|
|
134
|
-
title: 'Hello World',
|
|
135
|
-
name: 'hello-world',
|
|
136
|
-
description: 'This is a test item',
|
|
137
|
-
category: 'Actions',
|
|
138
|
-
type: 'callback',
|
|
139
|
-
callback: helloWorld
|
|
140
|
-
})
|
|
141
|
-
}
|
|
211
|
+
registerQuickSearchItems()
|
|
142
212
|
});
|
|
143
213
|
|
|
144
214
|
function helloWorld() {
|
|
@@ -19,35 +19,20 @@
|
|
|
19
19
|
</FormField>
|
|
20
20
|
|
|
21
21
|
<FormField label="Role" fake>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
value="engineering"
|
|
37
|
-
v-model="formData.role"
|
|
38
|
-
/>
|
|
39
|
-
<span>Engineering</span>
|
|
40
|
-
</label>
|
|
41
|
-
<label>
|
|
42
|
-
<input
|
|
43
|
-
type="radio"
|
|
44
|
-
name="role"
|
|
45
|
-
value="hr"
|
|
46
|
-
v-model="formData.role"
|
|
47
|
-
/>
|
|
48
|
-
<span>HR</span>
|
|
49
|
-
</label>
|
|
50
|
-
</div>
|
|
22
|
+
<RadioGroup
|
|
23
|
+
v-model="formData.role"
|
|
24
|
+
name="role"
|
|
25
|
+
:options="roleOptions"
|
|
26
|
+
/>
|
|
27
|
+
</FormField>
|
|
28
|
+
|
|
29
|
+
<FormField label="Term" fake>
|
|
30
|
+
<RadioGroup
|
|
31
|
+
v-model="formData.term"
|
|
32
|
+
name="term"
|
|
33
|
+
variant="boolean"
|
|
34
|
+
:options="termOptions"
|
|
35
|
+
/>
|
|
51
36
|
</FormField>
|
|
52
37
|
|
|
53
38
|
<FormField label="Salary" for="salary">
|
|
@@ -61,6 +46,23 @@
|
|
|
61
46
|
</select>
|
|
62
47
|
</FormField>
|
|
63
48
|
|
|
49
|
+
<FormField label="Compensation" fake>
|
|
50
|
+
<RadioGroup
|
|
51
|
+
v-model="formData.compensation"
|
|
52
|
+
name="compensation"
|
|
53
|
+
variant="list"
|
|
54
|
+
:options="compensationOptions"
|
|
55
|
+
/>
|
|
56
|
+
</FormField>
|
|
57
|
+
|
|
58
|
+
<FormField label="Accessibility requirements" fake>
|
|
59
|
+
<CheckGroup
|
|
60
|
+
v-model="formData.accessibility"
|
|
61
|
+
name="accessibility"
|
|
62
|
+
:options="accessibilityOptions"
|
|
63
|
+
/>
|
|
64
|
+
</FormField>
|
|
65
|
+
|
|
64
66
|
<FormField label="Email" for="email">
|
|
65
67
|
<input
|
|
66
68
|
id="email"
|
|
@@ -190,11 +192,41 @@ import { ref } from 'vue'
|
|
|
190
192
|
import Section from '../components/Section.vue'
|
|
191
193
|
import FormLayout from '../components/FormLayout.vue'
|
|
192
194
|
import FormField from '../components/FormField.vue'
|
|
195
|
+
import RadioGroup from '../components/RadioGroup.vue'
|
|
196
|
+
import CheckGroup from '../components/CheckGroup.vue'
|
|
197
|
+
|
|
198
|
+
// From femtocrank/tests/simple.html — Role (default), Term (boolean), Compensation (list)
|
|
199
|
+
const roleOptions = [
|
|
200
|
+
{ label: 'Sales', value: 'sales' },
|
|
201
|
+
{ label: 'Engineering', value: 'engineering' },
|
|
202
|
+
{ label: 'HR', value: 'hr' },
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
const termOptions = [
|
|
206
|
+
{ label: 'Full Time', value: 'full-time' },
|
|
207
|
+
{ label: 'Part Time', value: 'part-time' },
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
const compensationOptions = [
|
|
211
|
+
{ label: 'Performance Incentive', value: 'performance-incentive' },
|
|
212
|
+
{ label: 'Sales Target Incentive', value: 'sales-target-incentive' },
|
|
213
|
+
{ label: 'Objective Incentive', value: 'objective-incentive' },
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
const accessibilityOptions = [
|
|
217
|
+
{ label: 'Screen reader support', value: 'screen-reader' },
|
|
218
|
+
{ label: 'Keyboard navigation', value: 'keyboard-navigation' },
|
|
219
|
+
{ label: 'High contrast', value: 'high-contrast' },
|
|
220
|
+
{ label: 'Captions / transcripts', value: 'captions' },
|
|
221
|
+
]
|
|
193
222
|
|
|
194
223
|
const formData = ref({
|
|
195
224
|
name: '',
|
|
196
225
|
role: '',
|
|
226
|
+
term: '',
|
|
197
227
|
salary: '0',
|
|
228
|
+
compensation: '',
|
|
229
|
+
accessibility: [],
|
|
198
230
|
email: '',
|
|
199
231
|
isAdmin: false,
|
|
200
232
|
website: '',
|
|
@@ -227,7 +259,10 @@ function resetForm() {
|
|
|
227
259
|
formData.value = {
|
|
228
260
|
name: '',
|
|
229
261
|
role: '',
|
|
262
|
+
term: '',
|
|
230
263
|
salary: '0',
|
|
264
|
+
compensation: '',
|
|
265
|
+
accessibility: [],
|
|
231
266
|
email: '',
|
|
232
267
|
isAdmin: false,
|
|
233
268
|
website: '',
|