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 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.2",
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>
@@ -90,6 +90,15 @@ function addSeparator(id) {
90
90
  addNavigationLink(separator)
91
91
  }
92
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
+
93
102
  function addHtml(html, options = {}) {
94
103
  const htmlLink = {
95
104
  name: options.name || `html-item-${Date.now()}`,
@@ -153,6 +162,7 @@ provide('navigation', {
153
162
  addNavigationLink,
154
163
  addCallback,
155
164
  addSeparator,
165
+ addSection,
156
166
  addHtml,
157
167
  removeNavigationLink,
158
168
  clearNavigationLinks,
@@ -167,6 +177,7 @@ defineExpose({
167
177
  addNavigationLink,
168
178
  addCallback,
169
179
  addSeparator,
180
+ addSection,
170
181
  addHtml,
171
182
  removeNavigationLink,
172
183
  clearNavigationLinks,
@@ -90,7 +90,7 @@ const filteredLinks = computed(() => {
90
90
  // Exclude separators and HTML items by default
91
91
  if (props.excludeNonButtons) {
92
92
  links = links.filter(link =>
93
- link.type !== 'separator' && link.type !== 'html'
93
+ link.type !== 'separator' && link.type !== 'html' && link.type !== 'section'
94
94
  );
95
95
  }
96
96
 
@@ -286,7 +286,7 @@ function handleLinkClick(link) {
286
286
  }
287
287
 
288
288
  /* Dark theme support */
289
- @media (prefers-color-scheme: dark) {
289
+ html[data-theme="dark"] {
290
290
  .nav-button {
291
291
  background: transparent;
292
292
  }
@@ -0,0 +1,118 @@
1
+ <template>
2
+ <div
3
+ class="notification-popup notification show"
4
+ :class="popup.class"
5
+ role="status"
6
+ >
7
+ <button
8
+ type="button"
9
+ class="notification-popup-dismiss"
10
+ aria-label="Dismiss notification"
11
+ @click.stop="emit('dismiss', popup.id)"
12
+ >
13
+ <HugeiconsIcon :icon="Cancel01Icon" width="1em" height="1em" />
14
+ </button>
15
+
16
+ <div class="notification-popup-body">
17
+ <p class="notification-popup-message">
18
+ <strong v-if="popup.label">{{ popup.label }}:</strong>
19
+ {{ popup.message }}
20
+ </p>
21
+
22
+ <RouterLink
23
+ v-if="popup.linkTo && !isExternalLink"
24
+ :to="popup.linkTo"
25
+ class="notification-popup-link"
26
+ @click="emit('dismiss', popup.id)"
27
+ >
28
+ {{ popup.linkLabel || 'View' }}
29
+ </RouterLink>
30
+
31
+ <a
32
+ v-else-if="popup.linkTo && isExternalLink"
33
+ :href="popup.linkTo"
34
+ class="notification-popup-link"
35
+ target="_blank"
36
+ rel="noopener noreferrer"
37
+ @click="emit('dismiss', popup.id)"
38
+ >
39
+ {{ popup.linkLabel || 'View' }}
40
+ </a>
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup>
46
+ import { computed } from 'vue'
47
+ import { RouterLink } from 'vue-router'
48
+ import { HugeiconsIcon } from '@hugeicons/vue'
49
+ import { Cancel01Icon } from '@hugeicons/core-free-icons'
50
+
51
+ const props = defineProps({
52
+ popup: {
53
+ type: Object,
54
+ required: true,
55
+ },
56
+ })
57
+
58
+ const emit = defineEmits(['dismiss'])
59
+
60
+ const isExternalLink = computed(() =>
61
+ typeof props.popup.linkTo === 'string' && /^https?:\/\//.test(props.popup.linkTo)
62
+ )
63
+ </script>
64
+
65
+ <style scoped>
66
+ .notification-popup {
67
+ position: relative;
68
+ width: min(22rem, calc(100vw - 2rem));
69
+ margin: 0;
70
+ padding: 0.75rem 2.25rem 0.75rem 0.75rem;
71
+ cursor: default;
72
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
73
+ }
74
+
75
+ .notification-popup-message {
76
+ margin: 0;
77
+ }
78
+
79
+ .notification-popup-body {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 0.5rem;
83
+ }
84
+
85
+ .notification-popup-link {
86
+ color: inherit;
87
+ font-weight: bold;
88
+ text-decoration: underline;
89
+ align-self: flex-start;
90
+ }
91
+
92
+ .notification-popup-link:hover {
93
+ opacity: 0.85;
94
+ }
95
+
96
+ .notification-popup-dismiss {
97
+ position: absolute;
98
+ top: 0.35rem;
99
+ right: 0.35rem;
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ width: 1.75rem;
104
+ height: 1.75rem;
105
+ padding: 0;
106
+ border: 0;
107
+ border-radius: 0.35rem;
108
+ background: transparent;
109
+ color: inherit;
110
+ cursor: pointer;
111
+ opacity: 0.75;
112
+ }
113
+
114
+ .notification-popup-dismiss:hover {
115
+ opacity: 1;
116
+ background: rgba(0, 0, 0, 0.08);
117
+ }
118
+ </style>