create-nala 3.1.1 → 3.1.2
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/template/.agents/skills/nala-project/SKILL.md +24 -20
- package/template/AGENTS.md +114 -14
- package/template/CLAUDE.md +83 -13
- package/template/GEMINI.md +83 -13
- package/template/package.json +1 -1
- package/template/src/components/BrandIcon.vue +258 -0
- package/template/src/components.d.ts +1 -0
- package/template/src/layouts/AuthSplitLayout.vue +216 -0
- package/template/src/router/index.ts +100 -0
- package/template/src/style.css +0 -2
- package/template/src/views/auth/LockScreenView.vue +1 -1
- package/template/src/views/auth/MagicLinkView.vue +278 -0
- package/template/src/views/auth/SelectTenantView.vue +673 -0
- package/template/src/views/auth/SsoLoginView.vue +337 -0
- package/template/src/views/auth/SuspendedView.vue +341 -0
- package/template/src/views/auth/TwoFactorChallengeView.vue +307 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { Label } from '@/components/ui/label'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from '@/components/ui/dialog'
|
|
18
|
+
import { toast } from '@/components/ui/sonner'
|
|
19
|
+
import {
|
|
20
|
+
Building2,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
Plus,
|
|
23
|
+
Search,
|
|
24
|
+
Sparkles,
|
|
25
|
+
Users,
|
|
26
|
+
Globe,
|
|
27
|
+
ShieldCheck,
|
|
28
|
+
LogOut,
|
|
29
|
+
Layers,
|
|
30
|
+
Star,
|
|
31
|
+
Loader2,
|
|
32
|
+
Mail,
|
|
33
|
+
UserCheck,
|
|
34
|
+
Briefcase,
|
|
35
|
+
Zap,
|
|
36
|
+
} from '@lucide/vue'
|
|
37
|
+
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
|
|
40
|
+
// Current logged in user info
|
|
41
|
+
const currentUser = ref({
|
|
42
|
+
name: 'Alex Morgan',
|
|
43
|
+
email: 'alex.morgan@acme.corp',
|
|
44
|
+
avatar: 'AM',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Workspace data interface
|
|
48
|
+
export interface Workspace {
|
|
49
|
+
id: string
|
|
50
|
+
name: string
|
|
51
|
+
slug: string
|
|
52
|
+
domain: string
|
|
53
|
+
plan: 'Enterprise' | 'Team Pro' | 'Business' | 'Free Starter'
|
|
54
|
+
role: 'Owner' | 'Admin' | 'Member' | 'Billing'
|
|
55
|
+
membersCount: number
|
|
56
|
+
region: string
|
|
57
|
+
isDefault: boolean
|
|
58
|
+
hasSso: boolean
|
|
59
|
+
color: string
|
|
60
|
+
icon: 'building' | 'layers' | 'briefcase' | 'sparkles'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Initial workspaces list
|
|
64
|
+
const workspaces = ref<Workspace[]>([
|
|
65
|
+
{
|
|
66
|
+
id: 'ws-1',
|
|
67
|
+
name: 'Acme Corporation',
|
|
68
|
+
slug: 'acme-corp',
|
|
69
|
+
domain: 'acme.nala.app',
|
|
70
|
+
plan: 'Enterprise',
|
|
71
|
+
role: 'Owner',
|
|
72
|
+
membersCount: 148,
|
|
73
|
+
region: 'US-East (N. Virginia)',
|
|
74
|
+
isDefault: true,
|
|
75
|
+
hasSso: true,
|
|
76
|
+
color: 'from-violet-600 to-indigo-600',
|
|
77
|
+
icon: 'building',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'ws-2',
|
|
81
|
+
name: 'Horizon AI Labs',
|
|
82
|
+
slug: 'horizon-ai',
|
|
83
|
+
domain: 'horizon.nala.app',
|
|
84
|
+
plan: 'Team Pro',
|
|
85
|
+
role: 'Admin',
|
|
86
|
+
membersCount: 42,
|
|
87
|
+
region: 'EU-Central (Frankfurt)',
|
|
88
|
+
isDefault: false,
|
|
89
|
+
hasSso: true,
|
|
90
|
+
color: 'from-emerald-600 to-teal-600',
|
|
91
|
+
icon: 'sparkles',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'ws-3',
|
|
95
|
+
name: 'Vertex Media Global',
|
|
96
|
+
slug: 'vertex-media',
|
|
97
|
+
domain: 'vertex.nala.app',
|
|
98
|
+
plan: 'Business',
|
|
99
|
+
role: 'Member',
|
|
100
|
+
membersCount: 19,
|
|
101
|
+
region: 'AP-Southeast (Singapore)',
|
|
102
|
+
isDefault: false,
|
|
103
|
+
hasSso: false,
|
|
104
|
+
color: 'from-amber-600 to-orange-600',
|
|
105
|
+
icon: 'layers',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'ws-4',
|
|
109
|
+
name: 'Alex Morgan (Personal)',
|
|
110
|
+
slug: 'alex-morgan',
|
|
111
|
+
domain: 'alex.nala.app',
|
|
112
|
+
plan: 'Free Starter',
|
|
113
|
+
role: 'Owner',
|
|
114
|
+
membersCount: 1,
|
|
115
|
+
region: 'Global Edge',
|
|
116
|
+
isDefault: false,
|
|
117
|
+
hasSso: false,
|
|
118
|
+
color: 'from-slate-600 to-zinc-700',
|
|
119
|
+
icon: 'briefcase',
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
// Pending Invitation
|
|
124
|
+
const pendingInvitation = ref<{
|
|
125
|
+
id: string
|
|
126
|
+
orgName: string
|
|
127
|
+
invitedBy: string
|
|
128
|
+
role: string
|
|
129
|
+
membersCount: number
|
|
130
|
+
} | null>({
|
|
131
|
+
id: 'inv-1',
|
|
132
|
+
orgName: 'CyberSec Sentinel Ops',
|
|
133
|
+
invitedBy: 'sarah.jenkins@cybersec.io',
|
|
134
|
+
role: 'Security Auditor',
|
|
135
|
+
membersCount: 65,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Search & Filter State
|
|
139
|
+
const searchQuery = ref('')
|
|
140
|
+
const activeFilter = ref<'all' | 'enterprise' | 'owned'>('all')
|
|
141
|
+
const launchingId = ref<string | null>(null)
|
|
142
|
+
|
|
143
|
+
// Filtered Workspaces
|
|
144
|
+
const filteredWorkspaces = computed(() => {
|
|
145
|
+
return workspaces.value.filter((ws: Workspace) => {
|
|
146
|
+
const matchesSearch =
|
|
147
|
+
ws.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
|
148
|
+
ws.slug.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
|
149
|
+
ws.domain.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
150
|
+
|
|
151
|
+
if (!matchesSearch) return false
|
|
152
|
+
|
|
153
|
+
if (activeFilter.value === 'enterprise') {
|
|
154
|
+
return ws.plan === 'Enterprise' || ws.plan === 'Team Pro'
|
|
155
|
+
}
|
|
156
|
+
if (activeFilter.value === 'owned') {
|
|
157
|
+
return ws.role === 'Owner'
|
|
158
|
+
}
|
|
159
|
+
return true
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Plan styling helper
|
|
164
|
+
const getPlanBadgeClass = (plan: Workspace['plan']) => {
|
|
165
|
+
switch (plan) {
|
|
166
|
+
case 'Enterprise':
|
|
167
|
+
return 'bg-purple-500/15 text-purple-600 dark:text-purple-400 border-purple-500/30'
|
|
168
|
+
case 'Team Pro':
|
|
169
|
+
return 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30'
|
|
170
|
+
case 'Business':
|
|
171
|
+
return 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/30'
|
|
172
|
+
default:
|
|
173
|
+
return 'bg-muted text-muted-foreground border-border'
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Role styling helper
|
|
178
|
+
const getRoleBadgeClass = (role: Workspace['role']) => {
|
|
179
|
+
switch (role) {
|
|
180
|
+
case 'Owner':
|
|
181
|
+
return 'bg-primary/10 text-primary border-primary/20'
|
|
182
|
+
case 'Admin':
|
|
183
|
+
return 'bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-500/20'
|
|
184
|
+
case 'Billing':
|
|
185
|
+
return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20'
|
|
186
|
+
default:
|
|
187
|
+
return 'bg-muted/80 text-muted-foreground border-border/80'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Select / Launch Workspace
|
|
192
|
+
const launchWorkspace = (ws: Workspace) => {
|
|
193
|
+
launchingId.value = ws.id
|
|
194
|
+
toast.info(`Connecting to ${ws.name}...`, {
|
|
195
|
+
description: `Routing to tenant domain ${ws.domain}`,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
launchingId.value = null
|
|
200
|
+
toast.success(`Welcome back to ${ws.name}!`, {
|
|
201
|
+
description: `Active role: ${ws.role}`,
|
|
202
|
+
})
|
|
203
|
+
router.push('/dashboard')
|
|
204
|
+
}, 900)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Set Default Workspace
|
|
208
|
+
const setDefaultWorkspace = (ws: Workspace, e: MouseEvent) => {
|
|
209
|
+
e.stopPropagation()
|
|
210
|
+
workspaces.value.forEach((w: Workspace) => {
|
|
211
|
+
w.isDefault = w.id === ws.id
|
|
212
|
+
})
|
|
213
|
+
toast.success(`Default workspace updated`, {
|
|
214
|
+
description: `${ws.name} will now open automatically upon sign-in.`,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Accept Pending Invitation
|
|
219
|
+
const acceptInvitation = () => {
|
|
220
|
+
if (!pendingInvitation.value) return
|
|
221
|
+
const newWs: Workspace = {
|
|
222
|
+
id: `ws-${Date.now()}`,
|
|
223
|
+
name: pendingInvitation.value.orgName,
|
|
224
|
+
slug: pendingInvitation.value.orgName.toLowerCase().replace(/\s+/g, '-'),
|
|
225
|
+
domain: `${pendingInvitation.value.orgName.toLowerCase().replace(/\s+/g, '')}.nala.app`,
|
|
226
|
+
plan: 'Enterprise',
|
|
227
|
+
role: 'Member',
|
|
228
|
+
membersCount: pendingInvitation.value.membersCount + 1,
|
|
229
|
+
region: 'US-East (N. Virginia)',
|
|
230
|
+
isDefault: false,
|
|
231
|
+
hasSso: true,
|
|
232
|
+
color: 'from-cyan-600 to-blue-600',
|
|
233
|
+
icon: 'building',
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
workspaces.value.unshift(newWs)
|
|
237
|
+
toast.success(`Joined ${pendingInvitation.value.orgName}!`, {
|
|
238
|
+
description: `You are now a ${pendingInvitation.value.role}.`,
|
|
239
|
+
})
|
|
240
|
+
pendingInvitation.value = null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Decline Invitation
|
|
244
|
+
const declineInvitation = () => {
|
|
245
|
+
pendingInvitation.value = null
|
|
246
|
+
toast.info('Invitation dismissed')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Create New Workspace Modal State
|
|
250
|
+
const isCreateModalOpen = ref(false)
|
|
251
|
+
const isCreating = ref(false)
|
|
252
|
+
const newWorkspaceName = ref('')
|
|
253
|
+
const newWorkspaceSlug = ref('')
|
|
254
|
+
const selectedPlan = ref<'Starter' | 'Pro' | 'Enterprise'>('Pro')
|
|
255
|
+
const selectedRegion = ref('us-east-1')
|
|
256
|
+
|
|
257
|
+
const handleNameInput = () => {
|
|
258
|
+
newWorkspaceSlug.value = newWorkspaceName.value
|
|
259
|
+
.toLowerCase()
|
|
260
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
261
|
+
.replace(/(^-|-$)/g, '')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const handleCreateWorkspace = () => {
|
|
265
|
+
if (!newWorkspaceName.value.trim()) {
|
|
266
|
+
toast.error('Please enter a valid workspace name')
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
isCreating.value = true
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
isCreating.value = false
|
|
273
|
+
const created: Workspace = {
|
|
274
|
+
id: `ws-${Date.now()}`,
|
|
275
|
+
name: newWorkspaceName.value.trim(),
|
|
276
|
+
slug: newWorkspaceSlug.value || 'new-workspace',
|
|
277
|
+
domain: `${newWorkspaceSlug.value || 'new-workspace'}.nala.app`,
|
|
278
|
+
plan: selectedPlan.value === 'Enterprise' ? 'Enterprise' : selectedPlan.value === 'Pro' ? 'Team Pro' : 'Free Starter',
|
|
279
|
+
role: 'Owner',
|
|
280
|
+
membersCount: 1,
|
|
281
|
+
region: selectedRegion.value === 'eu-central-1' ? 'EU-Central (Frankfurt)' : selectedRegion.value === 'ap-southeast-1' ? 'AP-Southeast (Singapore)' : 'US-East (N. Virginia)',
|
|
282
|
+
isDefault: false,
|
|
283
|
+
hasSso: selectedPlan.value === 'Enterprise',
|
|
284
|
+
color: 'from-primary to-violet-600',
|
|
285
|
+
icon: 'sparkles',
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
workspaces.value.unshift(created)
|
|
289
|
+
isCreateModalOpen.value = false
|
|
290
|
+
newWorkspaceName.value = ''
|
|
291
|
+
newWorkspaceSlug.value = ''
|
|
292
|
+
toast.success(`Workspace "${created.name}" created successfully!`)
|
|
293
|
+
}, 1000)
|
|
294
|
+
}
|
|
295
|
+
</script>
|
|
296
|
+
|
|
297
|
+
<template>
|
|
298
|
+
<div class="w-full space-y-6">
|
|
299
|
+
<!-- User Account Summary Strip -->
|
|
300
|
+
<div class="flex items-center justify-between p-3.5 rounded-xl bg-card border border-border/70 shadow-2xs backdrop-blur-xs">
|
|
301
|
+
<div class="flex items-center gap-3">
|
|
302
|
+
<div class="h-9 w-9 rounded-full bg-primary/15 text-primary font-semibold text-xs flex items-center justify-center border border-primary/20 shrink-0">
|
|
303
|
+
{{ currentUser.avatar }}
|
|
304
|
+
</div>
|
|
305
|
+
<div class="min-w-0">
|
|
306
|
+
<div class="flex items-center gap-2">
|
|
307
|
+
<span class="text-xs font-semibold text-foreground truncate">{{ currentUser.name }}</span>
|
|
308
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
|
|
309
|
+
Active
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
312
|
+
<p class="text-[11px] text-muted-foreground truncate">{{ currentUser.email }}</p>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<router-link
|
|
317
|
+
to="/auth/login"
|
|
318
|
+
class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-muted/60"
|
|
319
|
+
title="Sign in with a different account"
|
|
320
|
+
>
|
|
321
|
+
<LogOut class="h-3.5 w-3.5" />
|
|
322
|
+
<span class="hidden sm:inline">Switch Account</span>
|
|
323
|
+
</router-link>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<!-- Main Card Container -->
|
|
327
|
+
<Card class="border-border/80 shadow-md overflow-hidden">
|
|
328
|
+
<CardHeader class="pb-4 space-y-1">
|
|
329
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
330
|
+
<div>
|
|
331
|
+
<CardTitle class="text-xl font-bold tracking-tight text-foreground flex items-center gap-2">
|
|
332
|
+
<Layers class="h-5 w-5 text-primary" />
|
|
333
|
+
Select Workspace
|
|
334
|
+
</CardTitle>
|
|
335
|
+
<CardDescription class="text-xs text-muted-foreground mt-0.5">
|
|
336
|
+
Choose an organization to enter its tenant dashboard or create a new team.
|
|
337
|
+
</CardDescription>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<!-- Create Workspace Dialog Trigger -->
|
|
341
|
+
<Dialog v-model:open="isCreateModalOpen">
|
|
342
|
+
<DialogTrigger as-child>
|
|
343
|
+
<Button size="sm" class="gap-1.5 text-xs shrink-0 cursor-pointer">
|
|
344
|
+
<Plus class="h-3.5 w-3.5" />
|
|
345
|
+
New Workspace
|
|
346
|
+
</Button>
|
|
347
|
+
</DialogTrigger>
|
|
348
|
+
|
|
349
|
+
<!-- Dialog Content with strict Title & Description accessibility -->
|
|
350
|
+
<DialogContent class="sm:max-w-md">
|
|
351
|
+
<DialogHeader>
|
|
352
|
+
<DialogTitle class="flex items-center gap-2 text-base font-semibold">
|
|
353
|
+
<Building2 class="h-4 w-4 text-primary" />
|
|
354
|
+
Create New Workspace
|
|
355
|
+
</DialogTitle>
|
|
356
|
+
<DialogDescription class="text-xs text-muted-foreground">
|
|
357
|
+
Set up a fresh multi-tenant organization, invite teammates, and choose your infrastructure region.
|
|
358
|
+
</DialogDescription>
|
|
359
|
+
</DialogHeader>
|
|
360
|
+
|
|
361
|
+
<div class="space-y-4 py-3">
|
|
362
|
+
<div class="space-y-1.5">
|
|
363
|
+
<Label for="ws-name" class="text-xs font-medium">Workspace Name</Label>
|
|
364
|
+
<Input
|
|
365
|
+
id="ws-name"
|
|
366
|
+
v-model="newWorkspaceName"
|
|
367
|
+
placeholder="e.g. Acme Studio"
|
|
368
|
+
class="h-9 text-xs"
|
|
369
|
+
@input="handleNameInput"
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div class="space-y-1.5">
|
|
374
|
+
<Label for="ws-slug" class="text-xs font-medium">Workspace URL Slug</Label>
|
|
375
|
+
<div class="flex items-center rounded-md border border-input bg-muted/40 px-2.5 py-1 text-xs text-muted-foreground">
|
|
376
|
+
<span class="text-muted-foreground/70">nala.app/</span>
|
|
377
|
+
<input
|
|
378
|
+
id="ws-slug"
|
|
379
|
+
v-model="newWorkspaceSlug"
|
|
380
|
+
placeholder="acme-studio"
|
|
381
|
+
class="flex-1 bg-transparent border-0 outline-none text-foreground px-1 text-xs"
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<!-- Plan Tier Options -->
|
|
387
|
+
<div class="space-y-2">
|
|
388
|
+
<Label class="text-xs font-medium">Select Plan</Label>
|
|
389
|
+
<div class="grid grid-cols-3 gap-2">
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
:class="[
|
|
393
|
+
'p-2.5 rounded-lg border text-left transition-all cursor-pointer flex flex-col justify-between',
|
|
394
|
+
selectedPlan === 'Starter'
|
|
395
|
+
? 'border-primary bg-primary/5 ring-1 ring-primary'
|
|
396
|
+
: 'border-border hover:bg-muted/40'
|
|
397
|
+
]"
|
|
398
|
+
@click="selectedPlan = 'Starter'"
|
|
399
|
+
>
|
|
400
|
+
<span class="text-xs font-semibold text-foreground">Starter</span>
|
|
401
|
+
<span class="text-[10px] text-muted-foreground mt-1">Free · 5 Seats</span>
|
|
402
|
+
</button>
|
|
403
|
+
|
|
404
|
+
<button
|
|
405
|
+
type="button"
|
|
406
|
+
:class="[
|
|
407
|
+
'p-2.5 rounded-lg border text-left transition-all cursor-pointer flex flex-col justify-between',
|
|
408
|
+
selectedPlan === 'Pro'
|
|
409
|
+
? 'border-primary bg-primary/5 ring-1 ring-primary'
|
|
410
|
+
: 'border-border hover:bg-muted/40'
|
|
411
|
+
]"
|
|
412
|
+
@click="selectedPlan = 'Pro'"
|
|
413
|
+
>
|
|
414
|
+
<div class="flex items-center justify-between">
|
|
415
|
+
<span class="text-xs font-semibold text-foreground">Team Pro</span>
|
|
416
|
+
<Zap class="h-3 w-3 text-amber-500" />
|
|
417
|
+
</div>
|
|
418
|
+
<span class="text-[10px] text-muted-foreground mt-1">$29/mo</span>
|
|
419
|
+
</button>
|
|
420
|
+
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
:class="[
|
|
424
|
+
'p-2.5 rounded-lg border text-left transition-all cursor-pointer flex flex-col justify-between',
|
|
425
|
+
selectedPlan === 'Enterprise'
|
|
426
|
+
? 'border-primary bg-primary/5 ring-1 ring-primary'
|
|
427
|
+
: 'border-border hover:bg-muted/40'
|
|
428
|
+
]"
|
|
429
|
+
@click="selectedPlan = 'Enterprise'"
|
|
430
|
+
>
|
|
431
|
+
<div class="flex items-center justify-between">
|
|
432
|
+
<span class="text-xs font-semibold text-foreground">Enterprise</span>
|
|
433
|
+
<Sparkles class="h-3 w-3 text-primary" />
|
|
434
|
+
</div>
|
|
435
|
+
<span class="text-[10px] text-muted-foreground mt-1">SSO & SLA</span>
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<!-- Region Selector -->
|
|
441
|
+
<div class="space-y-1.5">
|
|
442
|
+
<Label for="region-select" class="text-xs font-medium">Primary Cloud Region</Label>
|
|
443
|
+
<select
|
|
444
|
+
id="region-select"
|
|
445
|
+
v-model="selectedRegion"
|
|
446
|
+
class="h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
447
|
+
>
|
|
448
|
+
<option value="us-east-1">US-East (N. Virginia - us-east-1)</option>
|
|
449
|
+
<option value="eu-central-1">EU-Central (Frankfurt - eu-central-1)</option>
|
|
450
|
+
<option value="ap-southeast-1">AP-Southeast (Singapore - ap-southeast-1)</option>
|
|
451
|
+
</select>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<DialogFooter class="gap-2 sm:gap-0">
|
|
456
|
+
<Button variant="outline" size="sm" class="text-xs cursor-pointer" @click="isCreateModalOpen = false">
|
|
457
|
+
Cancel
|
|
458
|
+
</Button>
|
|
459
|
+
<Button size="sm" class="text-xs cursor-pointer gap-1.5" :disabled="isCreating || !newWorkspaceName.trim()" @click="handleCreateWorkspace">
|
|
460
|
+
<Loader2 v-if="isCreating" class="h-3.5 w-3.5 animate-spin" />
|
|
461
|
+
<span>{{ isCreating ? 'Creating Workspace...' : 'Create & Proceed' }}</span>
|
|
462
|
+
</Button>
|
|
463
|
+
</DialogFooter>
|
|
464
|
+
</DialogContent>
|
|
465
|
+
</Dialog>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Search Bar & Segmented Filter Tabs -->
|
|
469
|
+
<div class="pt-3 flex flex-col sm:flex-row gap-2 items-center">
|
|
470
|
+
<div class="relative w-full flex-1">
|
|
471
|
+
<Search class="h-3.5 w-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
472
|
+
<Input
|
|
473
|
+
v-model="searchQuery"
|
|
474
|
+
placeholder="Search by name, slug, or domain..."
|
|
475
|
+
class="h-8.5 text-xs pl-8.5 bg-muted/30"
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="flex items-center gap-1 bg-muted/50 p-0.5 rounded-lg border border-border/60 self-stretch sm:self-auto shrink-0">
|
|
480
|
+
<button
|
|
481
|
+
type="button"
|
|
482
|
+
:class="[
|
|
483
|
+
'px-2.5 py-1 text-xs font-medium rounded-md transition-all cursor-pointer',
|
|
484
|
+
activeFilter === 'all' ? 'bg-background text-foreground shadow-2xs' : 'text-muted-foreground hover:text-foreground'
|
|
485
|
+
]"
|
|
486
|
+
@click="activeFilter = 'all'"
|
|
487
|
+
>
|
|
488
|
+
All ({{ workspaces.length }})
|
|
489
|
+
</button>
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
:class="[
|
|
493
|
+
'px-2.5 py-1 text-xs font-medium rounded-md transition-all cursor-pointer',
|
|
494
|
+
activeFilter === 'enterprise' ? 'bg-background text-foreground shadow-2xs' : 'text-muted-foreground hover:text-foreground'
|
|
495
|
+
]"
|
|
496
|
+
@click="activeFilter = 'enterprise'"
|
|
497
|
+
>
|
|
498
|
+
Enterprise
|
|
499
|
+
</button>
|
|
500
|
+
<button
|
|
501
|
+
type="button"
|
|
502
|
+
:class="[
|
|
503
|
+
'px-2.5 py-1 text-xs font-medium rounded-md transition-all cursor-pointer',
|
|
504
|
+
activeFilter === 'owned' ? 'bg-background text-foreground shadow-2xs' : 'text-muted-foreground hover:text-foreground'
|
|
505
|
+
]"
|
|
506
|
+
@click="activeFilter = 'owned'"
|
|
507
|
+
>
|
|
508
|
+
Owner
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
</CardHeader>
|
|
513
|
+
|
|
514
|
+
<CardContent class="space-y-3 pt-1 pb-6">
|
|
515
|
+
<!-- Pending Invitation Alert Banner -->
|
|
516
|
+
<div
|
|
517
|
+
v-if="pendingInvitation"
|
|
518
|
+
class="flex flex-col sm:flex-row items-start sm:items-center justify-between p-3.5 rounded-xl border border-primary/20 bg-primary/5 gap-3"
|
|
519
|
+
>
|
|
520
|
+
<div class="flex items-start gap-2.5">
|
|
521
|
+
<div class="h-7 w-7 rounded-lg bg-primary/15 text-primary flex items-center justify-center shrink-0 mt-0.5">
|
|
522
|
+
<Mail class="h-3.5 w-3.5" />
|
|
523
|
+
</div>
|
|
524
|
+
<div>
|
|
525
|
+
<div class="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
526
|
+
Pending Workspace Invitation
|
|
527
|
+
<Badge variant="outline" class="text-[10px] py-0 border-primary/30 text-primary">
|
|
528
|
+
{{ pendingInvitation.role }}
|
|
529
|
+
</Badge>
|
|
530
|
+
</div>
|
|
531
|
+
<p class="text-[11px] text-muted-foreground mt-0.5">
|
|
532
|
+
<strong>{{ pendingInvitation.invitedBy }}</strong> invited you to join <strong>{{ pendingInvitation.orgName }}</strong>.
|
|
533
|
+
</p>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<div class="flex items-center gap-2 self-end sm:self-center shrink-0">
|
|
538
|
+
<Button size="sm" variant="ghost" class="h-7 text-xs px-2 cursor-pointer text-muted-foreground hover:text-foreground" @click="declineInvitation">
|
|
539
|
+
Decline
|
|
540
|
+
</Button>
|
|
541
|
+
<Button size="sm" class="h-7 text-xs px-2.5 gap-1.5 cursor-pointer" @click="acceptInvitation">
|
|
542
|
+
<UserCheck class="h-3.5 w-3.5" />
|
|
543
|
+
Accept & Join
|
|
544
|
+
</Button>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<!-- Workspaces List -->
|
|
549
|
+
<div v-if="filteredWorkspaces.length > 0" class="space-y-2.5">
|
|
550
|
+
<div
|
|
551
|
+
v-for="ws in filteredWorkspaces"
|
|
552
|
+
:key="ws.id"
|
|
553
|
+
class="group relative flex flex-col sm:flex-row sm:items-center justify-between p-3.5 rounded-xl border border-border/70 bg-card hover:border-primary/50 hover:bg-muted/30 transition-all duration-200 cursor-pointer shadow-2xs gap-3"
|
|
554
|
+
@click="launchWorkspace(ws)"
|
|
555
|
+
>
|
|
556
|
+
<!-- Left Info: Logo, Name, Domain, Role, Plan -->
|
|
557
|
+
<div class="flex items-start sm:items-center gap-3 min-w-0">
|
|
558
|
+
<!-- Workspace Avatar -->
|
|
559
|
+
<div
|
|
560
|
+
:class="[
|
|
561
|
+
'h-10 w-10 rounded-xl bg-linear-to-br text-white font-bold text-sm flex items-center justify-center shadow-xs shrink-0 transition-transform group-hover:scale-105',
|
|
562
|
+
ws.color
|
|
563
|
+
]"
|
|
564
|
+
>
|
|
565
|
+
<Building2 v-if="ws.icon === 'building'" class="h-5 w-5" />
|
|
566
|
+
<Sparkles v-else-if="ws.icon === 'sparkles'" class="h-5 w-5" />
|
|
567
|
+
<Layers v-else-if="ws.icon === 'layers'" class="h-5 w-5" />
|
|
568
|
+
<Briefcase v-else class="h-5 w-5" />
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<!-- Details -->
|
|
572
|
+
<div class="space-y-1 min-w-0">
|
|
573
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
574
|
+
<h3 class="text-xs sm:text-sm font-semibold text-foreground group-hover:text-primary transition-colors truncate">
|
|
575
|
+
{{ ws.name }}
|
|
576
|
+
</h3>
|
|
577
|
+
|
|
578
|
+
<!-- Default Star Badge -->
|
|
579
|
+
<button
|
|
580
|
+
type="button"
|
|
581
|
+
:class="[
|
|
582
|
+
'p-0.5 rounded transition-colors cursor-pointer',
|
|
583
|
+
ws.isDefault ? 'text-amber-500' : 'text-muted-foreground/40 hover:text-muted-foreground'
|
|
584
|
+
]"
|
|
585
|
+
:title="ws.isDefault ? 'Default workspace' : 'Set as default workspace'"
|
|
586
|
+
@click="setDefaultWorkspace(ws, $event)"
|
|
587
|
+
>
|
|
588
|
+
<Star class="h-3.5 w-3.5" :class="{ 'fill-amber-500': ws.isDefault }" />
|
|
589
|
+
</button>
|
|
590
|
+
|
|
591
|
+
<!-- Plan Badge -->
|
|
592
|
+
<span
|
|
593
|
+
:class="[
|
|
594
|
+
'inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold border',
|
|
595
|
+
getPlanBadgeClass(ws.plan)
|
|
596
|
+
]"
|
|
597
|
+
>
|
|
598
|
+
{{ ws.plan }}
|
|
599
|
+
</span>
|
|
600
|
+
|
|
601
|
+
<!-- Role Badge -->
|
|
602
|
+
<span
|
|
603
|
+
:class="[
|
|
604
|
+
'inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium border',
|
|
605
|
+
getRoleBadgeClass(ws.role)
|
|
606
|
+
]"
|
|
607
|
+
>
|
|
608
|
+
{{ ws.role }}
|
|
609
|
+
</span>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<!-- Meta row: Domain, Members, SSO, Region -->
|
|
613
|
+
<div class="flex items-center gap-3 text-[11px] text-muted-foreground flex-wrap">
|
|
614
|
+
<span class="font-mono text-muted-foreground/80">{{ ws.domain }}</span>
|
|
615
|
+
<span>·</span>
|
|
616
|
+
<span class="inline-flex items-center gap-1">
|
|
617
|
+
<Users class="h-3 w-3" />
|
|
618
|
+
{{ ws.membersCount }} {{ ws.membersCount === 1 ? 'member' : 'members' }}
|
|
619
|
+
</span>
|
|
620
|
+
<span>·</span>
|
|
621
|
+
<span class="inline-flex items-center gap-1">
|
|
622
|
+
<Globe class="h-3 w-3" />
|
|
623
|
+
{{ ws.region }}
|
|
624
|
+
</span>
|
|
625
|
+
<span v-if="ws.hasSso" class="inline-flex items-center gap-1 text-primary">
|
|
626
|
+
<span>·</span>
|
|
627
|
+
<ShieldCheck class="h-3 w-3" />
|
|
628
|
+
SSO
|
|
629
|
+
</span>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<!-- Right Action Trigger -->
|
|
635
|
+
<div class="flex items-center gap-2 self-end sm:self-center shrink-0">
|
|
636
|
+
<Button
|
|
637
|
+
size="sm"
|
|
638
|
+
variant="outline"
|
|
639
|
+
class="h-8 text-xs gap-1 group-hover:bg-primary group-hover:text-primary-foreground group-hover:border-primary transition-all cursor-pointer"
|
|
640
|
+
:disabled="launchingId === ws.id"
|
|
641
|
+
>
|
|
642
|
+
<Loader2 v-if="launchingId === ws.id" class="h-3.5 w-3.5 animate-spin" />
|
|
643
|
+
<span v-else>Launch</span>
|
|
644
|
+
<ChevronRight v-if="launchingId !== ws.id" class="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
|
645
|
+
</Button>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
<!-- Empty Filter State -->
|
|
651
|
+
<div v-else class="py-12 text-center space-y-3">
|
|
652
|
+
<div class="h-10 w-10 rounded-full bg-muted flex items-center justify-center mx-auto text-muted-foreground">
|
|
653
|
+
<Search class="h-5 w-5" />
|
|
654
|
+
</div>
|
|
655
|
+
<div class="space-y-1">
|
|
656
|
+
<p class="text-xs font-semibold text-foreground">No workspaces found</p>
|
|
657
|
+
<p class="text-[11px] text-muted-foreground">Try adjusting your search query or filter terms.</p>
|
|
658
|
+
</div>
|
|
659
|
+
<Button size="sm" variant="outline" class="text-xs cursor-pointer" @click="searchQuery = ''; activeFilter = 'all'">
|
|
660
|
+
Clear Filters
|
|
661
|
+
</Button>
|
|
662
|
+
</div>
|
|
663
|
+
</CardContent>
|
|
664
|
+
|
|
665
|
+
<CardFooter class="py-4 flex items-center justify-between text-xs text-muted-foreground border-t border-border/40">
|
|
666
|
+
<p class="text-[11px]">
|
|
667
|
+
Need to connect with an enterprise identity provider?
|
|
668
|
+
<router-link to="/auth/sso" class="text-primary hover:underline ml-1">Enterprise SSO →</router-link>
|
|
669
|
+
</p>
|
|
670
|
+
</CardFooter>
|
|
671
|
+
</Card>
|
|
672
|
+
</div>
|
|
673
|
+
</template>
|