picocrank 1.11.2 → 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 +1 -1
- package/vue/components/Login.vue +58 -5
- package/vue/views/LoginExample.vue +160 -2
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;
|
|
@@ -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>
|