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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.18.1",
3
+ "version": "1.18.3",
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": {
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 Example',
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 Item',
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 Example',
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: 'Form Example',
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: 'ReadOnlyTextArea Example',
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 Example',
124
+ title: 'Tabs',
94
125
  icon: Menu01Icon,
95
126
  }
96
127
  },
97
128
  {
98
- name: 'UserControlPanel',
99
- path: '/user-control-panel',
100
- component: () => import('./vue/views/UserControlPanel.vue'),
129
+ name: 'NavigationGridExample',
130
+ path: '/navigation-grid',
131
+ component: () => import('./vue/views/NavigationGridExample.vue'),
101
132
  meta: {
102
- title: 'User Control Panel',
103
- icon: SecurityValidationIcon,
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 Example',
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,3 +1,3 @@
1
1
  /* Import femtocrank styles */
2
2
  @import 'femtocrank/style.css';
3
- @import 'femtocrank/dark.css';
3
+ @import './styles/dark-theme.css';
@@ -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
- @media (prefers-color-scheme: dark) {
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
- <div class="nav-button-icon">
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
- @media (prefers-color-scheme: dark) {
289
+ html[data-theme="dark"] {
223
290
  .nav-button {
224
291
  background: transparent;
225
292
  }