@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
@@ -0,0 +1,92 @@
1
+ -- ============================================================================
2
+ -- Cards Table Migration
3
+ -- Productivity theme: Task cards within lists
4
+ -- ============================================================================
5
+
6
+ CREATE TABLE IF NOT EXISTS "cards" (
7
+ "id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
8
+ "title" VARCHAR(500) NOT NULL,
9
+ "description" TEXT,
10
+ "position" INTEGER DEFAULT 0,
11
+ "priority" VARCHAR(20) DEFAULT 'medium' CHECK ("priority" IN ('low', 'medium', 'high', 'urgent')),
12
+ "dueDate" DATE,
13
+ "labels" JSONB DEFAULT '[]'::jsonb,
14
+ "assigneeId" TEXT REFERENCES "users"("id") ON DELETE SET NULL,
15
+ "listId" TEXT NOT NULL REFERENCES "lists"("id") ON DELETE CASCADE,
16
+ "boardId" TEXT NOT NULL REFERENCES "boards"("id") ON DELETE CASCADE,
17
+ "userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
18
+ "teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
19
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
20
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
21
+ );
22
+
23
+ -- Indexes
24
+ CREATE INDEX IF NOT EXISTS "cards_listId_idx" ON "cards" ("listId");
25
+ CREATE INDEX IF NOT EXISTS "cards_boardId_idx" ON "cards" ("boardId");
26
+ CREATE INDEX IF NOT EXISTS "cards_teamId_idx" ON "cards" ("teamId");
27
+ CREATE INDEX IF NOT EXISTS "cards_assigneeId_idx" ON "cards" ("assigneeId");
28
+ CREATE INDEX IF NOT EXISTS "cards_position_idx" ON "cards" ("listId", "position");
29
+ CREATE INDEX IF NOT EXISTS "cards_dueDate_idx" ON "cards" ("dueDate") WHERE "dueDate" IS NOT NULL;
30
+ CREATE INDEX IF NOT EXISTS "cards_labels_idx" ON "cards" USING GIN ("labels");
31
+
32
+ -- Enable RLS
33
+ ALTER TABLE "cards" ENABLE ROW LEVEL SECURITY;
34
+
35
+ -- Drop existing policies
36
+ DROP POLICY IF EXISTS "cards_select_policy" ON "cards";
37
+ DROP POLICY IF EXISTS "cards_insert_policy" ON "cards";
38
+ DROP POLICY IF EXISTS "cards_update_policy" ON "cards";
39
+ DROP POLICY IF EXISTS "cards_delete_policy" ON "cards";
40
+
41
+ -- Policy: Team members can view cards
42
+ CREATE POLICY "cards_select_policy" ON "cards"
43
+ FOR SELECT TO authenticated
44
+ USING (
45
+ "teamId" = ANY(public.get_user_team_ids())
46
+ OR public.is_superadmin()
47
+ );
48
+
49
+ -- Policy: Team members can create cards
50
+ CREATE POLICY "cards_insert_policy" ON "cards"
51
+ FOR INSERT TO authenticated
52
+ WITH CHECK (
53
+ "teamId" = ANY(public.get_user_team_ids())
54
+ );
55
+
56
+ -- Policy: Team members can update cards
57
+ CREATE POLICY "cards_update_policy" ON "cards"
58
+ FOR UPDATE TO authenticated
59
+ USING (
60
+ "teamId" = ANY(public.get_user_team_ids())
61
+ OR public.is_superadmin()
62
+ );
63
+
64
+ -- Policy: Team members can delete cards
65
+ CREATE POLICY "cards_delete_policy" ON "cards"
66
+ FOR DELETE TO authenticated
67
+ USING (
68
+ "teamId" = ANY(public.get_user_team_ids())
69
+ OR public.is_superadmin()
70
+ );
71
+
72
+ -- Trigger for updatedAt
73
+ CREATE OR REPLACE FUNCTION update_cards_updated_at()
74
+ RETURNS TRIGGER AS $$
75
+ BEGIN
76
+ NEW."updatedAt" = NOW();
77
+ RETURN NEW;
78
+ END;
79
+ $$ LANGUAGE plpgsql;
80
+
81
+ DROP TRIGGER IF EXISTS cards_updated_at_trigger ON "cards";
82
+ CREATE TRIGGER cards_updated_at_trigger
83
+ BEFORE UPDATE ON "cards"
84
+ FOR EACH ROW
85
+ EXECUTE FUNCTION update_cards_updated_at();
86
+
87
+ -- Comments
88
+ COMMENT ON TABLE "cards" IS 'Task cards within lists - the main work items';
89
+ COMMENT ON COLUMN "cards"."labels" IS 'JSON array of label strings';
90
+ COMMENT ON COLUMN "cards"."position" IS 'Display order within the parent list';
91
+ COMMENT ON COLUMN "cards"."assigneeId" IS 'Team member assigned to this card';
92
+ COMMENT ON COLUMN "cards"."priority" IS 'Card priority: low, medium, high, urgent';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Lists Entity Configuration
3
+ *
4
+ * Columns within boards (To Do, In Progress, Done, etc.)
5
+ */
6
+
7
+ import { List } from 'lucide-react'
8
+ import type { EntityConfig } from '@nextsparkjs/core/lib/entities/types'
9
+ import { listFields } from './lists.fields'
10
+
11
+ export const listEntityConfig: EntityConfig = {
12
+ // ==========================================
13
+ // 1. BASIC IDENTIFICATION
14
+ // ==========================================
15
+ slug: 'lists',
16
+ enabled: true,
17
+ names: {
18
+ singular: 'list',
19
+ plural: 'Lists'
20
+ },
21
+ icon: List,
22
+
23
+ // ==========================================
24
+ // 2. ACCESS AND SCOPE CONFIGURATION
25
+ // ==========================================
26
+ access: {
27
+ public: false,
28
+ api: true,
29
+ metadata: false,
30
+ shared: true
31
+ },
32
+
33
+ // ==========================================
34
+ // 3. UI/UX FEATURES
35
+ // ==========================================
36
+ ui: {
37
+ dashboard: {
38
+ showInMenu: false, // Lists shown within boards, not separately
39
+ showInTopbar: false
40
+ },
41
+ public: {
42
+ hasArchivePage: false,
43
+ hasSinglePage: false
44
+ },
45
+ features: {
46
+ searchable: true,
47
+ sortable: true,
48
+ filterable: true,
49
+ bulkOperations: false,
50
+ importExport: false
51
+ }
52
+ },
53
+
54
+ // ==========================================
55
+ // FIELDS
56
+ // ==========================================
57
+ fields: listFields,
58
+ }
59
+
60
+ export default listEntityConfig
61
+
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Lists Entity Fields Configuration
3
+ */
4
+
5
+ import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
6
+
7
+ export const listFields: EntityField[] = [
8
+ {
9
+ name: 'name',
10
+ type: 'text',
11
+ required: true,
12
+ display: {
13
+ label: 'Name',
14
+ description: 'List name',
15
+ placeholder: 'Enter list name...',
16
+ showInList: true,
17
+ showInDetail: true,
18
+ showInForm: true,
19
+ order: 1,
20
+ columnWidth: 12,
21
+ },
22
+ api: {
23
+ readOnly: false,
24
+ searchable: true,
25
+ sortable: true,
26
+ },
27
+ },
28
+ {
29
+ name: 'position',
30
+ type: 'number',
31
+ required: false,
32
+ defaultValue: 0,
33
+ display: {
34
+ label: 'Position',
35
+ description: 'Display order within board',
36
+ showInList: false,
37
+ showInDetail: false,
38
+ showInForm: false,
39
+ order: 2,
40
+ },
41
+ api: {
42
+ readOnly: false,
43
+ searchable: false,
44
+ sortable: true,
45
+ },
46
+ },
47
+ {
48
+ name: 'boardId',
49
+ type: 'reference',
50
+ required: true,
51
+ referenceEntity: 'boards',
52
+ display: {
53
+ label: 'Board',
54
+ description: 'Parent board',
55
+ placeholder: 'Select board...',
56
+ showInList: true,
57
+ showInDetail: true,
58
+ showInForm: true,
59
+ order: 3,
60
+ columnWidth: 12,
61
+ },
62
+ api: {
63
+ readOnly: false,
64
+ searchable: false,
65
+ sortable: false,
66
+ },
67
+ },
68
+ {
69
+ name: 'createdAt',
70
+ type: 'datetime',
71
+ required: false,
72
+ display: {
73
+ label: 'Created At',
74
+ description: 'When the list was created',
75
+ showInList: false,
76
+ showInDetail: true,
77
+ showInForm: false,
78
+ order: 98,
79
+ },
80
+ api: {
81
+ readOnly: true,
82
+ searchable: false,
83
+ sortable: true,
84
+ },
85
+ },
86
+ {
87
+ name: 'updatedAt',
88
+ type: 'datetime',
89
+ required: false,
90
+ display: {
91
+ label: 'Updated At',
92
+ description: 'When the list was last modified',
93
+ showInList: false,
94
+ showInDetail: true,
95
+ showInForm: false,
96
+ order: 99,
97
+ },
98
+ api: {
99
+ readOnly: true,
100
+ searchable: false,
101
+ sortable: true,
102
+ },
103
+ },
104
+ ]
105
+
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Lists Service
3
+ * Provides data access methods for lists entity.
4
+ */
5
+ import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
6
+
7
+ export interface List {
8
+ id: string
9
+ name: string
10
+ boardId: string
11
+ position: number
12
+ isArchived: boolean
13
+ userId: string
14
+ teamId: string
15
+ createdAt: Date
16
+ updatedAt: Date
17
+ }
18
+
19
+ export interface ListListOptions {
20
+ limit?: number
21
+ offset?: number
22
+ orderBy?: string
23
+ orderDir?: 'asc' | 'desc'
24
+ teamId?: string
25
+ boardId?: string
26
+ isArchived?: boolean
27
+ }
28
+
29
+ export interface ListListResult {
30
+ lists: List[]
31
+ total: number
32
+ }
33
+
34
+ export interface ListCreateData {
35
+ name: string
36
+ boardId: string
37
+ position?: number
38
+ isArchived?: boolean
39
+ teamId: string
40
+ }
41
+
42
+ export interface ListUpdateData {
43
+ name?: string
44
+ boardId?: string
45
+ position?: number
46
+ isArchived?: boolean
47
+ }
48
+
49
+ interface DbList {
50
+ id: string
51
+ name: string
52
+ board_id: string
53
+ position: number
54
+ is_archived: boolean
55
+ user_id: string
56
+ team_id: string
57
+ created_at: string
58
+ updated_at: string
59
+ }
60
+
61
+ function mapDbList(dbList: DbList): List {
62
+ return {
63
+ id: dbList.id,
64
+ name: dbList.name,
65
+ boardId: dbList.board_id,
66
+ position: dbList.position,
67
+ isArchived: dbList.is_archived,
68
+ userId: dbList.user_id,
69
+ teamId: dbList.team_id,
70
+ createdAt: new Date(dbList.created_at),
71
+ updatedAt: new Date(dbList.updated_at),
72
+ }
73
+ }
74
+
75
+ export class ListsService {
76
+ /**
77
+ * Get a list by ID with RLS
78
+ */
79
+ static async getById(id: string, userId: string): Promise<List | null> {
80
+ const result = await queryOneWithRLS<DbList>(
81
+ userId,
82
+ `SELECT * FROM lists WHERE id = $1`,
83
+ [id]
84
+ )
85
+
86
+ return result ? mapDbList(result) : null
87
+ }
88
+
89
+ /**
90
+ * List lists with RLS and filtering
91
+ */
92
+ static async list(userId: string, options: ListListOptions = {}): Promise<ListListResult> {
93
+ const {
94
+ limit = 50,
95
+ offset = 0,
96
+ orderBy = 'position',
97
+ orderDir = 'asc',
98
+ teamId,
99
+ boardId,
100
+ isArchived,
101
+ } = options
102
+
103
+ const conditions: string[] = []
104
+ const params: any[] = []
105
+ let paramIndex = 1
106
+
107
+ if (teamId) {
108
+ conditions.push(`team_id = $${paramIndex++}`)
109
+ params.push(teamId)
110
+ }
111
+
112
+ if (boardId) {
113
+ conditions.push(`board_id = $${paramIndex++}`)
114
+ params.push(boardId)
115
+ }
116
+
117
+ if (isArchived !== undefined) {
118
+ conditions.push(`is_archived = $${paramIndex++}`)
119
+ params.push(isArchived)
120
+ }
121
+
122
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
123
+
124
+ // Get total count
125
+ const countResult = await queryOneWithRLS<{ count: string }>(
126
+ userId,
127
+ `SELECT COUNT(*) as count FROM lists ${whereClause}`,
128
+ params
129
+ )
130
+ const total = parseInt(countResult?.count || '0', 10)
131
+
132
+ // Get lists
133
+ const validOrderBy = ['position', 'name', 'created_at', 'updated_at'].includes(orderBy)
134
+ ? orderBy
135
+ : 'position'
136
+ const validOrderDir = orderDir === 'desc' ? 'DESC' : 'ASC'
137
+
138
+ params.push(limit, offset)
139
+ const lists = await queryWithRLS<DbList>(
140
+ userId,
141
+ `SELECT * FROM lists ${whereClause} ORDER BY ${validOrderBy} ${validOrderDir} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
142
+ params
143
+ )
144
+
145
+ return {
146
+ lists: lists.map(mapDbList),
147
+ total,
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get all lists for a specific board
153
+ */
154
+ static async getByBoard(boardId: string, userId: string): Promise<List[]> {
155
+ const result = await this.list(userId, {
156
+ boardId,
157
+ orderBy: 'position',
158
+ orderDir: 'asc',
159
+ isArchived: false,
160
+ })
161
+
162
+ return result.lists
163
+ }
164
+
165
+ /**
166
+ * Create a new list with RLS
167
+ */
168
+ static async create(userId: string, data: ListCreateData): Promise<List> {
169
+ const {
170
+ name,
171
+ boardId,
172
+ position = 0,
173
+ isArchived = false,
174
+ teamId,
175
+ } = data
176
+
177
+ const result = await mutateWithRLS<DbList>(
178
+ userId,
179
+ `INSERT INTO lists (name, board_id, position, is_archived, user_id, team_id)
180
+ VALUES ($1, $2, $3, $4, $5, $6)
181
+ RETURNING *`,
182
+ [name, boardId, position, isArchived, userId, teamId]
183
+ )
184
+
185
+ if (!result) {
186
+ throw new Error('Failed to create list')
187
+ }
188
+
189
+ return mapDbList(result)
190
+ }
191
+
192
+ /**
193
+ * Update a list with RLS
194
+ */
195
+ static async update(userId: string, id: string, data: ListUpdateData): Promise<List> {
196
+ const updates: string[] = []
197
+ const params: any[] = []
198
+ let paramIndex = 1
199
+
200
+ if (data.name !== undefined) {
201
+ updates.push(`name = $${paramIndex++}`)
202
+ params.push(data.name)
203
+ }
204
+
205
+ if (data.boardId !== undefined) {
206
+ updates.push(`board_id = $${paramIndex++}`)
207
+ params.push(data.boardId)
208
+ }
209
+
210
+ if (data.position !== undefined) {
211
+ updates.push(`position = $${paramIndex++}`)
212
+ params.push(data.position)
213
+ }
214
+
215
+ if (data.isArchived !== undefined) {
216
+ updates.push(`is_archived = $${paramIndex++}`)
217
+ params.push(data.isArchived)
218
+ }
219
+
220
+ if (updates.length === 0) {
221
+ throw new Error('No fields to update')
222
+ }
223
+
224
+ updates.push(`updated_at = NOW()`)
225
+ params.push(id)
226
+
227
+ const result = await mutateWithRLS<DbList>(
228
+ userId,
229
+ `UPDATE lists SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
230
+ params
231
+ )
232
+
233
+ if (!result) {
234
+ throw new Error('List not found or access denied')
235
+ }
236
+
237
+ return mapDbList(result)
238
+ }
239
+
240
+ /**
241
+ * Delete a list with RLS
242
+ */
243
+ static async delete(userId: string, id: string): Promise<boolean> {
244
+ const result = await mutateWithRLS<DbList>(
245
+ userId,
246
+ `DELETE FROM lists WHERE id = $1 RETURNING *`,
247
+ [id]
248
+ )
249
+
250
+ return result !== null
251
+ }
252
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * List Service Types
3
+ *
4
+ * Type definitions for the ListsService.
5
+ * Lists represent columns within a board that contain cards.
6
+ *
7
+ * @module ListsTypes
8
+ */
9
+
10
+ // Main entity interface
11
+ export interface List {
12
+ id: string
13
+ name: string
14
+ boardId: string
15
+ position: number
16
+ isArchived: boolean
17
+ teamId: string
18
+ userId: string
19
+ createdAt: string
20
+ updatedAt: string
21
+ }
22
+
23
+ // List options
24
+ export interface ListListOptions {
25
+ limit?: number
26
+ offset?: number
27
+ teamId?: string
28
+ boardId?: string
29
+ isArchived?: boolean
30
+ orderBy?: 'name' | 'position' | 'createdAt'
31
+ orderDir?: 'asc' | 'desc'
32
+ }
33
+
34
+ // List result
35
+ export interface ListListResult {
36
+ lists: List[]
37
+ total: number
38
+ }
39
+
40
+ // Create data (required fields + teamId + optional fields)
41
+ export interface ListCreateData {
42
+ name: string
43
+ boardId: string
44
+ teamId: string
45
+ position?: number
46
+ isArchived?: boolean
47
+ }
48
+
49
+ // Update data (all fields optional)
50
+ export interface ListUpdateData {
51
+ name?: string
52
+ boardId?: string
53
+ position?: number
54
+ isArchived?: boolean
55
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "title": "Lists",
3
+ "singular": "List",
4
+ "plural": "Lists",
5
+ "description": "Columns within boards to organize cards",
6
+
7
+ "fields": {
8
+ "name": {
9
+ "label": "Name",
10
+ "placeholder": "Enter list name...",
11
+ "description": "List name"
12
+ },
13
+ "position": {
14
+ "label": "Position",
15
+ "description": "Display order within board"
16
+ },
17
+ "boardId": {
18
+ "label": "Board",
19
+ "placeholder": "Select board...",
20
+ "description": "Parent board"
21
+ },
22
+ "createdAt": {
23
+ "label": "Created At",
24
+ "description": "When the list was created"
25
+ },
26
+ "updatedAt": {
27
+ "label": "Updated At",
28
+ "description": "When the list was last modified"
29
+ }
30
+ },
31
+
32
+ "actions": {
33
+ "create": "Add List",
34
+ "edit": "Edit List",
35
+ "delete": "Delete List",
36
+ "moveLeft": "Move Left",
37
+ "moveRight": "Move Right"
38
+ },
39
+
40
+ "messages": {
41
+ "created": "List created successfully",
42
+ "updated": "List updated successfully",
43
+ "deleted": "List deleted successfully",
44
+ "moved": "List moved",
45
+ "confirmDelete": "Are you sure you want to delete this list? All cards within it will be permanently removed."
46
+ },
47
+
48
+ "empty": {
49
+ "title": "No lists yet",
50
+ "description": "Add a list to start organizing your cards.",
51
+ "action": "Add a list"
52
+ },
53
+
54
+ "defaults": {
55
+ "todo": "To Do",
56
+ "inProgress": "In Progress",
57
+ "done": "Done"
58
+ }
59
+ }
60
+
@@ -0,0 +1,60 @@
1
+ {
2
+ "title": "Listas",
3
+ "singular": "Lista",
4
+ "plural": "Listas",
5
+ "description": "Columnas dentro de los tableros para organizar tarjetas",
6
+
7
+ "fields": {
8
+ "name": {
9
+ "label": "Nombre",
10
+ "placeholder": "Ingresa el nombre de la lista...",
11
+ "description": "Nombre de la lista"
12
+ },
13
+ "position": {
14
+ "label": "Posición",
15
+ "description": "Orden de visualización dentro del tablero"
16
+ },
17
+ "boardId": {
18
+ "label": "Tablero",
19
+ "placeholder": "Seleccionar tablero...",
20
+ "description": "Tablero padre"
21
+ },
22
+ "createdAt": {
23
+ "label": "Creado",
24
+ "description": "Cuándo se creó la lista"
25
+ },
26
+ "updatedAt": {
27
+ "label": "Actualizado",
28
+ "description": "Cuándo se modificó por última vez"
29
+ }
30
+ },
31
+
32
+ "actions": {
33
+ "create": "Agregar Lista",
34
+ "edit": "Editar Lista",
35
+ "delete": "Eliminar Lista",
36
+ "moveLeft": "Mover a la Izquierda",
37
+ "moveRight": "Mover a la Derecha"
38
+ },
39
+
40
+ "messages": {
41
+ "created": "Lista creada exitosamente",
42
+ "updated": "Lista actualizada exitosamente",
43
+ "deleted": "Lista eliminada exitosamente",
44
+ "moved": "Lista movida",
45
+ "confirmDelete": "¿Estás seguro de que quieres eliminar esta lista? Todas las tarjetas dentro de ella serán eliminadas permanentemente."
46
+ },
47
+
48
+ "empty": {
49
+ "title": "Aún no hay listas",
50
+ "description": "Agrega una lista para comenzar a organizar tus tarjetas.",
51
+ "action": "Agregar una lista"
52
+ },
53
+
54
+ "defaults": {
55
+ "todo": "Por Hacer",
56
+ "inProgress": "En Progreso",
57
+ "done": "Hecho"
58
+ }
59
+ }
60
+