@nextsparkjs/theme-productivity 0.1.0-beta.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 (57) hide show
  1. package/README.md +76 -0
  2. package/about.md +123 -0
  3. package/components/CardDetailModal.tsx +318 -0
  4. package/components/KanbanBoard.tsx +612 -0
  5. package/components/KanbanCard.tsx +218 -0
  6. package/components/KanbanColumn.tsx +264 -0
  7. package/components/SortableList.tsx +46 -0
  8. package/components/index.ts +4 -0
  9. package/config/app.config.ts +172 -0
  10. package/config/billing.config.ts +187 -0
  11. package/config/dashboard.config.ts +357 -0
  12. package/config/dev.config.ts +55 -0
  13. package/config/features.config.ts +256 -0
  14. package/config/flows.config.ts +484 -0
  15. package/config/permissions.config.ts +167 -0
  16. package/config/theme.config.ts +106 -0
  17. package/entities/boards/boards.config.ts +61 -0
  18. package/entities/boards/boards.fields.ts +154 -0
  19. package/entities/boards/boards.service.ts +256 -0
  20. package/entities/boards/boards.types.ts +57 -0
  21. package/entities/boards/messages/en.json +80 -0
  22. package/entities/boards/messages/es.json +80 -0
  23. package/entities/boards/migrations/001_boards_table.sql +83 -0
  24. package/entities/cards/cards.config.ts +61 -0
  25. package/entities/cards/cards.fields.ts +242 -0
  26. package/entities/cards/cards.service.ts +336 -0
  27. package/entities/cards/cards.types.ts +79 -0
  28. package/entities/cards/messages/en.json +114 -0
  29. package/entities/cards/messages/es.json +114 -0
  30. package/entities/cards/migrations/020_cards_table.sql +92 -0
  31. package/entities/lists/lists.config.ts +61 -0
  32. package/entities/lists/lists.fields.ts +105 -0
  33. package/entities/lists/lists.service.ts +252 -0
  34. package/entities/lists/lists.types.ts +55 -0
  35. package/entities/lists/messages/en.json +60 -0
  36. package/entities/lists/messages/es.json +60 -0
  37. package/entities/lists/migrations/010_lists_table.sql +79 -0
  38. package/lib/selectors.ts +206 -0
  39. package/messages/en.json +79 -0
  40. package/messages/es.json +79 -0
  41. package/migrations/999_theme_sample_data.sql +922 -0
  42. package/migrations/999a_initial_sample_data.sql +377 -0
  43. package/migrations/999b_abundant_sample_data.sql +346 -0
  44. package/package.json +17 -0
  45. package/permissions-matrix.md +122 -0
  46. package/styles/components.css +460 -0
  47. package/styles/globals.css +560 -0
  48. package/templates/dashboard/(main)/boards/[id]/[cardId]/page.tsx +238 -0
  49. package/templates/dashboard/(main)/boards/[id]/edit/page.tsx +390 -0
  50. package/templates/dashboard/(main)/boards/[id]/page.tsx +236 -0
  51. package/templates/dashboard/(main)/boards/create/page.tsx +236 -0
  52. package/templates/dashboard/(main)/boards/page.tsx +335 -0
  53. package/templates/dashboard/(main)/layout.tsx +32 -0
  54. package/templates/dashboard/(main)/page.tsx +592 -0
  55. package/templates/shared/ProductivityMobileNav.tsx +410 -0
  56. package/templates/shared/ProductivitySidebar.tsx +538 -0
  57. package/templates/shared/ProductivityTopBar.tsx +317 -0
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Productivity Theme
2
+
3
+ ## Activación
4
+
5
+ Para usar este theme, configura en `.env`:
6
+
7
+ ```bash
8
+ NEXT_PUBLIC_ACTIVE_THEME=productivity
9
+ ```
10
+
11
+ Luego regenera el registry y reinicia el servidor:
12
+
13
+ ```bash
14
+ npx tsx scripts/build-registry.mjs --build
15
+ pnpm dev
16
+ ```
17
+
18
+ ## Funcionalidades
19
+
20
+ ### Kanban Board
21
+
22
+ Vista de tablero estilo Trello con:
23
+
24
+ - **Boards**: Tableros de proyectos
25
+ - **Lists**: Columnas dentro de cada tablero
26
+ - **Cards**: Tarjetas/tareas arrastrables
27
+
28
+ ### Drag and Drop
29
+
30
+ Implementado con `@dnd-kit`:
31
+ - Arrastrar cards entre lists
32
+ - Reordenar cards dentro de una list
33
+ - Visual feedback durante el drag
34
+
35
+ ### Componentes
36
+
37
+ ```
38
+ components/
39
+ ├── KanbanBoard.tsx # Tablero completo con DnD context
40
+ ├── KanbanColumn.tsx # Columna con drop zone
41
+ ├── KanbanCard.tsx # Tarjeta draggable
42
+ └── index.ts # Exports
43
+ ```
44
+
45
+ ### Templates Override
46
+
47
+ ```
48
+ templates/
49
+ └── app/dashboard/(main)/boards/
50
+ ├── page.tsx # Grid de boards
51
+ └── [id]/
52
+ └── page.tsx # Vista Kanban del board
53
+ ```
54
+
55
+ ## Permisos
56
+
57
+ | Entidad | create | read | update | delete | archive |
58
+ |---------|:------:|:----:|:------:|:------:|:-------:|
59
+ | boards | owner, admin | todos | owner, admin | owner | owner, admin |
60
+ | lists | owner, admin | todos | owner, admin | owner, admin | - |
61
+ | cards | todos | todos | todos | owner, admin | - |
62
+
63
+ ## Usuarios de Prueba
64
+
65
+ | Email | Password | Rol en Product Team |
66
+ |-------|----------|---------------------|
67
+ | pm.torres@nextspark.dev | Testing1234 | owner |
68
+ | dev.luna@nextspark.dev | Testing1234 | admin |
69
+ | design.rios@nextspark.dev | Testing1234 | member |
70
+
71
+ ## Modo
72
+
73
+ - **Teams Mode**: `collaborative`
74
+ - Team switcher habilitado
75
+ - Puede crear equipos de trabajo
76
+
package/about.md ADDED
@@ -0,0 +1,123 @@
1
+ # Productivity Theme
2
+
3
+ ## Objetivo
4
+
5
+ Demostrar el modo `multi-tenant` donde usuarios pueden gestionar múltiples workspaces con diferentes roles en cada team, similar a Trello o Notion.
6
+
7
+ ## Producto
8
+
9
+ **TaskFlow** - Aplicación de gestión de tareas estilo Trello para equipos pequeños y medianos.
10
+
11
+ ## Empresa
12
+
13
+ **FlowWorks Labs** - Empresa de software de productividad que cree en la colaboración flexible y la organización visual del trabajo.
14
+
15
+ ## Teams Mode
16
+
17
+ ```
18
+ multi-tenant
19
+ ```
20
+
21
+ - Múltiples work teams (workspaces)
22
+ - Team switcher habilitado
23
+ - Puede crear nuevos teams
24
+ - Invitaciones habilitadas (owner/admin pueden invitar)
25
+
26
+ ## Entidades
27
+
28
+ | Entidad | Descripción |
29
+ |---------|-------------|
30
+ | boards | Tableros de proyectos con nombre, descripción, color y estado |
31
+ | lists | Columnas dentro de tableros para organizar tarjetas |
32
+ | cards | Tareas individuales con título, descripción, prioridad y fechas |
33
+
34
+ ## Features
35
+
36
+ | Feature | Descripción | Roles |
37
+ |---------|-------------|-------|
38
+ | boards.archive | Archivar tableros completados | owner, admin |
39
+ | cards.move | Mover tarjetas entre listas | owner, admin, member |
40
+
41
+ ## Permisos
42
+
43
+ ### boards
44
+
45
+ | Action | owner | admin | member |
46
+ |--------|:-----:|:-----:|:------:|
47
+ | create | ✅ | ✅ | ❌ |
48
+ | read | ✅ | ✅ | ✅ |
49
+ | update | ✅ | ✅ | ❌ |
50
+ | delete | ✅ | ❌ | ❌ |
51
+ | archive | ✅ | ✅ | ❌ |
52
+
53
+ ### lists
54
+
55
+ | Action | owner | admin | member |
56
+ |--------|:-----:|:-----:|:------:|
57
+ | create | ✅ | ✅ | ❌ |
58
+ | read | ✅ | ✅ | ✅ |
59
+ | update | ✅ | ✅ | ❌ |
60
+ | delete | ✅ | ✅ | ❌ |
61
+
62
+ ### cards
63
+
64
+ | Action | owner | admin | member |
65
+ |--------|:-----:|:-----:|:------:|
66
+ | create | ✅ | ✅ | ✅ |
67
+ | read | ✅ | ✅ | ✅ |
68
+ | update | ✅ | ✅ | ✅ |
69
+ | delete | ✅ | ✅ | ❌ |
70
+ | move | ✅ | ✅ | ✅ |
71
+
72
+ ## Usuarios de Prueba
73
+
74
+ | Email | Password | Product Team | Marketing Hub |
75
+ |-------|----------|--------------|---------------|
76
+ | prod_owner_patricia@nextspark.dev | Test1234 | owner | owner |
77
+ | prod_admin_member_lucas@nextspark.dev | Test1234 | admin | member |
78
+ | prod_member_diana@nextspark.dev | Test1234 | member | - |
79
+ | prod_member_marcos@nextspark.dev | Test1234 | - | member |
80
+
81
+ ## Billing
82
+
83
+ ### Modelo de Pricing: Suscripción por Team (Flat Rate)
84
+
85
+ > **Los planes y facturas siempre están asociados al team. El precio es fijo independiente del número de miembros (hasta el límite del plan).**
86
+
87
+ ### Planes Disponibles
88
+
89
+ | Plan | Mensual | Anual | Descuento |
90
+ |------|---------|-------|-----------|
91
+ | Free | $0 | $0 | - |
92
+ | Team | $12/mes | $115/año | ~20% off |
93
+ | Business | $24/mes | $230/año | ~20% off |
94
+
95
+ ### Características por Plan
96
+
97
+ | Feature | Free | Team | Business |
98
+ |---------|:----:|:----:|:--------:|
99
+ | Boards | 1 | Ilimitados | Ilimitados |
100
+ | Miembros | 3 | 10 | Ilimitados |
101
+ | Listas por board | 5 | Ilimitadas | Ilimitadas |
102
+ | Archivos adjuntos | ❌ | 10MB/archivo | 100MB/archivo |
103
+ | Integraciones | ❌ | ❌ | ✅ |
104
+ | Admin controls | ❌ | ❌ | ✅ |
105
+
106
+ ### Facturación
107
+
108
+ - **Unidad de cobro:** Por team (flat rate, no per-seat)
109
+ - **Ciclos:** Mensual o anual (20% descuento)
110
+
111
+ ### Sample Invoices
112
+
113
+ | Team | Plan | Invoices | Status | Total |
114
+ |------|------|----------|--------|-------|
115
+ | Product Team | Team | 6 | 5 paid + 1 pending | $72 |
116
+ | Marketing Hub | Team | 6 | 5 paid + 1 pending | $72 |
117
+
118
+ ## Casos de Uso
119
+
120
+ 1. Equipo de desarrollo gestionando sprints
121
+ 2. Agencia creativa organizando proyectos
122
+ 3. Startup coordinando tareas entre departamentos
123
+
@@ -0,0 +1,318 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from '@nextsparkjs/core/components/ui/dialog'
10
+ import { Button } from '@nextsparkjs/core/components/ui/button'
11
+ import { Input } from '@nextsparkjs/core/components/ui/input'
12
+ import { Textarea } from '@nextsparkjs/core/components/ui/textarea'
13
+ import { Label } from '@nextsparkjs/core/components/ui/label'
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from '@nextsparkjs/core/components/ui/select'
21
+ import { Calendar } from '@nextsparkjs/core/components/ui/calendar'
22
+ import {
23
+ Popover,
24
+ PopoverContent,
25
+ PopoverTrigger,
26
+ } from '@nextsparkjs/core/components/ui/popover'
27
+ import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
28
+ import { Calendar as CalendarIcon, Trash2, AlertCircle } from 'lucide-react'
29
+ import { cn } from '@nextsparkjs/core/lib/utils'
30
+ import { format } from 'date-fns'
31
+ import type { CardData } from './KanbanCard'
32
+
33
+ interface CardDetailModalProps {
34
+ card: CardData | null
35
+ isOpen: boolean
36
+ onClose: () => void
37
+ onUpdate: (card: CardData) => Promise<void>
38
+ onDelete?: (cardId: string) => Promise<void>
39
+ }
40
+
41
+ const priorityOptions = [
42
+ { value: 'low', label: 'Low', color: 'bg-slate-100 text-slate-700' },
43
+ { value: 'medium', label: 'Medium', color: 'bg-blue-100 text-blue-700' },
44
+ { value: 'high', label: 'High', color: 'bg-orange-100 text-orange-700' },
45
+ { value: 'urgent', label: 'Urgent', color: 'bg-red-100 text-red-700' },
46
+ ]
47
+
48
+ export function CardDetailModal({
49
+ card,
50
+ isOpen,
51
+ onClose,
52
+ onUpdate,
53
+ onDelete,
54
+ }: CardDetailModalProps) {
55
+ const [title, setTitle] = useState('')
56
+ const [description, setDescription] = useState('')
57
+ const [priority, setPriority] = useState<string>('')
58
+ const [dueDate, setDueDate] = useState<Date | undefined>(undefined)
59
+ const [isSaving, setIsSaving] = useState(false)
60
+ const [isDeleting, setIsDeleting] = useState(false)
61
+ const [hasChanges, setHasChanges] = useState(false)
62
+
63
+ // Initialize form when card changes
64
+ useEffect(() => {
65
+ if (card) {
66
+ setTitle(card.title)
67
+ setDescription(card.description || '')
68
+ setPriority(card.priority || '')
69
+ setDueDate(card.dueDate ? new Date(card.dueDate) : undefined)
70
+ setHasChanges(false)
71
+ }
72
+ }, [card])
73
+
74
+ // Track changes
75
+ useEffect(() => {
76
+ if (!card) return
77
+ const changed =
78
+ title !== card.title ||
79
+ description !== (card.description || '') ||
80
+ priority !== (card.priority || '') ||
81
+ (dueDate?.toISOString().split('T')[0] || '') !== (card.dueDate || '')
82
+ setHasChanges(changed)
83
+ }, [card, title, description, priority, dueDate])
84
+
85
+ const handleSave = useCallback(async () => {
86
+ if (!card || !title.trim()) return
87
+
88
+ setIsSaving(true)
89
+ try {
90
+ await onUpdate({
91
+ ...card,
92
+ title: title.trim(),
93
+ description: description.trim() || null,
94
+ priority: (priority as CardData['priority']) || null,
95
+ dueDate: dueDate ? dueDate.toISOString().split('T')[0] : null,
96
+ })
97
+ onClose()
98
+ } catch (error) {
99
+ console.error('Error saving card:', error)
100
+ } finally {
101
+ setIsSaving(false)
102
+ }
103
+ }, [card, title, description, priority, dueDate, onUpdate, onClose])
104
+
105
+ const handleDelete = useCallback(async () => {
106
+ if (!card || !onDelete) return
107
+ if (!confirm('Are you sure you want to delete this card?')) return
108
+
109
+ setIsDeleting(true)
110
+ try {
111
+ await onDelete(card.id)
112
+ onClose()
113
+ } catch (error) {
114
+ console.error('Error deleting card:', error)
115
+ } finally {
116
+ setIsDeleting(false)
117
+ }
118
+ }, [card, onDelete, onClose])
119
+
120
+ // Handle keyboard shortcuts
121
+ useEffect(() => {
122
+ const handleKeyDown = (e: KeyboardEvent) => {
123
+ if (!isOpen) return
124
+
125
+ // Escape to close (if no changes)
126
+ if (e.key === 'Escape' && !hasChanges) {
127
+ onClose()
128
+ }
129
+
130
+ // Cmd/Ctrl + Enter to save
131
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && hasChanges) {
132
+ e.preventDefault()
133
+ handleSave()
134
+ }
135
+ }
136
+
137
+ window.addEventListener('keydown', handleKeyDown)
138
+ return () => window.removeEventListener('keydown', handleKeyDown)
139
+ }, [isOpen, hasChanges, onClose, handleSave])
140
+
141
+ if (!card) return null
142
+
143
+ return (
144
+ <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
145
+ <DialogContent className="sm:max-w-[500px]" data-cy="cards-modal">
146
+ <DialogHeader>
147
+ <DialogTitle className="sr-only">Edit Card</DialogTitle>
148
+ </DialogHeader>
149
+
150
+ <div className="space-y-4">
151
+ {/* Title */}
152
+ <PermissionGate permission="cards.update" fallback={
153
+ <div>
154
+ <Label className="text-muted-foreground text-xs">Title</Label>
155
+ <p className="font-medium mt-1">{card.title}</p>
156
+ </div>
157
+ }>
158
+ <div>
159
+ <Label htmlFor="title">Title</Label>
160
+ <Input
161
+ id="title"
162
+ value={title}
163
+ onChange={(e) => setTitle(e.target.value)}
164
+ placeholder="Card title..."
165
+ className="mt-1"
166
+ autoFocus
167
+ data-cy="cards-modal-title"
168
+ />
169
+ </div>
170
+ </PermissionGate>
171
+
172
+ {/* Description */}
173
+ <PermissionGate permission="cards.update" fallback={
174
+ <div>
175
+ <Label className="text-muted-foreground text-xs">Description</Label>
176
+ <p className="text-sm mt-1 text-muted-foreground">
177
+ {card.description || 'No description'}
178
+ </p>
179
+ </div>
180
+ }>
181
+ <div>
182
+ <Label htmlFor="description">Description</Label>
183
+ <Textarea
184
+ id="description"
185
+ value={description}
186
+ onChange={(e) => setDescription(e.target.value)}
187
+ placeholder="Add a more detailed description..."
188
+ className="mt-1 min-h-[100px]"
189
+ data-cy="cards-modal-description"
190
+ />
191
+ </div>
192
+ </PermissionGate>
193
+
194
+ {/* Priority and Due Date Row */}
195
+ <div className="grid grid-cols-2 gap-4">
196
+ {/* Priority */}
197
+ <PermissionGate permission="cards.update" fallback={
198
+ <div>
199
+ <Label className="text-muted-foreground text-xs">Priority</Label>
200
+ <p className="text-sm mt-1">{card.priority || 'None'}</p>
201
+ </div>
202
+ }>
203
+ <div>
204
+ <Label>Priority</Label>
205
+ <Select value={priority} onValueChange={setPriority}>
206
+ <SelectTrigger className="mt-1" data-cy="cards-modal-priority">
207
+ <SelectValue placeholder="Select priority..." />
208
+ </SelectTrigger>
209
+ <SelectContent>
210
+ <SelectItem value="none">None</SelectItem>
211
+ {priorityOptions.map((opt) => (
212
+ <SelectItem key={opt.value} value={opt.value}>
213
+ <span className={cn('px-2 py-0.5 rounded text-xs', opt.color)}>
214
+ {opt.label}
215
+ </span>
216
+ </SelectItem>
217
+ ))}
218
+ </SelectContent>
219
+ </Select>
220
+ </div>
221
+ </PermissionGate>
222
+
223
+ {/* Due Date */}
224
+ <PermissionGate permission="cards.update" fallback={
225
+ <div>
226
+ <Label className="text-muted-foreground text-xs">Due Date</Label>
227
+ <p className="text-sm mt-1">
228
+ {card.dueDate ? format(new Date(card.dueDate), 'PPP') : 'None'}
229
+ </p>
230
+ </div>
231
+ }>
232
+ <div>
233
+ <Label>Due Date</Label>
234
+ <Popover>
235
+ <PopoverTrigger asChild>
236
+ <Button
237
+ variant="outline"
238
+ className={cn(
239
+ 'w-full mt-1 justify-start text-left font-normal',
240
+ !dueDate && 'text-muted-foreground'
241
+ )}
242
+ data-cy="cards-modal-due-date"
243
+ >
244
+ <CalendarIcon className="mr-2 h-4 w-4" />
245
+ {dueDate ? format(dueDate, 'PPP') : 'Pick a date'}
246
+ </Button>
247
+ </PopoverTrigger>
248
+ <PopoverContent className="w-auto p-0" align="start">
249
+ <Calendar
250
+ mode="single"
251
+ selected={dueDate}
252
+ onSelect={setDueDate}
253
+ initialFocus
254
+ />
255
+ {dueDate && (
256
+ <div className="p-2 border-t">
257
+ <Button
258
+ variant="ghost"
259
+ size="sm"
260
+ className="w-full"
261
+ onClick={() => setDueDate(undefined)}
262
+ >
263
+ Clear date
264
+ </Button>
265
+ </div>
266
+ )}
267
+ </PopoverContent>
268
+ </Popover>
269
+ </div>
270
+ </PermissionGate>
271
+ </div>
272
+
273
+ {/* Actions */}
274
+ <div className="flex items-center justify-between pt-4 border-t">
275
+ <PermissionGate permission="cards.delete">
276
+ <Button
277
+ variant="destructive"
278
+ size="sm"
279
+ onClick={handleDelete}
280
+ disabled={isDeleting}
281
+ data-cy="cards-modal-delete"
282
+ >
283
+ <Trash2 className="h-4 w-4 mr-2" />
284
+ {isDeleting ? 'Deleting...' : 'Delete'}
285
+ </Button>
286
+ </PermissionGate>
287
+
288
+ <div className="flex items-center gap-2 ml-auto">
289
+ {hasChanges && (
290
+ <span className="text-xs text-muted-foreground flex items-center gap-1">
291
+ <AlertCircle className="h-3 w-3" />
292
+ Unsaved changes
293
+ </span>
294
+ )}
295
+ <Button variant="outline" onClick={onClose} disabled={isSaving} data-cy="cards-modal-cancel">
296
+ Cancel
297
+ </Button>
298
+ <PermissionGate permission="cards.update">
299
+ <Button
300
+ onClick={handleSave}
301
+ disabled={!hasChanges || isSaving || !title.trim()}
302
+ data-cy="cards-modal-save"
303
+ >
304
+ {isSaving ? 'Saving...' : 'Save'}
305
+ </Button>
306
+ </PermissionGate>
307
+ </div>
308
+ </div>
309
+
310
+ {/* Keyboard hint */}
311
+ <p className="text-xs text-muted-foreground text-center">
312
+ Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-xs">⌘ Enter</kbd> to save
313
+ </p>
314
+ </div>
315
+ </DialogContent>
316
+ </Dialog>
317
+ )
318
+ }