picocrank 1.21.5 → 1.22.1

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.
@@ -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="true">
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
- if (quickSearchRef.value) {
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
- <div class="radio-group">
23
- <label>
24
- <input
25
- type="radio"
26
- name="role"
27
- value="sales"
28
- v-model="formData.role"
29
- />
30
- <span>Sales</span>
31
- </label>
32
- <label>
33
- <input
34
- type="radio"
35
- name="role"
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: '',