picocrank 1.10.0 → 1.11.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
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
@@ -6,6 +6,7 @@ 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
8
  import { EditIcon } from '@hugeicons/core-free-icons';
9
+ import { Menu01Icon } from '@hugeicons/core-free-icons';
9
10
 
10
11
  const routes = [
11
12
  {
@@ -56,6 +57,15 @@ const routes = [
56
57
  icon: CalendarIcon,
57
58
  }
58
59
  },
60
+ {
61
+ name: 'Login',
62
+ path: '/login',
63
+ component: () => import('./vue/views/Login.vue'),
64
+ meta: {
65
+ title: 'Login',
66
+ icon: SecurityValidationIcon,
67
+ }
68
+ },
59
69
  {
60
70
  name: 'FormExample',
61
71
  path: '/form-example',
@@ -64,6 +74,33 @@ const routes = [
64
74
  title: 'Form Example',
65
75
  icon: EditIcon,
66
76
  }
77
+ },
78
+ {
79
+ name: 'TabsExample',
80
+ path: '/tabs-example',
81
+ component: () => import('./vue/views/TabsExample.vue'),
82
+ meta: {
83
+ title: 'Tabs Example',
84
+ icon: Menu01Icon,
85
+ }
86
+ },
87
+ {
88
+ name: 'UserControlPanel',
89
+ path: '/user-control-panel',
90
+ component: () => import('./vue/views/UserControlPanel.vue'),
91
+ meta: {
92
+ title: 'User Control Panel',
93
+ icon: SecurityValidationIcon,
94
+ }
95
+ },
96
+ {
97
+ name: 'LoginExample',
98
+ path: '/login-example',
99
+ component: () => import('./vue/views/LoginExample.vue'),
100
+ meta: {
101
+ title: 'Login Example',
102
+ icon: SecurityValidationIcon,
103
+ }
67
104
  }
68
105
  ];
69
106
 
@@ -675,7 +675,7 @@ watch([currentMonth, currentYear], () => {
675
675
  }
676
676
 
677
677
  .calendar-day {
678
- height: 80px;
678
+ height: 110px;
679
679
  }
680
680
 
681
681
  .day-number {
@@ -696,10 +696,6 @@ watch([currentMonth, currentYear], () => {
696
696
  min-height: 300px;
697
697
  }
698
698
 
699
- .calendar-day {
700
- height: 60px;
701
- }
702
-
703
699
  .day-header {
704
700
  padding: 0.5rem 0.25rem;
705
701
  font-size: 0.8rem;
@@ -0,0 +1,279 @@
1
+ <template>
2
+ <div class="login-container">
3
+ <Tabs :tabs="loginTabs" @tab-change="onTabChange">
4
+ <!-- Local Login Tab -->
5
+ <template #tab-local>
6
+ <div class="login-section">
7
+ <form @submit.prevent="handleLocalLogin" class="local-login-form">
8
+ <input
9
+ id="username"
10
+ v-model="localLogin.username"
11
+ required
12
+ autocomplete="username"
13
+ placeholder="Username"
14
+ />
15
+ <input
16
+ id="password"
17
+ type="password"
18
+ v-model="localLogin.password"
19
+ required
20
+ autocomplete="current-password"
21
+ placeholder="Password"
22
+ />
23
+ <button type="submit" class="good">Login</button>
24
+ <div v-if="localLoginError" class="error-message">
25
+ {{ localLoginError }}
26
+ </div>
27
+ </form>
28
+ </div>
29
+ </template>
30
+
31
+ <!-- OAuth 2 Login Tab -->
32
+ <template #tab-oauth>
33
+ <div class="login-section">
34
+ <div v-if="oauthProviders.length > 0" class="oauth-providers">
35
+ <button
36
+ v-for="provider in oauthProviders"
37
+ :key="provider.id"
38
+ class="oauth-button"
39
+ :class="provider.class || 'neutral'"
40
+ @click="handleOAuthLogin(provider)"
41
+ :title="`Sign in with ${provider.name}`"
42
+ >
43
+ <HugeiconsIcon
44
+ v-if="provider.icon"
45
+ :icon="provider.icon"
46
+ width="1.2em"
47
+ height="1.2em"
48
+ />
49
+ <span>{{ provider.name }}</span>
50
+ </button>
51
+ </div>
52
+ <div v-else class="no-providers">
53
+ <p>No OAuth providers configured.</p>
54
+ </div>
55
+ <div v-if="oauthError" class="error-message">
56
+ {{ oauthError }}
57
+ </div>
58
+ </div>
59
+ </template>
60
+ </Tabs>
61
+ </div>
62
+ </template>
63
+
64
+ <script setup>
65
+ import { ref, reactive, watch } from 'vue';
66
+ import Tabs from './Tabs.vue';
67
+ import { HugeiconsIcon } from '@hugeicons/vue';
68
+
69
+ const props = defineProps({
70
+ // Custom OAuth providers (optional)
71
+ oauthProviders: {
72
+ type: Array,
73
+ default: () => []
74
+ },
75
+ // Custom tab labels (optional)
76
+ tabLabels: {
77
+ type: Object,
78
+ default: () => ({
79
+ local: 'Username & Password',
80
+ oauth: 'OAuth2'
81
+ })
82
+ }
83
+ });
84
+
85
+ const emit = defineEmits(['local-login', 'oauth-login', 'tab-change']);
86
+
87
+ // Login tabs configuration
88
+ const loginTabs = ref([
89
+ { id: 'local', label: props.tabLabels.local || 'Username & Password' },
90
+ { id: 'oauth', label: props.tabLabels.oauth || 'OAuth2' }
91
+ ]);
92
+
93
+ // Local login state
94
+ const localLogin = reactive({
95
+ username: '',
96
+ password: ''
97
+ });
98
+
99
+ const localLoginError = ref('');
100
+
101
+ // OAuth providers - provided by parent component
102
+ const oauthProviders = ref(props.oauthProviders);
103
+ const oauthError = ref('');
104
+
105
+ // Watch for changes to oauthProviders prop
106
+ watch(() => props.oauthProviders, (newProviders) => {
107
+ oauthProviders.value = newProviders;
108
+ }, { deep: true });
109
+
110
+ // Handle local login
111
+ function handleLocalLogin() {
112
+ localLoginError.value = '';
113
+
114
+ // Emit event for parent component to handle
115
+ emit('local-login', {
116
+ username: localLogin.username,
117
+ password: localLogin.password
118
+ });
119
+ }
120
+
121
+ // Reset local login form
122
+ function resetLocalForm() {
123
+ localLogin.username = '';
124
+ localLogin.password = '';
125
+ localLoginError.value = '';
126
+ }
127
+
128
+ // Handle OAuth login
129
+ function handleOAuthLogin(provider) {
130
+ oauthError.value = '';
131
+
132
+ // Emit event for parent component to handle
133
+ emit('oauth-login', provider);
134
+ }
135
+
136
+ // Handle tab change
137
+ function onTabChange(tab, tabId) {
138
+ // Clear errors when switching tabs
139
+ localLoginError.value = '';
140
+ oauthError.value = '';
141
+ emit('tab-change', tab, tabId);
142
+ }
143
+
144
+ // Method to set error messages
145
+ function setLocalLoginError(message) {
146
+ localLoginError.value = message;
147
+ }
148
+
149
+ function setOAuthError(message) {
150
+ oauthError.value = message;
151
+ }
152
+
153
+ // Method to add custom OAuth providers programmatically
154
+ function addOAuthProvider(provider) {
155
+ oauthProviders.value.push(provider);
156
+ }
157
+
158
+ // Expose methods for parent components
159
+ defineExpose({
160
+ addOAuthProvider,
161
+ resetLocalForm,
162
+ setLocalLoginError,
163
+ setOAuthError,
164
+ localLogin
165
+ });
166
+ </script>
167
+
168
+ <style scoped>
169
+ .login-container {
170
+ max-width: 500px;
171
+ margin: 2rem auto;
172
+ }
173
+
174
+ .login-section {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 1rem;
178
+ align-items: center;
179
+ }
180
+
181
+ .local-login-form {
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 1rem;
185
+ width: 100%;
186
+ }
187
+
188
+ .local-login-form input {
189
+ padding: 0.75rem;
190
+ border: 1px solid var(--border-color, #ccc);
191
+ border-radius: 4px;
192
+ font-size: 1em;
193
+ background-color: var(--input-bg, #fff);
194
+ color: var(--text-color, #000);
195
+ width: 100%;
196
+ }
197
+
198
+ .local-login-form input:focus {
199
+ outline: none;
200
+ border-color: var(--primary-color, #007bff);
201
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
202
+ }
203
+
204
+ .local-login-form button {
205
+ width: 100%;
206
+ padding: 0.75rem 1.5rem;
207
+ font-size: 1em;
208
+ cursor: pointer;
209
+ border: none;
210
+ border-radius: 4px;
211
+ transition: opacity 0.2s;
212
+ }
213
+
214
+ .local-login-form button:hover {
215
+ opacity: 0.9;
216
+ }
217
+
218
+ .oauth-providers {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 0.75rem;
222
+ width: 100%;
223
+ }
224
+
225
+ .oauth-button {
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ gap: 0.75rem;
230
+ padding: 0.75rem 1.5rem;
231
+ font-size: 1em;
232
+ cursor: pointer;
233
+ border: none;
234
+ border-radius: 4px;
235
+ transition: opacity 0.2s, transform 0.1s;
236
+ width: 100%;
237
+ }
238
+
239
+ .oauth-button:hover {
240
+ opacity: 0.9;
241
+ transform: translateY(-1px);
242
+ }
243
+
244
+ .oauth-button:active {
245
+ transform: translateY(0);
246
+ }
247
+
248
+ .oauth-button span {
249
+ font-weight: 500;
250
+ }
251
+
252
+ .error-message {
253
+ padding: 0.75rem;
254
+ background-color: var(--error-bg, #fee);
255
+ color: var(--error-color, #c33);
256
+ border-radius: 4px;
257
+ font-size: 0.9em;
258
+ margin-top: 0.5rem;
259
+ width: 100%;
260
+ }
261
+
262
+ .no-providers {
263
+ padding: 2rem;
264
+ text-align: center;
265
+ color: var(--text-muted, #666);
266
+ }
267
+
268
+ @media (min-width: 768px) {
269
+ .login-container {
270
+ max-width: 600px;
271
+ }
272
+
273
+ .oauth-providers {
274
+ display: grid;
275
+ grid-template-columns: repeat(2, 1fr);
276
+ gap: 0.75rem;
277
+ }
278
+ }
279
+ </style>
@@ -46,7 +46,8 @@ function addRouterLink(link, altTitle = null, options = {}) {
46
46
  to: toValue,
47
47
  icon: foundRoute.meta?.icon || PinIcon,
48
48
  type: 'route',
49
- props: options.props || {}
49
+ props: options.props || {},
50
+ description: options.description || foundRoute.meta?.description || null
50
51
  }
51
52
 
52
53
  addNavigationLink(routeLink)
@@ -67,7 +68,8 @@ function addCallback(title, callback, options = {}) {
67
68
  type: 'callback',
68
69
  icon: options.icon || PinIcon,
69
70
  callback: callback || (() => {}),
70
- title: title
71
+ title: title,
72
+ description: options.description || null
71
73
  }
72
74
 
73
75
  addNavigationLink(callbackLink)
@@ -0,0 +1,294 @@
1
+ <template>
2
+ <div class="navigation-grid-container">
3
+ <div class="navigation-grid" :class="{ 'compact': compact }">
4
+ <button
5
+ v-for="link in filteredLinks"
6
+ :key="link.name"
7
+ :class="['nav-button', { active: isLinkActive(link) }]"
8
+ :title="link.title"
9
+ @click="handleLinkClick(link)"
10
+ >
11
+ <div class="nav-button-icon">
12
+ <HugeiconsIcon
13
+ :icon="link.icon"
14
+ :width="iconSize"
15
+ :height="iconSize"
16
+ />
17
+ </div>
18
+ <div class="nav-button-label">{{ link.title }}</div>
19
+ <div v-if="link.description" class="nav-button-description">
20
+ {{ link.description }}
21
+ </div>
22
+ </button>
23
+ </div>
24
+ <div v-if="filteredLinks.length === 0" class="no-links">
25
+ <p>No navigation links available</p>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { computed, inject, ref } from 'vue';
32
+ import { useRouter, useRoute } from 'vue-router';
33
+ import { HugeiconsIcon } from '@hugeicons/vue';
34
+
35
+ const props = defineProps({
36
+ // Filter links by type: 'route', 'callback', or null for all
37
+ filterType: {
38
+ type: String,
39
+ default: null,
40
+ validator: (value) => value === null || ['route', 'callback'].includes(value)
41
+ },
42
+ // Custom filter function
43
+ filter: {
44
+ type: Function,
45
+ default: null
46
+ },
47
+ // Compact mode (smaller buttons)
48
+ compact: {
49
+ type: Boolean,
50
+ default: false
51
+ },
52
+ // Icon size
53
+ iconSize: {
54
+ type: String,
55
+ default: '2em'
56
+ },
57
+ // Exclude separator and html items
58
+ excludeNonButtons: {
59
+ type: Boolean,
60
+ default: true
61
+ }
62
+ });
63
+
64
+ const router = useRouter();
65
+ const route = useRoute();
66
+
67
+ // Inject navigation from Navigation component
68
+ const navigation = inject('navigation', null);
69
+
70
+ // Get navigation links (it's a ref from the Navigation component)
71
+ const navigationLinks = navigation ? navigation.navigationLinks : ref([]);
72
+
73
+ // Get isActive function from navigation
74
+ const isActive = navigation ? navigation.isActive : (() => false);
75
+
76
+ // Filter links based on props
77
+ const filteredLinks = computed(() => {
78
+ if (!navigationLinks.value || navigationLinks.value.length === 0) {
79
+ return [];
80
+ }
81
+
82
+ let links = [...navigationLinks.value];
83
+
84
+ // Exclude separators and HTML items by default
85
+ if (props.excludeNonButtons) {
86
+ links = links.filter(link =>
87
+ link.type !== 'separator' && link.type !== 'html'
88
+ );
89
+ }
90
+
91
+ // Filter by type if specified
92
+ if (props.filterType) {
93
+ links = links.filter(link => link.type === props.filterType);
94
+ }
95
+
96
+ // Apply custom filter if provided
97
+ if (props.filter && typeof props.filter === 'function') {
98
+ links = links.filter(props.filter);
99
+ }
100
+
101
+ return links;
102
+ });
103
+
104
+ function isLinkActive(link) {
105
+ return isActive(link);
106
+ }
107
+
108
+ function handleLinkClick(link) {
109
+ if (link.type === 'route') {
110
+ const to = link.to || link.path;
111
+ if (to) {
112
+ router.push(to);
113
+ }
114
+ } else if (link.type === 'callback' && link.callback) {
115
+ link.callback();
116
+ }
117
+ }
118
+ </script>
119
+
120
+ <style scoped>
121
+ .navigation-grid-container {
122
+ width: 100%;
123
+ }
124
+
125
+ .navigation-grid {
126
+ display: grid;
127
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
128
+ gap: 1rem;
129
+ padding: 1rem 0;
130
+ }
131
+
132
+ .navigation-grid.compact {
133
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
134
+ gap: 0.75rem;
135
+ }
136
+
137
+ .nav-button {
138
+ display: flex;
139
+ flex-direction: column;
140
+ align-items: top;
141
+ justify-content: flex-start;
142
+ gap: 0.75rem;
143
+ padding: 1.25rem 0.75rem;
144
+ background: transparent;
145
+ border: 1px solid var(--border-color, #e1e5e9);
146
+ border-radius: 8px;
147
+ cursor: pointer;
148
+ transition: all 0.2s ease;
149
+ text-align: left;
150
+ }
151
+
152
+ .navigation-grid.compact .nav-button {
153
+ padding: 0.75rem 0.5rem;
154
+ gap: 0.5rem;
155
+ }
156
+
157
+ .nav-button:hover {
158
+ border-color: var(--primary-color, #007bff);
159
+ transform: translateY(-2px);
160
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
161
+ }
162
+
163
+ .nav-button:active {
164
+ transform: translateY(0);
165
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
166
+ }
167
+
168
+ .nav-button.active {
169
+ background: var(--primary-color, #007bff);
170
+ border-color: var(--primary-color, #007bff);
171
+ color: #fff;
172
+ }
173
+
174
+ .nav-button.active .nav-button-icon {
175
+ color: #fff;
176
+ }
177
+
178
+ .nav-button-icon {
179
+ display: flex;
180
+ align-items: start;
181
+ justify-content: start;
182
+ color: var(--primary-color, #007bff);
183
+ transition: color 0.2s ease;
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .nav-button.active .nav-button-icon {
188
+ color: #fff;
189
+ }
190
+
191
+ .nav-button-label {
192
+ font-size: normal;
193
+ font-weight: bold;
194
+ word-break: break-word;
195
+ text-align: left;
196
+ line-height: 1.2;
197
+ }
198
+
199
+ .navigation-grid.compact .nav-button-label {
200
+ font-size: 0.85em;
201
+ }
202
+
203
+ .nav-button.active .nav-button-label {
204
+ color: #fff;
205
+ }
206
+
207
+ .nav-button-description {
208
+ font-size: small;
209
+ font-weight: normal;
210
+ color: var(--text-muted, #666);
211
+ word-break: break-word;
212
+ text-align: left;
213
+ line-height: 1.5;
214
+ opacity: 0.8;
215
+ }
216
+
217
+ .navigation-grid.compact .nav-button-description {
218
+ font-size: 0.7em;
219
+ }
220
+
221
+ .nav-button.active .nav-button-description {
222
+ color: rgba(255, 255, 255, 0.9);
223
+ }
224
+
225
+ .no-links {
226
+ padding: 2rem;
227
+ text-align: center;
228
+ color: var(--text-muted, #666);
229
+ }
230
+
231
+ /* Dark theme support */
232
+ @media (prefers-color-scheme: dark) {
233
+ .nav-button {
234
+ background: transparent;
235
+ }
236
+
237
+ .nav-button:hover {
238
+ background: var(--hover-background-color, rgba(96, 165, 250, 0.1));
239
+ border-color: var(--primary-color, #60a5fa);
240
+ }
241
+
242
+ .nav-button.active {
243
+ background: var(--primary-color, #60a5fa);
244
+ border-color: var(--primary-color, #60a5fa);
245
+ }
246
+
247
+ .nav-button-description {
248
+ color: var(--text-muted, #999);
249
+ }
250
+
251
+ .nav-button-icon {
252
+ color: var(--primary-color, #60a5fa);
253
+ }
254
+ }
255
+
256
+ /* Responsive */
257
+ @media (max-width: 768px) {
258
+ .navigation-grid {
259
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
260
+ gap: 0.75rem;
261
+ }
262
+
263
+ .nav-button {
264
+ padding: 1rem 0.5rem;
265
+ }
266
+
267
+ .nav-button-label {
268
+ font-size: normal;
269
+ }
270
+
271
+ .nav-button-description {
272
+ font-size: 0.7em;
273
+ }
274
+ }
275
+
276
+ @media (max-width: 480px) {
277
+ .navigation-grid {
278
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
279
+ gap: 0.5rem;
280
+ }
281
+
282
+ .nav-button {
283
+ padding: 0.75rem 0.25rem;
284
+ }
285
+
286
+ .nav-button-label {
287
+ font-size: 0.8em;
288
+ }
289
+
290
+ .nav-button-description {
291
+ font-size: 0.65em;
292
+ }
293
+ }
294
+ </style>