picocrank 1.18.2 → 1.18.3
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/index.html +10 -0
- package/package.json +1 -1
- package/router.js +43 -12
- package/styles/dark-theme.css +189 -0
- package/styles.css +1 -1
- package/vue/components/Calendar.vue +36 -2
- package/vue/components/Header.vue +18 -1
- package/vue/components/Navigation.vue +11 -0
- package/vue/components/NavigationGrid.vue +2 -2
- package/vue/components/NotificationPopup.vue +118 -0
- package/vue/components/NotificationPopups.vue +73 -0
- package/vue/components/Pagination.vue +1 -1
- package/vue/components/QuickSearch.vue +1 -1
- package/vue/components/Sidebar.vue +185 -31
- package/vue/components/Tabs.vue +1 -1
- package/vue/components/TopBar.vue +4 -2
- package/vue/composables/useNotificationPopups.js +78 -0
- package/vue/composables/useTheme.js +60 -0
- package/vue/views/App.vue +27 -20
- package/vue/views/ButtonsExample.vue +93 -0
- package/vue/views/CalendarExample.vue +26 -7
- package/vue/views/NavigationGridExample.vue +89 -0
- package/vue/views/PageStructure.vue +111 -0
- package/vue/views/StatusExample.vue +273 -0
- package/vue/views/TabsExample.vue +1 -1
- package/vue/views/ViewItem.vue +189 -3
- package/vue/views/Welcome.vue +218 -96
- package/vue/views/UserControlPanel.vue +0 -82
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<div
|
|
4
|
+
class="notification-popup-stack"
|
|
5
|
+
aria-live="polite"
|
|
6
|
+
aria-relevant="additions"
|
|
7
|
+
>
|
|
8
|
+
<TransitionGroup name="notification-popup">
|
|
9
|
+
<NotificationPopup
|
|
10
|
+
v-for="popup in notificationPopups"
|
|
11
|
+
:key="popup.id"
|
|
12
|
+
:popup="popup"
|
|
13
|
+
@dismiss="dismissNotificationPopup"
|
|
14
|
+
/>
|
|
15
|
+
</TransitionGroup>
|
|
16
|
+
</div>
|
|
17
|
+
</Teleport>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import { onUnmounted } from 'vue'
|
|
22
|
+
import NotificationPopup from './NotificationPopup.vue'
|
|
23
|
+
import {
|
|
24
|
+
notificationPopups,
|
|
25
|
+
dismissNotificationPopup,
|
|
26
|
+
dismissAllNotificationPopups,
|
|
27
|
+
showNotificationPopup,
|
|
28
|
+
clearNotificationPopupTimers,
|
|
29
|
+
} from '../composables/useNotificationPopups.js'
|
|
30
|
+
|
|
31
|
+
onUnmounted(() => {
|
|
32
|
+
clearNotificationPopupTimers()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
defineExpose({
|
|
36
|
+
popups: notificationPopups,
|
|
37
|
+
show: showNotificationPopup,
|
|
38
|
+
dismiss: dismissNotificationPopup,
|
|
39
|
+
dismissAll: dismissAllNotificationPopups,
|
|
40
|
+
})
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.notification-popup-stack {
|
|
45
|
+
position: fixed;
|
|
46
|
+
right: 1rem;
|
|
47
|
+
bottom: 1rem;
|
|
48
|
+
z-index: 1000;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 0.75rem;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.notification-popup-stack :deep(.notification-popup) {
|
|
56
|
+
pointer-events: auto;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.notification-popup-enter-active,
|
|
60
|
+
.notification-popup-leave-active {
|
|
61
|
+
transition: opacity 0.4s ease, transform 0.4s ease;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.notification-popup-enter-from,
|
|
65
|
+
.notification-popup-leave-to {
|
|
66
|
+
opacity: 0;
|
|
67
|
+
transform: translateX(1rem);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.notification-popup-move {
|
|
71
|
+
transition: transform 0.3s ease;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -15,43 +15,177 @@
|
|
|
15
15
|
|
|
16
16
|
<nav class="mainnav">
|
|
17
17
|
<ul class="navigation-links">
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
<template v-for="item in visibleNavItems" :key="item.key">
|
|
19
|
+
<li v-if="item.kind === 'section-header'" class="nav-section-header-item">
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="nav-section-header"
|
|
23
|
+
:aria-expanded="!item.collapsed"
|
|
24
|
+
:aria-controls="`nav-section-${item.section.id}`"
|
|
25
|
+
@click="toggleSection(item.section.id)"
|
|
26
|
+
>
|
|
27
|
+
<span>{{ item.section.title }}</span>
|
|
28
|
+
<HugeiconsIcon
|
|
29
|
+
:icon="item.collapsed ? ArrowRight01Icon : ArrowDown01Icon"
|
|
30
|
+
width="0.85em"
|
|
31
|
+
height="0.85em"
|
|
32
|
+
class="nav-section-chevron"
|
|
33
|
+
/>
|
|
34
|
+
</button>
|
|
35
|
+
</li>
|
|
36
|
+
|
|
37
|
+
<li
|
|
38
|
+
v-else
|
|
39
|
+
:title="item.link.title"
|
|
40
|
+
>
|
|
41
|
+
<div v-if="item.link.type === 'separator'" class="separator"></div>
|
|
42
|
+
<div v-else-if="item.link.type === 'callback'">
|
|
43
|
+
<a href="#" @click.prevent="handleLinkClick(() => item.link.callback())">
|
|
44
|
+
<HugeiconsIcon :icon="item.link.icon" />
|
|
45
|
+
<span>{{ item.link.title }}</span>
|
|
46
|
+
</a>
|
|
47
|
+
</div>
|
|
48
|
+
<div v-else-if="item.link.type === 'html'" v-html="item.link.html"></div>
|
|
49
|
+
<router-link
|
|
50
|
+
v-else
|
|
51
|
+
v-bind="item.link.props || {}"
|
|
52
|
+
:to="item.link.to || item.link.path"
|
|
53
|
+
:class="{ active: isActive(item.link) }"
|
|
54
|
+
@click="handleLinkClick()"
|
|
55
|
+
>
|
|
56
|
+
<HugeiconsIcon :icon="item.link.icon" />
|
|
57
|
+
<span>{{ item.link.title }}</span>
|
|
58
|
+
</router-link>
|
|
59
|
+
</li>
|
|
60
|
+
</template>
|
|
33
61
|
</ul>
|
|
34
62
|
</nav>
|
|
35
63
|
</aside>
|
|
36
64
|
</template>
|
|
37
65
|
|
|
38
66
|
<script setup>
|
|
39
|
-
import { ref, inject, onUnmounted, watch, nextTick } from 'vue'
|
|
67
|
+
import { ref, inject, onUnmounted, watch, nextTick, computed } from 'vue'
|
|
68
|
+
import { useRoute } from 'vue-router'
|
|
40
69
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
41
|
-
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
42
|
-
|
|
70
|
+
import { Pin02Icon, PinIcon, ArrowDown01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons'
|
|
71
|
+
|
|
72
|
+
const STORAGE_KEY = 'picocrank-nav-sections-collapsed'
|
|
43
73
|
|
|
44
74
|
const isOpen = ref(false)
|
|
45
75
|
const isStuck = ref(false)
|
|
46
76
|
const sidebarRef = ref(null)
|
|
77
|
+
const route = useRoute()
|
|
47
78
|
|
|
48
|
-
// Get navigation from Navigation component
|
|
49
79
|
const navigation = inject('navigation', null)
|
|
50
80
|
|
|
51
|
-
// Get navigation links and isActive function from navigation
|
|
52
81
|
const navigationLinks = navigation ? navigation.navigationLinks : ref([])
|
|
53
82
|
const isActive = navigation ? navigation.isActive : (() => false)
|
|
54
83
|
|
|
84
|
+
function loadCollapsedSections() {
|
|
85
|
+
try {
|
|
86
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
87
|
+
if (!raw) return new Set()
|
|
88
|
+
const parsed = JSON.parse(raw)
|
|
89
|
+
return new Set(Array.isArray(parsed) ? parsed : [])
|
|
90
|
+
} catch {
|
|
91
|
+
return new Set()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const collapsedSections = ref(loadCollapsedSections())
|
|
96
|
+
|
|
97
|
+
function saveCollapsedSections() {
|
|
98
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([...collapsedSections.value]))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isSectionCollapsed(sectionId) {
|
|
102
|
+
return collapsedSections.value.has(sectionId)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toggleSection(sectionId) {
|
|
106
|
+
const next = new Set(collapsedSections.value)
|
|
107
|
+
if (next.has(sectionId)) {
|
|
108
|
+
next.delete(sectionId)
|
|
109
|
+
} else {
|
|
110
|
+
next.add(sectionId)
|
|
111
|
+
}
|
|
112
|
+
collapsedSections.value = next
|
|
113
|
+
saveCollapsedSections()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const navigationGroups = computed(() => {
|
|
117
|
+
const ungrouped = []
|
|
118
|
+
const sections = []
|
|
119
|
+
let currentSection = null
|
|
120
|
+
|
|
121
|
+
for (const link of navigationLinks.value) {
|
|
122
|
+
if (link.type === 'section') {
|
|
123
|
+
currentSection = { id: link.name, title: link.title, items: [] }
|
|
124
|
+
sections.push(currentSection)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (link.type === 'separator') {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (currentSection) {
|
|
133
|
+
currentSection.items.push(link)
|
|
134
|
+
} else {
|
|
135
|
+
ungrouped.push(link)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ungrouped, sections }
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const visibleNavItems = computed(() => {
|
|
143
|
+
const items = []
|
|
144
|
+
|
|
145
|
+
for (const link of navigationGroups.value.ungrouped) {
|
|
146
|
+
items.push({ key: link.name, kind: 'link', link })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const section of navigationGroups.value.sections) {
|
|
150
|
+
const collapsed = isSectionCollapsed(section.id)
|
|
151
|
+
items.push({
|
|
152
|
+
key: section.id,
|
|
153
|
+
kind: 'section-header',
|
|
154
|
+
section,
|
|
155
|
+
collapsed,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
if (!collapsed) {
|
|
159
|
+
for (const link of section.items) {
|
|
160
|
+
items.push({
|
|
161
|
+
key: link.name,
|
|
162
|
+
kind: 'link',
|
|
163
|
+
link,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return items
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
function expandSectionContainingActiveRoute() {
|
|
173
|
+
for (const section of navigationGroups.value.sections) {
|
|
174
|
+
const hasActiveLink = section.items.some((link) => isActive(link))
|
|
175
|
+
if (!hasActiveLink || !isSectionCollapsed(section.id)) {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
const next = new Set(collapsedSections.value)
|
|
179
|
+
next.delete(section.id)
|
|
180
|
+
collapsedSections.value = next
|
|
181
|
+
saveCollapsedSections()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
watch(() => route.fullPath, () => {
|
|
186
|
+
expandSectionContainingActiveRoute()
|
|
187
|
+
}, { immediate: true })
|
|
188
|
+
|
|
55
189
|
function toggleStick() {
|
|
56
190
|
isStuck.value = !isStuck.value
|
|
57
191
|
}
|
|
@@ -88,19 +222,15 @@ function handleLinkClick(callback = null) {
|
|
|
88
222
|
}
|
|
89
223
|
|
|
90
224
|
function handleClickOutside(event) {
|
|
91
|
-
// Only close if sidebar is open and unpinned
|
|
92
225
|
if (!isOpen.value || isStuck.value) {
|
|
93
226
|
return
|
|
94
227
|
}
|
|
95
228
|
|
|
96
229
|
const target = event.target
|
|
97
230
|
|
|
98
|
-
// Check if click is on the sidebar toggle button or its container
|
|
99
|
-
// These elements should not trigger the close action
|
|
100
231
|
const sidebarButton = document.getElementById('sidebar-button')
|
|
101
232
|
const sidebarTogglerButton = document.getElementById('sidebar-toggler-button')
|
|
102
233
|
|
|
103
|
-
// Don't close if clicking on toggle button or its parent container
|
|
104
234
|
if (sidebarButton && sidebarButton.contains(target)) {
|
|
105
235
|
return
|
|
106
236
|
}
|
|
@@ -109,26 +239,21 @@ function handleClickOutside(event) {
|
|
|
109
239
|
return
|
|
110
240
|
}
|
|
111
241
|
|
|
112
|
-
// Check if click is outside the sidebar element
|
|
113
242
|
if (sidebarRef.value && !sidebarRef.value.contains(target)) {
|
|
114
243
|
close()
|
|
115
244
|
}
|
|
116
245
|
}
|
|
117
246
|
|
|
118
|
-
// Watch for sidebar open/close state to manage event listener
|
|
119
247
|
watch([isOpen, isStuck], ([open, stuck]) => {
|
|
120
248
|
nextTick(() => {
|
|
121
249
|
if (open && !stuck) {
|
|
122
|
-
// Add event listener when sidebar is open and unpinned
|
|
123
250
|
document.addEventListener('click', handleClickOutside)
|
|
124
251
|
} else {
|
|
125
|
-
// Remove event listener when sidebar is closed or pinned
|
|
126
252
|
document.removeEventListener('click', handleClickOutside)
|
|
127
253
|
}
|
|
128
254
|
})
|
|
129
255
|
}, { immediate: true })
|
|
130
256
|
|
|
131
|
-
// Clean up event listener on unmount
|
|
132
257
|
onUnmounted(() => {
|
|
133
258
|
document.removeEventListener('click', handleClickOutside)
|
|
134
259
|
})
|
|
@@ -171,6 +296,31 @@ button {
|
|
|
171
296
|
border-radius: 0;
|
|
172
297
|
}
|
|
173
298
|
|
|
299
|
+
.nav-section-header {
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
justify-content: space-between;
|
|
303
|
+
gap: 0.5rem;
|
|
304
|
+
width: 100%;
|
|
305
|
+
padding: 0.75em;
|
|
306
|
+
text-align: left;
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
font-size: 1em;
|
|
309
|
+
color: inherit;
|
|
310
|
+
background: transparent;
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.nav-section-header:hover {
|
|
315
|
+
background-color: var(--hover-background-color);
|
|
316
|
+
color: white;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.nav-section-chevron {
|
|
320
|
+
flex-shrink: 0;
|
|
321
|
+
opacity: 0.8;
|
|
322
|
+
}
|
|
323
|
+
|
|
174
324
|
.separator {
|
|
175
325
|
height: 1px;
|
|
176
326
|
background-color: #eee;
|
|
@@ -183,11 +333,15 @@ button {
|
|
|
183
333
|
text-align: center;
|
|
184
334
|
}
|
|
185
335
|
|
|
186
|
-
|
|
336
|
+
html[data-theme="dark"] {
|
|
187
337
|
.navigation-links a {
|
|
188
338
|
color: #f8f9fa;
|
|
189
339
|
}
|
|
190
340
|
|
|
341
|
+
.nav-section-header {
|
|
342
|
+
color: #f8f9fa;
|
|
343
|
+
}
|
|
344
|
+
|
|
191
345
|
.separator {
|
|
192
346
|
background-color: #444;
|
|
193
347
|
}
|
|
@@ -206,4 +360,4 @@ button {
|
|
|
206
360
|
left: 0;
|
|
207
361
|
}
|
|
208
362
|
}
|
|
209
|
-
</style>
|
|
363
|
+
</style>
|
package/vue/components/Tabs.vue
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
<nav class="topbar">
|
|
3
3
|
<div class="topbar-links flex-row">
|
|
4
4
|
<template v-for="link in navigationLinks" :key="link.name">
|
|
5
|
+
<!-- Skip section headers in the top bar -->
|
|
6
|
+
<template v-if="link.type === 'section'"></template>
|
|
5
7
|
<!-- Render separator as a divider -->
|
|
6
|
-
<div v-if="link.type === 'separator'" class="separator"></div>
|
|
8
|
+
<div v-else-if="link.type === 'separator'" class="separator"></div>
|
|
7
9
|
<!-- Render callback link -->
|
|
8
10
|
<a v-else-if="link.type === 'callback'" href="#" @click.prevent="handleLinkClick(() => link.callback())" class="button topbar-link">
|
|
9
11
|
<HugeiconsIcon :icon="link.icon" width="1em" height="1em" />
|
|
@@ -120,7 +122,7 @@ function handleLinkClick(callback = null) {
|
|
|
120
122
|
margin: 0 0.125rem;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
html[data-theme="dark"] {
|
|
124
126
|
.topbar-link {
|
|
125
127
|
color: #f8f9fa;
|
|
126
128
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const NOTIFICATION_POPUPS_KEY = Symbol('notificationPopups')
|
|
4
|
+
|
|
5
|
+
const DEFAULT_DURATION_MS = 5000
|
|
6
|
+
|
|
7
|
+
export const notificationPopups = ref([])
|
|
8
|
+
const timers = new Map()
|
|
9
|
+
let nextId = 0
|
|
10
|
+
|
|
11
|
+
function clearTimer(id) {
|
|
12
|
+
const timer = timers.get(id)
|
|
13
|
+
if (timer !== undefined) {
|
|
14
|
+
clearTimeout(timer)
|
|
15
|
+
timers.delete(id)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function dismissNotificationPopup(id) {
|
|
20
|
+
clearTimer(id)
|
|
21
|
+
notificationPopups.value = notificationPopups.value.filter((popup) => popup.id !== id)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function dismissAllNotificationPopups() {
|
|
25
|
+
for (const popup of notificationPopups.value) {
|
|
26
|
+
clearTimer(popup.id)
|
|
27
|
+
}
|
|
28
|
+
notificationPopups.value = []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function showNotificationPopup(options = {}) {
|
|
32
|
+
const message = options.message?.trim()
|
|
33
|
+
if (!message) {
|
|
34
|
+
console.warn('[NotificationPopups] show() requires a message.')
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const id = options.id || `notification-popup-${Date.now()}-${nextId++}`
|
|
39
|
+
const popup = {
|
|
40
|
+
id,
|
|
41
|
+
message,
|
|
42
|
+
label: options.label || null,
|
|
43
|
+
class: options.class || 'info',
|
|
44
|
+
linkTo: options.linkTo || null,
|
|
45
|
+
linkLabel: options.linkLabel || null,
|
|
46
|
+
durationMs: options.durationMs ?? DEFAULT_DURATION_MS,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
notificationPopups.value = [...notificationPopups.value, popup]
|
|
50
|
+
|
|
51
|
+
const durationMs = popup.durationMs
|
|
52
|
+
if (durationMs > 0) {
|
|
53
|
+
const timer = setTimeout(() => dismissNotificationPopup(id), durationMs)
|
|
54
|
+
timers.set(id, timer)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return id
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Access the global notification popup stack.
|
|
62
|
+
* Requires `<NotificationPopups />` to be mounted (typically in App.vue).
|
|
63
|
+
*/
|
|
64
|
+
export function useNotificationPopups() {
|
|
65
|
+
return {
|
|
66
|
+
popups: notificationPopups,
|
|
67
|
+
show: showNotificationPopup,
|
|
68
|
+
dismiss: dismissNotificationPopup,
|
|
69
|
+
dismissAll: dismissAllNotificationPopups,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function clearNotificationPopupTimers() {
|
|
74
|
+
for (const timer of timers.values()) {
|
|
75
|
+
clearTimeout(timer)
|
|
76
|
+
}
|
|
77
|
+
timers.clear()
|
|
78
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'picocrank-theme'
|
|
4
|
+
|
|
5
|
+
function getSystemTheme() {
|
|
6
|
+
if (typeof window === 'undefined') {
|
|
7
|
+
return 'light'
|
|
8
|
+
}
|
|
9
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readStoredTheme() {
|
|
13
|
+
if (typeof window === 'undefined') {
|
|
14
|
+
return 'light'
|
|
15
|
+
}
|
|
16
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
17
|
+
if (stored === 'light' || stored === 'dark') {
|
|
18
|
+
return stored
|
|
19
|
+
}
|
|
20
|
+
return getSystemTheme()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyTheme(value) {
|
|
24
|
+
if (typeof document === 'undefined') {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
document.documentElement.setAttribute('data-theme', value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const theme = ref(readStoredTheme())
|
|
31
|
+
|
|
32
|
+
applyTheme(theme.value)
|
|
33
|
+
|
|
34
|
+
watch(theme, (value) => {
|
|
35
|
+
applyTheme(value)
|
|
36
|
+
if (typeof localStorage !== 'undefined') {
|
|
37
|
+
localStorage.setItem(STORAGE_KEY, value)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export function useTheme() {
|
|
42
|
+
const isDark = computed(() => theme.value === 'dark')
|
|
43
|
+
|
|
44
|
+
function toggleTheme() {
|
|
45
|
+
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setTheme(value) {
|
|
49
|
+
if (value === 'light' || value === 'dark') {
|
|
50
|
+
theme.value = value
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
theme,
|
|
56
|
+
isDark,
|
|
57
|
+
toggleTheme,
|
|
58
|
+
setTheme,
|
|
59
|
+
}
|
|
60
|
+
}
|
package/vue/views/App.vue
CHANGED
|
@@ -26,20 +26,22 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
28
28
|
</Navigation>
|
|
29
|
+
|
|
30
|
+
<NotificationPopups />
|
|
29
31
|
</template>
|
|
30
32
|
|
|
31
33
|
<script setup>
|
|
32
34
|
import { ref, onMounted, provide } from 'vue';
|
|
33
|
-
import {
|
|
34
|
-
import { Pin02Icon, SecurityValidationIcon, EditIcon, ViewIcon, HomeIcon } from '@hugeicons/core-free-icons'
|
|
35
|
+
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
35
36
|
|
|
36
37
|
import QuickSearch from './../components/QuickSearch.vue'
|
|
37
38
|
import Navigation from './../components/Navigation.vue'
|
|
39
|
+
import NotificationPopups from './../components/NotificationPopups.vue'
|
|
38
40
|
import '../../styles.css'
|
|
41
|
+
import '../composables/useTheme.js'
|
|
39
42
|
import logoUrl from '/logo.png';
|
|
40
43
|
|
|
41
44
|
const quickSearchRef = ref(null)
|
|
42
|
-
const router = useRouter();
|
|
43
45
|
const sidebar = ref(null);
|
|
44
46
|
const navigation = ref(null);
|
|
45
47
|
const sidebarEnabled = ref(true);
|
|
@@ -90,24 +92,29 @@
|
|
|
90
92
|
onMounted(() => {
|
|
91
93
|
if (navigation.value) {
|
|
92
94
|
navigation.value.addRouterLink('Welcome')
|
|
93
|
-
|
|
94
|
-
navigation.value.
|
|
95
|
-
navigation.value.addRouterLink('
|
|
96
|
-
navigation.value.addRouterLink('
|
|
97
|
-
navigation.value.addRouterLink('
|
|
98
|
-
navigation.value.addSeparator('separator-1');
|
|
99
|
-
navigation.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
|
|
100
|
-
navigation.value.addSeparator('separator-2');
|
|
101
|
-
navigation.value.addCallback('Callback Example', helloWorld, { icon: Pin02Icon })
|
|
102
|
-
navigation.value.addSeparator('separator-3');
|
|
103
|
-
navigation.value.addHtml('<h2 style = "padding: 0.75em;">Authentication</h2>', { name: 'auth-heading' })
|
|
104
|
-
navigation.value.addRouterLink('LoginExample')
|
|
105
|
-
navigation.value.addSeparator('separator-4');
|
|
106
|
-
navigation.value.addHtml('<h2 style = "padding: 0.75em;">User Control Panel</h2>', { name: 'user-panel-heading' })
|
|
107
|
-
navigation.value.addRouterLink('UserControlPanel')
|
|
108
|
-
navigation.value.addSeparator('separator-5');
|
|
109
|
-
navigation.value.addHtml('<h2 style = "padding: 0.75em;">Administration</h2>', { name: 'admin-heading' })
|
|
95
|
+
|
|
96
|
+
navigation.value.addSection('Layouts', { name: 'nav-layouts' })
|
|
97
|
+
navigation.value.addRouterLink('PageStructure', 'Page structure')
|
|
98
|
+
navigation.value.addRouterLink('ViewItem', 'View item', { params: { id: 1 } })
|
|
99
|
+
navigation.value.addRouterLink('NavigationGridExample', 'Navigation Grid')
|
|
110
100
|
navigation.value.addRouterLink('Admin')
|
|
101
|
+
navigation.value.addRouterLink('TabsExample', 'Tabs')
|
|
102
|
+
|
|
103
|
+
navigation.value.addSection('Data display', { name: 'nav-data' })
|
|
104
|
+
navigation.value.addRouterLink('TableExample', 'Table')
|
|
105
|
+
navigation.value.addRouterLink('CalendarExample', 'Calendar')
|
|
106
|
+
navigation.value.addRouterLink('ReadOnlyTextAreaExample', 'Read-only output')
|
|
107
|
+
|
|
108
|
+
navigation.value.addSection('Forms & input', { name: 'nav-forms' })
|
|
109
|
+
navigation.value.addRouterLink('ButtonsExample', 'Buttons')
|
|
110
|
+
navigation.value.addRouterLink('FormExample', 'Forms')
|
|
111
|
+
navigation.value.addRouterLink('LoginExample', 'Login')
|
|
112
|
+
|
|
113
|
+
navigation.value.addSection('Feedback & status', { name: 'nav-feedback' })
|
|
114
|
+
navigation.value.addRouterLink('StatusExample', 'Status & notifications')
|
|
115
|
+
|
|
116
|
+
navigation.value.addSection('Patterns', { name: 'nav-patterns' })
|
|
117
|
+
navigation.value.addCallback('Callback example', helloWorld, { icon: Pin02Icon })
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
if (sidebar.value) {
|