deepspace 0.7.0 → 0.8.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +22 -8
  3. package/dist/cli.js +9812 -4674
  4. package/dist/cli.js.map +1 -1
  5. package/dist/feature-installer.js +638 -0
  6. package/dist/feature-installer.js.map +1 -0
  7. package/dist/index.d.ts +107 -117
  8. package/dist/index.js +610 -779
  9. package/dist/index.js.map +1 -1
  10. package/dist/server.d.ts +403 -167
  11. package/dist/server.js +498 -367
  12. package/dist/server.js.map +1 -1
  13. package/dist/testing.d.ts +115 -0
  14. package/dist/testing.js +3 -3
  15. package/dist/testing.js.map +1 -1
  16. package/dist/worker.d.ts +397 -160
  17. package/dist/worker.js +506 -367
  18. package/dist/worker.js.map +1 -1
  19. package/features/ai-chat/feature.json +2 -0
  20. package/features/ai-chat/src/AiChatPage.tsx +2 -3
  21. package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
  22. package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
  23. package/features/ai-chat/src/ChatPanel.tsx +158 -862
  24. package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
  25. package/features/canvas/src/CanvasListPage.tsx +1 -1
  26. package/features/docs/feature.json +4 -8
  27. package/features/docs/src/DocsEditorPage.tsx +33 -326
  28. package/features/docs/src/DocsListPage.tsx +33 -29
  29. package/features/docs/src/InviteDialog.tsx +11 -40
  30. package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
  31. package/features/docs/src/docs-library-types.ts +28 -3
  32. package/features/docs/src/docs-ui.css +1 -4
  33. package/features/docs/src/use-docs-presence-access.ts +330 -0
  34. package/features/file-attachments/feature.json +2 -2
  35. package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
  36. package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
  37. package/features/file-attachments/src/useFilePreview.ts +54 -68
  38. package/features/file-manager/src/FileManagerPage.tsx +1 -1
  39. package/features/integration-test/src/integration.spec.ts +1 -1
  40. package/features/items/feature.json +1 -5
  41. package/features/items/src/ItemsPage.tsx +207 -135
  42. package/features/landing/feature.json +3 -0
  43. package/features/landing/src/LandingPage.tsx +4 -18
  44. package/features/leaderboard/feature.json +1 -5
  45. package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
  46. package/features/messaging/feature.json +0 -4
  47. package/features/messaging/src/ChatMultiPage.tsx +1 -3
  48. package/features/messaging/src/ChatPage.tsx +1 -3
  49. package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
  50. package/features/search-bar/feature.json +8 -3
  51. package/features/search-bar/src/SearchOverlay.tsx +108 -252
  52. package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
  53. package/features/search-bar/src/search-model.ts +223 -0
  54. package/features/tasks/src/TasksPage.tsx +3 -3
  55. package/features/topbar/src/Topbar.tsx +0 -17
  56. package/package.json +2 -3
  57. package/features/docs/src/docs-favorites.ts +0 -18
  58. package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
  59. package/features/file-attachments/src/attachments-constants.ts +0 -9
  60. package/features/items/src/items-constants.ts +0 -10
  61. package/features/leaderboard/src/leaderboard-constants.ts +0 -21
  62. package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
  63. package/scripts/add-feature.cjs +0 -807
@@ -8,17 +8,25 @@
8
8
  * - Role-based UI elements
9
9
  */
10
10
 
11
- import { useState, useMemo } from 'react'
12
- import { useUser } from 'deepspace'
13
- import { useQuery } from 'deepspace'
14
- import { useMutations } from 'deepspace'
15
- import { Button, Modal, EmptyState, Badge } from '@/components/ui'
16
- import { ROLES, type Role } from 'deepspace'
17
- import { ITEM_STATUS } from '../components/items/items-constants'
18
-
19
- // ============================================================================
20
- // Types
21
- // ============================================================================
11
+ import { useMemo, useState } from 'react'
12
+ import { Archive, ArchiveRestore, Inbox, Plus, Trash2 } from 'lucide-react'
13
+ import { ROLES, useMutations, useQuery, useUser, type Role } from 'deepspace'
14
+ import {
15
+ Badge,
16
+ Button,
17
+ ConfirmModal,
18
+ EmptyState,
19
+ Input,
20
+ Label,
21
+ Modal,
22
+ Textarea,
23
+ useToast,
24
+ } from '@/components/ui'
25
+
26
+ const ITEM_STATUS = {
27
+ ACTIVE: 'active',
28
+ ARCHIVED: 'archived',
29
+ } as const
22
30
 
23
31
  interface Item {
24
32
  title: string
@@ -27,134 +35,157 @@ interface Item {
27
35
  ownerId: string
28
36
  }
29
37
 
38
+ type ItemRecord = {
39
+ recordId: string
40
+ data: Item
41
+ createdAt: string
42
+ }
43
+
30
44
  interface ItemsPageProps {
31
45
  className?: string
32
46
  }
33
47
 
34
- // ============================================================================
35
- // Create Item Modal
36
- // ============================================================================
37
-
38
48
  interface CreateItemModalProps {
39
49
  isOpen: boolean
40
50
  onClose: () => void
41
- onCreate: (title: string, description: string) => void
51
+ onCreate: (title: string, description: string) => Promise<boolean>
42
52
  }
43
53
 
44
54
  function CreateItemModal({ isOpen, onClose, onCreate }: CreateItemModalProps) {
45
55
  const [title, setTitle] = useState('')
46
56
  const [description, setDescription] = useState('')
57
+ const [creating, setCreating] = useState(false)
47
58
 
48
- const handleSubmit = () => {
49
- if (title.trim()) {
50
- onCreate(title.trim(), description.trim())
59
+ const handleSubmit = async () => {
60
+ const nextTitle = title.trim()
61
+ if (!nextTitle || creating) return
62
+
63
+ setCreating(true)
64
+ try {
65
+ const created = await onCreate(nextTitle, description.trim())
66
+ if (!created) return
51
67
  setTitle('')
52
68
  setDescription('')
53
69
  onClose()
70
+ } finally {
71
+ setCreating(false)
54
72
  }
55
73
  }
56
74
 
57
75
  return (
58
- <Modal open={isOpen} onClose={onClose} size="sm">
59
- <Modal.Header onClose={onClose}>
76
+ <Modal open={isOpen} onClose={creating ? () => undefined : onClose} size="sm">
77
+ <Modal.Header>
60
78
  <Modal.Title>Create Item</Modal.Title>
61
79
  </Modal.Header>
62
80
  <Modal.Body>
63
81
  <div className="space-y-4">
64
82
  <div>
65
- <label className="block text-sm font-medium text-muted-foreground mb-2">Title</label>
66
- <input
83
+ <Label htmlFor="item-title" className="mb-2 block">
84
+ Title
85
+ </Label>
86
+ <Input
87
+ id="item-title"
67
88
  type="text"
68
89
  value={title}
69
- onChange={(e) => setTitle(e.target.value)}
90
+ onChange={(event) => setTitle(event.target.value)}
70
91
  placeholder="Enter item title"
71
- className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-ring"
72
92
  autoFocus
73
93
  />
74
94
  </div>
75
95
  <div>
76
- <label className="block text-sm font-medium text-muted-foreground mb-2">Description</label>
77
- <textarea
96
+ <Label htmlFor="item-description" className="mb-2 block">
97
+ Description
98
+ </Label>
99
+ <Textarea
100
+ id="item-description"
78
101
  value={description}
79
- onChange={(e) => setDescription(e.target.value)}
102
+ onChange={(event) => setDescription(event.target.value)}
80
103
  placeholder="Enter description (optional)"
81
104
  rows={3}
82
- className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-ring resize-none"
105
+ className="resize-none"
83
106
  />
84
107
  </div>
85
108
  </div>
86
109
  </Modal.Body>
87
110
  <Modal.Footer>
88
- <Button variant="secondary" onClick={onClose}>Cancel</Button>
89
- <Button onClick={handleSubmit} disabled={!title.trim()}>Create</Button>
111
+ <Button variant="secondary" onClick={onClose} disabled={creating}>
112
+ Cancel
113
+ </Button>
114
+ <Button onClick={() => void handleSubmit()} disabled={!title.trim()} loading={creating}>
115
+ Create
116
+ </Button>
90
117
  </Modal.Footer>
91
118
  </Modal>
92
119
  )
93
120
  }
94
121
 
95
- // ============================================================================
96
- // Item Card Component
97
- // ============================================================================
98
-
99
122
  interface ItemCardProps {
100
- item: { recordId: string; data: Item; createdAt: string }
123
+ item: ItemRecord
101
124
  isOwner: boolean
125
+ isToggling?: boolean
102
126
  onToggleStatus: () => void
103
127
  onDelete: () => void
104
128
  }
105
129
 
106
- function ItemCard({ item, isOwner, onToggleStatus, onDelete }: ItemCardProps) {
130
+ function ItemCard({ item, isOwner, isToggling = false, onToggleStatus, onDelete }: ItemCardProps) {
107
131
  const isArchived = item.data.status === ITEM_STATUS.ARCHIVED
132
+ const statusAction = isArchived ? 'Restore' : 'Archive'
108
133
 
109
134
  return (
110
- <div className={`p-4 bg-card/60 rounded-xl border ${isArchived ? 'border-border/30 opacity-60' : 'border-border'}`}>
135
+ <div
136
+ className={`rounded-xl border bg-card/60 p-4 ${
137
+ isArchived ? 'border-border/30 opacity-60' : 'border-border'
138
+ }`}
139
+ >
111
140
  <div className="flex items-start justify-between gap-3">
112
- <div className="flex-1 min-w-0">
113
- <div className="flex items-center gap-2 mb-1">
114
- <h3 className={`font-medium ${isArchived ? 'text-muted-foreground line-through' : 'text-foreground'}`}>
141
+ <div className="min-w-0 flex-1">
142
+ <div className="mb-1 flex items-center gap-2">
143
+ <h3
144
+ className={`font-medium ${
145
+ isArchived ? 'text-muted-foreground line-through' : 'text-foreground'
146
+ }`}
147
+ >
115
148
  {item.data.title}
116
149
  </h3>
117
- <Badge
118
- variant={isArchived ? 'secondary' : 'success'}
119
- size="sm"
120
- >
150
+ <Badge variant={isArchived ? 'secondary' : 'success'} size="sm">
121
151
  {isArchived ? 'Archived' : 'Active'}
122
152
  </Badge>
123
153
  </div>
124
154
  {item.data.description && (
125
- <p className="text-sm text-muted-foreground line-clamp-2">{item.data.description}</p>
155
+ <p className="line-clamp-2 text-sm text-muted-foreground">{item.data.description}</p>
126
156
  )}
127
- <p className="text-xs text-muted-foreground mt-2">
157
+ <p className="mt-2 text-xs text-muted-foreground">
128
158
  {new Date(item.createdAt).toLocaleDateString()}
129
159
  </p>
130
160
  </div>
131
161
 
132
162
  {isOwner && (
133
163
  <div className="flex items-center gap-1">
134
- <button
164
+ <Button
165
+ variant="ghost"
166
+ size="icon"
167
+ className="size-8 text-muted-foreground"
135
168
  onClick={onToggleStatus}
136
- className="p-2 text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-lg transition-colors"
137
- title={isArchived ? 'Restore' : 'Archive'}
169
+ aria-label={`${statusAction} ${item.data.title}`}
170
+ loading={isToggling}
138
171
  >
139
- {isArchived ? (
140
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
141
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
142
- </svg>
143
- ) : (
144
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
145
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
146
- </svg>
147
- )}
148
- </button>
149
- <button
172
+ {!isToggling &&
173
+ (isArchived ? (
174
+ <ArchiveRestore aria-hidden="true" />
175
+ ) : (
176
+ <Archive aria-hidden="true" />
177
+ ))}
178
+ </Button>
179
+ <Button
180
+ variant="ghost"
181
+ size="icon"
182
+ className="size-8 text-muted-foreground hover:bg-destructive/20 hover:text-destructive"
150
183
  onClick={onDelete}
151
- className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/20 rounded-lg transition-colors"
152
- title="Delete"
184
+ disabled={isToggling}
185
+ aria-label={`Delete ${item.data.title}`}
153
186
  >
154
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
155
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
156
- </svg>
157
- </button>
187
+ <Trash2 aria-hidden="true" />
188
+ </Button>
158
189
  </div>
159
190
  )}
160
191
  </div>
@@ -162,91 +193,120 @@ function ItemCard({ item, isOwner, onToggleStatus, onDelete }: ItemCardProps) {
162
193
  )
163
194
  }
164
195
 
165
- // ============================================================================
166
- // Main Page
167
- // ============================================================================
168
-
169
196
  export default function ItemsPage({ className }: ItemsPageProps) {
170
197
  const { user } = useUser()
171
198
  const userRole = (user?.role ?? ROLES.VIEWER) as Role
172
199
  const canCreate = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
200
+ const toast = useToast()
173
201
 
174
202
  const [showCreateModal, setShowCreateModal] = useState(false)
203
+ const [togglingId, setTogglingId] = useState<string | null>(null)
204
+ const [deleteTarget, setDeleteTarget] = useState<ItemRecord | null>(null)
205
+ const [deleting, setDeleting] = useState(false)
175
206
 
176
- // Query all items
177
207
  const { records: items, status } = useQuery<Item>('items', {
178
208
  orderBy: 'createdAt',
179
209
  orderDir: 'desc',
180
210
  })
211
+ const { createConfirmed, putConfirmed, removeConfirmed } = useMutations<Item>('items')
181
212
 
182
- // Mutations for items
183
- const { create, put, remove } = useMutations<Item>('items')
184
-
185
- // Separate own items and others
186
213
  const { myItems, otherItems } = useMemo(() => {
187
214
  const my: typeof items = []
188
215
  const other: typeof items = []
189
216
 
190
- items.forEach(item => {
191
- if (item.data.ownerId === user?.id) {
192
- my.push(item)
193
- } else {
194
- other.push(item)
195
- }
217
+ items.forEach((item) => {
218
+ if (item.data.ownerId === user?.id) my.push(item)
219
+ else other.push(item)
196
220
  })
197
221
 
198
222
  return { myItems: my, otherItems: other }
199
223
  }, [items, user?.id])
200
224
 
201
- const handleCreate = async (title: string, description: string) => {
202
- await create({
203
- title,
204
- description,
205
- status: ITEM_STATUS.ACTIVE,
206
- ownerId: user!.id,
207
- })
225
+ const handleCreate = async (title: string, description: string): Promise<boolean> => {
226
+ if (!user) {
227
+ toast.error('Could not create item', 'Sign in and try again.')
228
+ return false
229
+ }
230
+
231
+ try {
232
+ await createConfirmed({
233
+ title,
234
+ description,
235
+ status: ITEM_STATUS.ACTIVE,
236
+ ownerId: user.id,
237
+ })
238
+ toast.success('Item created', `"${title}" is ready.`)
239
+ return true
240
+ } catch (error) {
241
+ toast.error(
242
+ 'Could not create item',
243
+ error instanceof Error ? error.message : 'Please try again.',
244
+ )
245
+ return false
246
+ }
208
247
  }
209
248
 
210
249
  const handleToggleStatus = async (itemId: string, currentStatus: string) => {
211
- const newStatus = currentStatus === ITEM_STATUS.ACTIVE
212
- ? ITEM_STATUS.ARCHIVED
213
- : ITEM_STATUS.ACTIVE
214
-
215
- const item = items.find(i => i.recordId === itemId)
216
- if (item) {
217
- await put(itemId, { ...item.data, status: newStatus })
250
+ const item = items.find((candidate) => candidate.recordId === itemId)
251
+ if (!item || togglingId) return
252
+
253
+ const newStatus =
254
+ currentStatus === ITEM_STATUS.ACTIVE ? ITEM_STATUS.ARCHIVED : ITEM_STATUS.ACTIVE
255
+ setTogglingId(itemId)
256
+ try {
257
+ await putConfirmed(itemId, { status: newStatus })
258
+ if (newStatus === ITEM_STATUS.ARCHIVED) {
259
+ toast.success('Item archived', `"${item.data.title}" moved to the archive.`)
260
+ } else {
261
+ toast.success('Item restored', `"${item.data.title}" is active again.`)
262
+ }
263
+ } catch (error) {
264
+ toast.error(
265
+ 'Could not update item',
266
+ error instanceof Error ? error.message : 'Please try again.',
267
+ )
268
+ } finally {
269
+ setTogglingId(null)
218
270
  }
219
271
  }
220
272
 
221
- const handleDelete = async (itemId: string) => {
222
- if (confirm('Are you sure you want to delete this item?')) {
223
- await remove(itemId)
273
+ const handleDelete = async () => {
274
+ if (!deleteTarget || deleting) return
275
+
276
+ setDeleting(true)
277
+ try {
278
+ await removeConfirmed(deleteTarget.recordId)
279
+ toast.success('Item deleted', `"${deleteTarget.data.title}" was deleted.`)
280
+ setDeleteTarget(null)
281
+ } catch (error) {
282
+ toast.error(
283
+ 'Could not delete item',
284
+ error instanceof Error ? error.message : 'Please try again.',
285
+ )
286
+ } finally {
287
+ setDeleting(false)
224
288
  }
225
289
  }
226
290
 
227
291
  const isLoading = status === 'loading'
228
292
 
229
293
  return (
230
- <div className={`h-full bg-background overflow-y-auto ${className ?? ''}`}>
231
- {/* Header */}
232
- <div className="bg-card/60 backdrop-blur-md border-b border-border sticky top-0 z-10">
233
- <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
294
+ <div className={`h-full overflow-y-auto bg-background ${className ?? ''}`}>
295
+ <div className="sticky top-0 z-10 border-b border-border bg-card/60 backdrop-blur-md">
296
+ <div className="mx-auto max-w-4xl px-4 py-4 sm:px-6 lg:px-8">
234
297
  <div className="flex items-center justify-between">
235
298
  <div>
236
- <h1 className="text-2xl font-bold text-foreground">Welcome, {user?.name ?? 'Guest'}</h1>
237
- <p className="text-muted-foreground mt-1">
238
- {canCreate
239
- ? 'Create and manage your items below'
240
- : 'You can view items but need member access to create'
241
- }
299
+ <h1 className="text-2xl font-bold text-foreground">Items</h1>
300
+ <p className="mt-1 text-muted-foreground">
301
+ {isLoading
302
+ ? 'Loading items...'
303
+ : `${items.length} ${items.length === 1 ? 'item' : 'items'} in this workspace`}
242
304
  </p>
243
305
  </div>
244
306
 
245
307
  {canCreate && (
246
308
  <Button onClick={() => setShowCreateModal(true)}>
247
- <svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
248
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
249
- </svg>
309
+ <Plus aria-hidden="true" />
250
310
  New Item
251
311
  </Button>
252
312
  )}
@@ -254,57 +314,58 @@ export default function ItemsPage({ className }: ItemsPageProps) {
254
314
  </div>
255
315
  </div>
256
316
 
257
- {/* Content */}
258
- <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
317
+ <div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">
259
318
  {isLoading ? (
260
- <div className="flex items-center justify-center py-16">
261
- <div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
319
+ <div className="grid gap-4 sm:grid-cols-2" aria-label="Loading items" aria-busy="true">
320
+ <div className="h-32 animate-pulse rounded-xl border border-border bg-muted/40" />
321
+ <div className="h-32 animate-pulse rounded-xl border border-border bg-muted/40" />
262
322
  </div>
263
323
  ) : items.length === 0 ? (
264
324
  <EmptyState
265
325
  title="No items yet"
266
- description={canCreate
267
- ? "Create your first item to get started"
268
- : "No items have been created yet"
326
+ description={
327
+ canCreate
328
+ ? 'Create your first item to get started.'
329
+ : 'No items have been created yet.'
269
330
  }
270
- icon={
271
- <svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
272
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
273
- </svg>
331
+ icon={<Inbox aria-hidden="true" />}
332
+ action={
333
+ canCreate ? { label: 'New item', onClick: () => setShowCreateModal(true) } : undefined
274
334
  }
275
335
  />
276
336
  ) : (
277
337
  <div className="space-y-8">
278
- {/* My Items */}
279
338
  {myItems.length > 0 && (
280
339
  <section>
281
- <h2 className="text-lg font-semibold text-foreground mb-4">My Items</h2>
340
+ <h2 className="mb-4 text-lg font-semibold text-foreground">My Items</h2>
282
341
  <div className="grid gap-4 sm:grid-cols-2">
283
- {myItems.map(item => (
342
+ {myItems.map((item) => (
284
343
  <ItemCard
285
344
  key={item.recordId}
286
345
  item={item}
287
- isOwner={true}
288
- onToggleStatus={() => handleToggleStatus(item.recordId, item.data.status)}
289
- onDelete={() => handleDelete(item.recordId)}
346
+ isOwner
347
+ isToggling={togglingId === item.recordId}
348
+ onToggleStatus={() =>
349
+ void handleToggleStatus(item.recordId, item.data.status)
350
+ }
351
+ onDelete={() => setDeleteTarget(item)}
290
352
  />
291
353
  ))}
292
354
  </div>
293
355
  </section>
294
356
  )}
295
357
 
296
- {/* Other Items */}
297
358
  {otherItems.length > 0 && (
298
359
  <section>
299
- <h2 className="text-lg font-semibold text-foreground mb-4">All Items</h2>
360
+ <h2 className="mb-4 text-lg font-semibold text-foreground">All Items</h2>
300
361
  <div className="grid gap-4 sm:grid-cols-2">
301
- {otherItems.map(item => (
362
+ {otherItems.map((item) => (
302
363
  <ItemCard
303
364
  key={item.recordId}
304
365
  item={item}
305
366
  isOwner={false}
306
- onToggleStatus={() => {}}
307
- onDelete={() => {}}
367
+ onToggleStatus={() => undefined}
368
+ onDelete={() => undefined}
308
369
  />
309
370
  ))}
310
371
  </div>
@@ -314,12 +375,23 @@ export default function ItemsPage({ className }: ItemsPageProps) {
314
375
  )}
315
376
  </div>
316
377
 
317
- {/* Create Modal */}
318
378
  <CreateItemModal
319
379
  isOpen={showCreateModal}
320
380
  onClose={() => setShowCreateModal(false)}
321
381
  onCreate={handleCreate}
322
382
  />
383
+
384
+ <ConfirmModal
385
+ open={deleteTarget !== null}
386
+ onClose={() => {
387
+ if (!deleting) setDeleteTarget(null)
388
+ }}
389
+ onConfirm={() => void handleDelete()}
390
+ title={deleteTarget ? `Delete "${deleteTarget.data.title}"?` : 'Delete item?'}
391
+ description="This cannot be undone."
392
+ confirmText="Delete"
393
+ loading={deleting}
394
+ />
323
395
  </div>
324
396
  )
325
397
  }
@@ -55,6 +55,9 @@
55
55
  "protected": false
56
56
  },
57
57
  "requires": [],
58
+ "dependencies": {
59
+ "framer-motion": "^12.38.0"
60
+ },
58
61
  "instructions": [
59
62
  "Configure APP_NAME, HERO_HEADLINE, HERO_SUBHEADLINE, FAQ_ITEMS etc. in LandingPage.tsx"
60
63
  ],
@@ -110,20 +110,6 @@ function useLandingCSS() {
110
110
  }, [])
111
111
  }
112
112
 
113
- // ============================================================================
114
- // Landing page "seen" flag
115
- // ============================================================================
116
-
117
- const LANDING_SEEN_KEY = 'app-landing-seen'
118
-
119
- export function hasSeenLanding(): boolean {
120
- try { return localStorage.getItem(LANDING_SEEN_KEY) === 'true' } catch { return false }
121
- }
122
-
123
- export function markLandingSeen(): void {
124
- try { localStorage.setItem(LANDING_SEEN_KEY, 'true') } catch { /* storage unavailable (private mode/quota): non-critical */ }
125
- }
126
-
127
113
  // ============================================================================
128
114
  // Configuration
129
115
  // ============================================================================
@@ -265,7 +251,7 @@ function LandingNav({ isScrolled, scrollRoot }: {
265
251
  })}
266
252
  <div className="h-px bg-foreground/[0.08] my-1" />
267
253
  <button
268
- onClick={() => { markLandingSeen(); navigate('/home') }}
254
+ onClick={() => navigate('/home')}
269
255
  className="px-4 py-2.5 rounded-xl text-sm font-medium text-left text-primary hover:bg-foreground/[0.08] transition-colors"
270
256
  >
271
257
  Get Started
@@ -298,7 +284,7 @@ function LandingNav({ isScrolled, scrollRoot }: {
298
284
  )
299
285
  })}
300
286
  </div>
301
- <Button size="sm" onClick={() => { markLandingSeen(); navigate('/home') }} className="hidden md:inline-flex">
287
+ <Button size="sm" onClick={() => navigate('/home')} className="hidden md:inline-flex">
302
288
  Get Started
303
289
  </Button>
304
290
  <button className="md:hidden transition-colors text-muted-foreground hover:text-foreground" onClick={() => setMobileOpen(prev => !prev)} aria-label="Toggle menu">
@@ -377,7 +363,7 @@ function HeroSection() {
377
363
  </motion.p>
378
364
 
379
365
  <motion.div initial={{ opacity: 0, y: 20 }} animate={typewriterDone ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }} transition={{ duration: 0.7, delay: 0.15, ease: [0.25, 0.4, 0.25, 1] }} className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
380
- <Button size="lg" onClick={() => { markLandingSeen(); navigate('/home') }} className="min-w-[180px] group">
366
+ <Button size="lg" onClick={() => navigate('/home')} className="min-w-[180px] group">
381
367
  Get Started
382
368
  <ArrowRight className="w-4 h-4 ml-2 transition-transform group-hover:translate-x-1" />
383
369
  </Button>
@@ -509,7 +495,7 @@ function CTASection() {
509
495
  <h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-foreground leading-tight tracking-[-0.02em]">Ready to get started?</h2>
510
496
  <p className="mt-5 text-muted-foreground text-lg max-w-md mx-auto leading-relaxed">Jump in and start exploring. No setup required.</p>
511
497
  <div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
512
- <Button size="lg" onClick={() => { markLandingSeen(); navigate('/home') }} className="min-w-[200px] group">
498
+ <Button size="lg" onClick={() => navigate('/home')} className="min-w-[200px] group">
513
499
  Launch App
514
500
  <ArrowRight className="w-4 h-4 ml-2 transition-transform group-hover:translate-x-1" />
515
501
  </Button>
@@ -3,7 +3,7 @@
3
3
  "name": "Leaderboard",
4
4
  "category": "data",
5
5
  "description": "Score-based leaderboard with categories, rankings, and admin editing",
6
- "details": "Ranked leaderboard table with score submission, category filtering, medal badges for top 3, and admin score editing. Uses useQuery with ordering for real-time rankings and useMutations for CRUD.",
6
+ "details": "Ranked leaderboard table with score submission, category labels, medal badges for the top 3, and admin score editing. Uses useQuery with ordering for real-time rankings and confirmed mutations for persisted CRUD feedback.",
7
7
  "files": [
8
8
  {
9
9
  "src": "src/LeaderboardPage.tsx",
@@ -12,10 +12,6 @@
12
12
  {
13
13
  "src": "src/leaderboard-schema.ts",
14
14
  "dest": "src/schemas/leaderboard-schema.ts"
15
- },
16
- {
17
- "src": "src/leaderboard-constants.ts",
18
- "dest": "src/components/leaderboard/leaderboard-constants.ts"
19
15
  }
20
16
  ],
21
17
  "schema": {