deepspace 0.3.10 → 0.4.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.
Files changed (120) hide show
  1. package/dist/cli.js +331 -115
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +199 -16
  4. package/dist/index.js +354 -166
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +242 -73
  7. package/dist/server.js +985 -374
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +322 -73
  10. package/dist/worker.js +961 -343
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. package/scripts/add-feature.cjs +654 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Landing Section: Video Embed
3
+ *
4
+ * Click-to-play video section with a glassmorphic container.
5
+ */
6
+
7
+ import { useState } from 'react'
8
+ import { Play } from 'lucide-react'
9
+ import { ScrollReveal, PlaceholderImage, SectionHeading, cn, motion } from '../primitives'
10
+
11
+ const VIDEO_URL = ''
12
+ const VIDEO_THUMBNAIL = ''
13
+ const SECTION_TITLE = 'See it in action'
14
+ const SECTION_SUBTITLE = 'Watch a quick walkthrough of how everything works.'
15
+
16
+ export default function VideoSection() {
17
+ const [isPlaying, setIsPlaying] = useState(false)
18
+
19
+ return (
20
+ <section className="relative py-28 md:py-36">
21
+ <div className="max-w-4xl mx-auto px-6">
22
+ <SectionHeading tag="Demo" title={SECTION_TITLE} subtitle={SECTION_SUBTITLE} />
23
+
24
+ <ScrollReveal>
25
+ <div className="relative rounded-2xl overflow-hidden border border-foreground/[0.1] shadow-2xl landing-gradient-border">
26
+ {isPlaying && VIDEO_URL ? (
27
+ <div className="aspect-video">
28
+ <iframe
29
+ src={VIDEO_URL}
30
+ className="w-full h-full"
31
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
32
+ allowFullScreen
33
+ title="Demo video"
34
+ />
35
+ </div>
36
+ ) : (
37
+ <button
38
+ onClick={() => { if (VIDEO_URL) setIsPlaying(true) }}
39
+ className="relative w-full group"
40
+ aria-label="Play video"
41
+ >
42
+ {VIDEO_THUMBNAIL ? (
43
+ <div className="aspect-video">
44
+ <img src={VIDEO_THUMBNAIL} alt="Video preview" className="w-full h-full object-cover" />
45
+ <div className="absolute inset-0 bg-background/50 group-hover:bg-background/30 transition-colors duration-300" />
46
+ </div>
47
+ ) : (
48
+ <PlaceholderImage label="Video Preview" aspectRatio="aspect-video" className="rounded-none border-0" />
49
+ )}
50
+
51
+ <div className="absolute inset-0 flex items-center justify-center">
52
+ <motion.div
53
+ className={cn(
54
+ 'w-20 h-20 rounded-full flex items-center justify-center',
55
+ 'bg-foreground/[0.1] backdrop-blur-sm border border-foreground/[0.2]',
56
+ 'group-hover:bg-foreground/[0.2] group-hover:scale-110',
57
+ 'transition-all duration-300',
58
+ )}
59
+ whileHover={{ scale: 1.1 }}
60
+ >
61
+ <Play className="w-8 h-8 text-foreground ml-1" />
62
+ </motion.div>
63
+ </div>
64
+ </button>
65
+ )}
66
+ </div>
67
+ </ScrollReveal>
68
+ </div>
69
+ </section>
70
+ )
71
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "id": "leaderboard",
3
+ "name": "Leaderboard",
4
+ "category": "data",
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.",
7
+ "files": [
8
+ {
9
+ "src": "src/LeaderboardPage.tsx",
10
+ "dest": "src/pages/leaderboard.tsx"
11
+ },
12
+ {
13
+ "src": "src/leaderboard-schema.ts",
14
+ "dest": "src/schemas/leaderboard-schema.ts"
15
+ },
16
+ {
17
+ "src": "src/leaderboard-constants.ts",
18
+ "dest": "src/components/leaderboard/leaderboard-constants.ts"
19
+ }
20
+ ],
21
+ "schema": {
22
+ "exportName": "leaderboardSchema",
23
+ "importPath": "./schemas/leaderboard-schema",
24
+ "spreadOperator": false
25
+ },
26
+ "route": {
27
+ "path": "/leaderboard",
28
+ "protected": true
29
+ },
30
+ "requires": [],
31
+ "instructions": [],
32
+ "patterns": []
33
+ }
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Leaderboard Page
3
+ *
4
+ * Demonstrates:
5
+ * - useQuery for fetching records with ordering
6
+ * - useMutations for CRUD operations
7
+ * - useUser for current user
8
+ * - Role-based UI (admin can edit anyone's score)
9
+ */
10
+
11
+ import { useState } from 'react'
12
+ import { useUser } from 'deepspace'
13
+ import { useQuery } from 'deepspace'
14
+ import { useMutations } from 'deepspace'
15
+ import { Button, Modal, EmptyState, Badge } from 'deepspace'
16
+ import { ROLES, type Role } from 'deepspace'
17
+ import { LEADERBOARD_CATEGORY, CATEGORY_CONFIG, type LeaderboardCategory } from '../components/leaderboard/leaderboard-constants'
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ interface LeaderboardEntry {
24
+ playerName: string
25
+ score: number
26
+ category: string
27
+ playerId: string
28
+ }
29
+
30
+ interface LeaderboardPageProps {
31
+ className?: string
32
+ }
33
+
34
+ // ============================================================================
35
+ // Submit Score Modal
36
+ // ============================================================================
37
+
38
+ interface SubmitScoreModalProps {
39
+ isOpen: boolean
40
+ onClose: () => void
41
+ onSubmit: (playerName: string, score: number, category: string) => void
42
+ userName: string
43
+ }
44
+
45
+ function SubmitScoreModal({ isOpen, onClose, onSubmit, userName }: SubmitScoreModalProps) {
46
+ const [playerName, setPlayerName] = useState(userName)
47
+ const [score, setScore] = useState('')
48
+ const [category, setCategory] = useState<string>(LEADERBOARD_CATEGORY.GENERAL)
49
+
50
+ const handleSubmit = () => {
51
+ const parsed = Number(score)
52
+ if (playerName.trim() && !isNaN(parsed)) {
53
+ onSubmit(playerName.trim(), parsed, category)
54
+ setPlayerName(userName)
55
+ setScore('')
56
+ setCategory(LEADERBOARD_CATEGORY.GENERAL)
57
+ onClose()
58
+ }
59
+ }
60
+
61
+ return (
62
+ <Modal open={isOpen} onClose={onClose} size="sm">
63
+ <Modal.Header onClose={onClose}>
64
+ <Modal.Title>Submit Score</Modal.Title>
65
+ </Modal.Header>
66
+ <Modal.Body>
67
+ <div className="space-y-4">
68
+ <div>
69
+ <label className="block text-sm font-medium text-muted-foreground mb-2">Player Name</label>
70
+ <input
71
+ type="text"
72
+ value={playerName}
73
+ onChange={(e) => setPlayerName(e.target.value)}
74
+ placeholder="Your name"
75
+ 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"
76
+ autoFocus
77
+ />
78
+ </div>
79
+ <div>
80
+ <label className="block text-sm font-medium text-muted-foreground mb-2">Score</label>
81
+ <input
82
+ type="number"
83
+ value={score}
84
+ onChange={(e) => setScore(e.target.value)}
85
+ placeholder="Enter your score"
86
+ 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"
87
+ />
88
+ </div>
89
+ <div>
90
+ <label className="block text-sm font-medium text-muted-foreground mb-2">Category</label>
91
+ <select
92
+ value={category}
93
+ onChange={(e) => setCategory(e.target.value)}
94
+ className="w-full px-3 py-2 bg-transparent border border-border rounded-lg text-foreground focus:outline-none focus:ring-ring"
95
+ >
96
+ {Object.values(LEADERBOARD_CATEGORY).map(cat => (
97
+ <option key={cat} value={cat}>{CATEGORY_CONFIG[cat].title}</option>
98
+ ))}
99
+ </select>
100
+ </div>
101
+ </div>
102
+ </Modal.Body>
103
+ <Modal.Footer>
104
+ <Button variant="secondary" onClick={onClose}>Cancel</Button>
105
+ <Button onClick={handleSubmit} disabled={!playerName.trim() || !score}>Submit</Button>
106
+ </Modal.Footer>
107
+ </Modal>
108
+ )
109
+ }
110
+
111
+ // ============================================================================
112
+ // Edit Score Modal (Admin)
113
+ // ============================================================================
114
+
115
+ interface EditScoreModalProps {
116
+ isOpen: boolean
117
+ onClose: () => void
118
+ onSave: (score: number) => void
119
+ entry: { recordId: string; data: LeaderboardEntry } | null
120
+ }
121
+
122
+ function EditScoreModal({ isOpen, onClose, onSave, entry }: EditScoreModalProps) {
123
+ const [score, setScore] = useState('')
124
+
125
+ const handleOpen = () => {
126
+ if (entry) setScore(String(entry.data.score))
127
+ }
128
+
129
+ const handleSave = () => {
130
+ const parsed = Number(score)
131
+ if (!isNaN(parsed)) {
132
+ onSave(parsed)
133
+ onClose()
134
+ }
135
+ }
136
+
137
+ // Reset score when entry changes
138
+ if (isOpen && entry && score === '') handleOpen()
139
+
140
+ return (
141
+ <Modal open={isOpen} onClose={onClose} size="sm">
142
+ <Modal.Header onClose={onClose}>
143
+ <Modal.Title>Edit Score - {entry?.data.playerName}</Modal.Title>
144
+ </Modal.Header>
145
+ <Modal.Body>
146
+ <div>
147
+ <label className="block text-sm font-medium text-muted-foreground mb-2">Score</label>
148
+ <input
149
+ type="number"
150
+ value={score}
151
+ onChange={(e) => setScore(e.target.value)}
152
+ 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"
153
+ autoFocus
154
+ />
155
+ </div>
156
+ </Modal.Body>
157
+ <Modal.Footer>
158
+ <Button variant="secondary" onClick={onClose}>Cancel</Button>
159
+ <Button onClick={handleSave} disabled={!score}>Save</Button>
160
+ </Modal.Footer>
161
+ </Modal>
162
+ )
163
+ }
164
+
165
+ // ============================================================================
166
+ // Rank Badge Component
167
+ // ============================================================================
168
+
169
+ function RankBadge({ rank }: { rank: number }) {
170
+ if (rank === 1) {
171
+ return <span className="text-lg" title="1st place">&#x1F947;</span>
172
+ }
173
+ if (rank === 2) {
174
+ return <span className="text-lg" title="2nd place">&#x1F948;</span>
175
+ }
176
+ if (rank === 3) {
177
+ return <span className="text-lg" title="3rd place">&#x1F949;</span>
178
+ }
179
+ return <span className="text-sm font-medium text-muted-foreground">#{rank}</span>
180
+ }
181
+
182
+ // ============================================================================
183
+ // Main Page
184
+ // ============================================================================
185
+
186
+ export default function LeaderboardPage({ className }: LeaderboardPageProps) {
187
+ const { user } = useUser()
188
+ const userRole = (user?.role ?? ROLES.VIEWER) as Role
189
+ const isAdmin = userRole === ROLES.ADMIN
190
+ const canSubmit = userRole === ROLES.MEMBER || userRole === ROLES.ADMIN
191
+
192
+ const [showSubmitModal, setShowSubmitModal] = useState(false)
193
+ const [editingEntry, setEditingEntry] = useState<{ recordId: string; data: LeaderboardEntry } | null>(null)
194
+
195
+ // Query all leaderboard entries, sorted by score descending
196
+ const { records: entries, status } = useQuery<LeaderboardEntry>('leaderboard', {
197
+ orderBy: 'score',
198
+ orderDir: 'desc',
199
+ })
200
+
201
+ // Mutations for leaderboard
202
+ const { create, put, remove } = useMutations<LeaderboardEntry>('leaderboard')
203
+
204
+ const handleSubmit = async (playerName: string, score: number, category: string) => {
205
+ await create({
206
+ playerName,
207
+ score,
208
+ category,
209
+ playerId: user!.id,
210
+ })
211
+ }
212
+
213
+ const handleEditSave = async (newScore: number) => {
214
+ if (editingEntry) {
215
+ await put(editingEntry.recordId, { ...editingEntry.data, score: newScore })
216
+ setEditingEntry(null)
217
+ }
218
+ }
219
+
220
+ const handleDelete = async (recordId: string) => {
221
+ if (confirm('Are you sure you want to delete this entry?')) {
222
+ await remove(recordId)
223
+ }
224
+ }
225
+
226
+ const isLoading = status === 'loading'
227
+
228
+ return (
229
+ <div className={`h-full bg-background overflow-y-auto ${className ?? ''}`}>
230
+ {/* Header */}
231
+ <div className="bg-card/60 backdrop-blur-md border-b border-border sticky top-0 z-10">
232
+ <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
233
+ <div className="flex items-center justify-between">
234
+ <div>
235
+ <h1 className="text-2xl font-bold text-foreground">Leaderboard</h1>
236
+ <p className="text-muted-foreground mt-1">
237
+ {canSubmit
238
+ ? 'Submit your score and compete for the top spot'
239
+ : 'View the rankings below'
240
+ }
241
+ </p>
242
+ </div>
243
+
244
+ {canSubmit && (
245
+ <Button onClick={() => setShowSubmitModal(true)}>
246
+ <svg className="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
247
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
248
+ </svg>
249
+ Submit Score
250
+ </Button>
251
+ )}
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ {/* Content */}
257
+ <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
258
+ {isLoading ? (
259
+ <div className="flex items-center justify-center py-16">
260
+ <div className="w-8 h-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
261
+ </div>
262
+ ) : entries.length === 0 ? (
263
+ <EmptyState
264
+ title="No scores yet"
265
+ description={canSubmit
266
+ ? "Be the first to submit a score!"
267
+ : "No scores have been submitted yet"
268
+ }
269
+ icon={
270
+ <svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
271
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
272
+ </svg>
273
+ }
274
+ />
275
+ ) : (
276
+ <div className="bg-card/60 rounded-xl border border-border overflow-hidden">
277
+ <table className="w-full">
278
+ <thead>
279
+ <tr className="border-b border-border">
280
+ <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider w-16">Rank</th>
281
+ <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Player</th>
282
+ <th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Category</th>
283
+ <th className="px-4 py-3 text-right text-xs font-semibold text-muted-foreground uppercase tracking-wider">Score</th>
284
+ {(canSubmit || isAdmin) && (
285
+ <th className="px-4 py-3 text-right text-xs font-semibold text-muted-foreground uppercase tracking-wider w-24">Actions</th>
286
+ )}
287
+ </tr>
288
+ </thead>
289
+ <tbody className="divide-y divide-border/50">
290
+ {entries.map((entry, index) => {
291
+ const rank = index + 1
292
+ const isOwn = entry.data.playerId === user?.id
293
+ const cat = entry.data.category as LeaderboardCategory
294
+ const catConfig = CATEGORY_CONFIG[cat] ?? CATEGORY_CONFIG[LEADERBOARD_CATEGORY.GENERAL]
295
+
296
+ return (
297
+ <tr
298
+ key={entry.recordId}
299
+ className={`transition-colors ${isOwn ? 'bg-primary/20/10' : 'hover:bg-muted/30'}`}
300
+ >
301
+ <td className="px-4 py-3">
302
+ <RankBadge rank={rank} />
303
+ </td>
304
+ <td className="px-4 py-3">
305
+ <span className={`font-medium ${isOwn ? 'text-primary' : 'text-foreground'}`}>
306
+ {entry.data.playerName}
307
+ </span>
308
+ {isOwn && (
309
+ <span className="ml-2 text-xs text-primary">(you)</span>
310
+ )}
311
+ </td>
312
+ <td className="px-4 py-3">
313
+ <Badge variant={catConfig.color} size="sm">{catConfig.title}</Badge>
314
+ </td>
315
+ <td className="px-4 py-3 text-right font-mono font-semibold text-foreground">
316
+ {entry.data.score.toLocaleString()}
317
+ </td>
318
+ {(canSubmit || isAdmin) && (
319
+ <td className="px-4 py-3 text-right">
320
+ <div className="flex items-center justify-end gap-1">
321
+ {(isOwn || isAdmin) && (
322
+ <button
323
+ onClick={() => setEditingEntry(entry)}
324
+ className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-lg transition-colors"
325
+ title="Edit score"
326
+ >
327
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
328
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
329
+ </svg>
330
+ </button>
331
+ )}
332
+ {(isOwn || isAdmin) && (
333
+ <button
334
+ onClick={() => handleDelete(entry.recordId)}
335
+ className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/20 rounded-lg transition-colors"
336
+ title="Delete"
337
+ >
338
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
339
+ <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" />
340
+ </svg>
341
+ </button>
342
+ )}
343
+ </div>
344
+ </td>
345
+ )}
346
+ </tr>
347
+ )
348
+ })}
349
+ </tbody>
350
+ </table>
351
+ </div>
352
+ )}
353
+ </div>
354
+
355
+ {/* Submit Modal */}
356
+ <SubmitScoreModal
357
+ isOpen={showSubmitModal}
358
+ onClose={() => setShowSubmitModal(false)}
359
+ onSubmit={handleSubmit}
360
+ userName={user?.name ?? ''}
361
+ />
362
+
363
+ {/* Edit Modal (admin or own) */}
364
+ <EditScoreModal
365
+ isOpen={!!editingEntry}
366
+ onClose={() => setEditingEntry(null)}
367
+ onSave={handleEditSave}
368
+ entry={editingEntry}
369
+ />
370
+ </div>
371
+ )
372
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Leaderboard Feature - Constants
3
+ */
4
+
5
+ import type { BadgeProps } from 'deepspace'
6
+
7
+ export type BadgeVariant = BadgeProps['variant']
8
+
9
+ export const LEADERBOARD_CATEGORY = {
10
+ GENERAL: 'general',
11
+ SPEED: 'speed',
12
+ ACCURACY: 'accuracy',
13
+ } as const
14
+
15
+ export type LeaderboardCategory = typeof LEADERBOARD_CATEGORY[keyof typeof LEADERBOARD_CATEGORY]
16
+
17
+ export const CATEGORY_CONFIG: Record<LeaderboardCategory, { title: string; color: BadgeVariant }> = {
18
+ [LEADERBOARD_CATEGORY.GENERAL]: { title: 'General', color: 'info' },
19
+ [LEADERBOARD_CATEGORY.SPEED]: { title: 'Speed', color: 'warning' },
20
+ [LEADERBOARD_CATEGORY.ACCURACY]: { title: 'Accuracy', color: 'success' },
21
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Leaderboard Feature - Schema
3
+ *
4
+ * A score-based leaderboard demonstrating:
5
+ * - ownerField for 'own' permission checks
6
+ * - userBound fields that auto-populate with current user ID
7
+ * - Admin override for updating any entry
8
+ */
9
+
10
+ import type { CollectionSchema } from 'deepspace/worker'
11
+
12
+ export const leaderboardSchema: CollectionSchema = {
13
+ name: 'leaderboard',
14
+ columns: [
15
+ { name: 'playerName', storage: 'text', interpretation: 'plain', required: true },
16
+ { name: 'score', storage: 'number', interpretation: 'plain', required: true },
17
+ { name: 'category', storage: 'text', interpretation: 'plain', default: 'general' },
18
+ { name: 'playerId', storage: 'text', interpretation: 'plain', required: true, userBound: true, immutable: true },
19
+ ],
20
+ ownerField: 'playerId',
21
+ permissions: {
22
+ viewer: {
23
+ read: true,
24
+ create: false,
25
+ update: false,
26
+ delete: false,
27
+ },
28
+ member: {
29
+ read: true,
30
+ create: true,
31
+ update: 'own',
32
+ delete: 'own',
33
+ },
34
+ admin: { read: true, create: true, update: true, delete: true },
35
+ },
36
+ }
@@ -0,0 +1,111 @@
1
+ {
2
+ "id": "messaging",
3
+ "name": "Messaging",
4
+ "category": "data",
5
+ "description": "Real-time chat with channels, threads, reactions, and multi-channel support",
6
+ "details": "Complete messaging system with single-channel chat (ChatPage) and multi-channel Slack-like interface (ChatMultiPage). Includes message threads, reactions, read receipts, user profile popovers, channel management (create, browse, settings), DMs, invitations, and member management. Uses SDK messaging hooks (useChannels, useMessages, useReadReceipts).",
7
+ "files": [
8
+ {
9
+ "src": "src/messaging-schema.ts",
10
+ "dest": "src/schemas/messaging-schema.ts"
11
+ },
12
+ {
13
+ "src": "src/ChatPage.tsx",
14
+ "dest": "src/pages/chat.tsx"
15
+ },
16
+ {
17
+ "src": "src/ChatMultiPage.tsx",
18
+ "dest": "src/pages/chat-multi.tsx"
19
+ },
20
+ {
21
+ "src": "src/hooks/useChatChannel.ts",
22
+ "dest": "src/components/messaging/hooks/useChatChannel.ts"
23
+ },
24
+ {
25
+ "src": "src/hooks/useLongPress.ts",
26
+ "dest": "src/components/messaging/hooks/useLongPress.ts"
27
+ },
28
+ {
29
+ "src": "src/hooks/useMultiChannel.ts",
30
+ "dest": "src/components/messaging/hooks/useMultiChannel.ts"
31
+ },
32
+ {
33
+ "src": "src/components/chat/ChatHeader.tsx",
34
+ "dest": "src/components/messaging/chat/ChatHeader.tsx"
35
+ },
36
+ {
37
+ "src": "src/components/chat/MessageList.tsx",
38
+ "dest": "src/components/messaging/chat/MessageList.tsx"
39
+ },
40
+ {
41
+ "src": "src/components/chat/MessageInput.tsx",
42
+ "dest": "src/components/messaging/chat/MessageInput.tsx"
43
+ },
44
+ {
45
+ "src": "src/components/chat/MessageItem.tsx",
46
+ "dest": "src/components/messaging/chat/MessageItem.tsx"
47
+ },
48
+ {
49
+ "src": "src/components/chat/ThreadPanel.tsx",
50
+ "dest": "src/components/messaging/chat/ThreadPanel.tsx"
51
+ },
52
+ {
53
+ "src": "src/components/chat/UserProfilePopover.tsx",
54
+ "dest": "src/components/messaging/chat/UserProfilePopover.tsx"
55
+ },
56
+ {
57
+ "src": "src/components/chat/MessageActionSheet.tsx",
58
+ "dest": "src/components/messaging/chat/MessageActionSheet.tsx"
59
+ },
60
+ {
61
+ "src": "src/components/chat/ReactionPicker.tsx",
62
+ "dest": "src/components/messaging/chat/ReactionPicker.tsx"
63
+ },
64
+ {
65
+ "src": "src/components/chat-multi/ChannelSidebar.tsx",
66
+ "dest": "src/components/messaging/chat-multi/ChannelSidebar.tsx"
67
+ },
68
+ {
69
+ "src": "src/components/chat-multi/ChannelListItem.tsx",
70
+ "dest": "src/components/messaging/chat-multi/ChannelListItem.tsx"
71
+ },
72
+ {
73
+ "src": "src/components/chat-multi/CreateGroupModal.tsx",
74
+ "dest": "src/components/messaging/chat-multi/CreateGroupModal.tsx"
75
+ },
76
+ {
77
+ "src": "src/components/chat-multi/NewMessageModal.tsx",
78
+ "dest": "src/components/messaging/chat-multi/NewMessageModal.tsx"
79
+ },
80
+ {
81
+ "src": "src/components/chat-multi/BrowseGroupsModal.tsx",
82
+ "dest": "src/components/messaging/chat-multi/BrowseGroupsModal.tsx"
83
+ },
84
+ {
85
+ "src": "src/components/chat-multi/ChannelSettingsPanel.tsx",
86
+ "dest": "src/components/messaging/chat-multi/ChannelSettingsPanel.tsx"
87
+ },
88
+ {
89
+ "src": "src/components/chat-multi/AddMemberModal.tsx",
90
+ "dest": "src/components/messaging/chat-multi/AddMemberModal.tsx"
91
+ },
92
+ {
93
+ "src": "src/components/chat-multi/InvitationList.tsx",
94
+ "dest": "src/components/messaging/chat-multi/InvitationList.tsx"
95
+ }
96
+ ],
97
+ "schema": {
98
+ "exportName": "messagingSchemas",
99
+ "importPath": "./schemas/messaging-schema",
100
+ "spreadOperator": true
101
+ },
102
+ "route": {
103
+ "path": "/chat",
104
+ "protected": true
105
+ },
106
+ "requires": [],
107
+ "instructions": [
108
+ "Optionally add a second route for /chat-multi -> ChatMultiPage"
109
+ ],
110
+ "patterns": []
111
+ }