picocrank 1.11.1 → 1.12.0
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
package/vue/components/Login.vue
CHANGED
|
@@ -57,12 +57,17 @@
|
|
|
57
57
|
</div>
|
|
58
58
|
</div>
|
|
59
59
|
</template>
|
|
60
|
+
|
|
61
|
+
<!-- Forward custom tab slots -->
|
|
62
|
+
<template v-for="slotName in customTabSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
|
|
63
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
64
|
+
</template>
|
|
60
65
|
</Tabs>
|
|
61
66
|
</div>
|
|
62
67
|
</template>
|
|
63
68
|
|
|
64
69
|
<script setup>
|
|
65
|
-
import { ref, reactive, watch } from 'vue';
|
|
70
|
+
import { ref, reactive, watch, computed, useSlots } from 'vue';
|
|
66
71
|
import Tabs from './Tabs.vue';
|
|
67
72
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
68
73
|
|
|
@@ -79,16 +84,63 @@ const props = defineProps({
|
|
|
79
84
|
local: 'Username & Password',
|
|
80
85
|
oauth: 'OAuth2'
|
|
81
86
|
})
|
|
87
|
+
},
|
|
88
|
+
// Custom tabs to inject (optional)
|
|
89
|
+
customTabs: {
|
|
90
|
+
type: Array,
|
|
91
|
+
default: () => []
|
|
92
|
+
},
|
|
93
|
+
// Whether to show default tabs (local and oauth)
|
|
94
|
+
showDefaultTabs: {
|
|
95
|
+
type: Boolean,
|
|
96
|
+
default: true
|
|
82
97
|
}
|
|
83
98
|
});
|
|
84
99
|
|
|
85
100
|
const emit = defineEmits(['local-login', 'oauth-login', 'tab-change']);
|
|
86
101
|
|
|
102
|
+
const slots = useSlots();
|
|
103
|
+
|
|
104
|
+
// Get custom tab slot names (exclude default tabs: tab-local and tab-oauth)
|
|
105
|
+
const customTabSlotNames = computed(() => {
|
|
106
|
+
if (!slots) return [];
|
|
107
|
+
const defaultSlots = ['tab-local', 'tab-oauth'];
|
|
108
|
+
const allSlotNames = Object.keys(slots);
|
|
109
|
+
return allSlotNames.filter(
|
|
110
|
+
slotName => slotName.startsWith('tab-') && !defaultSlots.includes(slotName)
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
87
114
|
// Login tabs configuration
|
|
88
|
-
const loginTabs = ref([
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
const loginTabs = ref([]);
|
|
116
|
+
|
|
117
|
+
// Initialize tabs
|
|
118
|
+
function initializeTabs() {
|
|
119
|
+
const tabs = [];
|
|
120
|
+
|
|
121
|
+
// Add default tabs if enabled
|
|
122
|
+
if (props.showDefaultTabs) {
|
|
123
|
+
tabs.push(
|
|
124
|
+
{ id: 'local', label: props.tabLabels.local || 'Username & Password' },
|
|
125
|
+
{ id: 'oauth', label: props.tabLabels.oauth || 'OAuth2' }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add custom tabs
|
|
130
|
+
if (props.customTabs && props.customTabs.length > 0) {
|
|
131
|
+
tabs.push(...props.customTabs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
loginTabs.value = tabs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Initialize tabs on mount
|
|
138
|
+
initializeTabs();
|
|
139
|
+
|
|
140
|
+
// Watch for changes to customTabs or showDefaultTabs
|
|
141
|
+
watch(() => [props.customTabs, props.showDefaultTabs], () => {
|
|
142
|
+
initializeTabs();
|
|
143
|
+
}, { deep: true });
|
|
92
144
|
|
|
93
145
|
// Local login state
|
|
94
146
|
const localLogin = reactive({
|
|
@@ -102,6 +154,7 @@ const localLoginError = ref('');
|
|
|
102
154
|
const oauthProviders = ref(props.oauthProviders);
|
|
103
155
|
const oauthError = ref('');
|
|
104
156
|
|
|
157
|
+
|
|
105
158
|
// Watch for changes to oauthProviders prop
|
|
106
159
|
watch(() => props.oauthProviders, (newProviders) => {
|
|
107
160
|
oauthProviders.value = newProviders;
|
|
@@ -189,33 +189,23 @@ function handleLinkClick(link) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
.nav-button-label {
|
|
192
|
-
font-size: normal;
|
|
193
192
|
font-weight: bold;
|
|
193
|
+
font-size: 1.125rem;
|
|
194
194
|
word-break: break-word;
|
|
195
195
|
text-align: left;
|
|
196
196
|
line-height: 1.2;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
.navigation-grid.compact .nav-button-label {
|
|
200
|
-
font-size: 0.85em;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
199
|
.nav-button.active .nav-button-label {
|
|
204
200
|
color: #fff;
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
.nav-button-description {
|
|
208
|
-
font-size: small;
|
|
209
204
|
font-weight: normal;
|
|
210
205
|
color: var(--text-muted, #666);
|
|
211
206
|
word-break: break-word;
|
|
212
207
|
text-align: left;
|
|
213
208
|
line-height: 1.5;
|
|
214
|
-
opacity: 0.8;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.navigation-grid.compact .nav-button-description {
|
|
218
|
-
font-size: 0.7em;
|
|
219
209
|
}
|
|
220
210
|
|
|
221
211
|
.nav-button.active .nav-button-description {
|
|
@@ -253,42 +243,14 @@ function handleLinkClick(link) {
|
|
|
253
243
|
}
|
|
254
244
|
}
|
|
255
245
|
|
|
256
|
-
/* Responsive */
|
|
257
246
|
@media (max-width: 768px) {
|
|
258
247
|
.navigation-grid {
|
|
259
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
248
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
260
249
|
gap: 0.75rem;
|
|
261
250
|
}
|
|
262
251
|
|
|
263
252
|
.nav-button {
|
|
264
253
|
padding: 1rem 0.5rem;
|
|
265
254
|
}
|
|
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
255
|
}
|
|
294
256
|
</style>
|
|
@@ -2,10 +2,53 @@
|
|
|
2
2
|
<Section title="Login Example" subtitle="A reusable login form component with local and OAuth2 authentication">
|
|
3
3
|
<Login
|
|
4
4
|
:oauth-providers="oauthProviders"
|
|
5
|
+
:custom-tabs="customTabs"
|
|
5
6
|
@local-login="handleLocalLogin"
|
|
6
7
|
@oauth-login="handleOAuthLogin"
|
|
7
8
|
@tab-change="onTabChange"
|
|
8
|
-
|
|
9
|
+
>
|
|
10
|
+
<!-- Custom tab: Magic Link Login -->
|
|
11
|
+
<template #tab-magic-link="{ tab }">
|
|
12
|
+
<div class="login-section">
|
|
13
|
+
<form @submit.prevent="handleMagicLinkLogin" class="magic-link-form">
|
|
14
|
+
<input
|
|
15
|
+
v-model="magicLinkEmail"
|
|
16
|
+
type="email"
|
|
17
|
+
required
|
|
18
|
+
autocomplete="email"
|
|
19
|
+
placeholder="Email address"
|
|
20
|
+
/>
|
|
21
|
+
<button type="submit" class="good">Send Magic Link</button>
|
|
22
|
+
<div v-if="magicLinkError" class="error-message">
|
|
23
|
+
{{ magicLinkError }}
|
|
24
|
+
</div>
|
|
25
|
+
<div v-if="magicLinkSuccess" class="success-message">
|
|
26
|
+
{{ magicLinkSuccess }}
|
|
27
|
+
</div>
|
|
28
|
+
</form>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<!-- Custom tab: SSO Login -->
|
|
33
|
+
<template #tab-sso="{ tab }">
|
|
34
|
+
<div class="login-section">
|
|
35
|
+
<div class="sso-providers">
|
|
36
|
+
<button
|
|
37
|
+
v-for="ssoProvider in ssoProviders"
|
|
38
|
+
:key="ssoProvider.id"
|
|
39
|
+
class="sso-button good"
|
|
40
|
+
@click="handleSSOLogin(ssoProvider)"
|
|
41
|
+
:title="`Sign in with ${ssoProvider.name}`"
|
|
42
|
+
>
|
|
43
|
+
<span>{{ ssoProvider.name }}</span>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-if="ssoError" class="error-message">
|
|
47
|
+
{{ ssoError }}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
</Login>
|
|
9
52
|
</Section>
|
|
10
53
|
</template>
|
|
11
54
|
|
|
@@ -17,6 +60,25 @@ import Login from '../components/Login.vue';
|
|
|
17
60
|
|
|
18
61
|
const router = useRouter();
|
|
19
62
|
|
|
63
|
+
// Custom tabs configuration
|
|
64
|
+
const customTabs = ref([
|
|
65
|
+
{ id: 'magic-link', label: 'Magic Link' },
|
|
66
|
+
{ id: 'sso', label: 'SSO' }
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
// Magic Link state
|
|
70
|
+
const magicLinkEmail = ref('');
|
|
71
|
+
const magicLinkError = ref('');
|
|
72
|
+
const magicLinkSuccess = ref('');
|
|
73
|
+
|
|
74
|
+
// SSO providers
|
|
75
|
+
const ssoProviders = ref([
|
|
76
|
+
{ id: 'okta', name: 'Okta SSO' },
|
|
77
|
+
{ id: 'auth0', name: 'Auth0 SSO' },
|
|
78
|
+
{ id: 'saml', name: 'SAML SSO' }
|
|
79
|
+
]);
|
|
80
|
+
const ssoError = ref('');
|
|
81
|
+
|
|
20
82
|
// OAuth providers configuration
|
|
21
83
|
const oauthProviders = ref([
|
|
22
84
|
{
|
|
@@ -109,9 +171,105 @@ function handleOAuthLogin(provider) {
|
|
|
109
171
|
// Handle tab change
|
|
110
172
|
function onTabChange(tab, tabId) {
|
|
111
173
|
console.log('Tab changed:', tab, tabId);
|
|
174
|
+
// Clear custom tab errors when switching tabs
|
|
175
|
+
magicLinkError.value = '';
|
|
176
|
+
magicLinkSuccess.value = '';
|
|
177
|
+
ssoError.value = '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle magic link login
|
|
181
|
+
function handleMagicLinkLogin() {
|
|
182
|
+
magicLinkError.value = '';
|
|
183
|
+
magicLinkSuccess.value = '';
|
|
184
|
+
|
|
185
|
+
if (!magicLinkEmail.value) {
|
|
186
|
+
magicLinkError.value = 'Please enter your email address';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// TODO: Implement actual magic link logic
|
|
191
|
+
// Example: Send magic link email
|
|
192
|
+
// try {
|
|
193
|
+
// const response = await fetch('/api/auth/magic-link', {
|
|
194
|
+
// method: 'POST',
|
|
195
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
// body: JSON.stringify({ email: magicLinkEmail.value })
|
|
197
|
+
// });
|
|
198
|
+
//
|
|
199
|
+
// if (response.ok) {
|
|
200
|
+
// magicLinkSuccess.value = 'Magic link sent! Check your email.';
|
|
201
|
+
// } else {
|
|
202
|
+
// magicLinkError.value = 'Failed to send magic link';
|
|
203
|
+
// }
|
|
204
|
+
// } catch (error) {
|
|
205
|
+
// magicLinkError.value = 'An error occurred';
|
|
206
|
+
// }
|
|
207
|
+
|
|
208
|
+
// Placeholder
|
|
209
|
+
magicLinkSuccess.value = `Magic link would be sent to: ${magicLinkEmail.value}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Handle SSO login
|
|
213
|
+
function handleSSOLogin(provider) {
|
|
214
|
+
ssoError.value = '';
|
|
215
|
+
|
|
216
|
+
// TODO: Implement actual SSO redirect logic
|
|
217
|
+
// Example: Redirect to SSO provider
|
|
218
|
+
// window.location.href = provider.authUrl;
|
|
219
|
+
|
|
220
|
+
// Placeholder
|
|
221
|
+
alert(`SSO login would redirect to: ${provider.name}`);
|
|
112
222
|
}
|
|
113
223
|
</script>
|
|
114
224
|
|
|
115
225
|
<style scoped>
|
|
116
|
-
|
|
226
|
+
.magic-link-form,
|
|
227
|
+
.sso-providers {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 1rem;
|
|
231
|
+
width: 100%;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.magic-link-form input {
|
|
235
|
+
padding: 0.75rem;
|
|
236
|
+
border: 1px solid var(--border-color, #ccc);
|
|
237
|
+
border-radius: 4px;
|
|
238
|
+
font-size: 1em;
|
|
239
|
+
background-color: var(--input-bg, #fff);
|
|
240
|
+
color: var(--text-color, #000);
|
|
241
|
+
width: 100%;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.magic-link-form input:focus {
|
|
245
|
+
outline: none;
|
|
246
|
+
border-color: var(--primary-color, #007bff);
|
|
247
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.magic-link-form button,
|
|
251
|
+
.sso-button {
|
|
252
|
+
width: 100%;
|
|
253
|
+
padding: 0.75rem 1.5rem;
|
|
254
|
+
font-size: 1em;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
border: none;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
transition: opacity 0.2s;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.magic-link-form button:hover,
|
|
262
|
+
.sso-button:hover {
|
|
263
|
+
opacity: 0.9;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.success-message {
|
|
267
|
+
padding: 0.75rem;
|
|
268
|
+
background-color: var(--success-bg, #efe);
|
|
269
|
+
color: var(--success-color, #3c3);
|
|
270
|
+
border-radius: 4px;
|
|
271
|
+
font-size: 0.9em;
|
|
272
|
+
margin-top: 0.5rem;
|
|
273
|
+
width: 100%;
|
|
274
|
+
}
|
|
117
275
|
</style>
|