Okoge-keys commited on
Commit
7cf23f8
·
verified ·
1 Parent(s): bebebd9

Upload 3 files

Browse files
Files changed (3) hide show
  1. kanban_v2/index.html +173 -0
  2. kanban_v2/script.js +1006 -0
  3. kanban_v2/styles.css +967 -0
kanban_v2/index.html ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>&#x30C8;&#x30E8;&#x30BF;&#x5F0F;&#x30AB;&#x30F3;&#x30D0;&#x30F3;</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="app-shell">
11
+ <header class="app-header">
12
+ <div class="brand">
13
+ <span class="brand-mark">&#x25EF;</span>
14
+ <div>
15
+ <h1>TOYOTA KANBAN</h1>
16
+ <p class="subtitle">&#x73FE;&#x5834;&#x306E;&#x8996;&#x70B9;&#x3067;&#x78E8;&#x304D;&#x4E0A;&#x3052;&#x305F;&#x30BF;&#x30B9;&#x30AF;&#x30D5;&#x30ED;&#x30FC;</p>
17
+ </div>
18
+ </div>
19
+ <div class="primary-controls">
20
+ <button class="btn primary" id="btn-new-task">&#x65B0;&#x898F;&#x30BF;&#x30B9;&#x30AF;</button>
21
+ <button class="btn ghost" id="btn-toggle-group">&#x89AA;&#x5B50;&#x30D3;&#x30E5;&#x30FC;</button>
22
+ <button class="btn ghost" id="btn-toggle-selection">&#x4E00;&#x62EC;&#x64CD;&#x4F5C;</button>
23
+ </div>
24
+ </header>
25
+
26
+ <section class="filters">
27
+ <div class="search-box">
28
+ <label for="search-input">&#x691C;&#x7D22;</label>
29
+ <input id="search-input" type="search" placeholder="&#x30AD;&#x30FC;&#x30EF;&#x30FC;&#x30C9;&#x3067;&#x691C;&#x7D22; (&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x30FB;&#x62C5;&#x5F53;&#x30FB;&#x5185;&#x5BB9;&#x30FB;&#x30BF;&#x30B0;&#x30FB;&#x30EA;&#x30F3;&#x30AF;)">
30
+ </div>
31
+ <div class="date-filter">
32
+ <label>&#x671F;&#x9650;</label>
33
+ <div class="date-inputs">
34
+ <input id="date-from" type="date">
35
+ <span>&#x301C;</span>
36
+ <input id="date-to" type="date">
37
+ </div>
38
+ </div>
39
+ <div class="parent-filter">
40
+ <label for="parent-filter-select">&#x89AA;&#x30BF;&#x30B9;&#x30AF;</label>
41
+ <input id="parent-filter-search" type="search" placeholder="&#x89AA;&#x30BF;&#x30B9;&#x30AF;&#x540D;&#x3067;&#x7D5E;&#x308A;&#x8FBC;&#x307F;">
42
+ <select id="parent-filter-select">
43
+ <option value="">&#x3059;&#x3079;&#x3066;</option>
44
+ </select>
45
+ </div>
46
+ <div class="tag-filter-row">
47
+ <span class="tag-filter-label">&#x30BF;&#x30B0;</span>
48
+ <div id="tag-toggle-container" class="tag-toggle-container"></div>
49
+ </div>
50
+ <div class="filter-actions">
51
+ <button class="btn small ghost" id="filter-reset-button">&#x30D5;&#x30A3;&#x30EB;&#x30BF;&#x30FC;&#x89E3;&#x9664;</button>
52
+ </div>
53
+ </section>
54
+
55
+ <section class="selection-toolbar" id="selection-toolbar">
56
+ <div class="selection-summary">
57
+ <span id="selection-count">0&#x4EF6;&#x9078;&#x629E;&#x4E2D;</span>
58
+ <button class="btn subtle" id="btn-select-all">&#x5168;&#x9078;&#x629E;</button>
59
+ <button class="btn subtle" id="btn-clear-selection">&#x9078;&#x629E;&#x89E3;&#x9664;</button>
60
+ </div>
61
+ <div class="selection-actions">
62
+ <div class="selection-status">
63
+ <span>&#x30B9;&#x30C6;&#x30FC;&#x30BF;&#x30B9;&#x5909;&#x66F4;:</span>
64
+ <div id="bulk-status-buttons" class="status-button-group"></div>
65
+ </div>
66
+ <button class="btn danger" id="btn-bulk-delete">&#x9078;&#x629E;&#x3092;&#x524A;&#x9664;</button>
67
+ </div>
68
+ </section>
69
+
70
+ <section class="status-dock" id="status-dock"></section>
71
+
72
+ <main class="board" id="board"></main>
73
+
74
+ <section class="parent-view" id="parent-view">
75
+ <header>
76
+ <h2>&#x89AA;&#x5B50;&#x30D3;&#x30E5;&#x30FC;</h2>
77
+ <div class="parent-view-meta">
78
+ <p>&#x89AA;&#x30BF;&#x30B9;&#x30AF;&#x3054;&#x3068;&#x306E;&#x9032;&#x6357;&#x3092;&#x4FEF;&#x77B0;&#x3057;&#x3001;&#x30EF;&#x30F3;&#x30AF;&#x30EA;&#x30C3;&#x30AF;&#x3067;&#x30DC;&#x30FC;&#x30C9;&#x306B;&#x53CD;&#x6620;&#x3067;&#x304D;&#x307E;&#x3059;&#x3002;</p>
79
+ <div class="parent-view-controls">
80
+ <input id="parent-view-search" type="search" placeholder="&#x89AA;&#x30BF;&#x30B9;&#x30AF;&#x30FB;&#x5B50;&#x30BF;&#x30B9;&#x30AF;&#x30FB;&#x30BF;&#x30B0;&#x3092;&#x691C;&#x7D22;">
81
+ <button class="btn small ghost" id="parent-view-clear">&#x30CF;&#x30A4;&#x30E9;&#x30A4;&#x30C8;&#x89E3;&#x9664;</button>
82
+ </div>
83
+ </div>
84
+ </header>
85
+ <div class="parent-clusters" id="parent-clusters"></div>
86
+ </section>
87
+ </div>
88
+
89
+ <dialog id="task-dialog" class="task-dialog">
90
+ <form method="dialog" id="task-form">
91
+ <header>
92
+ <h3 id="dialog-title">&#x30BF;&#x30B9;&#x30AF;&#x3092;&#x8FFD;&#x52A0;</h3>
93
+ <button type="button" class="icon-button" id="dialog-close" aria-label="&#x9589;&#x3058;&#x308B;">&#x00D7;</button>
94
+ </header>
95
+ <div class="form-body">
96
+ <div class="field">
97
+ <label for="task-title">&#x30BF;&#x30A4;&#x30C8;&#x30EB;</label>
98
+ <input id="task-title" name="title" type="text" required>
99
+ </div>
100
+ <div class="field-grid">
101
+ <div class="field">
102
+ <label for="task-assignee">&#x62C5;&#x5F53;</label>
103
+ <input id="task-assignee" name="assignee" type="text" placeholder="&#x62C5;&#x5F53;&#x8005;&#x540D;">
104
+ </div>
105
+ <div class="field">
106
+ <label for="task-due">&#x671F;&#x9650;</label>
107
+ <input id="task-due" name="dueDate" type="date">
108
+ </div>
109
+ </div>
110
+ <div class="field">
111
+ <label for="task-link">&#x30EA;&#x30F3;&#x30AF;</label>
112
+ <input id="task-link" name="link" type="url" placeholder="https:// &#x307E;&#x305F;&#x306F; file://">
113
+ </div>
114
+ <div class="field">
115
+ <label for="task-tags">&#x30BF;&#x30B0;</label>
116
+ <input id="task-tags" name="tags" type="text" placeholder="&#x30AB;&#x30F3;&#x30DE;&#x533A;&#x5207;&#x308A; (&#x4F8B;: &#x8A2D;&#x8A08;,&#x30EC;&#x30D3;&#x30E5;&#x30FC;)">
117
+ </div>
118
+ <div class="field">
119
+ <label for="task-progress">&#x9032;&#x6357;&#x30E1;&#x30E2;</label>
120
+ <textarea id="task-progress" name="progress" rows="3"></textarea>
121
+ </div>
122
+ <div class="field">
123
+ <label for="task-notes">&#x5099;&#x8003;&#x30E1;&#x30E2;</label>
124
+ <textarea id="task-notes" name="notes" rows="3"></textarea>
125
+ </div>
126
+ <div class="field">
127
+ <label for="task-parent">&#x89AA;&#x30BF;&#x30B9;&#x30AF;</label>
128
+ <select id="task-parent" name="parentId">
129
+ <option value="">&#x306A;&#x3057;</option>
130
+ </select>
131
+ </div>
132
+ <div class="field">
133
+ <label for="task-status">&#x30B9;&#x30C6;&#x30FC;&#x30BF;&#x30B9;</label>
134
+ <select id="task-status" name="status" required></select>
135
+ </div>
136
+ </div>
137
+ <footer>
138
+ <button type="submit" class="btn primary">&#x4FDD;&#x5B58;</button>
139
+ <button type="button" class="btn ghost" id="task-delete-button">&#x524A;&#x9664;</button>
140
+ </footer>
141
+ </form>
142
+ </dialog>
143
+
144
+ <template id="task-card-template">
145
+ <article class="task-card">
146
+ <header class="task-card-header">
147
+ <div class="title-row">
148
+ <input type="checkbox" class="task-select">
149
+ <h3 class="task-title"></h3>
150
+ </div>
151
+ <div class="status-quick-buttons"></div>
152
+ </header>
153
+ <div class="task-meta">
154
+ <span class="assignee"></span>
155
+ <span class="due-date"></span>
156
+ </div>
157
+ <div class="task-body">
158
+ <div class="progress"></div>
159
+ <div class="notes"></div>
160
+ <a class="resource-link" target="_blank" rel="noopener"></a>
161
+ </div>
162
+ <div class="task-tags"></div>
163
+ <div class="task-children"></div>
164
+ <footer class="task-footer">
165
+ <button class="btn tiny" data-action="edit">&#x7DE8;&#x96C6;</button>
166
+ <button class="btn tiny danger" data-action="delete">&#x524A;&#x9664;</button>
167
+ </footer>
168
+ </article>
169
+ </template>
170
+
171
+ <script src="script.js" defer></script>
172
+ </body>
173
+ </html>
kanban_v2/script.js ADDED
@@ -0,0 +1,1006 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const STORAGE_KEY = "toyota-kanban-tasks-v1";
2
+
3
+ const STATUSES = [
4
+ { id: "backlog", label: "\u30d0\u30c3\u30af\u30ed\u30b0", hint: "\u69cb\u60f3\u4e2d" },
5
+ { id: "ready", label: "\u6e96\u5099", hint: "\u6e96\u5099\u4e2d" },
6
+ { id: "inprogress", label: "\u4f5c\u696d", hint: "\u9032\u884c\u4e2d" },
7
+ { id: "waiting", label: "\u5f85\u6a5f", hint: "\u505c\u6b62\u4e2d" },
8
+ { id: "done", label: "\u5b8c\u4e86", hint: "\u5b8c\u4e86" }
9
+ ];
10
+
11
+ const state = {
12
+ tasks: [],
13
+ filters: {
14
+ search: "",
15
+ searchRaw: "",
16
+ selectedTags: [],
17
+ dateFrom: "",
18
+ dateTo: "",
19
+ parentId: "",
20
+ parentSearch: ""
21
+ },
22
+ selectionMode: false,
23
+ selectedIds: new Set(),
24
+ showParentView: false,
25
+ highlightParentId: "",
26
+ activeStatus: STATUSES[0].id,
27
+ parentViewSearch: ""
28
+ };
29
+
30
+ const dom = {};
31
+ let initialized = false;
32
+
33
+ function init() {
34
+ if (initialized) return;
35
+ initialized = true;
36
+ cacheDom();
37
+ bindGlobalEvents();
38
+ hydrateStaticUi();
39
+ loadTasks();
40
+ renderAll();
41
+ }
42
+
43
+ function cacheDom() {
44
+ dom.board = document.getElementById("board");
45
+ dom.statusDock = document.getElementById("status-dock");
46
+ dom.parentView = document.getElementById("parent-view");
47
+ dom.parentClusters = document.getElementById("parent-clusters");
48
+ dom.selectionToolbar = document.getElementById("selection-toolbar");
49
+ dom.selectionCount = document.getElementById("selection-count");
50
+ dom.bulkStatusButtons = document.getElementById("bulk-status-buttons");
51
+ dom.filterReset = document.getElementById("filter-reset-button");
52
+ dom.parentFilter = document.getElementById("parent-filter-select");
53
+ dom.parentFilterSearch = document.getElementById("parent-filter-search");
54
+ dom.tagToggleContainer = document.getElementById("tag-toggle-container");
55
+ dom.parentViewSearch = document.getElementById("parent-view-search");
56
+ dom.parentViewClear = document.getElementById("parent-view-clear");
57
+ dom.searchInput = document.getElementById("search-input");
58
+ dom.dateFrom = document.getElementById("date-from");
59
+ dom.dateTo = document.getElementById("date-to");
60
+ dom.toggleGroupBtn = document.getElementById("btn-toggle-group");
61
+ dom.toggleSelectionBtn = document.getElementById("btn-toggle-selection");
62
+ dom.newTaskBtn = document.getElementById("btn-new-task");
63
+ dom.bulkDeleteBtn = document.getElementById("btn-bulk-delete");
64
+ dom.selectAllBtn = document.getElementById("btn-select-all");
65
+ dom.clearSelectionBtn = document.getElementById("btn-clear-selection");
66
+ dom.dialog = document.getElementById("task-dialog");
67
+ dom.dialogTitle = document.getElementById("dialog-title");
68
+ dom.dialogClose = document.getElementById("dialog-close");
69
+ dom.taskForm = document.getElementById("task-form");
70
+ dom.taskDeleteBtn = document.getElementById("task-delete-button");
71
+ dom.taskStatusSelect = document.getElementById("task-status");
72
+ dom.taskParentSelect = document.getElementById("task-parent");
73
+ dom.taskCardTemplate = document.getElementById("task-card-template");
74
+ }
75
+
76
+ function bindGlobalEvents() {
77
+ dom.newTaskBtn.addEventListener("click", () => openTaskDialog());
78
+ dom.dialogClose.addEventListener("click", closeDialog);
79
+ dom.taskForm.addEventListener("submit", handleTaskSubmit);
80
+ dom.taskDeleteBtn.addEventListener("click", handleDialogDelete);
81
+ dom.toggleGroupBtn.addEventListener("click", toggleParentView);
82
+ dom.toggleSelectionBtn.addEventListener("click", toggleSelectionMode);
83
+ dom.bulkDeleteBtn.addEventListener("click", handleBulkDelete);
84
+ dom.selectAllBtn.addEventListener("click", handleSelectAll);
85
+ dom.clearSelectionBtn.addEventListener("click", () => {
86
+ state.selectedIds.clear();
87
+ updateBulkActions();
88
+ renderAll();
89
+ });
90
+ dom.searchInput.addEventListener("input", (event) => {
91
+ const raw = event.target.value.trim();
92
+ state.filters.searchRaw = raw;
93
+ state.filters.search = raw.toLowerCase();
94
+ renderAll();
95
+ });
96
+ dom.dateFrom.addEventListener("change", (event) => {
97
+ state.filters.dateFrom = event.target.value;
98
+ renderAll();
99
+ });
100
+ dom.dateTo.addEventListener("change", (event) => {
101
+ state.filters.dateTo = event.target.value;
102
+ renderAll();
103
+ });
104
+ dom.parentFilter.addEventListener("change", (event) => {
105
+ state.filters.parentId = event.target.value;
106
+ renderAll();
107
+ });
108
+ if (dom.filterReset) {
109
+ dom.filterReset.addEventListener("click", resetFilters);
110
+ }
111
+ if (dom.parentFilterSearch) {
112
+ dom.parentFilterSearch.addEventListener("input", (event) => {
113
+ state.filters.parentSearch = event.target.value.trim();
114
+ renderParentFilterOptions();
115
+ });
116
+ }
117
+ if (dom.parentViewSearch) {
118
+ dom.parentViewSearch.addEventListener("input", (event) => {
119
+ state.parentViewSearch = event.target.value.trim();
120
+ renderParentClusters();
121
+ });
122
+ }
123
+ if (dom.parentViewClear) {
124
+ dom.parentViewClear.addEventListener("click", () => {
125
+ state.parentViewSearch = "";
126
+ state.highlightParentId = "";
127
+ state.filters.parentId = "";
128
+ if (dom.parentViewSearch) dom.parentViewSearch.value = "";
129
+ renderAll();
130
+ });
131
+ }
132
+ dom.dialog.addEventListener("close", () => {
133
+ dom.taskForm.reset();
134
+ dom.taskDeleteBtn.hidden = true;
135
+ dom.taskForm.dataset.editingId = "";
136
+ });
137
+ }
138
+
139
+ function hydrateStaticUi() {
140
+ STATUSES.forEach((status) => {
141
+ const option = document.createElement("option");
142
+ option.value = status.id;
143
+ option.textContent = status.label;
144
+ dom.taskStatusSelect.appendChild(option);
145
+ });
146
+ renderBulkStatusButtons();
147
+ }
148
+
149
+ function loadTasks() {
150
+ try {
151
+ const raw = localStorage.getItem(STORAGE_KEY);
152
+ if (raw) {
153
+ const parsed = JSON.parse(raw);
154
+ if (Array.isArray(parsed)) {
155
+ state.tasks = parsed;
156
+ return;
157
+ }
158
+ }
159
+ } catch (error) {
160
+ console.error("Failed to parse storage", error);
161
+ }
162
+ state.tasks = createSampleTasks();
163
+ saveTasks();
164
+ }
165
+
166
+ function saveTasks() {
167
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state.tasks));
168
+ }
169
+
170
+ function renderAll() {
171
+ document.body.classList.toggle("selection-mode", state.selectionMode);
172
+ document.body.classList.toggle("parent-view-mode", state.showParentView);
173
+ dom.parentView.classList.toggle("active", state.showParentView);
174
+ dom.toggleGroupBtn.classList.toggle("active", state.showParentView);
175
+ dom.toggleSelectionBtn.classList.toggle("active", state.selectionMode);
176
+
177
+ if (dom.searchInput) dom.searchInput.value = state.filters.searchRaw;
178
+ if (dom.dateFrom) dom.dateFrom.value = state.filters.dateFrom;
179
+ if (dom.dateTo) dom.dateTo.value = state.filters.dateTo;
180
+ if (dom.parentViewSearch) dom.parentViewSearch.value = state.parentViewSearch;
181
+
182
+ const tagScopedTasks = getFilteredTasks({ includeTagFilter: false });
183
+ renderTagToggles(tagScopedTasks);
184
+
185
+ const filteredTasks = getFilteredTasks();
186
+ const groupedByStatus = groupByStatus(filteredTasks);
187
+
188
+ renderStatusDock(groupedByStatus);
189
+ renderBoard(groupedByStatus);
190
+ renderParentOptions();
191
+ renderParentFilterOptions();
192
+ renderParentClusters();
193
+ updateBulkActions();
194
+ }
195
+
196
+ function renderBoard(groupedTasks) {
197
+ dom.board.innerHTML = "";
198
+ const status = STATUSES.find((item) => item.id === state.activeStatus) ?? STATUSES[0];
199
+ const tasks = groupedTasks[status.id] ?? [];
200
+
201
+ const column = document.createElement("section");
202
+ column.className = "column";
203
+ column.dataset.status = status.id;
204
+
205
+ const header = document.createElement("header");
206
+ header.className = "column-header";
207
+ header.innerHTML = `<span>${status.label}</span><span class="column-count">${tasks.length}</span>`;
208
+
209
+ const body = document.createElement("div");
210
+ body.className = "column-body";
211
+ if (tasks.length === 0) {
212
+ body.classList.add("empty");
213
+ } else {
214
+ tasks.forEach((task) => body.appendChild(createTaskCard(task)));
215
+ }
216
+
217
+ column.append(header, body);
218
+ dom.board.appendChild(column);
219
+ }
220
+
221
+ function renderParentOptions(currentId = "") {
222
+ dom.taskParentSelect.innerHTML = "";
223
+ const noneOption = document.createElement("option");
224
+ noneOption.value = "";
225
+ noneOption.textContent = "\u306a\u3057";
226
+ dom.taskParentSelect.appendChild(noneOption);
227
+
228
+ state.tasks
229
+ .filter((task) => task.id !== currentId && !isDescendant(task.id, currentId))
230
+ .sort((a, b) => a.title.localeCompare(b.title, "ja"))
231
+ .forEach((task) => {
232
+ const option = document.createElement("option");
233
+ option.value = task.id;
234
+ option.textContent = task.title;
235
+ dom.taskParentSelect.appendChild(option);
236
+ });
237
+ }
238
+
239
+ function renderParentFilterOptions() {
240
+ const previous = state.filters.parentId || "";
241
+ const searchTerm = state.filters.parentSearch.trim().toLowerCase();
242
+ dom.parentFilter.innerHTML = "";
243
+ const allOption = document.createElement("option");
244
+ allOption.value = "";
245
+ allOption.textContent = "\u3059\u3079\u3066";
246
+ dom.parentFilter.appendChild(allOption);
247
+
248
+ const allParents = state.tasks
249
+ .filter((task) => !task.parentId)
250
+ .sort((a, b) => a.title.localeCompare(b.title, "ja"));
251
+
252
+ const filtered = [];
253
+ allParents.forEach((task) => {
254
+ if (!searchTerm || task.title.toLowerCase().includes(searchTerm)) {
255
+ filtered.push(task);
256
+ }
257
+ });
258
+
259
+ if (previous && !filtered.some((task) => task.id === previous)) {
260
+ const selected = allParents.find((task) => task.id === previous);
261
+ if (selected) {
262
+ filtered.unshift(selected);
263
+ }
264
+ }
265
+
266
+ filtered.forEach((task) => {
267
+ const option = document.createElement("option");
268
+ option.value = task.id;
269
+ option.textContent = task.title;
270
+ dom.parentFilter.appendChild(option);
271
+ });
272
+
273
+ const hasSelection = previous && allParents.some((task) => task.id === previous);
274
+ dom.parentFilter.value = hasSelection ? previous : "";
275
+ if (!hasSelection) {
276
+ state.filters.parentId = "";
277
+ }
278
+ if (dom.parentFilterSearch) {
279
+ dom.parentFilterSearch.value = state.filters.parentSearch;
280
+ }
281
+ }
282
+
283
+ function renderStatusDock(groupedTasks) {
284
+ dom.statusDock.innerHTML = "";
285
+ STATUSES.forEach((status) => {
286
+ const button = document.createElement("button");
287
+ button.type = "button";
288
+ button.className = "status-chip";
289
+ button.dataset.status = status.id;
290
+ if (status.id === state.activeStatus) {
291
+ button.classList.add("active");
292
+ }
293
+ const count = groupedTasks[status.id]?.length ?? 0;
294
+ button.innerHTML = `
295
+ <div class="status-chip-text">
296
+ <strong>${status.label}</strong>
297
+ <span>${status.hint}</span>
298
+ </div>
299
+ <span class="status-chip-count">${count}</span>
300
+ `;
301
+ button.addEventListener("click", () => {
302
+ if (state.activeStatus !== status.id) {
303
+ state.activeStatus = status.id;
304
+ renderAll();
305
+ }
306
+ });
307
+ dom.statusDock.appendChild(button);
308
+ });
309
+ }
310
+
311
+ function renderBulkStatusButtons() {
312
+ dom.bulkStatusButtons.innerHTML = "";
313
+ STATUSES.forEach((status) => {
314
+ const button = document.createElement("button");
315
+ button.type = "button";
316
+ button.className = "btn subtle";
317
+ button.textContent = status.label;
318
+ button.addEventListener("click", () => handleBulkMove(status.id));
319
+ dom.bulkStatusButtons.appendChild(button);
320
+ });
321
+ }
322
+
323
+ function renderTagToggles(tasks = []) {
324
+ const container = dom.tagToggleContainer;
325
+ if (!container) return;
326
+ container.innerHTML = "";
327
+
328
+ const tagSet = new Set();
329
+ tasks.forEach((task) => (task.tags ?? []).forEach((tag) => tagSet.add(tag)));
330
+ const sortedTags = [...tagSet].sort((a, b) => a.localeCompare(b, "ja"));
331
+
332
+ const available = new Set(sortedTags);
333
+ state.filters.selectedTags = state.filters.selectedTags.filter((tag) => available.has(tag));
334
+
335
+ if (!sortedTags.length) {
336
+ const placeholder = document.createElement("span");
337
+ placeholder.className = "tag-toggle-empty";
338
+ placeholder.textContent = "\u30bf\u30b0\u306f\u672a\u767b\u9332\u3067\u3059";
339
+ container.appendChild(placeholder);
340
+ return;
341
+ }
342
+
343
+ sortedTags.forEach((tag) => {
344
+ const button = document.createElement("button");
345
+ button.type = "button";
346
+ button.className = "tag-toggle";
347
+ button.textContent = tag;
348
+ const isActive = state.filters.selectedTags.includes(tag);
349
+ button.setAttribute("aria-pressed", isActive ? "true" : "false");
350
+ button.classList.toggle("active", isActive);
351
+ button.addEventListener("click", () => toggleTagFilter(tag));
352
+ container.appendChild(button);
353
+ });
354
+ }
355
+
356
+ function toggleTagFilter(tag) {
357
+ const list = state.filters.selectedTags;
358
+ const index = list.indexOf(tag);
359
+ if (index >= 0) {
360
+ list.splice(index, 1);
361
+ } else {
362
+ list.push(tag);
363
+ }
364
+ renderAll();
365
+ }
366
+
367
+ function createTaskCard(task) {
368
+ const card = dom.taskCardTemplate.content.firstElementChild.cloneNode(true);
369
+ card.dataset.id = task.id;
370
+ card.dataset.status = task.status;
371
+
372
+ if (state.highlightParentId && (state.highlightParentId === task.id || state.highlightParentId === task.parentId)) {
373
+ card.classList.add("highlighted");
374
+ }
375
+
376
+ const checkbox = card.querySelector(".task-select");
377
+ checkbox.checked = state.selectedIds.has(task.id);
378
+ checkbox.style.display = state.selectionMode ? "block" : "none";
379
+ checkbox.addEventListener("change", (event) => {
380
+ if (event.target.checked) {
381
+ state.selectedIds.add(task.id);
382
+ } else {
383
+ state.selectedIds.delete(task.id);
384
+ }
385
+ updateBulkActions();
386
+ });
387
+
388
+ card.querySelector(".task-title").textContent = task.title;
389
+ card.querySelector(".assignee").textContent = task.assignee ? `\u62c5\u5f53: ${task.assignee}` : "\u62c5\u5f53: \u672a\u5b9a";
390
+ card.querySelector(".due-date").textContent = task.dueDate ? `\u671f\u9650: ${formatDate(task.dueDate)}` : "\u671f\u9650: \u672a\u8a2d\u5b9a";
391
+ card.querySelector(".progress").textContent = task.progress || "\u9032\u6357\u30e1\u30e2\u306a\u3057";
392
+ card.querySelector(".notes").textContent = task.notes || "\u5099\u8003\u306a\u3057";
393
+
394
+ const linkEl = card.querySelector(".resource-link");
395
+ if (task.link) {
396
+ linkEl.href = task.link;
397
+ linkEl.textContent = task.link;
398
+ linkEl.style.display = "block";
399
+ } else {
400
+ linkEl.style.display = "none";
401
+ }
402
+
403
+ const tagContainer = card.querySelector(".task-tags");
404
+ tagContainer.innerHTML = "";
405
+ (task.tags || []).forEach((tag) => {
406
+ const tagEl = document.createElement("span");
407
+ tagEl.className = "task-tag";
408
+ tagEl.textContent = tag;
409
+ tagContainer.appendChild(tagEl);
410
+ });
411
+
412
+ const childrenContainer = card.querySelector(".task-children");
413
+ const children = state.tasks.filter((item) => item.parentId === task.id);
414
+ if (children.length) {
415
+ children.forEach((child) => {
416
+ const row = document.createElement("div");
417
+ row.className = "task-child";
418
+ const statusLabel = STATUSES.find((status) => status.id === child.status)?.label ?? "";
419
+ row.innerHTML = `<span>${child.title}</span><span>${statusLabel}</span>`;
420
+ childrenContainer.appendChild(row);
421
+ });
422
+ } else {
423
+ childrenContainer.style.display = "none";
424
+ }
425
+
426
+ const quickButtons = card.querySelector(".status-quick-buttons");
427
+ quickButtons.innerHTML = "";
428
+ STATUSES.forEach((status) => {
429
+ if (status.id === task.status) return;
430
+ const button = document.createElement("button");
431
+ button.type = "button";
432
+ button.textContent = status.label;
433
+ button.addEventListener("click", () => {
434
+ updateTask(task.id, { status: status.id }, { skipRender: true });
435
+ renderAll();
436
+ });
437
+ quickButtons.appendChild(button);
438
+ });
439
+
440
+ card.querySelector('[data-action="edit"]').addEventListener("click", () => openTaskDialog(task.id));
441
+ card.querySelector('[data-action="delete"]').addEventListener("click", () => {
442
+ if (confirm("\u3053\u306e\u30bf\u30b9\u30af\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\u5b50\u30bf\u30b9\u30af\u3082\u524a\u9664\u3055\u308c\u307e\u3059\u3002")) {
443
+ deleteTask(task.id);
444
+ renderAll();
445
+ }
446
+ });
447
+
448
+ return card;
449
+ }
450
+
451
+ function openTaskDialog(taskId = "") {
452
+ const editing = Boolean(taskId);
453
+ dom.dialogTitle.textContent = editing ? "\u30bf\u30b9\u30af\u3092\u7de8\u96c6" : "\u30bf\u30b9\u30af\u3092\u8ffd\u52a0";
454
+ dom.taskDeleteBtn.hidden = !editing;
455
+ dom.taskForm.dataset.editingId = editing ? taskId : "";
456
+ populateTaskForm(editing ? getTask(taskId) : null);
457
+ dom.dialog.showModal();
458
+ }
459
+
460
+ function closeDialog() {
461
+ dom.dialog.close();
462
+ }
463
+
464
+ function populateTaskForm(task) {
465
+ dom.taskForm.reset();
466
+ renderParentOptions(task?.id ?? "");
467
+ dom.taskForm.elements.title.value = task?.title ?? "";
468
+ dom.taskForm.elements.assignee.value = task?.assignee ?? "";
469
+ dom.taskForm.elements.dueDate.value = task?.dueDate ?? "";
470
+ dom.taskForm.elements.link.value = task?.link ?? "";
471
+ dom.taskForm.elements.tags.value = (task?.tags ?? []).join(", ");
472
+ dom.taskForm.elements.progress.value = task?.progress ?? "";
473
+ dom.taskForm.elements.notes.value = task?.notes ?? "";
474
+ dom.taskForm.elements.parentId.value = task?.parentId ?? "";
475
+ dom.taskForm.elements.status.value = task?.status ?? STATUSES[0].id;
476
+ }
477
+
478
+ function handleTaskSubmit(event) {
479
+ event.preventDefault();
480
+ const formData = new FormData(dom.taskForm);
481
+ const payload = {
482
+ title: formData.get("title").trim(),
483
+ assignee: formData.get("assignee").trim(),
484
+ dueDate: formData.get("dueDate") || "",
485
+ link: formData.get("link").trim(),
486
+ tags: formData
487
+ .get("tags")
488
+ .split(",")
489
+ .map((tag) => tag.trim())
490
+ .filter(Boolean),
491
+ progress: formData.get("progress").trim(),
492
+ notes: formData.get("notes").trim(),
493
+ parentId: formData.get("parentId") || "",
494
+ status: formData.get("status")
495
+ };
496
+
497
+ const editingId = dom.taskForm.dataset.editingId;
498
+ if (editingId) {
499
+ updateTask(editingId, payload, { skipRender: true, skipPersist: true });
500
+ saveTasks();
501
+ } else {
502
+ const newTask = {
503
+ id: generateId(),
504
+ createdAt: Date.now(),
505
+ updatedAt: Date.now(),
506
+ ...payload
507
+ };
508
+ state.tasks.push(newTask);
509
+ saveTasks();
510
+ }
511
+
512
+ renderAll();
513
+ closeDialog();
514
+ }
515
+
516
+ function handleDialogDelete() {
517
+ const editingId = dom.taskForm.dataset.editingId;
518
+ if (!editingId) return;
519
+ if (!confirm("\u3053\u306e\u30bf\u30b9\u30af\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\u5b50\u30bf\u30b9\u30af\u3082\u524a\u9664\u3055\u308c\u307e\u3059\u3002")) return;
520
+ deleteTask(editingId);
521
+ renderAll();
522
+ closeDialog();
523
+ }
524
+
525
+ function resetFilters() {
526
+ state.filters.search = "";
527
+ state.filters.searchRaw = "";
528
+ state.filters.selectedTags = [];
529
+ state.filters.dateFrom = "";
530
+ state.filters.dateTo = "";
531
+ state.filters.parentId = "";
532
+ state.filters.parentSearch = "";
533
+ state.parentViewSearch = "";
534
+ state.parentViewSearch = "";
535
+ state.highlightParentId = "";
536
+ state.selectedIds.clear();
537
+ state.selectionMode = false;
538
+ state.showParentView = false;
539
+ state.activeStatus = STATUSES[0].id;
540
+ if (dom.searchInput) dom.searchInput.value = "";
541
+ if (dom.dateFrom) dom.dateFrom.value = "";
542
+ if (dom.dateTo) dom.dateTo.value = "";
543
+ if (dom.parentFilterSearch) dom.parentFilterSearch.value = "";
544
+ if (dom.parentViewSearch) dom.parentViewSearch.value = "";
545
+ if (dom.parentViewClear) dom.parentViewClear.disabled = true;
546
+ renderAll();
547
+ }
548
+
549
+ function handleBulkDelete() {
550
+ if (!state.selectedIds.size) return;
551
+ if (!confirm("\u9078\u629e\u3057\u305f\u30bf\u30b9\u30af\u3092\u307e\u3068\u3081\u3066\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\u5b50\u30bf\u30b9\u30af\u3082\u524a\u9664\u3055\u308c\u307e\u3059\u3002")) {
552
+ return;
553
+ }
554
+ [...state.selectedIds].forEach((taskId) => deleteTask(taskId));
555
+ state.selectedIds.clear();
556
+ updateBulkActions();
557
+ renderAll();
558
+ }
559
+
560
+ function handleBulkMove(statusId) {
561
+ if (!state.selectedIds.size) return;
562
+ state.selectedIds.forEach((taskId) => updateTask(taskId, { status: statusId }, { skipRender: true, skipPersist: true }));
563
+ saveTasks();
564
+ renderAll();
565
+ }
566
+
567
+ function handleSelectAll() {
568
+ const filtered = getFilteredTasks();
569
+ filtered.forEach((task) => state.selectedIds.add(task.id));
570
+ updateBulkActions();
571
+ renderAll();
572
+ }
573
+
574
+ function focusParentTasks(parentId) {
575
+ const parentTask = getTask(parentId);
576
+ state.filters.search = "";
577
+ state.filters.searchRaw = "";
578
+ state.filters.selectedTags = [];
579
+ state.filters.dateFrom = "";
580
+ state.filters.dateTo = "";
581
+ state.filters.parentId = parentId;
582
+ state.highlightParentId = parentId;
583
+ state.showParentView = false;
584
+ state.selectionMode = false;
585
+ state.selectedIds.clear();
586
+ state.filters.parentSearch = "";
587
+ state.parentViewSearch = "";
588
+ if (parentTask) {
589
+ state.activeStatus = parentTask.status;
590
+ }
591
+ renderAll();
592
+ }
593
+
594
+ function toggleParentView() {
595
+ state.showParentView = !state.showParentView;
596
+ if (state.showParentView) {
597
+ state.selectionMode = false;
598
+ state.selectedIds.clear();
599
+ state.parentViewSearch = "";
600
+ } else {
601
+ state.highlightParentId = "";
602
+ }
603
+ renderAll();
604
+ }
605
+
606
+ function toggleSelectionMode() {
607
+ state.selectionMode = !state.selectionMode;
608
+ if (!state.selectionMode) {
609
+ state.selectedIds.clear();
610
+ }
611
+ renderAll();
612
+ }
613
+
614
+ function renderParentClusters() {
615
+ dom.parentClusters.innerHTML = "";
616
+ if (!state.showParentView) return;
617
+
618
+ if (dom.parentViewSearch) {
619
+ dom.parentViewSearch.value = state.parentViewSearch;
620
+ }
621
+ if (dom.parentViewClear) {
622
+ dom.parentViewClear.disabled = !state.highlightParentId;
623
+ }
624
+
625
+ const searchTerm = state.parentViewSearch.trim().toLowerCase();
626
+
627
+ const parents = state.tasks
628
+ .filter((task) => !task.parentId)
629
+ .filter((parent) => {
630
+ const matchesParent = matchesTaskFilters(parent);
631
+ const childMatches = state.tasks.some((child) => child.parentId === parent.id && matchesTaskFilters(child));
632
+ if (!(matchesParent || childMatches)) {
633
+ return false;
634
+ }
635
+
636
+ if (!searchTerm) {
637
+ return true;
638
+ }
639
+
640
+ const parentHaystack = [
641
+ parent.title,
642
+ parent.assignee,
643
+ parent.progress,
644
+ parent.notes,
645
+ parent.link,
646
+ ...(parent.tags ?? [])
647
+ ]
648
+ .filter(Boolean)
649
+ .join(" ")
650
+ .toLowerCase();
651
+ if (parentHaystack.includes(searchTerm)) {
652
+ return true;
653
+ }
654
+
655
+ return state.tasks.some((child) => {
656
+ if (child.parentId !== parent.id) return false;
657
+ const childHaystack = [
658
+ child.title,
659
+ child.assignee,
660
+ child.progress,
661
+ child.notes,
662
+ child.link,
663
+ ...(child.tags ?? [])
664
+ ]
665
+ .filter(Boolean)
666
+ .join(" ")
667
+ .toLowerCase();
668
+ return childHaystack.includes(searchTerm);
669
+ });
670
+ })
671
+ .sort((a, b) => a.title.localeCompare(b.title, "ja"));
672
+
673
+ if (!parents.length) {
674
+ const empty = document.createElement("div");
675
+ empty.className = "parent-empty";
676
+ empty.textContent = "条件に一致する親タスクがありません。";
677
+ dom.parentClusters.appendChild(empty);
678
+ return;
679
+ }
680
+
681
+ parents.forEach((parent) => {
682
+ const children = state.tasks.filter((task) => task.parentId === parent.id);
683
+ const isHighlighted = state.highlightParentId === parent.id;
684
+ let expanded = isHighlighted;
685
+
686
+ const cluster = document.createElement("div");
687
+ cluster.className = "parent-cluster";
688
+ if (isHighlighted) {
689
+ cluster.classList.add("highlighted");
690
+ cluster.classList.add("expanded");
691
+ }
692
+
693
+ const header = document.createElement("div");
694
+ header.className = "cluster-header";
695
+
696
+ const titleBox = document.createElement("div");
697
+ titleBox.className = "cluster-title";
698
+
699
+ const title = document.createElement("h4");
700
+ title.textContent = parent.title;
701
+
702
+ const meta = document.createElement("div");
703
+ meta.className = "cluster-meta";
704
+ const metaParts = [];
705
+ if (parent.assignee) metaParts.push(`担当: ${parent.assignee}`);
706
+ if (parent.dueDate) metaParts.push(`期限: ${formatDate(parent.dueDate)}`);
707
+ metaParts.push(`子タスク ${children.length} 件`);
708
+ meta.textContent = metaParts.join(" / ");
709
+
710
+ titleBox.append(title, meta);
711
+
712
+ const statusStrip = document.createElement("div");
713
+ statusStrip.className = "cluster-status";
714
+ STATUSES.forEach((status) => {
715
+ const count = children.filter((child) => child.status === status.id).length;
716
+ const pill = document.createElement("span");
717
+ pill.className = "status-pill";
718
+ pill.textContent = `${status.label} ${count}`;
719
+ statusStrip.appendChild(pill);
720
+ });
721
+
722
+ const actions = document.createElement("div");
723
+ actions.className = "cluster-actions";
724
+
725
+ const toggleButton = document.createElement("button");
726
+ toggleButton.type = "button";
727
+ toggleButton.className = "btn tiny ghost";
728
+ toggleButton.textContent = expanded ? "折りたたむ" : "展開";
729
+
730
+ const focusButton = document.createElement("button");
731
+ focusButton.type = "button";
732
+ focusButton.className = "btn tiny subtle";
733
+ focusButton.textContent = "ボードで表示";
734
+
735
+ actions.append(toggleButton, focusButton);
736
+ header.append(titleBox, statusStrip, actions);
737
+
738
+ const body = document.createElement("div");
739
+ body.className = "cluster-body";
740
+ body.hidden = !expanded;
741
+
742
+ if (children.length) {
743
+ children
744
+ .sort((a, b) => a.title.localeCompare(b.title, "ja"))
745
+ .forEach((child) => {
746
+ const row = document.createElement("div");
747
+ row.className = "cluster-child-row";
748
+ const statusLabel = STATUSES.find((status) => status.id === child.status)?.label ?? "";
749
+ row.innerHTML = `<span class="child-title">${child.title}</span><span class="child-status">${statusLabel}</span>`;
750
+ row.addEventListener("click", (event) => {
751
+ event.stopPropagation();
752
+ focusParentTasks(parent.id);
753
+ });
754
+ body.appendChild(row);
755
+ });
756
+ } else {
757
+ const emptyChild = document.createElement("div");
758
+ emptyChild.className = "cluster-child-empty";
759
+ emptyChild.textContent = "子タスクはまだありません。";
760
+ body.appendChild(emptyChild);
761
+ }
762
+
763
+ toggleButton.addEventListener("click", (event) => {
764
+ event.stopPropagation();
765
+ expanded = !expanded;
766
+ body.hidden = !expanded;
767
+ cluster.classList.toggle("expanded", expanded);
768
+ toggleButton.textContent = expanded ? "折りたたむ" : "展開";
769
+ });
770
+
771
+ focusButton.addEventListener("click", (event) => {
772
+ event.stopPropagation();
773
+ focusParentTasks(parent.id);
774
+ });
775
+
776
+ header.addEventListener("click", (event) => {
777
+ if (event.target.closest("button")) return;
778
+ focusParentTasks(parent.id);
779
+ });
780
+
781
+ cluster.append(header, body);
782
+ dom.parentClusters.appendChild(cluster);
783
+ });
784
+ }
785
+
786
+ function matchesTaskFilters(task, options = {}) {
787
+ const { includeTags = true } = options;
788
+ const search = state.filters.search;
789
+ const selectedTags = state.filters.selectedTags;
790
+ const from = state.filters.dateFrom;
791
+ const to = state.filters.dateTo;
792
+
793
+ if (search) {
794
+ const haystack = [
795
+ task.title,
796
+ task.assignee,
797
+ task.progress,
798
+ task.notes,
799
+ task.link,
800
+ ...(task.tags ?? [])
801
+ ]
802
+ .filter(Boolean)
803
+ .join(" ")
804
+ .toLowerCase();
805
+ if (!haystack.includes(search)) {
806
+ return false;
807
+ }
808
+ }
809
+
810
+ if (includeTags && selectedTags.length) {
811
+ const lowerTags = (task.tags ?? []).map((tag) => tag.toLowerCase());
812
+ const matchesAll = selectedTags.every((tag) => lowerTags.includes(tag.toLowerCase()));
813
+ if (!matchesAll) return false;
814
+ }
815
+
816
+ if (from && task.dueDate && task.dueDate < from) return false;
817
+ if (to && task.dueDate && task.dueDate > to) return false;
818
+
819
+ return true;
820
+ }
821
+
822
+ function getFilteredTasks(options = {}) {
823
+ const { includeTagFilter = true } = options;
824
+ const parentId = state.filters.parentId;
825
+ const highlight = state.highlightParentId;
826
+
827
+ return state.tasks.filter((task) => {
828
+ if (!matchesTaskFilters(task, { includeTags: includeTagFilter })) {
829
+ return false;
830
+ }
831
+
832
+ if (parentId && !(task.id === parentId || task.parentId === parentId)) {
833
+ return false;
834
+ }
835
+
836
+ if (highlight && !(task.id === highlight || task.parentId === highlight)) {
837
+ return false;
838
+ }
839
+
840
+ return true;
841
+ });
842
+ }
843
+
844
+ function groupByStatus(tasks) {
845
+ return tasks.reduce((acc, task) => {
846
+ if (!acc[task.status]) acc[task.status] = [];
847
+ acc[task.status].push(task);
848
+ return acc;
849
+ }, {});
850
+ }
851
+
852
+ function updateTask(taskId, patch, options = {}) {
853
+ const task = getTask(taskId);
854
+ if (!task) return;
855
+ const { skipRender = false, skipPersist = false } = options;
856
+ Object.assign(task, patch, { updatedAt: Date.now() });
857
+ if (!skipPersist) {
858
+ saveTasks();
859
+ }
860
+ if (!skipRender) {
861
+ renderAll();
862
+ }
863
+ }
864
+
865
+ function deleteTask(taskId) {
866
+ const childIds = state.tasks.filter((task) => task.parentId === taskId).map((task) => task.id);
867
+ state.tasks = state.tasks.filter((task) => task.id !== taskId);
868
+ childIds.forEach((childId) => deleteTask(childId));
869
+ state.selectedIds.delete(taskId);
870
+ saveTasks();
871
+ }
872
+
873
+ function getTask(taskId) {
874
+ return state.tasks.find((task) => task.id === taskId);
875
+ }
876
+
877
+ function isDescendant(candidateId, targetId) {
878
+ if (!candidateId || !targetId) return false;
879
+ let cursor = state.tasks.find((task) => task.id === candidateId)?.parentId;
880
+ while (cursor) {
881
+ if (cursor === targetId) return true;
882
+ cursor = state.tasks.find((task) => task.id === cursor)?.parentId;
883
+ }
884
+ return false;
885
+ }
886
+
887
+ function updateBulkActions() {
888
+ const visible = state.selectionMode;
889
+ const count = state.selectedIds.size;
890
+ if (dom.selectionToolbar) {
891
+ dom.selectionToolbar.classList.toggle("active", visible);
892
+ }
893
+ if (dom.selectionCount) {
894
+ dom.selectionCount.textContent = `${count}\u4EF6\u9078\u629E\u4E2D`;
895
+ }
896
+ dom.bulkDeleteBtn.disabled = count === 0;
897
+ dom.clearSelectionBtn.disabled = count === 0;
898
+ dom.bulkStatusButtons.querySelectorAll("button").forEach((button) => {
899
+ button.disabled = count === 0;
900
+ });
901
+ }
902
+
903
+ if (document.readyState === "loading") {
904
+ document.addEventListener("DOMContentLoaded", init, { once: true });
905
+ } else {
906
+ init();
907
+ }
908
+
909
+ function formatDate(isoDate) {
910
+ if (!isoDate) return "";
911
+ const [year, month, day] = isoDate.split("-");
912
+ return `${year}/${month}/${day}`;
913
+ }
914
+
915
+ function generateId() {
916
+ if (crypto?.randomUUID) return crypto.randomUUID();
917
+ return `task-${Date.now()}-${Math.random().toString(16).slice(2)}`;
918
+ }
919
+
920
+ function createSampleTasks() {
921
+ const now = new Date();
922
+ const iso = (offset) => {
923
+ const base = new Date(now);
924
+ base.setDate(base.getDate() + offset);
925
+ return base.toISOString().slice(0, 10);
926
+ };
927
+
928
+ const projectId = generateId();
929
+ return [
930
+ {
931
+ id: projectId,
932
+ title: "\u30e9\u30a4\u30f3\u6539\u5584\u306e\u5168\u4f53\u8a2d\u8a08",
933
+ assignee: "\u7530\u4e2d",
934
+ dueDate: iso(7),
935
+ link: "https://www.toyota.co.jp/",
936
+ tags: ["\u5de5\u7a0b\u7ba1\u7406", "\u512a\u5148"],
937
+ progress: "\u73fe\u72b6\u5206\u6790\u3092\u5b8c\u4e86\u3002\u6539\u5584\u6848\u306e\u521d\u7a3f\u3092\u4f5c\u6210\u6e08\u307f\u3002",
938
+ notes: "\u30b3\u30b9\u30c8\u8a66\u7b97\u3092\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u3042\u308a\u3002",
939
+ parentId: "",
940
+ status: "ready",
941
+ createdAt: Date.now(),
942
+ updatedAt: Date.now()
943
+ },
944
+ {
945
+ id: generateId(),
946
+ title: "\u6f5c\u5728\u30dc\u30c8\u30eb\u30cd\u30c3\u30af\u306e\u8abf\u67fb",
947
+ assignee: "\u4f50\u85e4",
948
+ dueDate: iso(3),
949
+ link: "",
950
+ tags: ["\u5de5\u7a0b\u7ba1\u7406", "\u8abf\u67fb"],
951
+ progress: "\u30e9\u30a4\u30f3\u505c\u6b62\u30ed\u30b0\u304b\u30895\u4ef6\u306e\u5019\u88dc\u3092\u62bd\u51fa\u3002",
952
+ notes: "\u73fe\u5834\u30d2\u30a2\u30ea\u30f3\u30b0\u3092\u4e88\u5b9a\u3002",
953
+ parentId: projectId,
954
+ status: "inprogress",
955
+ createdAt: Date.now(),
956
+ updatedAt: Date.now()
957
+ },
958
+ {
959
+ id: generateId(),
960
+ title: "\u5b89\u5168\u6559\u80b2\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u898b\u76f4\u3057",
961
+ assignee: "\u4e2d\u6751",
962
+ dueDate: iso(12),
963
+ link: "file:///\\\\server\\docs\\safety",
964
+ tags: ["\u6559\u80b2", "\u5b89\u5168"],
965
+ progress: "\u65e2\u5b58\u6559\u6750\u306e\u68da\u5378\u3057\u5b8c\u4e86\u3002",
966
+ notes: "\u65b0\u5165\u793e\u54e1\u5411\u3051\u306e\u8ffd\u52a0\u30d1\u30fc\u30c8\u304c\u5fc5\u8981\u3002",
967
+ parentId: "",
968
+ status: "backlog",
969
+ createdAt: Date.now(),
970
+ updatedAt: Date.now()
971
+ },
972
+ {
973
+ id: generateId(),
974
+ title: "AI\u5224\u5b9a\u30e2\u30c7\u30eb\u306e\u7cbe\u5ea6\u691c\u8a3c",
975
+ assignee: "\u9234\u6728",
976
+ dueDate: iso(-2),
977
+ link: "",
978
+ tags: ["AI", "\u54c1\u8cea"],
979
+ progress: "\u30c6\u30b9\u30c8\u30c7\u30fc\u30bf\u306795%\u7cbe\u5ea6\u3002",
980
+ notes: "\u73fe\u5834\u5c0e\u5165\u524d\u306b\u6700\u7d42\u78ba\u8a8d\u304c\u5fc5\u9808\u3002",
981
+ parentId: "",
982
+ status: "waiting",
983
+ createdAt: Date.now(),
984
+ updatedAt: Date.now()
985
+ },
986
+ {
987
+ id: generateId(),
988
+ title: "\u6539\u5584\u6848\u30ec\u30d3\u30e5\u30fc\u4f1a\u8b70",
989
+ assignee: "\u7530\u4e2d",
990
+ dueDate: iso(1),
991
+ link: "https://teams.microsoft.com/",
992
+ tags: ["\u4f1a\u8b70", "\u512a\u5148"],
993
+ progress: "\u30a2\u30b8\u30a7\u30f3\u30c0\u4f5c\u6210\u6e08\u307f\u3002",
994
+ notes: "\u53c2\u52a0\u8005\u3078\u306e\u8cc7\u6599\u914d\u5e03\u3092\u5b8c\u4e86\u3002",
995
+ parentId: projectId,
996
+ status: "ready",
997
+ createdAt: Date.now(),
998
+ updatedAt: Date.now()
999
+ }
1000
+ ];
1001
+ }
1002
+
1003
+
1004
+
1005
+
1006
+
kanban_v2/styles.css ADDED
@@ -0,0 +1,967 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
2
+
3
+ :root {
4
+ --bg: #040916;
5
+ --card-bg: rgba(10, 18, 38, 0.86);
6
+ --card-border: rgba(86, 116, 184, 0.32);
7
+ --accent: #6f8dff;
8
+ --accent-soft: rgba(111, 141, 255, 0.28);
9
+ --text-primary: #e9eefb;
10
+ --text-secondary: #9fb2dd;
11
+ --shadow-soft: 0 26px 60px rgba(2, 6, 20, 0.68);
12
+ --radius-large: 24px;
13
+ --radius-medium: 16px;
14
+ --radius-small: 12px;
15
+ --surface-muted: rgba(12, 22, 48, 0.82);
16
+ --surface-subtle: rgba(16, 28, 60, 0.88);
17
+ }
18
+
19
+ *,
20
+ *::before,
21
+ *::after {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ font-family: "Inter", "Hiragino Sans", "Noto Sans JP", sans-serif;
28
+ background: radial-gradient(130% 130% at 18% -12%, #162b52 0%, #0a1a36 48%, #030711 100%);
29
+ color: var(--text-primary);
30
+ min-height: 100vh;
31
+ }
32
+
33
+ .app-shell {
34
+ max-width: 1600px;
35
+ margin: 0 auto;
36
+ padding: 32px 40px 80px;
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 24px;
40
+ }
41
+
42
+ .app-header {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: center;
46
+ padding: 24px 32px;
47
+ background: var(--card-bg);
48
+ backdrop-filter: blur(18px);
49
+ border: 1px solid var(--card-border);
50
+ border-radius: var(--radius-large);
51
+ box-shadow: var(--shadow-soft);
52
+ }
53
+
54
+ .brand {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 18px;
58
+ }
59
+
60
+ .brand-mark {
61
+ display: grid;
62
+ place-items: center;
63
+ width: 54px;
64
+ height: 54px;
65
+ border-radius: 17px;
66
+ background: linear-gradient(145deg, rgba(111, 141, 255, 0.28), rgba(152, 176, 255, 0.54));
67
+ color: var(--accent);
68
+ font-size: 20px;
69
+ font-weight: 600;
70
+ }
71
+
72
+ .brand h1 {
73
+ margin: 0;
74
+ font-size: 20px;
75
+ font-weight: 600;
76
+ letter-spacing: 0.2em;
77
+ }
78
+
79
+ .subtitle {
80
+ margin: 4px 0 0;
81
+ font-size: 12px;
82
+ color: var(--text-secondary);
83
+ }
84
+
85
+ .primary-controls {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 12px;
89
+ }
90
+
91
+ .filters {
92
+ display: grid;
93
+ grid-template-columns: minmax(0, 2.8fr) repeat(2, minmax(0, 1fr));
94
+ gap: 16px;
95
+ align-items: end;
96
+ background: var(--card-bg);
97
+ border: 1px solid var(--card-border);
98
+ border-radius: var(--radius-large);
99
+ padding: 24px 28px;
100
+ box-shadow: var(--shadow-soft);
101
+ }
102
+
103
+ .filters label {
104
+ display: block;
105
+ font-size: 12px;
106
+ font-weight: 600;
107
+ color: var(--text-secondary);
108
+ margin-bottom: 6px;
109
+ }
110
+
111
+ .filters input,
112
+ .filters select,
113
+ .parent-view-controls input {
114
+ width: 100%;
115
+ min-width: 0;
116
+ border: 1px solid rgba(82, 114, 188, 0.38);
117
+ border-radius: var(--radius-small);
118
+ padding: 10px 14px;
119
+ background: var(--surface-subtle);
120
+ font-size: 14px;
121
+ color: var(--text-primary);
122
+ transition: border 0.2s ease, box-shadow 0.2s ease;
123
+ }
124
+
125
+ .filters input:focus,
126
+ .filters select:focus,
127
+ .parent-view-controls input:focus {
128
+ outline: none;
129
+ border-color: var(--accent);
130
+ box-shadow: 0 0 0 4px var(--accent-soft);
131
+ }
132
+
133
+ .filters input::placeholder,
134
+ .parent-view-controls input::placeholder {
135
+ color: rgba(149, 169, 214, 0.55);
136
+ }
137
+
138
+ .date-inputs {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 10px;
142
+ }
143
+
144
+ .date-inputs input {
145
+ flex: 1 1 0;
146
+ min-width: 0;
147
+ }
148
+
149
+ .date-inputs span {
150
+ white-space: nowrap;
151
+ font-size: 12px;
152
+ color: var(--text-secondary);
153
+ }
154
+
155
+ .search-box {
156
+ grid-column: 1 / span 2;
157
+ }
158
+
159
+ .parent-filter {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 10px;
163
+ }
164
+
165
+ .parent-filter input {
166
+ border-radius: var(--radius-small);
167
+ }
168
+
169
+ .tag-filter-row {
170
+ grid-column: 1 / -1;
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 12px;
174
+ flex-wrap: wrap;
175
+ padding-top: 4px;
176
+ }
177
+
178
+ .filter-actions {
179
+ grid-column: 1 / -1;
180
+ display: flex;
181
+ justify-content: flex-end;
182
+ margin-top: 8px;
183
+ }
184
+
185
+ .tag-filter-label {
186
+ font-size: 12px;
187
+ font-weight: 600;
188
+ color: var(--text-secondary);
189
+ }
190
+
191
+ .tag-toggle-container {
192
+ display: flex;
193
+ flex-wrap: wrap;
194
+ gap: 8px;
195
+ align-items: center;
196
+ flex: 1;
197
+ }
198
+
199
+ .tag-toggle {
200
+ border: 1px solid rgba(82, 114, 188, 0.45);
201
+ border-radius: 999px;
202
+ background: var(--surface-muted);
203
+ font-size: 12px;
204
+ font-weight: 500;
205
+ font-family: inherit;
206
+ padding: 6px 12px;
207
+ color: var(--text-secondary);
208
+ cursor: pointer;
209
+ transition: background 0.2s ease, color 0.2s ease, border 0.2s ease, box-shadow 0.2s ease;
210
+ }
211
+
212
+ .tag-toggle:hover {
213
+ border-color: var(--accent);
214
+ background: rgba(22, 35, 70, 0.92);
215
+ box-shadow: 0 0 0 3px var(--accent-soft);
216
+ }
217
+
218
+ .tag-toggle.active {
219
+ background: linear-gradient(135deg, #6789ff, #94a9ff);
220
+ color: #050f24;
221
+ border-color: transparent;
222
+ box-shadow: 0 14px 26px rgba(111, 141, 255, 0.32);
223
+ }
224
+
225
+ .tag-toggle-empty {
226
+ font-size: 12px;
227
+ color: var(--text-secondary);
228
+ }
229
+
230
+ .selection-toolbar {
231
+ display: none;
232
+ justify-content: space-between;
233
+ align-items: center;
234
+ gap: 16px;
235
+ flex-wrap: wrap;
236
+ background: var(--card-bg);
237
+ border: 1px solid var(--card-border);
238
+ border-radius: var(--radius-large);
239
+ padding: 18px 24px;
240
+ box-shadow: var(--shadow-soft);
241
+ opacity: 0;
242
+ pointer-events: none;
243
+ transition: opacity 0.2s ease;
244
+ }
245
+
246
+ .selection-toolbar.active {
247
+ display: flex;
248
+ opacity: 1;
249
+ pointer-events: all;
250
+ }
251
+ body.parent-view-mode .status-dock,
252
+ body.parent-view-mode .board,
253
+ body.parent-view-mode .selection-toolbar {
254
+ display: none !important;
255
+ }
256
+
257
+ body.parent-view-mode .parent-view {
258
+ display: flex;
259
+ }
260
+
261
+
262
+ .selection-summary {
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 12px;
266
+ flex-wrap: wrap;
267
+ font-size: 13px;
268
+ color: var(--text-secondary);
269
+ }
270
+
271
+ .selection-actions {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 16px;
275
+ flex-wrap: wrap;
276
+ }
277
+
278
+ .selection-status {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 10px;
282
+ flex-wrap: wrap;
283
+ font-size: 12px;
284
+ color: var(--text-secondary);
285
+ }
286
+
287
+ .status-button-group {
288
+ display: flex;
289
+ gap: 8px;
290
+ flex-wrap: wrap;
291
+ }
292
+
293
+ .status-dock {
294
+ display: grid;
295
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
296
+ gap: 12px;
297
+ padding: 12px 16px;
298
+ background: transparent;
299
+ }
300
+
301
+ .status-chip {
302
+ position: relative;
303
+ padding: 14px 20px;
304
+ border-radius: var(--radius-medium);
305
+ background: var(--surface-muted);
306
+ border: 1px solid rgba(82, 114, 188, 0.28);
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: space-between;
310
+ gap: 16px;
311
+ font-size: 14px;
312
+ font-weight: 600;
313
+ letter-spacing: 0.04em;
314
+ font-family: inherit;
315
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border 0.2s ease, background 0.2s ease;
316
+ cursor: pointer;
317
+ outline: none;
318
+ color: var(--text-primary);
319
+ }
320
+
321
+ .status-chip:hover {
322
+ transform: translateY(-1px);
323
+ box-shadow: 0 12px 24px rgba(4, 10, 28, 0.32);
324
+ }
325
+
326
+ .status-chip:focus-visible {
327
+ border-color: var(--accent);
328
+ box-shadow: 0 0 0 4px var(--accent-soft);
329
+ }
330
+
331
+ .status-chip.active {
332
+ background: linear-gradient(135deg, rgba(111, 141, 255, 0.26), rgba(162, 184, 255, 0.42));
333
+ border-color: rgba(128, 156, 255, 0.5);
334
+ box-shadow: 0 16px 28px rgba(111, 141, 255, 0.28);
335
+ }
336
+
337
+ .status-chip-text {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 4px;
341
+ text-align: left;
342
+ }
343
+
344
+ .status-chip-text strong {
345
+ font-weight: 600;
346
+ font-size: 14px;
347
+ }
348
+
349
+ .status-chip-text span {
350
+ font-size: 12px;
351
+ font-weight: 500;
352
+ color: var(--text-secondary);
353
+ }
354
+
355
+ .status-chip.active .status-chip-text span {
356
+ color: rgba(6, 15, 36, 0.72);
357
+ }
358
+
359
+ .status-chip-count {
360
+ font-size: 13px;
361
+ font-weight: 600;
362
+ padding: 6px 12px;
363
+ border-radius: 999px;
364
+ background: rgba(102, 136, 212, 0.26);
365
+ color: var(--text-secondary);
366
+ }
367
+
368
+ .status-chip.active .status-chip-count {
369
+ background: rgba(214, 226, 255, 0.34);
370
+ color: var(--accent);
371
+ }
372
+
373
+ .board {
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: 18px;
377
+ }
378
+
379
+ .column {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 18px;
383
+ min-height: 420px;
384
+ background: var(--card-bg);
385
+ border: 1px solid var(--card-border);
386
+ border-radius: var(--radius-large);
387
+ padding: 24px 20px;
388
+ box-shadow: var(--shadow-soft);
389
+ }
390
+
391
+ .column-header {
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: space-between;
395
+ font-weight: 600;
396
+ letter-spacing: 0.08em;
397
+ text-transform: uppercase;
398
+ font-size: 14px;
399
+ color: var(--text-secondary);
400
+ }
401
+
402
+ .column-count {
403
+ background: rgba(82, 114, 188, 0.26);
404
+ padding: 6px 12px;
405
+ border-radius: 999px;
406
+ font-size: 12px;
407
+ }
408
+
409
+ .column-body {
410
+ display: flex;
411
+ flex-direction: column;
412
+ gap: 16px;
413
+ flex: 1;
414
+ }
415
+
416
+ .column-body.empty::before {
417
+ content: "\u3053\u306E\u30B9\u30C6\u30FC\u30BF\u30B9\u306E\u30BF\u30B9\u30AF\u306F\u3042\u308A\u307E\u305B\u3093";
418
+ display: grid;
419
+ place-items: center;
420
+ border: 1px dashed rgba(82, 114, 188, 0.38);
421
+ border-radius: var(--radius-medium);
422
+ padding: 40px;
423
+ color: rgba(176, 196, 235, 0.65);
424
+ font-size: 13px;
425
+ letter-spacing: 0.05em;
426
+ }
427
+
428
+ .task-card {
429
+ background: rgba(15, 27, 56, 0.88);
430
+ border-radius: var(--radius-large);
431
+ border: 1px solid rgba(82, 114, 188, 0.32);
432
+ padding: 18px 22px;
433
+ box-shadow: 0 18px 32px rgba(4, 10, 26, 0.36);
434
+ display: flex;
435
+ flex-direction: column;
436
+ gap: 12px;
437
+ min-width: 100%;
438
+ color: var(--text-primary);
439
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
440
+ }
441
+
442
+ .task-card.highlighted {
443
+ box-shadow: 0 0 0 3px rgba(111, 141, 255, 0.45);
444
+ }
445
+
446
+ .task-card-header {
447
+ display: flex;
448
+ align-items: flex-start;
449
+ justify-content: space-between;
450
+ gap: 12px;
451
+ }
452
+
453
+ .title-row {
454
+ display: flex;
455
+ align-items: flex-start;
456
+ gap: 12px;
457
+ }
458
+
459
+ .task-title {
460
+ margin: 0;
461
+ font-size: 18px;
462
+ font-weight: 600;
463
+ color: var(--text-primary);
464
+ }
465
+
466
+ .task-select {
467
+ width: 18px;
468
+ height: 18px;
469
+ margin-top: 3px;
470
+ display: none;
471
+ }
472
+
473
+ body.selection-mode .task-select {
474
+ display: block;
475
+ }
476
+
477
+ body.selection-mode .task-card {
478
+ padding-top: 26px;
479
+ }
480
+
481
+ .status-quick-buttons {
482
+ display: flex;
483
+ gap: 6px;
484
+ }
485
+
486
+ .status-quick-buttons button {
487
+ border: none;
488
+ padding: 6px 10px;
489
+ border-radius: var(--radius-small);
490
+ font-size: 11px;
491
+ font-weight: 600;
492
+ letter-spacing: 0.05em;
493
+ background: rgba(82, 114, 188, 0.26);
494
+ color: var(--text-primary);
495
+ cursor: pointer;
496
+ transition: background 0.2s ease, color 0.2s ease;
497
+ }
498
+
499
+ .status-quick-buttons button:hover {
500
+ background: var(--accent);
501
+ color: white;
502
+ }
503
+
504
+ .task-meta {
505
+ display: flex;
506
+ gap: 12px;
507
+ font-size: 12px;
508
+ color: var(--text-secondary);
509
+ letter-spacing: 0.04em;
510
+ }
511
+
512
+ .task-body {
513
+ display: grid;
514
+ grid-template-columns: repeat(2, minmax(0, 1fr));
515
+ gap: 12px;
516
+ }
517
+
518
+ .task-body > div {
519
+ padding: 12px;
520
+ border-radius: var(--radius-small);
521
+ background: rgba(11, 22, 46, 0.7);
522
+ border: 1px solid rgba(82, 114, 188, 0.22);
523
+ font-size: 13px;
524
+ line-height: 1.6;
525
+ min-height: 64px;
526
+ color: var(--text-primary);
527
+ }
528
+
529
+ .task-body .resource-link {
530
+ grid-column: span 2;
531
+ color: var(--accent);
532
+ font-size: 13px;
533
+ text-decoration: none;
534
+ word-break: break-all;
535
+ }
536
+
537
+ .task-tags {
538
+ display: flex;
539
+ gap: 8px;
540
+ flex-wrap: wrap;
541
+ }
542
+
543
+ .task-tag {
544
+ padding: 4px 12px;
545
+ border-radius: 999px;
546
+ font-size: 11px;
547
+ font-weight: 600;
548
+ letter-spacing: 0.08em;
549
+ background: rgba(111, 141, 255, 0.26);
550
+ color: var(--accent);
551
+ }
552
+
553
+ .task-children {
554
+ border-left: 2px solid rgba(111, 141, 255, 0.34);
555
+ padding-left: 12px;
556
+ display: flex;
557
+ flex-direction: column;
558
+ gap: 6px;
559
+ }
560
+
561
+ .task-child {
562
+ font-size: 12px;
563
+ color: var(--text-secondary);
564
+ display: flex;
565
+ justify-content: space-between;
566
+ gap: 8px;
567
+ }
568
+
569
+ .task-footer {
570
+ display: flex;
571
+ gap: 8px;
572
+ }
573
+
574
+ .btn {
575
+ border: none;
576
+ border-radius: 999px;
577
+ padding: 10px 18px;
578
+ font-size: 13px;
579
+ font-weight: 600;
580
+ letter-spacing: 0.08em;
581
+ cursor: pointer;
582
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
583
+ }
584
+
585
+ .btn.primary,
586
+ .btn.ghost.active,
587
+ .btn.subtle.active {
588
+ background: linear-gradient(135deg, #6284ff, #8ea6ff);
589
+ color: #f8fbff;
590
+ box-shadow: 0 14px 26px rgba(111, 141, 255, 0.34);
591
+ }
592
+
593
+ .btn.primary:hover,
594
+ .btn.ghost.active:hover,
595
+ .btn.subtle.active:hover {
596
+ transform: translateY(-1px);
597
+ box-shadow: 0 18px 36px rgba(111, 141, 255, 0.48);
598
+ }
599
+
600
+ .btn.ghost {
601
+ background: rgba(11, 22, 46, 0.54);
602
+ color: var(--text-secondary);
603
+ }
604
+
605
+ .btn.ghost:hover {
606
+ background: rgba(17, 30, 60, 0.7);
607
+ }
608
+
609
+ .btn.subtle {
610
+ background: rgba(82, 114, 188, 0.28);
611
+ color: var(--text-secondary);
612
+ }
613
+
614
+ .btn.subtle:hover {
615
+ background: rgba(99, 136, 212, 0.36);
616
+ }
617
+
618
+ .btn.tiny {
619
+ padding: 6px 16px;
620
+ font-size: 11px;
621
+ border-radius: var(--radius-small);
622
+ }
623
+
624
+ .btn.small {
625
+ padding: 8px 16px;
626
+ font-size: 12px;
627
+ }
628
+
629
+ .btn.danger {
630
+ background: rgba(255, 59, 48, 0.16);
631
+ color: #ff3b30;
632
+ }
633
+
634
+ .btn.danger:hover {
635
+ background: rgba(255, 59, 48, 0.24);
636
+ }
637
+
638
+ .icon-button {
639
+ border: none;
640
+ background: transparent;
641
+ font-size: 18px;
642
+ cursor: pointer;
643
+ color: var(--text-secondary);
644
+ }
645
+
646
+ .task-dialog {
647
+ border: none;
648
+ border-radius: 32px;
649
+ padding: 0;
650
+ width: min(640px, 94vw);
651
+ background: rgba(11, 22, 48, 0.94);
652
+ box-shadow: 0 40px 80px rgba(3, 8, 22, 0.56);
653
+ color: var(--text-primary);
654
+ }
655
+
656
+ .task-dialog::backdrop {
657
+ backdrop-filter: blur(12px);
658
+ background: rgba(3, 8, 20, 0.6);
659
+ }
660
+
661
+ .task-dialog form {
662
+ display: flex;
663
+ flex-direction: column;
664
+ gap: 0;
665
+ }
666
+
667
+ .task-dialog header,
668
+ .task-dialog footer {
669
+ padding: 24px 32px;
670
+ display: flex;
671
+ align-items: center;
672
+ justify-content: space-between;
673
+ }
674
+
675
+ .task-dialog header {
676
+ border-bottom: 1px solid rgba(82, 114, 188, 0.26);
677
+ }
678
+
679
+ .task-dialog footer {
680
+ border-top: 1px solid rgba(82, 114, 188, 0.26);
681
+ }
682
+
683
+ .task-dialog .form-body {
684
+ padding: 24px 32px 32px;
685
+ max-height: 70vh;
686
+ overflow-y: auto;
687
+ display: grid;
688
+ gap: 18px;
689
+ }
690
+
691
+ .field {
692
+ display: flex;
693
+ flex-direction: column;
694
+ gap: 8px;
695
+ }
696
+
697
+ .field input,
698
+ .field select,
699
+ .field textarea {
700
+ border: 1px solid rgba(82, 114, 188, 0.38);
701
+ border-radius: var(--radius-medium);
702
+ padding: 12px 16px;
703
+ background: var(--surface-subtle);
704
+ font-size: 14px;
705
+ color: var(--text-primary);
706
+ transition: border 0.2s ease, box-shadow 0.2s ease;
707
+ }
708
+
709
+ .field textarea {
710
+ resize: vertical;
711
+ }
712
+
713
+ .field input:focus,
714
+ .field select:focus,
715
+ .field textarea:focus {
716
+ outline: none;
717
+ border-color: var(--accent);
718
+ box-shadow: 0 0 0 4px var(--accent-soft);
719
+ }
720
+
721
+ .field input::placeholder,
722
+ .field textarea::placeholder {
723
+ color: rgba(149, 169, 214, 0.55);
724
+ }
725
+
726
+ .field-grid {
727
+ display: grid;
728
+ grid-template-columns: repeat(2, minmax(0, 1fr));
729
+ gap: 16px;
730
+ }
731
+
732
+ .parent-view {
733
+ background: var(--card-bg);
734
+ border-radius: var(--radius-large);
735
+ border: 1px solid var(--card-border);
736
+ box-shadow: var(--shadow-soft);
737
+ padding: 24px 28px 32px;
738
+ display: none;
739
+ flex-direction: column;
740
+ gap: 16px;
741
+ }
742
+
743
+ .parent-view header {
744
+ display: flex;
745
+ flex-direction: column;
746
+ gap: 12px;
747
+ }
748
+
749
+ .parent-view-meta {
750
+ display: flex;
751
+ align-items: flex-start;
752
+ justify-content: space-between;
753
+ gap: 16px;
754
+ flex-wrap: wrap;
755
+ }
756
+
757
+ .parent-view-controls {
758
+ display: flex;
759
+ align-items: center;
760
+ gap: 12px;
761
+ flex-wrap: wrap;
762
+ }
763
+
764
+ .parent-view-controls input {
765
+ min-width: 220px;
766
+ width: auto;
767
+ flex: 1 1 260px;
768
+ }
769
+
770
+ .parent-view.active {
771
+ display: flex;
772
+ }
773
+
774
+ .parent-clusters {
775
+ display: grid;
776
+ grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
777
+ gap: 16px;
778
+ }
779
+
780
+ .parent-cluster {
781
+ padding: 20px 24px;
782
+ border-radius: var(--radius-large);
783
+ background: rgba(13, 24, 52, 0.88);
784
+ border: 1px solid rgba(82, 114, 188, 0.34);
785
+ box-shadow: 0 18px 36px rgba(4, 9, 24, 0.34);
786
+ display: flex;
787
+ flex-direction: column;
788
+ gap: 0;
789
+ color: var(--text-primary);
790
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border 0.2s ease;
791
+ }
792
+
793
+ .parent-cluster:hover {
794
+ transform: translateY(-2px);
795
+ box-shadow: 0 22px 44px rgba(4, 9, 24, 0.44);
796
+ }
797
+
798
+ .parent-cluster.highlighted {
799
+ box-shadow: 0 0 0 3px rgba(111, 141, 255, 0.45);
800
+ border-color: rgba(128, 156, 255, 0.5);
801
+ }
802
+
803
+ .cluster-header {
804
+ display: flex;
805
+ flex-direction: column;
806
+ gap: 12px;
807
+ cursor: pointer;
808
+ }
809
+
810
+ .cluster-title {
811
+ display: flex;
812
+ flex-direction: column;
813
+ gap: 4px;
814
+ }
815
+
816
+ .cluster-title h4 {
817
+ margin: 0;
818
+ font-size: 16px;
819
+ color: var(--text-primary);
820
+ }
821
+
822
+ .cluster-meta {
823
+ margin-top: 4px;
824
+ font-size: 12px;
825
+ color: var(--text-secondary);
826
+ }
827
+
828
+ .cluster-status {
829
+ display: flex;
830
+ flex-wrap: wrap;
831
+ gap: 8px;
832
+ }
833
+
834
+ .status-pill {
835
+ padding: 4px 10px;
836
+ border-radius: 999px;
837
+ background: rgba(82, 114, 188, 0.24);
838
+ color: var(--text-secondary);
839
+ font-size: 11px;
840
+ font-weight: 600;
841
+ }
842
+
843
+ .parent-cluster.highlighted .status-pill {
844
+ background: rgba(111, 141, 255, 0.26);
845
+ color: #06132a;
846
+ }
847
+
848
+ .cluster-actions {
849
+ display: flex;
850
+ justify-content: flex-end;
851
+ align-items: center;
852
+ gap: 8px;
853
+ flex-wrap: wrap;
854
+ }
855
+
856
+ .cluster-body {
857
+ display: flex;
858
+ flex-direction: column;
859
+ gap: 8px;
860
+ margin-top: 14px;
861
+ border-top: 1px solid rgba(82, 114, 188, 0.24);
862
+ padding-top: 12px;
863
+ }
864
+
865
+ .cluster-child-row {
866
+ display: flex;
867
+ justify-content: space-between;
868
+ gap: 12px;
869
+ align-items: baseline;
870
+ font-size: 12px;
871
+ color: var(--text-secondary);
872
+ }
873
+
874
+ .cluster-child-row .child-title {
875
+ font-weight: 600;
876
+ color: var(--text-primary);
877
+ }
878
+
879
+ .cluster-child-row .child-status {
880
+ font-size: 12px;
881
+ color: var(--text-secondary);
882
+ }
883
+
884
+ .cluster-child-empty {
885
+ font-size: 12px;
886
+ color: var(--text-secondary);
887
+ }
888
+
889
+ .parent-empty {
890
+ grid-column: 1 / -1;
891
+ padding: 28px;
892
+ border: 1px dashed rgba(82, 114, 188, 0.42);
893
+ border-radius: var(--radius-large);
894
+ text-align: center;
895
+ font-size: 13px;
896
+ color: var(--text-secondary);
897
+ }
898
+
899
+ @media (max-width: 1200px) {
900
+ .filters {
901
+ grid-template-columns: repeat(2, minmax(0, 1fr));
902
+ }
903
+
904
+ .search-box {
905
+ grid-column: 1 / -1;
906
+ }
907
+
908
+ .tag-filter-row {
909
+ grid-column: 1 / -1;
910
+ }
911
+
912
+ .filter-actions {
913
+ justify-content: flex-start;
914
+ }
915
+
916
+ .selection-toolbar {
917
+ flex-direction: column;
918
+ align-items: flex-start;
919
+ }
920
+
921
+ .task-body {
922
+ grid-template-columns: 1fr;
923
+ }
924
+ }
925
+
926
+ @media (max-width: 860px) {
927
+ .app-shell {
928
+ padding: 24px;
929
+ }
930
+
931
+ .app-header {
932
+ flex-direction: column;
933
+ align-items: flex-start;
934
+ gap: 16px;
935
+ }
936
+
937
+ .filters {
938
+ grid-template-columns: 1fr;
939
+ }
940
+
941
+ .status-dock {
942
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
943
+ }
944
+
945
+ .selection-actions {
946
+ align-items: flex-start;
947
+ }
948
+
949
+ .parent-view-controls {
950
+ flex-direction: column;
951
+ align-items: stretch;
952
+ gap: 10px;
953
+ }
954
+
955
+ .parent-view-controls input {
956
+ width: 100%;
957
+ }
958
+ }
959
+
960
+
961
+
962
+
963
+
964
+
965
+
966
+
967
+