Okoge-keys commited on
Commit
bebebd9
·
verified ·
1 Parent(s): 69acf30

Upload 3 files

Browse files
Files changed (3) hide show
  1. kanban/index.html +173 -0
  2. kanban/script.js +1006 -0
  3. kanban/styles.css +939 -0
kanban/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/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/styles.css ADDED
@@ -0,0 +1,939 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
2
+
3
+ :root {
4
+ --bg: #f3f4f8;
5
+ --card-bg: rgba(255, 255, 255, 0.78);
6
+ --card-border: rgba(93, 107, 130, 0.12);
7
+ --accent: #007aff;
8
+ --accent-soft: rgba(0, 122, 255, 0.12);
9
+ --text-primary: #0f172a;
10
+ --text-secondary: #475569;
11
+ --shadow-soft: 0 24px 40px rgba(15, 23, 42, 0.12);
12
+ --radius-large: 24px;
13
+ --radius-medium: 16px;
14
+ --radius-small: 12px;
15
+ }
16
+
17
+ *,
18
+ *::before,
19
+ *::after {
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ body {
24
+ margin: 0;
25
+ font-family: "Inter", "Hiragino Sans", "Noto Sans JP", sans-serif;
26
+ background: linear-gradient(135deg, #e3ebff 0%, #f8f9fb 35%, #eef5ff 100%);
27
+ color: var(--text-primary);
28
+ min-height: 100vh;
29
+ }
30
+
31
+ .app-shell {
32
+ max-width: 1600px;
33
+ margin: 0 auto;
34
+ padding: 32px 40px 80px;
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 24px;
38
+ }
39
+
40
+ .app-header {
41
+ display: flex;
42
+ justify-content: space-between;
43
+ align-items: center;
44
+ padding: 24px 32px;
45
+ background: var(--card-bg);
46
+ backdrop-filter: blur(18px);
47
+ border: 1px solid var(--card-border);
48
+ border-radius: var(--radius-large);
49
+ box-shadow: var(--shadow-soft);
50
+ }
51
+
52
+ .brand {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 18px;
56
+ }
57
+
58
+ .brand-mark {
59
+ display: grid;
60
+ place-items: center;
61
+ width: 54px;
62
+ height: 54px;
63
+ border-radius: 17px;
64
+ background: linear-gradient(135deg, rgba(0, 122, 255, 0.16), rgba(0, 122, 255, 0.36));
65
+ color: var(--accent);
66
+ font-size: 20px;
67
+ font-weight: 600;
68
+ }
69
+
70
+ .brand h1 {
71
+ margin: 0;
72
+ font-size: 20px;
73
+ font-weight: 600;
74
+ letter-spacing: 0.2em;
75
+ }
76
+
77
+ .subtitle {
78
+ margin: 4px 0 0;
79
+ font-size: 12px;
80
+ color: var(--text-secondary);
81
+ }
82
+
83
+ .primary-controls {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 12px;
87
+ }
88
+
89
+ .filters {
90
+ display: grid;
91
+ grid-template-columns: minmax(0, 2.8fr) repeat(2, minmax(0, 1fr));
92
+ gap: 16px;
93
+ align-items: end;
94
+ background: var(--card-bg);
95
+ border: 1px solid var(--card-border);
96
+ border-radius: var(--radius-large);
97
+ padding: 24px 28px;
98
+ box-shadow: var(--shadow-soft);
99
+ }
100
+
101
+ .filters label {
102
+ display: block;
103
+ font-size: 12px;
104
+ font-weight: 600;
105
+ color: var(--text-secondary);
106
+ margin-bottom: 6px;
107
+ }
108
+
109
+ .filters input,
110
+ .filters select {
111
+ width: 100%;
112
+ min-width: 0;
113
+ border: 1px solid rgba(148, 163, 184, 0.35);
114
+ border-radius: var(--radius-small);
115
+ padding: 10px 14px;
116
+ background: rgba(255, 255, 255, 0.88);
117
+ font-size: 14px;
118
+ transition: border 0.2s ease, box-shadow 0.2s ease;
119
+ }
120
+
121
+ .filters input:focus,
122
+ .filters select:focus {
123
+ outline: none;
124
+ border-color: var(--accent);
125
+ box-shadow: 0 0 0 4px var(--accent-soft);
126
+ }
127
+
128
+ .date-inputs {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 10px;
132
+ }
133
+
134
+ .date-inputs input {
135
+ flex: 1 1 0;
136
+ min-width: 0;
137
+ }
138
+
139
+ .date-inputs span {
140
+ white-space: nowrap;
141
+ font-size: 12px;
142
+ color: var(--text-secondary);
143
+ }
144
+
145
+ .search-box {
146
+ grid-column: 1 / span 2;
147
+ }
148
+
149
+ .parent-filter {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 10px;
153
+ }
154
+
155
+ .parent-filter input {
156
+ border-radius: var(--radius-small);
157
+ }
158
+
159
+ .tag-filter-row {
160
+ grid-column: 1 / -1;
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 12px;
164
+ flex-wrap: wrap;
165
+ padding-top: 4px;
166
+ }
167
+
168
+ .filter-actions {
169
+ grid-column: 1 / -1;
170
+ display: flex;
171
+ justify-content: flex-end;
172
+ margin-top: 8px;
173
+ }
174
+
175
+ .tag-filter-label {
176
+ font-size: 12px;
177
+ font-weight: 600;
178
+ color: var(--text-secondary);
179
+ }
180
+
181
+ .tag-toggle-container {
182
+ display: flex;
183
+ flex-wrap: wrap;
184
+ gap: 8px;
185
+ align-items: center;
186
+ flex: 1;
187
+ }
188
+
189
+ .tag-toggle {
190
+ border: 1px solid rgba(148, 163, 184, 0.45);
191
+ border-radius: 999px;
192
+ background: rgba(255, 255, 255, 0.9);
193
+ font-size: 12px;
194
+ font-weight: 500;
195
+ font-family: inherit;
196
+ padding: 6px 12px;
197
+ color: var(--text-secondary);
198
+ cursor: pointer;
199
+ transition: background 0.2s ease, color 0.2s ease, border 0.2s ease, box-shadow 0.2s ease;
200
+ }
201
+
202
+ .tag-toggle:hover {
203
+ border-color: var(--accent);
204
+ box-shadow: 0 0 0 3px var(--accent-soft);
205
+ }
206
+
207
+ .tag-toggle.active {
208
+ background: linear-gradient(135deg, #007aff, #0a84ff);
209
+ color: #f8fafc;
210
+ border-color: transparent;
211
+ box-shadow: 0 14px 26px rgba(10, 132, 255, 0.28);
212
+ }
213
+
214
+ .tag-toggle-empty {
215
+ font-size: 12px;
216
+ color: var(--text-secondary);
217
+ }
218
+
219
+ .selection-toolbar {
220
+ display: none;
221
+ justify-content: space-between;
222
+ align-items: center;
223
+ gap: 16px;
224
+ flex-wrap: wrap;
225
+ background: var(--card-bg);
226
+ border: 1px solid var(--card-border);
227
+ border-radius: var(--radius-large);
228
+ padding: 18px 24px;
229
+ box-shadow: var(--shadow-soft);
230
+ opacity: 0;
231
+ pointer-events: none;
232
+ transition: opacity 0.2s ease;
233
+ }
234
+
235
+ .selection-toolbar.active {
236
+ display: flex;
237
+ opacity: 1;
238
+ pointer-events: all;
239
+ }
240
+ body.parent-view-mode .status-dock,
241
+ body.parent-view-mode .board,
242
+ body.parent-view-mode .selection-toolbar {
243
+ display: none !important;
244
+ }
245
+
246
+ body.parent-view-mode .parent-view {
247
+ display: flex;
248
+ }
249
+
250
+
251
+ .selection-summary {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 12px;
255
+ flex-wrap: wrap;
256
+ font-size: 13px;
257
+ color: var(--text-secondary);
258
+ }
259
+
260
+ .selection-actions {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 16px;
264
+ flex-wrap: wrap;
265
+ }
266
+
267
+ .selection-status {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 10px;
271
+ flex-wrap: wrap;
272
+ font-size: 12px;
273
+ color: var(--text-secondary);
274
+ }
275
+
276
+ .status-button-group {
277
+ display: flex;
278
+ gap: 8px;
279
+ flex-wrap: wrap;
280
+ }
281
+
282
+ .status-dock {
283
+ display: grid;
284
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
285
+ gap: 12px;
286
+ padding: 12px 16px;
287
+ background: transparent;
288
+ }
289
+
290
+ .status-chip {
291
+ position: relative;
292
+ padding: 14px 20px;
293
+ border-radius: var(--radius-medium);
294
+ background: rgba(255, 255, 255, 0.72);
295
+ border: 1px solid transparent;
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: space-between;
299
+ gap: 16px;
300
+ font-size: 14px;
301
+ font-weight: 600;
302
+ letter-spacing: 0.04em;
303
+ font-family: inherit;
304
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border 0.2s ease, background 0.2s ease;
305
+ cursor: pointer;
306
+ outline: none;
307
+ color: var(--text-primary);
308
+ }
309
+
310
+ .status-chip:hover {
311
+ transform: translateY(-1px);
312
+ box-shadow: 0 12px 20px rgba(15, 23, 42, 0.12);
313
+ }
314
+
315
+ .status-chip:focus-visible {
316
+ border-color: var(--accent);
317
+ box-shadow: 0 0 0 4px var(--accent-soft);
318
+ }
319
+
320
+ .status-chip.active {
321
+ background: linear-gradient(135deg, rgba(0, 122, 255, 0.2), rgba(10, 132, 255, 0.36));
322
+ border-color: rgba(0, 122, 255, 0.25);
323
+ box-shadow: 0 16px 24px rgba(10, 132, 255, 0.18);
324
+ }
325
+
326
+ .status-chip-text {
327
+ display: flex;
328
+ flex-direction: column;
329
+ gap: 4px;
330
+ text-align: left;
331
+ }
332
+
333
+ .status-chip-text strong {
334
+ font-weight: 600;
335
+ font-size: 14px;
336
+ }
337
+
338
+ .status-chip-text span {
339
+ font-size: 12px;
340
+ font-weight: 500;
341
+ color: var(--text-secondary);
342
+ }
343
+
344
+ .status-chip.active .status-chip-text span {
345
+ color: rgba(15, 23, 42, 0.7);
346
+ }
347
+
348
+ .status-chip-count {
349
+ font-size: 13px;
350
+ font-weight: 600;
351
+ padding: 6px 12px;
352
+ border-radius: 999px;
353
+ background: rgba(148, 163, 184, 0.18);
354
+ color: var(--text-secondary);
355
+ }
356
+
357
+ .status-chip.active .status-chip-count {
358
+ background: rgba(255, 255, 255, 0.4);
359
+ color: var(--accent);
360
+ }
361
+
362
+ .board {
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 18px;
366
+ }
367
+
368
+ .column {
369
+ display: flex;
370
+ flex-direction: column;
371
+ gap: 18px;
372
+ min-height: 420px;
373
+ background: var(--card-bg);
374
+ border: 1px solid var(--card-border);
375
+ border-radius: var(--radius-large);
376
+ padding: 24px 20px;
377
+ box-shadow: var(--shadow-soft);
378
+ }
379
+
380
+ .column-header {
381
+ display: flex;
382
+ align-items: center;
383
+ justify-content: space-between;
384
+ font-weight: 600;
385
+ letter-spacing: 0.08em;
386
+ text-transform: uppercase;
387
+ font-size: 14px;
388
+ color: var(--text-secondary);
389
+ }
390
+
391
+ .column-count {
392
+ background: rgba(148, 163, 184, 0.16);
393
+ padding: 6px 12px;
394
+ border-radius: 999px;
395
+ font-size: 12px;
396
+ }
397
+
398
+ .column-body {
399
+ display: flex;
400
+ flex-direction: column;
401
+ gap: 16px;
402
+ flex: 1;
403
+ }
404
+
405
+ .column-body.empty::before {
406
+ content: "\u3053\u306E\u30B9\u30C6\u30FC\u30BF\u30B9\u306E\u30BF\u30B9\u30AF\u306F\u3042\u308A\u307E\u305B\u3093";
407
+ display: grid;
408
+ place-items: center;
409
+ border: 1px dashed rgba(15, 23, 42, 0.12);
410
+ border-radius: var(--radius-medium);
411
+ padding: 40px;
412
+ color: rgba(15, 23, 42, 0.45);
413
+ font-size: 13px;
414
+ letter-spacing: 0.05em;
415
+ }
416
+
417
+ .task-card {
418
+ background: rgba(255, 255, 255, 0.82);
419
+ border-radius: var(--radius-large);
420
+ border: 1px solid rgba(226, 232, 240, 0.9);
421
+ padding: 18px 22px;
422
+ box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
423
+ display: flex;
424
+ flex-direction: column;
425
+ gap: 12px;
426
+ min-width: 100%;
427
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
428
+ }
429
+
430
+ .task-card.highlighted {
431
+ box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.32);
432
+ }
433
+
434
+ .task-card-header {
435
+ display: flex;
436
+ align-items: flex-start;
437
+ justify-content: space-between;
438
+ gap: 12px;
439
+ }
440
+
441
+ .title-row {
442
+ display: flex;
443
+ align-items: flex-start;
444
+ gap: 12px;
445
+ }
446
+
447
+ .task-title {
448
+ margin: 0;
449
+ font-size: 18px;
450
+ font-weight: 600;
451
+ color: var(--text-primary);
452
+ }
453
+
454
+ .task-select {
455
+ width: 18px;
456
+ height: 18px;
457
+ margin-top: 3px;
458
+ display: none;
459
+ }
460
+
461
+ body.selection-mode .task-select {
462
+ display: block;
463
+ }
464
+
465
+ body.selection-mode .task-card {
466
+ padding-top: 26px;
467
+ }
468
+
469
+ .status-quick-buttons {
470
+ display: flex;
471
+ gap: 6px;
472
+ }
473
+
474
+ .status-quick-buttons button {
475
+ border: none;
476
+ padding: 6px 10px;
477
+ border-radius: var(--radius-small);
478
+ font-size: 11px;
479
+ font-weight: 600;
480
+ letter-spacing: 0.05em;
481
+ background: rgba(148, 163, 184, 0.16);
482
+ color: var(--text-secondary);
483
+ cursor: pointer;
484
+ transition: background 0.2s ease, color 0.2s ease;
485
+ }
486
+
487
+ .status-quick-buttons button:hover {
488
+ background: var(--accent);
489
+ color: white;
490
+ }
491
+
492
+ .task-meta {
493
+ display: flex;
494
+ gap: 12px;
495
+ font-size: 12px;
496
+ color: var(--text-secondary);
497
+ letter-spacing: 0.04em;
498
+ }
499
+
500
+ .task-body {
501
+ display: grid;
502
+ grid-template-columns: repeat(2, minmax(0, 1fr));
503
+ gap: 12px;
504
+ }
505
+
506
+ .task-body > div {
507
+ padding: 12px;
508
+ border-radius: var(--radius-small);
509
+ background: rgba(15, 23, 42, 0.03);
510
+ font-size: 13px;
511
+ line-height: 1.6;
512
+ min-height: 64px;
513
+ }
514
+
515
+ .task-body .resource-link {
516
+ grid-column: span 2;
517
+ color: var(--accent);
518
+ font-size: 13px;
519
+ text-decoration: none;
520
+ word-break: break-all;
521
+ }
522
+
523
+ .task-tags {
524
+ display: flex;
525
+ gap: 8px;
526
+ flex-wrap: wrap;
527
+ }
528
+
529
+ .task-tag {
530
+ padding: 4px 12px;
531
+ border-radius: 999px;
532
+ font-size: 11px;
533
+ font-weight: 600;
534
+ letter-spacing: 0.08em;
535
+ background: rgba(0, 122, 255, 0.14);
536
+ color: var(--accent);
537
+ }
538
+
539
+ .task-children {
540
+ border-left: 2px solid rgba(0, 122, 255, 0.16);
541
+ padding-left: 12px;
542
+ display: flex;
543
+ flex-direction: column;
544
+ gap: 6px;
545
+ }
546
+
547
+ .task-child {
548
+ font-size: 12px;
549
+ color: var(--text-secondary);
550
+ display: flex;
551
+ justify-content: space-between;
552
+ gap: 8px;
553
+ }
554
+
555
+ .task-footer {
556
+ display: flex;
557
+ gap: 8px;
558
+ }
559
+
560
+ .btn {
561
+ border: none;
562
+ border-radius: 999px;
563
+ padding: 10px 18px;
564
+ font-size: 13px;
565
+ font-weight: 600;
566
+ letter-spacing: 0.08em;
567
+ cursor: pointer;
568
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
569
+ }
570
+
571
+ .btn.primary {
572
+ background: linear-gradient(135deg, #007aff, #0a84ff);
573
+ color: #f8fafc;
574
+ box-shadow: 0 14px 24px rgba(10, 132, 255, 0.3);
575
+ }
576
+
577
+ .btn.primary:hover {
578
+ transform: translateY(-1px);
579
+ box-shadow: 0 18px 32px rgba(10, 132, 255, 0.4);
580
+ }
581
+
582
+ .btn.ghost {
583
+ background: rgba(15, 23, 42, 0.05);
584
+ color: var(--text-primary);
585
+ }
586
+
587
+ .btn.ghost:hover {
588
+ background: rgba(15, 23, 42, 0.1);
589
+ }
590
+
591
+ .btn.subtle {
592
+ background: rgba(148, 163, 184, 0.18);
593
+ color: var(--text-secondary);
594
+ }
595
+
596
+ .btn.subtle:hover {
597
+ background: rgba(148, 163, 184, 0.28);
598
+ }
599
+
600
+ .btn.tiny {
601
+ padding: 6px 16px;
602
+ font-size: 11px;
603
+ border-radius: var(--radius-small);
604
+ }
605
+
606
+ .btn.small {
607
+ padding: 8px 16px;
608
+ font-size: 12px;
609
+ }
610
+
611
+ .btn.danger {
612
+ background: rgba(255, 59, 48, 0.16);
613
+ color: #ff3b30;
614
+ }
615
+
616
+ .btn.danger:hover {
617
+ background: rgba(255, 59, 48, 0.24);
618
+ }
619
+
620
+ .icon-button {
621
+ border: none;
622
+ background: transparent;
623
+ font-size: 18px;
624
+ cursor: pointer;
625
+ color: var(--text-secondary);
626
+ }
627
+
628
+ .task-dialog {
629
+ border: none;
630
+ border-radius: 32px;
631
+ padding: 0;
632
+ width: min(640px, 94vw);
633
+ background: rgba(255, 255, 255, 0.94);
634
+ box-shadow: 0 40px 80px rgba(15, 23, 42, 0.24);
635
+ }
636
+
637
+ .task-dialog::backdrop {
638
+ backdrop-filter: blur(12px);
639
+ background: rgba(15, 23, 42, 0.24);
640
+ }
641
+
642
+ .task-dialog form {
643
+ display: flex;
644
+ flex-direction: column;
645
+ gap: 0;
646
+ }
647
+
648
+ .task-dialog header,
649
+ .task-dialog footer {
650
+ padding: 24px 32px;
651
+ display: flex;
652
+ align-items: center;
653
+ justify-content: space-between;
654
+ }
655
+
656
+ .task-dialog header {
657
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
658
+ }
659
+
660
+ .task-dialog footer {
661
+ border-top: 1px solid rgba(148, 163, 184, 0.18);
662
+ }
663
+
664
+ .task-dialog .form-body {
665
+ padding: 24px 32px 32px;
666
+ max-height: 70vh;
667
+ overflow-y: auto;
668
+ display: grid;
669
+ gap: 18px;
670
+ }
671
+
672
+ .field {
673
+ display: flex;
674
+ flex-direction: column;
675
+ gap: 8px;
676
+ }
677
+
678
+ .field input,
679
+ .field select,
680
+ .field textarea {
681
+ border: 1px solid rgba(148, 163, 184, 0.35);
682
+ border-radius: var(--radius-medium);
683
+ padding: 12px 16px;
684
+ background: rgba(255, 255, 255, 0.88);
685
+ font-size: 14px;
686
+ transition: border 0.2s ease, box-shadow 0.2s ease;
687
+ }
688
+
689
+ .field textarea {
690
+ resize: vertical;
691
+ }
692
+
693
+ .field input:focus,
694
+ .field select:focus,
695
+ .field textarea:focus {
696
+ outline: none;
697
+ border-color: var(--accent);
698
+ box-shadow: 0 0 0 4px var(--accent-soft);
699
+ }
700
+
701
+ .field-grid {
702
+ display: grid;
703
+ grid-template-columns: repeat(2, minmax(0, 1fr));
704
+ gap: 16px;
705
+ }
706
+
707
+ .parent-view {
708
+ background: var(--card-bg);
709
+ border-radius: var(--radius-large);
710
+ border: 1px solid var(--card-border);
711
+ box-shadow: var(--shadow-soft);
712
+ padding: 24px 28px 32px;
713
+ display: none;
714
+ flex-direction: column;
715
+ gap: 16px;
716
+ }
717
+
718
+ .parent-view header {
719
+ display: flex;
720
+ flex-direction: column;
721
+ gap: 12px;
722
+ }
723
+
724
+ .parent-view-meta {
725
+ display: flex;
726
+ align-items: flex-start;
727
+ justify-content: space-between;
728
+ gap: 16px;
729
+ flex-wrap: wrap;
730
+ }
731
+
732
+ .parent-view-controls {
733
+ display: flex;
734
+ align-items: center;
735
+ gap: 12px;
736
+ flex-wrap: wrap;
737
+ }
738
+
739
+ .parent-view-controls input {
740
+ min-width: 220px;
741
+ }
742
+
743
+ .parent-view.active {
744
+ display: flex;
745
+ }
746
+
747
+ .parent-clusters {
748
+ display: grid;
749
+ grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
750
+ gap: 16px;
751
+ }
752
+
753
+ .parent-cluster {
754
+ padding: 20px 24px;
755
+ border-radius: var(--radius-large);
756
+ background: rgba(255, 255, 255, 0.82);
757
+ border: 1px solid rgba(226, 232, 240, 0.9);
758
+ box-shadow: 0 14px 32px rgba(15, 23, 42, 0.1);
759
+ display: flex;
760
+ flex-direction: column;
761
+ gap: 0;
762
+ transition: transform 0.2s ease, box-shadow 0.2s ease, border 0.2s ease;
763
+ }
764
+
765
+ .parent-cluster:hover {
766
+ transform: translateY(-2px);
767
+ box-shadow: 0 18px 36px rgba(15, 23, 42, 0.14);
768
+ }
769
+
770
+ .parent-cluster.highlighted {
771
+ box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.32);
772
+ border-color: rgba(0, 122, 255, 0.24);
773
+ }
774
+
775
+ .cluster-header {
776
+ display: flex;
777
+ flex-direction: column;
778
+ gap: 12px;
779
+ cursor: pointer;
780
+ }
781
+
782
+ .cluster-title {
783
+ display: flex;
784
+ flex-direction: column;
785
+ gap: 4px;
786
+ }
787
+
788
+ .cluster-title h4 {
789
+ margin: 0;
790
+ font-size: 16px;
791
+ color: var(--text-primary);
792
+ }
793
+
794
+ .cluster-meta {
795
+ margin-top: 4px;
796
+ font-size: 12px;
797
+ color: var(--text-secondary);
798
+ }
799
+
800
+ .cluster-status {
801
+ display: flex;
802
+ flex-wrap: wrap;
803
+ gap: 8px;
804
+ }
805
+
806
+ .status-pill {
807
+ padding: 4px 10px;
808
+ border-radius: 999px;
809
+ background: rgba(148, 163, 184, 0.16);
810
+ color: var(--text-secondary);
811
+ font-size: 11px;
812
+ font-weight: 600;
813
+ }
814
+
815
+ .parent-cluster.highlighted .status-pill {
816
+ background: rgba(0, 122, 255, 0.16);
817
+ color: var(--accent);
818
+ }
819
+
820
+ .cluster-actions {
821
+ display: flex;
822
+ justify-content: flex-end;
823
+ align-items: center;
824
+ gap: 8px;
825
+ flex-wrap: wrap;
826
+ }
827
+
828
+ .cluster-body {
829
+ display: flex;
830
+ flex-direction: column;
831
+ gap: 8px;
832
+ margin-top: 14px;
833
+ border-top: 1px solid rgba(148, 163, 184, 0.18);
834
+ padding-top: 12px;
835
+ }
836
+
837
+ .cluster-child-row {
838
+ display: flex;
839
+ justify-content: space-between;
840
+ gap: 12px;
841
+ align-items: baseline;
842
+ font-size: 12px;
843
+ color: var(--text-secondary);
844
+ }
845
+
846
+ .cluster-child-row .child-title {
847
+ font-weight: 600;
848
+ color: var(--text-primary);
849
+ }
850
+
851
+ .cluster-child-row .child-status {
852
+ font-size: 12px;
853
+ color: var(--text-secondary);
854
+ }
855
+
856
+ .cluster-child-empty {
857
+ font-size: 12px;
858
+ color: var(--text-secondary);
859
+ }
860
+
861
+ .parent-empty {
862
+ grid-column: 1 / -1;
863
+ padding: 28px;
864
+ border: 1px dashed rgba(148, 163, 184, 0.4);
865
+ border-radius: var(--radius-large);
866
+ text-align: center;
867
+ font-size: 13px;
868
+ color: var(--text-secondary);
869
+ }
870
+
871
+ @media (max-width: 1200px) {
872
+ .filters {
873
+ grid-template-columns: repeat(2, minmax(0, 1fr));
874
+ }
875
+
876
+ .search-box {
877
+ grid-column: 1 / -1;
878
+ }
879
+
880
+ .tag-filter-row {
881
+ grid-column: 1 / -1;
882
+ }
883
+
884
+ .filter-actions {
885
+ justify-content: flex-start;
886
+ }
887
+
888
+ .selection-toolbar {
889
+ flex-direction: column;
890
+ align-items: flex-start;
891
+ }
892
+
893
+ .task-body {
894
+ grid-template-columns: 1fr;
895
+ }
896
+ }
897
+
898
+ @media (max-width: 860px) {
899
+ .app-shell {
900
+ padding: 24px;
901
+ }
902
+
903
+ .app-header {
904
+ flex-direction: column;
905
+ align-items: flex-start;
906
+ gap: 16px;
907
+ }
908
+
909
+ .filters {
910
+ grid-template-columns: 1fr;
911
+ }
912
+
913
+ .status-dock {
914
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
915
+ }
916
+
917
+ .selection-actions {
918
+ align-items: flex-start;
919
+ }
920
+
921
+ .parent-view-controls {
922
+ flex-direction: column;
923
+ align-items: stretch;
924
+ gap: 10px;
925
+ }
926
+
927
+ .parent-view-controls input {
928
+ width: 100%;
929
+ }
930
+ }
931
+
932
+
933
+
934
+
935
+
936
+
937
+
938
+
939
+