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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.11.2",
3
+ "version": "1.12.0",
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": {
@@ -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
- { id: 'local', label: props.tabLabels.local || 'Username & Password' },
90
- { id: 'oauth', label: props.tabLabels.oauth || 'OAuth2' }
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
- /* Additional styles if needed */
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>