@listen1954/board 0.1.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.
package/lib/client.js ADDED
@@ -0,0 +1,2374 @@
1
+ window.__ModuleLoader__.load({ id: "@listen1954/board", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ "use strict";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/client/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ apply: () => apply,
26
+ inject: () => inject
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/client/locales.ts
31
+ var NS = "board";
32
+ var zh = {
33
+ name: "\u770B\u677F",
34
+ session: "\u672C\u4F1A\u8BDD",
35
+ sessionColumn: "\u4F1A\u8BDD",
36
+ defaultTodoColumn: "\u5F85\u529E",
37
+ defaultDoingColumn: "\u8FDB\u884C\u4E2D",
38
+ defaultUncategorizedColumn: "\u672A\u5206\u7C7B",
39
+ global: "\u5168\u5C40",
40
+ addColumn: "\u6DFB\u52A0\u5217",
41
+ columnNamePlaceholder: "\u5217\u540D\u79F0\uFF08\u5FC5\u586B\uFF09",
42
+ columnNameRequired: "\u8BF7\u8F93\u5165\u5217\u540D\u79F0",
43
+ enterToCreate: "\u6309 Enter \u521B\u5EFA\uFF0CEsc \u53D6\u6D88",
44
+ addCard: "\u6DFB\u52A0\u5361\u7247",
45
+ addFirstCard: "\u6DFB\u52A0\u7B2C\u4E00\u5F20\u5361\u7247",
46
+ noSessionCards: "\u672C\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u5361\u7247",
47
+ noSessionCardsHint: "\u628A\u8981\u8DDF\u8FDB\u7684\u4E8B\u9879\u653E\u5230\u8FD9\u91CC\uFF0C\u968F\u65F6\u56DE\u6765\u7EE7\u7EED\u5904\u7406\u3002",
48
+ newColumn: "\u65B0\u5217",
49
+ notes: "\u5907\u6CE8",
50
+ reminder: "\u63D0\u9192",
51
+ empty: "\u8FD8\u6CA1\u6709\u5217\u3002\u6DFB\u52A0\u4E00\u5217\u5F00\u59CB\u7528\u3002",
52
+ deleteColumn: "\u5C06\u5220\u9664\u8BE5\u5217\u548C {n} \u5F20\u672A\u5B8C\u6210\u5361\u7247\uFF1B{m} \u5F20\u5386\u53F2\u4EFB\u52A1\u4F1A\u4FDD\u7559\u3002",
53
+ deleteColumnTitle: "\u5220\u9664\u201C{title}\u201D\uFF1F",
54
+ deleteCardTitle: "\u5220\u9664\u201C{title}\u201D\uFF1F",
55
+ deleteWarning: "\u5220\u9664\u540E\u65E0\u6CD5\u6062\u590D\uFF0C\u8BF7\u786E\u8BA4\u8FD9\u662F\u4F60\u60F3\u8981\u7684\u64CD\u4F5C\u3002",
56
+ confirmDelete: "\u786E\u8BA4\u5220\u9664",
57
+ conflict: "\u770B\u677F\u5DF2\u4ECE\u522B\u5904\u66F4\u65B0",
58
+ retry: "\u91CD\u8BD5",
59
+ loadError: "\u770B\u677F\u52A0\u8F7D\u5931\u8D25",
60
+ deleteCard: "\u5220\u9664\u5361\u7247",
61
+ close: "\u5173\u95ED",
62
+ closeBoard: "\u5173\u95ED\u770B\u677F",
63
+ due: "\u5DF2\u5230\u671F",
64
+ clearReminder: "\u6E05\u9664\u63D0\u9192",
65
+ reminderText: "\u63D0\u9192\u8BF4\u660E",
66
+ titlePlaceholder: "\u6807\u9898",
67
+ moveLeft: "\u5DE6\u79FB",
68
+ moveRight: "\u53F3\u79FB",
69
+ deleteColumnMenu: "\u5220\u9664\u5217",
70
+ deleteCardConfirm: "\u5220\u9664\u8FD9\u5F20\u5361\u7247\uFF1F",
71
+ cardDetails: "\u5361\u7247\u8BE6\u60C5",
72
+ notesPlaceholder: "\u5199\u4E0B\u80CC\u666F\u3001\u4E0B\u4E00\u6B65\u6216\u9700\u8981\u8BB0\u4F4F\u7684\u5185\u5BB9\u2026",
73
+ reminderTextPlaceholder: "\u5230\u65F6\u95F4\u540E\u60F3\u63D0\u9192\u81EA\u5DF1\u7684\u8BDD",
74
+ saveReminder: "\u4FDD\u5B58\u63D0\u9192",
75
+ logs: "\u8FC7\u7A0B\u8BB0\u5F55",
76
+ logCount: "{n} \u6761",
77
+ logPlaceholder: "\u8BB0\u5F55\u8FDB\u5EA6\u6216\u5907\u5FD8\u2026",
78
+ submitLog: "\u63D0\u4EA4\u8BB0\u5F55",
79
+ noLogs: "\u8FD8\u6CA1\u6709\u8FC7\u7A0B\u8BB0\u5F55",
80
+ uploadAttachment: "\u4E0A\u4F20\u9644\u4EF6",
81
+ removeAttachment: "\u79FB\u9664\u9644\u4EF6\u201C{name}\u201D",
82
+ attachmentLimit: "\u9644\u4EF6\u6700\u591A 10 \u4E2A\uFF0C\u5355\u4E2A\u4E0D\u8D85\u8FC7 50 MB",
83
+ viewImage: "\u67E5\u770B\u56FE\u7247\u201C{name}\u201D",
84
+ openFile: "\u6253\u5F00\u6587\u4EF6\u201C{name}\u201D",
85
+ imagePlaceholder: "\u56FE\u7247",
86
+ hostRestartRequired: "\u770B\u677F\u5BBF\u4E3B\u4ECD\u662F\u65E7\u7248\u672C\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA DSH\uFF08\u5305\u62EC\u6258\u76D8\uFF09\u540E\u91CD\u65B0\u6253\u5F00",
87
+ back: "\u8FD4\u56DE",
88
+ cancel: "\u53D6\u6D88",
89
+ optionalReminder: "\u63D0\u9192\u65F6\u95F4\uFF08\u53EF\u9009\uFF09",
90
+ taskTime: "\u4EFB\u52A1\u65F6\u95F4\uFF08\u53EF\u9009\uFF09",
91
+ timeReminder: "\u4EFB\u52A1\u65F6\u95F4\u63D0\u9192",
92
+ timeReminderHint: "\u5F00\u59CB\u548C\u5230\u671F\u65F6\u63D0\u9192\u6211",
93
+ taskTimeReminder: "\u4EFB\u52A1\u65F6\u95F4\u63D0\u9192",
94
+ taskStartedReminder: "\u4EFB\u52A1\u5DF2\u5F00\u59CB",
95
+ taskDueReminder: "\u4EFB\u52A1\u5DF2\u5230\u671F",
96
+ startTime: "\u5F00\u59CB\u65F6\u95F4",
97
+ endTime: "\u7ED3\u675F\u65F6\u95F4",
98
+ clearTime: "\u6E05\u9664\u65F6\u95F4",
99
+ invalidTimeRange: "\u7ED3\u675F\u65F6\u95F4\u5FC5\u987B\u665A\u4E8E\u5F00\u59CB\u65F6\u95F4",
100
+ startsIn: "{time}\u540E\u5F00\u59CB",
101
+ startedFor: "\u5DF2\u5F00\u59CB {time}",
102
+ timeRemaining: "\u5269\u4F59 {time}",
103
+ timeOverdue: "\u5DF2\u903E\u671F {time}",
104
+ progressPercent: "{n}%",
105
+ selectDate: "\u9009\u62E9\u65E5\u671F",
106
+ selectStartTime: "\u9009\u62E9\u5F00\u59CB\u65F6\u95F4",
107
+ selectEndTime: "\u9009\u62E9\u7ED3\u675F\u65F6\u95F4",
108
+ today: "\u4ECA\u5929",
109
+ previousMonth: "\u4E0A\u4E2A\u6708",
110
+ nextMonth: "\u4E0B\u4E2A\u6708",
111
+ time: "\u65F6\u95F4",
112
+ viewDetails: "\u67E5\u770B\u8BE6\u60C5",
113
+ settingUpSessionBoard: "\u6B63\u5728\u51C6\u5907\u672C\u4F1A\u8BDD\u770B\u677F\u2026",
114
+ cardCount: "{n} \u5F20\u5361\u7247",
115
+ complete: "\u6807\u8BB0\u5B8C\u6210",
116
+ completedTasks: "\u5DF2\u5B8C\u6210\u4EFB\u52A1",
117
+ completedCount: "\u5DF2\u5B8C\u6210 {n}",
118
+ allColumns: "\u5168\u90E8\u5217",
119
+ filterByColumn: "\u6309\u5217\u7B5B\u9009",
120
+ searchColumns: "\u641C\u7D22\u5217\u2026",
121
+ noMatchingColumns: "\u6CA1\u6709\u5339\u914D\u7684\u5217",
122
+ restoreTo: "\u6062\u590D\u5230",
123
+ restore: "\u6062\u590D",
124
+ selectAll: "\u5168\u9009",
125
+ selectedCount: "\u5DF2\u9009 {n} \u9879",
126
+ selectTask: "\u9009\u62E9\u201C{title}\u201D",
127
+ restoreSelected: "\u6279\u91CF\u6062\u590D",
128
+ deleteSelected: "\u6279\u91CF\u5220\u9664",
129
+ deleteCompletedTitle: "\u5220\u9664 {n} \u4E2A\u5DF2\u5B8C\u6210\u4EFB\u52A1\uFF1F",
130
+ deleteCompletedConfirm: "\u9009\u4E2D\u7684\u5DF2\u5B8C\u6210\u4EFB\u52A1\u5C06\u88AB\u6C38\u4E45\u5220\u9664\u3002",
131
+ noCompletedTasks: "\u8FD8\u6CA1\u6709\u5DF2\u5B8C\u6210\u7684\u4EFB\u52A1",
132
+ reminderDue: "\u770B\u677F\u63D0\u9192\u5230\u671F",
133
+ openTask: "\u70B9\u51FB\u67E5\u770B\u4EFB\u52A1"
134
+ };
135
+ var en = {
136
+ name: "Board",
137
+ session: "This session",
138
+ sessionColumn: "Session",
139
+ defaultTodoColumn: "To do",
140
+ defaultDoingColumn: "In progress",
141
+ defaultUncategorizedColumn: "Uncategorized",
142
+ global: "Global",
143
+ addColumn: "Add column",
144
+ columnNamePlaceholder: "Column name (required)",
145
+ columnNameRequired: "Enter a column name",
146
+ enterToCreate: "Enter to create \xB7 Esc to cancel",
147
+ addCard: "Add card",
148
+ addFirstCard: "Add first card",
149
+ noSessionCards: "No cards in this session",
150
+ noSessionCardsHint: "Add something to follow up and return to it anytime.",
151
+ newColumn: "New column",
152
+ notes: "Notes",
153
+ reminder: "Reminder",
154
+ empty: "No columns yet. Add one to get started.",
155
+ deleteColumn: "Delete this column and {n} active cards? {m} completed tasks will be kept.",
156
+ deleteColumnTitle: "Delete \u201C{title}\u201D?",
157
+ deleteCardTitle: "Delete \u201C{title}\u201D?",
158
+ deleteWarning: "This cannot be undone. Please confirm this is what you want.",
159
+ confirmDelete: "Delete",
160
+ conflict: "Board was updated elsewhere",
161
+ retry: "Retry",
162
+ loadError: "Failed to load board",
163
+ deleteCard: "Delete card",
164
+ close: "Close",
165
+ closeBoard: "Close board",
166
+ due: "Due",
167
+ clearReminder: "Clear reminder",
168
+ reminderText: "Reminder note",
169
+ titlePlaceholder: "Title",
170
+ moveLeft: "Move left",
171
+ moveRight: "Move right",
172
+ deleteColumnMenu: "Delete column",
173
+ deleteCardConfirm: "Delete this card?",
174
+ cardDetails: "Card details",
175
+ notesPlaceholder: "Add context, next steps, or anything worth keeping\u2026",
176
+ reminderTextPlaceholder: "What should this reminder say?",
177
+ saveReminder: "Save reminder",
178
+ logs: "Activity log",
179
+ logCount: "{n} entries",
180
+ logPlaceholder: "Record progress or a memo\u2026",
181
+ submitLog: "Add log",
182
+ noLogs: "No activity yet",
183
+ uploadAttachment: "Upload attachment",
184
+ removeAttachment: "Remove attachment \u201C{name}\u201D",
185
+ attachmentLimit: "Up to 10 files, 50 MB each",
186
+ viewImage: "View image \u201C{name}\u201D",
187
+ openFile: "Open file \u201C{name}\u201D",
188
+ imagePlaceholder: "IMG",
189
+ hostRestartRequired: "The Board host is outdated. Fully quit DSH, including the tray app, then reopen it.",
190
+ back: "Back",
191
+ cancel: "Cancel",
192
+ optionalReminder: "Reminder (optional)",
193
+ taskTime: "Task schedule (optional)",
194
+ timeReminder: "Task time notifications",
195
+ timeReminderHint: "Notify me when the task starts and is due",
196
+ taskTimeReminder: "Task time notification",
197
+ taskStartedReminder: "Task started",
198
+ taskDueReminder: "Task due",
199
+ startTime: "Start time",
200
+ endTime: "End time",
201
+ clearTime: "Clear time",
202
+ invalidTimeRange: "End time must be later than start time",
203
+ startsIn: "Starts in {time}",
204
+ startedFor: "Started {time} ago",
205
+ timeRemaining: "{time} remaining",
206
+ timeOverdue: "{time} overdue",
207
+ progressPercent: "{n}%",
208
+ selectDate: "Select date",
209
+ selectStartTime: "Select start time",
210
+ selectEndTime: "Select end time",
211
+ today: "Today",
212
+ previousMonth: "Previous month",
213
+ nextMonth: "Next month",
214
+ time: "Time",
215
+ viewDetails: "View details",
216
+ settingUpSessionBoard: "Preparing this session\u2019s board\u2026",
217
+ cardCount: "{n} cards",
218
+ complete: "Mark complete",
219
+ completedTasks: "Completed tasks",
220
+ completedCount: "{n} completed",
221
+ allColumns: "All columns",
222
+ filterByColumn: "Filter by column",
223
+ searchColumns: "Search columns\u2026",
224
+ noMatchingColumns: "No matching columns",
225
+ restoreTo: "Restore to",
226
+ restore: "Restore",
227
+ selectAll: "Select all",
228
+ selectedCount: "{n} selected",
229
+ selectTask: "Select \u201C{title}\u201D",
230
+ restoreSelected: "Restore selected",
231
+ deleteSelected: "Delete selected",
232
+ deleteCompletedTitle: "Delete {n} completed tasks?",
233
+ deleteCompletedConfirm: "The selected completed tasks will be permanently deleted.",
234
+ noCompletedTasks: "No completed tasks yet",
235
+ reminderDue: "Board reminder due",
236
+ openTask: "Click to view task"
237
+ };
238
+
239
+ // src/client/styles.ts
240
+ var TAG = "@listen/board/ui.css";
241
+ var CSS = `
242
+ .board-chip, .board-side, .board-btn, .board-iconbtn, .board-tab, .board-card, .board-col-title, .board-date-trigger, .board-calendar-day, .board-calendar-nav { box-sizing: border-box; font: inherit; -webkit-tap-highlight-color: transparent; }
243
+ .board-editor { position:relative; }
244
+ .board-editor,.board-editor * { box-sizing:border-box; }
245
+
246
+ .board-toast-stack { position:fixed; z-index:240; top:18px; right:18px; display:flex; flex-direction:column; gap:8px; width:min(360px,calc(100vw - 36px)); pointer-events:none; }.board-toast { display:flex; overflow:hidden; border:1px solid var(--dsw-alias-border-l3,rgb(0 0 0 / 16%)); border-radius:12px; background:var(--dsw-alias-bg-layer-1,#fff); color:var(--dsw-alias-label-primary,#17191c); box-shadow:var(--dsw-shadow-lv3,0 12px 32px rgb(0 0 0 / 18%)); pointer-events:auto; animation:board-toast-in .2s ease-out; }.board-toast-main { display:flex; align-items:flex-start; flex:1; min-width:0; gap:9px; padding:11px 8px 11px 12px; border:0; background:transparent; color:inherit; cursor:pointer; text-align:left; }.board-toast-mark { flex:none; display:grid; place-items:center; width:24px; height:24px; border-radius:50%; background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#a56500) 14%,transparent); color:var(--dsw-alias-state-warn-primary,#a56500); font-size:14px; }.board-toast-copy { display:flex; flex-direction:column; min-width:0; gap:2px; }.board-toast-copy strong { color:var(--dsw-alias-state-warn-primary,#a56500); font-size:11px; line-height:16px; }.board-toast-copy span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px; font-weight:650; line-height:19px; }.board-toast-copy small { display:-webkit-box; overflow:hidden; color:var(--dsw-alias-label-secondary,#62676d); font-size:11px; line-height:16px; white-space:pre-wrap; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.board-toast-close { flex:none; width:30px; height:30px; margin:5px 5px 0 0; padding:0; border:0; border-radius:7px; background:transparent; color:var(--dsw-alias-label-tertiary,#898f96); cursor:pointer; font-size:17px; }.board-toast-close:hover { background:var(--dsw-alias-interactive-bg-hover,rgb(0 0 0 / 5%)); color:var(--dsw-alias-label-primary,#17191c); }@keyframes board-toast-in { from { opacity:0; transform:translateY(-8px) scale(.98); } to { opacity:1; transform:none; } }
247
+
248
+ /* Prefer dsh semantic tokens; fallbacks give thin client assemblies a complete dark theme. */
249
+ .board-global, .board-overlay, .board-session-detail-layer, .board-date-popover-portal, .board-select-portal, .board-toast-stack, .board-image-preview {
250
+ --board-bg: var(--dsw-alias-bg-layer-1, #fff); --board-surface: var(--dsw-alias-bg-layer-2, #fff); --board-subtle: var(--dsw-alias-bg-layer-2, #f6f7f8); --board-raised: var(--dsw-alias-bg-layer-1, #fff);
251
+ --board-text: var(--dsw-alias-label-primary, #17191c); --board-muted: var(--dsw-alias-label-secondary, #62676d); --board-dim: var(--dsw-alias-label-tertiary, #898f96);
252
+ --board-border: var(--dsw-alias-border-l2, rgb(0 0 0 / 10%)); --board-border-strong: var(--dsw-alias-border-l3, rgb(0 0 0 / 18%));
253
+ --board-hover: var(--dsw-alias-interactive-bg-hover, rgb(0 0 0 / 5%)); --board-active: var(--dsw-alias-button-ghost-active-fill, rgb(0 0 0 / 8%));
254
+ --board-brand: var(--dsw-alias-button-primary-fill, #17191c); --board-brand-hover: var(--dsw-alias-button-primary-hover, #30343a); --board-brand-text: var(--dsw-alias-label-primary-foreground, #fff);
255
+ --board-danger: var(--dsw-alias-state-error-primary, #c63c32); --board-warn: var(--dsw-alias-state-warn-primary, #a56500); --board-warn-bg: var(--dsw-alias-state-warn-tertiary, #fff5db);
256
+ color: var(--board-text); color-scheme: light;
257
+ }
258
+ body[data-ds-dark-theme] .board-global, body[data-ds-dark-theme] .board-overlay, body[data-ds-dark-theme] .board-session-detail-layer, body[data-ds-dark-theme] .board-date-popover-portal, body[data-ds-dark-theme] .board-select-portal, body[data-ds-dark-theme] .board-toast-stack, body[data-ds-dark-theme] .board-image-preview { --board-bg: var(--dsw-alias-bg-layer-1, #171719); --board-surface: var(--dsw-alias-bg-layer-2, #242427); --board-subtle: var(--dsw-alias-bg-layer-2, #242427); --board-raised: var(--dsw-alias-bg-layer-1, #1d1d20); --board-text: var(--dsw-alias-label-primary, #f4f5f6); --board-muted: var(--dsw-alias-label-secondary, #c7cbd0); --board-dim: var(--dsw-alias-label-tertiary, #9da3aa); --board-border: var(--dsw-alias-border-l2, rgb(255 255 255 / 12%)); --board-border-strong: var(--dsw-alias-border-l3, rgb(255 255 255 / 20%)); --board-hover: var(--dsw-alias-interactive-bg-hover, rgb(255 255 255 / 8%)); --board-active: var(--dsw-alias-button-ghost-active-fill, rgb(255 255 255 / 12%)); --board-brand: var(--dsw-alias-button-primary-fill, #f3f4f5); --board-brand-hover: var(--dsw-alias-button-primary-hover, #fff); --board-brand-text: var(--dsw-alias-label-primary-foreground, #171719); --board-warn-bg: var(--dsw-alias-state-warn-tertiary, rgb(122 83 20 / 30%)); color-scheme:dark; }
259
+ .board-global ::selection,.board-overlay ::selection,.board-session-detail-layer ::selection,.board-date-popover-portal ::selection,.board-select-portal ::selection,.board-toast-stack ::selection { background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4176e6) 32%,transparent); color:var(--board-text); }
260
+
261
+ .board-chip, .board-side { position:relative; display:inline-flex; align-items:center; justify-content:center; gap:5px; border:0; border-radius:9px; background:transparent; color:var(--dsw-alias-label-secondary,#62676d); cursor:pointer; font-size:13px; line-height:20px; transition:color .15s ease,background .15s ease; }
262
+ .board-chip { min-width:28px; height:28px; padding:0 7px; }.board-chip:hover,.board-side:hover { color:var(--dsw-alias-label-primary,#17191c); background:var(--dsw-alias-interactive-bg-hover,rgb(0 0 0 / 5%)); }.board-chip[aria-pressed="true"] { color:var(--dsw-alias-label-primary,#17191c); background:var(--dsw-alias-button-ghost-active-fill,rgb(0 0 0 / 8%)); }.board-count { min-width:14px; font-variant-numeric:tabular-nums; font-size:11px; font-weight:650; }.board-dot { width:6px; height:6px; border-radius:50%; background:var(--dsw-alias-state-warn-primary,#a56500); box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1,#fff); }.board-side { width:100%; justify-content:flex-start; min-height:36px; padding:0 12px; }.board-side.rail { width:40px; height:40px; min-height:40px; padding:0; }
263
+
264
+ .board-overlay { pointer-events:auto; position:absolute; inset:auto 8px calc(100% + 8px); z-index:90; display:flex; flex-direction:column; width:auto; height:min(420px,calc(100vh - 100px)); min-height:320px; overflow:hidden; border:1px solid var(--board-border); border-radius:16px; background:var(--board-surface); box-shadow:var(--dsw-shadow-lv3,0 18px 50px rgb(0 0 0 / 18%)); }.board-global { pointer-events:auto; position:absolute; inset:0 0 0 auto; z-index:40; display:flex; flex-direction:column; min-width:0; background:var(--board-bg); border-left:1px solid var(--board-border); }.board-global-close { position:absolute; z-index:80; top:10px; right:48px; display:inline-flex; align-items:center; gap:5px; min-height:30px; padding:0 10px 0 7px; border:1px solid var(--board-border); border-radius:15px; background:var(--board-surface); color:var(--board-muted); box-shadow:0 1px 4px rgb(0 0 0 / 9%); cursor:pointer; font:inherit; font-size:11px; font-weight:600; line-height:18px; transition:background .15s ease,color .15s ease,border-color .15s ease; }.board-global-close:hover { border-color:var(--board-border-strong); background:var(--board-hover); color:var(--board-text); }.board-global-close-mark { display:grid; place-items:center; width:16px; height:16px; border-radius:50%; background:var(--board-active); font-size:17px; font-weight:400; line-height:1; }
265
+ .board-bar { display:flex; align-items:center; gap:8px; min-height:52px; padding:8px 12px 8px 16px; border-bottom:1px solid var(--board-border); background:var(--board-surface); flex:none; }.board-bar strong { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:15px; line-height:22px; font-weight:600; }.board-bar .spacer { flex:1; min-width:8px; }.board-tabs { display:inline-flex; align-items:center; gap:2px; padding:3px; border-radius:10px; background:var(--board-subtle); }
266
+ .board-tab,.board-btn,.board-iconbtn,.board-calendar-nav { display:inline-flex; align-items:center; justify-content:center; min-height:30px; border:0; border-radius:8px; padding:0 10px; background:transparent; color:var(--board-muted); cursor:pointer; font-size:13px; line-height:20px; transition:background .15s ease,color .15s ease,border-color .15s ease; }.board-tab:hover,.board-btn:not(:disabled):hover,.board-iconbtn:not(:disabled):hover,.board-calendar-nav:hover { background:var(--board-hover); color:var(--board-text); }.board-tab[aria-selected="true"] { background:var(--board-raised); color:var(--board-text); box-shadow:0 1px 2px rgb(0 0 0 / 8%); font-weight:600; }.board-btn { gap:5px; white-space:nowrap; }.board-btn.primary { background:var(--board-brand); color:var(--board-brand-text); font-weight:600; }.board-btn.primary:hover:not(:disabled) { background:var(--board-brand-hover); color:var(--board-brand-text); }.board-btn.danger { color:var(--board-danger); }.board-btn:disabled,.board-iconbtn:disabled { cursor:not-allowed; opacity:.42; }.board-iconbtn,.board-calendar-nav { flex:none; width:30px; padding:0; font-size:18px; line-height:1; }.board-body { flex:1; min-height:0; overflow:hidden; padding:14px; }
267
+
268
+ .board-editor,.board-workspace { min-width:0; height:100%; }.board-workspace { display:grid; grid-template-columns:minmax(0,1fr) minmax(280px,360px); overflow:hidden; background:transparent; }.board-canvas { min-width:0; overflow:auto; padding:0; overscroll-behavior:contain; }.board-canvas:only-child { grid-column:1 / -1; }.board-cols { display:flex; align-items:flex-start; gap:18px; min-width:max-content; min-height:100%; }.board-col { display:flex; flex:0 0 296px; flex-direction:column; min-width:296px; max-width:296px; gap:8px; padding:0; border:0; border-radius:0; background:transparent; transition:opacity .15s ease,transform .15s ease; }.board-col.column-dragging { opacity:.3; transform:scale(.97); }.board-col-head { display:flex; align-items:center; min-width:0; gap:2px; padding:0 2px 5px; }.board-col-grip { flex:none; width:16px; color:var(--board-dim); cursor:grab; font-size:15px; line-height:20px; text-align:center; }.board-col-grip:active { cursor:grabbing; }.board-col-title { flex:1; min-width:0; height:30px; overflow:hidden; padding:0 6px; border:1px solid transparent; border-radius:7px; background:transparent; color:var(--board-text); cursor:text; text-align:left; text-overflow:ellipsis; white-space:nowrap; font-weight:650; font-size:14px; }.board-col-title:hover { background:var(--board-hover); }.board-col-count { display:inline-grid; place-items:center; flex:none; min-width:20px; height:20px; margin-right:4px; padding:0 5px; border-radius:10px; background:var(--board-active); color:var(--board-muted); font-size:11px; font-weight:650; font-variant-numeric:tabular-nums; }.board-card-list { display:flex; flex-direction:column; gap:8px; min-width:0; }.board-session-single .board-cols { min-width:0; min-height:0; }.board-session-single .board-col { flex:1 1 auto; width:100%; min-width:0; max-width:none; min-height:0; }.board-session-single .board-col-head { display:none; }.board-session-single .board-card-list { flex-direction:row; align-items:flex-start; gap:10px; overflow:auto; padding:2px 2px 10px; }.board-session-single .board-card { flex:0 0 260px; }.board-session-single .board-add { flex:none; align-self:flex-start; width:auto; min-height:34px; margin:0; padding:0 12px; }
269
+ .board-col-title-input,.board-card-title,.board-field,.board-detail textarea,.board-time-input { box-sizing:border-box; width:100%; border:1px solid var(--board-border); border-radius:8px; outline:none; background:var(--board-raised); color:var(--board-text); font:inherit; font-size:13px; line-height:20px; transition:border-color .15s ease,box-shadow .15s ease; }.board-col-title-input { height:30px; padding:4px 6px; font-weight:620; }.board-card-title { min-height:36px; padding:7px 8px; }.board-field { min-height:34px; padding:6px 8px; }.board-col-title-input:focus,.board-card-title:focus,.board-field:focus,.board-detail textarea:focus,.board-time-input:focus,.board-date-trigger:focus-visible { border-color:var(--dsw-alias-brand-primary,#5c8df6); box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 20%,transparent); }
270
+ .board-col-title-input::placeholder,.board-card-title::placeholder,.board-field::placeholder,.board-detail textarea::placeholder,.board-time-input::placeholder,.board-card-notes::placeholder,.board-col-name-field::placeholder { color:var(--board-dim); opacity:1; }
271
+ .board-select { width:100%; min-width:0; }.board-select-trigger { display:flex; align-items:center; justify-content:space-between; width:100%; min-width:0; min-height:2.125rem; gap:.5rem; padding:.375rem .625rem; border:1px solid var(--board-border); border-radius:.5rem; outline:0; background:var(--board-raised); color:var(--board-text); cursor:pointer; font:inherit; font-size:.8125rem; line-height:1.25rem; text-align:left; transition:border-color .15s ease,background .15s ease,box-shadow .15s ease; }.board-select-trigger:hover { border-color:var(--board-border-strong); background:var(--board-surface); }.board-select-trigger:focus-visible,.board-select-trigger[aria-expanded="true"],.board-select-combobox { border-color:var(--dsw-alias-brand-primary,#5c8df6); box-shadow:0 0 0 .1875rem color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 18%,transparent); }.board-select-trigger > span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-select-trigger > .board-ui-icon { flex:none; width:.875rem; height:.875rem; color:var(--board-dim); transition:transform .15s ease; }.board-select-trigger[aria-expanded="true"] > .board-ui-icon,.board-select-combobox > .board-ui-icon:last-child { transform:rotate(180deg); }.board-select-combobox { cursor:text; }.board-select-combobox > .board-ui-icon:first-child { width:.75rem; height:.75rem; transform:none; }.board-select-combobox input { flex:1; min-width:0; border:0; outline:0; background:transparent; color:var(--board-text); font:inherit; font-size:inherit; line-height:inherit; }.board-select-combobox input::placeholder { color:var(--board-dim); opacity:1; }
272
+ .board-select-portal { position:fixed; z-index:330; box-sizing:border-box; min-width:8rem; max-height:14rem; overflow-y:auto; overscroll-behavior:contain; padding:.3125rem; border:1px solid var(--board-border-strong); border-radius:.625rem; background:var(--board-surface); color:var(--board-text); box-shadow:var(--dsw-shadow-lv3,0 .75rem 2rem rgb(0 0 0 / 22%)); animation:board-select-in .12s ease-out; }.board-select-option { display:flex; align-items:center; width:100%; min-width:0; min-height:2rem; gap:.5rem; padding:.375rem .5rem; border:0; border-radius:.4375rem; background:transparent; color:var(--board-muted); cursor:pointer; font:inherit; font-size:.75rem; line-height:1.125rem; text-align:left; }.board-select-option:hover,.board-select-option:focus-visible,.board-select-option.active { outline:0; background:var(--board-hover); color:var(--board-text); }.board-select-option[aria-selected="true"] { background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 13%,transparent); color:var(--board-text); font-weight:600; }.board-select-option[aria-selected="true"]::after { flex:none; margin-left:auto; color:var(--dsw-alias-brand-primary,#5c8df6); content:'\u2713'; }.board-select-option > span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-select-empty { padding:.75rem .5rem; color:var(--board-dim); font-size:.75rem; line-height:1.125rem; text-align:center; }@keyframes board-select-in { from { opacity:0; transform:translateY(-.25rem) scale(.98); } to { opacity:1; transform:none; } }
273
+ .board-card { position:relative; display:flex; flex-direction:column; width:100%; min-width:0; min-height:92px; overflow:hidden; border:1px solid var(--board-border); border-radius:10px; padding:0; background:var(--board-raised); color:var(--board-text); box-shadow:0 1px 2px rgb(0 0 0 / 4%); transition:opacity .22s ease,transform .18s ease,border-color .15s ease,background .15s ease,box-shadow .15s ease; }.board-card:hover { border-color:var(--board-border-strong); background:var(--board-surface); box-shadow:0 4px 12px rgb(0 0 0 / 8%); transform:translateY(-1px); }.board-card.dragging { opacity:.28; border-style:dashed; transform:scale(.97); }.board-card.completing { opacity:0; transform:scale(.96) translateY(5px); pointer-events:none; }.board-card-main { flex:1; width:100%; min-width:0; padding:10px 28px 3px 11px; border:0; outline:0; background:transparent; color:inherit; cursor:pointer; text-align:left; font:inherit; }.board-card-main:focus-visible { outline:2px solid var(--dsw-alias-brand-primary,#5c8df6); outline-offset:-2px; }.board-card .title { display:flex; align-items:flex-start; gap:6px; min-width:0; font-size:14px; line-height:21px; font-weight:620; }.board-card .title-text { display:-webkit-box; overflow:hidden; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.board-card .preview { display:-webkit-box; overflow:hidden; margin-top:4px; color:var(--board-dim); font-size:12px; line-height:18px; overflow-wrap:anywhere; white-space:pre-wrap; -webkit-box-orient:vertical; -webkit-line-clamp:5; }.board-card-delete { position:absolute; z-index:2; top:5px; right:5px; display:grid; place-items:center; width:20px; height:20px; padding:0; border:0; border-radius:5px; background:transparent; color:var(--board-dim); cursor:pointer; font:inherit; font-size:15px; opacity:0; transition:opacity .15s ease,background .15s ease,color .15s ease; }.board-card:hover .board-card-delete,.board-card-delete:focus-visible { opacity:.65; }.board-card-delete:hover { background:color-mix(in srgb,var(--board-danger) 10%,transparent); color:var(--board-danger); opacity:1; }.board-card-foot { display:flex; align-items:center; gap:6px; margin-top:auto; padding:6px 8px 7px 11px; color:var(--board-dim); font-size:11px; line-height:16px; }.board-card-date { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-card-date.due { color:var(--board-warn); font-weight:600; }.board-card-open,.board-complete { display:inline-flex; align-items:center; flex:none; min-height:24px; padding:0 6px; border:0; border-radius:6px; background:transparent; color:var(--board-dim); cursor:pointer; font:inherit; font-size:11px; white-space:nowrap; }.board-card-open { margin-left:auto; }.board-card-open:hover { background:var(--board-hover); color:var(--board-text); }.board-complete { gap:5px; border:1px solid var(--board-border); background:var(--board-surface); color:var(--board-muted); font-weight:600; }.board-complete:hover { border-color:var(--dsw-alias-state-success-primary,#27835b); background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#27835b) 10%,var(--board-surface)); color:var(--dsw-alias-state-success-primary,#27835b); }.board-complete:disabled { cursor:default; opacity:.6; }.board-complete-mark { position:relative; box-sizing:border-box; width:13px; height:13px; border:1.5px solid currentColor; border-radius:50%; }.board-complete:hover .board-complete-mark { border-color:var(--dsw-alias-state-success-primary,#27835b); background:var(--dsw-alias-state-success-primary,#27835b); }.board-complete:hover .board-complete-mark::after { position:absolute; top:1px; left:3px; width:4px; height:7px; border:solid white; border-width:0 1.5px 1.5px 0; content:''; transform:rotate(45deg); }.board-add { justify-content:flex-start; width:100%; min-height:34px; margin-top:2px; border:1px dashed var(--board-border-strong); color:var(--board-muted); }.board-add:hover { border-color:var(--dsw-alias-brand-primary,#5c8df6); }.board-col-drop { min-height:8px; border-radius:6px; transition:min-height .15s ease,background .15s ease; }.board-col-drop.accepting-card { min-height:34px; border:1px dashed var(--board-border); background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 7%,transparent); }.board-col:has(.board-card.dragging) .board-col-drop { background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 12%,transparent); }.board-card-composer { display:flex; flex-direction:column; gap:8px; width:100%; padding:10px; border:1px solid var(--board-border-strong); border-radius:10px; background:var(--board-raised); box-shadow:0 6px 18px rgb(0 0 0 / 8%); }.board-card-notes { box-sizing:border-box; width:100%; min-height:72px; padding:7px 8px; resize:vertical; border:1px solid var(--board-border); border-radius:8px; outline:none; background:var(--board-raised); color:var(--board-text); font:inherit; font-size:12px; line-height:18px; }.board-card-notes:focus { border-color:var(--dsw-alias-brand-primary,#5c8df6); box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 20%,transparent); }.board-composer-label { display:flex; flex-direction:column; gap:5px; color:var(--board-muted); font-size:11px; line-height:16px; }.board-composer-actions { display:flex; justify-content:flex-end; gap:6px; }
274
+ .board-empty { display:flex; flex-direction:column; align-items:flex-start; justify-content:center; gap:12px; min-height:180px; padding:24px; color:var(--board-muted); font-size:13px; line-height:20px; }.board-empty .board-btn { margin-top:2px; }.board-new-col { min-height:120px; padding-top:35px; }.board-add-column,.board-open-archive { justify-content:flex-start; min-height:42px; border:1px dashed var(--board-border-strong); color:var(--board-muted); }.board-add-column { width:100%; }.board-add-column:hover { border-color:var(--dsw-alias-brand-primary,#5c8df6); background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 7%,transparent); }.board-open-archive { border-style:solid; background:var(--board-raised); }.board-open-archive:hover { border-color:var(--board-border-strong); background:var(--board-hover); color:var(--board-text); }.board-archive-fab { position:absolute; z-index:75; right:18px; bottom:18px; min-height:36px; border-color:var(--board-border); border-radius:18px; padding:0 12px; box-shadow:0 4px 14px rgb(0 0 0 / 12%); }.board-banner { display:flex; align-items:center; gap:8px; padding:9px 12px; border:1px solid color-mix(in srgb,var(--board-warn) 32%,transparent); border-radius:10px; background:var(--board-warn-bg); color:var(--board-warn); font-size:13px; line-height:20px; }.board-error { display:flex; flex-direction:column; align-items:flex-start; gap:10px; padding:20px; color:var(--board-danger); }.board-error .preview { max-width:100%; overflow-wrap:anywhere; color:var(--board-muted); font-size:12px; }
275
+
276
+ .board-detail { display:flex; flex-direction:column; min-width:0; overflow:auto; padding:16px; border-left:1px solid var(--board-border); background:var(--board-surface); overscroll-behavior:contain; }.board-detail-header { display:flex; align-items:center; gap:8px; margin-bottom:14px; }.board-detail-heading { flex:1; min-width:0; color:var(--board-muted); font-size:12px; font-weight:600; line-height:18px; }.board-detail textarea { min-height:112px; padding:8px 9px; resize:vertical; }.board-field-label { display:flex; flex-direction:column; gap:6px; margin:14px 0 0; color:var(--board-muted); font-size:12px; line-height:18px; font-weight:550; }.board-field-label.first { margin-top:0; }.board-reminder-row { display:grid; grid-template-columns:minmax(0,1fr) 102px; gap:8px; }.board-time-input { height:34px; padding:5px 7px; color-scheme:inherit; }.board-time-input:disabled,.board-field:disabled { opacity:.5; }.board-detail-actions { display:flex; flex-wrap:wrap; gap:7px; margin-top:16px; padding-top:14px; border-top:1px solid var(--board-border); }.board-detail-actions .board-btn:first-child { margin-right:auto; }.board-due { color:var(--board-warn); font-weight:600; }.board-archive-empty { padding:28px 4px; color:var(--board-dim); font-size:12px; text-align:center; }.board-archive-list { display:flex; flex-direction:column; gap:10px; margin-top:14px; }.board-archive-card { display:flex; flex-direction:column; gap:7px; padding:11px; border:1px solid var(--board-border); border-radius:10px; background:var(--board-raised); }.board-archive-card strong { overflow-wrap:anywhere; font-size:13px; line-height:19px; }.board-archive-card .preview { display:-webkit-box; overflow:hidden; color:var(--board-dim); font-size:11px; line-height:17px; overflow-wrap:anywhere; white-space:pre-wrap; -webkit-box-orient:vertical; -webkit-line-clamp:4; }.board-archive-meta { display:flex; justify-content:space-between; gap:8px; color:var(--board-dim); font-size:10px; line-height:15px; }.board-restore-row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:6px; padding-top:7px; border-top:1px solid var(--board-border); color:var(--board-muted); font-size:11px; }.board-restore-row .board-field { min-height:30px; padding:4px 6px; font-size:11px; }.board-restore-row .board-btn { min-height:30px; padding:0 8px; font-size:11px; }
277
+ .board-date-picker { position:relative; min-width:0; }.board-date-trigger { display:flex; align-items:center; justify-content:space-between; gap:6px; width:100%; height:34px; padding:0 9px; overflow:hidden; border:1px solid var(--board-border); border-radius:8px; background:var(--board-raised); color:var(--board-text); cursor:pointer; text-align:left; font-size:13px; line-height:20px; }.board-date-trigger:hover { border-color:var(--board-border-strong); }.board-date-trigger .calendar-mark { flex:none; color:var(--board-dim); }.board-date-popover { position:absolute; z-index:120; top:calc(100% + 6px); left:0; width:282px; padding:10px; border:1px solid var(--board-border); border-radius:12px; background:var(--board-surface); box-shadow:var(--dsw-shadow-lv3,0 12px 32px rgb(0 0 0 / 18%)); }.board-calendar-head { display:flex; align-items:center; justify-content:space-between; gap:8px; padding-bottom:8px; }.board-calendar-month { color:var(--board-text); font-size:13px; font-weight:620; }.board-calendar-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:2px; }.board-calendar-weekday { display:grid; place-items:center; height:26px; color:var(--board-dim); font-size:11px; }.board-calendar-day { display:grid; place-items:center; height:32px; padding:0; border:0; border-radius:8px; background:transparent; color:var(--board-text); cursor:pointer; font-size:12px; }.board-calendar-day:hover { background:var(--board-hover); }.board-calendar-day.muted { color:var(--board-dim); }.board-calendar-day.today { box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary,#5c8df6); }.board-calendar-day.selected { background:var(--board-brand); color:var(--board-brand-text); box-shadow:none; font-weight:650; }.board-calendar-footer { display:flex; justify-content:flex-end; margin-top:8px; padding-top:8px; border-top:1px solid var(--board-border); }
278
+
279
+ .board-col-delete { flex:none; display:grid; place-items:center; width:24px; height:24px; padding:0; border:0; border-radius:6px; background:transparent; color:var(--board-dim); cursor:pointer; font:inherit; font-size:16px; opacity:.45; transition:opacity .15s ease,background .15s ease,color .15s ease; }.board-col-head:hover .board-col-delete,.board-col-delete:focus-visible { opacity:.75; }.board-col-delete:hover { background:color-mix(in srgb,var(--board-danger) 10%,transparent); color:var(--board-danger); opacity:1; }
280
+ .board-time-fieldset { display:flex; flex-direction:column; gap:7px; min-width:0; margin:0; padding:8px; border:1px solid var(--board-border); border-radius:9px; }.board-time-fieldset.detail { margin-top:.625rem; padding:.375rem .5rem; gap:.25rem; }.board-time-fieldset legend { padding:0 4px; color:var(--board-muted); font-size:11px; font-weight:600; }.board-datetime-label { display:block; min-width:0; color:var(--board-muted); font-size:11px; line-height:16px; }.board-datetime-row { display:grid; grid-template-columns:1.25rem minmax(0,1fr) 92px 26px; align-items:center; gap:6px; min-width:0; }.board-datetime-icon { display:grid; place-items:center; color:var(--board-dim); }.board-datetime-icon .board-ui-icon { width:.875rem; height:.875rem; }.board-time-clear { display:grid; place-items:center; width:26px; height:34px; padding:0; border:0; border-radius:7px; background:transparent; color:var(--board-dim); cursor:pointer; font:inherit; font-size:16px; }.board-time-clear:not(:disabled):hover { background:var(--board-hover); color:var(--board-text); }.board-time-clear:disabled { cursor:default; opacity:.25; }.board-field-error { color:var(--board-danger); font-size:11px; line-height:16px; }
281
+ .board-time-progress { --board-time-color:var(--dsw-alias-state-business-primary,#4f7bd9); margin:6px 11px 1px; color:var(--board-time-color); }.board-time-progress.warning { --board-time-color:var(--dsw-alias-state-warn-label,#a56500); }.board-time-progress.urgent { --board-time-color:var(--dsw-alias-state-error-secondary,#d5573f); }.board-time-progress.overdue { --board-time-color:var(--board-danger); }.board-time-progress.active { --board-time-color:var(--dsw-alias-state-success-primary,#27835b); }.board-time-status,.board-time-bounds { display:flex; align-items:center; justify-content:space-between; gap:8px; min-width:0; }.board-time-status { font-size:10px; line-height:15px; font-weight:650; }.board-time-status span:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-time-track { height:5px; margin:4px 0; overflow:hidden; border-radius:3px; background:color-mix(in srgb,var(--board-time-color) 16%,var(--board-subtle)); }.board-time-track span { display:block; min-width:2px; height:100%; border-radius:inherit; background:var(--board-time-color); transition:width .3s ease; }.board-time-bounds { color:var(--board-dim); font-size:9px; line-height:14px; }.board-time-bounds span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-time-bounds span:last-child { margin-left:auto; text-align:right; }
282
+ .board-confirm-backdrop { position:absolute; z-index:210; inset:0; display:grid; place-items:center; padding:20px; background:rgb(10 12 16 / 36%); backdrop-filter:blur(2px); animation:board-confirm-in .15s ease-out; }.board-confirm { display:grid; grid-template-columns:34px minmax(0,1fr); gap:11px; width:min(390px,100%); padding:16px; border:1px solid var(--board-border-strong); border-radius:14px; background:var(--board-surface); color:var(--board-text); box-shadow:var(--dsw-shadow-lv3,0 18px 48px rgb(0 0 0 / 24%)); }.board-confirm-mark { display:grid; place-items:center; width:32px; height:32px; border-radius:50%; background:color-mix(in srgb,var(--board-danger) 12%,transparent); color:var(--board-danger); font-size:16px; font-weight:750; }.board-confirm-copy { min-width:0; }.board-confirm-copy strong { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; line-height:21px; }.board-confirm-copy p { margin:5px 0 2px; color:var(--board-muted); font-size:12px; line-height:18px; }.board-confirm-copy small { color:var(--board-dim); font-size:10px; line-height:15px; }.board-confirm-actions { grid-column:1 / -1; display:flex; justify-content:flex-end; gap:7px; margin-top:3px; }.board-btn.confirm-danger { background:var(--board-danger); color:white; font-weight:650; }.board-btn.confirm-danger:hover:not(:disabled) { background:color-mix(in srgb,var(--board-danger) 84%,black); color:white; }@keyframes board-confirm-in { from { opacity:0; } to { opacity:1; } }
283
+ @media (max-width:740px) { .board-global { left:0 !important; border-left:0; }.board-global .board-workspace { grid-template-columns:minmax(0,1fr); }.board-global .board-detail { max-height:330px; border-top:1px solid var(--board-border); border-left:0; } }.board-overlay .board-body { padding:10px; }.board-overlay .board-workspace { grid-template-columns:minmax(0,1fr) minmax(250px,300px); }.board-overlay .board-detail { max-height:none; border-top:0; border-left:1px solid var(--board-border); }.board-overlay .board-col { flex-basis:280px; min-width:280px; max-width:280px; } @media (max-width:620px) { .board-overlay .board-workspace { grid-template-columns:minmax(0,1fr); }.board-overlay .board-detail { max-height:300px; border-top:1px solid var(--board-border); border-left:0; } } @media (max-width:520px) { .board-overlay { right:0; left:0; border-radius:12px 12px 0 0; }.board-bar { min-height:48px; padding:7px 8px; }.board-tab { padding:0 7px; }.board-bar .board-btn.primary { padding:0 8px; }.board-date-popover { right:0; left:auto; width:min(282px,calc(100vw - 32px)); } } @media (prefers-reduced-motion:reduce) { .board-chip,.board-side,.board-btn,.board-iconbtn,.board-card,.board-date-trigger { transition:none; } }
284
+ .board-global-close { right:64px; }
285
+ .board-session-badge { flex:none; padding:1px 5px; border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#4f7bd9) 28%,var(--board-border)); border-radius:5px; background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f7bd9) 8%,transparent); color:var(--dsw-alias-brand-primary,#4f7bd9); font-size:9px; font-weight:650; line-height:14px; }
286
+ .board-overlay { height:clamp(12rem,30vh,16rem); min-height:0; max-height:calc(100vh - 4rem); }
287
+ .board-session-single .board-workspace { position:relative; display:block; }
288
+ .board-session-single .board-canvas { height:100%; }
289
+ .board-session-single .board-cols,.board-session-single .board-col { height:100%; }
290
+ .board-session-single .board-col { min-width:0; max-width:none; }
291
+ .board-session-single .board-card { flex-basis:clamp(12rem,22vw,14rem); }
292
+ .board-session-single .board-card-list { flex:0 0 auto; min-height:auto; overflow-x:auto; overflow-y:hidden; overscroll-behavior:contain; scroll-behavior:smooth; }
293
+ .board-session-empty { display:flex; flex:1; flex-direction:column; align-items:center; justify-content:center; min-height:0; gap:.25rem; padding:1rem; color:var(--board-muted); text-align:center; }.board-session-empty-mark { display:grid; place-items:center; width:2.5rem; height:2.5rem; margin-bottom:.25rem; border-radius:50%; background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 14%,transparent); color:var(--dsw-alias-brand-primary,#5c8df6); }.board-session-empty strong { color:var(--board-text); font-size:.875rem; line-height:1.25rem; }.board-session-empty > span:not(.board-session-empty-mark) { font-size:.75rem; line-height:1.125rem; }.board-session-empty .board-btn { min-height:2.25rem; margin-top:.5rem; padding:0 1rem; }
294
+ .board-session-single .board-detail { position:absolute; z-index:110; inset:0 0 0 auto; box-sizing:border-box; width:min(20rem,calc(100% - 2rem)); max-height:none; border-top:0; border-left:1px solid var(--board-border); background:var(--board-surface); box-shadow:-.625rem 0 1.75rem rgb(0 0 0 / 14%); animation:board-detail-drawer-in .18s ease-out; }
295
+ .board-session-detail-layer { position:fixed; z-index:200; inset:0; box-sizing:border-box; display:flex; justify-content:flex-end; min-width:0; background:transparent; color:var(--board-text); pointer-events:none; }
296
+ .board-session-detail-drawer { width:clamp(22rem,34vw,28rem); height:100%; min-width:0; border-left:1px solid var(--board-border); background:var(--board-surface); box-shadow:-.75rem 0 2rem rgb(0 0 0 / 16%); animation:board-detail-drawer-in .18s ease-out; pointer-events:auto; }
297
+ .board-session-detail-layer .board-detail { box-sizing:border-box; width:100%; height:100%; border-left:0; }
298
+ .board-archive-fab { display:grid; place-items:center; align-content:center; justify-content:center; box-sizing:border-box; width:2.25rem; min-width:2.25rem; height:2.25rem; min-height:2.25rem; max-height:2.25rem; padding:0; line-height:0; }
299
+ .board-history-icon { display:block; width:1.125rem; height:1.125rem; margin:0; overflow:visible; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
300
+ .board-new-column-editor { box-sizing:border-box; display:flex; flex-direction:column; gap:.4rem; width:100%; padding:.6rem; border:1px solid var(--board-border-strong); border-radius:.65rem; background:var(--board-raised); box-shadow:0 .375rem 1.125rem rgb(0 0 0 / 8%); }
301
+ .board-col-name-field { box-sizing:border-box; width:100%; min-height:2.25rem; padding:.45rem .55rem; border:1px solid var(--board-border); border-radius:.5rem; outline:none; background:var(--board-raised); color:var(--board-text); font:inherit; font-size:.8125rem; line-height:1.25rem; }
302
+ .board-col-name-field:focus { border-color:var(--dsw-alias-brand-primary,#5c8df6); box-shadow:0 0 0 .1875rem color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 20%,transparent); }
303
+ .board-col-name-field.invalid { border-color:var(--board-danger); }
304
+ .board-new-column-hint { display:flex; align-items:center; justify-content:space-between; gap:.5rem; color:var(--board-dim); font-size:.6875rem; line-height:1rem; }
305
+ .board-new-column-hint .board-btn { min-height:1.75rem; padding:0 .5rem; font-size:.6875rem; }
306
+ .board-composer-main,.board-composer-schedule,.board-detail-main,.board-detail-schedule { display:contents; }
307
+ .board-overlay .board-bar { min-height:2.25rem; padding:.25rem .5rem .25rem .75rem; }
308
+ .board-overlay .board-bar strong { font-size:.8125rem; line-height:1.125rem; }
309
+ .board-overlay .board-bar .board-iconbtn { width:1.75rem; min-height:1.75rem; }
310
+ .board-overlay .board-body { padding:.5rem; }
311
+ .board-session-single .board-card-composer { order:-1; display:grid; grid-template-columns:minmax(13rem,.8fr) minmax(24rem,1.2fr); align-items:start; gap:.5rem .625rem; box-sizing:border-box; width:min(100%,55rem); padding:.5rem; }
312
+ .board-session-single .board-composer-main,.board-session-single .board-composer-schedule { display:flex; flex-direction:column; min-width:0; gap:.375rem; }
313
+ .board-session-single .board-card-title { min-height:2rem; padding:.3125rem .5rem; }
314
+ .board-session-single .board-card-notes { min-height:3.25rem; max-height:4.5rem; padding:.375rem .5rem; }
315
+ .board-session-single .board-time-fieldset { display:flex; flex-direction:column; gap:.25rem; padding:.375rem .5rem; }
316
+ .board-session-single .board-datetime-row { grid-template-columns:1.25rem minmax(0,1fr) 7.25rem 1.5rem; width:100%; gap:.25rem; }
317
+ .board-session-single .board-date-trigger span:first-child { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
318
+ .board-session-single .board-composer-label { gap:.1875rem; }
319
+ .board-session-single .board-composer-label .board-reminder-row { grid-template-columns:minmax(0,1fr) 7.25rem; width:100%; gap:.25rem; }
320
+ .board-session-single .board-composer-actions { grid-column:1 / -1; gap:.375rem; }
321
+ .board-session-single .board-composer-actions .board-btn { min-height:1.75rem; padding:0 .625rem; font-size:.75rem; }
322
+ .board-session-detail-layer .board-detail { position:relative; display:flex; overflow:hidden; padding:.75rem 1rem; }
323
+ .board-session-detail-layer .board-archive { overflow-y:auto; }
324
+ .board-session-detail-layer .board-detail-header { min-height:1.875rem; margin-bottom:.5rem; padding-right:2.25rem; }
325
+ .board-session-detail-layer .board-detail-header .board-iconbtn { position:absolute; z-index:2; top:.625rem; right:.625rem; width:1.875rem; height:1.875rem; min-height:1.875rem; }
326
+ .board-session-detail-layer .board-detail-editor { display:flex; flex:0 1 auto; flex-direction:column; min-height:0; overflow-y:auto; }
327
+ .board-session-detail-layer .board-detail-main,.board-session-detail-layer .board-detail-schedule { display:contents; }
328
+ .board-session-detail-layer .board-field-label { margin-top:.625rem; gap:.25rem; }
329
+ .board-session-detail-layer .board-field-label.first { margin-top:0; }
330
+ .board-session-detail-layer .board-notes-field textarea { min-height:7rem; }
331
+ .board-session-detail-layer .board-detail-actions { margin-top:.625rem; padding-top:.625rem; }
332
+ .board-log-section { display:flex; flex:1; flex-direction:column; min-height:7rem; overflow:hidden; gap:.375rem; margin-top:.75rem; padding-top:.75rem; border-top:1px solid var(--board-border); }
333
+ .board-log-heading { display:flex; align-items:center; justify-content:space-between; gap:.5rem; }.board-log-heading strong { color:var(--board-text); font-size:.8125rem; line-height:1.25rem; }.board-log-heading span { color:var(--board-dim); font-size:.6875rem; }
334
+ .board-log-scroll { flex:1; min-height:0; overflow-y:auto; overscroll-behavior:contain; }.board-log-composer { position:static; flex:none; padding:.25rem; border:1px solid var(--board-border); border-radius:.5rem; background:var(--board-surface); }.board-log-compose-row { display:flex; align-items:center; min-width:0; gap:.1875rem; }.board-log-input { flex:1; min-width:0; height:1.625rem; padding:.1875rem .3125rem; border:0; outline:0; background:transparent; color:var(--board-text); font:inherit; font-size:.75rem; line-height:1rem; }.board-log-input::placeholder { color:var(--board-dim); opacity:1; }.board-log-compose-row > input[type=file] { display:none; }.board-log-compose-row .board-btn { min-height:1.625rem; padding:0 .5rem; font-size:.6875rem; }.board-log-compose-row .board-log-upload { width:1.625rem; min-height:1.625rem; }
335
+ .board-log-pending { display:flex; flex-wrap:wrap; gap:.375rem; margin-bottom:.375rem; padding-bottom:.375rem; border-bottom:1px solid var(--board-border); }.board-log-pending > span { display:flex; align-items:center; min-width:0; max-width:100%; gap:.25rem; padding:.1875rem .25rem .1875rem .4375rem; border:1px solid var(--board-border); border-radius:.375rem; color:var(--board-muted); font-size:.6875rem; line-height:1rem; }.board-log-pending > span > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.board-log-pending button { display:grid; place-items:center; flex:none; width:1.125rem; height:1.125rem; padding:0; border:0; border-radius:.25rem; background:transparent; color:var(--board-dim); cursor:pointer; }.board-log-pending button:hover { background:var(--board-hover); color:var(--board-text); }.board-log-pending .board-ui-icon { width:.6875rem; height:.6875rem; }
336
+ .board-log-empty { padding:.625rem .5rem; color:var(--board-dim); font-size:.75rem; text-align:center; }.board-log-list { display:flex; flex-direction:column; }.board-log-item { position:relative; margin-left:.25rem; padding:0 0 .875rem 1rem; border-left:1px solid var(--board-border); }.board-log-item:last-child { padding-bottom:.125rem; }.board-log-item::before { position:absolute; top:.25rem; left:-.25rem; width:.4375rem; height:.4375rem; border:2px solid var(--board-surface); border-radius:50%; background:var(--dsw-alias-state-business-primary,#4176e6); content:''; }.board-log-item time { display:block; margin-bottom:.25rem; color:var(--board-dim); font-size:.625rem; line-height:.875rem; }.board-log-item p { margin:0; color:var(--board-text); font-size:.75rem; line-height:1.125rem; overflow-wrap:anywhere; white-space:pre-wrap; }
337
+ .board-log-attachments { display:flex; flex-wrap:wrap; gap:.375rem; margin-top:.4375rem; }.board-log-attachments button { display:flex; align-items:center; min-width:0; max-width:100%; gap:.375rem; min-height:1.75rem; padding:.25rem .5rem; border:1px solid var(--board-border); border-radius:.375rem; background:var(--board-raised); color:var(--board-muted); cursor:pointer; }.board-log-attachments button:hover { border-color:var(--board-border-strong); background:var(--board-hover); color:var(--board-text); }.board-log-attachments span { overflow:hidden; font-size:.6875rem; text-overflow:ellipsis; white-space:nowrap; }.board-log-attachments small { flex:none; color:var(--board-dim); font-size:.5625rem; }
338
+ .board-log-attachments .board-log-image { display:grid; grid-template-columns:minmax(0,1fr) auto; width:7.5rem; padding:.25rem; text-align:left; }.board-log-image img,.board-log-image-placeholder { grid-column:1 / -1; display:grid; place-items:center; width:100%; height:4.5rem; overflow:hidden; border-radius:.25rem; background:var(--board-subtle); color:var(--board-dim); object-fit:cover; font-size:.625rem; font-weight:700; }.board-log-image > span:not(.board-log-image-placeholder) { min-width:0; padding:.125rem .125rem 0; }.board-log-image > small { padding:.125rem .125rem 0; }.board-log-file::before { display:block; flex:none; width:.625rem; height:.75rem; border:1px solid currentColor; border-radius:.125rem; content:''; opacity:.65; }
339
+ .board-image-preview { position:fixed; z-index:320; inset:0; display:grid; place-items:center; padding:2rem; background:rgb(0 0 0 / 72%); }.board-image-preview-frame { position:relative; display:flex; flex-direction:column; align-items:center; max-width:min(90vw,75rem); max-height:90vh; gap:.5rem; }.board-image-preview-frame > img { display:block; max-width:100%; max-height:calc(90vh - 2.5rem); border-radius:.5rem; object-fit:contain; box-shadow:0 1rem 3rem rgb(0 0 0 / 35%); }.board-image-preview-frame > span { max-width:min(80vw,48rem); overflow:hidden; color:#fff; font-size:.75rem; text-overflow:ellipsis; white-space:nowrap; }.board-image-preview-frame > .board-iconbtn { position:absolute; z-index:2; top:.5rem; right:.5rem; border-radius:50%; background:rgb(0 0 0 / 55%); color:#fff; }.board-image-preview-frame > .board-iconbtn:hover { background:rgb(0 0 0 / 75%); color:#fff; }
340
+ .board-archive-selectbar { display:flex; align-items:center; justify-content:space-between; gap:.5rem; margin-top:.625rem; padding:.375rem .125rem; border-bottom:1px solid var(--board-border); color:var(--board-muted); font-size:.6875rem; line-height:1rem; }
341
+ .board-archive-selectbar label,.board-archive-card-check { display:flex; align-items:center; min-width:0; gap:.4375rem; cursor:pointer; }
342
+ .board-archive-selectbar input,.board-archive-card-check input { flex:none; width:.875rem; height:.875rem; margin:0; accent-color:var(--dsw-alias-brand-primary,#5c8df6); }
343
+ .board-archive-batch { position:sticky; z-index:2; top:-1rem; display:grid; grid-template-columns:minmax(0,1fr) auto auto; align-items:center; gap:.375rem; margin:.5rem 0 0; padding:.5rem; border:1px solid var(--board-border-strong); border-radius:.625rem; background:var(--board-surface); box-shadow:0 .25rem .75rem rgb(0 0 0 / 8%); }
344
+ .board-archive-batch .board-field { min-width:0; }
345
+ .board-batch-delete { color:var(--board-danger); }
346
+ .board-archive-card.selected { border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 50%,var(--board-border)); background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 6%,var(--board-raised)); }
347
+ .board-archive-card-head { display:flex; align-items:flex-start; gap:.375rem; min-width:0; }
348
+ .board-archive-card-check { flex:1; align-items:flex-start; }
349
+ .board-archive-card-check input { margin-top:.1875rem; }
350
+ .board-archive-card-check strong { min-width:0; }
351
+ .board-archive-delete { display:grid; place-items:center; flex:none; width:1.625rem; height:1.625rem; padding:0; border:0; border-radius:.375rem; background:transparent; color:var(--board-dim); cursor:pointer; opacity:.55; }
352
+ .board-archive-delete:hover { background:color-mix(in srgb,var(--board-danger) 10%,transparent); color:var(--board-danger); opacity:1; }
353
+ .board-archive-delete > .board-ui-icon { width:.8125rem; height:.8125rem; }
354
+ .board-archive-batch .board-select-trigger { min-width:0; }
355
+ .board-restore-row .board-select-trigger { min-height:1.875rem; padding:.25rem .375rem; font-size:.6875rem; }
356
+ .board-date-popover-portal { position:fixed; z-index:260; right:auto; box-sizing:border-box; width:min(16.5rem,calc(100vw - 1rem)); max-height:calc(100vh - 1rem); overflow:auto; padding:.5rem; color:var(--board-text); }
357
+ .board-date-popover-portal .board-calendar-head { padding-bottom:.375rem; }
358
+ .board-date-popover-portal .board-calendar-weekday { height:1.375rem; }
359
+ .board-date-popover-portal .board-calendar-day { height:1.75rem; border-radius:.375rem; }
360
+ .board-date-popover-portal .board-calendar-footer { margin-top:.375rem; padding-top:.375rem; }
361
+ .board-history-icon,.board-chip svg { display:block; flex:none; }
362
+ .board-ui-icon { display:block; flex:none; width:1rem; height:1rem; overflow:visible; fill:none; stroke:currentColor; stroke-width:1.75; stroke-linecap:round; stroke-linejoin:round; }
363
+ .board-iconbtn,.board-card-delete,.board-col-delete,.board-time-clear,.board-toast-close,.board-calendar-nav,.board-global-close-mark,.board-confirm-mark,.calendar-mark,.board-col-grip { display:grid; place-items:center; line-height:0; }
364
+ .board-iconbtn > .board-ui-icon,.board-card-delete > .board-ui-icon,.board-col-delete > .board-ui-icon,.board-time-clear > .board-ui-icon,.board-toast-close > .board-ui-icon { width:.875rem; height:.875rem; }
365
+ .board-calendar-nav > .board-ui-icon { width:.875rem; height:.875rem; }
366
+ .board-global-close-mark > .board-ui-icon { width:.75rem; height:.75rem; }
367
+ .calendar-mark { flex:none; width:1rem; height:1rem; }
368
+ .calendar-mark > .board-ui-icon { width:.6875rem; height:.6875rem; }
369
+ .board-confirm-mark > .board-ui-icon { width:1.125rem; height:1.125rem; stroke-width:2; }
370
+ .board-card-date { display:inline-flex; align-items:center; gap:.1875rem; }
371
+ .board-card-date > .board-ui-icon { width:.75rem; height:.75rem; }
372
+ .board-card-open { gap:.1875rem; }
373
+ .board-card-open > .board-ui-icon { width:.75rem; height:.75rem; }
374
+ .board-col-grip > .board-ui-icon { width:.875rem; height:.875rem; fill:currentColor; stroke:none; }
375
+ .board-toast-mark { display:grid; place-items:center; line-height:0; }
376
+ .board-toast-mark > .board-ui-icon { width:1rem; height:1rem; }
377
+ .board-btn > .board-ui-icon { width:.875rem; height:.875rem; }
378
+
379
+ /* Kanban layout rules: columns own their contents; creation controls never bleed into adjacent lanes. */
380
+ .board-cols { gap:1rem; padding:.125rem; }
381
+ .board-col { flex-basis:19rem; min-width:19rem; max-width:19rem; padding:.625rem; overflow:hidden; border:1px solid var(--board-border); border-radius:.75rem; background:var(--board-surface); box-shadow:0 .0625rem .125rem rgb(0 0 0 / 5%); transition:border-color .15s ease,box-shadow .15s ease,opacity .15s ease,transform .15s ease; }
382
+ .board-col:hover { border-color:var(--board-border-strong); box-shadow:0 .25rem .875rem rgb(0 0 0 / 6%); }
383
+ .board-col-head { min-height:1.75rem; padding:0 .125rem .375rem; border-bottom:1px solid var(--board-border); }
384
+ .board-card-composer { width:100%; min-width:0; padding:.625rem; overflow:hidden; }
385
+ .board-composer-head { display:flex; align-items:center; justify-content:space-between; min-width:0; min-height:1.75rem; margin:-.25rem -.1875rem 0 .0625rem; padding-bottom:.375rem; border-bottom:1px solid var(--board-border); }
386
+ .board-composer-head strong { overflow:hidden; color:var(--board-text); font-size:.75rem; font-weight:650; line-height:1.125rem; text-overflow:ellipsis; white-space:nowrap; }
387
+ .board-composer-head .board-iconbtn { width:1.625rem; min-height:1.625rem; }
388
+ .board-card-title,.board-field,.board-card-notes,.board-time-fieldset,.board-reminder-row,.board-datetime-row { min-width:0; max-width:100%; }
389
+ .board-datetime-row { grid-template-columns:1.25rem minmax(0,1fr) 5rem 1.375rem; gap:.25rem; }
390
+ .board-reminder-row { grid-template-columns:minmax(0,1fr) 5rem; gap:.375rem; }
391
+ .board-date-trigger { padding:0 .5rem; font-size:.75rem; white-space:nowrap; }
392
+ .board-time-input { min-width:0; padding:.3125rem .375rem; font-size:.75rem; }
393
+ .board-time-clear { width:1.375rem; }
394
+ .board-time-reminder { display:flex; align-items:center; gap:.5rem; margin-top:.125rem; padding:.375rem .125rem 0; border-top:1px solid var(--board-border); color:var(--board-text); cursor:pointer; }.board-time-reminder input { flex:none; width:.875rem; height:.875rem; margin:0; accent-color:var(--dsw-alias-brand-primary,#5c8df6); }.board-time-reminder input:disabled { opacity:.45; }.board-time-reminder:has(input:disabled) { cursor:default; opacity:.6; }.board-time-reminder > span { display:flex; min-width:0; align-items:baseline; gap:.375rem; }.board-time-reminder strong { font-size:.75rem; line-height:1rem; }.board-time-reminder small { color:var(--board-dim); font-size:.625rem; line-height:.875rem; }
395
+ .board-new-col,.board-overlay .board-new-col { flex-basis:16rem; min-width:16rem; max-width:16rem; min-height:0; padding:0 0 0 .25rem; overflow:visible; border:0; border-radius:0; background:transparent; box-shadow:none; }
396
+ .board-new-col:hover { border-color:transparent; box-shadow:none; }
397
+ .board-add-column { width:100%; min-height:2.75rem; padding:0 .875rem; border-radius:.625rem; background:color-mix(in srgb,var(--board-surface) 72%,transparent); }
398
+ .board-add-column:hover { background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5c8df6) 7%,var(--board-surface)); }
399
+ .board-session-single .board-col { padding:0; overflow:visible; border:0; border-radius:0; background:transparent; box-shadow:none; }
400
+ .board-session-single .board-col:hover { border-color:transparent; box-shadow:none; }
401
+ /* Session creation is a focused, full-overlay flow instead of an inline lane card. */
402
+ .board-overlay:has(.board-session-single .board-card-composer) { height:clamp(19rem,42vh,22rem); }
403
+ .board-overlay:has(.board-session-single .board-card-composer) .board-bar { display:none; }
404
+ .board-overlay:has(.board-session-single .board-card-composer) .board-body { padding:0; }
405
+ .board-session-single .board-card-composer { position:absolute; z-index:120; inset:0; display:grid; grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr); grid-template-rows:minmax(0,1fr) auto; align-items:stretch; width:100%; max-width:none; height:100%; padding:.75rem; overflow:auto; border:0; border-radius:0; background:var(--board-surface); box-shadow:none; }
406
+ .board-session-single .board-composer-main,.board-session-single .board-composer-schedule { height:100%; min-height:0; padding-top:.125rem; }
407
+ .board-session-single .board-card-notes { flex:1; min-height:5rem; max-height:none; }
408
+ .board-session-single .board-composer-actions { grid-column:1 / -1; align-items:center; justify-content:space-between; min-height:2.5rem; margin:0 -.125rem -.125rem; padding:.5rem .125rem 0; border-top:1px solid var(--board-border); }
409
+ @supports (height:100dvh) { .board-overlay { height:clamp(12rem,30dvh,16rem); max-height:calc(100dvh - 4rem); } }
410
+ @keyframes board-detail-drawer-in { from { opacity:0; transform:translateX(18px); } to { opacity:1; transform:none; } }
411
+ @media (prefers-reduced-motion:reduce) { .board-session-single .board-detail { animation:none; } }
412
+ @media (max-width:760px) { .board-session-single .board-card-composer { grid-template-columns:minmax(0,1fr); grid-template-rows:auto auto auto; width:100%; }.board-session-single .board-composer-actions { grid-column:1; }.board-session-detail-drawer { width:100vw; } }
413
+ @media (prefers-reduced-motion:reduce) { .board-session-detail-layer { animation:none; } }
414
+ `;
415
+ function ensureBoardStyles() {
416
+ if (typeof document === "undefined") return;
417
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(TAG)}]`) !== null) return;
418
+ const tag = document.createElement("style");
419
+ tag.dataset.plugin = "@listen/board";
420
+ tag.dataset.pluginCss = TAG;
421
+ tag.textContent = CSS;
422
+ document.head.appendChild(tag);
423
+ }
424
+
425
+ // src/client/SessionChip.tsx
426
+ var import_react2 = require("react");
427
+
428
+ // src/client/state.ts
429
+ var overlayBySession = /* @__PURE__ */ new Map();
430
+ var overlayListeners = /* @__PURE__ */ new Set();
431
+ var globalPanel = { open: false, sidebarWide: true };
432
+ var globalListeners = /* @__PURE__ */ new Set();
433
+ var reminderToasts = [];
434
+ var reminderListeners = /* @__PURE__ */ new Set();
435
+ var reminderToastTimers = /* @__PURE__ */ new Map();
436
+ var MAX_VISIBLE_REMINDER_TOASTS = 5;
437
+ function overlayOf(sessionId) {
438
+ const current = overlayBySession.get(sessionId);
439
+ if (current !== void 0) return current;
440
+ const initial = { open: false, tab: "session" };
441
+ overlayBySession.set(sessionId, initial);
442
+ return initial;
443
+ }
444
+ function getOverlay(sessionId) {
445
+ return overlayOf(sessionId);
446
+ }
447
+ function subscribeOverlay(fn) {
448
+ overlayListeners.add(fn);
449
+ return () => {
450
+ overlayListeners.delete(fn);
451
+ };
452
+ }
453
+ function toggleOverlay(sessionId) {
454
+ const state = overlayOf(sessionId);
455
+ overlayBySession.set(sessionId, { ...state, open: !state.open, tab: state.open ? state.tab : "session" });
456
+ overlayListeners.forEach((listener) => listener());
457
+ }
458
+ function setOverlayOpen(sessionId, open) {
459
+ const state = overlayOf(sessionId);
460
+ if (state.open === open) return;
461
+ overlayBySession.set(sessionId, { ...state, open, tab: open ? "session" : state.tab });
462
+ overlayListeners.forEach((listener) => listener());
463
+ }
464
+ function getGlobalPanel() {
465
+ return globalPanel;
466
+ }
467
+ function subscribeGlobalPanel(fn) {
468
+ globalListeners.add(fn);
469
+ return () => {
470
+ globalListeners.delete(fn);
471
+ };
472
+ }
473
+ function toggleGlobalPanel() {
474
+ globalPanel = { ...globalPanel, open: !globalPanel.open };
475
+ globalListeners.forEach((listener) => listener());
476
+ }
477
+ function setGlobalPanelOpen(open) {
478
+ if (globalPanel.open === open) return;
479
+ globalPanel = { ...globalPanel, open };
480
+ globalListeners.forEach((listener) => listener());
481
+ }
482
+ function setSidebarWide(wide) {
483
+ if (globalPanel.sidebarWide === wide) return;
484
+ globalPanel = { ...globalPanel, sidebarWide: wide };
485
+ globalListeners.forEach((listener) => listener());
486
+ }
487
+ function getReminderToasts() {
488
+ return reminderToasts;
489
+ }
490
+ function subscribeReminderToasts(fn) {
491
+ reminderListeners.add(fn);
492
+ return () => reminderListeners.delete(fn);
493
+ }
494
+ function dismissReminderToast(id) {
495
+ const timer = reminderToastTimers.get(id);
496
+ if (timer !== void 0) globalThis.clearTimeout(timer);
497
+ reminderToastTimers.delete(id);
498
+ const next = reminderToasts.filter((toast) => toast.id !== id);
499
+ if (next.length === reminderToasts.length) return;
500
+ reminderToasts = next;
501
+ reminderListeners.forEach((listener) => listener());
502
+ scheduleVisibleReminderToasts();
503
+ }
504
+ function publishBoardReminders(board, sessionId) {
505
+ const seen = new Set(seenReminderIds());
506
+ let seenChanged = false;
507
+ if (board.scope === "global") {
508
+ const existing = boardReminderEventIds(board);
509
+ for (const id of seen) {
510
+ if (existing.has(id)) continue;
511
+ seen.delete(id);
512
+ seenChanged = true;
513
+ }
514
+ }
515
+ const additions = [];
516
+ for (const card of board.cards) {
517
+ if (card.archivedAt !== null || !card.timeReminder.enabled) continue;
518
+ const events = [
519
+ card.startAt !== null && card.timeReminder.startFiredAt !== void 0 ? { kind: "start", at: card.startAt, firedAt: card.timeReminder.startFiredAt } : null,
520
+ card.endAt !== null && card.timeReminder.endFiredAt !== void 0 ? { kind: "end", at: card.endAt, firedAt: card.timeReminder.endFiredAt } : null
521
+ ];
522
+ for (const event of events) {
523
+ if (event === null) continue;
524
+ const id = reminderEventId(card.id, event.kind, event.firedAt);
525
+ if (seen.has(id)) continue;
526
+ seen.add(id);
527
+ seenChanged = true;
528
+ const ownerSessionId = board.columns.find((column) => column.id === card.columnId)?.sessionId;
529
+ additions.push({ id, scope: ownerSessionId === void 0 ? "global" : "session", sessionId: ownerSessionId ?? sessionId, cardId: card.id, title: card.title, kind: event.kind, at: event.at });
530
+ }
531
+ }
532
+ if (seenChanged) saveSeenReminderIds([...seen]);
533
+ if (additions.length === 0) return;
534
+ reminderToasts = [...reminderToasts, ...additions];
535
+ reminderListeners.forEach((listener) => listener());
536
+ scheduleVisibleReminderToasts();
537
+ }
538
+ function reminderEventId(cardId, kind, firedAt) {
539
+ return `${cardId}:${kind}:${firedAt}`;
540
+ }
541
+ var SEEN_REMINDERS_KEY = "@listen/board/seen-reminders-v2";
542
+ function seenReminderIds() {
543
+ try {
544
+ const parsed = JSON.parse(localStorage.getItem(SEEN_REMINDERS_KEY) ?? "[]");
545
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
546
+ } catch {
547
+ return [];
548
+ }
549
+ }
550
+ function boardReminderEventIds(board) {
551
+ const ids = /* @__PURE__ */ new Set();
552
+ for (const card of board.cards) {
553
+ if (card.timeReminder.startFiredAt !== void 0) ids.add(reminderEventId(card.id, "start", card.timeReminder.startFiredAt));
554
+ if (card.timeReminder.endFiredAt !== void 0) ids.add(reminderEventId(card.id, "end", card.timeReminder.endFiredAt));
555
+ }
556
+ return ids;
557
+ }
558
+ function saveSeenReminderIds(ids) {
559
+ try {
560
+ localStorage.setItem(SEEN_REMINDERS_KEY, JSON.stringify(ids));
561
+ } catch {
562
+ }
563
+ }
564
+ function scheduleVisibleReminderToasts() {
565
+ for (const toast of reminderToasts.slice(0, MAX_VISIBLE_REMINDER_TOASTS)) {
566
+ if (reminderToastTimers.has(toast.id)) continue;
567
+ const timer = globalThis.setTimeout(() => {
568
+ reminderToastTimers.delete(toast.id);
569
+ dismissReminderToast(toast.id);
570
+ }, 12e3);
571
+ reminderToastTimers.set(toast.id, timer);
572
+ }
573
+ }
574
+
575
+ // src/client/useBoard.ts
576
+ var import_react = require("react");
577
+
578
+ // src/client/api.ts
579
+ var BoardConflictError = class extends Error {
580
+ constructor() {
581
+ super("board revision mismatch");
582
+ this.name = "BoardConflictError";
583
+ }
584
+ };
585
+ var CHANNEL = "/board-rpc";
586
+ async function remoteGetBoard(ctx, sessionId, scope, defaultColumns) {
587
+ const result = await ctx.connection.rpc.call(CHANNEL, "getBoard", {
588
+ args: { sessionId, scope, defaultColumns }
589
+ });
590
+ if (!result.ok) throw new Error(result.error?.message ?? "board get failed");
591
+ return normalizeBoard(result.value);
592
+ }
593
+ async function remoteMutate(ctx, sessionId, scope, op, revision) {
594
+ const result = await ctx.connection.rpc.call(CHANNEL, "mutate", {
595
+ args: { sessionId, scope, op, revision }
596
+ });
597
+ if (!result.ok) {
598
+ const message = result.error?.message ?? "";
599
+ if (message.includes("revision")) throw new BoardConflictError();
600
+ throw new Error(message || "board mutate failed");
601
+ }
602
+ return normalizeBoard(result.value);
603
+ }
604
+ async function remoteAddLog(ctx, sessionId, scope, cardId, text, attachments, revision) {
605
+ const result = await ctx.connection.rpc.call(CHANNEL, "addLog", { args: { sessionId, scope, cardId, text, attachments, revision } });
606
+ if (!result.ok) {
607
+ const message = result.error?.message ?? "";
608
+ if (message.includes("revision")) throw new BoardConflictError();
609
+ throw new Error(message || "log submit failed");
610
+ }
611
+ return normalizeBoard(result.value);
612
+ }
613
+ async function remoteGetAttachment(ctx, sessionId, scope, cardId, attachmentId) {
614
+ const result = await ctx.connection.rpc.call(CHANNEL, "getAttachment", { args: { sessionId, scope, cardId, attachmentId } });
615
+ if (!result.ok) throw new Error(result.error?.message ?? "attachment download failed");
616
+ return result.value;
617
+ }
618
+ async function remoteOpenAttachment(ctx, sessionId, scope, cardId, attachmentId) {
619
+ const result = await ctx.connection.rpc.call(CHANNEL, "openAttachment", { args: { sessionId, scope, cardId, attachmentId } });
620
+ if (!result.ok) throw new Error(result.error?.message ?? "attachment open failed");
621
+ }
622
+ function normalizeBoard(board) {
623
+ return {
624
+ ...board,
625
+ cards: board.cards.map((card) => ({
626
+ ...card,
627
+ startAt: card.startAt ?? null,
628
+ endAt: card.endAt ?? null,
629
+ archivedAt: card.archivedAt ?? null,
630
+ archivedColumn: card.archivedColumn ?? null,
631
+ logs: card.logs ?? []
632
+ }))
633
+ };
634
+ }
635
+
636
+ // src/client/useBoard.ts
637
+ function useBoard(ctx, sessionId, scope, t) {
638
+ const [board, setBoard] = (0, import_react.useState)(null);
639
+ const [loading, setLoading] = (0, import_react.useState)(true);
640
+ const [error, setError] = (0, import_react.useState)(null);
641
+ const [conflict, setConflict] = (0, import_react.useState)(false);
642
+ const boardRef = (0, import_react.useRef)(null);
643
+ const getDefaultColumns = () => [t("defaultTodoColumn"), t("defaultDoingColumn"), t("defaultUncategorizedColumn")];
644
+ const acceptBoard = (0, import_react.useCallback)((next) => {
645
+ publishBoardReminders(next, sessionId);
646
+ if (boardRef.current === null || boardRef.current.revision < next.revision) {
647
+ boardRef.current = next;
648
+ setBoard(next);
649
+ }
650
+ setConflict(false);
651
+ setError(null);
652
+ setLoading(false);
653
+ }, [sessionId]);
654
+ const reload = (0, import_react.useCallback)(() => {
655
+ setLoading(true);
656
+ setError(null);
657
+ setConflict(false);
658
+ void remoteGetBoard(ctx, sessionId, scope, getDefaultColumns()).then(
659
+ acceptBoard,
660
+ (reason) => {
661
+ setError(reason instanceof Error ? reason.message : String(reason));
662
+ setLoading(false);
663
+ }
664
+ );
665
+ }, [acceptBoard, ctx, sessionId, scope]);
666
+ (0, import_react.useEffect)(() => {
667
+ reload();
668
+ const timer = globalThis.setInterval(() => {
669
+ void remoteGetBoard(ctx, sessionId, scope, getDefaultColumns()).then(acceptBoard, () => void 0);
670
+ }, 3e3);
671
+ return () => globalThis.clearInterval(timer);
672
+ }, [acceptBoard, ctx, reload, scope, sessionId]);
673
+ const mutate = (0, import_react.useCallback)(async (op) => {
674
+ let current = boardRef.current;
675
+ if (current === null) return;
676
+ for (let attempt = 0; attempt < 2; attempt += 1) {
677
+ try {
678
+ acceptBoard(await remoteMutate(ctx, sessionId, scope, op, current.revision));
679
+ return;
680
+ } catch (reason) {
681
+ if (reason instanceof BoardConflictError && attempt === 0) {
682
+ current = await remoteGetBoard(ctx, sessionId, scope, getDefaultColumns());
683
+ acceptBoard(current);
684
+ continue;
685
+ }
686
+ if (reason instanceof BoardConflictError) {
687
+ setConflict(true);
688
+ reload();
689
+ return;
690
+ }
691
+ setError(reason instanceof Error ? reason.message : String(reason));
692
+ return;
693
+ }
694
+ }
695
+ }, [acceptBoard, ctx, reload, scope, sessionId]);
696
+ const addLog = (0, import_react.useCallback)(async (cardId, text, attachments) => {
697
+ let current = boardRef.current;
698
+ if (current === null) return;
699
+ for (let attempt = 0; attempt < 2; attempt += 1) {
700
+ try {
701
+ acceptBoard(await remoteAddLog(ctx, sessionId, scope, cardId, text, attachments, current.revision));
702
+ return;
703
+ } catch (reason) {
704
+ if (reason instanceof BoardConflictError && attempt === 0) {
705
+ current = await remoteGetBoard(ctx, sessionId, scope, getDefaultColumns());
706
+ acceptBoard(current);
707
+ continue;
708
+ }
709
+ setError(reason instanceof Error ? reason.message : String(reason));
710
+ throw reason;
711
+ }
712
+ }
713
+ }, [acceptBoard, ctx, scope, sessionId]);
714
+ const downloadAttachment = (0, import_react.useCallback)((cardId, attachmentId) => remoteGetAttachment(ctx, sessionId, scope, cardId, attachmentId), [ctx, scope, sessionId]);
715
+ const openAttachment = (0, import_react.useCallback)((cardId, attachmentId) => remoteOpenAttachment(ctx, sessionId, scope, cardId, attachmentId), [ctx, scope, sessionId]);
716
+ return { board, loading, error, conflict, mutate, addLog, downloadAttachment, openAttachment, reload };
717
+ }
718
+
719
+ // src/client/SessionChip.tsx
720
+ var import_jsx_runtime = require("react/jsx-runtime");
721
+ function SessionChip(props) {
722
+ const overlay = (0, import_react2.useSyncExternalStore)(subscribeOverlay, () => getOverlay(props.sessionId));
723
+ const list = (0, import_react2.useSyncExternalStore)(props.sessions.subscribe, props.sessions.getSnapshot);
724
+ const row = list.byId[props.sessionId];
725
+ const { board } = useBoard(props.rpc, props.sessionId, "session", props.t);
726
+ if (row?.origin === "subagent") return null;
727
+ const count = board?.cards.length ?? 0;
728
+ const empty = (board?.columns.length ?? 0) === 0;
729
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
730
+ "button",
731
+ {
732
+ type: "button",
733
+ className: "board-chip",
734
+ "aria-label": props.t("name"),
735
+ "aria-pressed": overlay.open,
736
+ title: props.t("name"),
737
+ onClick: () => toggleOverlay(props.sessionId),
738
+ children: [
739
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KanbanIcon, {}),
740
+ !(empty && count === 0) && count > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "board-count", children: count })
741
+ ]
742
+ }
743
+ );
744
+ }
745
+ function KanbanIcon() {
746
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "1.5", y: "2", width: "4", height: "12", rx: "1", fill: "currentColor", opacity: "0.9" }),
748
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "6", y: "2", width: "4", height: "8", rx: "1", fill: "currentColor", opacity: "0.7" }),
749
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "10.5", y: "2", width: "4", height: "10", rx: "1", fill: "currentColor", opacity: "0.5" })
750
+ ] });
751
+ }
752
+
753
+ // src/client/SessionOverlay.tsx
754
+ var import_react4 = require("react");
755
+ var import_react5 = require("react");
756
+
757
+ // src/client/BoardEditor.tsx
758
+ var import_react3 = require("react");
759
+ var import_react_dom = require("react-dom");
760
+
761
+ // src/client/Icons.tsx
762
+ var import_jsx_runtime2 = require("react/jsx-runtime");
763
+ function iconClass(className) {
764
+ return `board-ui-icon${className === void 0 ? "" : ` ${className}`}`;
765
+ }
766
+ function CloseIcon(props) {
767
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m5.5 5.5 9 9m0-9-9 9" }) });
768
+ }
769
+ function ChevronIcon(props) {
770
+ const path = props.direction === "left" ? "m12.5 4.5-5.5 5.5 5.5 5.5" : props.direction === "right" ? "m7.5 4.5 5.5 5.5-5.5 5.5" : "m4.5 7.5 5.5 5.5 5.5-5.5";
771
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: path }) });
772
+ }
773
+ function PlusIcon(props) {
774
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 4v12M4 10h12" }) });
775
+ }
776
+ function ArrowRightIcon(props) {
777
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 10h12m-4.5-4.5L16 10l-4.5 4.5" }) });
778
+ }
779
+ function ClockIcon(props) {
780
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "10", cy: "10", r: "6.5" }),
782
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 6.5V10l2.5 1.5" })
783
+ ] });
784
+ }
785
+ function PlayIcon(props) {
786
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m7 5 7 5-7 5Z" }) });
787
+ }
788
+ function StopIcon(props) {
789
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "6", y: "6", width: "8", height: "8", rx: "1" }) });
790
+ }
791
+ function AlertIcon(props) {
792
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: [
793
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 6v5" }),
794
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10 14.25h.01" })
795
+ ] });
796
+ }
797
+ function GripIcon(props) {
798
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: [
799
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "7", cy: "5", r: "1" }),
800
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "13", cy: "5", r: "1" }),
801
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "7", cy: "10", r: "1" }),
802
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "13", cy: "10", r: "1" }),
803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "7", cy: "15", r: "1" }),
804
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "13", cy: "15", r: "1" })
805
+ ] });
806
+ }
807
+ function TrashIcon(props) {
808
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4.5 6h11M8 3.75h4M6 6l.65 10h6.7L14 6M8.25 8.5v5M11.75 8.5v5" }) });
809
+ }
810
+ function AttachmentIcon(props) {
811
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m7.25 10.75 4.8-4.8a2.5 2.5 0 0 1 3.54 3.54l-6.01 6.02a4 4 0 0 1-5.66-5.66l6.02-6.01a1.5 1.5 0 0 1 2.12 2.12l-6.01 6.01a1 1 0 0 0 1.41 1.42l5.31-5.31" }) });
812
+ }
813
+ function SearchIcon(props) {
814
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: iconClass(props.className), viewBox: "0 0 20 20", "aria-hidden": "true", children: [
815
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8.75", cy: "8.75", r: "4.75" }),
816
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m12.25 12.25 3.5 3.5" })
817
+ ] });
818
+ }
819
+
820
+ // src/client/BoardEditor.tsx
821
+ var import_jsx_runtime3 = require("react/jsx-runtime");
822
+ function BoardEditor(props) {
823
+ const { board, loading, error, conflict, singleColumn = false, sessionColumnTitle, t, onMutate, onRetry } = props;
824
+ const [selectedId, setSelectedId] = (0, import_react3.useState)(null);
825
+ const [archiveOpen, setArchiveOpen] = (0, import_react3.useState)(false);
826
+ const [draggedCardId, setDraggedCardId] = (0, import_react3.useState)(null);
827
+ const [draggedColumnId, setDraggedColumnId] = (0, import_react3.useState)(null);
828
+ const [cardPreview, setCardPreview] = (0, import_react3.useState)(null);
829
+ const [columnPreviewIds, setColumnPreviewIds] = (0, import_react3.useState)(null);
830
+ const [completingIds, setCompletingIds] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set());
831
+ const [confirmTarget, setConfirmTarget] = (0, import_react3.useState)(null);
832
+ const [nowMs, setNowMs] = (0, import_react3.useState)(() => Date.now());
833
+ const [newColumnRequest, setNewColumnRequest] = (0, import_react3.useState)(null);
834
+ const [autoDraftColumnId, setAutoDraftColumnId] = (0, import_react3.useState)(null);
835
+ const seededEmptyRevision = (0, import_react3.useRef)(null);
836
+ const syncingSessionTitle = (0, import_react3.useRef)(null);
837
+ const cardPreviewRef = (0, import_react3.useRef)(null);
838
+ const columnPreviewRef = (0, import_react3.useRef)(null);
839
+ const cardDropHandled = (0, import_react3.useRef)(false);
840
+ const columnDropHandled = (0, import_react3.useRef)(false);
841
+ const baseColumns = (0, import_react3.useMemo)(() => {
842
+ if (board === null) return [];
843
+ const sorted = [...board.columns].sort((a, b) => a.order - b.order);
844
+ return singleColumn && sorted[0] !== void 0 ? [sorted[0]] : sorted;
845
+ }, [board, singleColumn]);
846
+ const columns = (0, import_react3.useMemo)(() => {
847
+ if (columnPreviewIds === null) return baseColumns;
848
+ const byId = new Map(baseColumns.map((column) => [column.id, column]));
849
+ return columnPreviewIds.flatMap((id) => {
850
+ const column = byId.get(id);
851
+ return column === void 0 ? [] : [column];
852
+ });
853
+ }, [baseColumns, columnPreviewIds]);
854
+ const baseCardLayout = (0, import_react3.useMemo)(() => {
855
+ if (board === null) return {};
856
+ const active = board.cards.filter((card) => card.archivedAt === null);
857
+ if (singleColumn) {
858
+ const column = baseColumns[0];
859
+ if (column === void 0) return {};
860
+ const columnOrder = new Map(board.columns.map((item) => [item.id, item.order]));
861
+ active.sort((a, b) => (columnOrder.get(a.columnId) ?? 0) - (columnOrder.get(b.columnId) ?? 0) || a.order - b.order);
862
+ return { [column.id]: active.map((card) => card.id) };
863
+ }
864
+ return Object.fromEntries(baseColumns.map((column) => [column.id, active.filter((card) => card.columnId === column.id).sort((a, b) => a.order - b.order).map((card) => card.id)]));
865
+ }, [baseColumns, board, singleColumn]);
866
+ const selected = board?.cards.find((card) => card.id === selectedId && card.archivedAt === null) ?? null;
867
+ (0, import_react3.useEffect)(() => {
868
+ if (selectedId !== null && selected === null) setSelectedId(null);
869
+ }, [selected, selectedId]);
870
+ (0, import_react3.useEffect)(() => {
871
+ if (!singleColumn || board === null) return;
872
+ if (board.columns.length > 0) {
873
+ seededEmptyRevision.current = null;
874
+ return;
875
+ }
876
+ const seedKey = `${board.id}:${String(board.revision)}`;
877
+ if (seededEmptyRevision.current === seedKey) return;
878
+ seededEmptyRevision.current = seedKey;
879
+ void onMutate({ type: "addColumn", title: sessionColumnTitle?.trim() || t("session") }).catch(() => {
880
+ seededEmptyRevision.current = null;
881
+ });
882
+ }, [board, onMutate, sessionColumnTitle, singleColumn, t]);
883
+ (0, import_react3.useEffect)(() => {
884
+ const title = sessionColumnTitle?.trim();
885
+ const column = board?.columns[0];
886
+ if (!singleColumn || board === null || column === void 0 || title === void 0 || title.length === 0 || column.title === title) return;
887
+ const key = `${String(board.revision)}:${column.id}:${title}`;
888
+ if (syncingSessionTitle.current === key) return;
889
+ syncingSessionTitle.current = key;
890
+ void onMutate({ type: "renameColumn", columnId: column.id, title }).finally(() => {
891
+ if (syncingSessionTitle.current === key) syncingSessionTitle.current = null;
892
+ });
893
+ }, [board, onMutate, sessionColumnTitle, singleColumn]);
894
+ (0, import_react3.useEffect)(() => {
895
+ const timer = window.setInterval(() => setNowMs(Date.now()), 3e4);
896
+ return () => window.clearInterval(timer);
897
+ }, []);
898
+ (0, import_react3.useEffect)(() => {
899
+ if (board === null || newColumnRequest === null || board.revision <= newColumnRequest.revision) return;
900
+ const added = board.columns.find((column) => !newColumnRequest.baseIds.has(column.id));
901
+ setNewColumnRequest(null);
902
+ if (added !== void 0) setAutoDraftColumnId(added.id);
903
+ }, [board, newColumnRequest]);
904
+ if (loading && board === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-empty", children: [
905
+ t("name"),
906
+ "\u2026"
907
+ ] });
908
+ if (error !== null && board === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-error", children: [
909
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: t("loadError") }),
910
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "preview", children: error }),
911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn primary", onClick: onRetry, children: t("retry") })
912
+ ] });
913
+ if (board === null) return null;
914
+ const cardById = new Map(board.cards.map((card) => [card.id, card]));
915
+ const archivedCards = board.cards.filter((card) => card.archivedAt !== null);
916
+ const cardsOf = (columnId) => (cardPreview?.[columnId] ?? baseCardLayout[columnId] ?? []).flatMap((id) => {
917
+ const card = cardById.get(id);
918
+ return card === void 0 ? [] : [card];
919
+ });
920
+ const setCardLayout = (next) => {
921
+ cardPreviewRef.current = next;
922
+ setCardPreview(next);
923
+ };
924
+ const setColumnOrder = (next) => {
925
+ columnPreviewRef.current = next;
926
+ setColumnPreviewIds(next);
927
+ };
928
+ const beginCardDrag = (cardId) => {
929
+ cardDropHandled.current = false;
930
+ setDraggedCardId(cardId);
931
+ setCardLayout(cloneLayout(baseCardLayout));
932
+ };
933
+ const previewCard = (columnId, targetCardId, after) => {
934
+ if (draggedCardId === null || targetCardId === draggedCardId) return;
935
+ const next = cloneLayout(cardPreviewRef.current ?? baseCardLayout);
936
+ for (const ids of Object.values(next)) {
937
+ const index = ids.indexOf(draggedCardId);
938
+ if (index >= 0) ids.splice(index, 1);
939
+ }
940
+ const target = next[columnId] ?? [];
941
+ next[columnId] = target;
942
+ const targetIndex = targetCardId === null ? target.length : target.indexOf(targetCardId);
943
+ target.splice(targetIndex < 0 ? target.length : targetIndex + (after ? 1 : 0), 0, draggedCardId);
944
+ setCardLayout(next);
945
+ };
946
+ const commitCardDrag = async () => {
947
+ const cardId = draggedCardId;
948
+ const layout = cardPreviewRef.current;
949
+ setDraggedCardId(null);
950
+ if (cardId === null || layout === null) {
951
+ setCardLayout(null);
952
+ return;
953
+ }
954
+ const placement = Object.entries(layout).find(([, ids]) => ids.includes(cardId));
955
+ if (placement !== void 0) {
956
+ const [columnId, ids] = placement;
957
+ const card = cardById.get(cardId);
958
+ const index = ids.indexOf(cardId);
959
+ if (card !== void 0 && (card.columnId !== columnId || card.order !== index)) {
960
+ await onMutate({ type: "moveCard", cardId, columnId, index });
961
+ }
962
+ }
963
+ setCardLayout(null);
964
+ };
965
+ const cancelCardDrag = () => {
966
+ setDraggedCardId(null);
967
+ setCardLayout(null);
968
+ };
969
+ const beginColumnDrag = (columnId) => {
970
+ columnDropHandled.current = false;
971
+ setDraggedColumnId(columnId);
972
+ setColumnOrder(baseColumns.map((column) => column.id));
973
+ };
974
+ const previewColumn = (targetColumnId, after) => {
975
+ if (draggedColumnId === null || targetColumnId === draggedColumnId) return;
976
+ const next = [...columnPreviewRef.current ?? baseColumns.map((column) => column.id)];
977
+ const from = next.indexOf(draggedColumnId);
978
+ if (from < 0) return;
979
+ next.splice(from, 1);
980
+ const target = next.indexOf(targetColumnId);
981
+ next.splice(target < 0 ? next.length : target + (after ? 1 : 0), 0, draggedColumnId);
982
+ setColumnOrder(next);
983
+ };
984
+ const commitColumnDrag = async () => {
985
+ const next = columnPreviewRef.current;
986
+ setDraggedColumnId(null);
987
+ if (next !== null && !sameIds(next, baseColumns.map((column) => column.id))) {
988
+ await onMutate({ type: "reorderColumns", columnIds: next });
989
+ }
990
+ setColumnOrder(null);
991
+ };
992
+ const cancelColumnDrag = () => {
993
+ setDraggedColumnId(null);
994
+ setColumnOrder(null);
995
+ };
996
+ const completeCard = async (cardId) => {
997
+ setCompletingIds((current) => new Set(current).add(cardId));
998
+ try {
999
+ await new Promise((resolve) => {
1000
+ setTimeout(resolve, 220);
1001
+ });
1002
+ await onMutate({ type: "archiveCard", cardId });
1003
+ if (selectedId === cardId) setSelectedId(null);
1004
+ } finally {
1005
+ setCompletingIds((current) => {
1006
+ const next = new Set(current);
1007
+ next.delete(cardId);
1008
+ return next;
1009
+ });
1010
+ }
1011
+ };
1012
+ const confirmDelete = async () => {
1013
+ const target = confirmTarget;
1014
+ if (target === null) return;
1015
+ setConfirmTarget(null);
1016
+ if (target.kind === "card") {
1017
+ if (selectedId === target.id) setSelectedId(null);
1018
+ await onMutate({ type: "deleteCard", cardId: target.id });
1019
+ return;
1020
+ }
1021
+ if (target.kind === "cards") {
1022
+ await onMutate({ type: "deleteCards", cardIds: target.ids });
1023
+ return;
1024
+ }
1025
+ const selectedCard = selectedId === null ? void 0 : cardById.get(selectedId);
1026
+ if (selectedCard?.columnId === target.id) setSelectedId(null);
1027
+ await onMutate({ type: "deleteColumn", columnId: target.id, confirm: true });
1028
+ };
1029
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `board-editor${singleColumn ? " board-session-single" : ""}`, children: [
1030
+ conflict && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-banner", role: "status", children: [
1031
+ t("conflict"),
1032
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn", onClick: onRetry, children: t("retry") })
1033
+ ] }),
1034
+ board.columns.length === 0 && singleColumn ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-empty", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: t("settingUpSessionBoard") }) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-workspace", children: [
1035
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-cols", children: [
1036
+ columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1037
+ ColumnView,
1038
+ {
1039
+ column,
1040
+ cards: cardsOf(column.id),
1041
+ canManage: !singleColumn,
1042
+ draggedCardId,
1043
+ draggedColumnId,
1044
+ completingIds,
1045
+ nowMs,
1046
+ autoDraft: autoDraftColumnId === column.id,
1047
+ onAutoDraftConsumed: () => setAutoDraftColumnId(null),
1048
+ t,
1049
+ onRename: (title) => void onMutate({ type: "renameColumn", columnId: column.id, title }),
1050
+ onAddCard: (input) => {
1051
+ setArchiveOpen(false);
1052
+ void onMutate({ type: "addCard", columnId: column.id, ...input });
1053
+ },
1054
+ onOpenCard: (cardId) => {
1055
+ setArchiveOpen(false);
1056
+ setSelectedId(cardId);
1057
+ },
1058
+ horizontalCards: singleColumn,
1059
+ onCardDragStart: beginCardDrag,
1060
+ onCardDragEnd: () => {
1061
+ if (cardDropHandled.current) {
1062
+ cardDropHandled.current = false;
1063
+ } else cancelCardDrag();
1064
+ },
1065
+ onPreviewCard: (targetCardId, after) => previewCard(column.id, targetCardId, after),
1066
+ onDropCard: () => {
1067
+ cardDropHandled.current = true;
1068
+ void commitCardDrag();
1069
+ },
1070
+ onColumnDragStart: beginColumnDrag,
1071
+ onColumnDragEnd: () => {
1072
+ if (columnDropHandled.current) {
1073
+ columnDropHandled.current = false;
1074
+ } else cancelColumnDrag();
1075
+ },
1076
+ onPreviewColumn: (after) => previewColumn(column.id, after),
1077
+ onDropColumn: () => {
1078
+ columnDropHandled.current = true;
1079
+ void commitColumnDrag();
1080
+ },
1081
+ onComplete: (cardId) => void completeCard(cardId),
1082
+ onDeleteCard: (cardId) => {
1083
+ const card = cardById.get(cardId);
1084
+ if (card !== void 0) setConfirmTarget({ kind: "card", id: card.id, title: card.title });
1085
+ },
1086
+ onDeleteColumn: () => setConfirmTarget({ kind: "column", id: column.id, title: column.title, activeCount: board.cards.filter((card) => card.columnId === column.id && card.archivedAt === null).length, archivedCount: board.cards.filter((card) => card.archivedColumn?.id === column.id).length })
1087
+ },
1088
+ column.id
1089
+ )),
1090
+ !singleColumn && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NewColumnControl, { t, onCreate: (title) => {
1091
+ setNewColumnRequest({ revision: board.revision, baseIds: new Set(board.columns.map((column) => column.id)) });
1092
+ return onMutate({ type: "addColumn", title });
1093
+ } })
1094
+ ] }) }),
1095
+ selected !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardDetailLayer, { card: selected, t, onClose: () => setSelectedId(null), onSave: (patch) => onMutate({ type: "updateCard", cardId: selected.id, ...patch }), onAddLog: (text, attachments) => props.onAddLog(selected.id, text, attachments), onDownloadAttachment: (attachmentId) => props.onDownloadAttachment(selected.id, attachmentId), onOpenAttachment: (attachmentId) => props.onOpenAttachment(selected.id, attachmentId) }, selected.id),
1096
+ !singleColumn && archiveOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArchivePanelLayer, { cards: archivedCards, columns: baseColumns, t, onClose: () => setArchiveOpen(false), onRestore: (cardId, columnId) => onMutate({ type: "restoreCard", cardId, columnId }), onRestoreMany: (cards) => onMutate({ type: "restoreCards", cards }), onRequestDelete: (cards) => {
1097
+ if (cards.length === 1 && cards[0] !== void 0) setConfirmTarget({ kind: "card", id: cards[0].id, title: cards[0].title });
1098
+ else if (cards.length > 1) setConfirmTarget({ kind: "cards", ids: cards.map((card) => card.id), count: cards.length });
1099
+ } })
1100
+ ] }),
1101
+ !singleColumn && !archiveOpen && selected === null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn board-open-archive board-archive-fab", title: t("completedCount", { n: archivedCards.length }), "aria-label": t("completedCount", { n: archivedCards.length }), onClick: () => setArchiveOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HistoryIcon, {}) }),
1102
+ confirmTarget !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ConfirmDialog, { target: confirmTarget, t, onCancel: () => setConfirmTarget(null), onConfirm: () => void confirmDelete() })
1103
+ ] });
1104
+ }
1105
+ function NewColumnControl(props) {
1106
+ const [editing, setEditing] = (0, import_react3.useState)(false);
1107
+ const [title, setTitle] = (0, import_react3.useState)("");
1108
+ const [attempted, setAttempted] = (0, import_react3.useState)(false);
1109
+ const [creating, setCreating] = (0, import_react3.useState)(false);
1110
+ const inputRef = (0, import_react3.useRef)(null);
1111
+ (0, import_react3.useEffect)(() => {
1112
+ if (editing) inputRef.current?.focus();
1113
+ }, [editing]);
1114
+ const cancel = () => {
1115
+ setEditing(false);
1116
+ setTitle("");
1117
+ setAttempted(false);
1118
+ };
1119
+ const create = async () => {
1120
+ const next = title.trim();
1121
+ if (next.length === 0) {
1122
+ setAttempted(true);
1123
+ inputRef.current?.focus();
1124
+ return;
1125
+ }
1126
+ if (creating) return;
1127
+ setCreating(true);
1128
+ try {
1129
+ await props.onCreate(next);
1130
+ cancel();
1131
+ } finally {
1132
+ setCreating(false);
1133
+ }
1134
+ };
1135
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-col board-new-col", children: editing ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-new-column-editor", children: [
1136
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: inputRef, className: `board-col-name-field${attempted ? " invalid" : ""}`, value: title, placeholder: props.t("columnNamePlaceholder"), "aria-label": props.t("columnNamePlaceholder"), "aria-invalid": attempted, disabled: creating, onChange: (event) => {
1137
+ setTitle(event.target.value);
1138
+ if (attempted && event.target.value.trim().length > 0) setAttempted(false);
1139
+ }, onKeyDown: (event) => {
1140
+ if (event.key === "Enter") {
1141
+ event.preventDefault();
1142
+ void create();
1143
+ } else if (event.key === "Escape") {
1144
+ event.preventDefault();
1145
+ cancel();
1146
+ }
1147
+ } }),
1148
+ attempted && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-field-error", children: props.t("columnNameRequired") }),
1149
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-new-column-hint", children: [
1150
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("enterToCreate") }),
1151
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn", disabled: creating, onClick: cancel, children: props.t("cancel") })
1152
+ ] })
1153
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-btn board-add-column", onClick: () => setEditing(true), children: [
1154
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlusIcon, {}),
1155
+ props.t("addColumn")
1156
+ ] }) });
1157
+ }
1158
+ function HistoryIcon() {
1159
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { className: "board-history-icon", viewBox: "0 0 20 20", "aria-hidden": "true", children: [
1160
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M3.75 7A7 7 0 1 1 3.2 12.1" }),
1161
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M3.75 3.75V7H7" }),
1162
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M10 6.25V10l2.5 1.5" })
1163
+ ] });
1164
+ }
1165
+ function ColumnView(props) {
1166
+ const [editingTitle, setEditingTitle] = (0, import_react3.useState)(false);
1167
+ const [title, setTitle] = (0, import_react3.useState)(props.column.title);
1168
+ const [drafting, setDrafting] = (0, import_react3.useState)(false);
1169
+ const [cardDraft, setCardDraft] = (0, import_react3.useState)("");
1170
+ const [cardBodyDraft, setCardBodyDraft] = (0, import_react3.useState)("");
1171
+ const [cardStartDateDraft, setCardStartDateDraft] = (0, import_react3.useState)("");
1172
+ const [cardStartTimeDraft, setCardStartTimeDraft] = (0, import_react3.useState)("09:00");
1173
+ const [cardEndDateDraft, setCardEndDateDraft] = (0, import_react3.useState)("");
1174
+ const [cardEndTimeDraft, setCardEndTimeDraft] = (0, import_react3.useState)("18:00");
1175
+ const [cardTimeReminder, setCardTimeReminder] = (0, import_react3.useState)(false);
1176
+ const titleRef = (0, import_react3.useRef)(null);
1177
+ const cardRef = (0, import_react3.useRef)(null);
1178
+ (0, import_react3.useEffect)(() => {
1179
+ setTitle(props.column.title);
1180
+ }, [props.column.title]);
1181
+ (0, import_react3.useEffect)(() => {
1182
+ if (editingTitle) titleRef.current?.focus();
1183
+ }, [editingTitle]);
1184
+ (0, import_react3.useEffect)(() => {
1185
+ if (drafting) cardRef.current?.focus();
1186
+ }, [drafting]);
1187
+ (0, import_react3.useEffect)(() => {
1188
+ if (!props.autoDraft) return;
1189
+ setDrafting(true);
1190
+ props.onAutoDraftConsumed();
1191
+ }, [props.autoDraft, props.onAutoDraftConsumed]);
1192
+ (0, import_react3.useEffect)(() => {
1193
+ const list = document.querySelector(".board-session-single .board-card-list");
1194
+ if (!props.horizontalCards || list === null) return;
1195
+ const scrollHorizontally = (event) => {
1196
+ if (event.deltaY === 0 || Math.abs(event.deltaX) >= Math.abs(event.deltaY)) return;
1197
+ const max = list.scrollWidth - list.clientWidth;
1198
+ if (max <= 0) return;
1199
+ const unit = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 24 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? list.clientWidth : 1;
1200
+ const next = Math.max(0, Math.min(max, list.scrollLeft + event.deltaY * unit));
1201
+ if (next === list.scrollLeft) return;
1202
+ event.preventDefault();
1203
+ list.scrollLeft = next;
1204
+ };
1205
+ list.addEventListener("wheel", scrollHorizontally, { passive: false });
1206
+ return () => list.removeEventListener("wheel", scrollHorizontally);
1207
+ }, [props.horizontalCards]);
1208
+ const commitTitle = () => {
1209
+ const next = title.trim();
1210
+ setEditingTitle(false);
1211
+ if (next.length > 0 && next !== props.column.title) props.onRename(next);
1212
+ else setTitle(props.column.title);
1213
+ };
1214
+ const resetCardDraft = () => {
1215
+ setDrafting(false);
1216
+ setCardDraft("");
1217
+ setCardBodyDraft("");
1218
+ setCardStartDateDraft("");
1219
+ setCardStartTimeDraft("09:00");
1220
+ setCardEndDateDraft("");
1221
+ setCardEndTimeDraft("18:00");
1222
+ setCardTimeReminder(false);
1223
+ };
1224
+ const startAt = optionalIso(cardStartDateDraft, cardStartTimeDraft);
1225
+ const endAt = optionalIso(cardEndDateDraft, cardEndTimeDraft);
1226
+ const timeRangeValid = validTimeRange(startAt, endAt);
1227
+ const finishCard = () => {
1228
+ const title2 = cardDraft.trim();
1229
+ if (title2.length === 0 || startAt === void 0 || endAt === void 0 || !timeRangeValid) return;
1230
+ props.onAddCard({ title: title2, body: cardBodyDraft, ...startAt === null ? {} : { startAt }, ...endAt === null ? {} : { endAt }, ...cardTimeReminder ? { timeReminder: true } : {} });
1231
+ resetCardDraft();
1232
+ };
1233
+ const sessionEmpty = props.horizontalCards && props.cards.length === 0 && !drafting;
1234
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `board-col${props.draggedColumnId === props.column.id ? " column-dragging" : ""}`, onDragOver: (event) => {
1235
+ if (props.draggedColumnId === null) return;
1236
+ event.preventDefault();
1237
+ const rect = event.currentTarget.getBoundingClientRect();
1238
+ props.onPreviewColumn(event.clientX > rect.left + rect.width / 2);
1239
+ }, onDrop: (event) => {
1240
+ if (props.draggedColumnId === null) return;
1241
+ event.preventDefault();
1242
+ props.onDropColumn();
1243
+ }, children: [
1244
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-col-head", children: [
1245
+ props.canManage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-col-grip", draggable: true, "aria-label": props.column.title, onDragStart: (event) => {
1246
+ event.dataTransfer.effectAllowed = "move";
1247
+ props.onColumnDragStart(props.column.id);
1248
+ }, onDragEnd: props.onColumnDragEnd, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GripIcon, {}) }),
1249
+ editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: titleRef, className: "board-col-title-input", value: title, "aria-label": props.column.title, onChange: (event) => setTitle(event.target.value), onBlur: commitTitle, onKeyDown: (event) => {
1250
+ if (event.key === "Enter") event.currentTarget.blur();
1251
+ if (event.key === "Escape") {
1252
+ setTitle(props.column.title);
1253
+ setEditingTitle(false);
1254
+ }
1255
+ } }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-col-title", title: props.column.title, onClick: () => setEditingTitle(true), children: props.column.title }),
1256
+ props.canManage && props.column.sessionId !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-session-badge", title: props.column.sessionId, children: props.t("sessionColumn") }),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-col-count", "aria-label": props.t("cardCount", { n: props.cards.length }), children: props.cards.length }),
1258
+ props.canManage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-col-delete", title: props.t("deleteColumnMenu"), "aria-label": props.t("deleteColumnMenu"), onClick: props.onDeleteColumn, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
1259
+ ] }),
1260
+ sessionEmpty ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-session-empty", children: [
1261
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-session-empty-mark", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlusIcon, {}) }),
1262
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("noSessionCards") }),
1263
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("noSessionCardsHint") }),
1264
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-btn primary", onClick: () => setDrafting(true), children: [
1265
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlusIcon, {}),
1266
+ props.t("addFirstCard")
1267
+ ] })
1268
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1269
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-card-list", children: props.cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { draggable: true, className: `board-card${props.draggedCardId === card.id ? " dragging" : ""}${props.completingIds.has(card.id) ? " completing" : ""}`, onClick: (event) => {
1270
+ if (!(event.target instanceof Element) || event.target.closest("button,input,textarea,select,a") !== null) return;
1271
+ props.onOpenCard(card.id);
1272
+ }, onDragStart: (event) => {
1273
+ event.dataTransfer.effectAllowed = "move";
1274
+ event.stopPropagation();
1275
+ props.onCardDragStart(card.id);
1276
+ }, onDragEnd: props.onCardDragEnd, onDragOver: (event) => {
1277
+ if (props.draggedCardId === null) return;
1278
+ event.preventDefault();
1279
+ event.stopPropagation();
1280
+ const rect = event.currentTarget.getBoundingClientRect();
1281
+ props.onPreviewCard(card.id, props.horizontalCards ? event.clientX > rect.left + rect.width / 2 : event.clientY > rect.top + rect.height / 2);
1282
+ }, onDrop: (event) => {
1283
+ if (props.draggedCardId === null) return;
1284
+ event.preventDefault();
1285
+ event.stopPropagation();
1286
+ props.onDropCard();
1287
+ }, children: [
1288
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-card-delete", title: props.t("deleteCard"), "aria-label": props.t("deleteCard"), onClick: () => props.onDeleteCard(card.id), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) }),
1289
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-card-main", onClick: () => props.onOpenCard(card.id), children: [
1290
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "title", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "title-text", children: card.title }) }),
1291
+ card.body.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "preview", children: card.body })
1292
+ ] }),
1293
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardTimeProgress, { card, nowMs: props.nowMs, t: props.t }),
1294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-card-foot", children: [
1295
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-complete", disabled: props.completingIds.has(card.id), onClick: () => props.onComplete(card.id), children: [
1296
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-complete-mark", "aria-hidden": "true" }),
1297
+ props.t("complete")
1298
+ ] }),
1299
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-card-open", onClick: () => props.onOpenCard(card.id), children: [
1300
+ props.t("viewDetails"),
1301
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowRightIcon, {})
1302
+ ] })
1303
+ ] })
1304
+ ] }, card.id)) }),
1305
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `board-col-drop${props.draggedCardId !== null ? " accepting-card" : ""}`, onDragOver: (event) => {
1306
+ if (props.draggedCardId === null) return;
1307
+ event.preventDefault();
1308
+ event.stopPropagation();
1309
+ props.onPreviewCard(null, false);
1310
+ }, onDrop: (event) => {
1311
+ if (props.draggedCardId === null) return;
1312
+ event.preventDefault();
1313
+ event.stopPropagation();
1314
+ props.onDropCard();
1315
+ } })
1316
+ ] }),
1317
+ drafting ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: "board-card-composer", onSubmit: (event) => {
1318
+ event.preventDefault();
1319
+ finishCard();
1320
+ }, onKeyDown: (event) => {
1321
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey) && event.target instanceof HTMLTextAreaElement) {
1322
+ event.preventDefault();
1323
+ finishCard();
1324
+ }
1325
+ }, children: [
1326
+ !props.horizontalCards && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-composer-head", children: [
1327
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("addCard") }),
1328
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", "aria-label": props.t("close"), onClick: resetCardDraft, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
1329
+ ] }),
1330
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-composer-main", children: [
1331
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: cardRef, className: "board-card-title", value: cardDraft, "aria-label": props.t("titlePlaceholder"), placeholder: props.t("titlePlaceholder"), onChange: (event) => setCardDraft(event.target.value), onKeyDown: (event) => {
1332
+ if (event.key === "Escape") resetCardDraft();
1333
+ } }),
1334
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("textarea", { className: "board-card-notes", value: cardBodyDraft, placeholder: props.t("notesPlaceholder"), onChange: (event) => setCardBodyDraft(event.target.value) })
1335
+ ] }),
1336
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-composer-schedule", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("fieldset", { className: "board-time-fieldset", children: [
1337
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("legend", { children: props.t("taskTime") }),
1338
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DateTimeEditor, { kind: "start", label: props.t("startTime"), placeholder: props.t("selectStartTime"), date: cardStartDateDraft, time: cardStartTimeDraft, t: props.t, onDate: setCardStartDateDraft, onTime: setCardStartTimeDraft }),
1339
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DateTimeEditor, { kind: "end", label: props.t("endTime"), placeholder: props.t("selectEndTime"), date: cardEndDateDraft, time: cardEndTimeDraft, t: props.t, onDate: setCardEndDateDraft, onTime: setCardEndTimeDraft }),
1340
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeReminderToggle, { checked: cardTimeReminder, disabled: !cardTimeReminder && cardStartDateDraft.length === 0 && cardEndDateDraft.length === 0, t: props.t, onChange: setCardTimeReminder }),
1341
+ !timeRangeValid && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-field-error", children: props.t("invalidTimeRange") })
1342
+ ] }) }),
1343
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-composer-actions", children: [
1344
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-btn", onClick: resetCardDraft, children: [
1345
+ props.horizontalCards && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "left" }),
1346
+ props.t(props.horizontalCards ? "back" : "cancel")
1347
+ ] }),
1348
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: "board-btn primary", disabled: cardDraft.trim().length === 0 || startAt === void 0 || endAt === void 0 || !timeRangeValid, children: props.t("addCard") })
1349
+ ] })
1350
+ ] }) : !sessionEmpty && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-btn board-add", onClick: () => setDrafting(true), children: [
1351
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlusIcon, {}),
1352
+ props.t("addCard")
1353
+ ] })
1354
+ ] });
1355
+ }
1356
+ function sameCardDetailPatch(a, b) {
1357
+ return a.title === b.title && a.body === b.body && a.startAt === b.startAt && a.endAt === b.endAt && a.timeReminder === b.timeReminder;
1358
+ }
1359
+ function CardDetailLayer(props) {
1360
+ const drawer = (0, import_react3.useRef)(null);
1361
+ const commitRef = (0, import_react3.useRef)(() => void 0);
1362
+ (0, import_react3.useEffect)(() => {
1363
+ const closeDetail2 = () => {
1364
+ commitRef.current();
1365
+ props.onClose();
1366
+ };
1367
+ const close = (event) => {
1368
+ if (event.key === "Escape" && document.querySelector(".board-image-preview") === null) {
1369
+ event.stopPropagation();
1370
+ closeDetail2();
1371
+ }
1372
+ };
1373
+ const closeOutside = (event) => {
1374
+ if (!(event.target instanceof Node) || drawer.current?.contains(event.target)) return;
1375
+ if (event.target instanceof Element && event.target.closest(".board-date-popover-portal,.board-image-preview") !== null) return;
1376
+ closeDetail2();
1377
+ };
1378
+ document.addEventListener("keydown", close);
1379
+ document.addEventListener("pointerdown", closeOutside, true);
1380
+ return () => {
1381
+ document.removeEventListener("keydown", close);
1382
+ document.removeEventListener("pointerdown", closeOutside, true);
1383
+ };
1384
+ }, [props.onClose]);
1385
+ const closeDetail = () => {
1386
+ commitRef.current();
1387
+ props.onClose();
1388
+ };
1389
+ return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-session-detail-layer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: drawer, className: "board-session-detail-drawer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardDetail, { card: props.card, t: props.t, commitRef, onClose: closeDetail, onSave: props.onSave, onAddLog: props.onAddLog, onDownloadAttachment: props.onDownloadAttachment, onOpenAttachment: props.onOpenAttachment }) }) }), document.body);
1390
+ }
1391
+ function CardDetail(props) {
1392
+ const titleRef = (0, import_react3.useRef)(null);
1393
+ const [title, setTitle] = (0, import_react3.useState)(props.card.title);
1394
+ const [body, setBody] = (0, import_react3.useState)(props.card.body);
1395
+ const [startDate, setStartDate] = (0, import_react3.useState)(datePart(props.card.startAt));
1396
+ const [startTime, setStartTime] = (0, import_react3.useState)(timePart(props.card.startAt, "09:00"));
1397
+ const [endDate, setEndDate] = (0, import_react3.useState)(datePart(props.card.endAt));
1398
+ const [endTime, setEndTime] = (0, import_react3.useState)(timePart(props.card.endAt, "18:00"));
1399
+ const [timeReminder, setTimeReminder] = (0, import_react3.useState)(props.card.timeReminder.enabled);
1400
+ const saved = (0, import_react3.useRef)({ title: props.card.title, body: props.card.body, startAt: props.card.startAt, endAt: props.card.endAt, timeReminder: props.card.timeReminder.enabled });
1401
+ const saveQueue = (0, import_react3.useRef)(Promise.resolve());
1402
+ (0, import_react3.useEffect)(() => {
1403
+ titleRef.current?.focus();
1404
+ }, [props.card.id]);
1405
+ const startAt = optionalIso(startDate, startTime);
1406
+ const endAt = optionalIso(endDate, endTime);
1407
+ const timeRangeValid = validTimeRange(startAt, endAt);
1408
+ const commit = (values = {}) => {
1409
+ const nextTitle = (values.title ?? title).trim();
1410
+ const nextStartAt = optionalIso(values.startDate ?? startDate, values.startTime ?? startTime);
1411
+ const nextEndAt = optionalIso(values.endDate ?? endDate, values.endTime ?? endTime);
1412
+ if (nextTitle.length === 0 || nextStartAt === void 0 || nextEndAt === void 0 || !validTimeRange(nextStartAt, nextEndAt)) return;
1413
+ const patch = { title: nextTitle, body: values.body ?? body, startAt: nextStartAt, endAt: nextEndAt, timeReminder: values.timeReminder ?? timeReminder };
1414
+ if (sameCardDetailPatch(saved.current, patch)) return;
1415
+ saved.current = patch;
1416
+ saveQueue.current = saveQueue.current.then(() => props.onSave(patch), () => props.onSave(patch));
1417
+ };
1418
+ props.commitRef.current = commit;
1419
+ const invalid = startAt === void 0 || endAt === void 0 || !timeRangeValid;
1420
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: "board-detail", "aria-label": props.t("name"), onSubmit: (event) => {
1421
+ event.preventDefault();
1422
+ commit();
1423
+ }, onKeyDown: (event) => {
1424
+ if (event.target instanceof Element && event.target.closest(".board-log-composer") !== null) return;
1425
+ if (event.key !== "Enter") return;
1426
+ if (event.target instanceof HTMLInputElement || (event.metaKey || event.ctrlKey) && event.target instanceof HTMLTextAreaElement) {
1427
+ event.preventDefault();
1428
+ commit();
1429
+ }
1430
+ }, children: [
1431
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-header", children: [
1432
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-detail-heading", children: props.t("cardDetails") }),
1433
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", "aria-label": props.t("close"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
1434
+ ] }),
1435
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-editor", children: [
1436
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-main", children: [
1437
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "board-field-label first", children: [
1438
+ props.t("titlePlaceholder"),
1439
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: titleRef, className: "board-field", value: title, onChange: (event) => setTitle(event.target.value), onBlur: () => {
1440
+ if (title.trim().length === 0) setTitle(saved.current.title);
1441
+ else commit();
1442
+ } })
1443
+ ] }),
1444
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "board-field-label board-notes-field", children: [
1445
+ props.t("notes"),
1446
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("textarea", { value: body, placeholder: props.t("notesPlaceholder"), onChange: (event) => setBody(event.target.value), onBlur: () => commit() })
1447
+ ] })
1448
+ ] }),
1449
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-detail-schedule", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("fieldset", { className: "board-time-fieldset detail", children: [
1450
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("legend", { children: props.t("taskTime") }),
1451
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DateTimeEditor, { kind: "start", label: props.t("startTime"), placeholder: props.t("selectStartTime"), date: startDate, time: startTime, t: props.t, onDate: (value) => {
1452
+ setStartDate(value);
1453
+ commit({ startDate: value });
1454
+ }, onTime: setStartTime, onCommit: () => commit() }),
1455
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DateTimeEditor, { kind: "end", label: props.t("endTime"), placeholder: props.t("selectEndTime"), date: endDate, time: endTime, t: props.t, onDate: (value) => {
1456
+ setEndDate(value);
1457
+ commit({ endDate: value });
1458
+ }, onTime: setEndTime, onCommit: () => commit() }),
1459
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeReminderToggle, { checked: timeReminder, disabled: !timeReminder && startDate.length === 0 && endDate.length === 0, t: props.t, onChange: (checked) => {
1460
+ setTimeReminder(checked);
1461
+ commit({ timeReminder: checked });
1462
+ } }),
1463
+ invalid && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-field-error", children: props.t("invalidTimeRange") })
1464
+ ] }) })
1465
+ ] }),
1466
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardLogSection, { card: props.card, t: props.t, onAdd: props.onAddLog, onDownload: props.onDownloadAttachment, onOpen: props.onOpenAttachment })
1467
+ ] });
1468
+ }
1469
+ function CardLogSection(props) {
1470
+ const [text, setText] = (0, import_react3.useState)("");
1471
+ const [files, setFiles] = (0, import_react3.useState)([]);
1472
+ const [submitting, setSubmitting] = (0, import_react3.useState)(false);
1473
+ const [error, setError] = (0, import_react3.useState)(null);
1474
+ const [imagePreview, setImagePreview] = (0, import_react3.useState)(null);
1475
+ const fileInput = (0, import_react3.useRef)(null);
1476
+ (0, import_react3.useEffect)(() => () => {
1477
+ if (imagePreview !== null) URL.revokeObjectURL(imagePreview.url);
1478
+ }, [imagePreview]);
1479
+ const addFiles = (incoming) => {
1480
+ const next = [...files, ...Array.from(incoming)];
1481
+ if (next.length > 10 || next.some((file) => file.size > 50 * 1024 * 1024)) {
1482
+ setError(props.t("attachmentLimit"));
1483
+ return;
1484
+ }
1485
+ setError(null);
1486
+ setFiles(next);
1487
+ };
1488
+ const submit = async () => {
1489
+ if (submitting || text.trim().length === 0 && files.length === 0) return;
1490
+ setSubmitting(true);
1491
+ setError(null);
1492
+ try {
1493
+ await props.onAdd(text, await Promise.all(files.map(fileUpload)));
1494
+ setText("");
1495
+ setFiles([]);
1496
+ } catch (reason) {
1497
+ setError(readableLogError(reason, props.t));
1498
+ } finally {
1499
+ setSubmitting(false);
1500
+ }
1501
+ };
1502
+ const openImage = async (attachment) => {
1503
+ try {
1504
+ setError(null);
1505
+ const file = await props.onDownload(attachment.id);
1506
+ const bytes = base64Bytes(file.data);
1507
+ const url = URL.createObjectURL(new Blob([bytes], { type: file.type || "application/octet-stream" }));
1508
+ setImagePreview({ url, name: file.name });
1509
+ } catch (reason) {
1510
+ setError(readableLogError(reason, props.t));
1511
+ }
1512
+ };
1513
+ const openFile = async (attachment) => {
1514
+ try {
1515
+ setError(null);
1516
+ await props.onOpen(attachment.id);
1517
+ } catch (reason) {
1518
+ setError(readableLogError(reason, props.t));
1519
+ }
1520
+ };
1521
+ const logs = [...props.card.logs].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
1522
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "board-log-section", "aria-label": props.t("logs"), children: [
1523
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-log-heading", children: [
1524
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("logs") }),
1525
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("logCount", { n: logs.length }) })
1526
+ ] }),
1527
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-scroll", children: logs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-empty", children: props.t("noLogs") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-list", children: logs.map((log) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { className: "board-log-item", children: [
1528
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("time", { dateTime: log.createdAt, children: formatLogDate(log.createdAt) }),
1529
+ log.text.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: log.text }),
1530
+ log.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-attachments", children: log.attachments.map((attachment) => isImageAttachment(attachment) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-log-image", title: props.t("viewImage", { name: attachment.name }), onClick: () => void openImage(attachment), children: [
1531
+ attachment.preview !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: attachment.preview, alt: "" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-log-image-placeholder", children: props.t("imagePlaceholder") }),
1532
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: attachment.name }),
1533
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { children: formatFileSize(attachment.size) })
1534
+ ] }, attachment.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-log-file", title: props.t("openFile", { name: attachment.name }), onClick: () => void openFile(attachment), children: [
1535
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: attachment.name }),
1536
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { children: formatFileSize(attachment.size) })
1537
+ ] }, attachment.id)) })
1538
+ ] }, log.id)) }) }),
1539
+ error !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-field-error", children: error }),
1540
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-log-composer", children: [
1541
+ files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-log-pending", children: files.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1542
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: file.name, children: file.name }),
1543
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "aria-label": props.t("removeAttachment", { name: file.name }), onClick: () => setFiles((current) => current.filter((_, itemIndex) => itemIndex !== index)), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
1544
+ ] }, `${file.name}:${String(file.lastModified)}:${String(index)}`)) }),
1545
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-log-compose-row", children: [
1546
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { className: "board-log-input", value: text, placeholder: props.t("logPlaceholder"), onChange: (event) => setText(event.target.value), onPaste: (event) => {
1547
+ const pasted = Array.from(event.clipboardData.items).flatMap((item) => {
1548
+ const file = item.kind === "file" ? item.getAsFile() : null;
1549
+ return file === null ? [] : [file];
1550
+ });
1551
+ if (pasted.length > 0) addFiles(pasted);
1552
+ }, onKeyDown: (event) => {
1553
+ if (event.key === "Enter" && !event.nativeEvent.isComposing) {
1554
+ event.preventDefault();
1555
+ void submit();
1556
+ }
1557
+ } }),
1558
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: fileInput, type: "file", multiple: true, tabIndex: -1, onChange: (event) => {
1559
+ if (event.target.files !== null) addFiles(event.target.files);
1560
+ event.currentTarget.value = "";
1561
+ } }),
1562
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn board-log-upload", title: props.t("uploadAttachment"), "aria-label": props.t("uploadAttachment"), onClick: () => fileInput.current?.click(), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AttachmentIcon, {}) }),
1563
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn primary", disabled: submitting || text.trim().length === 0 && files.length === 0, onClick: () => void submit(), children: props.t("submitLog") })
1564
+ ] })
1565
+ ] }),
1566
+ imagePreview !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ImagePreviewLayer, { preview: imagePreview, t: props.t, onClose: () => setImagePreview(null) })
1567
+ ] });
1568
+ }
1569
+ function ImagePreviewLayer(props) {
1570
+ (0, import_react3.useEffect)(() => {
1571
+ const close = (event) => {
1572
+ if (event.key === "Escape") {
1573
+ event.stopPropagation();
1574
+ props.onClose();
1575
+ }
1576
+ };
1577
+ document.addEventListener("keydown", close);
1578
+ return () => document.removeEventListener("keydown", close);
1579
+ }, [props]);
1580
+ return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-image-preview", role: "dialog", "aria-modal": "true", "aria-label": props.preview.name, onPointerDown: (event) => {
1581
+ if (event.target === event.currentTarget) props.onClose();
1582
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-image-preview-frame", children: [
1583
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", "aria-label": props.t("close"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) }),
1584
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: props.preview.url, alt: props.preview.name }),
1585
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.preview.name })
1586
+ ] }) }), document.body);
1587
+ }
1588
+ function DateTimeEditor(props) {
1589
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "board-datetime-label", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "board-datetime-row", children: [
1590
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-datetime-icon", title: props.label, children: props.kind === "start" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PlayIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StopIcon, {}) }),
1591
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CalendarPicker, { value: props.date, placeholder: props.placeholder, t: props.t, onChange: props.onDate }),
1592
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { className: "board-time-input", type: "time", value: props.time, "aria-label": props.label, disabled: props.date.length === 0, onChange: (event) => props.onTime(event.target.value), onBlur: props.onCommit }),
1593
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-time-clear", tabIndex: -1, title: props.t("clearTime"), "aria-label": `${props.t("clearTime")} \xB7 ${props.label}`, disabled: props.date.length === 0, onClick: () => props.onDate(""), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
1594
+ ] }) });
1595
+ }
1596
+ function TimeReminderToggle(props) {
1597
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "board-time-reminder", children: [
1598
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "checkbox", checked: props.checked, disabled: props.disabled, onChange: (event) => {
1599
+ const checked = event.target.checked;
1600
+ props.onChange(checked);
1601
+ if (checked && typeof Notification !== "undefined" && Notification.permission === "default") void Notification.requestPermission();
1602
+ } }),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1604
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: props.t("timeReminder") }),
1605
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { children: props.t("timeReminderHint") })
1606
+ ] })
1607
+ ] });
1608
+ }
1609
+ function CardTimeProgress(props) {
1610
+ const { card, nowMs, t } = props;
1611
+ if (card.startAt === null && card.endAt === null) return null;
1612
+ const explicitStart = card.startAt === null ? null : Date.parse(card.startAt);
1613
+ const end = card.endAt === null ? null : Date.parse(card.endAt);
1614
+ const created = Date.parse(card.createdAt);
1615
+ let tone = "normal";
1616
+ let percent = 0;
1617
+ let status = "";
1618
+ if (end !== null) {
1619
+ const start = explicitStart ?? Math.min(created, end - 1);
1620
+ if (nowMs < start) {
1621
+ status = t("startsIn", { time: formatDuration(start - nowMs) });
1622
+ } else if (nowMs >= end) {
1623
+ percent = 100;
1624
+ tone = "overdue";
1625
+ status = t("timeOverdue", { time: formatDuration(nowMs - end) });
1626
+ } else {
1627
+ percent = Math.max(0, Math.min(100, (nowMs - start) / (end - start) * 100));
1628
+ const remaining = end - nowMs;
1629
+ tone = remaining <= 24 * 60 * 60 * 1e3 || percent >= 85 ? "urgent" : remaining <= 3 * 24 * 60 * 60 * 1e3 || percent >= 65 ? "warning" : "normal";
1630
+ status = t("timeRemaining", { time: formatDuration(remaining) });
1631
+ }
1632
+ } else if (explicitStart !== null) {
1633
+ if (nowMs < explicitStart) status = t("startsIn", { time: formatDuration(explicitStart - nowMs) });
1634
+ else {
1635
+ percent = 100;
1636
+ tone = "active";
1637
+ status = t("startedFor", { time: formatDuration(nowMs - explicitStart) });
1638
+ }
1639
+ }
1640
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `board-time-progress ${tone}`, title: `${card.startAt === null ? "" : `${t("startTime")}: ${formatCardDate(card.startAt)}`}${card.startAt !== null && card.endAt !== null ? "\n" : ""}${card.endAt === null ? "" : `${t("endTime")}: ${formatCardDate(card.endAt)}`}`, children: [
1641
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-time-status", children: [
1642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: status }),
1643
+ end !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: t("progressPercent", { n: Math.round(percent) }) })
1644
+ ] }),
1645
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-time-track", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { width: `${percent}%` } }) }),
1646
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-time-bounds", children: [
1647
+ card.startAt !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1648
+ "\u25B6 ",
1649
+ formatCardDate(card.startAt)
1650
+ ] }),
1651
+ card.endAt !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
1652
+ "\u25A0 ",
1653
+ formatCardDate(card.endAt)
1654
+ ] })
1655
+ ] })
1656
+ ] });
1657
+ }
1658
+ function ConfirmDialog(props) {
1659
+ const confirmRef = (0, import_react3.useRef)(null);
1660
+ (0, import_react3.useEffect)(() => {
1661
+ confirmRef.current?.focus();
1662
+ const escape = (event) => {
1663
+ if (event.key === "Escape") props.onCancel();
1664
+ };
1665
+ document.addEventListener("keydown", escape);
1666
+ return () => document.removeEventListener("keydown", escape);
1667
+ }, [props]);
1668
+ const title = props.target.kind === "column" ? props.t("deleteColumnTitle", { title: props.target.title }) : props.target.kind === "cards" ? props.t("deleteCompletedTitle", { n: props.target.count }) : props.t("deleteCardTitle", { title: props.target.title });
1669
+ const detail = props.target.kind === "column" ? props.t("deleteColumn", { n: props.target.activeCount, m: props.target.archivedCount }) : props.target.kind === "cards" ? props.t("deleteCompletedConfirm") : props.t("deleteCardConfirm");
1670
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-confirm-backdrop", role: "presentation", onPointerDown: (event) => {
1671
+ if (event.target === event.currentTarget) props.onCancel();
1672
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: "board-confirm", role: "alertdialog", "aria-modal": "true", "aria-labelledby": "board-confirm-title", children: [
1673
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-confirm-mark", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertIcon, {}) }),
1674
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-confirm-copy", children: [
1675
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { id: "board-confirm-title", children: title }),
1676
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: detail }),
1677
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { children: props.t("deleteWarning") })
1678
+ ] }),
1679
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-confirm-actions", children: [
1680
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn", onClick: props.onCancel, children: props.t("cancel") }),
1681
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { ref: confirmRef, type: "button", className: "board-btn confirm-danger", onClick: props.onConfirm, children: props.t("confirmDelete") })
1682
+ ] })
1683
+ ] }) });
1684
+ }
1685
+ function ArchivePanelLayer(props) {
1686
+ const drawer = (0, import_react3.useRef)(null);
1687
+ (0, import_react3.useEffect)(() => {
1688
+ const close = (event) => {
1689
+ if (event.key !== "Escape" || document.querySelector(".board-select-portal,.board-confirm-backdrop") !== null) return;
1690
+ event.stopPropagation();
1691
+ props.onClose();
1692
+ };
1693
+ const closeOutside = (event) => {
1694
+ if (!(event.target instanceof Node) || drawer.current?.contains(event.target)) return;
1695
+ if (event.target instanceof Element && event.target.closest(".board-select-portal,.board-confirm-backdrop") !== null) return;
1696
+ props.onClose();
1697
+ };
1698
+ document.addEventListener("keydown", close);
1699
+ document.addEventListener("pointerdown", closeOutside, true);
1700
+ return () => {
1701
+ document.removeEventListener("keydown", close);
1702
+ document.removeEventListener("pointerdown", closeOutside, true);
1703
+ };
1704
+ }, [props.onClose]);
1705
+ return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-session-detail-layer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: drawer, className: "board-session-detail-drawer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArchivePanel, { ...props }) }) }), document.body);
1706
+ }
1707
+ function ArchivePanel(props) {
1708
+ const [filter, setFilter] = (0, import_react3.useState)("all");
1709
+ const [targets, setTargets] = (0, import_react3.useState)({});
1710
+ const [selected, setSelected] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set());
1711
+ const [batchColumnId, setBatchColumnId] = (0, import_react3.useState)(props.columns[0]?.id ?? "");
1712
+ const selectAllRef = (0, import_react3.useRef)(null);
1713
+ const columns = [...props.columns].sort((a, b) => a.order - b.order);
1714
+ const cards = [...props.cards].filter((card) => filter === "all" || archivedColumnKey(card) === filter).sort((a, b) => Date.parse(b.archivedAt ?? "") - Date.parse(a.archivedAt ?? ""));
1715
+ const targetOf = (card) => targets[card.id] ?? (columns.some((column) => column.id === card.columnId) ? card.columnId : columns[0]?.id ?? "");
1716
+ const visibleIds = cards.map((card) => card.id);
1717
+ const selectedCards = props.cards.filter((card) => selected.has(card.id));
1718
+ const columnOptions = columns.map((column) => ({ value: column.id, label: column.title }));
1719
+ const archivedColumnOptions = [...new Map(props.cards.flatMap((card) => card.archivedColumn === null ? [] : [[archivedColumnKey(card), { value: archivedColumnKey(card), label: card.archivedColumn.title }]])).values()];
1720
+ const archivedColumnIds = new Set(archivedColumnOptions.map((option) => option.value));
1721
+ const allVisibleSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
1722
+ const someVisibleSelected = visibleIds.some((id) => selected.has(id));
1723
+ (0, import_react3.useEffect)(() => {
1724
+ if (selectAllRef.current !== null) selectAllRef.current.indeterminate = someVisibleSelected && !allVisibleSelected;
1725
+ }, [allVisibleSelected, someVisibleSelected]);
1726
+ (0, import_react3.useEffect)(() => {
1727
+ setSelected((current) => new Set([...current].filter((id) => props.cards.some((card) => card.id === id))));
1728
+ }, [props.cards]);
1729
+ (0, import_react3.useEffect)(() => {
1730
+ if (!columns.some((column) => column.id === batchColumnId)) setBatchColumnId(columns[0]?.id ?? "");
1731
+ }, [batchColumnId, columns]);
1732
+ (0, import_react3.useEffect)(() => {
1733
+ if (filter !== "all" && !archivedColumnIds.has(filter)) setFilter("all");
1734
+ }, [filter, props.cards]);
1735
+ const toggleAll = () => setSelected((current) => {
1736
+ const next = new Set(current);
1737
+ if (allVisibleSelected) for (const id of visibleIds) next.delete(id);
1738
+ else for (const id of visibleIds) next.add(id);
1739
+ return next;
1740
+ });
1741
+ const restoreSelected = async () => {
1742
+ if (selectedCards.length === 0 || batchColumnId.length === 0) return;
1743
+ await props.onRestoreMany(selectedCards.map((card) => ({ cardId: card.id, columnId: batchColumnId })));
1744
+ setSelected(/* @__PURE__ */ new Set());
1745
+ };
1746
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("aside", { className: "board-detail board-archive", "aria-label": props.t("completedTasks"), children: [
1747
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-detail-header", children: [
1748
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "board-detail-heading", children: [
1749
+ props.t("completedTasks"),
1750
+ " \xB7 ",
1751
+ props.cards.length
1752
+ ] }),
1753
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-iconbtn", "aria-label": props.t("close"), onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CloseIcon, {}) })
1754
+ ] }),
1755
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-field-label first", children: [
1756
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("filterByColumn") }),
1757
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BoardSelect, { ariaLabel: props.t("filterByColumn"), searchPlaceholder: props.t("searchColumns"), emptyText: props.t("noMatchingColumns"), value: filter, options: [{ value: "all", label: props.t("allColumns") }, ...archivedColumnOptions], onChange: setFilter })
1758
+ ] }),
1759
+ cards.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-archive-selectbar", children: [
1760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { children: [
1761
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: selectAllRef, type: "checkbox", checked: allVisibleSelected, onChange: toggleAll }),
1762
+ props.t("selectAll")
1763
+ ] }),
1764
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("selectedCount", { n: selectedCards.length }) })
1765
+ ] }),
1766
+ selectedCards.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-archive-batch", children: [
1767
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BoardSelect, { ariaLabel: props.t("restoreTo"), searchPlaceholder: props.t("searchColumns"), emptyText: props.t("noMatchingColumns"), value: batchColumnId, options: columnOptions, onChange: setBatchColumnId }),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn primary", disabled: batchColumnId.length === 0, onClick: () => void restoreSelected(), children: props.t("restoreSelected") }),
1769
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-btn danger board-batch-delete", onClick: () => props.onRequestDelete(selectedCards), children: [
1770
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrashIcon, {}),
1771
+ props.t("deleteSelected")
1772
+ ] })
1773
+ ] }),
1774
+ cards.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-archive-empty", children: props.t("noCompletedTasks") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-archive-list", children: cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { className: `board-archive-card${selected.has(card.id) ? " selected" : ""}`, children: [
1775
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-archive-card-head", children: [
1776
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "board-archive-card-check", children: [
1777
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "checkbox", checked: selected.has(card.id), "aria-label": props.t("selectTask", { title: card.title }), onChange: () => setSelected((current) => {
1778
+ const next = new Set(current);
1779
+ if (next.has(card.id)) next.delete(card.id);
1780
+ else next.add(card.id);
1781
+ return next;
1782
+ }) }),
1783
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: card.title })
1784
+ ] }),
1785
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-archive-delete", title: props.t("deleteCard"), "aria-label": props.t("deleteCard"), onClick: () => props.onRequestDelete([card]), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrashIcon, {}) })
1786
+ ] }),
1787
+ card.body.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "preview", children: card.body }),
1788
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-archive-meta", children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: formatArchivedDate(card.archivedAt) }),
1790
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: card.archivedColumn?.title })
1791
+ ] }),
1792
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-restore-row", children: [
1793
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.t("restoreTo") }),
1794
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BoardSelect, { ariaLabel: props.t("restoreTo"), searchPlaceholder: props.t("searchColumns"), emptyText: props.t("noMatchingColumns"), value: targetOf(card), options: columnOptions, onChange: (value) => setTargets((current) => ({ ...current, [card.id]: value })) }),
1795
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn primary", disabled: targetOf(card).length === 0, onClick: () => void props.onRestore(card.id, targetOf(card)), children: props.t("restore") })
1796
+ ] })
1797
+ ] }, card.id)) })
1798
+ ] });
1799
+ }
1800
+ function archivedColumnKey(card) {
1801
+ return card.archivedColumn === null ? "" : `${card.archivedColumn.id}
1802
+ ${card.archivedColumn.title}`;
1803
+ }
1804
+ function BoardSelect(props) {
1805
+ const [open, setOpen] = (0, import_react3.useState)(false);
1806
+ const [query, setQuery] = (0, import_react3.useState)("");
1807
+ const [activeIndex, setActiveIndex] = (0, import_react3.useState)(0);
1808
+ const [position, setPosition] = (0, import_react3.useState)({ top: 0, left: 0, width: 0 });
1809
+ const anchor = (0, import_react3.useRef)(null);
1810
+ const trigger = (0, import_react3.useRef)(null);
1811
+ const menu = (0, import_react3.useRef)(null);
1812
+ const search = (0, import_react3.useRef)(null);
1813
+ const listId = (0, import_react3.useId)();
1814
+ const searchable = props.options.length > 10;
1815
+ const selected = props.options.find((option) => option.value === props.value);
1816
+ const normalizedQuery = query.trim().toLocaleLowerCase(activeLocale());
1817
+ const visibleOptions = normalizedQuery.length === 0 ? props.options : props.options.filter((option) => option.label.toLocaleLowerCase(activeLocale()).includes(normalizedQuery));
1818
+ const updatePosition = () => {
1819
+ if (anchor.current === null) return;
1820
+ const rect = anchor.current.getBoundingClientRect();
1821
+ const height = Math.min(visibleOptions.length * 34 + 8, 224);
1822
+ const top = rect.bottom + 5 + height <= window.innerHeight - 8 ? rect.bottom + 5 : Math.max(8, rect.top - height - 5);
1823
+ setPosition({ top, left: Math.max(8, Math.min(rect.left, window.innerWidth - rect.width - 8)), width: rect.width });
1824
+ };
1825
+ (0, import_react3.useLayoutEffect)(() => {
1826
+ if (open) {
1827
+ updatePosition();
1828
+ if (searchable) search.current?.focus();
1829
+ }
1830
+ }, [open, searchable, visibleOptions.length]);
1831
+ (0, import_react3.useLayoutEffect)(() => {
1832
+ if (open && activeIndex >= 0) menu.current?.querySelector(`[data-option-index="${String(activeIndex)}"]`)?.scrollIntoView({ block: "nearest" });
1833
+ }, [activeIndex, open]);
1834
+ (0, import_react3.useEffect)(() => {
1835
+ if (!open) return;
1836
+ const closeOutside = (event) => {
1837
+ if (event.target instanceof Node && !anchor.current?.contains(event.target) && !menu.current?.contains(event.target)) setOpen(false);
1838
+ };
1839
+ const reposition = () => updatePosition();
1840
+ document.addEventListener("pointerdown", closeOutside);
1841
+ window.addEventListener("resize", reposition);
1842
+ window.addEventListener("scroll", reposition, true);
1843
+ return () => {
1844
+ document.removeEventListener("pointerdown", closeOutside);
1845
+ window.removeEventListener("resize", reposition);
1846
+ window.removeEventListener("scroll", reposition, true);
1847
+ };
1848
+ }, [open, visibleOptions.length]);
1849
+ (0, import_react3.useEffect)(() => {
1850
+ if (!open) return;
1851
+ const index = visibleOptions.findIndex((option) => option.value === props.value);
1852
+ setActiveIndex(visibleOptions.length === 0 ? -1 : Math.max(0, index));
1853
+ }, [open, query, props.value, props.options]);
1854
+ const close = () => {
1855
+ setOpen(false);
1856
+ requestAnimationFrame(() => trigger.current?.focus());
1857
+ };
1858
+ const select = (value) => {
1859
+ props.onChange(value);
1860
+ close();
1861
+ };
1862
+ const show = () => {
1863
+ setQuery("");
1864
+ const index = props.options.findIndex((option) => option.value === props.value);
1865
+ setActiveIndex(Math.max(0, index));
1866
+ setOpen(true);
1867
+ };
1868
+ const onKeyDown = (event) => {
1869
+ if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1870
+ event.preventDefault();
1871
+ show();
1872
+ return;
1873
+ }
1874
+ if (!open) return;
1875
+ if (event.key === "Escape") {
1876
+ event.preventDefault();
1877
+ close();
1878
+ return;
1879
+ }
1880
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1881
+ event.preventDefault();
1882
+ if (visibleOptions.length > 0) setActiveIndex((current) => (Math.max(0, current) + (event.key === "ArrowDown" ? 1 : visibleOptions.length - 1)) % visibleOptions.length);
1883
+ return;
1884
+ }
1885
+ if (event.key === "Enter" && activeIndex >= 0 && visibleOptions[activeIndex] !== void 0) {
1886
+ event.preventDefault();
1887
+ select(visibleOptions[activeIndex].value);
1888
+ }
1889
+ };
1890
+ const popup = open ? (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: menu, id: listId, className: "board-select-portal", role: "listbox", "aria-label": props.ariaLabel, style: position, children: visibleOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-select-empty", children: props.emptyText }) : visibleOptions.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { id: `${listId}-${String(index)}`, type: "button", "data-option-index": index, className: `board-select-option${index === activeIndex ? " active" : ""}`, role: "option", tabIndex: -1, "aria-selected": option.value === props.value, title: option.label, onPointerEnter: () => setActiveIndex(index), onClick: () => select(option.value), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label }) }, option.value)) }), document.body) : null;
1891
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: anchor, className: "board-select", children: [
1892
+ open && searchable ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-select-trigger board-select-combobox", children: [
1893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SearchIcon, {}),
1894
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { ref: search, role: "combobox", "aria-label": props.ariaLabel, "aria-controls": listId, "aria-expanded": "true", "aria-activedescendant": activeIndex >= 0 ? `${listId}-${String(activeIndex)}` : void 0, value: query, placeholder: props.searchPlaceholder, onChange: (event) => setQuery(event.target.value), onKeyDown }),
1895
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "down" })
1896
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { ref: trigger, type: "button", className: "board-select-trigger", "aria-label": props.ariaLabel, "aria-haspopup": "listbox", "aria-controls": listId, "aria-expanded": open, "aria-activedescendant": open && activeIndex >= 0 ? `${listId}-${String(activeIndex)}` : void 0, onKeyDown, onClick: () => {
1897
+ if (open) close();
1898
+ else show();
1899
+ }, children: [
1900
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: selected?.label ?? "" }),
1901
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "down" })
1902
+ ] }),
1903
+ popup
1904
+ ] });
1905
+ }
1906
+ function CalendarPicker(props) {
1907
+ const [open, setOpen] = (0, import_react3.useState)(false);
1908
+ const [month, setMonth] = (0, import_react3.useState)(() => monthOf(props.value));
1909
+ const [position, setPosition] = (0, import_react3.useState)({ top: 8, left: 8 });
1910
+ const root = (0, import_react3.useRef)(null);
1911
+ const popover = (0, import_react3.useRef)(null);
1912
+ const updatePosition = () => {
1913
+ if (root.current === null || popover.current === null) return;
1914
+ const trigger = root.current.getBoundingClientRect();
1915
+ const popup = popover.current.getBoundingClientRect();
1916
+ const gap = 6;
1917
+ const edge = 8;
1918
+ const below = trigger.bottom + gap;
1919
+ const top = below + popup.height <= window.innerHeight - edge ? below : Math.max(edge, trigger.top - popup.height - gap);
1920
+ const left = Math.max(edge, Math.min(trigger.left, window.innerWidth - popup.width - edge));
1921
+ setPosition((current) => current.top === top && current.left === left ? current : { top, left });
1922
+ };
1923
+ (0, import_react3.useEffect)(() => {
1924
+ if (props.value.length > 0) setMonth(monthOf(props.value));
1925
+ }, [props.value]);
1926
+ (0, import_react3.useLayoutEffect)(() => {
1927
+ if (open) updatePosition();
1928
+ }, [month, open]);
1929
+ (0, import_react3.useEffect)(() => {
1930
+ if (!open) return;
1931
+ const close = (event) => {
1932
+ if (event.target instanceof Node && !root.current?.contains(event.target) && !popover.current?.contains(event.target)) setOpen(false);
1933
+ };
1934
+ const escape = (event) => {
1935
+ if (event.key === "Escape") setOpen(false);
1936
+ };
1937
+ const reposition = () => updatePosition();
1938
+ document.addEventListener("pointerdown", close);
1939
+ document.addEventListener("keydown", escape);
1940
+ window.addEventListener("resize", reposition);
1941
+ window.addEventListener("scroll", reposition, true);
1942
+ return () => {
1943
+ document.removeEventListener("pointerdown", close);
1944
+ document.removeEventListener("keydown", escape);
1945
+ window.removeEventListener("resize", reposition);
1946
+ window.removeEventListener("scroll", reposition, true);
1947
+ };
1948
+ }, [open]);
1949
+ const today = dateKey(/* @__PURE__ */ new Date());
1950
+ const calendar = open ? (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: popover, className: "board-date-popover board-date-popover-portal", style: position, role: "dialog", "aria-label": props.placeholder, children: [
1951
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-calendar-head", children: [
1952
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-calendar-nav", "aria-label": props.t("previousMonth"), onClick: () => setMonth((current) => new Date(current.getFullYear(), current.getMonth() - 1, 1)), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "left" }) }),
1953
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-calendar-month", children: month.toLocaleDateString(activeLocale(), { year: "numeric", month: "long" }) }),
1954
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-calendar-nav", "aria-label": props.t("nextMonth"), onClick: () => setMonth((current) => new Date(current.getFullYear(), current.getMonth() + 1, 1)), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "right" }) })
1955
+ ] }),
1956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "board-calendar-grid", children: [
1957
+ weekdays().map((day) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "board-calendar-weekday", children: day }, day)),
1958
+ calendarDays(month).map((day) => {
1959
+ const key = dateKey(day.date);
1960
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: `board-calendar-day${day.inMonth ? "" : " muted"}${key === today ? " today" : ""}${key === props.value ? " selected" : ""}`, onClick: () => {
1961
+ props.onChange(key);
1962
+ setOpen(false);
1963
+ }, children: day.date.getDate() }, key);
1964
+ })
1965
+ ] }),
1966
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-calendar-footer", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "board-btn", onClick: () => {
1967
+ props.onChange(today);
1968
+ setMonth(monthOf(today));
1969
+ setOpen(false);
1970
+ }, children: props.t("today") }) })
1971
+ ] }), document.body) : null;
1972
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1973
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "board-date-picker", ref: root, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { type: "button", className: "board-date-trigger", "aria-label": props.placeholder, "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => setOpen((current) => !current), children: [
1974
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: props.value.length > 0 ? displayDate(props.value) : props.placeholder }),
1975
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "calendar-mark", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ChevronIcon, { direction: "down" }) })
1976
+ ] }) }),
1977
+ calendar
1978
+ ] });
1979
+ }
1980
+ function datePart(iso) {
1981
+ if (iso === null || iso === void 0) return "";
1982
+ const date = new Date(iso);
1983
+ return Number.isNaN(date.getTime()) ? "" : dateKey(date);
1984
+ }
1985
+ function timePart(iso, fallback) {
1986
+ if (iso === null || iso === void 0) return fallback;
1987
+ const date = new Date(iso);
1988
+ return Number.isNaN(date.getTime()) ? fallback : `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
1989
+ }
1990
+ function toIso(date, time) {
1991
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time)) return null;
1992
+ const value = /* @__PURE__ */ new Date(`${date}T${time}`);
1993
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
1994
+ }
1995
+ function optionalIso(date, time) {
1996
+ return date.length === 0 ? null : toIso(date, time) ?? void 0;
1997
+ }
1998
+ function validTimeRange(startAt, endAt) {
1999
+ return startAt !== void 0 && endAt !== void 0 && (startAt === null || endAt === null || Date.parse(startAt) < Date.parse(endAt));
2000
+ }
2001
+ function dateKey(date) {
2002
+ return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
2003
+ }
2004
+ function monthOf(key) {
2005
+ const [year, month] = key.split("-").map(Number);
2006
+ return Number.isSafeInteger(year) && Number.isSafeInteger(month) ? new Date(year, month - 1, 1) : new Date((/* @__PURE__ */ new Date()).getFullYear(), (/* @__PURE__ */ new Date()).getMonth(), 1);
2007
+ }
2008
+ function calendarDays(month) {
2009
+ const first = new Date(month.getFullYear(), month.getMonth(), 1);
2010
+ const offset = (first.getDay() + 6) % 7;
2011
+ return Array.from({ length: 42 }, (_, index) => {
2012
+ const date = new Date(month.getFullYear(), month.getMonth(), 1 - offset + index);
2013
+ return { date, inMonth: date.getMonth() === month.getMonth() };
2014
+ });
2015
+ }
2016
+ function weekdays() {
2017
+ const monday = new Date(2024, 0, 1);
2018
+ return Array.from({ length: 7 }, (_, index) => new Intl.DateTimeFormat(activeLocale(), { weekday: "narrow" }).format(new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + index)));
2019
+ }
2020
+ function displayDate(value) {
2021
+ const [year, month, day] = value.split("-").map(Number);
2022
+ const date = new Date(year ?? 0, (month ?? 1) - 1, day ?? 1);
2023
+ return date.toLocaleDateString(activeLocale(), { year: "numeric", month: "numeric", day: "numeric" });
2024
+ }
2025
+ function formatCardDate(iso) {
2026
+ const date = new Date(iso);
2027
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString(activeLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
2028
+ }
2029
+ function formatArchivedDate(iso) {
2030
+ if (iso === null) return "";
2031
+ const date = new Date(iso);
2032
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString(activeLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
2033
+ }
2034
+ function formatLogDate(iso) {
2035
+ const date = new Date(iso);
2036
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString(activeLocale(), { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
2037
+ }
2038
+ function formatFileSize(size) {
2039
+ return size < 1024 ? `${String(size)} B` : size < 1024 * 1024 ? `${(size / 1024).toFixed(1)} KB` : `${(size / (1024 * 1024)).toFixed(1)} MB`;
2040
+ }
2041
+ function readableLogError(reason, t) {
2042
+ const message = reason instanceof Error ? reason.message : String(reason);
2043
+ return message.includes("unknown board rpc") ? t("hostRestartRequired") : message;
2044
+ }
2045
+ function isImageAttachment(attachment) {
2046
+ return attachment.type.startsWith("image/") || /\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(attachment.name);
2047
+ }
2048
+ async function fileUpload(file) {
2049
+ const [url, preview] = await Promise.all([readDataUrl(file), createImagePreview(file)]);
2050
+ return { name: file.name || "clipboard-image.png", type: file.type, data: url.slice(url.indexOf(",") + 1), ...preview === void 0 ? {} : { preview } };
2051
+ }
2052
+ async function createImagePreview(file) {
2053
+ if (!file.type.startsWith("image/") && !/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(file.name)) return void 0;
2054
+ try {
2055
+ const bitmap = await createImageBitmap(file);
2056
+ const ratio = Math.min(1, 240 / bitmap.width, 160 / bitmap.height);
2057
+ const canvas = document.createElement("canvas");
2058
+ canvas.width = Math.max(1, Math.round(bitmap.width * ratio));
2059
+ canvas.height = Math.max(1, Math.round(bitmap.height * ratio));
2060
+ canvas.getContext("2d")?.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
2061
+ bitmap.close();
2062
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/webp", 0.78));
2063
+ return blob === null ? void 0 : readDataUrl(blob);
2064
+ } catch {
2065
+ return void 0;
2066
+ }
2067
+ }
2068
+ function readDataUrl(blob) {
2069
+ return new Promise((resolve, reject) => {
2070
+ const reader = new FileReader();
2071
+ reader.onerror = () => reject(reader.error ?? new Error("file read failed"));
2072
+ reader.onload = () => resolve(String(reader.result));
2073
+ reader.readAsDataURL(blob);
2074
+ });
2075
+ }
2076
+ function base64Bytes(data) {
2077
+ const binary = atob(data);
2078
+ const bytes = new Uint8Array(binary.length);
2079
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
2080
+ return bytes.buffer;
2081
+ }
2082
+ function formatDuration(ms) {
2083
+ const minutes = Math.max(1, Math.round(ms / 6e4));
2084
+ const [value, unit] = minutes < 60 ? [minutes, "minute"] : minutes < 24 * 60 ? [Math.round(minutes / 60), "hour"] : [Math.round(minutes / (24 * 60)), "day"];
2085
+ return new Intl.NumberFormat(activeLocale(), { style: "unit", unit, unitDisplay: "short" }).format(value);
2086
+ }
2087
+ function activeLocale() {
2088
+ return document.documentElement.lang || void 0;
2089
+ }
2090
+ function cloneLayout(layout) {
2091
+ return Object.fromEntries(Object.entries(layout).map(([columnId, ids]) => [columnId, [...ids]]));
2092
+ }
2093
+ function sameIds(a, b) {
2094
+ return a.length === b.length && a.every((id, index) => id === b[index]);
2095
+ }
2096
+
2097
+ // src/client/sessionTypes.ts
2098
+ function sessionTitle(summary) {
2099
+ const title = summary?.title?.trim();
2100
+ return title !== void 0 && title.length > 0 ? title : null;
2101
+ }
2102
+
2103
+ // src/client/SessionOverlay.tsx
2104
+ var import_jsx_runtime4 = require("react/jsx-runtime");
2105
+ function SessionOverlay(props) {
2106
+ const overlay = (0, import_react5.useSyncExternalStore)(subscribeOverlay, () => getOverlay(props.sessionId));
2107
+ const sessions = (0, import_react5.useSyncExternalStore)(props.sessions.subscribe, props.sessions.getSnapshot);
2108
+ const boardState = useBoard(props.rpc, props.sessionId, "session", props.t);
2109
+ const columnTitle = sessionTitle(sessions.byId[props.sessionId]) ?? props.t("session");
2110
+ (0, import_react4.useEffect)(() => {
2111
+ if (!overlay.open) return;
2112
+ const onKey = (event) => {
2113
+ if (event.key === "Escape" && document.querySelector(".board-session-detail-layer") === null) setOverlayOpen(props.sessionId, false);
2114
+ };
2115
+ window.addEventListener("keydown", onKey);
2116
+ return () => window.removeEventListener("keydown", onKey);
2117
+ }, [overlay.open, props.sessionId]);
2118
+ (0, import_react4.useEffect)(() => {
2119
+ return () => setOverlayOpen(props.sessionId, false);
2120
+ }, [props.sessionId]);
2121
+ if (!overlay.open) return null;
2122
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "board-overlay", role: "dialog", "aria-label": props.t("name"), children: [
2123
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "board-bar", children: [
2124
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: props.t("session") }),
2125
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "spacer" }),
2126
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: "board-iconbtn", "aria-label": props.t("close"), onClick: () => setOverlayOpen(props.sessionId, false), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CloseIcon, {}) })
2127
+ ] }),
2128
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "board-body", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2129
+ BoardEditor,
2130
+ {
2131
+ board: boardState.board,
2132
+ loading: boardState.loading,
2133
+ error: boardState.error,
2134
+ conflict: boardState.conflict,
2135
+ singleColumn: true,
2136
+ sessionColumnTitle: columnTitle,
2137
+ t: props.t,
2138
+ onMutate: boardState.mutate,
2139
+ onAddLog: boardState.addLog,
2140
+ onDownloadAttachment: boardState.downloadAttachment,
2141
+ onOpenAttachment: boardState.openAttachment,
2142
+ onRetry: boardState.reload
2143
+ }
2144
+ ) })
2145
+ ] });
2146
+ }
2147
+
2148
+ // src/client/SidebarAction.tsx
2149
+ var import_react6 = require("react");
2150
+ var import_react7 = require("react");
2151
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2152
+ function SidebarAction(props) {
2153
+ (0, import_react6.useEffect)(() => {
2154
+ setSidebarWide(props.wide);
2155
+ }, [props.wide]);
2156
+ const panel = (0, import_react7.useSyncExternalStore)(subscribeGlobalPanel, getGlobalPanel);
2157
+ const { board } = useBoard(props.rpc, props.sessionId, "global", props.t);
2158
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2159
+ "button",
2160
+ {
2161
+ type: "button",
2162
+ className: props.wide ? "board-side" : "board-side rail",
2163
+ "aria-label": props.t("name"),
2164
+ "aria-pressed": panel.open,
2165
+ title: props.t("name"),
2166
+ onClick: () => toggleGlobalPanel(),
2167
+ children: [
2168
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(KanbanIcon, {}),
2169
+ props.wide && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: props.t("name") })
2170
+ ]
2171
+ }
2172
+ );
2173
+ }
2174
+
2175
+ // src/client/GlobalPanel.tsx
2176
+ var import_react8 = require("react");
2177
+ var import_react9 = require("react");
2178
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2179
+ function GlobalPanel(props) {
2180
+ const panel = (0, import_react9.useSyncExternalStore)(subscribeGlobalPanel, getGlobalPanel);
2181
+ const sessions = (0, import_react9.useSyncExternalStore)(props.sessions.subscribe, props.sessions.getSnapshot);
2182
+ const boardState = useBoard(props.rpc, props.sessionId, "global", props.t);
2183
+ const previousSessionId = (0, import_react8.useRef)(null);
2184
+ const pendingTitleSync = (0, import_react8.useRef)(null);
2185
+ (0, import_react8.useEffect)(() => {
2186
+ if (!panel.open || boardState.board === null) return;
2187
+ const column = boardState.board.columns.find((item) => {
2188
+ if (item.sessionId === void 0) return false;
2189
+ const summary = sessions.byId[item.sessionId];
2190
+ const title2 = sessionTitle(summary);
2191
+ return title2 !== null && item.title !== title2;
2192
+ });
2193
+ if (column?.sessionId === void 0) return;
2194
+ const title = sessionTitle(sessions.byId[column.sessionId]);
2195
+ if (title === null) return;
2196
+ const key = `${String(boardState.board.revision)}:${column.id}:${title}`;
2197
+ if (pendingTitleSync.current === key) return;
2198
+ pendingTitleSync.current = key;
2199
+ void boardState.mutate({ type: "renameColumn", columnId: column.id, title }).finally(() => {
2200
+ if (pendingTitleSync.current === key) pendingTitleSync.current = null;
2201
+ });
2202
+ }, [boardState.board, boardState.mutate, panel.open, sessions]);
2203
+ (0, import_react8.useEffect)(() => {
2204
+ if (previousSessionId.current !== null && previousSessionId.current !== props.sessionId) {
2205
+ setGlobalPanelOpen(false);
2206
+ }
2207
+ previousSessionId.current = props.sessionId;
2208
+ }, [props.sessionId]);
2209
+ (0, import_react8.useEffect)(() => {
2210
+ if (!panel.open) return;
2211
+ const onKey = (event) => {
2212
+ if (event.key === "Escape" && document.querySelector(".board-session-detail-layer") === null) setGlobalPanelOpen(false);
2213
+ };
2214
+ window.addEventListener("keydown", onKey);
2215
+ return () => window.removeEventListener("keydown", onKey);
2216
+ }, [panel.open]);
2217
+ if (!panel.open) return null;
2218
+ const left = panel.sidebarWide ? 280 : 56;
2219
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "board-global", style: { left }, role: "dialog", "aria-label": props.t("global"), children: [
2220
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", className: "board-global-close", "aria-label": props.t("closeBoard"), onClick: () => setGlobalPanelOpen(false), children: [
2221
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "board-global-close-mark", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronIcon, { direction: "left" }) }),
2222
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: props.t("closeBoard") })
2223
+ ] }),
2224
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "board-body", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2225
+ BoardEditor,
2226
+ {
2227
+ board: boardState.board,
2228
+ loading: boardState.loading,
2229
+ error: boardState.error,
2230
+ conflict: boardState.conflict,
2231
+ t: props.t,
2232
+ onMutate: boardState.mutate,
2233
+ onAddLog: boardState.addLog,
2234
+ onDownloadAttachment: boardState.downloadAttachment,
2235
+ onOpenAttachment: boardState.openAttachment,
2236
+ onRetry: boardState.reload
2237
+ }
2238
+ ) })
2239
+ ] });
2240
+ }
2241
+
2242
+ // src/client/ReminderToasts.tsx
2243
+ var import_react10 = require("react");
2244
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2245
+ function ReminderToasts(props) {
2246
+ const toasts = (0, import_react10.useSyncExternalStore)(subscribeReminderToasts, getReminderToasts);
2247
+ const visibleToasts = toasts.slice(0, 5);
2248
+ const nativeSeen = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
2249
+ (0, import_react10.useEffect)(() => {
2250
+ if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
2251
+ for (const toast of visibleToasts) {
2252
+ if (nativeSeen.current.has(toast.id)) continue;
2253
+ nativeSeen.current.add(toast.id);
2254
+ const notification = new Notification(`${props.t(toast.kind === "start" ? "taskStartedReminder" : "taskDueReminder")}: ${toast.title}`, {
2255
+ body: props.t("openTask"),
2256
+ tag: toast.id
2257
+ });
2258
+ notification.onclick = () => {
2259
+ window.focus();
2260
+ openToast(toast.scope, toast.sessionId);
2261
+ dismissReminderToast(toast.id);
2262
+ notification.close();
2263
+ };
2264
+ }
2265
+ }, [props, toasts]);
2266
+ if (toasts.length === 0) return null;
2267
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "board-toast-stack", role: "region", "aria-label": props.t("taskTimeReminder"), children: visibleToasts.map((toast) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("article", { className: "board-toast", role: "alert", children: [
2268
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("button", { type: "button", className: "board-toast-main", onClick: () => {
2269
+ openToast(toast.scope, toast.sessionId);
2270
+ dismissReminderToast(toast.id);
2271
+ }, children: [
2272
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "board-toast-mark", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ClockIcon, {}) }),
2273
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "board-toast-copy", children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { children: props.t(toast.kind === "start" ? "taskStartedReminder" : "taskDueReminder") }),
2275
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: toast.title })
2276
+ ] })
2277
+ ] }),
2278
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "board-toast-close", "aria-label": props.t("close"), onClick: () => dismissReminderToast(toast.id), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseIcon, {}) })
2279
+ ] }, toast.id)) });
2280
+ }
2281
+ function openToast(scope, sessionId) {
2282
+ if (scope === "global") setGlobalPanelOpen(true);
2283
+ else setOverlayOpen(sessionId, true);
2284
+ }
2285
+
2286
+ // src/client/index.ts
2287
+ var inject = ["slots", "locale", "connection", "sessions"];
2288
+ function apply(ctx) {
2289
+ ensureBoardStyles();
2290
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "board: dictionaries");
2291
+ const t = ctx.locale.bind(NS);
2292
+ const rpc = ctx;
2293
+ const callerSession = () => ctx.sessions.list.getSnapshot().current ?? "ui";
2294
+ ctx.effect(() => {
2295
+ let previous = callerSession();
2296
+ return ctx.sessions.list.subscribe(() => {
2297
+ const current = callerSession();
2298
+ if (current === previous) return;
2299
+ setGlobalPanelOpen(false);
2300
+ if (previous !== "ui") setOverlayOpen(previous, false);
2301
+ previous = current;
2302
+ });
2303
+ }, "board: close panels on session change");
2304
+ ctx.effect(() => {
2305
+ const closeOnCurrentSessionClick = (event) => {
2306
+ if (!(event.target instanceof Element)) return;
2307
+ const row = event.target.closest('[role="treeitem"][aria-selected="true"]');
2308
+ if (row === null || row.closest('[role="tree"]') === null) return;
2309
+ const current = ctx.sessions.list.getSnapshot().current;
2310
+ if (current === void 0) return;
2311
+ setGlobalPanelOpen(false);
2312
+ setOverlayOpen(current, false);
2313
+ };
2314
+ document.addEventListener("click", closeOnCurrentSessionClick);
2315
+ return () => document.removeEventListener("click", closeOnCurrentSessionClick);
2316
+ }, "board: close panels when current session row is reopened");
2317
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
2318
+ name: "conversation.input.left",
2319
+ id: "board",
2320
+ locale: NS,
2321
+ inject: (sessionId) => ({
2322
+ sessionId,
2323
+ t,
2324
+ sessions: ctx.sessions.list,
2325
+ rpc
2326
+ })
2327
+ }, SessionChip));
2328
+ ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
2329
+ name: "conversation.input.overlay",
2330
+ id: "board-overlay",
2331
+ order: 10,
2332
+ locale: NS,
2333
+ inject: (sessionId) => ({
2334
+ sessionId,
2335
+ t,
2336
+ rpc,
2337
+ sessions: ctx.sessions.list
2338
+ })
2339
+ }, SessionOverlay));
2340
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
2341
+ name: "sidebar.footer.action",
2342
+ id: "board",
2343
+ order: 100,
2344
+ locale: NS,
2345
+ inject: () => ({
2346
+ t,
2347
+ rpc,
2348
+ get sessionId() {
2349
+ return callerSession();
2350
+ }
2351
+ })
2352
+ }, SidebarAction));
2353
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
2354
+ name: "shell.overlay",
2355
+ id: "board-global",
2356
+ locale: NS,
2357
+ inject: () => ({
2358
+ t,
2359
+ rpc,
2360
+ sessions: ctx.sessions.list,
2361
+ get sessionId() {
2362
+ return callerSession();
2363
+ }
2364
+ })
2365
+ }, GlobalPanel));
2366
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
2367
+ name: "shell.overlay",
2368
+ id: "board-reminders",
2369
+ order: 100,
2370
+ locale: NS,
2371
+ inject: () => ({ t })
2372
+ }, ReminderToasts));
2373
+ }
2374
+ module.exports = { apply, inject }; return module.exports; } });