picocrank 1.18.1 → 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 +19 -2
- package/vue/components/NavigationGrid.vue +71 -4
- 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 -79
package/index.html
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
<meta name = "viewport" content = "width=device-width, initial-scale=1.0, user-scalable=no" />
|
|
8
8
|
|
|
9
|
+
<script>
|
|
10
|
+
(function () {
|
|
11
|
+
var key = 'picocrank-theme';
|
|
12
|
+
var stored = localStorage.getItem(key);
|
|
13
|
+
var theme = stored === 'light' || stored === 'dark'
|
|
14
|
+
? stored
|
|
15
|
+
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
16
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
9
19
|
</head>
|
|
10
20
|
|
|
11
21
|
<body>
|
package/package.json
CHANGED
package/router.js
CHANGED
|
@@ -7,7 +7,11 @@ import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
|
|
|
7
7
|
import { CalendarIcon } from '@hugeicons/core-free-icons';
|
|
8
8
|
import { EditIcon } from '@hugeicons/core-free-icons';
|
|
9
9
|
import { Menu01Icon } from '@hugeicons/core-free-icons';
|
|
10
|
+
import { Navigation01Icon } from '@hugeicons/core-free-icons';
|
|
10
11
|
import { ClipboardCopyIcon } from '@hugeicons/core-free-icons';
|
|
12
|
+
import { CursorPointer01Icon } from '@hugeicons/core-free-icons';
|
|
13
|
+
import { LayoutGridIcon } from '@hugeicons/core-free-icons';
|
|
14
|
+
import { StatusIcon } from '@hugeicons/core-free-icons';
|
|
11
15
|
|
|
12
16
|
const routes = [
|
|
13
17
|
{
|
|
@@ -19,13 +23,22 @@ const routes = [
|
|
|
19
23
|
icon: HomeIcon,
|
|
20
24
|
}
|
|
21
25
|
},
|
|
26
|
+
{
|
|
27
|
+
name: 'PageStructure',
|
|
28
|
+
path: '/page-structure',
|
|
29
|
+
component: () => import('./vue/views/PageStructure.vue'),
|
|
30
|
+
meta: {
|
|
31
|
+
title: 'Page structure',
|
|
32
|
+
icon: LayoutGridIcon,
|
|
33
|
+
}
|
|
34
|
+
},
|
|
22
35
|
{
|
|
23
36
|
name: 'TableExample',
|
|
24
37
|
path: '/table-example',
|
|
25
38
|
title: 'Table Example',
|
|
26
39
|
component: () => import('./vue/views/TableExample.vue'),
|
|
27
40
|
meta: {
|
|
28
|
-
title: 'Table
|
|
41
|
+
title: 'Table',
|
|
29
42
|
icon: TableIcon,
|
|
30
43
|
}
|
|
31
44
|
},
|
|
@@ -35,7 +48,7 @@ const routes = [
|
|
|
35
48
|
component: () => import('./vue/views/ViewItem.vue'),
|
|
36
49
|
props: true,
|
|
37
50
|
meta: {
|
|
38
|
-
title: 'View
|
|
51
|
+
title: 'View item',
|
|
39
52
|
icon: ViewIcon,
|
|
40
53
|
}
|
|
41
54
|
},
|
|
@@ -54,7 +67,7 @@ const routes = [
|
|
|
54
67
|
component: () => import('./vue/views/CalendarExample.vue'),
|
|
55
68
|
props: true,
|
|
56
69
|
meta: {
|
|
57
|
-
title: 'Calendar
|
|
70
|
+
title: 'Calendar',
|
|
58
71
|
icon: CalendarIcon,
|
|
59
72
|
}
|
|
60
73
|
},
|
|
@@ -67,12 +80,21 @@ const routes = [
|
|
|
67
80
|
icon: SecurityValidationIcon,
|
|
68
81
|
}
|
|
69
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: 'ButtonsExample',
|
|
85
|
+
path: '/buttons-example',
|
|
86
|
+
component: () => import('./vue/views/ButtonsExample.vue'),
|
|
87
|
+
meta: {
|
|
88
|
+
title: 'Buttons',
|
|
89
|
+
icon: CursorPointer01Icon,
|
|
90
|
+
}
|
|
91
|
+
},
|
|
70
92
|
{
|
|
71
93
|
name: 'FormExample',
|
|
72
94
|
path: '/form-example',
|
|
73
95
|
component: () => import('./vue/views/FormExample.vue'),
|
|
74
96
|
meta: {
|
|
75
|
-
title: '
|
|
97
|
+
title: 'Forms',
|
|
76
98
|
icon: EditIcon,
|
|
77
99
|
}
|
|
78
100
|
},
|
|
@@ -81,26 +103,35 @@ const routes = [
|
|
|
81
103
|
path: '/readonly-textarea-example',
|
|
82
104
|
component: () => import('./vue/views/ReadOnlyTextAreaExample.vue'),
|
|
83
105
|
meta: {
|
|
84
|
-
title: '
|
|
106
|
+
title: 'Read-only output',
|
|
85
107
|
icon: ClipboardCopyIcon,
|
|
86
108
|
}
|
|
87
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: 'StatusExample',
|
|
112
|
+
path: '/status-example',
|
|
113
|
+
component: () => import('./vue/views/StatusExample.vue'),
|
|
114
|
+
meta: {
|
|
115
|
+
title: 'Status & notifications',
|
|
116
|
+
icon: StatusIcon,
|
|
117
|
+
}
|
|
118
|
+
},
|
|
88
119
|
{
|
|
89
120
|
name: 'TabsExample',
|
|
90
121
|
path: '/tabs-example',
|
|
91
122
|
component: () => import('./vue/views/TabsExample.vue'),
|
|
92
123
|
meta: {
|
|
93
|
-
title: 'Tabs
|
|
124
|
+
title: 'Tabs',
|
|
94
125
|
icon: Menu01Icon,
|
|
95
126
|
}
|
|
96
127
|
},
|
|
97
128
|
{
|
|
98
|
-
name: '
|
|
99
|
-
path: '/
|
|
100
|
-
component: () => import('./vue/views/
|
|
129
|
+
name: 'NavigationGridExample',
|
|
130
|
+
path: '/navigation-grid',
|
|
131
|
+
component: () => import('./vue/views/NavigationGridExample.vue'),
|
|
101
132
|
meta: {
|
|
102
|
-
title: '
|
|
103
|
-
icon:
|
|
133
|
+
title: 'Navigation Grid',
|
|
134
|
+
icon: Navigation01Icon,
|
|
104
135
|
}
|
|
105
136
|
},
|
|
106
137
|
{
|
|
@@ -108,7 +139,7 @@ const routes = [
|
|
|
108
139
|
path: '/login-example',
|
|
109
140
|
component: () => import('./vue/views/LoginExample.vue'),
|
|
110
141
|
meta: {
|
|
111
|
-
title: 'Login
|
|
142
|
+
title: 'Login',
|
|
112
143
|
icon: SecurityValidationIcon,
|
|
113
144
|
}
|
|
114
145
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
html[data-theme="dark"] {
|
|
3
|
+
color-scheme: dark;
|
|
4
|
+
|
|
5
|
+
--border-color: #595959;
|
|
6
|
+
--hover-background-color: #1d345c;
|
|
7
|
+
--header-hover-background-color: #1d345c;
|
|
8
|
+
--standout-bg-color: #1f1f1f;
|
|
9
|
+
--disabled-background-color: #333;
|
|
10
|
+
--disabled-text-color: #999;
|
|
11
|
+
|
|
12
|
+
& header {
|
|
13
|
+
background-color: black;
|
|
14
|
+
box-shadow: 0px 0px 1em #1c1c1c;
|
|
15
|
+
border-bottom: 1px solid #222;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
& body {
|
|
19
|
+
background-color: #262626;
|
|
20
|
+
color: #ddd;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
& section {
|
|
24
|
+
background-color: #343434;
|
|
25
|
+
color: #ddd;
|
|
26
|
+
box-shadow: 0 0 .5em #121212;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
& section .section-header {
|
|
30
|
+
background-color: #343434;
|
|
31
|
+
border-bottom-color: var(--border-color);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
& section .section-subheader {
|
|
35
|
+
border-bottom-color: var(--border-color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
& button,
|
|
39
|
+
& .button {
|
|
40
|
+
color: #ddd;
|
|
41
|
+
border: 1px solid var(--border-color);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
& button:hover,
|
|
45
|
+
& .button:hover,
|
|
46
|
+
& .button:visited {
|
|
47
|
+
background-color: var(--hover-background-color);
|
|
48
|
+
color: white;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
& footer a,
|
|
52
|
+
& footer a:visited {
|
|
53
|
+
color: lightblue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
& header ul[role="menubar"] a,
|
|
57
|
+
& header ul[role="menubar"] a:visited {
|
|
58
|
+
background-color: black;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
& header ul[role="menubar"] a:hover {
|
|
62
|
+
background-color: var(--hover-background-color);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
& header div > ul > li > a {
|
|
66
|
+
background-color: black;
|
|
67
|
+
color: white;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
& ul[role="menubar"] div ul a:hover {
|
|
71
|
+
background-color: var(--hover-background-color);
|
|
72
|
+
color: white;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
& ul[role="menubar"] li:hover {
|
|
76
|
+
background-color: var(--hover-background-color);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
& label:has(input[type="checkbox"]):hover,
|
|
80
|
+
& label:has(input[type="radio"]):hover {
|
|
81
|
+
background-color: var(--hover-background-color);
|
|
82
|
+
color: white;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
& label:has(input:checked) {
|
|
86
|
+
background-color: #488448;
|
|
87
|
+
color: white;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
& label:has(input:checked):hover {
|
|
91
|
+
background-color: #3a6f3a;
|
|
92
|
+
color: white;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
& a,
|
|
96
|
+
& a:visited,
|
|
97
|
+
& aside a,
|
|
98
|
+
& aside a:visited {
|
|
99
|
+
color: lightblue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
& table,
|
|
103
|
+
& th,
|
|
104
|
+
& td {
|
|
105
|
+
border-color: var(--border-color) !important;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
& table.row-hover tr:hover td {
|
|
109
|
+
background-color: var(--hover-background-color);
|
|
110
|
+
color: white;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
& footer span {
|
|
114
|
+
background-color: #444;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
& input[type="text"],
|
|
118
|
+
& input[type="email"],
|
|
119
|
+
& input[type="password"],
|
|
120
|
+
& input[type="url"],
|
|
121
|
+
& input[type="number"],
|
|
122
|
+
& input[type="color"],
|
|
123
|
+
& input[type="datetime-local"] {
|
|
124
|
+
background-color: #4e4e4e;
|
|
125
|
+
color: #ddd;
|
|
126
|
+
border-color: var(--border-color);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
& select {
|
|
130
|
+
background-color: #4e4e4e;
|
|
131
|
+
color: #ddd;
|
|
132
|
+
border-color: var(--border-color);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
& textarea {
|
|
136
|
+
background-color: #4e4e4e;
|
|
137
|
+
color: #ddd;
|
|
138
|
+
border-color: var(--border-color);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
& .grid-boxed .stat-display {
|
|
142
|
+
background-color: var(--standout-bg-color);
|
|
143
|
+
border-color: var(--border-color) !important;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
& pre {
|
|
147
|
+
background-color: var(--standout-bg-color);
|
|
148
|
+
color: #ddd;
|
|
149
|
+
border: 1px solid var(--border-color);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
& .radio-group {
|
|
153
|
+
border-color: var(--border-color);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
& .radio-group label {
|
|
157
|
+
background-color: #555;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
& aside {
|
|
161
|
+
background-color: #000;
|
|
162
|
+
box-shadow: 0 0 .5em #121212;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
& aside ul li a:hover {
|
|
166
|
+
background-color: var(--hover-background-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
& dt {
|
|
170
|
+
border-color: var(--border-color);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
& .annotation,
|
|
174
|
+
& .tag,
|
|
175
|
+
& .inline-notification {
|
|
176
|
+
background-color: var(--standout-bg-color);
|
|
177
|
+
color: #ddd;
|
|
178
|
+
border-color: var(--border-color);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
& .annotation-val {
|
|
182
|
+
color: #ddd;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
html[data-theme="light"] {
|
|
188
|
+
color-scheme: light;
|
|
189
|
+
}
|
package/styles.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
|
3
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
3
4
|
|
|
4
5
|
export interface CalendarEvent {
|
|
5
6
|
id: string | number
|
|
@@ -7,6 +8,10 @@ export interface CalendarEvent {
|
|
|
7
8
|
startDate?: Date | string | null
|
|
8
9
|
endDate?: Date | string | null
|
|
9
10
|
date?: Date | string | null
|
|
11
|
+
/** Optional Hugeicons icon (from @hugeicons/core-free-icons). */
|
|
12
|
+
icon?: unknown
|
|
13
|
+
/** Optional CSS color overriding the default event background. */
|
|
14
|
+
color?: string
|
|
10
15
|
[key: string]: any
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -228,6 +233,11 @@ function formatEventTimeDefault(event: CalendarEvent, date: Date): string {
|
|
|
228
233
|
return 'All day'
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
function getEventStyle(event: CalendarEvent): Record<string, string> | undefined {
|
|
237
|
+
if (!event.color) return undefined
|
|
238
|
+
return { '--event-color': event.color }
|
|
239
|
+
}
|
|
240
|
+
|
|
231
241
|
// Get ordinal suffix for a number (1st, 2nd, 3rd, 4th, etc.)
|
|
232
242
|
function getOrdinalSuffix(day: number): string {
|
|
233
243
|
const j = day % 10
|
|
@@ -655,8 +665,10 @@ watch([currentMonth, currentYear], () => {
|
|
|
655
665
|
'multi-day-start': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'start',
|
|
656
666
|
'multi-day-middle': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'middle',
|
|
657
667
|
'multi-day-end': isMultiDayEvent(event) && getMultiDayPosition(event, day.date) === 'end',
|
|
658
|
-
'drag-source': draggingEventId === event.id
|
|
668
|
+
'drag-source': draggingEventId === event.id,
|
|
669
|
+
'has-custom-color': Boolean(event.color)
|
|
659
670
|
}"
|
|
671
|
+
:style="getEventStyle(event)"
|
|
660
672
|
:draggable="eventDragEnabled"
|
|
661
673
|
@dragstart.stop="onEventDragStart(event, day.date, $event)"
|
|
662
674
|
@dragend.stop="onEventDragEnd"
|
|
@@ -666,6 +678,13 @@ watch([currentMonth, currentYear], () => {
|
|
|
666
678
|
<slot name="event" :event="event" :date="day.date" :position="getMultiDayPosition(event, day.date)">
|
|
667
679
|
<div class="event-content">
|
|
668
680
|
<div class="event-title">
|
|
681
|
+
<HugeiconsIcon
|
|
682
|
+
v-if="event.icon"
|
|
683
|
+
:icon="event.icon"
|
|
684
|
+
class="event-icon"
|
|
685
|
+
width="0.9em"
|
|
686
|
+
height="0.9em"
|
|
687
|
+
/>
|
|
669
688
|
{{ event.title }}
|
|
670
689
|
<span v-if="isMultiDayEvent(event)" class="multi-day-indicator">
|
|
671
690
|
{{ getMultiDayPosition(event, day.date) === 'start' ? '▶' :
|
|
@@ -932,6 +951,21 @@ watch([currentMonth, currentYear], () => {
|
|
|
932
951
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
933
952
|
}
|
|
934
953
|
|
|
954
|
+
.calendar-event.has-custom-color {
|
|
955
|
+
background: var(--event-color);
|
|
956
|
+
border-color: color-mix(in srgb, var(--event-color) 75%, black);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.calendar-event.has-custom-color:hover {
|
|
960
|
+
background: var(--event-color);
|
|
961
|
+
border-color: color-mix(in srgb, var(--event-color) 75%, black);
|
|
962
|
+
filter: brightness(0.92);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.event-icon {
|
|
966
|
+
flex-shrink: 0;
|
|
967
|
+
}
|
|
968
|
+
|
|
935
969
|
.calendar-event.multi-day-start {
|
|
936
970
|
border-top-left-radius: 4px;
|
|
937
971
|
border-bottom-left-radius: 4px;
|
|
@@ -1034,7 +1068,7 @@ watch([currentMonth, currentYear], () => {
|
|
|
1034
1068
|
}
|
|
1035
1069
|
}
|
|
1036
1070
|
|
|
1037
|
-
|
|
1071
|
+
html[data-theme="dark"] {
|
|
1038
1072
|
.calendar-container {
|
|
1039
1073
|
background: #565656;
|
|
1040
1074
|
border-color: #374151;
|
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
<div class = "fg1"></div>
|
|
25
25
|
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class="theme-toggle neutral"
|
|
29
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
30
|
+
:title="isDark ? 'Light mode' : 'Dark mode'"
|
|
31
|
+
@click="toggleTheme"
|
|
32
|
+
>
|
|
33
|
+
<HugeiconsIcon :icon="isDark ? Sun01Icon : Moon02Icon" width="1em" height="1em" />
|
|
34
|
+
</button>
|
|
35
|
+
|
|
26
36
|
<slot name = "user-info">
|
|
27
37
|
<div class = "user-info">
|
|
28
38
|
<span v-if="username">{{ username }}</span>
|
|
@@ -33,10 +43,13 @@
|
|
|
33
43
|
|
|
34
44
|
<script setup>
|
|
35
45
|
import { HugeiconsIcon } from "@hugeicons/vue";
|
|
36
|
-
import { Menu01Icon } from "@hugeicons/core-free-icons";
|
|
46
|
+
import { Menu01Icon, Moon02Icon, Sun01Icon } from "@hugeicons/core-free-icons";
|
|
37
47
|
|
|
38
48
|
import Breadcrumbs from "./Breadcrumbs.vue";
|
|
39
49
|
import TopBar from "./TopBar.vue";
|
|
50
|
+
import { useTheme } from '../composables/useTheme.js';
|
|
51
|
+
|
|
52
|
+
const { isDark, toggleTheme } = useTheme();
|
|
40
53
|
|
|
41
54
|
const emit = defineEmits(["toggleSidebar", "logoClick"]);
|
|
42
55
|
|
|
@@ -101,4 +114,8 @@ button:hover {
|
|
|
101
114
|
.user-info span {
|
|
102
115
|
padding-right: 0.5rem;
|
|
103
116
|
}
|
|
117
|
+
|
|
118
|
+
.theme-toggle {
|
|
119
|
+
margin-right: 0.25rem;
|
|
120
|
+
}
|
|
104
121
|
</style>
|
|
@@ -47,7 +47,10 @@ function addRouterLink(link, altTitle = null, options = {}) {
|
|
|
47
47
|
icon: foundRoute.meta?.icon || PinIcon,
|
|
48
48
|
type: 'route',
|
|
49
49
|
props: options.props || {},
|
|
50
|
-
description: options.description || foundRoute.meta?.description || null
|
|
50
|
+
description: options.description || foundRoute.meta?.description || null,
|
|
51
|
+
disabled: options.disabled || false,
|
|
52
|
+
iconColor: options.iconColor || null,
|
|
53
|
+
indicator: options.indicator || false
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
addNavigationLink(routeLink)
|
|
@@ -69,7 +72,10 @@ function addCallback(title, callback, options = {}) {
|
|
|
69
72
|
icon: options.icon || PinIcon,
|
|
70
73
|
callback: callback || (() => {}),
|
|
71
74
|
title: title,
|
|
72
|
-
description: options.description || null
|
|
75
|
+
description: options.description || null,
|
|
76
|
+
disabled: options.disabled || false,
|
|
77
|
+
iconColor: options.iconColor || null,
|
|
78
|
+
indicator: options.indicator || false
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
addNavigationLink(callbackLink)
|
|
@@ -84,6 +90,15 @@ function addSeparator(id) {
|
|
|
84
90
|
addNavigationLink(separator)
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
function addSection(title, options = {}) {
|
|
94
|
+
const section = {
|
|
95
|
+
name: options.name || `nav-section-${title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
|
|
96
|
+
type: 'section',
|
|
97
|
+
title,
|
|
98
|
+
}
|
|
99
|
+
addNavigationLink(section)
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
function addHtml(html, options = {}) {
|
|
88
103
|
const htmlLink = {
|
|
89
104
|
name: options.name || `html-item-${Date.now()}`,
|
|
@@ -147,6 +162,7 @@ provide('navigation', {
|
|
|
147
162
|
addNavigationLink,
|
|
148
163
|
addCallback,
|
|
149
164
|
addSeparator,
|
|
165
|
+
addSection,
|
|
150
166
|
addHtml,
|
|
151
167
|
removeNavigationLink,
|
|
152
168
|
clearNavigationLinks,
|
|
@@ -161,6 +177,7 @@ defineExpose({
|
|
|
161
177
|
addNavigationLink,
|
|
162
178
|
addCallback,
|
|
163
179
|
addSeparator,
|
|
180
|
+
addSection,
|
|
164
181
|
addHtml,
|
|
165
182
|
removeNavigationLink,
|
|
166
183
|
clearNavigationLinks,
|
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
<button
|
|
5
5
|
v-for="link in filteredLinks"
|
|
6
6
|
:key="link.name"
|
|
7
|
-
:class="['nav-button', { active: isLinkActive(link) }]"
|
|
7
|
+
:class="['nav-button', { active: isLinkActive(link), disabled: link.disabled }]"
|
|
8
|
+
:disabled="link.disabled"
|
|
8
9
|
:title="link.title"
|
|
9
10
|
@click="handleLinkClick(link)"
|
|
10
11
|
>
|
|
11
|
-
<
|
|
12
|
+
<span
|
|
13
|
+
v-if="showIndicator(link)"
|
|
14
|
+
class="nav-button-indicator"
|
|
15
|
+
aria-label="Requires attention"
|
|
16
|
+
/>
|
|
17
|
+
<div class="nav-button-icon" :style="getIconStyle(link)">
|
|
12
18
|
<HugeiconsIcon
|
|
13
19
|
:icon="link.icon"
|
|
14
20
|
:width="iconSize"
|
|
@@ -84,7 +90,7 @@ const filteredLinks = computed(() => {
|
|
|
84
90
|
// Exclude separators and HTML items by default
|
|
85
91
|
if (props.excludeNonButtons) {
|
|
86
92
|
links = links.filter(link =>
|
|
87
|
-
link.type !== 'separator' && link.type !== 'html'
|
|
93
|
+
link.type !== 'separator' && link.type !== 'html' && link.type !== 'section'
|
|
88
94
|
);
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -102,10 +108,30 @@ const filteredLinks = computed(() => {
|
|
|
102
108
|
});
|
|
103
109
|
|
|
104
110
|
function isLinkActive(link) {
|
|
111
|
+
if (link.disabled) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
105
114
|
return isActive(link);
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
function getIconStyle(link) {
|
|
118
|
+
if (link.disabled || isLinkActive(link)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (link.iconColor) {
|
|
122
|
+
return { color: link.iconColor };
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function showIndicator(link) {
|
|
128
|
+
return link.indicator && !link.disabled;
|
|
129
|
+
}
|
|
130
|
+
|
|
108
131
|
function handleLinkClick(link) {
|
|
132
|
+
if (link.disabled) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
109
135
|
if (link.type === 'route') {
|
|
110
136
|
const to = link.to || link.path;
|
|
111
137
|
if (to) {
|
|
@@ -147,6 +173,7 @@ function handleLinkClick(link) {
|
|
|
147
173
|
cursor: pointer;
|
|
148
174
|
transition: all 0.2s ease;
|
|
149
175
|
text-align: left;
|
|
176
|
+
position: relative;
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
.navigation-grid.compact .nav-button {
|
|
@@ -188,6 +215,46 @@ function handleLinkClick(link) {
|
|
|
188
215
|
color: #fff;
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
.nav-button:disabled,
|
|
219
|
+
.nav-button.disabled {
|
|
220
|
+
cursor: not-allowed;
|
|
221
|
+
opacity: 0.6;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.nav-button:disabled:hover,
|
|
225
|
+
.nav-button.disabled:hover {
|
|
226
|
+
border-color: var(--border-color, #e1e5e9);
|
|
227
|
+
transform: none;
|
|
228
|
+
box-shadow: none;
|
|
229
|
+
background: transparent;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.nav-button:disabled .nav-button-icon,
|
|
233
|
+
.nav-button.disabled .nav-button-icon {
|
|
234
|
+
color: var(--text-muted, #999);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.nav-button-indicator {
|
|
238
|
+
position: absolute;
|
|
239
|
+
top: 0.75rem;
|
|
240
|
+
right: 0.75rem;
|
|
241
|
+
width: 0.625rem;
|
|
242
|
+
height: 0.625rem;
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
background: var(--indicator-color, #dc3545);
|
|
245
|
+
box-shadow: 0 0 0 2px var(--background-color, #fff);
|
|
246
|
+
flex-shrink: 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.navigation-grid.compact .nav-button-indicator {
|
|
250
|
+
top: 0.5rem;
|
|
251
|
+
right: 0.5rem;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.nav-button.active .nav-button-indicator {
|
|
255
|
+
box-shadow: 0 0 0 2px var(--primary-color, #007bff);
|
|
256
|
+
}
|
|
257
|
+
|
|
191
258
|
.nav-button-label {
|
|
192
259
|
font-weight: bold;
|
|
193
260
|
font-size: 1.125rem;
|
|
@@ -219,7 +286,7 @@ function handleLinkClick(link) {
|
|
|
219
286
|
}
|
|
220
287
|
|
|
221
288
|
/* Dark theme support */
|
|
222
|
-
|
|
289
|
+
html[data-theme="dark"] {
|
|
223
290
|
.nav-button {
|
|
224
291
|
background: transparent;
|
|
225
292
|
}
|