opencastle 0.7.0 → 0.8.1
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 +30 -3
- package/bin/cli.mjs +2 -0
- package/dist/cli/adapters/claude-code.d.ts +2 -5
- package/dist/cli/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/adapters/claude-code.js +12 -251
- package/dist/cli/adapters/claude-code.js.map +1 -1
- package/dist/cli/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/adapters/cursor.js +3 -17
- package/dist/cli/adapters/cursor.js.map +1 -1
- package/dist/cli/adapters/frontmatter.d.ts +26 -0
- package/dist/cli/adapters/frontmatter.d.ts.map +1 -0
- package/dist/cli/adapters/frontmatter.js +40 -0
- package/dist/cli/adapters/frontmatter.js.map +1 -0
- package/dist/cli/adapters/index.d.ts +5 -0
- package/dist/cli/adapters/index.d.ts.map +1 -0
- package/dist/cli/adapters/index.js +9 -0
- package/dist/cli/adapters/index.js.map +1 -0
- package/dist/cli/adapters/opencode.d.ts +2 -5
- package/dist/cli/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/adapters/opencode.js +12 -250
- package/dist/cli/adapters/opencode.js.map +1 -1
- package/dist/cli/adapters/single-file-base.d.ts +40 -0
- package/dist/cli/adapters/single-file-base.d.ts.map +1 -0
- package/dist/cli/adapters/single-file-base.js +246 -0
- package/dist/cli/adapters/single-file-base.js.map +1 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +3 -2
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/detect.d.ts.map +1 -1
- package/dist/cli/detect.js +13 -11
- package/dist/cli/detect.js.map +1 -1
- package/dist/cli/doctor.d.ts +3 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +205 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +31 -19
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run/schema.d.ts +1 -5
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +6 -330
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +14 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +0 -5
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +4 -17
- package/dist/cli/update.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/adapters/claude-code.ts +13 -304
- package/src/cli/adapters/cursor.ts +3 -23
- package/src/cli/adapters/frontmatter.ts +47 -0
- package/src/cli/adapters/index.ts +13 -0
- package/src/cli/adapters/opencode.ts +12 -301
- package/src/cli/adapters/single-file-base.ts +320 -0
- package/src/cli/dashboard.ts +3 -2
- package/src/cli/detect.ts +19 -15
- package/src/cli/doctor.ts +235 -0
- package/src/cli/init.ts +31 -24
- package/src/cli/run/schema.ts +7 -365
- package/src/cli/run.ts +17 -1
- package/src/cli/types.ts +0 -6
- package/src/cli/update.ts +5 -23
- package/src/dashboard/dist/_astro/{index.CWVzbF4T.css → index.Bnq19_1M.css} +1 -1
- package/src/dashboard/dist/index.html +170 -11
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/seed-data/reviews.ndjson +6 -0
- package/src/dashboard/src/pages/index.astro +213 -10
- package/src/dashboard/src/styles/dashboard.css +196 -0
- package/src/orchestrator/agent-workflows/bug-fix.md +2 -2
- package/src/orchestrator/agent-workflows/data-pipeline.md +8 -8
- package/src/orchestrator/agent-workflows/database-migration.md +2 -2
- package/src/orchestrator/agent-workflows/feature-implementation.md +12 -5
- package/src/orchestrator/agent-workflows/performance-optimization.md +2 -2
- package/src/orchestrator/agent-workflows/refactoring.md +2 -2
- package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
- package/src/orchestrator/agent-workflows/security-audit.md +2 -2
- package/src/orchestrator/agents/data-expert.agent.md +2 -2
- package/src/orchestrator/agents/researcher.agent.md +0 -16
- package/src/orchestrator/agents/team-lead.agent.md +17 -6
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +1 -3
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +1 -1
- package/src/orchestrator/prompts/bug-fix.prompt.md +11 -6
- package/src/orchestrator/prompts/implement-feature.prompt.md +9 -4
- package/src/orchestrator/prompts/quick-refinement.prompt.md +9 -5
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +18 -4
- package/src/orchestrator/skills/agent-hooks/SKILL.md +4 -2
- package/src/orchestrator/skills/fast-review/SKILL.md +15 -4
- package/src/orchestrator/skills/self-improvement/SKILL.md +1 -1
- package/src/orchestrator/skills/validation-gates/SKILL.md +152 -15
- package/src/orchestrator/prompts/metrics-report.prompt.md +0 -144
package/src/cli/run/schema.ts
CHANGED
|
@@ -1,375 +1,17 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Minimal YAML parser for task spec files.
|
|
6
|
-
* Handles: key-value, lists, nested objects, block scalars (|), comments, quoted strings.
|
|
7
|
-
* Does NOT handle: anchors, aliases, flow mappings, merge keys, tags.
|
|
8
|
-
*/
|
|
2
|
+
import { parse as yamlParse } from 'yaml'
|
|
3
|
+
import type { TaskSpec, ValidationResult } from '../types.js'
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Parse a YAML string into a JS object.
|
|
7
|
+
* Uses the `yaml` npm package for full YAML 1.2 compliance.
|
|
12
8
|
*/
|
|
13
9
|
export function parseYaml(text: string): Record<string, unknown> {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Remove inline comments and trim trailing whitespace.
|
|
20
|
-
* Respects quoted strings — won't strip # inside quotes.
|
|
21
|
-
*/
|
|
22
|
-
function stripInlineComment(line: string): string {
|
|
23
|
-
let inSingle = false
|
|
24
|
-
let inDouble = false
|
|
25
|
-
for (let i = 0; i < line.length; i++) {
|
|
26
|
-
const ch = line[i]
|
|
27
|
-
if (ch === "'" && !inDouble) inSingle = !inSingle
|
|
28
|
-
else if (ch === '"' && !inSingle) inDouble = !inDouble
|
|
29
|
-
else if (ch === '#' && !inSingle && !inDouble) {
|
|
30
|
-
// Must be preceded by whitespace (or be at start)
|
|
31
|
-
if (i === 0 || /\s/.test(line[i - 1])) {
|
|
32
|
-
return line.slice(0, i).trimEnd()
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return line.trimEnd()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Measure indent level (number of leading spaces).
|
|
41
|
-
*/
|
|
42
|
-
function indentOf(line: string): number {
|
|
43
|
-
const m = line.match(/^( *)/)
|
|
44
|
-
return m ? m[1].length : 0
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Unquote a string value (strip surrounding quotes).
|
|
49
|
-
*/
|
|
50
|
-
function unquote(val: string): string {
|
|
51
|
-
if (
|
|
52
|
-
(val.startsWith('"') && val.endsWith('"')) ||
|
|
53
|
-
(val.startsWith("'") && val.endsWith("'"))
|
|
54
|
-
) {
|
|
55
|
-
return val.slice(1, -1)
|
|
10
|
+
const result = yamlParse(text)
|
|
11
|
+
if (!result || typeof result !== 'object') {
|
|
12
|
+
throw new Error('YAML must be a mapping at the top level')
|
|
56
13
|
}
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Cast a scalar value to its JS type.
|
|
62
|
-
*/
|
|
63
|
-
function castScalar(raw: string): string | number | boolean | null {
|
|
64
|
-
const val = raw.trim()
|
|
65
|
-
if (val === '' || val === '~' || val === 'null') return null
|
|
66
|
-
if (val === 'true') return true
|
|
67
|
-
if (val === 'false') return false
|
|
68
|
-
if (/^-?\d+$/.test(val)) return parseInt(val, 10)
|
|
69
|
-
if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val)
|
|
70
|
-
return unquote(val)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Parse a block of YAML lines starting at `startIdx` with minimum indent `parentIndent`.
|
|
75
|
-
* Returns { value, nextIndex }.
|
|
76
|
-
*/
|
|
77
|
-
function parseBlock(lines: string[], startIdx: number, parentIndent: number): ParseResult {
|
|
78
|
-
let i = startIdx
|
|
79
|
-
|
|
80
|
-
// Skip blank / comment-only lines
|
|
81
|
-
while (i < lines.length) {
|
|
82
|
-
const stripped = lines[i].trimStart()
|
|
83
|
-
if (stripped === '' || stripped.startsWith('#')) {
|
|
84
|
-
i++
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
break
|
|
88
|
-
}
|
|
89
|
-
if (i >= lines.length) return { value: null, nextIndex: i }
|
|
90
|
-
|
|
91
|
-
const firstLine = stripInlineComment(lines[i])
|
|
92
|
-
const firstIndent = indentOf(firstLine)
|
|
93
|
-
if (firstIndent <= parentIndent) return { value: null, nextIndex: i }
|
|
94
|
-
|
|
95
|
-
const trimmedFirst = firstLine.trimStart()
|
|
96
|
-
|
|
97
|
-
// Detect whether this block is a list or a mapping
|
|
98
|
-
if (trimmedFirst.startsWith('- ') || trimmedFirst === '-') {
|
|
99
|
-
return parseList(lines, i, firstIndent)
|
|
100
|
-
}
|
|
101
|
-
return parseMapping(lines, i, firstIndent)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Parse a YAML list block.
|
|
106
|
-
*/
|
|
107
|
-
function parseList(lines: string[], startIdx: number, blockIndent: number): ParseResult {
|
|
108
|
-
const result: unknown[] = []
|
|
109
|
-
let i = startIdx
|
|
110
|
-
|
|
111
|
-
while (i < lines.length) {
|
|
112
|
-
// Skip blanks / comments
|
|
113
|
-
const raw = lines[i]
|
|
114
|
-
const stripped = raw.trimStart()
|
|
115
|
-
if (stripped === '' || stripped.startsWith('#')) {
|
|
116
|
-
i++
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const indent = indentOf(raw)
|
|
121
|
-
if (indent < blockIndent) break
|
|
122
|
-
if (indent > blockIndent) break // Shouldn't happen at list level
|
|
123
|
-
|
|
124
|
-
const line = stripInlineComment(raw)
|
|
125
|
-
const trimmed = line.trimStart()
|
|
126
|
-
|
|
127
|
-
if (!trimmed.startsWith('- ') && trimmed !== '-') break
|
|
128
|
-
|
|
129
|
-
// Content after "- "
|
|
130
|
-
const after = trimmed === '-' ? '' : trimmed.slice(2).trim()
|
|
131
|
-
i++
|
|
132
|
-
|
|
133
|
-
if (after === '' || after.endsWith(':')) {
|
|
134
|
-
// List item is a nested mapping or empty
|
|
135
|
-
// Check if the next non-empty lines at deeper indent form an object
|
|
136
|
-
// If after ends with ':', it's the first key in a mapping
|
|
137
|
-
const obj: Record<string, unknown> = {}
|
|
138
|
-
if (after.endsWith(':')) {
|
|
139
|
-
const key = after.slice(0, -1).trim()
|
|
140
|
-
// Value on next lines or empty
|
|
141
|
-
const nested = parseValueAfterColon('', lines, i, blockIndent + 2)
|
|
142
|
-
obj[key] = nested.value
|
|
143
|
-
i = nested.nextIndex
|
|
144
|
-
}
|
|
145
|
-
// Collect remaining keys at the deeper indent
|
|
146
|
-
const sub = parseItemBody(lines, i, blockIndent + 2)
|
|
147
|
-
Object.assign(obj, (sub.value as Record<string, unknown>) || {})
|
|
148
|
-
i = sub.nextIndex
|
|
149
|
-
result.push(Object.keys(obj).length > 0 ? obj : null)
|
|
150
|
-
} else if (after.includes(': ') || after.endsWith(':')) {
|
|
151
|
-
// Inline mapping start: "- key: value"
|
|
152
|
-
const colonIdx = after.indexOf(':')
|
|
153
|
-
const key = after.slice(0, colonIdx).trim()
|
|
154
|
-
const rest = after.slice(colonIdx + 1).trim()
|
|
155
|
-
const obj: Record<string, unknown> = {}
|
|
156
|
-
|
|
157
|
-
if (rest === '' || rest === '|') {
|
|
158
|
-
const nested = parseValueAfterColon(rest, lines, i, blockIndent + 2)
|
|
159
|
-
obj[key] = nested.value
|
|
160
|
-
i = nested.nextIndex
|
|
161
|
-
} else {
|
|
162
|
-
obj[key] = castScalar(rest)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Collect remaining keys at deeper indent
|
|
166
|
-
const sub = parseItemBody(lines, i, blockIndent + 2)
|
|
167
|
-
Object.assign(obj, (sub.value as Record<string, unknown>) || {})
|
|
168
|
-
i = sub.nextIndex
|
|
169
|
-
result.push(obj)
|
|
170
|
-
} else if (after.startsWith('[') && after.endsWith(']')) {
|
|
171
|
-
// Inline flow sequence
|
|
172
|
-
result.push(parseFlowSequence(after))
|
|
173
|
-
} else {
|
|
174
|
-
// Simple scalar list item
|
|
175
|
-
result.push(castScalar(after))
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return { value: result, nextIndex: i }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Parse the body (remaining keys) of a list item at a given indent.
|
|
184
|
-
*/
|
|
185
|
-
function parseItemBody(lines: string[], startIdx: number, minIndent: number): ParseResult {
|
|
186
|
-
let i = startIdx
|
|
187
|
-
const obj: Record<string, unknown> = {}
|
|
188
|
-
|
|
189
|
-
while (i < lines.length) {
|
|
190
|
-
const raw = lines[i]
|
|
191
|
-
const stripped = raw.trimStart()
|
|
192
|
-
if (stripped === '' || stripped.startsWith('#')) {
|
|
193
|
-
i++
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const indent = indentOf(raw)
|
|
198
|
-
if (indent < minIndent) break
|
|
199
|
-
|
|
200
|
-
const line = stripInlineComment(raw)
|
|
201
|
-
const trimmed = line.trimStart()
|
|
202
|
-
|
|
203
|
-
// If this is a list item at this indent, it belongs to a parent list
|
|
204
|
-
if (trimmed.startsWith('- ')) break
|
|
205
|
-
|
|
206
|
-
const colonIdx = trimmed.indexOf(':')
|
|
207
|
-
if (colonIdx === -1) {
|
|
208
|
-
i++
|
|
209
|
-
continue
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const key = trimmed.slice(0, colonIdx).trim()
|
|
213
|
-
const rest = trimmed.slice(colonIdx + 1).trim()
|
|
214
|
-
i++
|
|
215
|
-
|
|
216
|
-
const nested = parseValueAfterColon(rest, lines, i, indent)
|
|
217
|
-
obj[key] = nested.value
|
|
218
|
-
i = nested.nextIndex
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return { value: Object.keys(obj).length > 0 ? obj : null, nextIndex: i }
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Parse a YAML mapping block.
|
|
226
|
-
*/
|
|
227
|
-
function parseMapping(lines: string[], startIdx: number, blockIndent: number): ParseResult {
|
|
228
|
-
const result: Record<string, unknown> = {}
|
|
229
|
-
let i = startIdx
|
|
230
|
-
|
|
231
|
-
while (i < lines.length) {
|
|
232
|
-
const raw = lines[i]
|
|
233
|
-
const stripped = raw.trimStart()
|
|
234
|
-
if (stripped === '' || stripped.startsWith('#')) {
|
|
235
|
-
i++
|
|
236
|
-
continue
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const indent = indentOf(raw)
|
|
240
|
-
if (indent < blockIndent) break
|
|
241
|
-
if (indent > blockIndent) {
|
|
242
|
-
i++
|
|
243
|
-
continue
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const line = stripInlineComment(raw)
|
|
247
|
-
const trimmed = line.trimStart()
|
|
248
|
-
|
|
249
|
-
const colonIdx = trimmed.indexOf(':')
|
|
250
|
-
if (colonIdx === -1) {
|
|
251
|
-
i++
|
|
252
|
-
continue
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const key = trimmed.slice(0, colonIdx).trim()
|
|
256
|
-
const rest = trimmed.slice(colonIdx + 1).trim()
|
|
257
|
-
i++
|
|
258
|
-
|
|
259
|
-
const nested = parseValueAfterColon(rest, lines, i, blockIndent)
|
|
260
|
-
result[key] = nested.value
|
|
261
|
-
i = nested.nextIndex
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return { value: result, nextIndex: i }
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Parse the value after a colon — could be inline scalar, block scalar (|),
|
|
269
|
-
* nested mapping, or nested list.
|
|
270
|
-
*/
|
|
271
|
-
function parseValueAfterColon(
|
|
272
|
-
rest: string,
|
|
273
|
-
lines: string[],
|
|
274
|
-
nextIdx: number,
|
|
275
|
-
parentIndent: number
|
|
276
|
-
): ParseResult {
|
|
277
|
-
// Block scalar
|
|
278
|
-
if (rest === '|') {
|
|
279
|
-
return parseBlockScalar(lines, nextIdx, parentIndent)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Flow sequence [a, b, c]
|
|
283
|
-
if (rest.startsWith('[') && rest.endsWith(']')) {
|
|
284
|
-
return { value: parseFlowSequence(rest), nextIndex: nextIdx }
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Inline scalar value present
|
|
288
|
-
if (rest !== '') {
|
|
289
|
-
return { value: castScalar(rest), nextIndex: nextIdx }
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Empty after colon — check for nested block
|
|
293
|
-
const nested = parseBlock(lines, nextIdx, parentIndent)
|
|
294
|
-
if (nested.value !== null) {
|
|
295
|
-
return nested
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return { value: null, nextIndex: nextIdx }
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Parse a block scalar (| indicator).
|
|
303
|
-
* Collects all lines with indent greater than the parent.
|
|
304
|
-
*/
|
|
305
|
-
function parseBlockScalar(lines: string[], startIdx: number, parentIndent: number): ParseResult {
|
|
306
|
-
let i = startIdx
|
|
307
|
-
const collected: string[] = []
|
|
308
|
-
let blockIndent = -1
|
|
309
|
-
|
|
310
|
-
while (i < lines.length) {
|
|
311
|
-
const raw = lines[i]
|
|
312
|
-
|
|
313
|
-
// Blank line inside block scalar — preserve it
|
|
314
|
-
if (raw.trim() === '') {
|
|
315
|
-
collected.push('')
|
|
316
|
-
i++
|
|
317
|
-
continue
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const indent = indentOf(raw)
|
|
321
|
-
if (blockIndent === -1) {
|
|
322
|
-
// First content line determines the block indent
|
|
323
|
-
if (indent <= parentIndent) break
|
|
324
|
-
blockIndent = indent
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (indent < blockIndent) break
|
|
328
|
-
|
|
329
|
-
collected.push(raw.slice(blockIndent))
|
|
330
|
-
i++
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Remove trailing blank lines
|
|
334
|
-
while (collected.length > 0 && collected[collected.length - 1] === '') {
|
|
335
|
-
collected.pop()
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return { value: collected.join('\n') + '\n', nextIndex: i }
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Parse a flow sequence: [item1, item2, item3]
|
|
343
|
-
* Handles quoted strings that may contain commas.
|
|
344
|
-
*/
|
|
345
|
-
function parseFlowSequence(text: string): Array<string | number | boolean | null> {
|
|
346
|
-
const inner = text.slice(1, -1).trim()
|
|
347
|
-
if (inner === '') return []
|
|
348
|
-
|
|
349
|
-
const items: string[] = []
|
|
350
|
-
let current = ''
|
|
351
|
-
let inQuote: string | null = null
|
|
352
|
-
|
|
353
|
-
for (let i = 0; i < inner.length; i++) {
|
|
354
|
-
const ch = inner[i]
|
|
355
|
-
if (inQuote) {
|
|
356
|
-
if (ch === inQuote) {
|
|
357
|
-
inQuote = null
|
|
358
|
-
} else {
|
|
359
|
-
current += ch
|
|
360
|
-
}
|
|
361
|
-
} else if (ch === '"' || ch === "'") {
|
|
362
|
-
inQuote = ch
|
|
363
|
-
} else if (ch === ',') {
|
|
364
|
-
items.push(current.trim())
|
|
365
|
-
current = ''
|
|
366
|
-
} else {
|
|
367
|
-
current += ch
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (current.trim()) items.push(current.trim())
|
|
371
|
-
|
|
372
|
-
return items.map(castScalar)
|
|
14
|
+
return result as Record<string, unknown>
|
|
373
15
|
}
|
|
374
16
|
|
|
375
17
|
// ── Schema validation ──────────────────────────────────────────────
|
package/src/cli/run.ts
CHANGED
|
@@ -114,9 +114,25 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
114
114
|
const adapter = await getAdapter(spec.adapter)
|
|
115
115
|
const available = await adapter.isAvailable()
|
|
116
116
|
if (!available) {
|
|
117
|
+
const hints: Record<string, string> = {
|
|
118
|
+
'claude-code':
|
|
119
|
+
' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
120
|
+
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
121
|
+
copilot:
|
|
122
|
+
' Install: npm install -g @anthropic-ai/claude-code (or use VS Code)\n' +
|
|
123
|
+
' The Copilot CLI is bundled with GitHub Copilot in VS Code.\n' +
|
|
124
|
+
' Docs: https://docs.github.com/en/copilot',
|
|
125
|
+
cursor:
|
|
126
|
+
' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
127
|
+
' Install Cursor from https://cursor.com and ensure the\n' +
|
|
128
|
+
' "agent" command is on your PATH (Cursor > Install CLI).',
|
|
129
|
+
}
|
|
130
|
+
const cliName = spec.adapter === 'claude-code' ? 'claude' : spec.adapter
|
|
131
|
+
const hint = hints[spec.adapter] ?? ''
|
|
117
132
|
console.error(
|
|
118
133
|
` ✗ Adapter "${spec.adapter}" is not available.\n` +
|
|
119
|
-
` Make sure the "${
|
|
134
|
+
` Make sure the "${cliName}" CLI is installed and on your PATH.\n` +
|
|
135
|
+
hint
|
|
120
136
|
)
|
|
121
137
|
process.exit(1)
|
|
122
138
|
}
|
package/src/cli/types.ts
CHANGED
|
@@ -233,12 +233,6 @@ export interface RunOptions {
|
|
|
233
233
|
help: boolean;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/** Parse result from YAML block parser. */
|
|
237
|
-
export interface ParseResult {
|
|
238
|
-
value: unknown;
|
|
239
|
-
nextIndex: number;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
236
|
/** Validation result. */
|
|
243
237
|
export interface ValidationResult {
|
|
244
238
|
valid: boolean;
|
package/src/cli/update.ts
CHANGED
|
@@ -2,27 +2,9 @@ import { resolve } from 'node:path'
|
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { readManifest, writeManifest } from './manifest.js'
|
|
4
4
|
import { confirm, closePrompts, c } from './prompt.js'
|
|
5
|
-
import { isLegacyStack, migrateStackConfig } from './types.js'
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
const ADAPTERS: Record<string, () => Promise<IdeAdapter>> = {
|
|
9
|
-
vscode: () => import('./adapters/vscode.js') as Promise<IdeAdapter>,
|
|
10
|
-
cursor: () => import('./adapters/cursor.js') as Promise<IdeAdapter>,
|
|
11
|
-
'claude-code': () =>
|
|
12
|
-
import('./adapters/claude-code.js') as Promise<IdeAdapter>,
|
|
13
|
-
opencode: () =>
|
|
14
|
-
import('./adapters/opencode.js') as Promise<IdeAdapter>,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const VALID_IDES = Object.keys(ADAPTERS)
|
|
18
|
-
|
|
19
|
-
/** IDE display labels */
|
|
20
|
-
const IDE_DISPLAY: Record<IdeChoice, string> = {
|
|
21
|
-
vscode: 'VS Code',
|
|
22
|
-
cursor: 'Cursor',
|
|
23
|
-
'claude-code': 'Claude Code',
|
|
24
|
-
opencode: 'OpenCode',
|
|
25
|
-
}
|
|
5
|
+
import { isLegacyStack, migrateStackConfig, IDE_LABELS } from './types.js'
|
|
6
|
+
import { IDE_ADAPTERS, VALID_IDES } from './adapters/index.js'
|
|
7
|
+
import type { CliContext, IdeChoice } from './types.js'
|
|
26
8
|
|
|
27
9
|
export default async function update({
|
|
28
10
|
pkgRoot,
|
|
@@ -65,7 +47,7 @@ export default async function update({
|
|
|
65
47
|
return
|
|
66
48
|
}
|
|
67
49
|
|
|
68
|
-
const ideNames = ides.map((id) =>
|
|
50
|
+
const ideNames = ides.map((id) => IDE_LABELS[id as IdeChoice] ?? id).join(', ')
|
|
69
51
|
console.log(
|
|
70
52
|
`\n 🏰 ${c.bold('OpenCastle')} ${dryRun ? 'dry-run' : 'update'}: ${c.dim(`v${manifest.version}`)} → ${c.green(`v${pkg.version}`)}\n`
|
|
71
53
|
)
|
|
@@ -98,7 +80,7 @@ export default async function update({
|
|
|
98
80
|
const allManagedPaths = { framework: [] as string[], customizable: [] as string[] }
|
|
99
81
|
|
|
100
82
|
for (const ide of ides) {
|
|
101
|
-
const adapter = await
|
|
83
|
+
const adapter = await IDE_ADAPTERS[ide]()
|
|
102
84
|
const results = await adapter.update(pkgRoot, projectRoot, manifest.stack)
|
|
103
85
|
totalCopied += results.copied.length
|
|
104
86
|
totalCreated += results.created.length
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #5a5a6e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}
|
|
1
|
+
:root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #5a5a6e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}.filter-bar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;padding:16px 20px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px}.filter-group{display:flex;flex-direction:column;gap:4px;min-width:0}.filter-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.filter-input,.filter-select{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;transition:border-color var(--transition-fast);font-family:inherit}.filter-input:focus,.filter-select:focus{border-color:var(--border-accent)}.filter-input{width:140px;color-scheme:dark}.filter-select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.filter-reset{height:34px;font-size:.75rem}.dash-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:.8125rem;font-weight:500;font-family:inherit;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast)}.dash-btn--ghost{color:var(--text-secondary);background:#ffffff0f}.dash-btn--ghost:hover{color:var(--text-primary);background:#ffffff1a}.dash-header__actions{display:flex;align-items:center;gap:8px}@media(max-width:480px){.dash-header__inner{padding:0 12px}.dash-main{padding:12px;gap:12px}.kpi-card,.chart-card__header{padding:14px 16px}.chart-card__body{padding:12px 16px 16px}.filter-bar{padding:12px;gap:8px}.filter-input,.filter-select{width:100%;min-width:unset}.filter-group{flex:1 1 calc(50% - 4px)}.filter-reset{width:100%}.dash-header__title{font-size:.875rem}.exec-step__meta{flex-direction:column;gap:2px}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5),.sessions-table th:nth-child(6),.sessions-table td:nth-child(6),.sessions-table th:nth-child(7),.sessions-table td:nth-child(7),.sessions-table th:nth-child(8),.sessions-table td:nth-child(8){display:none}}@media(max-width:768px){.charts-row{grid-template-columns:1fr}.pipeline{flex-wrap:wrap;gap:8px}.pipeline-arrow{display:none}.pipeline-stage{flex:1 1 calc(50% - 4px);min-width:100px}.tier-chart .donut-container,.donut-container{flex-direction:column;align-items:center}.sessions-table{font-size:.75rem}.sessions-table th,.sessions-table td{padding:8px 6px}}
|