create-nala 3.1.0 → 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.
@@ -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 &middot; 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>&middot;</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>&middot;</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>&middot;</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 &rarr;</router-link>
669
+ </p>
670
+ </CardFooter>
671
+ </Card>
672
+ </div>
673
+ </template>