picocrank 1.9.0 → 1.10.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/router.js +10 -0
- package/vue/components/Header.vue +4 -0
- package/vue/components/Navigation.vue +165 -0
- package/vue/components/QuickSearch.vue +1 -2
- package/vue/components/Sidebar.vue +6 -136
- package/vue/components/Table.vue +5 -1
- package/vue/views/App.vue +28 -20
- package/vue/views/FormExample.vue +268 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocrank",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
|
|
5
5
|
"main": "vite.config.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"url": "https://github.com/jamesread/picocrank/issues"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@hugeicons/core-free-icons": "^
|
|
22
|
-
"@hugeicons/vue": "^1.0.
|
|
23
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
24
|
-
"femtocrank": "^2.4.
|
|
21
|
+
"@hugeicons/core-free-icons": "^3.1.0",
|
|
22
|
+
"@hugeicons/vue": "^1.0.4",
|
|
23
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
24
|
+
"femtocrank": "^2.4.12",
|
|
25
25
|
"unplugin-vue-components": "^30.0.0",
|
|
26
|
-
"vite": "^7.
|
|
27
|
-
"vue": "^3.5.
|
|
28
|
-
"vue-router": "^4.6.
|
|
26
|
+
"vite": "^7.3.0",
|
|
27
|
+
"vue": "^3.5.26",
|
|
28
|
+
"vue-router": "^4.6.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"semantic-release": "^25.0.2"
|
package/router.js
CHANGED
|
@@ -5,6 +5,7 @@ import { TableIcon } from '@hugeicons/core-free-icons';
|
|
|
5
5
|
import { ViewIcon } from '@hugeicons/core-free-icons';
|
|
6
6
|
import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
|
|
7
7
|
import { CalendarIcon } from '@hugeicons/core-free-icons';
|
|
8
|
+
import { EditIcon } from '@hugeicons/core-free-icons';
|
|
8
9
|
|
|
9
10
|
const routes = [
|
|
10
11
|
{
|
|
@@ -54,6 +55,15 @@ const routes = [
|
|
|
54
55
|
title: 'Calendar Example',
|
|
55
56
|
icon: CalendarIcon,
|
|
56
57
|
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'FormExample',
|
|
61
|
+
path: '/form-example',
|
|
62
|
+
component: () => import('./vue/views/FormExample.vue'),
|
|
63
|
+
meta: {
|
|
64
|
+
title: 'Form Example',
|
|
65
|
+
icon: EditIcon,
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
68
|
];
|
|
59
69
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Navigation component has no visual output, it only manages navigation state -->
|
|
3
|
+
<slot />
|
|
4
|
+
</template>
|
|
5
|
+
|
|
6
|
+
<script setup>
|
|
7
|
+
import { ref, provide } from 'vue'
|
|
8
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
9
|
+
import { PinIcon } from '@hugeicons/core-free-icons'
|
|
10
|
+
|
|
11
|
+
const navigationLinks = ref([])
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const router = useRouter()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a router link to the navigation
|
|
17
|
+
* @param {string} link - Route name
|
|
18
|
+
* @param {string|null} altTitle - Optional alternative title
|
|
19
|
+
* @param {object} options - Additional options
|
|
20
|
+
* @param {object} options.params - Route params to pass (e.g., { id: 1 })
|
|
21
|
+
* @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
|
|
22
|
+
* @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
|
|
23
|
+
*/
|
|
24
|
+
function addRouterLink(link, altTitle = null, options = {}) {
|
|
25
|
+
const foundRoute = router.getRoutes().find(r => r.name === link)
|
|
26
|
+
|
|
27
|
+
if (!foundRoute) {
|
|
28
|
+
console.warn(`Route "${link}" not found`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const title = altTitle || foundRoute.meta?.title || foundRoute.name
|
|
33
|
+
|
|
34
|
+
// Determine the 'to' value: use options.to if provided, otherwise construct from params, or fall back to path
|
|
35
|
+
let toValue = foundRoute.path
|
|
36
|
+
if (options.to) {
|
|
37
|
+
toValue = options.to
|
|
38
|
+
} else if (options.params) {
|
|
39
|
+
toValue = { name: link, params: options.params }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const routeLink = {
|
|
43
|
+
name: link,
|
|
44
|
+
title: title,
|
|
45
|
+
path: foundRoute.path,
|
|
46
|
+
to: toValue,
|
|
47
|
+
icon: foundRoute.meta?.icon || PinIcon,
|
|
48
|
+
type: 'route',
|
|
49
|
+
props: options.props || {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addNavigationLink(routeLink)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addNavigationLink(link) {
|
|
56
|
+
const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
|
|
57
|
+
if (existingIndex >= 0) {
|
|
58
|
+
navigationLinks.value[existingIndex] = { ...link }
|
|
59
|
+
} else {
|
|
60
|
+
navigationLinks.value.push({ ...link })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function addCallback(title, callback, options = {}) {
|
|
65
|
+
const callbackLink = {
|
|
66
|
+
name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
|
|
67
|
+
type: 'callback',
|
|
68
|
+
icon: options.icon || PinIcon,
|
|
69
|
+
callback: callback || (() => {}),
|
|
70
|
+
title: title
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
addNavigationLink(callbackLink)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addSeparator(id) {
|
|
77
|
+
const separator = {
|
|
78
|
+
name: id || `nav-separator-${Date.now()}`,
|
|
79
|
+
type: 'separator',
|
|
80
|
+
title: 'Separator'
|
|
81
|
+
}
|
|
82
|
+
addNavigationLink(separator)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function addHtml(html, options = {}) {
|
|
86
|
+
const htmlLink = {
|
|
87
|
+
name: options.name || `html-item-${Date.now()}`,
|
|
88
|
+
type: 'html',
|
|
89
|
+
html: html,
|
|
90
|
+
title: options.title || 'HTML Item'
|
|
91
|
+
}
|
|
92
|
+
addNavigationLink(htmlLink)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function removeNavigationLink(linkId) {
|
|
96
|
+
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function clearNavigationLinks() {
|
|
100
|
+
navigationLinks.value = []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getNavigationLinks() {
|
|
104
|
+
return [...navigationLinks.value]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isActive(link) {
|
|
108
|
+
// If link has a 'to' object with a name, compare route name and params
|
|
109
|
+
if (link.to && typeof link.to === 'object' && link.to.name) {
|
|
110
|
+
// Compare route names first
|
|
111
|
+
if (route.name !== link.to.name) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If params are specified in the link, all specified params must match
|
|
116
|
+
if (link.to.params && Object.keys(link.to.params).length > 0) {
|
|
117
|
+
const linkParams = link.to.params
|
|
118
|
+
const routeParams = route.params
|
|
119
|
+
|
|
120
|
+
// Check if all params in link.to.params match route.params
|
|
121
|
+
for (const key in linkParams) {
|
|
122
|
+
if (String(routeParams[key]) !== String(linkParams[key])) {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Route name matches, and either no params specified or all params match
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fall back to path comparison for string paths
|
|
133
|
+
const linkPath = typeof link.to === 'string' ? link.to : link.path
|
|
134
|
+
return route.path === linkPath
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Provide navigation API for child components
|
|
138
|
+
provide('navigation', {
|
|
139
|
+
navigationLinks,
|
|
140
|
+
addRouterLink,
|
|
141
|
+
addNavigationLink,
|
|
142
|
+
addCallback,
|
|
143
|
+
addSeparator,
|
|
144
|
+
addHtml,
|
|
145
|
+
removeNavigationLink,
|
|
146
|
+
clearNavigationLinks,
|
|
147
|
+
getNavigationLinks,
|
|
148
|
+
isActive
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Expose API for direct ref access
|
|
152
|
+
defineExpose({
|
|
153
|
+
navigationLinks,
|
|
154
|
+
addRouterLink,
|
|
155
|
+
addNavigationLink,
|
|
156
|
+
addCallback,
|
|
157
|
+
addSeparator,
|
|
158
|
+
addHtml,
|
|
159
|
+
removeNavigationLink,
|
|
160
|
+
clearNavigationLinks,
|
|
161
|
+
getNavigationLinks,
|
|
162
|
+
isActive
|
|
163
|
+
})
|
|
164
|
+
</script>
|
|
165
|
+
|
|
@@ -303,7 +303,6 @@ defineExpose({
|
|
|
303
303
|
.quick-search {
|
|
304
304
|
position: relative;
|
|
305
305
|
width: 100%;
|
|
306
|
-
max-width: 400px;
|
|
307
306
|
}
|
|
308
307
|
|
|
309
308
|
.search-container {
|
|
@@ -370,7 +369,7 @@ defineExpose({
|
|
|
370
369
|
|
|
371
370
|
.search-result-item:hover,
|
|
372
371
|
.search-result-item.active {
|
|
373
|
-
background-color:
|
|
372
|
+
background-color: var(--hover-background-color);
|
|
374
373
|
}
|
|
375
374
|
|
|
376
375
|
.result-content {
|
|
@@ -36,111 +36,21 @@
|
|
|
36
36
|
</template>
|
|
37
37
|
|
|
38
38
|
<script setup>
|
|
39
|
-
import { ref,
|
|
40
|
-
import { useRoute, useRouter } from 'vue-router'
|
|
39
|
+
import { ref, inject, onUnmounted, watch, nextTick } from 'vue'
|
|
41
40
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
42
41
|
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
43
42
|
import { PinIcon } from '@hugeicons/core-free-icons'
|
|
44
43
|
|
|
45
44
|
const isOpen = ref(false)
|
|
46
45
|
const isStuck = ref(false)
|
|
47
|
-
const navigationLinks = ref([])
|
|
48
46
|
const sidebarRef = ref(null)
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
const
|
|
48
|
+
// Get navigation from Navigation component
|
|
49
|
+
const navigation = inject('navigation', null)
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* @param {string|null} altTitle - Optional alternative title
|
|
57
|
-
* @param {object} options - Additional options
|
|
58
|
-
* @param {object} options.params - Route params to pass (e.g., { id: 1 })
|
|
59
|
-
* @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
|
|
60
|
-
* @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
|
|
61
|
-
*/
|
|
62
|
-
function addRouterLink(link, altTitle = null, options = {}) {
|
|
63
|
-
const foundRoute = router.getRoutes().find(r => r.name === link)
|
|
64
|
-
|
|
65
|
-
if (!foundRoute) {
|
|
66
|
-
console.warn(`Route "${link}" not found`)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const title = altTitle || foundRoute.meta?.title || foundRoute.name
|
|
71
|
-
|
|
72
|
-
// Determine the 'to' value: use options.to if provided, otherwise construct from params, or fall back to path
|
|
73
|
-
let toValue = foundRoute.path
|
|
74
|
-
if (options.to) {
|
|
75
|
-
toValue = options.to
|
|
76
|
-
} else if (options.params) {
|
|
77
|
-
toValue = { name: link, params: options.params }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const routeLink = {
|
|
81
|
-
name: link,
|
|
82
|
-
title: title,
|
|
83
|
-
path: foundRoute.path,
|
|
84
|
-
to: toValue,
|
|
85
|
-
icon: foundRoute.meta?.icon || PinIcon,
|
|
86
|
-
type: 'route',
|
|
87
|
-
props: options.props || {}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
addNavigationLink(routeLink)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function addNavigationLink(link) {
|
|
94
|
-
const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
|
|
95
|
-
if (existingIndex >= 0) {
|
|
96
|
-
navigationLinks.value[existingIndex] = { ...link }
|
|
97
|
-
} else {
|
|
98
|
-
navigationLinks.value.push({ ...link })
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function addCallback(title, callback, options = {}) {
|
|
103
|
-
const callbackLink = {
|
|
104
|
-
name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
|
|
105
|
-
type: 'callback',
|
|
106
|
-
icon: options.icon || PinIcon,
|
|
107
|
-
callback: callback || (() => {}),
|
|
108
|
-
title: title
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
addNavigationLink(callbackLink)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function addSeparator(id) {
|
|
115
|
-
const separator = {
|
|
116
|
-
name: id || `nav-separator-${Date.now()}`,
|
|
117
|
-
type: 'separator',
|
|
118
|
-
title: 'Separator'
|
|
119
|
-
}
|
|
120
|
-
addNavigationLink(separator)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function addHtml(html, options = {}) {
|
|
124
|
-
const htmlLink = {
|
|
125
|
-
name: options.name || `html-item-${Date.now()}`,
|
|
126
|
-
type: 'html',
|
|
127
|
-
html: html,
|
|
128
|
-
title: options.title || 'HTML Item'
|
|
129
|
-
}
|
|
130
|
-
addNavigationLink(htmlLink)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function removeNavigationLink(linkId) {
|
|
134
|
-
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function clearNavigationLinks() {
|
|
138
|
-
navigationLinks.value = []
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getNavigationLinks() {
|
|
142
|
-
return [...navigationLinks.value]
|
|
143
|
-
}
|
|
51
|
+
// Get navigation links and isActive function from navigation
|
|
52
|
+
const navigationLinks = navigation ? navigation.navigationLinks : ref([])
|
|
53
|
+
const isActive = navigation ? navigation.isActive : (() => false)
|
|
144
54
|
|
|
145
55
|
function toggleStick() {
|
|
146
56
|
isStuck.value = !isStuck.value
|
|
@@ -168,36 +78,6 @@ function close() {
|
|
|
168
78
|
isStuck.value = false
|
|
169
79
|
}
|
|
170
80
|
|
|
171
|
-
function isActive(link) {
|
|
172
|
-
// If link has a 'to' object with a name, compare route name and params
|
|
173
|
-
if (link.to && typeof link.to === 'object' && link.to.name) {
|
|
174
|
-
// Compare route names first
|
|
175
|
-
if (route.name !== link.to.name) {
|
|
176
|
-
return false
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// If params are specified in the link, all specified params must match
|
|
180
|
-
if (link.to.params && Object.keys(link.to.params).length > 0) {
|
|
181
|
-
const linkParams = link.to.params
|
|
182
|
-
const routeParams = route.params
|
|
183
|
-
|
|
184
|
-
// Check if all params in link.to.params match route.params
|
|
185
|
-
for (const key in linkParams) {
|
|
186
|
-
if (String(routeParams[key]) !== String(linkParams[key])) {
|
|
187
|
-
return false
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Route name matches, and either no params specified or all params match
|
|
193
|
-
return true
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Fall back to path comparison for string paths
|
|
197
|
-
const linkPath = typeof link.to === 'string' ? link.to : link.path
|
|
198
|
-
return route.path === linkPath
|
|
199
|
-
}
|
|
200
|
-
|
|
201
81
|
function handleLinkClick(callback = null) {
|
|
202
82
|
if (callback) {
|
|
203
83
|
callback()
|
|
@@ -255,22 +135,12 @@ onUnmounted(() => {
|
|
|
255
135
|
|
|
256
136
|
defineExpose({
|
|
257
137
|
isOpen,
|
|
258
|
-
navigationLinks,
|
|
259
138
|
stick,
|
|
260
139
|
unstick,
|
|
261
140
|
toggleStick,
|
|
262
141
|
toggle,
|
|
263
142
|
open,
|
|
264
143
|
close,
|
|
265
|
-
isActive,
|
|
266
|
-
addNavigationLink,
|
|
267
|
-
addRouterLink,
|
|
268
|
-
addCallback,
|
|
269
|
-
addSeparator,
|
|
270
|
-
addHtml,
|
|
271
|
-
removeNavigationLink,
|
|
272
|
-
clearNavigationLinks,
|
|
273
|
-
getNavigationLinks,
|
|
274
144
|
})
|
|
275
145
|
</script>
|
|
276
146
|
|
package/vue/components/Table.vue
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</tr>
|
|
34
34
|
</tbody>
|
|
35
35
|
</table>
|
|
36
|
-
<div class = "padding">
|
|
36
|
+
<div v-if="showPagination" class = "padding">
|
|
37
37
|
<Pagination :total = "total" v-model:page="page" v-model:page-size="pageSize" />
|
|
38
38
|
</div>
|
|
39
39
|
</template>
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
type: Array,
|
|
57
57
|
default: () => [],
|
|
58
58
|
},
|
|
59
|
+
showPagination: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
default: true,
|
|
62
|
+
},
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
const slots = useSlots()
|
package/vue/views/App.vue
CHANGED
|
@@ -10,19 +10,21 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
</Header>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
<
|
|
13
|
+
<Navigation ref="navigation">
|
|
14
|
+
<div id = "layout">
|
|
15
|
+
<Sidebar v-if="sidebarEnabled" ref = "sidebar" />
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<div id = "content">
|
|
18
|
+
<main>
|
|
19
|
+
<router-view />
|
|
20
|
+
</main>
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
<footer>
|
|
23
|
+
<span><a href = "https://github.com/jamesread/picocrank">PicoCrank</a></span>
|
|
24
|
+
</footer>
|
|
25
|
+
</div>
|
|
24
26
|
</div>
|
|
25
|
-
</
|
|
27
|
+
</Navigation>
|
|
26
28
|
</template>
|
|
27
29
|
|
|
28
30
|
<script setup>
|
|
@@ -31,12 +33,14 @@
|
|
|
31
33
|
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
32
34
|
|
|
33
35
|
import QuickSearch from './../components/QuickSearch.vue'
|
|
36
|
+
import Navigation from './../components/Navigation.vue'
|
|
34
37
|
import '../../styles.css'
|
|
35
38
|
import logoUrl from '/logo.png';
|
|
36
39
|
|
|
37
40
|
const quickSearchRef = ref(null)
|
|
38
41
|
const router = useRouter();
|
|
39
42
|
const sidebar = ref(null);
|
|
43
|
+
const navigation = ref(null);
|
|
40
44
|
const sidebarEnabled = ref(true);
|
|
41
45
|
|
|
42
46
|
function toggleSidebar() {
|
|
@@ -54,17 +58,21 @@
|
|
|
54
58
|
provide('toggleSidebarEnabled', toggleSidebarEnabled);
|
|
55
59
|
|
|
56
60
|
onMounted(() => {
|
|
61
|
+
if (navigation.value) {
|
|
62
|
+
navigation.value.addRouterLink('Welcome')
|
|
63
|
+
navigation.value.addRouterLink('TableExample')
|
|
64
|
+
navigation.value.addRouterLink('CalendarExample')
|
|
65
|
+
navigation.value.addRouterLink('FormExample')
|
|
66
|
+
navigation.value.addSeparator('separator-1');
|
|
67
|
+
navigation.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
|
|
68
|
+
navigation.value.addSeparator('separator-2');
|
|
69
|
+
navigation.value.addCallback('Callback Example', helloWorld, { icon: Pin02Icon })
|
|
70
|
+
navigation.value.addSeparator('separator-3');
|
|
71
|
+
navigation.value.addHtml('<h2 style = "padding: 0.75em;">Administration</h2>', { name: 'admin-heading' })
|
|
72
|
+
navigation.value.addRouterLink('Admin')
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
if (sidebar.value) {
|
|
58
|
-
sidebar.value.addRouterLink('Welcome')
|
|
59
|
-
sidebar.value.addRouterLink('TableExample')
|
|
60
|
-
sidebar.value.addRouterLink('CalendarExample')
|
|
61
|
-
sidebar.value.addSeparator('separator-1');
|
|
62
|
-
sidebar.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
|
|
63
|
-
sidebar.value.addSeparator('separator-2');
|
|
64
|
-
sidebar.value.addCallback('Callback Example', helloWorld, { icon: Pin02Icon })
|
|
65
|
-
sidebar.value.addSeparator('separator-3');
|
|
66
|
-
sidebar.value.addHtml('<h2 style = "padding: 0.75em;">Administration</h2>', { name: 'admin-heading' })
|
|
67
|
-
sidebar.value.addRouterLink('Admin')
|
|
68
76
|
sidebar.value.open();
|
|
69
77
|
sidebar.value.stick();
|
|
70
78
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section
|
|
3
|
+
title="Form Example"
|
|
4
|
+
subtitle="A simple form with various input types"
|
|
5
|
+
>
|
|
6
|
+
<template #toolbar>
|
|
7
|
+
<button @click="resetForm">Reset</button>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<form @submit.prevent="handleSubmit">
|
|
11
|
+
<div class="form-group">
|
|
12
|
+
<label for="name">Name</label>
|
|
13
|
+
<input
|
|
14
|
+
id="name"
|
|
15
|
+
v-model="formData.name"
|
|
16
|
+
type="text"
|
|
17
|
+
placeholder="Enter your name"
|
|
18
|
+
required
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="email">Email</label>
|
|
24
|
+
<input
|
|
25
|
+
id="email"
|
|
26
|
+
v-model="formData.email"
|
|
27
|
+
type="email"
|
|
28
|
+
placeholder="Enter your email"
|
|
29
|
+
required
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="form-group">
|
|
34
|
+
<label for="country">Country</label>
|
|
35
|
+
<select id="country" v-model="formData.country" required>
|
|
36
|
+
<option value="">Select a country</option>
|
|
37
|
+
<option value="us">United States</option>
|
|
38
|
+
<option value="uk">United Kingdom</option>
|
|
39
|
+
<option value="ca">Canada</option>
|
|
40
|
+
<option value="au">Australia</option>
|
|
41
|
+
<option value="de">Germany</option>
|
|
42
|
+
<option value="fr">France</option>
|
|
43
|
+
</select>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="form-group">
|
|
47
|
+
<label for="message">Message</label>
|
|
48
|
+
<textarea
|
|
49
|
+
id="message"
|
|
50
|
+
v-model="formData.message"
|
|
51
|
+
placeholder="Enter your message"
|
|
52
|
+
rows="4"
|
|
53
|
+
></textarea>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="form-group">
|
|
57
|
+
<label>
|
|
58
|
+
<input
|
|
59
|
+
type="checkbox"
|
|
60
|
+
v-model="formData.newsletter"
|
|
61
|
+
/>
|
|
62
|
+
Subscribe to newsletter
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="form-group">
|
|
67
|
+
<label>Preferred Contact Method</label>
|
|
68
|
+
<div class="radio-group">
|
|
69
|
+
<label>
|
|
70
|
+
<input
|
|
71
|
+
type="radio"
|
|
72
|
+
v-model="formData.contactMethod"
|
|
73
|
+
value="email"
|
|
74
|
+
/>
|
|
75
|
+
Email
|
|
76
|
+
</label>
|
|
77
|
+
<label>
|
|
78
|
+
<input
|
|
79
|
+
type="radio"
|
|
80
|
+
v-model="formData.contactMethod"
|
|
81
|
+
value="phone"
|
|
82
|
+
/>
|
|
83
|
+
Phone
|
|
84
|
+
</label>
|
|
85
|
+
<label>
|
|
86
|
+
<input
|
|
87
|
+
type="radio"
|
|
88
|
+
v-model="formData.contactMethod"
|
|
89
|
+
value="mail"
|
|
90
|
+
/>
|
|
91
|
+
Mail
|
|
92
|
+
</label>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="form-actions">
|
|
97
|
+
<button type="submit">Submit</button>
|
|
98
|
+
<button type="button" @click="resetForm">Clear</button>
|
|
99
|
+
</div>
|
|
100
|
+
</form>
|
|
101
|
+
|
|
102
|
+
<div v-if="submitted" class="form-result">
|
|
103
|
+
<h3>Form Submitted!</h3>
|
|
104
|
+
<pre>{{ JSON.stringify(formData, null, 2) }}</pre>
|
|
105
|
+
</div>
|
|
106
|
+
</Section>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<script setup>
|
|
110
|
+
import { ref } from 'vue'
|
|
111
|
+
import Section from '../components/Section.vue'
|
|
112
|
+
|
|
113
|
+
const formData = ref({
|
|
114
|
+
name: '',
|
|
115
|
+
email: '',
|
|
116
|
+
country: '',
|
|
117
|
+
message: '',
|
|
118
|
+
newsletter: false,
|
|
119
|
+
contactMethod: 'email'
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const submitted = ref(false)
|
|
123
|
+
|
|
124
|
+
function handleSubmit() {
|
|
125
|
+
submitted.value = true
|
|
126
|
+
console.log('Form submitted:', formData.value)
|
|
127
|
+
// In a real application, you would send this data to a server
|
|
128
|
+
alert('Form submitted! Check the console and the form result below.')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resetForm() {
|
|
132
|
+
formData.value = {
|
|
133
|
+
name: '',
|
|
134
|
+
email: '',
|
|
135
|
+
country: '',
|
|
136
|
+
message: '',
|
|
137
|
+
newsletter: false,
|
|
138
|
+
contactMethod: 'email'
|
|
139
|
+
}
|
|
140
|
+
submitted.value = false
|
|
141
|
+
}
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
.form-group {
|
|
146
|
+
margin-bottom: 1.5rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.form-group label {
|
|
150
|
+
display: block;
|
|
151
|
+
margin-bottom: 0.5rem;
|
|
152
|
+
font-weight: 500;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.form-group input[type="text"],
|
|
156
|
+
.form-group input[type="email"],
|
|
157
|
+
.form-group select,
|
|
158
|
+
.form-group textarea {
|
|
159
|
+
width: 100%;
|
|
160
|
+
padding: 0.5rem;
|
|
161
|
+
border: 1px solid #ccc;
|
|
162
|
+
border-radius: 0.25rem;
|
|
163
|
+
font-size: 1rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.form-group textarea {
|
|
167
|
+
resize: vertical;
|
|
168
|
+
font-family: inherit;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.form-group input[type="checkbox"],
|
|
172
|
+
.form-group input[type="radio"] {
|
|
173
|
+
margin-right: 0.5rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.form-group label:has(input[type="checkbox"]) {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
font-weight: normal;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.radio-group {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: 1.5rem;
|
|
185
|
+
flex-wrap: wrap;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.radio-group label {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
font-weight: normal;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.form-actions {
|
|
195
|
+
display: flex;
|
|
196
|
+
gap: 1rem;
|
|
197
|
+
margin-top: 2rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.form-actions button {
|
|
201
|
+
padding: 0.75rem 1.5rem;
|
|
202
|
+
border: none;
|
|
203
|
+
border-radius: 0.25rem;
|
|
204
|
+
font-size: 1rem;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.form-actions button[type="submit"] {
|
|
209
|
+
background-color: #0366d6;
|
|
210
|
+
color: white;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.form-actions button[type="submit"]:hover {
|
|
214
|
+
background-color: #0256c2;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.form-actions button[type="button"] {
|
|
218
|
+
background-color: #6c757d;
|
|
219
|
+
color: white;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.form-actions button[type="button"]:hover {
|
|
223
|
+
background-color: #5a6268;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.form-result {
|
|
227
|
+
margin-top: 2rem;
|
|
228
|
+
padding: 1rem;
|
|
229
|
+
background-color: #f8f9fa;
|
|
230
|
+
border-radius: 0.25rem;
|
|
231
|
+
border: 1px solid #dee2e6;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.form-result h3 {
|
|
235
|
+
margin-top: 0;
|
|
236
|
+
margin-bottom: 1rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.form-result pre {
|
|
240
|
+
margin: 0;
|
|
241
|
+
padding: 1rem;
|
|
242
|
+
background-color: white;
|
|
243
|
+
border-radius: 0.25rem;
|
|
244
|
+
overflow-x: auto;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@media (prefers-color-scheme: dark) {
|
|
248
|
+
.form-group input[type="text"],
|
|
249
|
+
.form-group input[type="email"],
|
|
250
|
+
.form-group select,
|
|
251
|
+
.form-group textarea {
|
|
252
|
+
background-color: #333;
|
|
253
|
+
border-color: #555;
|
|
254
|
+
color: #fff;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.form-result {
|
|
258
|
+
background-color: #2d2d2d;
|
|
259
|
+
border-color: #444;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.form-result pre {
|
|
263
|
+
background-color: #1a1a1a;
|
|
264
|
+
color: #fff;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
</style>
|
|
268
|
+
|