@fayz-ai/plugin-tasks 0.9.3 → 0.10.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 (48) hide show
  1. package/dist/components/TasksDrawer.d.ts +1 -1
  2. package/dist/components/TasksDrawer.d.ts.map +1 -1
  3. package/dist/components/TasksLauncher.d.ts +11 -0
  4. package/dist/components/TasksLauncher.d.ts.map +1 -0
  5. package/dist/context.d.ts +0 -11
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/data/registries.d.ts.map +1 -1
  8. package/dist/index.d.ts +5 -6
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +128 -875
  11. package/dist/index.js.map +1 -1
  12. package/dist/migrations/index.d.ts +1 -0
  13. package/dist/migrations/index.d.ts.map +1 -1
  14. package/package.json +7 -7
  15. package/dist/components/TaskCard.d.ts +0 -10
  16. package/dist/components/TaskCard.d.ts.map +0 -1
  17. package/dist/components/TaskDetail.d.ts +0 -5
  18. package/dist/components/TaskDetail.d.ts.map +0 -1
  19. package/dist/components/TaskLabelBadge.d.ts +0 -9
  20. package/dist/components/TaskLabelBadge.d.ts.map +0 -1
  21. package/dist/components/TaskPriorityBadge.d.ts +0 -7
  22. package/dist/components/TaskPriorityBadge.d.ts.map +0 -1
  23. package/dist/components/TaskQuickAdd.d.ts +0 -5
  24. package/dist/components/TaskQuickAdd.d.ts.map +0 -1
  25. package/dist/components/TaskStatusGroup.d.ts +0 -7
  26. package/dist/components/TaskStatusGroup.d.ts.map +0 -1
  27. package/dist/components/TasksFilterBar.d.ts +0 -3
  28. package/dist/components/TasksFilterBar.d.ts.map +0 -1
  29. package/dist/components/TasksPanel.d.ts +0 -7
  30. package/dist/components/TasksPanel.d.ts.map +0 -1
  31. package/dist/components/TasksTopbarButton.d.ts +0 -15
  32. package/dist/components/TasksTopbarButton.d.ts.map +0 -1
  33. package/dist/data/capability.test.d.ts +0 -2
  34. package/dist/data/capability.test.d.ts.map +0 -1
  35. package/dist/data/mock.d.ts +0 -3
  36. package/dist/data/mock.d.ts.map +0 -1
  37. package/dist/data/supabase.d.ts +0 -3
  38. package/dist/data/supabase.d.ts.map +0 -1
  39. package/dist/data/tables.d.ts +0 -5
  40. package/dist/data/tables.d.ts.map +0 -1
  41. package/dist/data/types.d.ts +0 -13
  42. package/dist/data/types.d.ts.map +0 -1
  43. package/dist/lib/tenant.d.ts +0 -3
  44. package/dist/lib/tenant.d.ts.map +0 -1
  45. package/dist/store.d.ts +0 -27
  46. package/dist/store.d.ts.map +0 -1
  47. package/dist/types.d.ts +0 -68
  48. package/dist/types.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,375 +1,12 @@
1
- import React4, { useEffect, useState, useCallback } from 'react';
2
- import { createPluginContext, registerNotesTasksAdapter, NOTE_LABEL_PREFIX, dedup, useRightRailStore, useRightRailPanel, useTenantPluginSettings, useDelegateToAssistant } from '@fayz-ai/admin';
3
- import { registerTranslations, createSafeDataProvider, useTranslation, getActiveTenantId, getSupabaseClientOptional } from '@fayz-ai/core';
4
- import { createStore } from 'zustand/vanilla';
1
+ import { registerTranslations, useTranslation } from '@fayz-ai/core';
2
+ import { getTasksProvider, registerNotesTasksAdapter, useTasksStore, NOTE_LABEL_PREFIX, useTenantPluginSettings, useRightRailStore, useWidgetSurface, useTasksSummaryLine, RightRailPage, TasksPanel } from '@fayz-ai/admin';
3
+ export { TaskCard, TaskDetail, TaskQuickAdd, TasksFilterBar, TasksPanel, createMockTasksProvider, createSupabaseTasksProvider, useTasksStore, useTasksSummaryLine } from '@fayz-ai/admin';
5
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
- import { CheckSquare, ClipboardList, Plus, ArrowLeft, Trash2, ChevronDown, Calendar, Sparkles, ListChecks, ChevronRight, AlertTriangle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
7
- import { Button, cn, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, Checkbox } from '@fayz-ai/ui';
8
- import { useStore } from 'zustand';
5
+ import { useEffect } from 'react';
6
+ import { CheckSquare } from 'lucide-react';
7
+ import { useLayoutStore, useRailRow, cn, SheetBody } from '@fayz-ai/ui';
9
8
 
10
9
  // src/index.ts
11
- var ctx = createPluginContext("TasksContextProvider");
12
- var TasksContextProvider = ctx.ContextProvider;
13
- ctx.useConfig;
14
- ctx.useProvider;
15
- var useTasksStore = ctx.useStore;
16
-
17
- // src/data/mock.ts
18
- function uid() {
19
- return crypto.randomUUID?.() ?? String(Date.now());
20
- }
21
- function now() {
22
- return (/* @__PURE__ */ new Date()).toISOString();
23
- }
24
- function today() {
25
- return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
26
- }
27
- var SEED_LABELS = [
28
- { id: "lbl-bug", name: "Bug", color: "#ef4444", isActive: true, tenantId: "mock", createdAt: now() },
29
- { id: "lbl-feature", name: "Feature", color: "#3b82f6", isActive: true, tenantId: "mock", createdAt: now() },
30
- { id: "lbl-urgent", name: "Urgent", color: "#f59e0b", isActive: true, tenantId: "mock", createdAt: now() },
31
- { id: "lbl-personal", name: "Personal", color: "#8b5cf6", isActive: true, tenantId: "mock", createdAt: now() }
32
- ];
33
- function makeSeedTasks() {
34
- const base = {
35
- tenantId: "mock",
36
- description: null,
37
- assignedToId: null,
38
- assignedToName: null,
39
- parentId: null,
40
- labels: [],
41
- createdById: null,
42
- createdByName: null,
43
- createdAt: now(),
44
- updatedAt: now()
45
- };
46
- return [
47
- { ...base, id: uid(), title: "Setup payment gateway", status: "todo", priority: "high", dueDate: today(), position: 0 },
48
- { ...base, id: uid(), title: "Update menu photos", status: "todo", priority: "medium", dueDate: null, position: 1 },
49
- { ...base, id: uid(), title: "Review monthly report", status: "in_progress", priority: "medium", dueDate: null, position: 0 },
50
- { ...base, id: uid(), title: "Fix login bug", status: "in_progress", priority: "urgent", dueDate: today(), position: 1, labels: ["lbl-bug"] },
51
- { ...base, id: uid(), title: "Onboard new employee", status: "done", priority: "low", dueDate: null, position: 0 }
52
- ];
53
- }
54
- function matchesQuery(task, query) {
55
- if (query.status) {
56
- const statuses = Array.isArray(query.status) ? query.status : [query.status];
57
- if (!statuses.includes(task.status)) return false;
58
- }
59
- if (query.priority) {
60
- const priorities = Array.isArray(query.priority) ? query.priority : [query.priority];
61
- if (!priorities.includes(task.priority)) return false;
62
- }
63
- if (query.assignedToId && task.assignedToId !== query.assignedToId) return false;
64
- if (query.parentId !== void 0) {
65
- if (query.parentId === null && task.parentId !== null) return false;
66
- if (query.parentId !== null && task.parentId !== query.parentId) return false;
67
- }
68
- if (query.search) {
69
- const s = query.search.toLowerCase();
70
- if (!task.title.toLowerCase().includes(s)) return false;
71
- }
72
- if (query.overdue && task.dueDate) {
73
- if (task.dueDate >= today()) return false;
74
- }
75
- return true;
76
- }
77
- function createMockTasksProvider() {
78
- let tasks = makeSeedTasks();
79
- const labels = [...SEED_LABELS];
80
- return {
81
- async getTasks(query) {
82
- return tasks.filter((t) => matchesQuery(t, query)).sort((a, b) => a.position - b.position);
83
- },
84
- async getTaskById(id) {
85
- return tasks.find((t) => t.id === id) ?? null;
86
- },
87
- async getSubtasks(parentId) {
88
- return tasks.filter((t) => t.parentId === parentId).sort((a, b) => a.position - b.position);
89
- },
90
- async createTask(input) {
91
- const statusTasks = tasks.filter((t) => t.status === (input.status ?? "todo") && t.parentId === (input.parentId ?? null));
92
- const maxPos = statusTasks.reduce((max, t) => Math.max(max, t.position), -1);
93
- const task = {
94
- id: uid(),
95
- title: input.title,
96
- description: input.description ?? null,
97
- status: input.status ?? "todo",
98
- priority: input.priority ?? "medium",
99
- dueDate: input.dueDate ?? null,
100
- assignedToId: input.assignedToId ?? null,
101
- assignedToName: null,
102
- parentId: input.parentId ?? null,
103
- labels: input.labels ?? [],
104
- position: maxPos + 1,
105
- createdById: null,
106
- createdByName: null,
107
- tenantId: "mock",
108
- createdAt: now(),
109
- updatedAt: now()
110
- };
111
- tasks.push(task);
112
- return task;
113
- },
114
- async updateTask(id, input) {
115
- const idx = tasks.findIndex((t) => t.id === id);
116
- if (idx === -1) throw new Error("Task not found");
117
- tasks[idx] = { ...tasks[idx], ...input, updatedAt: now() };
118
- return tasks[idx];
119
- },
120
- async deleteTask(id) {
121
- tasks = tasks.filter((t) => t.id !== id && t.parentId !== id);
122
- },
123
- async reorderTask(id, newPosition, status) {
124
- const task = tasks.find((t) => t.id === id);
125
- if (!task) return;
126
- task.status = status;
127
- task.position = newPosition;
128
- task.updatedAt = now();
129
- },
130
- async getLabels() {
131
- return labels.filter((l) => l.isActive);
132
- },
133
- async getSummary() {
134
- const topLevel = tasks.filter((t) => t.parentId === null);
135
- const d = today();
136
- return {
137
- total: topLevel.length,
138
- todo: topLevel.filter((t) => t.status === "todo").length,
139
- inProgress: topLevel.filter((t) => t.status === "in_progress").length,
140
- done: topLevel.filter((t) => t.status === "done").length,
141
- overdue: topLevel.filter((t) => t.dueDate != null && t.dueDate < d && t.status !== "done" && t.status !== "cancelled").length,
142
- dueToday: topLevel.filter((t) => t.dueDate === d && t.status !== "done" && t.status !== "cancelled").length,
143
- highPriority: topLevel.filter((t) => (t.priority === "high" || t.priority === "urgent") && t.status !== "done" && t.status !== "cancelled").length
144
- };
145
- }
146
- };
147
- }
148
-
149
- // src/data/tables.ts
150
- var T = {
151
- tasks: "plg_tasks_tasks",
152
- labels: "plg_tasks_labels"
153
- };
154
-
155
- // src/data/supabase.ts
156
- function getTenantId() {
157
- return getActiveTenantId();
158
- }
159
- function getClient() {
160
- const supabase = getSupabaseClientOptional();
161
- if (!supabase) throw new Error("Supabase not initialized");
162
- return supabase;
163
- }
164
- function snakeToCamel(obj) {
165
- const result = {};
166
- for (const [key, value] of Object.entries(obj)) {
167
- result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value;
168
- }
169
- return result;
170
- }
171
- function toTask(row) {
172
- const t = snakeToCamel(row);
173
- if (!Array.isArray(t.labels)) t.labels = [];
174
- return t;
175
- }
176
- function createSupabaseTasksProvider() {
177
- return {
178
- async getTasks(query) {
179
- const tenantId = getTenantId();
180
- if (!tenantId) return [];
181
- let q = getClient().from(T.tasks).select("*").eq("tenant_id", tenantId);
182
- if (query.parentId === null) {
183
- q = q.is("parent_id", null);
184
- } else if (query.parentId) {
185
- q = q.eq("parent_id", query.parentId);
186
- }
187
- if (query.status) {
188
- const statuses = Array.isArray(query.status) ? query.status : [query.status];
189
- q = q.in("status", statuses);
190
- }
191
- if (query.priority) {
192
- const priorities = Array.isArray(query.priority) ? query.priority : [query.priority];
193
- q = q.in("priority", priorities);
194
- }
195
- if (query.assignedToId) {
196
- q = q.eq("assigned_to_id", query.assignedToId);
197
- }
198
- if (query.search) {
199
- q = q.ilike("title", `%${query.search}%`);
200
- }
201
- if (query.overdue) {
202
- q = q.lt("due_date", (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)).not("status", "in", '("done","cancelled")');
203
- }
204
- q = q.order("position", { ascending: true }).order("created_at", { ascending: false });
205
- const { data } = await q;
206
- return (data ?? []).map(toTask);
207
- },
208
- async getTaskById(id) {
209
- const { data } = await getClient().from(T.tasks).select("*").eq("id", id).maybeSingle();
210
- return data ? toTask(data) : null;
211
- },
212
- async getSubtasks(parentId) {
213
- const { data } = await getClient().from(T.tasks).select("*").eq("parent_id", parentId).order("position", { ascending: true });
214
- return (data ?? []).map(toTask);
215
- },
216
- async createTask(input) {
217
- const tenantId = getTenantId();
218
- if (!tenantId) throw new Error("No active tenant");
219
- const { data: maxData } = await getClient().from(T.tasks).select("position").eq("tenant_id", tenantId).eq("status", input.status ?? "todo").is("parent_id", input.parentId ?? null).order("position", { ascending: false }).limit(1);
220
- const maxPos = maxData?.[0]?.position ?? -1;
221
- const { data, error } = await getClient().from(T.tasks).insert({
222
- tenant_id: tenantId,
223
- title: input.title,
224
- description: input.description ?? null,
225
- status: input.status ?? "todo",
226
- priority: input.priority ?? "medium",
227
- due_date: input.dueDate ?? null,
228
- assigned_to_id: input.assignedToId ?? null,
229
- parent_id: input.parentId ?? null,
230
- labels: input.labels ?? [],
231
- position: maxPos + 1
232
- }).select("*").single();
233
- if (error) throw error;
234
- return toTask(data);
235
- },
236
- async updateTask(id, input) {
237
- const payload = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
238
- if (input.title !== void 0) payload.title = input.title;
239
- if (input.description !== void 0) payload.description = input.description;
240
- if (input.status !== void 0) payload.status = input.status;
241
- if (input.priority !== void 0) payload.priority = input.priority;
242
- if (input.dueDate !== void 0) payload.due_date = input.dueDate;
243
- if (input.assignedToId !== void 0) payload.assigned_to_id = input.assignedToId;
244
- if (input.labels !== void 0) payload.labels = input.labels;
245
- if (input.position !== void 0) payload.position = input.position;
246
- const { data, error } = await getClient().from(T.tasks).update(payload).eq("id", id).select("*").single();
247
- if (error) throw error;
248
- return toTask(data);
249
- },
250
- async deleteTask(id) {
251
- await getClient().from(T.tasks).delete().eq("id", id);
252
- },
253
- async reorderTask(id, newPosition, status) {
254
- await getClient().from(T.tasks).update({ position: newPosition, status, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id);
255
- },
256
- async getLabels() {
257
- const tenantId = getTenantId();
258
- if (!tenantId) return [];
259
- const { data } = await getClient().from(T.labels).select("*").eq("tenant_id", tenantId).eq("is_active", true).order("name");
260
- return (data ?? []).map((r) => snakeToCamel(r));
261
- },
262
- async getSummary() {
263
- const tenantId = getTenantId();
264
- if (!tenantId) return { total: 0, todo: 0, inProgress: 0, done: 0, overdue: 0, dueToday: 0, highPriority: 0 };
265
- const { data } = await getClient().from(T.tasks).select("status, priority, due_date").eq("tenant_id", tenantId).is("parent_id", null);
266
- const tasks = data ?? [];
267
- const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
268
- return {
269
- total: tasks.length,
270
- todo: tasks.filter((t) => t.status === "todo").length,
271
- inProgress: tasks.filter((t) => t.status === "in_progress").length,
272
- done: tasks.filter((t) => t.status === "done").length,
273
- overdue: tasks.filter((t) => t.due_date && t.due_date < today2 && t.status !== "done" && t.status !== "cancelled").length,
274
- dueToday: tasks.filter((t) => t.due_date === today2 && t.status !== "done" && t.status !== "cancelled").length,
275
- highPriority: tasks.filter((t) => (t.priority === "high" || t.priority === "urgent") && t.status !== "done" && t.status !== "cancelled").length
276
- };
277
- }
278
- };
279
- }
280
- function createTasksStore(provider) {
281
- return createStore((set, get) => ({
282
- tasks: [],
283
- tasksLoading: false,
284
- labels: [],
285
- summary: null,
286
- summaryLoading: false,
287
- drawerOpen: false,
288
- selectedTaskId: null,
289
- activeFilter: "all",
290
- // ----- Data actions -----
291
- async fetchTasks(query) {
292
- return dedup("tasks:list", async () => {
293
- set({ tasksLoading: true });
294
- try {
295
- const filter = get().activeFilter;
296
- const q = {
297
- parentId: null,
298
- ...query,
299
- ...filter !== "all" ? { status: filter } : {}
300
- };
301
- const tasks = await provider.getTasks(q);
302
- set({ tasks, tasksLoading: false });
303
- } catch {
304
- set({ tasksLoading: false });
305
- }
306
- });
307
- },
308
- async fetchLabels() {
309
- return dedup("tasks:labels", async () => {
310
- try {
311
- const labels = await provider.getLabels();
312
- set({ labels });
313
- } catch {
314
- }
315
- });
316
- },
317
- async fetchSummary() {
318
- return dedup("tasks:summary", async () => {
319
- set({ summaryLoading: true });
320
- try {
321
- const summary = await provider.getSummary();
322
- set({ summary, summaryLoading: false });
323
- } catch {
324
- set({ summaryLoading: false });
325
- }
326
- });
327
- },
328
- async createTask(input) {
329
- const task = await provider.createTask(input);
330
- await get().fetchTasks();
331
- await get().fetchSummary();
332
- return task;
333
- },
334
- async updateTask(id, input) {
335
- const task = await provider.updateTask(id, input);
336
- set((s) => ({ tasks: s.tasks.map((t) => t.id === id ? task : t) }));
337
- await get().fetchSummary();
338
- return task;
339
- },
340
- async deleteTask(id) {
341
- await provider.deleteTask(id);
342
- set((s) => ({ tasks: s.tasks.filter((t) => t.id !== id), selectedTaskId: null }));
343
- await get().fetchSummary();
344
- },
345
- async reorderTask(id, newPosition, status) {
346
- await provider.reorderTask(id, newPosition, status);
347
- await get().fetchTasks();
348
- },
349
- // ----- UI actions -----
350
- openDrawer() {
351
- set({ drawerOpen: true });
352
- get().fetchTasks();
353
- get().fetchLabels();
354
- get().fetchSummary();
355
- },
356
- closeDrawer() {
357
- set({ drawerOpen: false, selectedTaskId: null });
358
- },
359
- toggleDrawer() {
360
- const { drawerOpen } = get();
361
- if (drawerOpen) get().closeDrawer();
362
- else get().openDrawer();
363
- },
364
- selectTask(id) {
365
- set({ selectedTaskId: id });
366
- },
367
- setFilter(filter) {
368
- set({ activeFilter: filter });
369
- get().fetchTasks();
370
- }
371
- }));
372
- }
373
10
 
374
11
  // src/locales/en.ts
375
12
  var en = {
@@ -466,6 +103,7 @@ var tasksLocales = {
466
103
  };
467
104
 
468
105
  // src/data/registries.ts
106
+ var LABELS_TABLE = "plg_tasks_labels";
469
107
  var taskLabelEntity = {
470
108
  name: "Task Label",
471
109
  namePlural: "Task Labels",
@@ -477,7 +115,7 @@ var taskLabelEntity = {
477
115
  { key: "color", label: "Color", type: "text", showInTable: true, defaultValue: "#6366f1" },
478
116
  { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
479
117
  ],
480
- data: { table: T.labels, tenantScoped: true }
118
+ data: { table: LABELS_TABLE, tenantScoped: true }
481
119
  };
482
120
  var tasksRegistries = [
483
121
  {
@@ -524,516 +162,86 @@ function TasksGeneralSettings() {
524
162
  ] }) })
525
163
  ] });
526
164
  }
527
- var FILTERS = [
528
- { key: "all", labelKey: "tasks.filter.all" },
529
- { key: "todo", labelKey: "tasks.filter.todo" },
530
- { key: "in_progress", labelKey: "tasks.filter.inProgress" },
531
- { key: "done", labelKey: "tasks.filter.done" }
532
- ];
533
- function TasksFilterBar() {
534
- const t = useTranslation();
535
- const activeFilter = useTasksStore((s) => s.activeFilter);
536
- const setFilter = useTasksStore((s) => s.setFilter);
537
- const summary = useTasksStore((s) => s.summary);
538
- const getCount = (key) => {
539
- if (!summary) return void 0;
540
- switch (key) {
541
- case "all":
542
- return summary.total;
543
- case "todo":
544
- return summary.todo;
545
- case "in_progress":
546
- return summary.inProgress;
547
- case "done":
548
- return summary.done;
549
- default:
550
- return void 0;
551
- }
552
- };
553
- return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-3 py-2 border-b overflow-x-auto", children: FILTERS.map(({ key, labelKey }) => {
554
- const active = activeFilter === key;
555
- const count = getCount(key);
556
- return /* @__PURE__ */ jsxs(
557
- "button",
558
- {
559
- type: "button",
560
- onClick: () => setFilter(key),
561
- className: `inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors whitespace-nowrap ${active ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
562
- children: [
563
- t(labelKey),
564
- count != null && /* @__PURE__ */ jsx("span", { className: `text-[10px] ${active ? "text-primary-foreground/80" : "text-muted-foreground"}`, children: count })
565
- ]
566
- },
567
- key
568
- );
569
- }) });
570
- }
571
- function TaskQuickAdd({ parentId }) {
165
+ function TasksDrawer() {
572
166
  const t = useTranslation();
573
- const createTask = useTasksStore((s) => s.createTask);
574
- const [title, setTitle] = useState("");
575
- const [loading, setLoading] = useState(false);
576
- const handleSubmit = useCallback(async (e) => {
577
- e.preventDefault();
578
- const trimmed = title.trim();
579
- if (!trimmed || loading) return;
580
- setLoading(true);
581
- try {
582
- await createTask({
583
- title: trimmed,
584
- parentId: parentId ?? null
585
- });
586
- setTitle("");
587
- } finally {
588
- setLoading(false);
589
- }
590
- }, [title, loading, createTask, parentId]);
591
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex items-center gap-2 px-3 py-2 border-b", children: [
592
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
593
- /* @__PURE__ */ jsx(
594
- "input",
595
- {
596
- type: "text",
597
- value: title,
598
- onChange: (e) => setTitle(e.target.value),
599
- placeholder: parentId ? t("tasks.detail.addSubtask") : t("tasks.quickAdd.placeholder"),
600
- className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60",
601
- disabled: loading
602
- }
603
- )
604
- ] });
605
- }
606
- var PRIORITY_CONFIG = {
607
- low: { color: "text-muted-foreground", bg: "bg-muted", icon: ArrowDown },
608
- medium: { color: "text-info-soft-foreground", bg: "bg-info-soft", icon: Minus },
609
- high: { color: "text-warning-soft-foreground", bg: "bg-warning-soft", icon: ArrowUp },
610
- urgent: { color: "text-destructive-soft-foreground", bg: "bg-destructive-soft", icon: AlertTriangle }
611
- };
612
- function TaskPriorityBadge({ priority, showLabel }) {
613
- const cfg = PRIORITY_CONFIG[priority];
614
- const Icon = cfg.icon;
615
- return /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium ${cfg.color} ${cfg.bg}`, children: [
616
- /* @__PURE__ */ jsx(Icon, { className: "h-3 w-3" }),
617
- showLabel && /* @__PURE__ */ jsx("span", { className: "capitalize", children: priority })
618
- ] });
619
- }
620
- function TaskLabelBadge({ labelId }) {
621
- const labels = useTasksStore((s) => s.labels);
622
- const label = labels.find((l) => l.id === labelId);
623
- if (!label) return null;
624
- return /* @__PURE__ */ jsx(
625
- "span",
626
- {
627
- className: "inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium leading-none",
628
- style: {
629
- backgroundColor: label.color + "20",
630
- color: label.color
631
- },
632
- children: label.name
633
- }
634
- );
635
- }
636
- function TaskLabelBadges({ labelIds, max = 2 }) {
637
- if (labelIds.length === 0) return null;
638
- const visible = labelIds.slice(0, max);
639
- const overflow = labelIds.length - max;
640
- return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
641
- visible.map((id) => /* @__PURE__ */ jsx(TaskLabelBadge, { labelId: id }, id)),
642
- overflow > 0 && /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
643
- "+",
644
- overflow
645
- ] })
646
- ] });
647
- }
648
- function isOverdue(dueDate) {
649
- if (!dueDate) return false;
650
- return dueDate < (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
651
- }
652
- function isToday(dueDate) {
653
- if (!dueDate) return false;
654
- return dueDate === (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
655
- }
656
- function formatDueDate(dueDate) {
657
- const d = /* @__PURE__ */ new Date(dueDate + "T00:00:00");
658
- return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
659
- }
660
- function TaskCard({ task, subtaskCount }) {
661
- const selectTask = useTasksStore((s) => s.selectTask);
662
- const updateTask = useTasksStore((s) => s.updateTask);
663
- const isDone = task.status === "done";
664
- const overdue = !isDone && isOverdue(task.dueDate);
665
- const dueToday = !isDone && isToday(task.dueDate);
666
- const handleCheck = (e) => {
667
- e.stopPropagation();
668
- updateTask(task.id, { status: isDone ? "todo" : "done" });
669
- };
167
+ const drawerOpen = useTasksStore((s) => s.drawerOpen);
168
+ const closeDrawer = useTasksStore((s) => s.closeDrawer);
169
+ const summaryLine = useTasksSummaryLine();
670
170
  return /* @__PURE__ */ jsxs(
671
- "div",
171
+ RightRailPage,
672
172
  {
673
- onClick: () => selectTask(task.id),
674
- className: "group flex items-start gap-2.5 rounded-lg border border-transparent px-3 py-2.5 transition-colors hover:bg-muted bg-card shadow-button active:shadow-button-inset hover:border-border cursor-pointer",
173
+ title: t("tasks.drawer.title"),
174
+ open: drawerOpen,
175
+ onClose: closeDrawer,
176
+ width: "max-w-lg",
675
177
  children: [
676
- /* @__PURE__ */ jsx("div", { className: "pt-0.5", onClick: handleCheck, children: /* @__PURE__ */ jsx(Checkbox, { checked: isDone, onChange: () => {
677
- } }) }),
678
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
679
- /* @__PURE__ */ jsx("p", { className: `text-sm leading-snug ${isDone ? "line-through text-muted-foreground" : ""}`, children: task.title }),
680
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
681
- /* @__PURE__ */ jsx(TaskPriorityBadge, { priority: task.priority }),
682
- task.dueDate && /* @__PURE__ */ jsxs("span", { className: `inline-flex items-center gap-1 text-[11px] ${overdue ? "text-destructive font-medium" : dueToday ? "text-warning font-medium" : "text-muted-foreground"}`, children: [
683
- /* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3" }),
684
- formatDueDate(task.dueDate)
685
- ] }),
686
- subtaskCount && subtaskCount.total > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-[11px] text-muted-foreground", children: [
687
- /* @__PURE__ */ jsx(ListChecks, { className: "h-3 w-3" }),
688
- subtaskCount.done,
689
- "/",
690
- subtaskCount.total
691
- ] }),
692
- /* @__PURE__ */ jsx(TaskLabelBadges, { labelIds: task.labels, max: 2 })
693
- ] })
694
- ] }),
695
- task.assignedToName && /* @__PURE__ */ jsx("div", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary", title: task.assignedToName, children: task.assignedToName.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase() })
178
+ /* @__PURE__ */ jsx("p", { className: "shrink-0 border-b border-border/40 px-5 py-2 text-xs text-muted-foreground", children: summaryLine || t("tasks.drawer.subtitle") }),
179
+ /* @__PURE__ */ jsx(SheetBody, { className: "flex flex-col px-0 py-0", children: /* @__PURE__ */ jsx(TasksPanel, {}) })
696
180
  ]
697
181
  }
698
182
  );
699
183
  }
700
- var STATUS_COLORS = {
701
- todo: "bg-muted-foreground",
702
- in_progress: "bg-info",
703
- done: "bg-success",
704
- cancelled: "bg-muted-foreground/60"
705
- };
706
- function TaskStatusGroup({ status, tasks }) {
707
- const t = useTranslation();
708
- const [collapsed, setCollapsed] = useState(status === "cancelled");
709
- if (tasks.length === 0) return null;
710
- const Chevron = collapsed ? ChevronRight : ChevronDown;
711
- return /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
712
- /* @__PURE__ */ jsxs(
713
- "button",
714
- {
715
- type: "button",
716
- onClick: () => setCollapsed(!collapsed),
717
- className: "flex w-full items-center gap-2 px-3 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors",
718
- children: [
719
- /* @__PURE__ */ jsx("span", { className: `h-2 w-2 rounded-full ${STATUS_COLORS[status]}` }),
720
- /* @__PURE__ */ jsx("span", { children: t(`tasks.status.${status}`) }),
721
- /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-normal", children: [
722
- "(",
723
- tasks.length,
724
- ")"
725
- ] }),
726
- /* @__PURE__ */ jsx(Chevron, { className: "ml-auto h-3.5 w-3.5" })
727
- ]
728
- }
729
- ),
730
- !collapsed && /* @__PURE__ */ jsx("div", { children: tasks.map((task) => /* @__PURE__ */ jsx(TaskCard, { task }, task.id)) })
731
- ] });
732
- }
733
-
734
- // src/types.ts
735
- var TASK_STATUSES = ["todo", "in_progress", "done", "cancelled"];
736
- var TASK_PRIORITIES = ["low", "medium", "high", "urgent"];
737
- var STATUS_DOT = {
738
- todo: "bg-muted-foreground",
739
- in_progress: "bg-info",
740
- done: "bg-success",
741
- cancelled: "bg-muted-foreground/60"
742
- };
743
- function formatDateTime(iso) {
744
- const d = new Date(iso);
745
- const sameDay = d.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
746
- return sameDay ? d.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString(void 0, { day: "2-digit", month: "2-digit", year: "2-digit" });
747
- }
748
- function TaskDetail({ taskId }) {
749
- const t = useTranslation();
750
- const tasks = useTasksStore((s) => s.tasks);
751
- const selectTask = useTasksStore((s) => s.selectTask);
752
- const updateTask = useTasksStore((s) => s.updateTask);
753
- const deleteTask = useTasksStore((s) => s.deleteTask);
754
- const task = tasks.find((tt) => tt.id === taskId);
755
- const delegate = useDelegateToAssistant();
756
- const [title, setTitle] = useState("");
757
- const [description, setDescription] = useState("");
758
- const [subtasks, setSubtasks] = useState([]);
759
- useEffect(() => {
760
- if (task) {
761
- setTitle(task.title);
762
- setDescription(task.description ?? "");
763
- }
764
- }, [task]);
765
- useEffect(() => {
766
- setSubtasks([]);
767
- }, [taskId]);
768
- const handleTitleBlur = useCallback(() => {
769
- if (task && title.trim() && title !== task.title) {
770
- updateTask(task.id, { title: title.trim() });
771
- }
772
- }, [task, title, updateTask]);
773
- const handleDescriptionBlur = useCallback(() => {
774
- if (task && description !== (task.description ?? "")) {
775
- updateTask(task.id, { description: description || null });
776
- }
777
- }, [task, description, updateTask]);
778
- const handleDelete = useCallback(() => {
779
- if (task && confirm(t("tasks.detail.deleteConfirm"))) {
780
- deleteTask(task.id);
781
- }
782
- }, [task, deleteTask, t]);
783
- if (!task) {
784
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-muted-foreground", children: [
785
- /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Task not found" }),
786
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => selectTask(null), className: "mt-2 text-xs text-primary hover:underline", children: t("tasks.detail.back") })
787
- ] });
788
- }
789
- return (
790
- // Same frame as the Notes editor: header row with back + icon actions,
791
- // full-bleed px-4 content, sectioned by hairlines, thin timestamp footer.
792
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col", children: [
793
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 border-b border-border/40 px-2 py-1.5", children: [
794
- /* @__PURE__ */ jsxs(
795
- "button",
796
- {
797
- type: "button",
798
- onClick: () => selectTask(null),
799
- className: "inline-flex items-center gap-1 rounded-lg px-1.5 py-1 text-[11.5px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
800
- children: [
801
- /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" }),
802
- t("tasks.drawer.title")
803
- ]
804
- }
805
- ),
806
- /* @__PURE__ */ jsx("span", { className: "flex-1" }),
807
- /* @__PURE__ */ jsx(
808
- "button",
809
- {
810
- type: "button",
811
- onClick: handleDelete,
812
- "aria-label": t("tasks.detail.delete"),
813
- title: t("tasks.detail.delete"),
814
- className: "inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-destructive",
815
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
816
- }
817
- )
818
- ] }),
819
- /* @__PURE__ */ jsx(
820
- "input",
821
- {
822
- type: "text",
823
- value: title,
824
- onChange: (e) => setTitle(e.target.value),
825
- onBlur: handleTitleBlur,
826
- onKeyDown: (e) => {
827
- if (e.key === "Enter") e.currentTarget.blur();
828
- },
829
- className: "bg-transparent px-4 pt-3 text-sm font-semibold text-foreground outline-none"
830
- }
831
- ),
832
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5 px-4 pt-2", children: [
833
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
834
- /* @__PURE__ */ jsx(
835
- "select",
836
- {
837
- value: task.status,
838
- onChange: (e) => updateTask(task.id, { status: e.target.value }),
839
- className: "appearance-none rounded-full border bg-background pl-5 pr-6 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors",
840
- children: TASK_STATUSES.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: t(`tasks.status.${s}`) }, s))
841
- }
842
- ),
843
- /* @__PURE__ */ jsx("span", { className: `absolute left-2 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full ${STATUS_DOT[task.status]}` }),
844
- /* @__PURE__ */ jsx(ChevronDown, { className: "absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" })
845
- ] }),
846
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
847
- /* @__PURE__ */ jsx(
848
- "select",
849
- {
850
- value: task.priority,
851
- onChange: (e) => updateTask(task.id, { priority: e.target.value }),
852
- className: "appearance-none rounded-full border bg-background pl-2 pr-6 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors",
853
- children: TASK_PRIORITIES.map((p) => /* @__PURE__ */ jsx("option", { value: p, children: t(`tasks.priority.${p}`) }, p))
854
- }
855
- ),
856
- /* @__PURE__ */ jsx(ChevronDown, { className: "absolute right-1.5 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" })
857
- ] }),
858
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
859
- /* @__PURE__ */ jsx(
860
- "input",
861
- {
862
- type: "date",
863
- value: task.dueDate ?? "",
864
- onChange: (e) => updateTask(task.id, { dueDate: e.target.value || null }),
865
- className: "appearance-none rounded-full border bg-background pl-6 pr-2 py-1 text-xs font-medium cursor-pointer hover:border-primary/40 transition-colors w-[120px]"
866
- }
867
- ),
868
- /* @__PURE__ */ jsx(Calendar, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none" })
869
- ] }),
870
- task.labels.length > 0 && /* @__PURE__ */ jsx(TaskLabelBadges, { labelIds: task.labels, max: 3 })
871
- ] }),
872
- /* @__PURE__ */ jsx(
873
- "textarea",
874
- {
875
- value: description,
876
- onChange: (e) => setDescription(e.target.value),
877
- onBlur: handleDescriptionBlur,
878
- placeholder: t("tasks.detail.descriptionPlaceholder"),
879
- className: "min-h-[72px] flex-1 resize-none bg-transparent px-4 py-3 text-sm leading-relaxed text-foreground outline-none placeholder:text-muted-foreground/60"
880
- }
881
- ),
882
- /* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsxs(
883
- "button",
884
- {
885
- type: "button",
886
- onClick: () => delegate(
887
- `${t("tasks.detail.chatWithAi.prompt")}
888
-
889
- "${task.title}"${task.description ? `
890
- ${task.description}` : ""}${task.dueDate ? `
891
- (${t("tasks.detail.dueDate")}: ${task.dueDate})` : ""}`
892
- ),
893
- className: "group flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-primary via-primary/80 to-primary bg-[length:200%_100%] bg-left px-3 py-2 text-[13px] font-semibold text-primary-foreground shadow-sm transition-all duration-500 hover:bg-right hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
894
- children: [
895
- /* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 transition-transform duration-300 group-hover:rotate-12" }),
896
- t("tasks.detail.chatWithAi")
897
- ]
898
- }
899
- ) }),
900
- /* @__PURE__ */ jsxs("div", { className: "border-t border-border/40 px-4 py-2.5", children: [
901
- /* @__PURE__ */ jsxs("p", { className: "pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60", children: [
902
- t("tasks.detail.subtasks"),
903
- subtasks.length > 0 ? ` \xB7 ${subtasks.length}` : ""
904
- ] }),
905
- /* @__PURE__ */ jsx("div", { className: "-mx-3", children: /* @__PURE__ */ jsx(TaskQuickAdd, { parentId: task.id }) }),
906
- subtasks.length > 0 && /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/40", children: subtasks.map((st) => /* @__PURE__ */ jsx(TaskCard, { task: st }, st.id)) })
907
- ] }),
908
- /* @__PURE__ */ jsxs("p", { className: "border-t border-border/40 px-4 py-1.5 text-[10px] text-muted-foreground/60", children: [
909
- t("tasks.detail.created"),
910
- " ",
911
- formatDateTime(task.createdAt),
912
- " \xB7 ",
913
- t("tasks.detail.updated"),
914
- " ",
915
- formatDateTime(task.updatedAt)
916
- ] })
917
- ] })
918
- );
919
- }
920
- var STATUS_ORDER = ["todo", "in_progress", "done", "cancelled"];
921
- function TasksPanel() {
922
- const t = useTranslation();
923
- const selectedTaskId = useTasksStore((s) => s.selectedTaskId);
924
- const tasks = useTasksStore((s) => s.tasks);
925
- const tasksLoading = useTasksStore((s) => s.tasksLoading);
926
- const activeFilter = useTasksStore((s) => s.activeFilter);
927
- const fetchTasks = useTasksStore((s) => s.fetchTasks);
928
- const fetchLabels = useTasksStore((s) => s.fetchLabels);
929
- const fetchSummary = useTasksStore((s) => s.fetchSummary);
930
- React4.useEffect(() => {
931
- void fetchTasks();
932
- void fetchLabels();
933
- void fetchSummary();
934
- }, [fetchTasks, fetchLabels, fetchSummary]);
935
- const grouped = React4.useMemo(() => {
936
- const map = /* @__PURE__ */ new Map();
937
- for (const s of STATUS_ORDER) map.set(s, []);
938
- for (const task of tasks) {
939
- const group = map.get(task.status);
940
- if (group) group.push(task);
941
- }
942
- return map;
943
- }, [tasks]);
184
+ function TasksLauncher() {
185
+ const railMounted = useRightRailStore((s) => s.mounted);
944
186
  return /* @__PURE__ */ jsxs(Fragment, { children: [
945
- !selectedTaskId && /* @__PURE__ */ jsx(TasksFilterBar, {}),
946
- !selectedTaskId && /* @__PURE__ */ jsx(TaskQuickAdd, {}),
947
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: selectedTaskId ? /* @__PURE__ */ jsx(TaskDetail, { taskId: selectedTaskId }) : tasksLoading && tasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-16", children: /* @__PURE__ */ jsx("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" }) }) : tasks.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-muted-foreground", children: [
948
- /* @__PURE__ */ jsx(ClipboardList, { className: "mb-3 h-10 w-10 opacity-40" }),
949
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("tasks.empty") }),
950
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs", children: t("tasks.empty.description") })
951
- ] }) : activeFilter !== "all" ? /* @__PURE__ */ jsx("div", { className: "py-1", children: tasks.map((task) => /* @__PURE__ */ jsx("div", { className: "px-2", children: /* @__PURE__ */ jsx(TaskCard, { task }) }, task.id)) }) : /* @__PURE__ */ jsx("div", { className: "space-y-2 py-2", children: STATUS_ORDER.filter((s) => s !== "cancelled" || (grouped.get(s)?.length ?? 0) > 0).map((status) => /* @__PURE__ */ jsx(TaskStatusGroup, { status, tasks: grouped.get(status) ?? [] }, status)) }) })
187
+ /* @__PURE__ */ jsx(TasksLauncherRow, { railMounted }),
188
+ !railMounted && /* @__PURE__ */ jsx(TasksDrawer, {})
952
189
  ] });
953
190
  }
954
- function useTasksSummaryLine() {
191
+ function TasksLauncherRow({ railMounted }) {
955
192
  const t = useTranslation();
193
+ const toggleDrawer = useTasksStore((s) => s.toggleDrawer);
194
+ const fetchSummary = useTasksStore((s) => s.fetchSummary);
956
195
  const summary = useTasksStore((s) => s.summary);
957
- return React4.useMemo(() => {
958
- if (!summary) return "";
959
- const parts = [];
960
- if (summary.overdue > 0) parts.push(`${summary.overdue} ${t("tasks.summary.overdue")}`);
961
- if (summary.dueToday > 0) parts.push(`${summary.dueToday} ${t("tasks.summary.dueToday")}`);
962
- return parts.join(" \xB7 ");
963
- }, [summary, t]);
964
- }
965
- function TasksDrawer() {
966
- const t = useTranslation();
967
- const drawerOpen = useTasksStore((s) => s.drawerOpen);
968
- const closeDrawer = useTasksStore((s) => s.closeDrawer);
969
- const summaryLine = useTasksSummaryLine();
970
- return /* @__PURE__ */ jsx(Sheet, { open: drawerOpen, onOpenChange: (v) => {
971
- if (!v) closeDrawer();
972
- }, children: /* @__PURE__ */ jsxs(SheetContent, { width: "max-w-lg", overlay: "dim", children: [
973
- /* @__PURE__ */ jsxs(SheetHeader, { children: [
974
- /* @__PURE__ */ jsx(SheetTitle, { children: t("tasks.drawer.title") }),
975
- /* @__PURE__ */ jsx(SheetDescription, { children: summaryLine || t("tasks.drawer.subtitle") })
976
- ] }),
977
- /* @__PURE__ */ jsx(SheetBody, { className: "flex flex-col px-0 py-0", children: /* @__PURE__ */ jsx(TasksPanel, {}) })
978
- ] }) });
979
- }
980
- function TasksTopbarButton({ config }) {
981
- const { tasksConfig, tasksProvider, tasksStore } = config;
982
- const t = useTranslation();
983
- const railMounted = useRightRailStore((s) => s.mounted);
984
- const summary = useStore(tasksStore, (s) => s.summary);
985
- const openCount = summary ? summary.todo + summary.inProgress : 0;
986
- const Panel = React4.useMemo(() => {
987
- const RailPanel = () => /* @__PURE__ */ jsx(TasksContextProvider, { config: tasksConfig, provider: tasksProvider, store: tasksStore, children: /* @__PURE__ */ jsx(TasksPanel, {}) });
988
- RailPanel.displayName = "TasksRailPanel";
989
- return RailPanel;
990
- }, [tasksConfig, tasksProvider, tasksStore]);
991
- useRightRailPanel(
992
- React4.useMemo(
993
- () => railMounted ? {
994
- id: "tasks",
995
- label: t("tasks.drawer.title") || "Tasks",
996
- icon: CheckSquare,
997
- order: 20,
998
- badge: { count: openCount, tone: "destructive" },
999
- Component: Panel
1000
- } : null,
1001
- [railMounted, t, Panel, openCount]
1002
- )
1003
- );
1004
- return /* @__PURE__ */ jsxs(TasksContextProvider, { config: tasksConfig, provider: tasksProvider, store: tasksStore, children: [
1005
- /* @__PURE__ */ jsx(TasksTopbarButtonInner, { store: tasksStore, railMounted }),
1006
- !railMounted && /* @__PURE__ */ jsx(TasksDrawer, {})
1007
- ] });
1008
- }
1009
- function TasksTopbarButtonInner({
1010
- store,
1011
- railMounted
1012
- }) {
1013
- const toggleDrawer = useStore(store, (s) => s.toggleDrawer);
1014
- const fetchSummary = useStore(store, (s) => s.fetchSummary);
1015
- const summary = useStore(store, (s) => s.summary);
1016
196
  const togglePanel = useRightRailStore((s) => s.togglePanel);
1017
197
  useEffect(() => {
1018
198
  fetchSummary();
1019
199
  }, [fetchSummary]);
1020
200
  const pendingCount = summary ? summary.todo + summary.inProgress : 0;
201
+ const collapsed = useLayoutStore((s) => s.sidebarCollapsed);
202
+ const surface = useWidgetSurface();
203
+ const compact = collapsed || surface === "topbar";
204
+ const label = t("tasks.drawer.title") || "Tasks";
205
+ const open = () => railMounted ? togglePanel("tasks") : toggleDrawer();
206
+ const railRow = useRailRow(
207
+ surface === "rail" ? { key: "tasks", label, badge: pendingCount > 0 ? pendingCount : void 0 } : null
208
+ );
209
+ if (compact) {
210
+ return /* @__PURE__ */ jsxs(
211
+ "button",
212
+ {
213
+ ...railRow,
214
+ onClick: open,
215
+ "aria-label": label,
216
+ title: surface === "topbar" ? label : void 0,
217
+ className: cn(
218
+ "fz-tap relative flex shrink-0 items-center justify-center rounded-button p-2 text-sidebar-muted transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
219
+ // Na trilha ele é uma linha (ocupa a coluna); na barra, um ícone
220
+ // entre outros, e esticar tomaria a barra inteira.
221
+ surface === "rail" && "w-full"
222
+ ),
223
+ children: [
224
+ /* @__PURE__ */ jsx(CheckSquare, { className: "h-4 w-4" }),
225
+ pendingCount > 0 && /* @__PURE__ */ jsx("span", { className: "absolute right-1 top-1 h-2 w-2 rounded-full bg-destructive" })
226
+ ]
227
+ }
228
+ );
229
+ }
1021
230
  return /* @__PURE__ */ jsxs(
1022
- Button,
231
+ "button",
1023
232
  {
1024
- variant: "ghost",
1025
- size: "icon",
1026
- className: cn("relative text-sidebar-foreground/70 hover:bg-sidebar/30 hover:text-sidebar-foreground"),
1027
- onClick: () => railMounted ? togglePanel("tasks") : toggleDrawer(),
1028
- "aria-label": "Tasks",
233
+ onClick: open,
234
+ "aria-label": label,
235
+ className: "fz-tap flex w-full items-center gap-2 rounded-button px-2.5 py-1.5 text-sm text-sidebar-muted transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
1029
236
  children: [
1030
- /* @__PURE__ */ jsx(CheckSquare, { className: "h-5 w-5" }),
237
+ /* @__PURE__ */ jsx(CheckSquare, { className: "h-4 w-4 shrink-0" }),
238
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: label }),
1031
239
  pendingCount > 0 && /* @__PURE__ */ jsx(
1032
240
  "span",
1033
241
  {
1034
242
  className: cn(
1035
- "absolute -right-0.5 -top-0.5 flex items-center justify-center rounded-full bg-destructive text-destructive-foreground",
1036
- pendingCount > 9 ? "h-5 min-w-5 px-1 text-[10px]" : "h-4 w-4 text-[10px]"
243
+ "flex shrink-0 items-center justify-center rounded-full bg-destructive text-[10px] text-destructive-foreground",
244
+ pendingCount > 9 ? "h-5 min-w-5 px-1" : "h-4 w-4"
1037
245
  ),
1038
246
  children: pendingCount > 99 ? "99+" : pendingCount
1039
247
  }
@@ -1123,8 +331,56 @@ DROP POLICY IF EXISTS plg_tasks_labels_delete ON public.plg_tasks_labels;
1123
331
  CREATE POLICY plg_tasks_labels_delete ON public.plg_tasks_labels FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
1124
332
  GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_tasks_labels TO authenticated;
1125
333
  `;
334
+ var MIGRATION_002_TASKS_TO_SPINE = `-- 002_tasks_to_spine.sql \u2014 carry this plugin's rows into the spine's tasks table.
335
+ --
336
+ -- public.tasks (db migration 047) is now the one table admin work lives in: the
337
+ -- shell's getting-started checklist writes it, so it has to exist in apps that
338
+ -- never installed this plugin. This file moves what plg_tasks_tasks already has.
339
+ --
340
+ -- WHY THE COPY IS HERE AND NOT IN 047. \`fayz db apply\` runs the spine BEFORE
341
+ -- plugins (cli/src/lib/migration-plan.ts). On a fresh pool 047 runs while
342
+ -- plg_tasks_tasks does not exist yet, and a checksum-frozen file cannot try
343
+ -- again later. Here, both tables are guaranteed to exist.
344
+ --
345
+ -- plg_tasks_tasks is NOT dropped. It is retired in place; a later migration
346
+ -- removes it once the fleet has been verified on the new table.
347
+ --
348
+ -- Idempotent.
1126
349
 
1127
- // src/index.ts
350
+ DO $$
351
+ BEGIN
352
+ -- An app on an older @fayz-ai/db has no spine table to copy into. Say so and
353
+ -- leave: the plugin still works against its own table until db is upgraded.
354
+ IF to_regclass('public.tasks') IS NULL THEN
355
+ RAISE NOTICE 'plugin-tasks 002: public.tasks not found \u2014 upgrade @fayz-ai/db to move tasks to the spine';
356
+ RETURN;
357
+ END IF;
358
+
359
+ IF to_regclass('public.plg_tasks_tasks') IS NULL THEN
360
+ RETURN; -- fresh pool: nothing to carry
361
+ END IF;
362
+
363
+ -- The id is preserved deliberately. The notes bridge keys a note's todos by
364
+ -- task id and stores the link as a 'note:<id>' string in \`labels\`; a new id
365
+ -- here would orphan every note todo in the fleet.
366
+ --
367
+ -- Roots before children so the self-referencing parent_id always resolves.
368
+ -- auth.uid() is NULL in a migration session and both of 047's stamping
369
+ -- triggers COALESCE, so these explicit values win.
370
+ INSERT INTO public.tasks (
371
+ id, tenant_id, title, description, status, priority, due_date,
372
+ assigned_to_id, assigned_to_name, parent_id, labels, position,
373
+ created_by, created_by_name, owner_id, created_at, updated_at
374
+ )
375
+ SELECT
376
+ src.id, src.tenant_id, src.title, src.description, src.status, src.priority, src.due_date,
377
+ src.assigned_to_id, src.assigned_to_name, src.parent_id, src.labels, src.position,
378
+ src.created_by_id, src.created_by_name, src.created_by_id, src.created_at, src.updated_at
379
+ FROM public.plg_tasks_tasks src
380
+ ORDER BY src.parent_id NULLS FIRST
381
+ ON CONFLICT (id) DO NOTHING;
382
+ END $$;
383
+ `;
1128
384
  var DEFAULT_LABELS = {
1129
385
  drawerTitle: "Tasks",
1130
386
  settingsTitle: "Tasks",
@@ -1138,11 +394,8 @@ function resolveConfig(options) {
1138
394
  function createTasksPlugin(options) {
1139
395
  const config = resolveConfig(options);
1140
396
  registerTranslations(tasksLocales);
1141
- const provider = options?.dataProvider ?? createSafeDataProvider(
1142
- () => createSupabaseTasksProvider(),
1143
- () => createMockTasksProvider()
1144
- );
1145
- const store = createTasksStore(provider);
397
+ const provider = options?.dataProvider ?? getTasksProvider();
398
+ const store = useTasksStore;
1146
399
  registerNotesTasksAdapter({
1147
400
  async createTodo({ title, noteId, noteTitle }) {
1148
401
  const task = await provider.createTask({
@@ -1169,6 +422,7 @@ function createTasksPlugin(options) {
1169
422
  });
1170
423
  return {
1171
424
  id: "tasks",
425
+ defaultAgentRole: "setup",
1172
426
  name: config.labels.drawerTitle,
1173
427
  icon: "CheckSquare",
1174
428
  version: "1.0.0",
@@ -1180,14 +434,15 @@ function createTasksPlugin(options) {
1180
434
  routes: [],
1181
435
  widgets: [
1182
436
  {
1183
- id: "tasks-topbar-button",
1184
- zone: "shell.topbar.end",
1185
- component: TasksTopbarButton,
437
+ // No pé da trilha, não no topo: tarefas são do workspace, não da
438
+ // tela aberta — e no topo elas dividiam o canto com as ações da página.
439
+ id: "tasks-launcher",
440
+ zone: "shell.sidebar.footer",
441
+ component: TasksLauncher,
1186
442
  order: 10,
1187
443
  props: {
1188
444
  tasksConfig: config,
1189
- tasksProvider: provider,
1190
- tasksStore: store
445
+ tasksProvider: provider
1191
446
  }
1192
447
  }
1193
448
  ],
@@ -1222,15 +477,7 @@ function createTasksPlugin(options) {
1222
477
  id: "tasks",
1223
478
  label: config.labels.settingsTitle,
1224
479
  icon: "CheckSquare",
1225
- component: (() => {
1226
- const Tab = () => React4.createElement(
1227
- TasksContextProvider,
1228
- { config, provider, store },
1229
- React4.createElement(TasksGeneralSettings)
1230
- );
1231
- Tab.displayName = "TasksSettingsTab";
1232
- return Tab;
1233
- })(),
480
+ component: TasksGeneralSettings,
1234
481
  order: 14
1235
482
  }
1236
483
  ],
@@ -1246,6 +493,12 @@ function createTasksPlugin(options) {
1246
493
  version: "1.0.0",
1247
494
  sql: MIGRATION_001_TASKS_BASE,
1248
495
  description: "Create plg_tasks_tasks and plg_tasks_labels"
496
+ },
497
+ {
498
+ id: "tasks-002-to-spine",
499
+ version: "1.1.0",
500
+ sql: MIGRATION_002_TASKS_TO_SPINE,
501
+ description: "Copy plg_tasks_tasks rows into the spine public.tasks (ids preserved; the old table is retired in place, not dropped)"
1249
502
  }
1250
503
  ],
1251
504
  locales: tasksLocales