@leonarto/spec-embryo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -0
- package/package.json +48 -0
- package/src/backends/base.ts +18 -0
- package/src/backends/deterministic.ts +105 -0
- package/src/backends/index.ts +26 -0
- package/src/backends/prompt.ts +169 -0
- package/src/backends/subprocess.ts +198 -0
- package/src/cli.ts +111 -0
- package/src/commands/agents.ts +16 -0
- package/src/commands/current.ts +95 -0
- package/src/commands/doctor.ts +12 -0
- package/src/commands/handoff.ts +64 -0
- package/src/commands/init.ts +101 -0
- package/src/commands/reshape.ts +20 -0
- package/src/commands/resume.ts +19 -0
- package/src/commands/spec.ts +108 -0
- package/src/commands/status.ts +98 -0
- package/src/commands/task.ts +190 -0
- package/src/commands/ui.ts +35 -0
- package/src/domain.ts +357 -0
- package/src/engine.ts +290 -0
- package/src/frontmatter.ts +83 -0
- package/src/index.ts +75 -0
- package/src/paths.ts +32 -0
- package/src/repository.ts +807 -0
- package/src/services/adoption.ts +169 -0
- package/src/services/agents.ts +191 -0
- package/src/services/dashboard.ts +776 -0
- package/src/services/details.ts +453 -0
- package/src/services/doctor.ts +452 -0
- package/src/services/layout.ts +420 -0
- package/src/services/spec-answer-evaluation.ts +103 -0
- package/src/services/spec-import.ts +217 -0
- package/src/services/spec-questions.ts +343 -0
- package/src/services/ui.ts +34 -0
- package/src/storage.ts +57 -0
- package/src/templates.ts +270 -0
- package/tsconfig.json +17 -0
- package/web/package.json +24 -0
- package/web/src/app.css +83 -0
- package/web/src/app.d.ts +6 -0
- package/web/src/app.html +11 -0
- package/web/src/lib/components/AnalysisFilters.svelte +293 -0
- package/web/src/lib/components/DocumentBody.svelte +100 -0
- package/web/src/lib/components/MultiSelectDropdown.svelte +280 -0
- package/web/src/lib/components/SelectDropdown.svelte +265 -0
- package/web/src/lib/server/project-root.ts +34 -0
- package/web/src/lib/task-board.ts +20 -0
- package/web/src/routes/+layout.server.ts +57 -0
- package/web/src/routes/+layout.svelte +421 -0
- package/web/src/routes/+layout.ts +1 -0
- package/web/src/routes/+page.svelte +530 -0
- package/web/src/routes/specs/+page.svelte +416 -0
- package/web/src/routes/specs/[specId]/+page.server.ts +81 -0
- package/web/src/routes/specs/[specId]/+page.svelte +675 -0
- package/web/src/routes/tasks/+page.svelte +341 -0
- package/web/src/routes/tasks/[taskId]/+page.server.ts +12 -0
- package/web/src/routes/tasks/[taskId]/+page.svelte +431 -0
- package/web/src/routes/timeline/+page.svelte +1093 -0
- package/web/svelte.config.js +10 -0
- package/web/tsconfig.json +9 -0
- package/web/vite.config.ts +11 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DocumentBodySection } from "../../../../src/services/details.ts";
|
|
3
|
+
|
|
4
|
+
let { sections, emptyLabel = "No narrative body yet." } = $props<{
|
|
5
|
+
sections: DocumentBodySection[];
|
|
6
|
+
emptyLabel?: string;
|
|
7
|
+
}>();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#if sections.length === 0}
|
|
11
|
+
<p class="empty">{emptyLabel}</p>
|
|
12
|
+
{:else}
|
|
13
|
+
<div class="body-sections">
|
|
14
|
+
{#each sections as section}
|
|
15
|
+
<section class="body-section">
|
|
16
|
+
<div class="section-head">
|
|
17
|
+
<span class="section-rule"></span>
|
|
18
|
+
<h3>{section.heading}</h3>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="section-content">
|
|
22
|
+
{#each section.blocks as block}
|
|
23
|
+
{#if block.kind === "paragraph"}
|
|
24
|
+
{#each block.lines as line}
|
|
25
|
+
<p>{line}</p>
|
|
26
|
+
{/each}
|
|
27
|
+
{:else if block.kind === "list"}
|
|
28
|
+
<ul>
|
|
29
|
+
{#each block.lines as line}
|
|
30
|
+
<li>{line}</li>
|
|
31
|
+
{/each}
|
|
32
|
+
</ul>
|
|
33
|
+
{/if}
|
|
34
|
+
{/each}
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
{/each}
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.empty,
|
|
43
|
+
p,
|
|
44
|
+
li {
|
|
45
|
+
color: var(--muted);
|
|
46
|
+
line-height: 1.6;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.body-sections {
|
|
50
|
+
display: grid;
|
|
51
|
+
gap: 1rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.body-section {
|
|
55
|
+
display: grid;
|
|
56
|
+
gap: 0.72rem;
|
|
57
|
+
padding: 1rem;
|
|
58
|
+
border-radius: var(--radius-lg);
|
|
59
|
+
border: 1px solid var(--line);
|
|
60
|
+
background: rgba(255, 255, 255, 0.76);
|
|
61
|
+
box-shadow: var(--shadow-soft);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.section-head {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.75rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.section-rule {
|
|
71
|
+
width: 1.65rem;
|
|
72
|
+
height: 0.18rem;
|
|
73
|
+
border-radius: 999px;
|
|
74
|
+
background: linear-gradient(90deg, rgba(15, 141, 96, 0.9), rgba(15, 141, 96, 0.2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
h3,
|
|
78
|
+
p,
|
|
79
|
+
ul {
|
|
80
|
+
margin: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
h3 {
|
|
84
|
+
font-family: var(--display-font);
|
|
85
|
+
font-size: 1.08rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.section-content {
|
|
89
|
+
display: grid;
|
|
90
|
+
gap: 0.7rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ul {
|
|
94
|
+
padding-left: 1.1rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
li + li {
|
|
98
|
+
margin-top: 0.32rem;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Check, ChevronDown, ListFilter } from "@lucide/svelte";
|
|
3
|
+
|
|
4
|
+
export interface MultiSelectOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
name,
|
|
12
|
+
options,
|
|
13
|
+
selectedValues = [],
|
|
14
|
+
buttonLabel = "Select items",
|
|
15
|
+
emptyLabel = "No filters applied",
|
|
16
|
+
} = $props<{
|
|
17
|
+
name: string;
|
|
18
|
+
options: readonly MultiSelectOption[];
|
|
19
|
+
selectedValues?: string[];
|
|
20
|
+
buttonLabel?: string;
|
|
21
|
+
emptyLabel?: string;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const selectedSet = $derived(new Set(selectedValues));
|
|
25
|
+
const selectedOptions = $derived(options.filter((option: MultiSelectOption) => selectedSet.has(option.value)));
|
|
26
|
+
const summaryText = $derived(
|
|
27
|
+
selectedOptions.length === 0
|
|
28
|
+
? emptyLabel
|
|
29
|
+
: selectedOptions.length === 1
|
|
30
|
+
? selectedOptions[0]?.label ?? emptyLabel
|
|
31
|
+
: `${selectedOptions.length} selected`,
|
|
32
|
+
);
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<details class="dropdown-shell">
|
|
36
|
+
<summary class="dropdown-trigger">
|
|
37
|
+
<div class="summary-copy">
|
|
38
|
+
<span class="summary-label">
|
|
39
|
+
<ListFilter size={14} strokeWidth={1.9} />
|
|
40
|
+
{buttonLabel}
|
|
41
|
+
</span>
|
|
42
|
+
<strong>{summaryText}</strong>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="summary-meta">
|
|
46
|
+
{#if selectedOptions.length > 0}
|
|
47
|
+
<span class="count-pill">{selectedOptions.length}</span>
|
|
48
|
+
{/if}
|
|
49
|
+
<span class="chevron">
|
|
50
|
+
<ChevronDown size={16} strokeWidth={1.9} />
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
</summary>
|
|
54
|
+
|
|
55
|
+
<div class="dropdown-panel">
|
|
56
|
+
<div class="option-list">
|
|
57
|
+
{#each options as option}
|
|
58
|
+
<label class:selected={selectedSet.has(option.value)}>
|
|
59
|
+
<input type="checkbox" name={name} value={option.value} checked={selectedSet.has(option.value)} />
|
|
60
|
+
<span class="checkbox-mark">
|
|
61
|
+
<Check size={12} strokeWidth={2.2} />
|
|
62
|
+
</span>
|
|
63
|
+
<span class="option-copy">
|
|
64
|
+
<strong>{option.label}</strong>
|
|
65
|
+
{#if option.description}
|
|
66
|
+
<small>{option.description}</small>
|
|
67
|
+
{/if}
|
|
68
|
+
</span>
|
|
69
|
+
</label>
|
|
70
|
+
{/each}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</details>
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.dropdown-shell {
|
|
77
|
+
position: relative;
|
|
78
|
+
display: grid;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.dropdown-trigger {
|
|
82
|
+
list-style: none;
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: space-between;
|
|
86
|
+
gap: 1rem;
|
|
87
|
+
min-width: 0;
|
|
88
|
+
padding: 0.78rem 0.88rem;
|
|
89
|
+
border-radius: 1rem;
|
|
90
|
+
border: 1px solid var(--line);
|
|
91
|
+
background: rgba(255, 255, 255, 0.86);
|
|
92
|
+
box-shadow: var(--shadow-soft);
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
transition:
|
|
95
|
+
border-color 120ms ease,
|
|
96
|
+
box-shadow 120ms ease,
|
|
97
|
+
background 120ms ease,
|
|
98
|
+
transform 120ms ease;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dropdown-trigger::-webkit-details-marker {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.dropdown-shell[open] .dropdown-trigger,
|
|
106
|
+
.dropdown-trigger:hover {
|
|
107
|
+
border-color: rgba(15, 141, 96, 0.2);
|
|
108
|
+
background: rgba(255, 255, 255, 0.94);
|
|
109
|
+
box-shadow:
|
|
110
|
+
0 0 0 1px rgba(255, 255, 255, 0.34) inset,
|
|
111
|
+
0 12px 30px rgba(24, 22, 18, 0.08);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.summary-copy,
|
|
115
|
+
.summary-label,
|
|
116
|
+
.summary-meta,
|
|
117
|
+
.option-copy {
|
|
118
|
+
display: flex;
|
|
119
|
+
gap: 0.42rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.summary-copy {
|
|
123
|
+
min-width: 0;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: 0.14rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.summary-label,
|
|
129
|
+
.summary-meta {
|
|
130
|
+
align-items: center;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.summary-label {
|
|
134
|
+
color: var(--muted-soft);
|
|
135
|
+
font-size: 0.78rem;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.summary-copy strong {
|
|
140
|
+
min-width: 0;
|
|
141
|
+
color: var(--ink);
|
|
142
|
+
font-size: 0.9rem;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
line-height: 1.2;
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
text-overflow: ellipsis;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.summary-meta {
|
|
151
|
+
flex-shrink: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.count-pill {
|
|
155
|
+
display: inline-flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
justify-content: center;
|
|
158
|
+
min-width: 1.55rem;
|
|
159
|
+
height: 1.55rem;
|
|
160
|
+
padding: 0 0.45rem;
|
|
161
|
+
border-radius: 999px;
|
|
162
|
+
border: 1px solid rgba(15, 141, 96, 0.18);
|
|
163
|
+
background: rgba(15, 141, 96, 0.1);
|
|
164
|
+
color: #0d6e4b;
|
|
165
|
+
box-shadow: 0 0 16px rgba(15, 141, 96, 0.12);
|
|
166
|
+
font-size: 0.72rem;
|
|
167
|
+
font-weight: 700;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.chevron {
|
|
171
|
+
color: var(--muted);
|
|
172
|
+
transition: transform 120ms ease;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.dropdown-shell[open] .chevron {
|
|
176
|
+
transform: rotate(180deg);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.dropdown-panel {
|
|
180
|
+
position: absolute;
|
|
181
|
+
top: calc(100% + 0.5rem);
|
|
182
|
+
left: 0;
|
|
183
|
+
right: 0;
|
|
184
|
+
z-index: 15;
|
|
185
|
+
padding: 0.5rem;
|
|
186
|
+
border-radius: 1.05rem;
|
|
187
|
+
border: 1px solid rgba(24, 22, 18, 0.12);
|
|
188
|
+
background: rgba(255, 252, 246, 0.98);
|
|
189
|
+
box-shadow:
|
|
190
|
+
0 0 0 1px rgba(255, 255, 255, 0.4) inset,
|
|
191
|
+
0 22px 50px rgba(24, 22, 18, 0.12);
|
|
192
|
+
backdrop-filter: blur(14px);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.option-list {
|
|
196
|
+
display: grid;
|
|
197
|
+
gap: 0.42rem;
|
|
198
|
+
max-height: 18rem;
|
|
199
|
+
overflow: auto;
|
|
200
|
+
padding-right: 0.1rem;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.option-list label {
|
|
204
|
+
display: grid;
|
|
205
|
+
grid-template-columns: auto auto minmax(0, 1fr);
|
|
206
|
+
align-items: start;
|
|
207
|
+
gap: 0.6rem;
|
|
208
|
+
padding: 0.68rem 0.72rem;
|
|
209
|
+
border-radius: 0.95rem;
|
|
210
|
+
border: 1px solid var(--line);
|
|
211
|
+
background: rgba(255, 255, 255, 0.82);
|
|
212
|
+
cursor: pointer;
|
|
213
|
+
transition:
|
|
214
|
+
border-color 120ms ease,
|
|
215
|
+
transform 120ms ease,
|
|
216
|
+
box-shadow 120ms ease,
|
|
217
|
+
background 120ms ease;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.option-list label:hover {
|
|
221
|
+
transform: translateY(-1px);
|
|
222
|
+
border-color: rgba(24, 22, 18, 0.16);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.option-list label.selected {
|
|
226
|
+
border-color: rgba(15, 141, 96, 0.26);
|
|
227
|
+
background: rgba(15, 141, 96, 0.08);
|
|
228
|
+
box-shadow:
|
|
229
|
+
0 0 0 1px rgba(255, 255, 255, 0.34) inset,
|
|
230
|
+
0 10px 24px rgba(15, 141, 96, 0.08);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.option-list input {
|
|
234
|
+
position: absolute;
|
|
235
|
+
opacity: 0;
|
|
236
|
+
pointer-events: none;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.checkbox-mark {
|
|
240
|
+
display: inline-flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
width: 1.05rem;
|
|
244
|
+
height: 1.05rem;
|
|
245
|
+
margin-top: 0.12rem;
|
|
246
|
+
border-radius: 0.32rem;
|
|
247
|
+
border: 1px solid var(--line-strong);
|
|
248
|
+
background: rgba(255, 255, 255, 0.94);
|
|
249
|
+
color: transparent;
|
|
250
|
+
transition:
|
|
251
|
+
background 120ms ease,
|
|
252
|
+
color 120ms ease,
|
|
253
|
+
border-color 120ms ease,
|
|
254
|
+
box-shadow 120ms ease;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.option-list label.selected .checkbox-mark {
|
|
258
|
+
border-color: rgba(15, 141, 96, 0.22);
|
|
259
|
+
background: rgba(15, 141, 96, 0.14);
|
|
260
|
+
color: #0d6e4b;
|
|
261
|
+
box-shadow: 0 0 14px rgba(15, 141, 96, 0.12);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.option-copy {
|
|
265
|
+
min-width: 0;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
gap: 0.12rem;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.option-copy strong {
|
|
271
|
+
font-size: 0.82rem;
|
|
272
|
+
font-weight: 700;
|
|
273
|
+
letter-spacing: 0.04em;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.option-copy small {
|
|
277
|
+
color: var(--muted);
|
|
278
|
+
line-height: 1.35;
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Check, ChevronDown, ListFilter } from "@lucide/svelte";
|
|
3
|
+
|
|
4
|
+
export interface SelectDropdownOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
name,
|
|
12
|
+
options,
|
|
13
|
+
selectedValue = "",
|
|
14
|
+
buttonLabel = "Choose item",
|
|
15
|
+
emptyLabel = "All items",
|
|
16
|
+
} = $props<{
|
|
17
|
+
name: string;
|
|
18
|
+
options: readonly SelectDropdownOption[];
|
|
19
|
+
selectedValue?: string;
|
|
20
|
+
buttonLabel?: string;
|
|
21
|
+
emptyLabel?: string;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const selectedOption = $derived(options.find((option: SelectDropdownOption) => option.value === selectedValue));
|
|
25
|
+
const summaryText = $derived(selectedOption?.label ?? emptyLabel);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<details class="dropdown-shell">
|
|
29
|
+
<summary class="dropdown-trigger">
|
|
30
|
+
<div class="summary-copy">
|
|
31
|
+
<span class="summary-label">
|
|
32
|
+
<ListFilter size={14} strokeWidth={1.9} />
|
|
33
|
+
{buttonLabel}
|
|
34
|
+
</span>
|
|
35
|
+
<strong>{summaryText}</strong>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<span class="summary-meta">
|
|
39
|
+
<span class="chevron">
|
|
40
|
+
<ChevronDown size={16} strokeWidth={1.9} />
|
|
41
|
+
</span>
|
|
42
|
+
</span>
|
|
43
|
+
</summary>
|
|
44
|
+
|
|
45
|
+
<div class="dropdown-panel">
|
|
46
|
+
<div class="option-list">
|
|
47
|
+
<label class:selected={selectedValue === ""}>
|
|
48
|
+
<input type="radio" name={name} value="" checked={selectedValue === ""} />
|
|
49
|
+
<span class="checkbox-mark">
|
|
50
|
+
<Check size={12} strokeWidth={2.2} />
|
|
51
|
+
</span>
|
|
52
|
+
<span class="option-copy">
|
|
53
|
+
<strong>{emptyLabel}</strong>
|
|
54
|
+
<small>Do not constrain this field.</small>
|
|
55
|
+
</span>
|
|
56
|
+
</label>
|
|
57
|
+
|
|
58
|
+
{#each options as option}
|
|
59
|
+
<label class:selected={selectedValue === option.value}>
|
|
60
|
+
<input type="radio" name={name} value={option.value} checked={selectedValue === option.value} />
|
|
61
|
+
<span class="checkbox-mark">
|
|
62
|
+
<Check size={12} strokeWidth={2.2} />
|
|
63
|
+
</span>
|
|
64
|
+
<span class="option-copy">
|
|
65
|
+
<strong>{option.label}</strong>
|
|
66
|
+
{#if option.description}
|
|
67
|
+
<small>{option.description}</small>
|
|
68
|
+
{/if}
|
|
69
|
+
</span>
|
|
70
|
+
</label>
|
|
71
|
+
{/each}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.dropdown-shell {
|
|
78
|
+
position: relative;
|
|
79
|
+
display: grid;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dropdown-trigger {
|
|
83
|
+
list-style: none;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: space-between;
|
|
87
|
+
gap: 1rem;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
padding: 0.72rem 0.84rem;
|
|
90
|
+
border-radius: 1rem;
|
|
91
|
+
border: 1px solid var(--line);
|
|
92
|
+
background: rgba(255, 255, 255, 0.86);
|
|
93
|
+
box-shadow: var(--shadow-soft);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
transition:
|
|
96
|
+
border-color 120ms ease,
|
|
97
|
+
box-shadow 120ms ease,
|
|
98
|
+
background 120ms ease,
|
|
99
|
+
transform 120ms ease;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.dropdown-trigger::-webkit-details-marker {
|
|
103
|
+
display: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.dropdown-shell[open] .dropdown-trigger,
|
|
107
|
+
.dropdown-trigger:hover {
|
|
108
|
+
border-color: rgba(15, 141, 96, 0.2);
|
|
109
|
+
background: rgba(255, 255, 255, 0.94);
|
|
110
|
+
box-shadow:
|
|
111
|
+
0 0 0 1px rgba(255, 255, 255, 0.34) inset,
|
|
112
|
+
0 12px 30px rgba(24, 22, 18, 0.08);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.summary-copy,
|
|
116
|
+
.summary-label,
|
|
117
|
+
.summary-meta,
|
|
118
|
+
.option-copy {
|
|
119
|
+
display: flex;
|
|
120
|
+
gap: 0.42rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.summary-copy {
|
|
124
|
+
min-width: 0;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
gap: 0.14rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.summary-label,
|
|
130
|
+
.summary-meta {
|
|
131
|
+
align-items: center;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.summary-label {
|
|
135
|
+
color: var(--muted-soft);
|
|
136
|
+
font-size: 0.78rem;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.summary-copy strong {
|
|
141
|
+
min-width: 0;
|
|
142
|
+
color: var(--ink);
|
|
143
|
+
font-size: 0.9rem;
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
line-height: 1.2;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
text-overflow: ellipsis;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.summary-meta {
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.chevron {
|
|
156
|
+
color: var(--muted);
|
|
157
|
+
transition: transform 120ms ease;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.dropdown-shell[open] .chevron {
|
|
161
|
+
transform: rotate(180deg);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.dropdown-panel {
|
|
165
|
+
position: absolute;
|
|
166
|
+
top: calc(100% + 0.5rem);
|
|
167
|
+
left: 0;
|
|
168
|
+
right: 0;
|
|
169
|
+
z-index: 15;
|
|
170
|
+
padding: 0.5rem;
|
|
171
|
+
border-radius: 1.05rem;
|
|
172
|
+
border: 1px solid rgba(24, 22, 18, 0.12);
|
|
173
|
+
background: rgba(255, 252, 246, 0.98);
|
|
174
|
+
box-shadow:
|
|
175
|
+
0 0 0 1px rgba(255, 255, 255, 0.4) inset,
|
|
176
|
+
0 22px 50px rgba(24, 22, 18, 0.12);
|
|
177
|
+
backdrop-filter: blur(14px);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.option-list {
|
|
181
|
+
display: grid;
|
|
182
|
+
gap: 0.42rem;
|
|
183
|
+
max-height: 18rem;
|
|
184
|
+
overflow: auto;
|
|
185
|
+
padding-right: 0.1rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.option-list label {
|
|
189
|
+
display: grid;
|
|
190
|
+
grid-template-columns: auto auto minmax(0, 1fr);
|
|
191
|
+
align-items: start;
|
|
192
|
+
gap: 0.6rem;
|
|
193
|
+
padding: 0.68rem 0.72rem;
|
|
194
|
+
border-radius: 0.95rem;
|
|
195
|
+
border: 1px solid var(--line);
|
|
196
|
+
background: rgba(255, 255, 255, 0.82);
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition:
|
|
199
|
+
border-color 120ms ease,
|
|
200
|
+
transform 120ms ease,
|
|
201
|
+
box-shadow 120ms ease,
|
|
202
|
+
background 120ms ease;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.option-list label:hover {
|
|
206
|
+
transform: translateY(-1px);
|
|
207
|
+
border-color: rgba(24, 22, 18, 0.16);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.option-list label.selected {
|
|
211
|
+
border-color: rgba(15, 141, 96, 0.26);
|
|
212
|
+
background: rgba(15, 141, 96, 0.08);
|
|
213
|
+
box-shadow:
|
|
214
|
+
0 0 0 1px rgba(255, 255, 255, 0.34) inset,
|
|
215
|
+
0 10px 24px rgba(15, 141, 96, 0.08);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.option-list input {
|
|
219
|
+
position: absolute;
|
|
220
|
+
opacity: 0;
|
|
221
|
+
pointer-events: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.checkbox-mark {
|
|
225
|
+
display: inline-flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
width: 1.05rem;
|
|
229
|
+
height: 1.05rem;
|
|
230
|
+
margin-top: 0.12rem;
|
|
231
|
+
border-radius: 999px;
|
|
232
|
+
border: 1px solid var(--line-strong);
|
|
233
|
+
background: rgba(255, 255, 255, 0.94);
|
|
234
|
+
color: transparent;
|
|
235
|
+
transition:
|
|
236
|
+
background 120ms ease,
|
|
237
|
+
color 120ms ease,
|
|
238
|
+
border-color 120ms ease,
|
|
239
|
+
box-shadow 120ms ease;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.option-list label.selected .checkbox-mark {
|
|
243
|
+
border-color: rgba(15, 141, 96, 0.22);
|
|
244
|
+
background: rgba(15, 141, 96, 0.14);
|
|
245
|
+
color: #0d6e4b;
|
|
246
|
+
box-shadow: 0 0 14px rgba(15, 141, 96, 0.12);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.option-copy {
|
|
250
|
+
min-width: 0;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: 0.12rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.option-copy strong {
|
|
256
|
+
font-size: 0.82rem;
|
|
257
|
+
font-weight: 700;
|
|
258
|
+
letter-spacing: 0.04em;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.option-copy small {
|
|
262
|
+
color: var(--muted);
|
|
263
|
+
line-height: 1.35;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
function hasProjectConfig(dirPath: string): boolean {
|
|
5
|
+
return existsSync(join(dirPath, ".specpm", "config.toml"));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveProjectRootFrom(startDir: string, configuredRoot?: string): string {
|
|
9
|
+
if (configuredRoot && configuredRoot.trim().length > 0) {
|
|
10
|
+
return resolve(configuredRoot);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let current = resolve(startDir);
|
|
14
|
+
while (true) {
|
|
15
|
+
if (hasProjectConfig(current)) {
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parent = dirname(current);
|
|
20
|
+
if (parent === current) {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
current = parent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Could not resolve the Spec Embryo project root. Set SPM_PROJECT_ROOT or run the UI from a repo with `.specpm/config.toml`.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveProjectRoot(): string {
|
|
33
|
+
return resolveProjectRootFrom(process.cwd(), process.env.SPM_PROJECT_ROOT);
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DashboardKanbanColumn, DashboardTaskCard } from "../../../src/services/dashboard.ts";
|
|
2
|
+
|
|
3
|
+
export function matchesSelectedSpecs(task: DashboardTaskCard, selectedSpecIds: string[]): boolean {
|
|
4
|
+
if (selectedSpecIds.length === 0) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return task.specIds.some((specId) => selectedSpecIds.includes(specId));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function filterKanbanBySpecs(columns: DashboardKanbanColumn[], selectedSpecIds: string[]): DashboardKanbanColumn[] {
|
|
12
|
+
return columns.map((column) => ({
|
|
13
|
+
...column,
|
|
14
|
+
tasks: column.tasks.filter((task) => matchesSelectedSpecs(task, selectedSpecIds)),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function countKanbanTasks(columns: DashboardKanbanColumn[]): number {
|
|
19
|
+
return columns.reduce((total, column) => total + column.tasks.length, 0);
|
|
20
|
+
}
|