chore: 不跟踪 todo/ 与 design/maestro-ds/(误入 git add -A,应保持未跟踪)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -1,76 +0,0 @@
|
||||
# CLAUDE.md — Maestro Design System
|
||||
|
||||
Engineering conventions for working in this design-system repo. Read alongside `README.md` (design foundations) and `SKILL.md` (the design language quick-start). The phosphor-console aesthetic and the rules in SKILL.md are binding — this file covers **how the code is organized and how to extend it correctly**.
|
||||
|
||||
## What this repo is
|
||||
|
||||
A compiler-backed design system. On every change an automated compiler reads the sources and regenerates three files — **never hand-edit these**:
|
||||
|
||||
- `_ds_bundle.js` — the compiled component library, exposed as `window.MaestroDesignSystem_a6a290`
|
||||
- `_ds_manifest.json` — card / token / component index
|
||||
- `_adherence.oxlintrc.json` — lint rules
|
||||
|
||||
The only fixed entry point is `styles.css` (root). It must stay a list of `@import` lines — never inline rules there.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
styles.css # entry — @import only
|
||||
tokens/ # CSS custom properties, one file per concern
|
||||
colors.css # base + signal + -dim + semantic aliases + [data-theme=light] scope
|
||||
typography.css # --mono family, size/leading/tracking scale
|
||||
effects.css # spacing, radii, borders, shadows, @keyframes
|
||||
fonts.css # Google Fonts @import (IBM Plex Mono + Noto Sans SC)
|
||||
guidelines/*.card.html # specimen cards for the Design System tab
|
||||
components/<group>/ # core | forms | surfaces
|
||||
Name.jsx # the component (named export)
|
||||
Name.d.ts # props contract (+ @startingPoint JSDoc if applicable)
|
||||
Name.prompt.md # one-line what/when + JSX example
|
||||
<group>.card.html # @dsCard showcase for the whole group
|
||||
ui_kits/console/ # desktop console recreation (composes components)
|
||||
ui_kits/console_mobile/ # 390px mobile recreation
|
||||
assets/icons/*.svg # double-tone icon set
|
||||
```
|
||||
|
||||
## Authoring a component (the contract the compiler enforces)
|
||||
|
||||
A component is discovered when a directory holds **all three**: `Name.jsx` (PascalCase, with `export function Name(props)`), `Name.d.ts`, and ideally `Name.prompt.md`. To add or change one:
|
||||
|
||||
1. Write `Name.jsx` — React only, no npm deps, no CSS-in-JS libs. Style via the CSS custom properties (`var(--…)`). Self-contained; siblings may import each other by relative path.
|
||||
2. Inject component CSS once via an `ensure<X>Css()` guard that appends a `<style id="…">` (see any existing component) — keeps class rules out of global scope and idempotent.
|
||||
3. Write `Name.d.ts` with the full props interface and JSDoc. Write `Name.prompt.md`: line 1 = what & when, then a small JSX example, then notable variants.
|
||||
4. Add the variant/state to the directory's `<group>.card.html` so it shows in the Design System tab. Load the bundle via `<script src="…/_ds_bundle.js">` and read `const { Name } = window.MaestroDesignSystem_a6a290`. **Never `<script src>` a `.jsx` directly** — the export is unreachable that way.
|
||||
5. Run `check_design_system` and fix anything it reports until clean.
|
||||
|
||||
Do **not** write `_ds_bundle.js`, `_ds_manifest.json`, `_adherence.oxlintrc.json`, or a barrel `index.js` — all generated.
|
||||
|
||||
## Styling rules
|
||||
|
||||
- All color/spacing/radius/motion values come from tokens. If a value isn't a token yet and is reused, add it to the right `tokens/*.css` file rather than hardcoding.
|
||||
- New tokens: base value + a semantic alias when it has meaning (`--violet` + `--status-gate`). Theme overrides go under the `[data-theme="light"]` scope in `colors.css`.
|
||||
- Respect the five-signal-color discipline (SKILL.md). Approval = violet, MED = amber — do not collapse them.
|
||||
- Radii use `--radius-xs/sm/md/lg`; never `border-radius: 0` for new surfaces (zero-radius was retired in v1.1) and never a raw px circle except status dots.
|
||||
|
||||
## UI-kit conventions (`ui_kits/console`)
|
||||
|
||||
- `index.html` is the React entry (Babel-in-browser, pinned versions + integrity hashes — keep them). Each surface is its own `*.jsx` file attached to `window.MaestroKit*`; `data.js` holds the mock, `i18n.js` holds all strings.
|
||||
- **Every user-facing string goes through `t.<key>` in i18n.js across all 5 languages (zh/en/es/ja/fr).** Never hardcode display text in a component. Status/gate/event/autonomy labels are passed as props from `t`.
|
||||
- Persisted UI state (sidebar/event collapse, column widths, language, theme) uses `localStorage` keys prefixed `maestro-kit-`. Never clear keys you didn't write.
|
||||
- Flyout panels that live inside the `overflow:hidden` rails must use `position: fixed` with a measured anchor rect (see `GlobalConfigRow` / `AgentsRow` / `UserRow`) so they aren't clipped.
|
||||
- Three-column grid widths are drag-resizable with min/max clamps that guarantee a ≥480px center; collapsed rails are 52px. Keep that invariant if you touch the layout.
|
||||
|
||||
## Icons
|
||||
|
||||
Add new icons to `assets/icons/` on the same grid: 24×24, `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, round caps/joins. Two-tone semantic strokes use `var(--icon-accent, <signal fallback>)`. Don't add a third-party icon library. Brand glyphs (`▍ ▮ → ↳ · « »`) stay as mono characters, not SVG.
|
||||
|
||||
## Before finishing any change
|
||||
|
||||
1. `check_design_system` → resolve all reported issues.
|
||||
2. Verify the affected `ui_kits/*/index.html` renders with no console errors, in both themes and at least zh + en.
|
||||
3. Keep `README.md` INDEX and component lists in sync if you add/remove files.
|
||||
|
||||
## Known caveats
|
||||
|
||||
- Fonts are CDN-loaded; no woff2 binaries in-repo. For offline/production, drop woff2 into the project and convert `tokens/fonts.css` to local `@font-face`.
|
||||
- The logo is a 15×12 pixel-octopus drawn in canvas/CSS (see `guidelines/brand-logo.card.html`) — there is no logo image file.
|
||||
- `_ds_bundle.js` recompiles at end of turn; a freshly edited component may look stale in the preview until then.
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: maestro-design
|
||||
description: Generate well-branded interfaces, components, and assets for Maestro (多项目任务调度平台) — production code or throwaway prototypes/mocks. Contains the full design language (colors, type, fonts, motion, iconography), reusable React components, and two pixel-accurate UI kits (desktop console + mobile). Invoke for any Maestro UI, screen, slide, or asset work.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Maestro Design Skill
|
||||
|
||||
Maestro is a local-first multi-project task-orchestration daemon. Its one product surface is the **Web 调度台 (console)** — a three-column board: project rail · approval-gates + task tree · event stream, refreshed over WebSocket, with in-page accept/reject of agent work. The brand metaphor is a **phosphor console**: a carbon-green CRT scheduling terminal, not a SaaS dashboard.
|
||||
|
||||
## How to use this skill
|
||||
|
||||
1. **Read `README.md` first** — it carries the full CONTENT, VISUAL, and ICONOGRAPHY foundations plus the file index. Treat it as the source of truth; this file is the quick-start.
|
||||
2. **Reuse, don't reinvent.** Components live in `components/{core,forms,surfaces}/` and compile into `_ds_bundle.js` under namespace `window.MaestroDesignSystem_a6a290`. The two UI kits in `ui_kits/` show exactly how to compose them.
|
||||
3. **Match the language exactly** — fonts, colors, motion, copy tone below. When unsure, open the matching `guidelines/*.card.html` specimen.
|
||||
4. For **throwaway artifacts** (slides, mocks, one-off prototypes): copy the assets/tokens you need and emit standalone HTML the user can open. For **production code**: read the rules here and the component `.prompt.md` / `.d.ts` files to become an expert, then write against the real tokens.
|
||||
5. If invoked with no brief, **ask what to build**, ask a few scoping questions, then act as an expert Maestro designer.
|
||||
|
||||
## Non-negotiable rules (the "do / don't")
|
||||
|
||||
- **One type family only:** `--mono` = IBM Plex Mono + Noto Sans SC fallback. There is no heading font. Build hierarchy from size (10–20px, body 13px), weight (400/500/600/700), and letter-spacing (.04em–.35em). Uppercase + wide tracking is the strongest hierarchy signal.
|
||||
- **Five signal colors, fixed meanings:** green = runnable/success/brand · **violet** = approval gate / awaiting decision · amber = warning / MED complexity / waiting-on-deps · red = failed/reject/Hard · cyan = executing/agent/link. Each has a `-dim` companion for borders and 5–8% tint fills. Never repurpose a signal color. (Approval was amber pre-v1.1 — it is violet now, to separate it from MED.)
|
||||
- **Glow, not shadow.** Ambient emphasis is a same-color `0 0 8–18px` glow (brand word, status dots, running chips, button hover). Black drop-shadows are reserved for floating layers only (popovers / modals / toasts). No inner shadows.
|
||||
- **Small, restrained radii:** badge/chip 3px, button/input 4px, panel/card 6px, modal/popover 10px (`--radius-xs/sm/md/lg`). True circles are status dots ONLY.
|
||||
- **Borders carry structure:** 1px `--line` / `--line-soft`; dashed = empty/placeholder/weak-group; 2px left solid = current/active/doc-block.
|
||||
- **Motion is fast and hard:** transitions .1–.12s; entrances use `rise` (5px up + fade, .18–.25s); attention via `blink` (steps(1) cursor) and `pulse` (.8–2.2s, for awaiting/executing). No bounce, no easing flourishes. Honor `prefers-reduced-motion`.
|
||||
- **No emoji.** Icons are the double-tone SVG set in `assets/icons/` (24×24, 2px round-cap, `currentColor` + `--icon-accent`). Brand glyphs `▍ ▮ → ↳ · — « »` stay as mono characters. Never pull in lucide/heroicons.
|
||||
- **Copy:** Chinese-first, terse telegraphic 2–4-char labels, `·` interpunct as the lead separator, English terms kept verbatim (HARD/MED/EASY, plan/spec/operations, agent, worktree, accept/reject). Address the user as 「你」; the system has no self-name. Required fields use a red `*`; placeholders give real examples (`/path/to/repo`, `npm test(可空)`).
|
||||
- **Full-screen surfaces** get the `body::before` ambience layer (1px scanlines + vignette, opacity .5 dark / .12 light) and `::selection` green-on-white.
|
||||
|
||||
## Theming & i18n (build these in from the start)
|
||||
|
||||
- **Dark is default; light via `<html data-theme="light">`.** Every color flows through CSS variables, so components adapt automatically — never hardcode a hex; use `var(--…)`.
|
||||
- **UI chrome is bilingual+ (zh/en/es/ja/fr)** in `ui_kits/console/i18n.js`. Status/gate/event labels are injected through component `label`/`kindLabel` props. User content (task titles, paths) is never translated.
|
||||
|
||||
## Components (namespace `window.MaestroDesignSystem_a6a290`)
|
||||
|
||||
- **core:** `Button` (variants default/solid/ghost/accept/reject, size xs) · `StatusChip` (16 状态机状态) · `ComplexityBadge` (hard/medium/easy) · `ComplexitySeg` (segmented picker, optional cyan `auto`/「智能」 = model-decides) · `CountBadge` (gate/ready/run/blocked, hover-expands) · `QuotaMeter` (LED bar, ≤80 cyan / >80 amber / >95 red) · `SectionHead` (▍ + uppercase title + action)
|
||||
- **forms:** `Input` · `Select` · `Textarea` (danger variant for reject reasons)
|
||||
- **surfaces:** `Panel` · `GateCard` (collapsible, double-click → fullscreen, accept/reject footer) · `Toast` (ok/err/warn) · `EventItem` (square color-coded marker) · `Timeline`
|
||||
|
||||
Each component has a sibling `.prompt.md` (what/when + JSX example) and `.d.ts` (props contract). Read them before using.
|
||||
|
||||
## UI kits (copy these patterns)
|
||||
|
||||
- `ui_kits/console/` — full desktop console: resizable collapsible rails, project status dots + pending badges, global Agents/Settings/user flyouts, approval gates, filterable task tree with dependency jump + complexity re-pick, archive with paging + detail modal, quota meters, 5-language + light/dark toggles. `data.js` = mock, `i18n.js` = strings.
|
||||
- `ui_kits/console_mobile/` — 390px single column + bottom tab bar (tasks/gates/events/projects), ≥44px touch targets.
|
||||
|
||||
## Files
|
||||
`styles.css` (entry, @import only) → `tokens/{colors,typography,effects,fonts}.css` · `guidelines/*.card.html` (specimens) · `assets/icons/*.svg` · component dirs · `ui_kits/*`. Fonts load from Google Fonts CDN (IBM Plex Mono + Noto Sans SC); no binaries shipped — supply woff2 for offline use.
|
||||
@@ -1,389 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"react",
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"react/forbid-elements": [
|
||||
"warn",
|
||||
{
|
||||
"forbid": []
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"components/core/**",
|
||||
"components/forms/**",
|
||||
"components/surfaces/**",
|
||||
"ui_kits/console/**",
|
||||
"ui_kits/console_mobile/**"
|
||||
],
|
||||
"message": "Import design-system components from 'index.js', not component internals."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]",
|
||||
"message": "Raw hex color — use a design-system color token via var()."
|
||||
},
|
||||
{
|
||||
"selector": "Literal[value=/\\b\\d+px\\b/]",
|
||||
"message": "Raw px value — use a design-system spacing token via var()."
|
||||
},
|
||||
{
|
||||
"selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:IBM Plex Mono|Noto Sans SC))/i]",
|
||||
"message": "Font not provided by the design system. Available: IBM Plex Mono, Noto Sans SC."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|size|disabled|type|onClick|style|children|key|ref|className|style|children)$/]",
|
||||
"message": "<Button> doesn't accept that prop. Declared props: variant, size, disabled, type, onClick, style, children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute[name.name='variant'] > Literal[value!=/^(?:default|solid|ghost|accept|reject)$/]",
|
||||
"message": "<Button> variant must be one of 'default' | 'solid' | 'ghost' | 'accept' | 'reject'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:xs)$/]",
|
||||
"message": "<Button> size must be one of 'xs'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute[name.name='type'] > Literal[value!=/^(?:button|submit)$/]",
|
||||
"message": "<Button> type must be one of 'button' | 'submit'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='ComplexityBadge'] > JSXAttribute > JSXIdentifier[name!=/^(?:complexity|key|ref|className|style|children)$/]",
|
||||
"message": "<ComplexityBadge> doesn't accept that prop. Declared props: complexity."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='ComplexityBadge'] > JSXAttribute[name.name='complexity'] > Literal[value!=/^(?:hard|medium|easy)$/]",
|
||||
"message": "<ComplexityBadge> complexity must be one of 'hard' | 'medium' | 'easy'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='ComplexitySeg'] > JSXAttribute > JSXIdentifier[name!=/^(?:value|defaultValue|onChange|includeAuto|autoLabel|labels|key|ref|className|style|children)$/]",
|
||||
"message": "<ComplexitySeg> doesn't accept that prop. Declared props: value, defaultValue, onChange, includeAuto, autoLabel, labels."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='ComplexitySeg'] > JSXAttribute[name.name='value'] > Literal[value!=/^(?:hard|medium|easy|auto)$/]",
|
||||
"message": "<ComplexitySeg> value must be one of 'hard' | 'medium' | 'easy' | 'auto'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='ComplexitySeg'] > JSXAttribute[name.name='defaultValue'] > Literal[value!=/^(?:hard|medium|easy|auto)$/]",
|
||||
"message": "<ComplexitySeg> defaultValue must be one of 'hard' | 'medium' | 'easy' | 'auto'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='CountBadge'] > JSXAttribute > JSXIdentifier[name!=/^(?:kind|count|label|title|key|ref|className|style|children)$/]",
|
||||
"message": "<CountBadge> doesn't accept that prop. Declared props: kind, count, label, title."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='CountBadge'] > JSXAttribute[name.name='kind'] > Literal[value!=/^(?:gate|ready|run|blocked|total)$/]",
|
||||
"message": "<CountBadge> kind must be one of 'gate' | 'ready' | 'run' | 'blocked' | 'total'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='EventItem'] > JSXAttribute > JSXIdentifier[name!=/^(?:type|time|detail|label|key|ref|className|style|children)$/]",
|
||||
"message": "<EventItem> doesn't accept that prop. Declared props: type, time, detail, label."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='EventItem'] > JSXAttribute[name.name='type'] > Literal[value!=/^(?:task\\.created|task\\.updated|status\\.changed|approval\\.requested|approval\\.granted|approval\\.rejected|run\\.started|run\\.finished|project\\.synced)$/]",
|
||||
"message": "<EventItem> type must be one of 'task.created' | 'task.updated' | 'status.changed' | 'approval.requested' | 'approval.granted' | 'approval.rejected' | 'run.started' | 'run.finished' | 'project.synced'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='GateCard'] > JSXAttribute > JSXIdentifier[name!=/^(?:gate|kindLabel|title|meta|docLabel|doc|actions|collapsed|foldable|onToggleFold|foldTitle|headerExtra|onHeaderDoubleClick|headerTitle|children|style|key|ref|className|style|children)$/]",
|
||||
"message": "<GateCard> doesn't accept that prop. Declared props: gate, kindLabel, title, meta, docLabel, doc, actions, collapsed, foldable, onToggleFold, foldTitle, headerExtra, onHeaderDoubleClick, headerTitle, children, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='GateCard'] > JSXAttribute[name.name='gate'] > Literal[value!=/^(?:plan|spec|exec)$/]",
|
||||
"message": "<GateCard> gate must be one of 'plan' | 'spec' | 'exec'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Input'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|required|type|placeholder|value|defaultValue|onChange|width|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Input> doesn't accept that prop. Declared props: label, required, type, placeholder, value, defaultValue, onChange, width, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Input'] > JSXAttribute[name.name='type'] > Literal[value!=/^(?:text|number)$/]",
|
||||
"message": "<Input> type must be one of 'text' | 'number'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Panel'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|padding|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Panel> doesn't accept that prop. Declared props: children, padding, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='QuotaMeter'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|pct|detail|cells|style|key|ref|className|style|children)$/]",
|
||||
"message": "<QuotaMeter> doesn't accept that prop. Declared props: label, pct, detail, cells, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='SectionHead'] > JSXAttribute > JSXIdentifier[name!=/^(?:mark|title|action|sticky|style|key|ref|className|style|children)$/]",
|
||||
"message": "<SectionHead> doesn't accept that prop. Declared props: mark, title, action, sticky, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='SectionHead'] > JSXAttribute[name.name='mark'] > Literal[value!=/^(?:green|violet|amber|cyan)$/]",
|
||||
"message": "<SectionHead> mark must be one of 'green' | 'violet' | 'amber' | 'cyan'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Select'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|required|options|value|defaultValue|onChange|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Select> doesn't accept that prop. Declared props: label, required, options, value, defaultValue, onChange, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='StatusChip'] > JSXAttribute > JSXIdentifier[name!=/^(?:status|label|key|ref|className|style|children)$/]",
|
||||
"message": "<StatusChip> doesn't accept that prop. Declared props: status, label."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='StatusChip'] > JSXAttribute[name.name='status'] > Literal[value!=/^(?:init|analyzing|plan_review|decomposed|speccing|spec_review|ready|blocked|queued|executing|exec_review|failed|needs_attention|done|paused|cancelled)$/]",
|
||||
"message": "<StatusChip> status must be one of 'init' | 'analyzing' | 'plan_review' | 'decomposed' | 'speccing' | 'spec_review' | 'ready' | 'blocked' | 'queued' | 'executing' | 'exec_review' | 'failed' | 'needs_attention' | 'done' | 'paused' | 'cancelled'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Textarea'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|required|placeholder|value|defaultValue|onChange|minHeight|danger|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Textarea> doesn't accept that prop. Declared props: label, required, placeholder, value, defaultValue, onChange, minHeight, danger, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='TimelineItem'] > JSXAttribute > JSXIdentifier[name!=/^(?:time|text|who|color|key|ref|className|style|children)$/]",
|
||||
"message": "<TimelineItem> doesn't accept that prop. Declared props: time, text, who, color."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Toast'] > JSXAttribute > JSXIdentifier[name!=/^(?:kind|children|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Toast> doesn't accept that prop. Declared props: kind, children, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Toast'] > JSXAttribute[name.name='kind'] > Literal[value!=/^(?:ok|err|warn)$/]",
|
||||
"message": "<Toast> kind must be one of 'ok' | 'err' | 'warn'."
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/index.js"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-imports": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-omelette": {
|
||||
"components": {
|
||||
"Button": {
|
||||
"replaces": []
|
||||
},
|
||||
"ComplexityBadge": {
|
||||
"replaces": []
|
||||
},
|
||||
"ComplexitySeg": {
|
||||
"replaces": []
|
||||
},
|
||||
"CountBadge": {
|
||||
"replaces": []
|
||||
},
|
||||
"EventItem": {
|
||||
"replaces": []
|
||||
},
|
||||
"GateCard": {
|
||||
"replaces": []
|
||||
},
|
||||
"Input": {
|
||||
"replaces": []
|
||||
},
|
||||
"Panel": {
|
||||
"replaces": []
|
||||
},
|
||||
"QuotaMeter": {
|
||||
"replaces": []
|
||||
},
|
||||
"SectionHead": {
|
||||
"replaces": []
|
||||
},
|
||||
"Select": {
|
||||
"replaces": []
|
||||
},
|
||||
"StatusChip": {
|
||||
"replaces": []
|
||||
},
|
||||
"Textarea": {
|
||||
"replaces": []
|
||||
},
|
||||
"TimelineItem": {
|
||||
"replaces": []
|
||||
},
|
||||
"Toast": {
|
||||
"replaces": []
|
||||
}
|
||||
},
|
||||
"tokens": [
|
||||
"--accent",
|
||||
"--accent-dim",
|
||||
"--amber",
|
||||
"--amber-dim",
|
||||
"--bg",
|
||||
"--bg-deep",
|
||||
"--border-accent-w",
|
||||
"--border-default",
|
||||
"--border-soft",
|
||||
"--border-w",
|
||||
"--cyan",
|
||||
"--cyan-dim",
|
||||
"--ease-base",
|
||||
"--ease-fast",
|
||||
"--faint",
|
||||
"--font-body",
|
||||
"--glow-cyan",
|
||||
"--glow-green",
|
||||
"--glow-red",
|
||||
"--glow-violet",
|
||||
"--green",
|
||||
"--green-dim",
|
||||
"--ink",
|
||||
"--leading-body",
|
||||
"--leading-doc",
|
||||
"--line",
|
||||
"--line-soft",
|
||||
"--mono",
|
||||
"--muted",
|
||||
"--pad-btn",
|
||||
"--pad-btn-xs",
|
||||
"--pad-card",
|
||||
"--pad-chip",
|
||||
"--pad-input",
|
||||
"--panel",
|
||||
"--panel-2",
|
||||
"--radius-dot",
|
||||
"--radius-lg",
|
||||
"--radius-md",
|
||||
"--radius-sm",
|
||||
"--radius-xs",
|
||||
"--red",
|
||||
"--red-dim",
|
||||
"--shadow-modal",
|
||||
"--shadow-pop",
|
||||
"--shadow-toast",
|
||||
"--space-1",
|
||||
"--space-2",
|
||||
"--space-3",
|
||||
"--space-4",
|
||||
"--space-5",
|
||||
"--space-6",
|
||||
"--space-7",
|
||||
"--space-8",
|
||||
"--space-9",
|
||||
"--status-bad",
|
||||
"--status-gate",
|
||||
"--status-go",
|
||||
"--status-run",
|
||||
"--surface-card",
|
||||
"--surface-page",
|
||||
"--surface-rail",
|
||||
"--surface-raised",
|
||||
"--text-2xl",
|
||||
"--text-2xs",
|
||||
"--text-body",
|
||||
"--text-disabled",
|
||||
"--text-lg",
|
||||
"--text-md",
|
||||
"--text-num",
|
||||
"--text-secondary",
|
||||
"--text-sm",
|
||||
"--text-xl",
|
||||
"--text-xs",
|
||||
"--tracking-badge",
|
||||
"--tracking-head",
|
||||
"--tracking-label",
|
||||
"--tracking-logo",
|
||||
"--tracking-tight",
|
||||
"--tracking-widest",
|
||||
"--violet",
|
||||
"--violet-dim"
|
||||
],
|
||||
"tokenKinds": {
|
||||
"--bg": "color",
|
||||
"--bg-deep": "color",
|
||||
"--panel": "color",
|
||||
"--panel-2": "color",
|
||||
"--line": "color",
|
||||
"--line-soft": "color",
|
||||
"--ink": "color",
|
||||
"--muted": "color",
|
||||
"--faint": "color",
|
||||
"--green": "color",
|
||||
"--green-dim": "color",
|
||||
"--violet": "color",
|
||||
"--violet-dim": "color",
|
||||
"--amber": "color",
|
||||
"--amber-dim": "color",
|
||||
"--red": "color",
|
||||
"--red-dim": "color",
|
||||
"--cyan": "color",
|
||||
"--cyan-dim": "color",
|
||||
"--glow-green": "shadow",
|
||||
"--glow-violet": "shadow",
|
||||
"--glow-cyan": "shadow",
|
||||
"--glow-red": "shadow",
|
||||
"--surface-page": "color",
|
||||
"--surface-rail": "color",
|
||||
"--surface-card": "color",
|
||||
"--surface-raised": "color",
|
||||
"--text-body": "font",
|
||||
"--text-secondary": "font",
|
||||
"--text-disabled": "font",
|
||||
"--border-default": "color",
|
||||
"--border-soft": "color",
|
||||
"--accent": "color",
|
||||
"--accent-dim": "color",
|
||||
"--status-go": "color",
|
||||
"--status-gate": "color",
|
||||
"--status-bad": "color",
|
||||
"--status-run": "color",
|
||||
"--mono": "font",
|
||||
"--font-body": "font",
|
||||
"--text-2xs": "font",
|
||||
"--text-xs": "font",
|
||||
"--text-sm": "font",
|
||||
"--text-md": "font",
|
||||
"--text-lg": "font",
|
||||
"--text-xl": "font",
|
||||
"--text-2xl": "font",
|
||||
"--text-num": "font",
|
||||
"--leading-body": "font",
|
||||
"--leading-doc": "font",
|
||||
"--tracking-tight": "font",
|
||||
"--tracking-label": "font",
|
||||
"--tracking-badge": "font",
|
||||
"--tracking-head": "font",
|
||||
"--tracking-logo": "font",
|
||||
"--tracking-widest": "font",
|
||||
"--radius-xs": "radius",
|
||||
"--radius-sm": "radius",
|
||||
"--radius-md": "radius",
|
||||
"--radius-lg": "radius",
|
||||
"--radius-dot": "radius",
|
||||
"--border-w": "spacing",
|
||||
"--border-accent-w": "spacing",
|
||||
"--space-1": "spacing",
|
||||
"--space-2": "spacing",
|
||||
"--space-3": "spacing",
|
||||
"--space-4": "spacing",
|
||||
"--space-5": "spacing",
|
||||
"--space-6": "spacing",
|
||||
"--space-7": "spacing",
|
||||
"--space-8": "spacing",
|
||||
"--space-9": "spacing",
|
||||
"--pad-btn": "spacing",
|
||||
"--pad-btn-xs": "spacing",
|
||||
"--pad-input": "spacing",
|
||||
"--pad-chip": "spacing",
|
||||
"--pad-card": "spacing",
|
||||
"--shadow-pop": "shadow",
|
||||
"--shadow-modal": "shadow",
|
||||
"--shadow-toast": "shadow",
|
||||
"--ease-fast": "other",
|
||||
"--ease-base": "other"
|
||||
},
|
||||
"fontFamilies": [
|
||||
"IBM Plex Mono",
|
||||
"Noto Sans SC"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="4" x2="12" y2="20" stroke="var(--icon-accent, #5fdd7d)"></line><line x1="4" y1="12" x2="20" y2="12" stroke="var(--icon-accent, #5fdd7d)"></line></svg>
|
||||
|
Before Width: | Height: | Size: 326 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="5" x2="19" y2="19"></line><line x1="19" y1="5" x2="5" y2="19"></line></svg>
|
||||
|
Before Width: | Height: | Size: 250 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="12" y2="6"></line><circle cx="15" cy="6" r="2.5" stroke="var(--icon-accent, #5fdd7d)"></circle><line x1="18" y1="6" x2="21" y2="6"></line><line x1="3" y1="12" x2="5.5" y2="12"></line><circle cx="9" cy="12" r="2.5" stroke="var(--icon-accent, #5fdd7d)"></circle><line x1="12.5" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="12.5" y2="18"></line><circle cx="16" cy="18" r="2.5" stroke="var(--icon-accent, #5fdd7d)"></circle><line x1="19.5" y1="18" x2="21" y2="18"></line></svg>
|
||||
|
Before Width: | Height: | Size: 666 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"></rect><rect x="14" y="14" width="7" height="7" rx="1.5"></rect><line x1="10" y1="10" x2="14" y2="14" stroke="var(--icon-accent, #f0b429)"></line></svg>
|
||||
|
Before Width: | Height: | Size: 358 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2 12 7 12 10 5 14 19 17 12 22 12" stroke="var(--icon-accent, #59c8d8)"></polyline></svg>
|
||||
|
Before Width: | Height: | Size: 264 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="12" x2="19" y2="12"></line><polyline points="13 6 19 12 13 18" stroke="var(--icon-accent, #59c8d8)"></polyline></svg>
|
||||
|
Before Width: | Height: | Size: 292 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 L22 21 L2 21 Z"></path><line x1="12" y1="10" x2="12" y2="14" stroke="var(--icon-accent, #b88ef5)"></line><line x1="12" y1="17.5" x2="12" y2="17.51" stroke="var(--icon-accent, #b88ef5)"></line></svg>
|
||||
|
Before Width: | Height: | Size: 371 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9" stroke="var(--icon-accent, #5fdd7d)"></path></svg>
|
||||
|
Before Width: | Height: | Size: 356 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7 a2 2 0 0 1 2-2 h4 l2 3 h8 a2 2 0 0 1 2 2 v8 a2 2 0 0 1-2 2 H5 a2 2 0 0 1-2-2 Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 265 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4 L19 12 L7 20 Z" stroke="var(--icon-accent, #5fdd7d)"></path></svg>
|
||||
|
Before Width: | Height: | Size: 238 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="5 4 5 14 18 14"></polyline><polyline points="13 9 18 14 13 19" stroke="var(--icon-accent, #f0b429)"></polyline></svg>
|
||||
|
Before Width: | Height: | Size: 293 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"></circle><circle cx="12" cy="12" r="3" fill="var(--icon-accent, #59c8d8)" stroke="none"></circle></svg>
|
||||
|
Before Width: | Height: | Size: 291 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5"></circle><line x1="15.5" y1="15.5" x2="21" y2="21" stroke="var(--icon-accent, #5fdd7d)"></line></svg>
|
||||
|
Before Width: | Height: | Size: 295 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5" stroke="var(--icon-accent, #5fdd7d)"></path></svg>
|
||||
|
Before Width: | Height: | Size: 296 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2.5"></rect><polyline points="8 12 11 15 16 9" stroke="var(--icon-accent, #5fdd7d)"></polyline></svg>
|
||||
|
Before Width: | Height: | Size: 304 B |
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Maestro 按钮。直角、等宽字体、hover 抬升或辉光。
|
||||
* @startingPoint section="Components" subtitle="五种变体的等宽直角按钮" viewport="700x180"
|
||||
*/
|
||||
export interface ButtonProps {
|
||||
/** 'default' 描边 · 'solid' 实心绿(主操作) · 'ghost' 无边框 · 'accept' 绿描边(通过) · 'reject' 红描边(驳回) */
|
||||
variant?: 'default' | 'solid' | 'ghost' | 'accept' | 'reject';
|
||||
/** 'xs' 紧凑尺寸(区块头内联动作) */
|
||||
size?: 'xs';
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit';
|
||||
onClick?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export declare function Button(props: ButtonProps): JSX.Element;
|
||||
@@ -1,31 +0,0 @@
|
||||
const css = `
|
||||
.m-btn{font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.08em;background:transparent;color:var(--ink);border:1px solid var(--line);padding:6px 14px;cursor:pointer;transition:all .12s;border-radius:var(--radius-sm,4px);white-space:nowrap}
|
||||
.m-btn:hover{border-color:var(--muted);background:var(--panel-2)}
|
||||
.m-btn--xs{padding:2px 8px;font-size:11px}
|
||||
.m-btn--ghost{border-color:transparent;color:var(--muted)}
|
||||
.m-btn--ghost:hover{color:var(--green);border-color:var(--green-dim);background:transparent}
|
||||
.m-btn--solid{background:var(--green);color:var(--bg-deep);border-color:var(--green)}
|
||||
.m-btn--solid:hover{background:#79ec94;border-color:#79ec94;box-shadow:0 0 14px rgba(95,221,125,.35)}
|
||||
.m-btn--accept{border-color:var(--green-dim);color:var(--green)}
|
||||
.m-btn--accept:hover{background:var(--green);color:var(--bg-deep);border-color:var(--green);box-shadow:0 0 14px rgba(95,221,125,.3)}
|
||||
.m-btn--reject{border-color:var(--red-dim);color:var(--red)}
|
||||
.m-btn--reject:hover{background:var(--red);color:var(--bg-deep);border-color:var(--red);box-shadow:0 0 14px rgba(255,93,93,.3)}
|
||||
.m-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-btn-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-btn-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Button({ variant = 'default', size, disabled, onClick, type = 'button', style, children }) {
|
||||
ensureCss();
|
||||
const cls = ['m-btn'];
|
||||
if (variant !== 'default') cls.push('m-btn--' + variant);
|
||||
if (size === 'xs') cls.push('m-btn--xs');
|
||||
return (
|
||||
<button type={type} className={cls.join(' ')} disabled={disabled} onClick={onClick} style={style}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
Maestro 的按钮:直角等宽,描边为默认形态,实心绿仅用于每屏最多一个主操作;accept/reject 成对出现在审批闸。
|
||||
|
||||
```jsx
|
||||
<Button variant="solid">创建任务</Button>
|
||||
<Button variant="accept">✓ 通过</Button>
|
||||
<Button variant="reject">✕ 驳回</Button>
|
||||
<Button variant="ghost" size="xs">+ 新建</Button>
|
||||
```
|
||||
|
||||
变体:`default`(描边)· `solid`(主操作)· `ghost`(低调内联)· `accept` / `reject`(审批对,hover 填充信号色+辉光)。`size="xs"` 用于区块头内的小动作。disabled = opacity .4。
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 复杂度徽章:HARD(红)/ MED(琥珀)/ EASY(绿),信号色 7% 透明底。
|
||||
*/
|
||||
export interface ComplexityBadgeProps {
|
||||
complexity?: 'hard' | 'medium' | 'easy';
|
||||
}
|
||||
export declare function ComplexityBadge(props: ComplexityBadgeProps): JSX.Element;
|
||||
@@ -1,17 +0,0 @@
|
||||
const css = `
|
||||
.m-cplx{flex:none;display:inline-block;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em;padding:1px 7px;border:1px solid;border-radius:var(--radius-xs,3px)}
|
||||
.m-cplx--hard{color:var(--red);border-color:var(--red-dim);background:rgba(255,93,93,.07)}
|
||||
.m-cplx--medium{color:var(--amber);border-color:var(--amber-dim);background:rgba(240,180,41,.07)}
|
||||
.m-cplx--easy{color:var(--green);border-color:var(--green-dim);background:rgba(95,221,125,.07)}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-cplx-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-cplx-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const LABEL = { hard: 'HARD', medium: 'MED', easy: 'EASY' };
|
||||
|
||||
export function ComplexityBadge({ complexity = 'medium' }) {
|
||||
ensureCss();
|
||||
return <span className={'m-cplx m-cplx--' + complexity}>{LABEL[complexity] || complexity}</span>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
复杂度徽章:任务行右侧的 HARD / MED / EASY 三级标签,红/琥珀/绿 + 7% 透明信号底。
|
||||
|
||||
```jsx
|
||||
<ComplexityBadge complexity="hard" />
|
||||
<ComplexityBadge complexity="easy" />
|
||||
```
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* 顶栏计数徽章:⚠ 待审批(紫脉冲)· ▸ 待执行(绿)· ◉ 执行中(青)· ⛓ 被阻塞(琥珀虚线边)· Σ 总量(中性)。hover 展开文字标签;count 为 0 时灰化。
|
||||
*/
|
||||
export interface CountBadgeProps {
|
||||
kind?: 'gate' | 'ready' | 'run' | 'blocked' | 'total';
|
||||
count?: number;
|
||||
/** 覆盖默认中文标签 */
|
||||
label?: string;
|
||||
title?: string;
|
||||
}
|
||||
export declare function CountBadge(props: CountBadgeProps): JSX.Element;
|
||||
@@ -1,40 +0,0 @@
|
||||
const css = `
|
||||
.m-bdg{display:inline-flex;align-items:center;font-family:var(--mono);font-size:11.5px;line-height:1;letter-spacing:.08em;height:28px;padding:0 10px;cursor:default;white-space:nowrap;border-radius:var(--radius-sm,4px)}
|
||||
.m-bdg .m-bdg-ico{margin-right:5px;font-size:12px;line-height:1;font-family:var(--mono)}
|
||||
.m-bdg .m-bdg-n{font-weight:700}
|
||||
.m-bdg .m-bdg-label{max-width:0;opacity:0;overflow:hidden;transition:max-width .28s ease,opacity .22s ease,margin-left .28s ease}
|
||||
.m-bdg:hover .m-bdg-label{max-width:8em;opacity:1;margin-left:5px}
|
||||
.m-bdg--gate{color:var(--violet);border:1px solid var(--violet-dim)}
|
||||
.m-bdg--gate:not(.m-bdg--zero){animation:maestro-pulse 2.2s infinite}
|
||||
.m-bdg--ready{color:var(--green);border:1px solid var(--green-dim)}
|
||||
.m-bdg--run{color:var(--cyan);border:1px solid var(--cyan-dim)}
|
||||
.m-bdg--run:not(.m-bdg--zero) .m-bdg-ico{animation:maestro-pulse .9s infinite}
|
||||
.m-bdg--blocked{color:var(--amber);border:1px dashed var(--amber-dim)}
|
||||
.m-bdg--total{color:var(--ink);border:1px solid var(--line)}
|
||||
.m-bdg--zero{color:var(--faint);border-color:var(--line-soft);animation:none}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-bdg-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-bdg-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const META = {
|
||||
gate: { ico: '⚠', label: '项待审批' },
|
||||
ready: { ico: '▸', label: '待执行' },
|
||||
run: { ico: '◉', label: '执行中' },
|
||||
blocked: { ico: '⛓', label: '被阻塞' },
|
||||
total: { ico: 'Σ', label: '总量' },
|
||||
};
|
||||
|
||||
export function CountBadge({ kind = 'ready', count = 0, label, title }) {
|
||||
ensureCss();
|
||||
const m = META[kind] || META.ready;
|
||||
const cls = 'm-bdg m-bdg--' + kind + (count === 0 ? ' m-bdg--zero' : '');
|
||||
return (
|
||||
<span className={cls} title={title}>
|
||||
<span className="m-bdg-ico">{m.ico}</span>
|
||||
<span className="m-bdg-n">{count}</span>
|
||||
<span className="m-bdg-label">{label || m.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
顶栏徽章:图标 + 数字,hover 横向展开中文标签。三种语义固定:gate ⚠ 琥珀 / ready ▸ 绿 / run ◉ 青。
|
||||
|
||||
```jsx
|
||||
<CountBadge kind="gate" count={2} />
|
||||
<CountBadge kind="ready" count={5} />
|
||||
<CountBadge kind="run" count={1} />
|
||||
```
|
||||
|
||||
count=0 自动灰化并停掉脉冲动画。
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* 额度/用量计量条:LED 分段小灯 + 同色加粗百分比,用量分级自动变色(≤80% 青 / >80% 琥珀 / >95% 红),已用格带辉光。
|
||||
*/
|
||||
export interface QuotaMeterProps {
|
||||
/** 左侧小标签,如 "5h" / "周" */
|
||||
label?: string;
|
||||
/** 用量百分比 0–100 */
|
||||
pct?: number;
|
||||
/** 右侧弱化说明,如 "3h 8m 后重置" */
|
||||
detail?: string;
|
||||
/** 分段格数,默认 16 */
|
||||
cells?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function QuotaMeter(props: QuotaMeterProps): JSX.Element;
|
||||
@@ -1,24 +0,0 @@
|
||||
// 额度计量条:LED 分段 + 用量分级变色(≤80% 青 / >80% 琥珀 / >95% 红,与产品 .agent-usage 阈值对齐)
|
||||
export function QuotaMeter({ label, pct = 0, detail, cells = 16, style }) {
|
||||
const p = Math.max(0, Math.min(100, pct));
|
||||
const filled = Math.round((p / 100) * cells);
|
||||
const color = p > 95 ? 'var(--red)' : p > 80 ? 'var(--amber)' : 'var(--cyan)';
|
||||
const glow = p > 95 ? 'rgba(255,93,93,.4)' : p > 80 ? 'rgba(240,180,41,.35)' : 'rgba(89,200,216,.4)';
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: 'var(--mono)', ...style }}
|
||||
title={(label ? label + ' · ' : '') + p + '%' + (detail ? ' · ' + detail : '')}>
|
||||
{label ? <span style={{ fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em', flex: 'none' }}>{label}</span> : null}
|
||||
<span style={{ display: 'inline-flex', gap: 2, padding: 3, background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 3 }}>
|
||||
{Array.from({ length: cells }, (_, i) => (
|
||||
<span key={i} style={{
|
||||
width: 4, height: 9, borderRadius: 1,
|
||||
background: i < filled ? color : 'var(--panel-2)',
|
||||
boxShadow: i < filled ? '0 0 5px ' + glow : 'none',
|
||||
}}></span>
|
||||
))}
|
||||
</span>
|
||||
<b style={{ fontSize: 12, color }}>{p}%</b>
|
||||
{detail ? <span style={{ fontSize: 10.5, color: 'var(--faint)' }}>{detail}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
额度计量条:agent 用量/配额的 LED 分段显示,按用量自动变色(青→琥珀→红),已用格带辉光。
|
||||
|
||||
```jsx
|
||||
<QuotaMeter label="5h" pct={50} detail="3h 8m 后重置" />
|
||||
<QuotaMeter label="周" pct={78} detail="16h 38m 后重置" />
|
||||
<QuotaMeter pct={94} cells={24} />
|
||||
```
|
||||
|
||||
`pct` 决定填充与颜色(≥70% 琥珀、≥90% 红);`detail` 放重置时间等弱化说明;`cells` 调分段密度。
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 区块标题:▍色块标记 + 全大写宽字距标题,可挂右侧动作。
|
||||
*/
|
||||
export interface SectionHeadProps {
|
||||
/** ▍标记颜色:绿(默认)/ 紫(审批)/ 琥珀(警示)/ 青(agent) */
|
||||
mark?: 'green' | 'violet' | 'amber' | 'cyan';
|
||||
title?: React.ReactNode;
|
||||
/** 右侧动作(通常是 ghost xs 按钮) */
|
||||
action?: React.ReactNode;
|
||||
/** 滚动容器内吸顶 + 底缘淡出 */
|
||||
sticky?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function SectionHead(props: SectionHeadProps): JSX.Element;
|
||||
@@ -1,17 +0,0 @@
|
||||
export function SectionHead({ mark = 'green', title, action, sticky, style }) {
|
||||
const markColor = { green: 'var(--green)', amber: 'var(--amber)', cyan: 'var(--cyan)', violet: 'var(--violet)' }[mark] || 'var(--green)';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.22em',
|
||||
color: 'var(--muted)', textTransform: 'uppercase',
|
||||
padding: '18px 0 10px',
|
||||
...(sticky ? { position: 'sticky', top: 0, zIndex: 5, background: 'linear-gradient(var(--bg) 75%, transparent)' } : {}),
|
||||
...style,
|
||||
}}>
|
||||
<span style={{ color: markColor, flex: 'none' }}>▍</span>
|
||||
<span style={{ whiteSpace: 'nowrap', display: 'inline-flex', alignItems: 'center', minWidth: 0 }}>{title}</span>
|
||||
{action ? <span style={{ marginLeft: 'auto', flex: 'none' }}>{action}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
区块标题:▍ + 全大写 .22em 字距小标题,Maestro 每个区块的开头。mark 颜色表达区块语义(amber=审批闸、cyan=agent)。
|
||||
|
||||
```jsx
|
||||
<SectionHead title="任务树" action={<Button variant="ghost" size="xs">+ 新建任务</Button>} />
|
||||
<SectionHead mark="amber" title="审批闸 · 等待裁决" />
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 任务状态 chip:圆点 + 压缩状态词,按状态机分组上色(等待类脉冲动画)。
|
||||
*/
|
||||
export interface StatusChipProps {
|
||||
/** 状态机 16 态之一;决定颜色组与动画 */
|
||||
status?: 'init' | 'analyzing' | 'plan_review' | 'decomposed' | 'speccing' | 'spec_review'
|
||||
| 'ready' | 'blocked' | 'queued' | 'executing' | 'exec_review' | 'failed'
|
||||
| 'needs_attention' | 'done' | 'paused' | 'cancelled';
|
||||
/** 覆盖默认中文标签 */
|
||||
label?: string;
|
||||
}
|
||||
export declare function StatusChip(props: StatusChipProps): JSX.Element;
|
||||
@@ -1,46 +0,0 @@
|
||||
const STATUS_LABEL = {
|
||||
init: '新建', analyzing: '分析拆解中', plan_review: '待确认拆解',
|
||||
decomposed: '已拆解', speccing: '写方案中', spec_review: '待确认方案',
|
||||
ready: '可执行', blocked: '被依赖阻塞', queued: '排队中',
|
||||
executing: '执行中', exec_review: '待审/合', failed: '失败',
|
||||
needs_attention: '需人工', done: '完成', paused: '暂停', cancelled: '取消',
|
||||
};
|
||||
const STATUS_GROUP = {
|
||||
init: 'idle', analyzing: 'work', speccing: 'work',
|
||||
plan_review: 'gate', spec_review: 'gate', exec_review: 'gate',
|
||||
decomposed: 'container', ready: 'go', queued: 'go', executing: 'run',
|
||||
blocked: 'hold', paused: 'hold', failed: 'bad', needs_attention: 'bad',
|
||||
done: 'done', cancelled: 'dead',
|
||||
};
|
||||
const css = `
|
||||
.m-chip{flex:none;display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:11px;padding:1px 8px;border:1px solid var(--line);color:var(--muted);white-space:nowrap;border-radius:var(--radius-xs,3px)}
|
||||
.m-chip::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}
|
||||
.m-chip--work{color:var(--cyan);border-color:var(--cyan-dim)}
|
||||
.m-chip--gate{color:var(--violet);border-color:var(--violet-dim)}
|
||||
.m-chip--gate::before{animation:maestro-pulse 1.4s infinite}
|
||||
.m-chip--container{color:#9bb4c8;border-color:#2c3c4a}
|
||||
.m-chip--go{color:var(--green);border-color:var(--green-dim)}
|
||||
.m-chip--run{color:var(--cyan);border-color:var(--cyan);box-shadow:0 0 10px rgba(89,200,216,.25)}
|
||||
.m-chip--run::before{animation:maestro-pulse .8s infinite}
|
||||
.m-chip--hold{color:var(--faint)}
|
||||
.m-chip--bad{color:var(--red);border-color:var(--red-dim)}
|
||||
.m-chip--bad::before{animation:maestro-pulse 1s infinite}
|
||||
.m-chip--done{color:var(--bg-deep);background:var(--green);border-color:var(--green);font-weight:700}
|
||||
.m-chip--dead{color:var(--faint);text-decoration:line-through}
|
||||
.m-chip--dead::before{background:var(--faint)}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-chip-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-chip-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function StatusChip({ status = 'init', label }) {
|
||||
ensureCss();
|
||||
const group = STATUS_GROUP[status] || 'idle';
|
||||
return (
|
||||
<span className={'m-chip m-chip--' + group}>
|
||||
{label || STATUS_LABEL[status] || status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
状态 chip:把 16 个状态机状态渲染成「圆点 + 中文短词」,颜色按组(gate=琥珀脉冲、run=青辉光、done=实心绿、bad=红脉冲)。
|
||||
|
||||
```jsx
|
||||
<StatusChip status="executing" />
|
||||
<StatusChip status="exec_review" />
|
||||
<StatusChip status="done" />
|
||||
```
|
||||
|
||||
传 `status` 即可,标签与颜色自动对应;`label` 可覆盖文字。等待审批/执行中/失败的圆点会脉冲。
|
||||
@@ -1,53 +0,0 @@
|
||||
<!-- @dsCard group="Components" viewport="700x280" name="Core · 按钮与徽章" subtitle="Button 五变体 · StatusChip · ComplexityBadge · CountBadge · QuotaMeter · SectionHead" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../../styles.css">
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script src="../../_ds_bundle.js"></script>
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:14px 16px}</style>
|
||||
</head><body><div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { Button, StatusChip, ComplexityBadge, CountBadge, SectionHead, QuotaMeter } = window.MaestroDesignSystem_a6a290;
|
||||
const row = { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' };
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<div style={row}>
|
||||
<Button>默认</Button>
|
||||
<Button variant="solid">创建任务</Button>
|
||||
<Button variant="accept">✓ 通过</Button>
|
||||
<Button variant="reject">✕ 驳回</Button>
|
||||
<Button variant="ghost">+ 新建</Button>
|
||||
<Button size="xs">⟳ 同步 todo</Button>
|
||||
<Button disabled>禁用</Button>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<StatusChip status="ready" />
|
||||
<StatusChip status="executing" />
|
||||
<StatusChip status="exec_review" />
|
||||
<StatusChip status="blocked" />
|
||||
<StatusChip status="failed" />
|
||||
<StatusChip status="done" />
|
||||
<StatusChip status="cancelled" />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<ComplexityBadge complexity="hard" />
|
||||
<ComplexityBadge complexity="medium" />
|
||||
<ComplexityBadge complexity="easy" />
|
||||
<span style={{ width: 10 }}></span>
|
||||
<CountBadge kind="gate" count={2} />
|
||||
<CountBadge kind="ready" count={5} />
|
||||
<CountBadge kind="run" count={1} />
|
||||
<CountBadge kind="blocked" count={1} />
|
||||
<CountBadge kind="total" count={5} />
|
||||
<CountBadge kind="run" count={0} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<QuotaMeter label="5h" pct={50} detail="3h 8m 后重置" />
|
||||
<QuotaMeter label="周" pct={78} detail="16h 38m 后重置" />
|
||||
<QuotaMeter pct={94} />
|
||||
</div>
|
||||
<SectionHead title="任务树" style={{ padding: '4px 0 0' }} action={<Button variant="ghost" size="xs">+ 新建任务</Button>} />
|
||||
</div>
|
||||
);
|
||||
</script></body></html>
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 复杂度分段选择:HARD / MEDIUM / EASY 三段,选中段填充对应信号色 dim 底。
|
||||
* 可选第四段 auto(智能 · 由模型决定复杂度),青色信号。
|
||||
*/
|
||||
export interface ComplexitySegProps {
|
||||
value?: 'hard' | 'medium' | 'easy' | 'auto';
|
||||
defaultValue?: 'hard' | 'medium' | 'easy' | 'auto';
|
||||
onChange?: (value: 'hard' | 'medium' | 'easy' | 'auto') => void;
|
||||
/** 追加 auto 段(青色):由模型决定复杂度 */
|
||||
includeAuto?: boolean;
|
||||
/** auto 段文字,默认 'AUTO'(可传本地化「智能」) */
|
||||
autoLabel?: string;
|
||||
/** 覆盖各段文字:{ hard, medium, easy, auto } 局部即可 */
|
||||
labels?: Partial<Record<'hard' | 'medium' | 'easy' | 'auto', string>>;
|
||||
}
|
||||
export declare function ComplexitySeg(props: ComplexitySegProps): JSX.Element;
|
||||
@@ -1,32 +0,0 @@
|
||||
const css = `
|
||||
.m-seg{display:inline-flex;border:1px solid var(--line);font-family:var(--mono);border-radius:var(--radius-sm,4px);overflow:hidden}
|
||||
.m-seg button{font-family:var(--mono);padding:7px 13px;cursor:pointer;font-size:11px;font-weight:700;letter-spacing:.12em;color:var(--faint);background:transparent;border:none;border-radius:0}
|
||||
.m-seg button + button{border-left:1px solid var(--line)}
|
||||
.m-seg button.on-hard{background:var(--red-dim);color:var(--red)}
|
||||
.m-seg button.on-medium{background:var(--amber-dim);color:var(--amber)}
|
||||
.m-seg button.on-easy{background:var(--green-dim);color:var(--green)}
|
||||
.m-seg button.on-auto{background:var(--cyan-dim);color:var(--cyan)}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-seg-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-seg-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const BASE_OPTS = [['hard', 'HARD'], ['medium', 'MEDIUM'], ['easy', 'EASY']];
|
||||
|
||||
export function ComplexitySeg({ value, defaultValue = 'medium', onChange, includeAuto, autoLabel = 'AUTO', labels }) {
|
||||
ensureCss();
|
||||
const [inner, setInner] = React.useState(defaultValue);
|
||||
const cur = value !== undefined ? value : inner;
|
||||
const opts = includeAuto ? [...BASE_OPTS, ['auto', autoLabel]] : BASE_OPTS;
|
||||
return (
|
||||
<span className="m-seg">
|
||||
{opts.map(([v, lab]) => (
|
||||
<button key={v} type="button" className={cur === v ? 'on-' + v : ''}
|
||||
onClick={() => { setInner(v); if (onChange) onChange(v); }}>
|
||||
{(labels && labels[v]) || lab}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
复杂度三段选择器,建任务表单用。选中段填充信号色 dim 底(红/琥珀/绿)。
|
||||
|
||||
```jsx
|
||||
<ComplexitySeg defaultValue="medium" onChange={(v) => setCplx(v)} />
|
||||
```
|
||||
|
||||
可加第四段 `auto`(青色,「智能」= 由模型决定复杂度)。`autoLabel` 传本地化文字,`labels` 覆盖任意段:
|
||||
|
||||
```jsx
|
||||
<ComplexitySeg defaultValue="auto" includeAuto autoLabel="智能" />
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* 文本/数字输入框:深底直角,focus 绿 dim 边框 + 1px ring。可带顶部小标签与红色必填星。
|
||||
*/
|
||||
export interface InputProps {
|
||||
/** 顶部 11px 字距标签 */
|
||||
label?: string;
|
||||
/** 标签后追加红色 * */
|
||||
required?: boolean;
|
||||
type?: 'text' | 'number';
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
width?: number | string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Input(props: InputProps): JSX.Element;
|
||||
@@ -1,28 +0,0 @@
|
||||
const css = `
|
||||
.m-label{display:flex;flex-direction:column;gap:4px;font-family:var(--mono);font-size:11px;color:var(--muted);letter-spacing:.08em}
|
||||
.m-input{font-family:var(--mono);font-size:13px;background:var(--bg-deep);color:var(--ink);border:1px solid var(--line);padding:7px 10px;outline:none;transition:border-color .12s;border-radius:var(--radius-sm,4px)}
|
||||
.m-input:focus{border-color:var(--green-dim);box-shadow:0 0 0 1px var(--green-dim)}
|
||||
.m-input::placeholder{color:var(--faint)}
|
||||
.m-req{color:var(--red)}
|
||||
`;
|
||||
export function ensureFieldCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-field-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-field-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Input({ label, required, type = 'text', placeholder, value, defaultValue, onChange, width, style }) {
|
||||
ensureFieldCss();
|
||||
const ctrl = (
|
||||
<input className="m-input" type={type} placeholder={placeholder} value={value}
|
||||
defaultValue={defaultValue} style={{ width, ...style }} autoComplete="off"
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined} readOnly={value !== undefined && !onChange} />
|
||||
);
|
||||
if (!label) return ctrl;
|
||||
return (
|
||||
<label className="m-label">
|
||||
<span>{label}{required ? <span className="m-req"> *</span> : null}</span>
|
||||
{ctrl}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
输入框:bg-deep 底、直角、focus 时绿 dim 描边+ring。占位文案给真实示例(如 `/path/to/repo`)。
|
||||
|
||||
```jsx
|
||||
<Input label="名称" required placeholder="my-project" />
|
||||
<Input label="校验命令" placeholder="npm test(可空)" />
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* 下拉选择(自绘弹层,非原生 select):触发器与 Input 同形制,弹出磷光风格选项菜单——当前项绿色 ▍ 标记 + 抬升底。选项文案用「值 · 中文」格式(如 "manual · 手动")。
|
||||
*/
|
||||
export interface SelectProps {
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
options?: Array<string | { value: string; label: string }>;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Select(props: SelectProps): JSX.Element;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ensureFieldCss } from './Input.jsx';
|
||||
|
||||
const css = `
|
||||
.m-select{position:relative;display:inline-flex;flex-direction:column;min-width:120px}
|
||||
.m-select-trigger{font-family:var(--mono);font-size:13px;background:var(--bg-deep);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-sm,4px);padding:7px 10px;cursor:pointer;outline:none;transition:border-color .12s;display:flex;align-items:center;gap:10px;text-align:left;width:100%}
|
||||
.m-select-trigger:hover{border-color:var(--muted)}
|
||||
.m-select-trigger.open,.m-select-trigger:focus{border-color:var(--green-dim);box-shadow:0 0 0 1px var(--green-dim)}
|
||||
.m-select-value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.m-select-caret{flex:none;font-size:8px;color:var(--faint);transition:transform .12s}
|
||||
.m-select-trigger.open .m-select-caret{transform:rotate(180deg)}
|
||||
.m-select-menu{position:absolute;top:calc(100% + 5px);left:0;right:0;z-index:80;display:grid;background:var(--bg-deep);border:1px solid var(--line);border-radius:var(--radius-md,6px);padding:4px;box-shadow:0 10px 30px rgba(0,0,0,.65);animation:maestro-rise .12s ease both;max-height:260px;overflow-y:auto}
|
||||
.m-select-opt{font-family:var(--mono);font-size:12.5px;text-align:left;background:transparent;color:var(--ink);border:none;border-radius:4px;padding:7px 10px 7px 8px;cursor:pointer;display:flex;align-items:center;gap:7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.m-select-opt:hover{background:var(--panel)}
|
||||
.m-select-opt.on{background:var(--panel-2);color:var(--green);font-weight:700}
|
||||
.m-select-mark{flex:none;width:8px;color:var(--green);font-size:10px}
|
||||
`;
|
||||
function ensureSelectCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-select-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-select-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Select({ label, required, options = [], value, defaultValue, onChange, style }) {
|
||||
ensureFieldCss();
|
||||
ensureSelectCss();
|
||||
const opts = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
||||
const [inner, setInner] = React.useState(defaultValue !== undefined ? defaultValue : (opts[0] ? opts[0].value : ''));
|
||||
const cur = value !== undefined ? value : inner;
|
||||
const curOpt = opts.find((o) => o.value === cur);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
||||
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
|
||||
}, [open]);
|
||||
const pick = (v) => {
|
||||
setInner(v); setOpen(false);
|
||||
if (onChange) onChange(v);
|
||||
};
|
||||
const ctrl = (
|
||||
<span className="m-select" ref={ref} style={style}>
|
||||
<button type="button" className={'m-select-trigger' + (open ? ' open' : '')} onClick={() => setOpen(!open)}>
|
||||
<span className="m-select-value">{curOpt ? curOpt.label : cur}</span>
|
||||
<span className="m-select-caret">▼</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<span className="m-select-menu">
|
||||
{opts.map((o) => (
|
||||
<button type="button" key={o.value} className={'m-select-opt' + (o.value === cur ? ' on' : '')}
|
||||
onClick={() => pick(o.value)}>
|
||||
<span className="m-select-mark">{o.value === cur ? '▍' : ''}</span>{o.label}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
if (!label) return ctrl;
|
||||
return (
|
||||
<label className="m-label">
|
||||
<span>{label}{required ? <span className="m-req"> *</span> : null}</span>
|
||||
{ctrl}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
下拉选择,与 Input 同形制。选项标签遵循「英文值 · 中文说明」惯例。
|
||||
|
||||
```jsx
|
||||
<Select label="工作模式" options={[
|
||||
{ value: 'manual', label: 'manual · 手动' },
|
||||
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
|
||||
]} />
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 多行文本:纵向可拉伸。danger 形态(红 dim 边)用于驳回意见(reject 必填理由)。
|
||||
*/
|
||||
export interface TextareaProps {
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
minHeight?: number;
|
||||
/** 红 dim 边框 — 驳回意见输入 */
|
||||
danger?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Textarea(props: TextareaProps): JSX.Element;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ensureFieldCss } from './Input.jsx';
|
||||
|
||||
export function Textarea({ label, required, placeholder, value, defaultValue, onChange, minHeight = 72, danger, style }) {
|
||||
ensureFieldCss();
|
||||
const ctrl = (
|
||||
<textarea className="m-input" placeholder={placeholder} value={value} defaultValue={defaultValue}
|
||||
style={{ resize: 'vertical', minHeight, width: '100%', ...(danger ? { borderColor: 'var(--red-dim)' } : {}), ...style }}
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined} readOnly={value !== undefined && !onChange} />
|
||||
);
|
||||
if (!label) return ctrl;
|
||||
return (
|
||||
<label className="m-label" style={{ width: '100%' }}>
|
||||
<span>{label}{required ? <span className="m-req"> *</span> : null}</span>
|
||||
{ctrl}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
多行文本框。`danger` 形态用于驳回意见——Maestro 规定 reject 必须带改进意见。
|
||||
|
||||
```jsx
|
||||
<Textarea label="改动方案" placeholder="改动内容 + 为什么这么做" />
|
||||
<Textarea danger placeholder="驳回理由(必填)" minHeight={56} />
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
<!-- @dsCard group="Components" viewport="700x240" name="Forms · 表单控件" subtitle="Input · Select · Textarea(danger) · ComplexitySeg(含智能段)" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../../styles.css">
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script src="../../_ds_bundle.js"></script>
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:14px 16px}</style>
|
||||
</head><body><div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { Input, Select, Textarea, ComplexitySeg } = window.MaestroDesignSystem_a6a290;
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<Input label="名称" required placeholder="my-project" />
|
||||
<Input label="仓库路径" required placeholder="/path/to/repo" width={180} />
|
||||
<Select label="工作模式" options={[
|
||||
{ value: 'manual', label: 'manual · 手动' },
|
||||
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
|
||||
{ value: 'auto-approved', label: 'auto-approved · 自动执行已批准' },
|
||||
]} />
|
||||
<ComplexitySeg defaultValue="auto" includeAuto autoLabel="智能" />
|
||||
</div>
|
||||
<Textarea danger placeholder="驳回理由(必填)" minHeight={56} />
|
||||
</div>
|
||||
);
|
||||
</script></body></html>
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 事件流条目:左侧 5px 方形色标 + 事件类型 + 时间 + 详情,类型决定颜色。放在 ul 里使用。
|
||||
*/
|
||||
export interface EventItemProps {
|
||||
/** 事件类型 → 中文标签 + 色标颜色 */
|
||||
type?: 'task.created' | 'task.updated' | 'status.changed' | 'approval.requested'
|
||||
| 'approval.granted' | 'approval.rejected' | 'run.started' | 'run.finished' | 'project.synced';
|
||||
/** 右对齐时间文字,如 "14:02:31" */
|
||||
time?: string;
|
||||
detail?: React.ReactNode;
|
||||
/** 覆盖默认类型标签 */
|
||||
label?: string;
|
||||
}
|
||||
export declare function EventItem(props: EventItemProps): JSX.Element;
|
||||
@@ -1,38 +0,0 @@
|
||||
const css = `
|
||||
.m-ev{font-family:var(--mono);padding:7px 0 7px 12px;border-bottom:1px solid var(--line-soft);position:relative;font-size:11.5px;color:var(--ink);animation:maestro-rise .25s ease both;list-style:none}
|
||||
.m-ev::before{content:'';position:absolute;left:0;top:13px;width:5px;height:5px;background:var(--m-ev-c,var(--muted));border-radius:1.5px}
|
||||
.m-ev-head{display:flex;gap:8px;align-items:baseline}
|
||||
.m-ev-type{font-weight:600;color:var(--m-ev-c,var(--ink));letter-spacing:.04em}
|
||||
.m-ev-time{color:var(--faint);font-size:10.5px;margin-left:auto;flex:none}
|
||||
.m-ev-detail{color:var(--muted);margin-top:1px;word-break:break-word}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-ev-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-ev-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const TYPE_META = {
|
||||
'task.created': ['任务创建', 'var(--green)'],
|
||||
'task.updated': ['任务更新', 'var(--muted)'],
|
||||
'status.changed': ['状态变更', 'var(--cyan)'],
|
||||
'approval.requested': ['请求审批', 'var(--violet)'],
|
||||
'approval.granted': ['审批通过', 'var(--green)'],
|
||||
'approval.rejected': ['审批驳回', 'var(--red)'],
|
||||
'run.started': ['运行开始', 'var(--cyan)'],
|
||||
'run.finished': ['运行结束', 'var(--cyan)'],
|
||||
'project.synced': ['todo 同步', 'var(--green)'],
|
||||
};
|
||||
|
||||
export function EventItem({ type = 'task.updated', time, detail, label }) {
|
||||
ensureCss();
|
||||
const [lab, color] = TYPE_META[type] || [type, 'var(--muted)'];
|
||||
return (
|
||||
<li className="m-ev" style={{ '--m-ev-c': color }}>
|
||||
<div className="m-ev-head">
|
||||
<span className="m-ev-type">{label || lab}</span>
|
||||
{time ? <span className="m-ev-time">{time}</span> : null}
|
||||
</div>
|
||||
{detail ? <div className="m-ev-detail">{detail}</div> : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
事件流条目:右栏审计流的一行——方形色标(注意:方的,不是圆点)+ 类型 + 时间 + 详情。
|
||||
|
||||
```jsx
|
||||
<ul style={{ padding: 0 }}>
|
||||
<EventItem type="approval.rejected" time="14:02" detail="重构 sync 模块 ↳ 缺少回滚方案" />
|
||||
<EventItem type="run.started" time="14:05" detail="executor · worktree wt-127" />
|
||||
</ul>
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 审批闸卡片:琥珀 dim 边 + 实心琥珀闸标签 + 产出文档块 + accept/reject 动作脚。配套 GateStripe 斜纹警示条。
|
||||
*/
|
||||
export interface GateCardProps {
|
||||
/** 闸类型颜色:plan 拆解评审 · spec 方案评审 · exec 结果评审 */
|
||||
gate?: 'plan' | 'spec' | 'exec';
|
||||
/** 覆盖闸标签文字(多语言场景) */
|
||||
kindLabel?: string;
|
||||
title?: React.ReactNode;
|
||||
/** 头部右侧 meta 文字 */
|
||||
meta?: React.ReactNode;
|
||||
/** 文档块上方 .18em 字距小标签,如 "改动方案(SPEC)" */
|
||||
docLabel?: string;
|
||||
/** 产出文本,渲染进 pre.doc(左侧绿 dim 强调边) */
|
||||
doc?: string;
|
||||
/** 底部动作行(accept / reject 按钮对) */
|
||||
actions?: React.ReactNode;
|
||||
/** 折叠时隐藏正文与动作脚,仅留头部 */
|
||||
collapsed?: boolean;
|
||||
/** 显示头部右上的折叠/展开 ▾▸ 按钮 */
|
||||
foldable?: boolean;
|
||||
onToggleFold?: () => void;
|
||||
foldTitle?: string;
|
||||
/** 头部右侧自定义元素(折叠按钮左侧) */
|
||||
headerExtra?: React.ReactNode;
|
||||
/** 头部双击回调(如全屏阅读) */
|
||||
onHeaderDoubleClick?: () => void;
|
||||
/** 头部/整卡 title 提示(如「双击全屏」) */
|
||||
headerTitle?: string;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function GateCard(props: GateCardProps): JSX.Element;
|
||||
@@ -1,61 +0,0 @@
|
||||
const css = `
|
||||
.m-gate{background:var(--panel);border:1px solid var(--violet-dim);font-family:var(--mono);color:var(--ink);animation:maestro-rise .25s ease both;border-radius:var(--radius-md,6px);overflow:hidden}
|
||||
.m-gate-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line-soft)}
|
||||
.m-gate--collapsed .m-gate-head{border-bottom:none}
|
||||
.m-gate-hx{margin-left:auto;display:flex;align-items:center;gap:6px;flex:none}
|
||||
.m-gate-fold{font-family:var(--mono);font-size:12px;line-height:1;cursor:pointer;background:transparent;color:var(--muted);border:1px solid var(--line);border-radius:var(--radius-sm,4px);padding:3px 7px;transition:color .12s,border-color .12s}
|
||||
.m-gate-fold:hover{color:var(--violet);border-color:var(--violet-dim)}
|
||||
.m-gate-kind{font-size:10.5px;font-weight:700;letter-spacing:.18em;color:var(--bg);background:var(--violet);padding:2px 8px;border-radius:var(--radius-xs,3px)}
|
||||
.m-gate-title{font-weight:600;font-size:13px}
|
||||
.m-gate-body{padding:12px 14px;font-size:12.5px}
|
||||
.m-gate-actions{display:flex;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--line-soft)}
|
||||
.m-gate-doclabel{font-size:10.5px;color:var(--muted);letter-spacing:.18em;margin:8px 0 4px}
|
||||
.m-gate-doclabel:first-child{margin-top:0}
|
||||
.m-gate-doc{background:var(--bg-deep);border:1px solid var(--line-soft);border-left:2px solid var(--green-dim);padding:10px 12px;font-family:var(--mono);font-size:12.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto;color:var(--ink);margin:0;border-radius:var(--radius-sm,4px)}
|
||||
.m-gate-stripe{height:5px;background:repeating-linear-gradient(-45deg,var(--violet) 0 9px,transparent 9px 18px);opacity:.8;border-radius:3px}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-gate-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-gate-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const GATE_LABEL = { plan: '拆解评审', spec: '方案评审', exec: '结果评审' };
|
||||
|
||||
export function GateCard({ gate = 'spec', kindLabel, title, meta, docLabel, doc, actions, children, style, collapsed, foldable, onToggleFold, foldTitle, headerExtra, onHeaderDoubleClick, headerTitle }) {
|
||||
ensureCss();
|
||||
return (
|
||||
<div className={'m-gate' + (collapsed ? ' m-gate--collapsed' : '')} style={style} onDoubleClick={onHeaderDoubleClick}>
|
||||
<div className="m-gate-head" title={headerTitle}>
|
||||
<span className="m-gate-kind">{kindLabel || GATE_LABEL[gate] || gate}</span>
|
||||
<span className="m-gate-title">{title}</span>
|
||||
{meta ? <span style={{ fontSize: 11, color: 'var(--muted)' }}>{meta}</span> : null}
|
||||
{(headerExtra || foldable) ? (
|
||||
<span className="m-gate-hx">
|
||||
{headerExtra}
|
||||
{foldable ? (
|
||||
<button type="button" className="m-gate-fold" title={foldTitle}
|
||||
onClick={(e) => { e.stopPropagation(); onToggleFold && onToggleFold(); }}>
|
||||
{collapsed ? '▸' : '▾'}
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{!collapsed ? (
|
||||
<React.Fragment>
|
||||
<div className="m-gate-body">
|
||||
{docLabel ? <div className="m-gate-doclabel">{docLabel}</div> : null}
|
||||
{doc ? <pre className="m-gate-doc">{doc}</pre> : null}
|
||||
{children}
|
||||
</div>
|
||||
{actions ? <div className="m-gate-actions">{actions}</div> : null}
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GateStripe() {
|
||||
ensureCss();
|
||||
return <div className="m-gate-stripe"></div>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
审批闸卡片——Maestro 的核心交互单元。琥珀边框 + 实心琥珀闸标签,体内放 agent 产出文档,脚部放 accept/reject 按钮对(reject 必填理由)。
|
||||
|
||||
```jsx
|
||||
<GateStripe />
|
||||
<GateCard gate="spec" title="重构 sync 模块" meta="maestro · MED"
|
||||
docLabel="改动方案(SPEC)" doc={specText}
|
||||
actions={<><Button variant="accept">✓ 通过</Button><Button variant="reject">✕ 驳回</Button></>} />
|
||||
```
|
||||
|
||||
`GateStripe` 是审批区顶部的琥珀斜纹警示条,整区一条即可。
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 基础面板:--panel 底 + 1px --line 边,直角无阴影。表单、配置区的容器。
|
||||
*/
|
||||
export interface PanelProps {
|
||||
children?: React.ReactNode;
|
||||
padding?: string | number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Panel(props: PanelProps): JSX.Element;
|
||||
@@ -1,10 +0,0 @@
|
||||
export function Panel({ children, padding = '14px', style }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md, 6px)',
|
||||
fontFamily: 'var(--mono)', color: 'var(--ink)', padding, ...style,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
基础面板容器:panel 底 + line 边 + 直角。无阴影——阴影只属于浮层。
|
||||
|
||||
```jsx
|
||||
<Panel><SectionHead title="新建项目" style={{ padding: 0 }} /> …表单… </Panel>
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* 状态流转时间线:左侧 2px 竖线 + 圆点的只读事件序列,用于归档详情/任务历史。
|
||||
*/
|
||||
export interface TimelineItem {
|
||||
/** 时间文字,如 "2026-06-12 14:02:31" */
|
||||
time: string;
|
||||
/** 事件文字,如 "执行中 → 待审/合" */
|
||||
text: string;
|
||||
/** 操作者/来源,如 "编排器 · 依赖满足自动放行" */
|
||||
who?: string;
|
||||
/** 圆点颜色(CSS 值),默认 --muted */
|
||||
color?: string;
|
||||
}
|
||||
export interface TimelineProps {
|
||||
items?: TimelineItem[];
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Timeline(props: TimelineProps): JSX.Element;
|
||||
@@ -1,28 +0,0 @@
|
||||
const css = `
|
||||
.m-tl{display:grid;font-family:var(--mono)}
|
||||
.m-tl-item{display:flex;gap:12px;align-items:baseline;padding:5px 0 5px 14px;font-size:12px;border-left:2px solid var(--line);position:relative;color:var(--ink)}
|
||||
.m-tl-item::before{content:'';position:absolute;left:-4px;top:11px;width:6px;height:6px;border-radius:50%;background:var(--m-tl-c,var(--muted))}
|
||||
.m-tl-time{flex:none;color:var(--faint);font-size:11px;min-width:130px}
|
||||
.m-tl-text{color:var(--ink)}
|
||||
.m-tl-who{color:var(--muted);font-size:11px}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-tl-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-tl-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Timeline({ items = [], style }) {
|
||||
ensureCss();
|
||||
return (
|
||||
<div className="m-tl" style={style}>
|
||||
{items.map((it, i) => (
|
||||
<div key={i} className="m-tl-item" style={it.color ? { '--m-tl-c': it.color } : null}>
|
||||
<span className="m-tl-time">{it.time}</span>
|
||||
<span className="m-tl-text">{it.text}</span>
|
||||
{it.who ? <span className="m-tl-who">{it.who}</span> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
状态流转时间线:归档详情/任务历史的只读事件序列——左竖线 + 圆点 + 时间/文字/操作者三段。
|
||||
|
||||
```jsx
|
||||
<Timeline items={[
|
||||
{ time: '06-12 05:58', text: '审批驳回(方案评审):缺少回滚方案', who: '审批人 you', color: 'var(--red)' },
|
||||
{ time: '06-12 06:10', text: '写方案中 → 待确认方案', who: '编排器' },
|
||||
{ time: '06-12 06:31', text: '审批通过(方案评审)', color: 'var(--green)' },
|
||||
]} />
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Toast 提示:深底 + 信号色边框与文字(ok/err/warn),底部居中堆叠,8px 上移入场。
|
||||
*/
|
||||
export interface ToastProps {
|
||||
/** 无 kind = 中性 ink;ok 绿 · err 红 · warn 琥珀 */
|
||||
kind?: 'ok' | 'err' | 'warn';
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Toast(props: ToastProps): JSX.Element;
|
||||
@@ -1,17 +0,0 @@
|
||||
const css = `
|
||||
.m-toast{font-family:var(--mono);font-size:12.5px;background:var(--bg-deep);border:1px solid var(--line);color:var(--ink);padding:9px 16px;max-width:70vw;animation:m-toast-in .18s ease both;box-shadow:0 8px 30px rgba(0,0,0,.55);border-radius:var(--radius-md,6px)}
|
||||
.m-toast--err{border-color:var(--red);color:var(--red)}
|
||||
.m-toast--ok{border-color:var(--green-dim);color:var(--green)}
|
||||
.m-toast--warn{border-color:var(--amber-dim);color:var(--amber)}
|
||||
@keyframes m-toast-in{from{opacity:0;transform:translateY(8px)}}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-toast-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-toast-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Toast({ kind, children, style }) {
|
||||
ensureCss();
|
||||
return <div className={'m-toast' + (kind ? ' m-toast--' + kind : '')} style={style}>{children}</div>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
Toast:屏幕底部居中堆叠的提示条,深底 + 信号色描边同色文字。文案短促:「已通过」「驳回需填理由」。
|
||||
|
||||
```jsx
|
||||
<Toast kind="ok">已通过 · 合并到 main</Toast>
|
||||
<Toast kind="err">驳回必须填写改进意见</Toast>
|
||||
```
|
||||
@@ -1,46 +0,0 @@
|
||||
<!-- @dsCard group="Components" viewport="700x400" name="Surfaces · 面板与反馈" subtitle="GateCard 审批闸 · Toast · EventItem 事件流 · Timeline 时间线 · Panel" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../../styles.css">
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script src="../../_ds_bundle.js"></script>
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:14px 16px}</style>
|
||||
</head><body><div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { GateCard, GateStripe, Toast, EventItem, Button, Timeline } = window.MaestroDesignSystem_a6a290;
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 240px', gap: 14 }}>
|
||||
<div>
|
||||
<GateStripe />
|
||||
<GateCard gate="spec" title="重构 sync 模块" meta="maestro · MED" style={{ marginTop: 10 }}
|
||||
docLabel="改动方案(SPEC)"
|
||||
doc={"把轮询改为 WS 推送:\n1. store 层加 events 订阅\n2. web 端断线重连 + 指数退避"}
|
||||
actions={<>
|
||||
<Button variant="accept">✓ 通过</Button>
|
||||
<Button variant="reject">✕ 驳回</Button>
|
||||
</>} />
|
||||
<div style={{ display: 'grid', gap: 8, marginTop: 12, justifyItems: 'start' }}>
|
||||
<Toast kind="ok">已通过 · 进入 ready</Toast>
|
||||
<Toast kind="err">驳回必须填写改进意见</Toast>
|
||||
<Toast kind="warn">daemon 重连中…</Toast>
|
||||
</div>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>状态流转时间线</div>
|
||||
<Timeline items={[
|
||||
{ time: '06-12 05:58', text: '审批驳回(方案评审):缺少回滚方案', who: 'you', color: 'var(--red)' },
|
||||
{ time: '06-12 06:10', text: '写方案中 → 待确认方案', who: '编排器' },
|
||||
{ time: '06-12 06:31', text: '审批通过(方案评审)', who: 'you', color: 'var(--green)' },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<ul style={{ padding: 0, margin: 0 }}>
|
||||
<EventItem type="task.created" time="13:58" detail="重构 sync 模块" />
|
||||
<EventItem type="approval.requested" time="14:00" detail="spec_review · 等待裁决" />
|
||||
<EventItem type="approval.rejected" time="14:02" detail="↳ 缺少回滚方案" />
|
||||
<EventItem type="run.started" time="14:05" detail="executor · worktree wt-127" />
|
||||
<EventItem type="status.changed" time="14:09" detail="executing → exec_review" />
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
</script></body></html>
|
||||
@@ -1,28 +0,0 @@
|
||||
<!-- @dsCard group="Brand" viewport="700x280" subtitle="双色调图标集:基础笔画中性灰 + 语义元素信号色 · 24×24 · 2px 圆头描边 · assets/icons/" name="图标 · Icons" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:16px;display:grid;gap:10px}
|
||||
.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
|
||||
.g{border:1px solid var(--line-soft);border-radius:6px;padding:12px 4px 8px;text-align:center;color:var(--muted)}
|
||||
.g svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.f{font-size:9.5px;color:var(--muted);letter-spacing:.04em;margin-top:6px}
|
||||
.note{font-size:10px;color:var(--faint);letter-spacing:.06em;line-height:1.6}</style></head><body>
|
||||
<div class="grid">
|
||||
<div class="g" style="--icon-accent:var(--violet)"><svg viewBox="0 0 24 24"><path d="M12 3 L22 21 L2 21 Z"/><line x1="12" y1="10" x2="12" y2="14" stroke="var(--icon-accent)"/><line x1="12" y1="17.5" x2="12" y2="17.51" stroke="var(--icon-accent)"/></svg><div class="f">gate 待审批</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><path d="M7 4 L19 12 L7 20 Z" stroke="var(--icon-accent)"/></svg><div class="f">ready 可执行</div></div>
|
||||
<div class="g" style="--icon-accent:var(--cyan)"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3" fill="var(--icon-accent)" stroke="none"/></svg><div class="f">running 执行中</div></div>
|
||||
<div class="g" style="--icon-accent:var(--cyan)"><svg viewBox="0 0 24 24"><polyline points="2 12 7 12 10 5 14 19 17 12 22 12" stroke="var(--icon-accent)"/></svg><div class="f">event 事件</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2.5"/><polyline points="8 12 11 15 16 9" stroke="var(--icon-accent)"/></svg><div class="f">task 任务</div></div>
|
||||
<div class="g" style="--icon-accent:var(--amber)"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><line x1="10" y1="10" x2="14" y2="14" stroke="var(--icon-accent)"/></svg><div class="f">deps 依赖</div></div>
|
||||
<div class="g" style="--icon-accent:var(--amber)"><svg viewBox="0 0 24 24"><polyline points="5 4 5 14 18 14"/><polyline points="13 9 18 14 13 19" stroke="var(--icon-accent)"/></svg><div class="f">reason 原因</div></div>
|
||||
<div class="g" style="--icon-accent:var(--cyan)"><svg viewBox="0 0 24 24"><line x1="4" y1="12" x2="19" y2="12"/><polyline points="13 6 19 12 13 18" stroke="var(--icon-accent)"/></svg><div class="f">flow 流转</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9" stroke="var(--icon-accent)"/></svg><div class="f">git 项目仓库</div></div>
|
||||
<div class="g"><svg viewBox="0 0 24 24"><path d="M3 7 a2 2 0 0 1 2-2 h4 l2 3 h8 a2 2 0 0 1 2 2 v8 a2 2 0 0 1-2 2 H5 a2 2 0 0 1-2-2 Z"/></svg><div class="f">project 项目</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5" stroke="var(--icon-accent)"/></svg><div class="f">sync 同步</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><line x1="3" y1="6" x2="12" y2="6"/><circle cx="15" cy="6" r="2.5" stroke="var(--icon-accent)"/><line x1="18" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="5.5" y2="12"/><circle cx="9" cy="12" r="2.5" stroke="var(--icon-accent)"/><line x1="12.5" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="12.5" y2="18"/><circle cx="16" cy="18" r="2.5" stroke="var(--icon-accent)"/><line x1="19.5" y1="18" x2="21" y2="18"/></svg><div class="f">config 配置</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5"/><line x1="15.5" y1="15.5" x2="21" y2="21" stroke="var(--icon-accent)"/></svg><div class="f">search 搜索</div></div>
|
||||
<div class="g" style="--icon-accent:var(--green)"><svg viewBox="0 0 24 24"><line x1="12" y1="4" x2="12" y2="20" stroke="var(--icon-accent)"/><line x1="4" y1="12" x2="20" y2="12" stroke="var(--icon-accent)"/></svg><div class="f">add 新建</div></div>
|
||||
<div class="g"><svg viewBox="0 0 24 24"><line x1="5" y1="5" x2="19" y2="19"/><line x1="19" y1="5" x2="5" y2="19"/></svg><div class="f">close 关闭</div></div>
|
||||
</div>
|
||||
<div class="note">双色调规则:基础笔画 currentColor(默认 --muted);语义元素 stroke=var(--icon-accent, 信号色回退)——内联使用时按语境覆盖,<img> 引用时用内置回退色。纯工具图标(project/close)保持单色。文件:assets/icons/<name>.svg · ▍/▮ 为品牌字符保留</div>
|
||||
</body></html>
|
||||
@@ -1,64 +0,0 @@
|
||||
<!-- @dsCard group="Brand" viewport="700x230" subtitle="像素章鱼图案:一脑多臂 · 横排锁定 + 独立方形徽标" name="Logo" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body{margin:0;background:var(--bg-deep);font-family:var(--mono);height:230px;display:flex;align-items:center;justify-content:center;gap:56px}
|
||||
canvas{image-rendering:pixelated;filter:drop-shadow(0 0 7px rgba(95,221,125,.4))}
|
||||
.lockup{display:flex;align-items:center;gap:16px}
|
||||
.logo-word{font-size:24px;font-weight:700;letter-spacing:.28em;color:var(--green);text-shadow:0 0 12px rgba(95,221,125,.45)}
|
||||
.cursor{animation:maestro-blink 1.1s steps(1) infinite;margin-left:1px}
|
||||
.logo-sub{margin-top:3px;font-size:11px;color:var(--muted);letter-spacing:.35em}
|
||||
.badge{width:108px;height:108px;border:1px solid var(--green-dim);border-radius:10px;display:flex;align-items:center;justify-content:center}
|
||||
.lab{font-size:10px;color:var(--faint);letter-spacing:.1em;text-align:center;margin-top:10px;white-space:nowrap}
|
||||
.col{text-align:center}
|
||||
.divider{width:1px;height:120px;background:var(--line-soft)}
|
||||
</style></head><body>
|
||||
|
||||
<div class="col">
|
||||
<div class="lockup">
|
||||
<canvas id="markA" width="15" height="12"></canvas>
|
||||
<div>
|
||||
<div class="logo-word">MAESTRO<span class="cursor">▮</span></div>
|
||||
<div class="logo-sub">多项目任务调度台</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lab">横排锁定 · 侧栏 / 文档页眉</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="col">
|
||||
<div class="badge"><canvas id="markB" width="15" height="12"></canvas></div>
|
||||
<div class="lab">方形徽标 · favicon / 头像</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 像素章鱼 15×12 — L 高光 / G 磷光绿 / D 暗位 / E 眼
|
||||
const MAP = [
|
||||
'.....LLLLL.....',
|
||||
'...LLLLLLLLL...',
|
||||
'..GGGGGGGGGGG..',
|
||||
'..GGEEGGGEEGG..',
|
||||
'..GGEEGGGEEGG..',
|
||||
'..GGGGGGGGGGG..',
|
||||
'...GGGGGGGGG...',
|
||||
'...G..G.G..G...',
|
||||
'...G..G.G..G...',
|
||||
'..G...G.G...G..',
|
||||
'..D...D.D...D..',
|
||||
'.D...D...D...D.',
|
||||
];
|
||||
const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' };
|
||||
function draw(id, scale) {
|
||||
const c = document.getElementById(id);
|
||||
const ctx = c.getContext('2d');
|
||||
MAP.forEach((row, y) => [...row].forEach((ch, x) => {
|
||||
if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); }
|
||||
}));
|
||||
c.style.width = (15 * scale) + 'px';
|
||||
c.style.height = (12 * scale) + 'px';
|
||||
}
|
||||
draw('markA', 4.4);
|
||||
draw('markB', 5.6);
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -1,13 +0,0 @@
|
||||
<!-- @dsCard group="Brand" viewport="700x150" subtitle="动效三件套:blink(光标)· pulse(等待/执行)· rise(入场)" name="动效 · Motion" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:20px;display:flex;align-items:center;gap:24px;font-size:10px}
|
||||
.c{flex:1;text-align:center;border:1px solid var(--line-soft);border-radius:8px;padding:16px 8px}
|
||||
.f{color:var(--muted);margin-top:10px}
|
||||
.blink{font-size:20px;color:var(--green);animation:maestro-blink 1.1s steps(1) infinite}
|
||||
.pulse{display:inline-block;font-size:11px;letter-spacing:.12em;color:var(--violet);border:1px solid var(--violet-dim);padding:4px 10px;border-radius:4px;animation:maestro-pulse 2.2s infinite}
|
||||
.rise{display:inline-block;font-size:11px;background:var(--panel);border:1px solid var(--line);padding:6px 12px;border-radius:6px;animation:maestro-rise .25s ease both infinite alternate;animation-duration:1.6s}</style></head><body>
|
||||
<div class="c"><span class="blink">▮</span><div class="f">blink · steps(1) 1.1s · 硬切</div></div>
|
||||
<div class="c"><span class="pulse">2 项待审批</span><div class="f">pulse · 透明度脉冲 .8–2.2s</div></div>
|
||||
<div class="c"><span class="rise">任务创建</span><div class="f">rise · 5px 上移淡入 .18–.25s</div></div>
|
||||
</body></html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- @dsCard group="Brand" viewport="700x160" subtitle="氛围层:1px 扫描线 + 暗角;琥珀斜纹警示条" name="质感 · Texture" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:16px;display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:10px}
|
||||
.f{color:var(--muted);margin-top:6px}
|
||||
.scan{position:relative;height:96px;background:var(--panel);border:1px solid var(--line);border-radius:6px;overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--muted)}
|
||||
.scan::before{content:'';position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(0,0,0,.16) 0 1px,transparent 1px 3px),radial-gradient(ellipse at 50% 40%,transparent 55%,rgba(0,0,0,.5));opacity:.5}
|
||||
.stripe{height:5px;background:repeating-linear-gradient(-45deg,var(--violet) 0 9px,transparent 9px 18px);opacity:.8}</style></head><body>
|
||||
<div><div class="scan">扫描线 + 暗角 · body::before</div><div class="f">全屏界面统一氛围层 · opacity .5</div></div>
|
||||
<div><div style="height:96px;border:1px solid var(--violet-dim);border-radius:6px;overflow:hidden;background:var(--panel);display:flex;flex-direction:column"><div class="stripe"></div><div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--violet);font-size:11px;letter-spacing:.22em">审批闸 · 等待裁决</div></div><div class="f">磷光紫斜纹 · 审批区警示条</div></div>
|
||||
</body></html>
|
||||
@@ -1,10 +0,0 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x130" subtitle="文字三档:ink / muted / faint" name="文字色 · Ink" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);padding:18px 20px;display:grid;gap:12px;font-size:13px;line-height:1.55}
|
||||
.row{display:flex;align-items:baseline;gap:14px}
|
||||
.t{flex:1}.k{font-size:10px;letter-spacing:.12em;width:90px;flex:none}</style></head><body>
|
||||
<div class="row" style="color:var(--ink)"><span class="k">--ink</span><span class="t">正文与标题:建任务 → 审批闸 → ready → 执行 → 等你审。</span><span style="font-size:10px;opacity:.6">#d8e4d4</span></div>
|
||||
<div class="row" style="color:var(--muted)"><span class="k">--muted</span><span class="t">次要信息、区块头、表单 label、meta 行。</span><span style="font-size:10px;opacity:.8">#76876f</span></div>
|
||||
<div class="row" style="color:var(--faint)"><span class="k">--faint</span><span class="t">弱化:占位、路径、时间戳、disabled。</span><span style="font-size:10px">#4a5747</span></div>
|
||||
</body></html>
|
||||
@@ -1,26 +0,0 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x180" subtitle="Light 主题:<html data-theme="light"> 激活,信号色加深保对比" name="Light 主题" -->
|
||||
<!doctype html><html lang="zh-CN" data-theme="light"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:16px;display:grid;gap:12px}
|
||||
.row{display:flex;gap:10px}
|
||||
.sw{flex:1;height:56px;border:1px solid var(--line);border-radius:6px;display:flex;flex-direction:column;justify-content:flex-end;padding:6px 9px}
|
||||
.n{font-size:10px;font-weight:600;letter-spacing:.06em}
|
||||
.demo{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
||||
.chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:1px 8px;border:1px solid;border-radius:3px}
|
||||
.chip::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}</style></head><body>
|
||||
<div class="row">
|
||||
<div class="sw" style="background:var(--bg-deep)"><span class="n">--bg-deep</span></div>
|
||||
<div class="sw" style="background:var(--bg)"><span class="n">--bg</span></div>
|
||||
<div class="sw" style="background:var(--panel)"><span class="n">--panel</span></div>
|
||||
<div class="sw" style="background:var(--panel-2)"><span class="n">--panel-2</span></div>
|
||||
<div class="sw" style="background:var(--line)"><span class="n">--line</span></div>
|
||||
</div>
|
||||
<div class="demo">
|
||||
<button style="font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.08em;background:var(--green);color:var(--bg-deep);border:1px solid var(--green);border-radius:4px;padding:6px 14px">创建任务</button>
|
||||
<span class="chip" style="color:var(--violet);border-color:var(--violet-dim)">待审/合</span>
|
||||
<span class="chip" style="color:var(--cyan);border-color:var(--cyan)">执行中</span>
|
||||
<span class="chip" style="color:var(--red);border-color:var(--red-dim)">失败</span>
|
||||
<span style="font-size:10px;font-weight:700;letter-spacing:.14em;color:var(--amber);border:1px solid var(--amber-dim);border-radius:3px;padding:1px 7px">MED</span>
|
||||
<span style="font-size:13px;font-weight:700;letter-spacing:.28em;color:var(--green);text-shadow:var(--glow-green)">MAESTRO▮</span>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x300" subtitle="五个磷光信号色 + 状态机分组全展示(审批闸已由琥珀改为磷光紫)" name="信号色 · Signals" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:16px;display:grid;gap:14px}
|
||||
.cols{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
|
||||
.col{border:1px solid var(--line-soft);border-radius:6px;overflow:hidden}
|
||||
.main{height:64px;display:flex;flex-direction:column;justify-content:flex-end;padding:8px 10px;color:var(--bg-deep)}
|
||||
.dim{height:34px;display:flex;align-items:center;padding:0 10px;font-size:10px;color:var(--ink)}
|
||||
.n{font-size:11px;font-weight:700;letter-spacing:.08em}.v{font-size:10px;opacity:.75}
|
||||
.u{font-size:10px;color:var(--muted);padding:5px 10px;border-top:1px solid var(--line-soft)}
|
||||
.head{font-size:10px;color:var(--muted);letter-spacing:.18em}
|
||||
.chips{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:1px 8px;border:1px solid var(--line);color:var(--muted);border-radius:3px;white-space:nowrap}
|
||||
.chip::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}
|
||||
.c-gate{color:var(--violet);border-color:var(--violet-dim)}
|
||||
.c-go{color:var(--green);border-color:var(--green-dim)}
|
||||
.c-run{color:var(--cyan);border-color:var(--cyan)}
|
||||
.c-work{color:var(--cyan);border-color:var(--cyan-dim)}
|
||||
.c-hold{color:var(--faint)}
|
||||
.c-bad{color:var(--red);border-color:var(--red-dim)}
|
||||
.c-done{color:var(--bg-deep);background:var(--green);border-color:var(--green);font-weight:700}
|
||||
.c-dead{color:var(--faint);text-decoration:line-through}
|
||||
.c-container{color:#9bb4c8;border-color:#2c3c4a}</style></head><body>
|
||||
<div class="cols">
|
||||
<div class="col"><div class="main" style="background:var(--green)"><span class="n">--green</span><span class="v">#5fdd7d</span></div><div class="dim" style="background:var(--green-dim)">--green-dim</div><div class="u">可执行 · 完成 · 品牌</div></div>
|
||||
<div class="col"><div class="main" style="background:var(--violet)"><span class="n">--violet</span><span class="v">#b88ef5</span></div><div class="dim" style="background:var(--violet-dim)">--violet-dim</div><div class="u">审批闸 · 等待裁决</div></div>
|
||||
<div class="col"><div class="main" style="background:var(--cyan)"><span class="n">--cyan</span><span class="v">#59c8d8</span></div><div class="dim" style="background:var(--cyan-dim)">--cyan-dim</div><div class="u">执行中 · agent</div></div>
|
||||
<div class="col"><div class="main" style="background:var(--amber)"><span class="n">--amber</span><span class="v">#f0b429</span></div><div class="dim" style="background:var(--amber-dim)">--amber-dim</div><div class="u">警示 · MED · 待依赖</div></div>
|
||||
<div class="col"><div class="main" style="background:var(--red)"><span class="n">--red</span><span class="v">#ff5d5d</span></div><div class="dim" style="background:var(--red-dim)">--red-dim</div><div class="u">失败 · 驳回 · HARD</div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="head" style="margin-bottom:8px">16 个任务状态 · 按状态机分组上色</div>
|
||||
<div class="chips">
|
||||
<span class="chip">新建</span>
|
||||
<span class="chip c-work">分析拆解中</span>
|
||||
<span class="chip c-gate">待确认拆解</span>
|
||||
<span class="chip c-container">已拆解</span>
|
||||
<span class="chip c-work">写方案中</span>
|
||||
<span class="chip c-gate">待确认方案</span>
|
||||
<span class="chip c-go">可执行</span>
|
||||
<span class="chip c-hold">被依赖阻塞</span>
|
||||
<span class="chip c-go">排队中</span>
|
||||
<span class="chip c-run">执行中</span>
|
||||
<span class="chip c-gate">待审/合</span>
|
||||
<span class="chip c-bad">失败</span>
|
||||
<span class="chip c-bad">需人工</span>
|
||||
<span class="chip c-done">完成</span>
|
||||
<span class="chip c-hold">暂停</span>
|
||||
<span class="chip c-dead">取消</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -1,13 +0,0 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x150" subtitle="碳绿基底四层抬升 + 两档描边" name="基底 · Surfaces" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:16px;display:flex;gap:10px}
|
||||
.sw{flex:1;height:118px;border:1px solid var(--line);border-radius:6px;display:flex;flex-direction:column;justify-content:flex-end;padding:8px 10px}
|
||||
.n{font-size:11px;font-weight:600;letter-spacing:.08em}.v{font-size:10px;color:var(--muted)}</style></head><body>
|
||||
<div class="sw" style="background:var(--bg-deep)"><span class="n">--bg-deep</span><span class="v">#070908 · 侧栏/输入</span></div>
|
||||
<div class="sw" style="background:var(--bg)"><span class="n">--bg</span><span class="v">#0a0d0b · 主背景</span></div>
|
||||
<div class="sw" style="background:var(--panel)"><span class="n">--panel</span><span class="v">#10140f · 卡片</span></div>
|
||||
<div class="sw" style="background:var(--panel-2)"><span class="n">--panel-2</span><span class="v">#151b14 · hover/抬升</span></div>
|
||||
<div class="sw" style="background:var(--line)"><span class="n">--line</span><span class="v">#232d23 · 描边</span></div>
|
||||
<div class="sw" style="background:var(--line-soft)"><span class="n">--line-soft</span><span class="v">#1a221a · 弱描边</span></div>
|
||||
</body></html>
|
||||
@@ -1,13 +0,0 @@
|
||||
<!-- @dsCard group="Colors" viewable="true" viewport="700x140" subtitle="信号色 7–8% 透明底 + dim 边框的 chip 配方" name="信号底 · Tints" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);padding:20px;display:flex;gap:10px;flex-wrap:wrap;align-content:center}
|
||||
.tile{flex:1;min-width:140px;height:96px;border:1px solid;border-radius:6px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:6px}
|
||||
.lab{font-size:10px;font-weight:700;letter-spacing:.14em;padding:1px 7px;border:1px solid;border-radius:3px}
|
||||
.f{font-size:10px;color:var(--muted)}</style></head><body>
|
||||
<div class="tile" style="background:rgba(95,221,125,.07);border-color:var(--green-dim)"><span class="lab" style="color:var(--green);border-color:var(--green-dim)">EASY</span><span class="f">rgba(green,.07)</span></div>
|
||||
<div class="tile" style="background:rgba(240,180,41,.07);border-color:var(--amber-dim)"><span class="lab" style="color:var(--amber);border-color:var(--amber-dim)">MED</span><span class="f">rgba(amber,.07)</span></div>
|
||||
<div class="tile" style="background:rgba(255,93,93,.07);border-color:var(--red-dim)"><span class="lab" style="color:var(--red);border-color:var(--red-dim)">HARD</span><span class="f">rgba(red,.07)</span></div>
|
||||
<div class="tile" style="background:rgba(184,142,245,.08);border-color:var(--violet-dim)"><span class="lab" style="color:var(--violet);border-color:var(--violet-dim)">GATE</span><span class="f">rgba(violet,.08)</span></div>
|
||||
<div class="tile" style="background:rgba(89,200,216,.07);border-color:var(--cyan-dim)"><span class="lab" style="color:var(--cyan);border-color:var(--cyan-dim)">RUN</span><span class="f">rgba(cyan,.07)</span></div>
|
||||
</body></html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x150" subtitle="边框语言:细线两档 · 虚线空态 · 2px 左侧强调边" name="边框 · Borders" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:16px;display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:11px}
|
||||
.b{height:110px;padding:10px;border-radius:6px;display:flex;flex-direction:column;justify-content:flex-end;gap:2px}
|
||||
.f{color:var(--muted);font-size:10px}</style></head><body>
|
||||
<div class="b" style="background:var(--panel);border:1px solid var(--line)"><b>1px --line</b><span class="f">标准卡片边</span></div>
|
||||
<div class="b" style="background:var(--panel);border:1px solid var(--line-soft)"><b>1px --line-soft</b><span class="f">内部分隔</span></div>
|
||||
<div class="b" style="border:1px dashed var(--line)"><b>1px dashed</b><span class="f">空态 / 占位 / 弱分组</span></div>
|
||||
<div class="b" style="background:var(--panel);border-bottom:1px solid var(--line);border-left:2px solid var(--green-dim)"><b>2px 左侧强调</b><span class="f">激活项 / 详情 / doc 块</span></div>
|
||||
</body></html>
|
||||
@@ -1,10 +0,0 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x150" subtitle="辉光替代阴影;黑色外阴影只给浮层" name="辉光 · Glow" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:20px;display:flex;align-items:center;gap:26px;font-size:10px}
|
||||
.c{text-align:center}.f{color:var(--muted);margin-top:8px}</style></head><body>
|
||||
<div class="c"><span style="font-size:17px;font-weight:700;letter-spacing:.28em;color:var(--green);text-shadow:0 0 12px rgba(95,221,125,.45)">MAESTRO</span><div class="f">品牌字辉光</div></div>
|
||||
<div class="c"><span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--cyan);box-shadow:0 0 8px var(--cyan)"></span><div class="f">状态点辉光</div></div>
|
||||
<div class="c"><button style="font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.08em;background:var(--green);color:var(--bg-deep);border:1px solid var(--green);padding:6px 14px;border-radius:4px;box-shadow:0 0 14px rgba(95,221,125,.35)">hover 发光</button><div class="f">按钮 hover 辉光</div></div>
|
||||
<div class="c"><div style="background:var(--bg-deep);border:1px solid var(--line);border-radius:6px;padding:8px 14px;font-size:12px;box-shadow:0 8px 30px rgba(0,0,0,.55)">浮层</div><div class="f">黑阴影 · 仅弹层/Toast</div></div>
|
||||
</body></html>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x150" subtitle="圆角体系:控件 3–4px · 面板 6px · 浮层 10px · 圆形只属于状态点" name="圆角 · Radius" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:20px;display:flex;align-items:center;gap:22px;font-size:11px}
|
||||
.f{color:var(--muted);font-size:10px;margin-top:6px}.c{text-align:center}</style></head><body>
|
||||
<div class="c"><span style="display:inline-block;font-size:10px;font-weight:700;letter-spacing:.14em;color:var(--amber);border:1px solid var(--amber-dim);padding:1px 7px;border-radius:var(--radius-xs)">MED</span><div class="f">--radius-xs · 3px · 徽章/chip</div></div>
|
||||
<div class="c"><button style="font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.08em;background:var(--green);color:var(--bg-deep);border:1px solid var(--green);padding:6px 14px;border-radius:var(--radius-sm)">按钮</button><div class="f">--radius-sm · 4px · 按钮/输入框</div></div>
|
||||
<div class="c"><div style="width:110px;height:56px;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-md)"></div><div class="f">--radius-md · 6px · 面板/卡片</div></div>
|
||||
<div class="c"><div style="width:110px;height:56px;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:0 8px 30px rgba(0,0,0,.55)"></div><div class="f">--radius-lg · 10px · 模态/浮层</div></div>
|
||||
<div class="c"><span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green)"></span><div class="f">状态点 · 唯一的正圆</div></div>
|
||||
</body></html>
|
||||
@@ -1,14 +0,0 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x160" subtitle="间距阶 4–22px 与控件内边距" name="间距 · Scale" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:18px 22px;display:grid;gap:8px;font-size:10px}
|
||||
.r{display:flex;align-items:center;gap:12px;color:var(--muted)}
|
||||
.bar{height:12px;background:var(--green-dim);flex:none}
|
||||
.k{width:110px;flex:none;letter-spacing:.08em}</style></head><body>
|
||||
<div class="r"><span class="k">--space-1 · 4px</span><span class="bar" style="width:4px"></span><span>chip 内 gap</span></div>
|
||||
<div class="r"><span class="k">--space-3 · 8px</span><span class="bar" style="width:8px"></span><span>行内元素 gap</span></div>
|
||||
<div class="r"><span class="k">--space-4 · 10px</span><span class="bar" style="width:10px"></span><span>按钮组 gap</span></div>
|
||||
<div class="r"><span class="k">--space-6 · 14px</span><span class="bar" style="width:14px"></span><span>卡片内边距 / 表单 gap</span></div>
|
||||
<div class="r"><span class="k">--space-7 · 16px</span><span class="bar" style="width:16px"></span><span>栏内边距</span></div>
|
||||
<div class="r"><span class="k">--space-9 · 22px</span><span class="bar" style="width:22px"></span><span>主栏左右留白</span></div>
|
||||
</body></html>
|
||||
@@ -1,12 +0,0 @@
|
||||
<!-- @dsCard group="Type" viewport="700x170" subtitle="单字族:IBM Plex Mono + Noto Sans SC 等宽中文混排" name="字族 · Mono" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:18px 22px;display:grid;gap:10px}
|
||||
.big{font-size:21px;font-weight:700;letter-spacing:.04em}
|
||||
.mix{font-size:13px;line-height:1.55}
|
||||
.meta{font-size:11px;color:var(--muted);letter-spacing:.08em}</style></head><body>
|
||||
<div class="big">多项目任务调度台 MAESTRO 0123456789</div>
|
||||
<div class="mix">按任务复杂度(Hard / Medium / Easy)分级驱动审批闸,<span style="color:var(--cyan)">agent</span> 在隔离 git worktree 自动执行——改动不自动合并、等你审。</div>
|
||||
<div class="mix" style="color:var(--muted)">if (canTransition(task.status, 'exec_review')) → 待审/合 · P1 · 中</div>
|
||||
<div class="meta">IBM PLEX MONO 400 / 500 / 600 / 700 · NOTO SANS SC 中文回退 · 全站无第二字族</div>
|
||||
</body></html>
|
||||
@@ -1,14 +0,0 @@
|
||||
<!-- @dsCard group="Type" viewport="700x200" subtitle="字号阶 10–40px,正文基准 13px" name="字号阶 · Scale" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:16px 22px;display:grid;gap:7px}
|
||||
.r{display:flex;align-items:baseline;gap:14px}
|
||||
.k{width:150px;flex:none;font-size:10px;color:var(--muted);letter-spacing:.1em}</style></head><body>
|
||||
<div class="r"><span class="k">--text-num · 40px 700</span><span style="font-size:40px;font-weight:700;color:var(--cyan);text-shadow:var(--glow-cyan);line-height:1">3</span><span style="font-size:9px;font-weight:600;letter-spacing:.3em;color:var(--muted)">RUNNING</span></div>
|
||||
<div class="r"><span class="k">--text-2xl · 20px 700</span><span style="font-size:20px;font-weight:700;letter-spacing:.04em">项目标题 maestro</span></div>
|
||||
<div class="r"><span class="k">--text-xl · 19px 700</span><span style="font-size:19px;font-weight:700;letter-spacing:.28em;color:var(--green)">MAESTRO</span></div>
|
||||
<div class="r"><span class="k">--text-md · 13px 400</span><span style="font-size:13px">正文基准 · 任务标题用 500</span></div>
|
||||
<div class="r"><span class="k">--text-sm · 12px 600</span><span style="font-size:12px;font-weight:600;letter-spacing:.08em">按钮文字</span></div>
|
||||
<div class="r"><span class="k">--text-xs · 11px 600</span><span style="font-size:11px;font-weight:600;letter-spacing:.22em;color:var(--muted)">区块标题 UPPERCASE</span></div>
|
||||
<div class="r"><span class="k">--text-2xs · 10px 700</span><span style="font-size:10px;font-weight:700;letter-spacing:.14em;color:var(--amber)">徽章 MED</span></div>
|
||||
</body></html>
|
||||
@@ -1,13 +0,0 @@
|
||||
<!-- @dsCard group="Type" viewport="700x170" subtitle="层级靠字距:.04em 标题 → .35em logo 副标" name="字距 · Tracking" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--mono);padding:18px 22px;display:grid;gap:9px}
|
||||
.r{display:flex;align-items:baseline;gap:14px;font-size:12px}
|
||||
.k{width:120px;flex:none;font-size:10px;color:var(--faint);letter-spacing:.06em}</style></head><body>
|
||||
<div class="r"><span class="k">.04em tight</span><span style="letter-spacing:.04em;font-weight:700;font-size:15px">标题 · proj-title</span></div>
|
||||
<div class="r"><span class="k">.08em label</span><span style="letter-spacing:.08em;font-weight:600">按钮 · BTN LABEL · meta</span></div>
|
||||
<div class="r"><span class="k">.12em badge</span><span style="letter-spacing:.12em;color:var(--amber)">2 项待审批</span></div>
|
||||
<div class="r"><span class="k">.22em head</span><span style="letter-spacing:.22em;font-weight:600;font-size:11px;color:var(--muted)">▍任务树 SECTION HEAD</span></div>
|
||||
<div class="r"><span class="k">.28em logo</span><span style="letter-spacing:.28em;font-weight:700;font-size:15px;color:var(--green)">MAESTRO</span></div>
|
||||
<div class="r"><span class="k">.35em widest</span><span style="letter-spacing:.35em;font-size:11px;color:var(--muted)">多项目任务调度台</span></div>
|
||||
</body></html>
|
||||
@@ -1,79 +0,0 @@
|
||||
# Maestro Design System
|
||||
|
||||
**Maestro(多项目任务调度平台)** 是一个本地优先的任务编排 daemon:管理多个本地 git 项目的任务树,按复杂度(Hard / Medium / Easy)分级驱动审批闸,用后台 agent(Claude Code,经 MCP + Agent SDK)在隔离 git worktree 自动执行任务——改动不自动合并,等人审。
|
||||
|
||||
唯一的产品界面是 **Web 调度台(看板)**:三栏布局(项目侧栏 · 闸+任务树 · 事件流),实时 WebSocket 刷新,页面内 accept/reject。
|
||||
|
||||
## 设计概念:phosphor console(磷光调度台)
|
||||
|
||||
整个品牌是一台老式磷光终端的隐喻:碳绿近黑底、磷光绿/琥珀/信号红/青四色信号系统、全站等宽字体中英混排、扫描线+暗角氛围层、零圆角、辉光代替阴影。它看起来像机房里的调度终端,而不是 SaaS 仪表盘。
|
||||
|
||||
## 来源
|
||||
|
||||
- 代码库:本地挂载 `maestro/`(Node/TypeScript daemon + 无框架 vanilla JS 看板)
|
||||
- 视觉唯一事实来源:`maestro/web/style.css`(自称 "MAESTRO 调度台 · phosphor console")
|
||||
- 结构:`maestro/web/index.html`;文案与状态模型:`maestro/web/app.js`、`maestro/DESIGN.md`、`maestro/README.md`
|
||||
- 无 Figma、无图片资产 —— logo = 像素章鱼图案(一脑多臂,喻多项目并行调度)+ 纯文字 `MAESTRO▮` 字标(见 ICONOGRAPHY 与 `guidelines/brand-logo.card.html`,15×12 像素图,磷光绿三档 + 暗眼,canvas 或 CSS 均可复现)。
|
||||
- 字体经 Google Fonts CDN 加载(IBM Plex Mono + Noto Sans SC),仓库内无字体二进制。
|
||||
|
||||
---
|
||||
|
||||
## CONTENT FUNDAMENTALS(文案基本面)
|
||||
|
||||
- **语言:中文为主,术语保留英文。** 界面文案是简体中文,但状态机/复杂度/角色词保留英文原样:`HARD / MED / EASY`、`plan / spec / operations`、`agent`、`worktree`、`accept/reject`。中英文混排是常态:「待 Claude Code 产出(经 MCP 写入并提交评审)」。
|
||||
- **电报式短语,不写完整句。** 标签和状态是 2–4 字的压缩词:「可执行」「待审/合」「需人工」「被依赖阻塞」「分析拆解中」。按钮同样短:「+ 新建」「保存配置」「⟳ 同步 todo」。
|
||||
- **间隔号 `·` 是标点主角。** 用于并置短语:「多项目任务调度台」「审批闸 · 等待裁决」「manual · 手动」「P1 · 中」。其次是箭头 `→`(流转)和 `↳`(引用/原因)。
|
||||
- **称谓:直接称「你」,系统无自称。** 「等待你裁决的审批闸」「改动不自动合并、等你审」。语气是工程师对工程师:克制、精确、略带机房黑话(「闸」「裁决」「返工」「兜底」)。
|
||||
- **无 emoji。** 表意一律用 Unicode 几何字符与符号(▍ ▮ ⚠ ▸ ◉ ⟳ ⚙ ⌕ ✕ ↳ ·),见 ICONOGRAPHY。
|
||||
- **大写字母标签 + 宽字距** 用于结构性小标题:`DIFF 摘要`、`DEPS · 依赖`、区块头全大写 + `.22em` 字距。
|
||||
- **必填用红色 `*`;占位文案给真实示例**:`/path/to/repo`、`npm test(可空)`、「要做什么」。
|
||||
- **空态文案直白且短**:「暂无产出与历史」「连接中…」。
|
||||
|
||||
## VISUAL FOUNDATIONS(视觉基本面)
|
||||
|
||||
- **主题**:默认深色(磷光终端);`<html data-theme="light">` 激活浅色主题——纸白绿灰底,信号色加深保对比,`-dim` 变淡色底,辉光减弱。全部组件走 CSS 变量,自动适配两套主题。
|
||||
- **多语言**:界面 chrome 文案双语(zh / en,见 `ui_kits/console/i18n.js`);状态/闸/事件标签经组件的 `label`/`kindLabel` prop 注入;任务标题等用户内容不翻译。
|
||||
- **颜色**:碳绿近黑的底(`--bg #0a0d0b`,带极轻微绿味),三层抬升(bg-deep → panel → panel-2),全部偏绿灰。五个磷光信号色各司其职:**绿**=可执行/成功/品牌,**紫**=审批闸/等待裁决(v1.1 起由琥珀改紫,与 MED 复杂度区分),**琥珀**=警示/MED/待依赖,**红**=失败/驳回/Hard,**青**=执行中/agent/链接。每个信号色配一个 `-dim` 暗位,专门做边框和半透明底(`rgba(信号色, .05–.08)` 做 chip 底)。文字三档:ink / muted / faint。
|
||||
- **字体**:全站只有一个字族 `--mono`(IBM Plex Mono + Noto Sans SC 回退)。没有「标题字体」——层级靠字号(10–20px,正文 13px)、字重(400/500/600/700)和字距(.04em–.35em)。全大写 + 宽字距是最强的层级信号。
|
||||
- **圆角**:小而克制(v1.1 起由零圆角调整):徽章/chip 3px、按钮/输入框 4px、面板/卡片 6px、模态/浮层 10px(`--radius-xs/sm/md/lg`);正圆仍只允许出现在状态点(6–7px 圆点)。
|
||||
- **边框**:1px 细线是主要分界手段(`--line` / `--line-soft` 两档);虚线(dashed)表示空态、占位、弱分组;左侧 2px 实色边表示「当前/激活/文档块」。
|
||||
- **阴影与辉光**:环境光是**辉光(glow)**而非阴影——品牌字、状态点、执行中 chip 都带 `0 0 8–18px` 的同色辉光;hover 按钮发光。黑色外阴影只用于浮层(弹层/模态/Toast)。无内阴影。
|
||||
- **背景质感**:全屏界面盖一层 `body::before` 氛围层——1px 重复扫描线 + 椭圆暗角,opacity .5。无图片、无渐变背景(唯一的「渐变」是 sticky 区块头下缘的淡出,和审批区的磷光紫斜纹警示条 `repeating-linear-gradient(-45deg)`)。
|
||||
- **动效**:快而硬。过渡 .1–.12s;入场统一 `rise`(5px 上移淡入,.18–.25s);关键注意力靠 **blink**(光标方块,steps(1))和 **pulse**(透明度脉冲,.8–2.2s,用于等待审批/执行中)。无弹跳、无缓动炫技。
|
||||
- **hover**:背景抬一层(panel → panel-2)或边框提亮(line → muted);强按钮 hover 提亮自身色 + 辉光。**press 无单独状态**。disabled 是 opacity .4。
|
||||
- **focus**:输入框边框变 `--green-dim` + 1px 同色 ring(box-shadow),不用浏览器默认 outline。
|
||||
- **卡片**:`--panel` 底 + 1px `--line` 边,6px 圆角,无阴影;语义卡片用信号色 dim 边(闸卡片紫边、agent 面板青边)。卡片内部用 `--line-soft` 分隔头/体/脚。
|
||||
- **布局**:固定三栏 grid(232px / 1fr / 320px),整页 100vh、栏内各自滚动;1100px 以下退化为单栏纵排。区块头 sticky。
|
||||
- **透明与模糊**:模态遮罩 `rgba(4,6,5,.78) + blur(2px)`;信号色 chip 底用 7–8% 透明信号色。除此之外不用玻璃拟态。
|
||||
- **选区**:`::selection` 绿底白字。
|
||||
|
||||
## ICONOGRAPHY(图标系统)
|
||||
|
||||
- **统一 SVG 图标集**(`assets/icons/`,15 个,v1.1 起取代原纯 Unicode 方案):24×24 网格、2px 圆头描边、**双色调**——基础笔画 `currentColor`(默认 --muted),语义元素 `stroke="var(--icon-accent, 信号色回退)"`:
|
||||
- gate 紫 · ready/task/add/sync/config/search/git 绿 · running/event/flow 青 · deps/reason 琥珀 · project/close 保持单色
|
||||
- 内联使用时可按语境覆盖 `--icon-accent`;`<img>` 引用时用内置回退色(暗色主题值)
|
||||
- 不要引入 lucide/heroicons 等第三方图标库;新图标按同一网格/描边/双色规则绘制
|
||||
- **品牌字符保留**(非图标,锁 `--mono` 等宽字体渲染):
|
||||
- `▍` 区块标题前的色块标记(绿/紫/琥珀/青上色) · `▮` logo 末尾的闪烁光标(blink 动画)
|
||||
- `→` 状态流转 · `↳` 驳回原因/引用 · `·` 间隔号 · `—` 空值 · `«`/`»` 面板折叠
|
||||
- 状态点/agent 点:纯 CSS 圆点(非字符)
|
||||
- **emoji 禁用。**
|
||||
|
||||
## 字体替代说明
|
||||
|
||||
仓库不含字体文件;原产品本身就从 Google Fonts CDN 加载 **IBM Plex Mono** 与 **Noto Sans SC**(`tokens/fonts.css` 沿用同一来源,非替代品,无失真)。如需离线分发,请提供 woff2 文件。
|
||||
|
||||
---
|
||||
|
||||
## INDEX(目录清单)
|
||||
|
||||
- `styles.css` — 全局入口,仅 @import
|
||||
- `tokens/` — `colors.css`(基底+信号色+语义别名)· `typography.css`(字族/字号/字距)· `effects.css`(间距/边框/阴影/动效)· `fonts.css`(Google Fonts)
|
||||
- `guidelines/` — 设计系统标签页的基础规范卡片(颜色/类型/间距/品牌等)
|
||||
- `components/core/` — Button · StatusChip · ComplexityBadge · CountBadge · QuotaMeter · SectionHead
|
||||
- `components/forms/` — Input · Select · Textarea · ComplexitySeg
|
||||
- `components/surfaces/` — Panel · GateCard · Toast · EventItem · Timeline
|
||||
- `ui_kits/console/` — Web 调度台整屏交互复刻(index.html 可点击,侧栏/事件流可折叠;含已归档区分页与归档详情模态)
|
||||
- `ui_kits/console_mobile/` — 移动版调度台(390px 单栏 + 底部 Tab:任务/审批/事件/项目)
|
||||
- `assets/icons/` — 双色调 SVG 图标集(24×24 · 2px 圆头描边 · currentColor + --icon-accent)
|
||||
- `SKILL.md` — Agent Skill 入口
|
||||
@@ -1,4 +0,0 @@
|
||||
@import './tokens/fonts.css';
|
||||
@import './tokens/colors.css';
|
||||
@import './tokens/typography.css';
|
||||
@import './tokens/effects.css';
|
||||
@@ -1,81 +0,0 @@
|
||||
/* MAESTRO · 颜色令牌 — phosphor console palette
|
||||
Source: maestro/web/style.css :root */
|
||||
:root {
|
||||
/* ── 基底(碳绿底) ── */
|
||||
--bg: #0a0d0b; /* 主背景 */
|
||||
--bg-deep: #070908; /* 侧栏 / 输入框 / 更深一层 */
|
||||
--panel: #10140f; /* 卡片 / 面板 */
|
||||
--panel-2: #151b14; /* 面板 hover / 强调层 */
|
||||
--line: #232d23; /* 标准描边 */
|
||||
--line-soft: #1a221a; /* 弱描边 / 分隔虚线 */
|
||||
|
||||
/* ── 文字 ── */
|
||||
--ink: #d8e4d4; /* 正文 */
|
||||
--muted: #76876f; /* 次要文字 / 标签 */
|
||||
--faint: #4a5747; /* 弱化文字 / 占位 */
|
||||
|
||||
/* ── 磷光信号色(每色配 -dim 暗位用作边框/底色) ── */
|
||||
--green: #5fdd7d; /* 成功 / 可执行 / 品牌主色 */
|
||||
--green-dim: #2e6b3d;
|
||||
--violet: #b88ef5; /* 审批闸 / 等待裁决 */
|
||||
--violet-dim:#503070;
|
||||
--amber: #f0b429; /* 警示 / MED 复杂度 / 等待依赖 */
|
||||
--amber-dim: #6b5414;
|
||||
--red: #ff5d5d; /* 失败 / 驳回 / Hard */
|
||||
--red-dim: #6b2424;
|
||||
--cyan: #59c8d8; /* 执行中 / agent / 链接 */
|
||||
--cyan-dim: #1e4e57;
|
||||
|
||||
/* ── 辉光(text-shadow / box-shadow 用) ── */
|
||||
--glow-green: 0 0 12px rgba(95, 221, 125, .45);
|
||||
--glow-violet: 0 0 14px rgba(184, 142, 245, .4);
|
||||
--glow-cyan: 0 0 18px rgba(89, 200, 216, .5);
|
||||
--glow-red: 0 0 14px rgba(255, 93, 93, .3);
|
||||
|
||||
/* ── 语义别名 ── */
|
||||
--surface-page: var(--bg);
|
||||
--surface-rail: var(--bg-deep);
|
||||
--surface-card: var(--panel);
|
||||
--surface-raised: var(--panel-2);
|
||||
--text-body: var(--ink);
|
||||
--text-secondary: var(--muted);
|
||||
--text-disabled: var(--faint);
|
||||
--border-default: var(--line);
|
||||
--border-soft: var(--line-soft);
|
||||
--accent: var(--green);
|
||||
--accent-dim: var(--green-dim);
|
||||
--status-go: var(--green);
|
||||
--status-gate: var(--violet);
|
||||
--status-bad: var(--red);
|
||||
--status-run: var(--cyan);
|
||||
}
|
||||
|
||||
/* ── Light 主题:<html data-theme="light"> 激活 — 纸白底,信号色加深保对比,-dim 变淡色底 ── */
|
||||
[data-theme="light"] {
|
||||
--bg: #f3f6f1;
|
||||
--bg-deep: #e9eee6;
|
||||
--panel: #fcfdfb;
|
||||
--panel-2: #e4ebe1;
|
||||
--line: #c9d4c6;
|
||||
--line-soft: #dbe3d8;
|
||||
|
||||
--ink: #1d251b;
|
||||
--muted: #5c6b57;
|
||||
--faint: #92a08c;
|
||||
|
||||
--green: #1b8f46;
|
||||
--green-dim: #b5e2c3;
|
||||
--violet: #7747d1;
|
||||
--violet-dim:#dcccf6;
|
||||
--amber: #946d06;
|
||||
--amber-dim: #ecd9a0;
|
||||
--red: #cd3434;
|
||||
--red-dim: #f2baba;
|
||||
--cyan: #0b7488;
|
||||
--cyan-dim: #b2dfe7;
|
||||
|
||||
--glow-green: 0 0 10px rgba(27, 143, 70, .25);
|
||||
--glow-violet: 0 0 10px rgba(119, 71, 209, .22);
|
||||
--glow-cyan: 0 0 12px rgba(11, 116, 136, .25);
|
||||
--glow-red: 0 0 10px rgba(205, 52, 52, .2);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/* MAESTRO · 形制令牌 — 直角、细线、辉光、扫描线 */
|
||||
:root {
|
||||
/* 圆角:小而克制 — 控件 3–4px、面板 6px、浮层 10px;圆形仍只用于状态点 */
|
||||
--radius-xs: 3px; /* chip / 徽章 / 小标签 */
|
||||
--radius-sm: 4px; /* 按钮 / 输入框 / doc 块 */
|
||||
--radius-md: 6px; /* 面板 / 卡片 / toast */
|
||||
--radius-lg: 10px; /* 模态 / 预览浮层 */
|
||||
--radius-dot: 50%;
|
||||
|
||||
/* 边框 */
|
||||
--border-w: 1px;
|
||||
--border-accent-w: 2px; /* 左侧强调边(active 项 / doc 块) */
|
||||
|
||||
/* 间距阶(px)— 从实际版式提取 */
|
||||
--space-1: 4px;
|
||||
--space-2: 6px;
|
||||
--space-3: 8px;
|
||||
--space-4: 10px;
|
||||
--space-5: 12px;
|
||||
--space-6: 14px;
|
||||
--space-7: 16px;
|
||||
--space-8: 20px;
|
||||
--space-9: 22px;
|
||||
|
||||
/* 控件内边距 */
|
||||
--pad-btn: 6px 14px;
|
||||
--pad-btn-xs: 2px 8px;
|
||||
--pad-input: 7px 10px;
|
||||
--pad-chip: 1px 8px;
|
||||
--pad-card: 12px 14px;
|
||||
|
||||
/* 阴影:外阴影只用于浮层;辉光是主角 */
|
||||
--shadow-pop: 0 10px 30px rgba(0, 0, 0, .65);
|
||||
--shadow-modal: 0 18px 60px rgba(0, 0, 0, .6);
|
||||
--shadow-toast: 0 8px 30px rgba(0, 0, 0, .55);
|
||||
|
||||
/* 动效 */
|
||||
--ease-fast: .12s; /* @kind other */
|
||||
--ease-base: .2s ease; /* @kind other */
|
||||
}
|
||||
|
||||
/* 扫描线 + 暗角氛围层:加在 body::before(全屏界面用) */
|
||||
@keyframes maestro-blink { 50% { opacity: 0; } }
|
||||
@keyframes maestro-pulse { 50% { opacity: .25; } }
|
||||
@keyframes maestro-rise { from { opacity: 0; transform: translateY(5px); } }
|
||||
@@ -1,4 +0,0 @@
|
||||
/* MAESTRO · webfonts
|
||||
原产品经 Google Fonts CDN 加载(maestro/web/index.html),仓库内无字体二进制。
|
||||
此处沿用同一 CDN 源;如需离线可下载 woff2 后改为本地 @font-face。 */
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap');
|
||||
@@ -1,30 +0,0 @@
|
||||
/* MAESTRO · 字体令牌 — 等宽中文混排
|
||||
全站单字族:IBM Plex Mono(拉丁)+ Noto Sans SC(中文回退) */
|
||||
:root {
|
||||
--mono: 'IBM Plex Mono', 'Noto Sans SC', ui-monospace, monospace; /* @kind font */
|
||||
--font-body: var(--mono); /* 没有第二字族:标题、正文、按钮全部等宽 */
|
||||
|
||||
/* 字号阶(px) */
|
||||
--text-2xs: 10px; /* 徽章 / chip 标签 */
|
||||
--text-xs: 11px; /* 区块标题 / 表单 label / meta */
|
||||
--text-sm: 12px; /* 按钮 / 次要正文 */
|
||||
--text-md: 13px; /* 正文基准(body) */
|
||||
--text-lg: 15px; /* 预览标题 */
|
||||
--text-xl: 19px; /* logo 字 */
|
||||
--text-2xl: 20px; /* 项目标题 */
|
||||
--text-num: 40px; /* 大数字(agent 计数) */
|
||||
|
||||
/* 行高 */
|
||||
--leading-body: 1.55;
|
||||
--leading-doc: 1.7;
|
||||
|
||||
/* 字重:400 正文 · 500 任务标题 · 600 按钮/区块头 · 700 强调/徽章 */
|
||||
|
||||
/* 字距(全大写小标签是核心母题) */
|
||||
--tracking-tight: .04em; /* 标题 */
|
||||
--tracking-label: .08em; /* 按钮 / meta */
|
||||
--tracking-badge: .12em; /* 徽章 / 计数 */
|
||||
--tracking-head: .22em; /* 区块标题(uppercase) */
|
||||
--tracking-logo: .28em; /* logo */
|
||||
--tracking-widest: .35em; /* logo 副标 */
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// 已归档区(done / 取消,时间倒序分页)+ 归档详情模态
|
||||
function ArchiveRow({ item, t, onOpen }) {
|
||||
const { ComplexityBadge, StatusChip } = window.MaestroDesignSystem_a6a290;
|
||||
const [hover, setHover] = React.useState(false);
|
||||
return (
|
||||
<div onClick={() => onOpen(item)} title={t.archiveTip}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
|
||||
padding: '7px 12px', fontSize: 12.5,
|
||||
borderBottom: '1px dashed var(--line-soft)',
|
||||
background: hover ? 'var(--panel)' : 'transparent',
|
||||
}}>
|
||||
<span style={{ color: 'var(--muted)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{item.title}<span style={{ color: 'var(--faint)', fontSize: 10.5, marginLeft: 6 }}>{item.id}</span>
|
||||
</span>
|
||||
{item.subs ? <span style={{ flex: 'none', fontSize: 10.5, color: 'var(--faint)' }}>{t.archiveSubs.replace('{n}', item.subs)}</span> : null}
|
||||
<span style={{ flex: 1, minWidth: 8 }}></span>
|
||||
<ComplexityBadge complexity={item.cplx} />
|
||||
<StatusChip status={item.status} label={t.status[item.status]} />
|
||||
<span style={{ flex: 'none', fontSize: 11, color: 'var(--faint)' }} title={item.timeFull}>{item.time}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SizeSelect({ size, setSize, t }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [hover, setHover] = React.useState(false);
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
return (
|
||||
<span ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<button onClick={() => setOpen(!open)}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.04em',
|
||||
background: 'transparent', color: hover || open ? 'var(--green)' : 'var(--muted)',
|
||||
border: '1px solid ' + (hover || open ? 'var(--green-dim)' : 'var(--line)'),
|
||||
borderRadius: 'var(--radius-sm, 4px)', padding: '3px 9px', cursor: 'pointer',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 7, transition: 'color .12s, border-color .12s',
|
||||
}}>
|
||||
{t.perPage} <b style={{ color: 'var(--green)', fontSize: 11.5 }}>{size}</b>
|
||||
<span style={{ fontSize: 8, color: 'var(--faint)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}>▼</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 'calc(100% + 5px)', right: 0, zIndex: 60,
|
||||
display: 'grid', minWidth: '100%',
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md, 6px)',
|
||||
padding: 4, boxShadow: '0 10px 30px rgba(0,0,0,.65)', animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
{[10, 20, 50, 100].map((n) => {
|
||||
const active = n === size;
|
||||
return (
|
||||
<button key={n} onClick={() => { setSize(n); setOpen(false); }} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 11.5, fontWeight: active ? 700 : 400, textAlign: 'left',
|
||||
background: active ? 'var(--panel-2)' : 'transparent',
|
||||
color: active ? 'var(--green)' : 'var(--ink)',
|
||||
border: 'none', borderRadius: 4, padding: '6px 12px 6px 8px', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 7,
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
|
||||
<span style={{ width: 8, color: 'var(--green)', fontSize: 10 }}>{active ? '▍' : ''}</span>{n}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ArchivePager({ page, pages, size, setPage, setSize, t }) {
|
||||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', justifyContent: 'center', fontFamily: 'var(--mono)' }}>
|
||||
{pages > 1 ? (
|
||||
<React.Fragment>
|
||||
<Button size="xs" disabled={page <= 1} onClick={() => setPage(page - 1)}>{t.pagePrev}</Button>
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{t.pageOf.replace('{a}', page).replace('{b}', pages)}</span>
|
||||
<Button size="xs" disabled={page >= pages} onClick={() => setPage(page + 1)}>{t.pageNext}</Button>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
<SizeSelect size={size} setSize={setSize} t={t} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ArchiveSection({ items, t, onOpen }) {
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [size, setSize] = React.useState(20);
|
||||
if (!items.length) return null;
|
||||
const pages = Math.max(1, Math.ceil(items.length / size));
|
||||
const cur = Math.min(page, pages);
|
||||
const slice = items.slice((cur - 1) * size, cur * size);
|
||||
return (
|
||||
<section style={{ marginTop: 28, opacity: .82 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.22em', color: 'var(--muted)', textTransform: 'uppercase', padding: '18px 0 10px' }}>
|
||||
<span style={{ color: 'var(--faint)' }}>▣</span>{t.archive} · {items.length}
|
||||
</div>
|
||||
<div>{slice.map((it) => <ArchiveRow key={it.id} item={it} t={t} onOpen={onOpen} />)}</div>
|
||||
<ArchivePager page={cur} pages={pages} size={size} setPage={setPage} setSize={(n) => { setSize(n); setPage(1); }} t={t} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// 归档详情模态(只读:属性 / 产出 / 执行历史 / 结果 / 审批 / 时间线)
|
||||
function ArchiveModal({ item, t, onClose }) {
|
||||
const { ComplexityBadge, StatusChip, Timeline } = window.MaestroDesignSystem_a6a290;
|
||||
React.useEffect(() => {
|
||||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose]);
|
||||
if (!item) return null;
|
||||
const d = item.detail || {};
|
||||
const label = (text) => <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '14px 0 4px' }}>{text}</div>;
|
||||
const doc = (text) => (
|
||||
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)' }}>{text}</pre>
|
||||
);
|
||||
const kvGrid = { fontSize: 12, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '3px 12px', margin: 0 };
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
|
||||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.78)', backdropFilter: 'blur(2px)' }}></div>
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', width: 'min(860px, 94vw)', maxHeight: '92vh', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 'var(--radius-lg, 10px)', overflow: 'hidden', boxShadow: '0 24px 64px rgba(0,0,0,.6)', animation: 'maestro-rise .18s ease both' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)' }}>
|
||||
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--bg-deep)', background: 'var(--muted)', padding: '2px 8px', borderRadius: 3 }}>{t.archiveDetail}</span>
|
||||
<span style={{ fontWeight: 700, fontSize: 15 }}>{item.title}</span>
|
||||
<ComplexityBadge complexity={item.cplx} />
|
||||
<StatusChip status={item.status} label={t.status[item.status]} />
|
||||
<span style={{ flex: 1 }}></span>
|
||||
<button onClick={onClose} title={t.closeTip} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer', padding: '4px 8px' }}>✕</button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 20px 24px', fontFamily: 'var(--mono)' }}>
|
||||
{d.attrs ? <dl style={kvGrid}>{d.attrs.map(([k, v], i) => (
|
||||
<React.Fragment key={i}><dt style={{ color: 'var(--muted)' }}>{k}</dt><dd style={{ margin: 0, wordBreak: 'break-all' }}>{v}</dd></React.Fragment>
|
||||
))}</dl> : null}
|
||||
{d.spec ? <React.Fragment>{label('SPEC · 方案')}{doc(d.spec)}</React.Fragment> : null}
|
||||
{d.ops ? <React.Fragment>{label('OPERATIONS · 操作')}{doc(d.ops)}</React.Fragment> : null}
|
||||
{d.runs && d.runs.length ? <React.Fragment>
|
||||
{label(t.runsLabel + '(' + d.runs.length + ')')}
|
||||
<dl style={kvGrid}>{d.runs.map((r, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<dt style={{ color: 'var(--muted)' }}>{r.kind} · {r.status}</dt>
|
||||
<dd style={{ margin: 0 }}>{r.span}{r.ref ? <span style={{ display: 'block', color: 'var(--faint)', fontSize: 10.5 }}>{r.ref}</span> : null}</dd>
|
||||
</React.Fragment>
|
||||
))}</dl>
|
||||
</React.Fragment> : null}
|
||||
{d.result ? <React.Fragment>
|
||||
{label(t.resultLabel)}
|
||||
<dl style={kvGrid}>
|
||||
<dt style={{ color: 'var(--muted)' }}>{t.branch}</dt><dd style={{ margin: 0 }}>{d.result.branch}</dd>
|
||||
<dt style={{ color: 'var(--muted)' }}>commits</dt><dd style={{ margin: 0 }}>{d.result.commits.join(' · ')}</dd>
|
||||
<dt style={{ color: 'var(--muted)' }}>diff</dt><dd style={{ margin: 0 }}>{d.result.diff}</dd>
|
||||
</dl>
|
||||
</React.Fragment> : null}
|
||||
{d.approvals && d.approvals.length ? <React.Fragment>
|
||||
{label(t.approvalsLabel)}
|
||||
<dl style={kvGrid}>{d.approvals.map((a, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<dt style={{ color: a.action === 'accept' ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>{a.gate} · {a.action === 'accept' ? t.accepted : t.rejected}</dt>
|
||||
<dd style={{ margin: 0 }}>{t.approver} {a.actor} · {a.at}{a.reason ? <span style={{ display: 'block', color: 'var(--amber)' }}>↳ {a.reason}</span> : null}</dd>
|
||||
</React.Fragment>
|
||||
))}</dl>
|
||||
</React.Fragment> : null}
|
||||
{d.timeline && d.timeline.length ? <React.Fragment>
|
||||
{label(t.timelineLabel)}
|
||||
<Timeline items={d.timeline} />
|
||||
</React.Fragment> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MaestroKitArchiveSection: ArchiveSection, MaestroKitArchiveModal: ArchiveModal });
|
||||
@@ -1,45 +0,0 @@
|
||||
// 右栏:事件流 · 可折叠(折叠后仅图标)
|
||||
function MaestroEventIcon({ size = 16 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flex: 'none' }}>
|
||||
<polyline points="2 12 7 12 10 5 14 19 17 12 22 12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function maestroPanelToggleProps(onClick, title) {
|
||||
return {
|
||||
onClick, title,
|
||||
style: {
|
||||
cursor: 'pointer', flex: 'none', fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1,
|
||||
background: 'transparent', color: 'var(--muted)',
|
||||
border: '1px solid var(--line)', borderRadius: 'var(--radius-sm, 4px)', padding: '4px 7px',
|
||||
},
|
||||
onMouseEnter: (e) => { e.currentTarget.style.color = 'var(--green)'; e.currentTarget.style.borderColor = 'var(--green-dim)'; },
|
||||
onMouseLeave: (e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; },
|
||||
};
|
||||
}
|
||||
|
||||
function EventPanel({ events, collapsed, onToggleCollapse, t }) {
|
||||
const { EventItem, SectionHead } = window.MaestroDesignSystem_a6a290;
|
||||
if (collapsed) {
|
||||
return (
|
||||
<aside style={{ background: 'var(--bg-deep)', borderLeft: '1px solid var(--line)', display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '16px 0', gap: 14 }}>
|
||||
<button {...maestroPanelToggleProps(onToggleCollapse, t.expandEvents)}>«</button>
|
||||
<span style={{ color: 'var(--muted)' }} title={t.events + ' · ' + events.length}><MaestroEventIcon size={18} /></span>
|
||||
<span style={{ fontSize: 10.5, color: 'var(--faint)', letterSpacing: '.04em' }}>{events.length}</span>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<aside style={{ background: 'var(--bg-deep)', borderLeft: '1px solid var(--line)', overflowY: 'auto', paddingBottom: 30 }}>
|
||||
<SectionHead title={t.events} action={<button {...maestroPanelToggleProps(onToggleCollapse, t.collapseEvents)}>»</button>}
|
||||
style={{ padding: '14px 14px 10px', position: 'sticky', top: 0, zIndex: 5, background: 'linear-gradient(var(--bg-deep) 75%, transparent)' }} />
|
||||
<ul style={{ listStyle: 'none', padding: '0 14px', margin: 0 }}>
|
||||
{events.map((e, i) => <EventItem key={i} type={e.type} label={t.eventTypes[e.type]} time={e.time} detail={e.detail} />)}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
window.MaestroKitEventPanel = EventPanel;
|
||||
@@ -1,101 +0,0 @@
|
||||
// 全屏预览:读完整方案 + 就地裁决
|
||||
function GatePreview({ item, t, onDecide, onClose }) {
|
||||
const { ComplexityBadge, Button, Textarea } = window.MaestroDesignSystem_a6a290;
|
||||
const [rejecting, setRejecting] = React.useState(false);
|
||||
const [reason, setReason] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose]);
|
||||
if (!item) return null;
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
|
||||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', width: 'min(1080px, 94vw)', height: '94vh', background: 'var(--panel)', border: '1px solid var(--violet-dim)', borderRadius: 'var(--radius-lg, 10px)', overflow: 'hidden', boxShadow: '0 0 0 1px var(--line-soft), 0 24px 64px rgba(0,0,0,.6)', animation: 'maestro-rise .18s ease both' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)' }}>
|
||||
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--bg)', background: 'var(--violet)', padding: '2px 8px', borderRadius: 3 }}>{t.gates[item.gate]}</span>
|
||||
<span style={{ fontWeight: 700, fontSize: 15 }}>{item.title}</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{item.meta}</span>
|
||||
<span style={{ flex: 1 }}></span>
|
||||
<button onClick={onClose} title={t.closeTip} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer', padding: '4px 8px' }}>✕</button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 20px 28px', fontFamily: 'var(--mono)' }}>
|
||||
{item.docLabel ? <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>{item.docLabel}</div> : null}
|
||||
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '12px 16px', fontSize: 13.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>{item.doc}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderTop: '1px solid var(--line)', background: 'var(--panel-2)' }}>
|
||||
{rejecting ? (
|
||||
<React.Fragment>
|
||||
<span style={{ flexBasis: '100%', display: 'flex' }}>
|
||||
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
||||
</span>
|
||||
<Button variant="reject" disabled={!reason.trim()} onClick={() => { onDecide(item, 'reject', reason); onClose(); }}>{t.confirmReject}</Button>
|
||||
<Button onClick={() => { setRejecting(false); setReason(''); }}>{t.cancel}</Button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Button variant="accept" onClick={() => { onDecide(item, 'accept'); onClose(); }}>{t.accept}</Button>
|
||||
<Button variant="reject" onClick={() => setRejecting(true)}>{t.reject}</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 审批闸区:斜纹条 + GateCard 列表(accept/reject + 驳回必填理由)
|
||||
function GateSection({ approvals, onDecide, t }) {
|
||||
const { GateCard, GateStripe, Button, Textarea, SectionHead } = window.MaestroDesignSystem_a6a290;
|
||||
const [rejecting, setRejecting] = React.useState(null);
|
||||
const [reason, setReason] = React.useState('');
|
||||
const [preview, setPreview] = React.useState(null);
|
||||
const [folded, setFolded] = React.useState(() => new Set());
|
||||
const toggleFold = (id) => setFolded((prev) => {
|
||||
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
|
||||
});
|
||||
if (approvals.length === 0) return null;
|
||||
return (
|
||||
<section style={{ marginTop: 18 }}>
|
||||
<GateStripe />
|
||||
<SectionHead mark="violet" title={t.gateSection} style={{ paddingTop: 12, color: 'var(--violet)' }} />
|
||||
{approvals.map((a) => {
|
||||
const isFolded = folded.has(a.id);
|
||||
return (
|
||||
<div key={a.id} style={{ marginBottom: 12 }}>
|
||||
<GateCard gate={a.gate} kindLabel={t.gates[a.gate]} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.doc}
|
||||
collapsed={isFolded} foldable onToggleFold={() => toggleFold(a.id)}
|
||||
foldTitle={isFolded ? t.gateExpand : t.gateCollapse}
|
||||
onHeaderDoubleClick={() => setPreview(a)} headerTitle={t.gateDblTip}
|
||||
headerExtra={<button type="button" title={t.fullRead} onClick={() => setPreview(a)}
|
||||
style={{ fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1, cursor: 'pointer', background: 'transparent', color: 'var(--muted)', border: '1px solid var(--line)', borderRadius: 'var(--radius-sm,4px)', padding: '3px 7px' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--violet)'; e.currentTarget.style.borderColor = 'var(--violet-dim)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>⛶</button>}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<Button variant="accept" onClick={() => onDecide(a, 'accept')}>{t.accept}</Button>
|
||||
<Button variant="reject" onClick={() => setRejecting(rejecting === a.id ? null : a.id)}>{t.reject}</Button>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
<Button variant="ghost" size="xs" title={t.fullRead} onClick={() => setPreview(a)}>⛶ {t.fullRead}</Button>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
}>
|
||||
{rejecting === a.id ? (
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginTop: 10 }}>
|
||||
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
||||
<Button variant="reject" disabled={!reason.trim()}
|
||||
onClick={() => { onDecide(a, 'reject', reason); setRejecting(null); setReason(''); }}>
|
||||
{t.confirmReject}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</GateCard>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<GatePreview item={preview} t={t} onDecide={onDecide} onClose={() => setPreview(null)} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
window.MaestroKitGateSection = GateSection;
|
||||
@@ -1,397 +0,0 @@
|
||||
// 左栏:logo + 项目列表(git 图标)+ WS 状态脚 · 可折叠(折叠后仅图标)
|
||||
// 项目 logo:图片优先(原型无图),回退首字母色块 hsl(h 45% 60%)
|
||||
function ProjectLogo({ project, size = 26 }) {
|
||||
return (
|
||||
<span style={{
|
||||
flex: 'none', width: size, height: size, borderRadius: 5,
|
||||
display: 'grid', placeItems: 'center',
|
||||
fontSize: size * 0.5, fontWeight: 700, color: '#0a0d0b',
|
||||
background: 'hsl(' + (project.hue || 140) + ' 45% 60%)',
|
||||
}}>{(project.name || '?')[0].toUpperCase()}</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MaestroGitIcon({ size = 16 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flex: 'none' }}>
|
||||
<line x1="6" y1="3" x2="6" y2="15" />
|
||||
<circle cx="18" cy="6" r="3" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<path d="M18 9a9 9 0 0 1-9 9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// 像素章鱼 mark(折叠态 logo)
|
||||
function MaestroMark({ scale = 2.2 }) {
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const MAP = ['.....LLLLL.....','...LLLLLLLLL...','..GGGGGGGGGGG..','..GGEEGGGEEGG..','..GGEEGGGEEGG..','..GGGGGGGGGGG..','...GGGGGGGGG...','...G..G.G..G...','...G..G.G..G...','..G...G.G...G..','..D...D.D...D..','.D...D...D...D.'];
|
||||
const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' };
|
||||
const ctx = ref.current.getContext('2d');
|
||||
MAP.forEach((row, y) => [...row].forEach((ch, x) => {
|
||||
if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); }
|
||||
}));
|
||||
}, []);
|
||||
return <canvas ref={ref} width="15" height="12"
|
||||
style={{ width: 15 * scale, height: 12 * scale, imageRendering: 'pixelated', filter: 'drop-shadow(0 0 6px rgba(95,221,125,.4))' }} />;
|
||||
}
|
||||
|
||||
// 项目状态:运行中(青脉冲) / 暂停(faint) / 阻塞(琥珀) / 空闲(muted)
|
||||
function projStateMeta(state, t) {
|
||||
return ({
|
||||
running: { color: 'var(--cyan)', pulse: true, label: t.projRunning },
|
||||
paused: { color: 'var(--faint)', pulse: false, label: t.projPaused },
|
||||
blocked: { color: 'var(--amber)', pulse: true, label: t.projBlocked },
|
||||
idle: { color: 'var(--muted)', pulse: false, label: t.projIdle },
|
||||
})[state] || { color: 'var(--muted)', pulse: false, label: t.projIdle };
|
||||
}
|
||||
|
||||
function ProjStatusDot({ meta, size = 7 }) {
|
||||
return (
|
||||
<span style={{
|
||||
flex: 'none', width: size, height: size, borderRadius: '50%', background: meta.color,
|
||||
boxShadow: '0 0 7px ' + meta.color,
|
||||
animation: meta.pulse ? 'maestro-pulse 1.4s infinite' : 'none',
|
||||
}}></span>
|
||||
);
|
||||
}
|
||||
|
||||
// 侧栏底部小行:图标 + 主文 + 副文(折叠时仅图标,hover 高亮)
|
||||
function SideRow({ icon, label, detail, onClick, collapsed, title, accent }) {
|
||||
const interactive = !!onClick;
|
||||
return (
|
||||
<div onClick={onClick} title={title || label}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: collapsed ? '9px 0' : '8px 12px', minHeight: 40,
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
cursor: interactive ? 'pointer' : 'default', transition: 'background .1s',
|
||||
}}
|
||||
onMouseEnter={interactive ? (e) => { e.currentTarget.style.background = 'var(--panel)'; } : undefined}
|
||||
onMouseLeave={interactive ? (e) => { e.currentTarget.style.background = 'transparent'; } : undefined}>
|
||||
<span style={{ flex: 'none', color: accent || 'var(--muted)', display: 'inline-flex' }}>{icon}</span>
|
||||
{!collapsed ? (
|
||||
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{label}</span>
|
||||
{detail ? <span style={{ fontSize: 10, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{detail}</span> : null}
|
||||
</span>
|
||||
) : null}
|
||||
{!collapsed && interactive ? <span style={{ flex: 'none', color: 'var(--faint)', fontSize: 11 }}>›</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { flex: 'none' } };
|
||||
function IcoAgents() { return <svg {...sIco}><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none" /></svg>; }
|
||||
function IcoGlobalCfg() { return <svg {...sIco}><circle cx="12" cy="12" r="9" /><line x1="3" y1="12" x2="21" y2="12" /><path d="M12 3a14 14 0 0 0 0 18a14 14 0 0 0 0-18" /></svg>; }
|
||||
|
||||
function UserRow({ user, t, collapsed }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
const avatar = (
|
||||
<span style={{
|
||||
flex: 'none', width: 26, height: 26, borderRadius: '50%', display: 'grid', placeItems: 'center',
|
||||
fontSize: 12, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + (user.hue || 200) + ' 50% 62%)',
|
||||
}}>{user.initial}</span>
|
||||
);
|
||||
return (
|
||||
<div ref={ref} style={{ position: 'relative', borderTop: '1px solid var(--line-soft)' }}>
|
||||
<div onClick={() => setOpen(!open)} title={user.name + ' · ' + user.plan}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: collapsed ? '10px 0' : '10px 12px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', transition: 'background .1s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
|
||||
{avatar}
|
||||
{!collapsed ? (
|
||||
<React.Fragment>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
|
||||
<span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{user.name}</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--green)', whiteSpace: 'nowrap' }}>{user.plan}</span>
|
||||
</span>
|
||||
<span style={{ flex: 'none', color: 'var(--faint)', fontSize: 11, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}>▴</span>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
{open ? (
|
||||
<div style={{
|
||||
position: collapsed ? 'fixed' : 'absolute',
|
||||
...(collapsed
|
||||
? { left: 58, bottom: 14, width: 180 }
|
||||
: { bottom: 'calc(100% + 4px)', left: 12, right: 12, minWidth: 160 }),
|
||||
zIndex: 80,
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
|
||||
boxShadow: '0 10px 30px rgba(0,0,0,.65)', padding: 6, animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--line-soft)', marginBottom: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600 }}>{user.name} <span style={{ color: 'var(--faint)', fontWeight: 400 }}>{user.handle}</span></div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--green)', marginTop: 2 }}>{t.gUserPlan} · {user.plan}</div>
|
||||
</div>
|
||||
{[t.gUserSettings, t.gUserSignOut].map((label, i) => (
|
||||
<div key={label} style={{ padding: '6px 10px', fontSize: 12, color: i === 1 ? 'var(--red)' : 'var(--ink)', cursor: 'pointer', borderRadius: 3 }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel-2)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{label}</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function fmtTokens(n) {
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
|
||||
if (n >= 1e3) return Math.round(n / 1e3) + 'K';
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function AgentsRow({ global, summary, t, collapsed }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [pos, setPos] = React.useState(null);
|
||||
const ref = React.useRef(null);
|
||||
const rowRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && rowRef.current && !rowRef.current.contains(e.target)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
const toggle = () => {
|
||||
if (!open && rowRef.current) {
|
||||
const r = rowRef.current.getBoundingClientRect();
|
||||
const W = 286;
|
||||
const left = collapsed ? r.right + 6 : Math.min(r.left, window.innerWidth - W - 8);
|
||||
setPos({ left, bottom: window.innerHeight - r.top + 6, width: W });
|
||||
}
|
||||
setOpen((o) => !o);
|
||||
};
|
||||
const maxTok = Math.max(...summary.byProject.map((p) => p.tokens), 1);
|
||||
const detail = t.gAgentsDetail.replace('{p}', global.runningProjects).replace('{P}', global.projectCount).replace('{a}', global.runningAgents);
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div ref={rowRef}>
|
||||
<SideRow collapsed={collapsed} icon={<IcoAgents />} accent="var(--cyan)" onClick={toggle}
|
||||
label={t.gAgents} detail={detail} title={t.gAgents + ' · ' + detail} />
|
||||
</div>
|
||||
{open && pos ? (
|
||||
<div ref={ref} style={{
|
||||
position: 'fixed', left: pos.left, bottom: pos.bottom, width: pos.width, zIndex: 1000,
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
|
||||
boxShadow: '0 12px 34px rgba(0,0,0,.7)', padding: '12px 14px', animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--muted)', marginBottom: 10 }}>
|
||||
<span style={{ color: 'var(--cyan)' }}>▍</span>{t.gAgents.toUpperCase()}
|
||||
<span style={{ marginLeft: 'auto', fontWeight: 400, letterSpacing: '.04em', color: 'var(--faint)' }}>{t.apWeek}</span>
|
||||
</div>
|
||||
{/* 总结 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 12 }}>
|
||||
{[
|
||||
[fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'],
|
||||
['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'],
|
||||
[summary.runsWeek, t.apRuns, 'var(--ink)'],
|
||||
[summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'],
|
||||
].map(([v, k, c], i) => (
|
||||
<div key={i} style={{ background: 'var(--bg-deep)', padding: '8px 10px' }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 按项目 */}
|
||||
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '.18em', color: 'var(--faint)', marginBottom: 7 }}>{t.apPerProj}</div>
|
||||
<div style={{ display: 'grid', gap: 9 }}>
|
||||
{summary.byProject.map((p) => (
|
||||
<div key={p.id}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
|
||||
<span style={{ flex: 'none', width: 16, height: 16, borderRadius: 4, display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + p.hue + ' 45% 60%)' }}>{p.name[0].toUpperCase()}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
|
||||
{p.active > 0 ? (
|
||||
<span style={{ flex: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 9.5, color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 3, padding: '0 5px' }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 6px var(--cyan)', animation: 'maestro-pulse .9s infinite' }}></span>{p.active}
|
||||
</span>
|
||||
) : <span style={{ flex: 'none', fontSize: 9.5, color: 'var(--faint)' }}>{t.apIdle}</span>}
|
||||
<span style={{ marginLeft: 'auto', flex: 'none', fontSize: 11, fontWeight: 600, color: 'var(--cyan)' }}>{fmtTokens(p.tokens)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingLeft: 24 }}>
|
||||
<span style={{ flex: 1, height: 4, background: 'var(--panel-2)', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<span style={{ display: 'block', height: '100%', width: (p.tokens / maxTok * 100) + '%', background: 'var(--cyan)', boxShadow: '0 0 6px rgba(89,200,216,.5)' }}></span>
|
||||
</span>
|
||||
<span style={{ flex: 'none', fontSize: 10, color: 'var(--faint)' }}>{p.runs} {t.apRuns}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GlobalConfigRow({ global, t, collapsed }) {
|
||||
const { Select } = window.MaestroDesignSystem_a6a290;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [pos, setPos] = React.useState(null);
|
||||
const ref = React.useRef(null);
|
||||
const rowRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && rowRef.current && !rowRef.current.contains(e.target)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
const toggle = () => {
|
||||
if (!open && rowRef.current) {
|
||||
const r = rowRef.current.getBoundingClientRect();
|
||||
const W = 250;
|
||||
// 优先在行上方对齐左缘;折叠态贴侧栏右侧
|
||||
const left = collapsed ? r.right + 6 : Math.min(r.left, window.innerWidth - W - 8);
|
||||
setPos({ left, bottom: window.innerHeight - r.top + 6, width: W });
|
||||
}
|
||||
setOpen((o) => !o);
|
||||
};
|
||||
const stat = (k, v, accent) => (
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, padding: '5px 0', borderBottom: '1px solid var(--line-soft)' }}>
|
||||
<span style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.06em' }}>{k}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: accent || 'var(--ink)' }}>{v}</span>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div ref={rowRef}>
|
||||
<SideRow collapsed={collapsed} icon={<IcoGlobalCfg />} onClick={toggle}
|
||||
label={t.gConfig}
|
||||
detail={t.gConfigDetail.replace('{n}', global.maxAgents).replace('{v}', global.daemon)}
|
||||
title={t.gConfig} />
|
||||
</div>
|
||||
{open && pos ? (
|
||||
<div ref={ref} style={{
|
||||
position: 'fixed', left: pos.left, bottom: pos.bottom, width: pos.width,
|
||||
zIndex: 1000, background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
|
||||
boxShadow: '0 12px 34px rgba(0,0,0,.7)', padding: '12px 14px', animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--muted)', marginBottom: 8 }}>
|
||||
<span style={{ color: 'var(--cyan)' }}>▍</span>{t.gConfig.toUpperCase()}
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{stat('daemon', global.daemon, 'var(--green)')}
|
||||
{stat('port', ':' + global.port)}
|
||||
{stat('uptime', global.uptime)}
|
||||
{stat(t.gAgents, global.runningAgents + ' / ' + global.maxAgents, 'var(--cyan)')}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
|
||||
<span>{t.maxConcurrency}</span>
|
||||
<Select defaultValue={String(global.maxAgents)} style={{ width: '100%' }} options={['1', '2', '4', '6', '8'].map((n) => ({ value: n, label: n }))} />
|
||||
</label>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
|
||||
<span>{t.workMode}</span>
|
||||
<Select defaultValue={global.autonomy} style={{ width: '100%' }} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig }) {
|
||||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<aside style={{ background: 'var(--bg-deep)', borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column', overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
<div style={{ padding: collapsed ? '16px 0 12px' : '20px 16px 14px', borderBottom: '1px solid var(--line-soft)', display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'flex-start' }}>
|
||||
{collapsed ? <MaestroMark /> : (
|
||||
<React.Fragment>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<MaestroMark scale={1.8} />
|
||||
<span style={{ fontSize: 19, fontWeight: 700, letterSpacing: '.28em', color: 'var(--green)', textShadow: '0 0 12px rgba(95,221,125,.45)' }}>
|
||||
MAESTRO<span style={{ marginLeft: 1, animation: 'maestro-blink 1.1s steps(1) infinite' }}>▮</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 5, fontSize: 9, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.2em', lineHeight: 1.7, textTransform: 'uppercase' }}>{t.logoSub}</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, fontWeight: 600, letterSpacing: '.22em', color: 'var(--muted)', padding: '16px 16px 8px' }}>
|
||||
<span style={{ color: 'var(--green)' }}>▍</span>{t.projects}
|
||||
<span style={{ marginLeft: 'auto' }}><Button variant="ghost" size="xs" onClick={onNewProject}>{t.newProject}</Button></span>
|
||||
</div>
|
||||
) : <div style={{ height: 12 }}></div>}
|
||||
<ul style={{ listStyle: 'none', flex: 1, margin: 0, padding: 0 }}>
|
||||
{projects.map((p) => {
|
||||
const active = p.id === currentId;
|
||||
const meta = projStateMeta(p.state, t);
|
||||
return (
|
||||
<li key={p.id} onClick={() => onSelect(p.id)} title={p.name + ' · ' + meta.label + (p.pending ? ' · ' + t.projPendingTip.replace('{n}', p.pending) : '')}
|
||||
style={{
|
||||
position: 'relative',
|
||||
padding: collapsed ? '10px 0' : '9px 14px 9px 12px', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 9,
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
borderLeft: '2px solid ' + (active ? 'var(--green)' : 'transparent'),
|
||||
background: active ? 'var(--panel-2)' : 'transparent',
|
||||
color: active ? 'var(--green)' : 'var(--muted)',
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
|
||||
<ProjectLogo project={p} size={collapsed ? 26 : 24} />
|
||||
{!collapsed ? (
|
||||
<React.Fragment>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 13, color: active ? 'var(--green)' : 'var(--ink)' }}>{p.name}</span>
|
||||
<span style={{ fontSize: 10.5, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.path}</span>
|
||||
</span>
|
||||
<span style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{p.pending > 0 ? (
|
||||
<span title={t.projPendingTip.replace('{n}', p.pending)} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 9.5, fontWeight: 700, lineHeight: '15px',
|
||||
minWidth: 15, height: 15, textAlign: 'center', padding: '0 4px',
|
||||
color: 'var(--bg-deep)', background: 'var(--violet)', borderRadius: 8,
|
||||
}}>{p.pending}</span>
|
||||
) : null}
|
||||
<ProjStatusDot meta={meta} />
|
||||
</span>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<span style={{ position: 'absolute', top: 7, right: 9, display: 'flex', alignItems: 'center' }}>
|
||||
<ProjStatusDot meta={meta} size={p.pending > 0 ? 8 : 6} />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{/* ── 底部:全局 agent · 全局配置 · 用户 ── */}
|
||||
<div style={{ borderTop: '1px solid var(--line-soft)' }}>
|
||||
<AgentsRow global={global} summary={summary} t={t} collapsed={collapsed} />
|
||||
<GlobalConfigRow global={global} t={t} collapsed={collapsed} />
|
||||
</div>
|
||||
<UserRow user={user} t={t} collapsed={collapsed} />
|
||||
<div style={{ borderTop: '1px solid var(--line-soft)', padding: collapsed ? '10px 0' : '10px 12px', fontSize: 11, color: 'var(--muted)', display: 'flex', alignItems: 'center', gap: 7, justifyContent: collapsed ? 'center' : 'flex-start' }}>
|
||||
{!collapsed ? (
|
||||
<React.Fragment>
|
||||
<span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 8px var(--green)', display: 'inline-block', flex: 'none' }}></span>
|
||||
<span style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{t.connected}</span>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
<button onClick={onToggleCollapse} title={collapsed ? t.expandSide : t.collapseSide}
|
||||
style={{
|
||||
marginLeft: collapsed ? 0 : 'auto', flex: 'none', cursor: 'pointer',
|
||||
fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1,
|
||||
background: 'transparent', color: 'var(--muted)',
|
||||
border: '1px solid var(--line)', borderRadius: 'var(--radius-sm, 4px)', padding: '4px 7px',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--green)'; e.currentTarget.style.borderColor = 'var(--green-dim)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>
|
||||
{collapsed ? '»' : '«'}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
window.MaestroKitSidebar = Sidebar;
|
||||
@@ -1,317 +0,0 @@
|
||||
// 任务树:筛选栏(搜索+复杂度+状态分组)+ 折叠层级 + 复杂度可点换档 + 依赖可视化跳转 + 展开详情;新建任务表单
|
||||
const KIT_FILTER_GROUPS = [
|
||||
['todo', ['init', 'ready', 'blocked']],
|
||||
['doing', ['analyzing', 'speccing', 'queued', 'executing', 'decomposed']],
|
||||
['gate', ['plan_review', 'spec_review', 'exec_review']],
|
||||
['bad', ['failed', 'needs_attention']],
|
||||
['hold', ['paused', 'cancelled']],
|
||||
['done', ['done']],
|
||||
];
|
||||
|
||||
function kitFlatten(tasks, map = new Map()) {
|
||||
for (const tk of tasks) { map.set(tk.id, tk); if (tk.children) kitFlatten(tk.children, map); }
|
||||
return map;
|
||||
}
|
||||
|
||||
// 复杂度徽章 + 点击换档下拉
|
||||
function CplxPicker({ task, cplx, onChange }) {
|
||||
const { ComplexityBadge } = window.MaestroDesignSystem_a6a290;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
return (
|
||||
<span ref={ref} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }} onClick={(e) => e.stopPropagation()}>
|
||||
<button onClick={() => setOpen(!open)} title="点击调整复杂度" style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex' }}>
|
||||
<ComplexityBadge complexity={cplx} />
|
||||
</button>
|
||||
{open ? (
|
||||
<span style={{
|
||||
position: 'absolute', top: 'calc(100% + 5px)', right: 0, zIndex: 60,
|
||||
display: 'flex', gap: 5, background: 'var(--bg-deep)', border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--radius-md, 6px)', padding: 6, boxShadow: '0 10px 30px rgba(0,0,0,.65)',
|
||||
animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
{['hard', 'medium', 'easy'].map((v) => (
|
||||
<button key={v} onClick={() => { onChange(task.id, v); setOpen(false); }} style={{
|
||||
background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex',
|
||||
outline: v === cplx ? '1px solid currentColor' : 'none', outlineOffset: 1,
|
||||
filter: 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.filter = 'brightness(1.35)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.filter = 'none'; }}>
|
||||
<ComplexityBadge complexity={v} />
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterBar({ t, kw, setKw, cplxSet, toggleCplx, statusSet, toggleGroup, matchCount, filtering, onClear }) {
|
||||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||||
const chip = (on, color, dim, label, onClick) => (
|
||||
<button key={label} onClick={onClick} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 700, letterSpacing: '.1em',
|
||||
background: on ? 'rgba(95,221,125,.08)' : 'transparent',
|
||||
color: on ? color : 'var(--faint)',
|
||||
border: '1px solid ' + (on ? dim : 'var(--line)'),
|
||||
borderRadius: 3, padding: '2px 8px', cursor: 'pointer', transition: 'all .1s', whiteSpace: 'nowrap',
|
||||
}}>{label}</button>
|
||||
);
|
||||
return (
|
||||
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px', marginBottom: 12, display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<input value={kw} onChange={(e) => setKw(e.target.value)} placeholder={t.searchPh} autoComplete="off" style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 12, width: 190, padding: '4px 9px',
|
||||
background: 'var(--bg-deep)', color: 'var(--ink)', border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--radius-sm, 4px)', outline: 'none',
|
||||
}} />
|
||||
<span style={{ display: 'inline-flex', gap: 6 }}>
|
||||
{chip(cplxSet.has('hard'), 'var(--red)', 'var(--red-dim)', 'HARD', () => toggleCplx('hard'))}
|
||||
{chip(cplxSet.has('medium'), 'var(--amber)', 'var(--amber-dim)', 'MED', () => toggleCplx('medium'))}
|
||||
{chip(cplxSet.has('easy'), 'var(--green)', 'var(--green-dim)', 'EASY', () => toggleCplx('easy'))}
|
||||
</span>
|
||||
<span style={{ flex: 1 }}></span>
|
||||
{filtering ? <span style={{ fontSize: 11, color: 'var(--amber)', letterSpacing: '.08em' }}>{t.matchCount.replace('{n}', matchCount)}</span> : null}
|
||||
{filtering ? <Button variant="ghost" size="xs" onClick={onClear}>{t.clearFilter}</Button> : null}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{KIT_FILTER_GROUPS.map(([g, sts]) => {
|
||||
const on = sts.every((s) => statusSet.has(s));
|
||||
const part = !on && sts.some((s) => statusSet.has(s));
|
||||
return (
|
||||
<button key={g} onClick={() => toggleGroup(sts)} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '.04em',
|
||||
background: 'transparent',
|
||||
color: on ? 'var(--green)' : part ? 'var(--amber)' : 'var(--muted)',
|
||||
border: '1px dashed ' + (on ? 'var(--green-dim)' : part ? 'var(--amber-dim)' : 'var(--line-soft)'),
|
||||
borderRadius: 3, padding: '2px 9px', cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
}}>{t.fgroups[g]}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleOpen, t, byId, cplxOf, onChangeCplx, flashId, onJump, visibleSet }) {
|
||||
const { StatusChip } = window.MaestroDesignSystem_a6a290;
|
||||
const kids = (task.children || []).filter((k) => !visibleSet || visibleSet.has(k.id));
|
||||
const open = openIds.has(task.id) || !!visibleSet; // 筛选时自动展开可见节点
|
||||
const expanded = expandedId === task.id;
|
||||
const isGate = ['plan_review', 'spec_review', 'exec_review'].includes(task.status);
|
||||
const flashing = flashId === task.id;
|
||||
const [hover, setHover] = React.useState(false);
|
||||
return (
|
||||
<div style={depth > 0 ? { borderLeft: '1px solid var(--line-soft)' } : null}>
|
||||
<div onClick={() => setExpandedId(expanded ? null : task.id)} data-task-id={task.id}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px 8px 6px',
|
||||
borderBottom: '1px solid var(--line-soft)', cursor: 'pointer', transition: 'background .1s',
|
||||
background: expanded ? 'var(--panel-2)' : isGate ? 'rgba(184,142,245,.05)' : hover ? 'var(--panel)' : 'transparent',
|
||||
opacity: ctx ? .55 : 1,
|
||||
animation: flashing ? 'maestro-locate 1.8s ease-out' : 'none',
|
||||
}}>
|
||||
<span onClick={(e) => { e.stopPropagation(); if (kids.length) toggleOpen(task.id); }}
|
||||
style={{
|
||||
width: 16, flex: 'none', textAlign: 'center', fontSize: 10, userSelect: 'none',
|
||||
color: kids.length ? (open ? 'var(--green)' : 'var(--muted)') : 'var(--faint)',
|
||||
transform: kids.length && open ? 'rotate(90deg)' : 'none', transition: 'transform .12s',
|
||||
}}>
|
||||
{kids.length ? '▶' : '·'}
|
||||
</span>
|
||||
<span style={{ fontWeight: 500, color: ctx ? 'var(--muted)' : 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{task.title}<span style={{ color: 'var(--faint)', fontSize: 10.5, marginLeft: 6 }}>{task.id}</span>
|
||||
</span>
|
||||
<span style={{ flex: 1, minWidth: 8 }}></span>
|
||||
{task.deps ? (
|
||||
<span style={{ flex: 'none', fontSize: 10.5, letterSpacing: '.06em', color: 'var(--amber)', border: '1px dashed var(--amber-dim)', borderRadius: 3, padding: '1px 7px', lineHeight: 1.5, whiteSpace: 'nowrap' }}>
|
||||
{t.depsWait} {task.deps.length}
|
||||
</span>
|
||||
) : null}
|
||||
<span style={{ fontSize: 10.5, color: task.prio === 'P0' ? 'var(--amber)' : 'var(--faint)', flex: 'none' }}>{task.prio}</span>
|
||||
<CplxPicker task={task} cplx={cplxOf(task)} onChange={onChangeCplx} />
|
||||
<StatusChip status={task.status} label={t.status[task.status]} />
|
||||
</div>
|
||||
{expanded ? <TaskDetail task={task} t={t} byId={byId} onJump={onJump} /> : null}
|
||||
{kids.length && open ? (
|
||||
<div style={{ marginLeft: 22 }}>
|
||||
{kids.map((k) => (
|
||||
<TaskRow key={k.id} task={k} depth={depth + 1} ctx={visibleSet ? visibleSet.ctx.has(k.id) : false}
|
||||
expandedId={expandedId} setExpandedId={setExpandedId} openIds={openIds} toggleOpen={toggleOpen} t={t}
|
||||
byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskDetail({ task, t, byId, onJump }) {
|
||||
const label = task.doc ? t.specLabel : task.ops ? t.opsLabel : null;
|
||||
const text = task.doc || task.ops;
|
||||
return (
|
||||
<div style={{ background: 'var(--panel)', borderBottom: '1px solid var(--line)', borderLeft: '2px solid var(--green-dim)', padding: '14px 16px', animation: 'maestro-rise .2s ease both', display: 'grid', gap: 14 }}>
|
||||
{label ? (
|
||||
<div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{label}</div>
|
||||
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)', borderRadius: 4 }}>{text}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '10px 12px', border: '1px dashed var(--line)', borderRadius: 6, color: 'var(--faint)', fontStyle: 'italic', fontSize: 12 }}>
|
||||
{t.pendingDoc}
|
||||
</div>
|
||||
)}
|
||||
{task.deps && task.deps.length ? (
|
||||
<div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{t.depsLabel}</div>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
{task.deps.map((d) => {
|
||||
const dep = byId.get(d);
|
||||
const ok = dep && dep.status === 'done';
|
||||
return (
|
||||
<div key={d} onClick={(e) => { e.stopPropagation(); onJump(d); }} title="→" style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, padding: '4px 10px', cursor: 'pointer',
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 4,
|
||||
borderLeft: '2px solid ' + (ok ? 'var(--green-dim)' : 'var(--amber-dim)'),
|
||||
transition: 'border-color .12s, background .12s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel-2)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--bg-deep)'; }}>
|
||||
<span style={{ color: ok ? 'var(--green)' : 'var(--amber)', flex: 'none' }}>{ok ? '✓' : '◌'}</span>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{dep ? dep.title : d}</span>
|
||||
<span style={{ flex: 'none', fontSize: 10.5, color: ok ? 'var(--green)' : 'var(--amber)' }}>{ok ? t.depDone : t.depWait}</span>
|
||||
<span style={{ marginLeft: 'auto', color: 'var(--faint)', fontSize: 12 }}>→</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
|
||||
const { Button, Input, Select, ComplexitySeg } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<form style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: 14, marginBottom: 14 }}
|
||||
onSubmit={(e) => { e.preventDefault(); onCreate(); }}>
|
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<span style={{ flex: 1, minWidth: 320, display: 'flex' }}><Input label={t.titleLabel} required placeholder={t.titlePlaceholder} style={{ width: '100%' }} /></span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap', marginTop: 12 }}>
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
|
||||
<span>{t.complexity} <span style={{ color: 'var(--red)' }}>*</span></span>
|
||||
<ComplexitySeg defaultValue="auto" includeAuto autoLabel={t.cplxAuto} />
|
||||
</label>
|
||||
<Select label={t.priority} defaultValue="1" options={[
|
||||
{ value: '0', label: t.p0 }, { value: '1', label: t.p1 }, { value: '2', label: t.p2 },
|
||||
]} />
|
||||
<span style={{ flex: 1, minWidth: 240, display: 'flex' }}>
|
||||
<Select label={t.parentTask} style={{ width: '100%' }} options={[{ value: '', label: t.topLevel }, ...tasks.map((tk) => ({ value: tk.id, label: tk.title }))]} />
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--line-soft)' }}>
|
||||
<Button onClick={onCancel}>{t.cancel}</Button>
|
||||
<Button variant="solid" type="submit">{t.create}</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskSection({ tasks, onToast, t }) {
|
||||
const { Button, SectionHead } = window.MaestroDesignSystem_a6a290;
|
||||
const [expandedId, setExpandedId] = React.useState(null);
|
||||
const [openIds, setOpenIds] = React.useState(() => new Set(['t1']));
|
||||
const [showNew, setShowNew] = React.useState(false);
|
||||
const [kw, setKw] = React.useState('');
|
||||
const [cplxSet, setCplxSet] = React.useState(() => new Set());
|
||||
const [statusSet, setStatusSet] = React.useState(() => new Set());
|
||||
const [cplxOverride, setCplxOverride] = React.useState({});
|
||||
const [flashId, setFlashId] = React.useState(null);
|
||||
const byId = React.useMemo(() => kitFlatten(tasks), [tasks]);
|
||||
const cplxOf = (task) => cplxOverride[task.id] || task.cplx;
|
||||
const onChangeCplx = (id, v) => setCplxOverride((m) => ({ ...m, [id]: v }));
|
||||
const toggleOpen = (id) => setOpenIds((prev) => {
|
||||
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
|
||||
});
|
||||
const toggleCplx = (v) => setCplxSet((prev) => {
|
||||
const next = new Set(prev); next.has(v) ? next.delete(v) : next.add(v); return next;
|
||||
});
|
||||
const toggleGroup = (sts) => setStatusSet((prev) => {
|
||||
const next = new Set(prev);
|
||||
const allOn = sts.every((s) => next.has(s));
|
||||
for (const s of sts) allOn ? next.delete(s) : next.add(s);
|
||||
return next;
|
||||
});
|
||||
const filtering = kw.trim() !== '' || cplxSet.size > 0 || statusSet.size > 0;
|
||||
const clearFilters = () => { setKw(''); setCplxSet(new Set()); setStatusSet(new Set()); };
|
||||
|
||||
// 筛选:自身命中 → 显示;祖先链作为上下文淡显;命中节点的父级自动展开
|
||||
const { visibleSet, matchCount } = React.useMemo(() => {
|
||||
if (!filtering) return { visibleSet: null, matchCount: 0 };
|
||||
const matches = (tk) =>
|
||||
(kw.trim() === '' || tk.title.toLowerCase().includes(kw.trim().toLowerCase())) &&
|
||||
(cplxSet.size === 0 || cplxSet.has(cplxOverride[tk.id] || tk.cplx)) &&
|
||||
(statusSet.size === 0 || statusSet.has(tk.status));
|
||||
const vis = new Set(); const ctx = new Set(); let count = 0;
|
||||
const walk = (tk) => {
|
||||
let childHit = false;
|
||||
for (const k of tk.children || []) if (walk(k)) childHit = true;
|
||||
const hit = matches(tk);
|
||||
if (hit) count++;
|
||||
if (hit || childHit) { vis.add(tk.id); if (!hit) ctx.add(tk.id); return true; }
|
||||
return false;
|
||||
};
|
||||
for (const tk of tasks) walk(tk);
|
||||
const set = new Set(vis); set.ctx = ctx;
|
||||
return { visibleSet: set, matchCount: count };
|
||||
}, [filtering, kw, cplxSet, statusSet, tasks, cplxOverride]);
|
||||
|
||||
// 依赖跳转:展开所有祖先 + flash 定位
|
||||
const onJump = (id) => {
|
||||
setOpenIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
const openAncestors = (list, chain) => {
|
||||
for (const tk of list) {
|
||||
if (tk.id === id) { for (const c of chain) next.add(c); return true; }
|
||||
if (tk.children && openAncestors(tk.children, [...chain, tk.id])) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
openAncestors(tasks, []);
|
||||
return next;
|
||||
});
|
||||
setFlashId(null);
|
||||
requestAnimationFrame(() => setFlashId(id));
|
||||
setTimeout(() => setFlashId((f) => (f === id ? null : f)), 1900);
|
||||
};
|
||||
|
||||
const roots = visibleSet ? tasks.filter((tk) => visibleSet.has(tk.id)) : tasks;
|
||||
return (
|
||||
<section>
|
||||
<SectionHead title={t.taskSection} sticky action={<Button variant="ghost" size="xs" onClick={() => setShowNew(!showNew)}>{t.newTask}</Button>} />
|
||||
{showNew ? <NewTaskPanel tasks={tasks} t={t} onCancel={() => setShowNew(false)}
|
||||
onCreate={() => { setShowNew(false); onToast('ok', t.toastCreated); }} /> : null}
|
||||
<FilterBar t={t} kw={kw} setKw={setKw} cplxSet={cplxSet} toggleCplx={toggleCplx}
|
||||
statusSet={statusSet} toggleGroup={toggleGroup} matchCount={matchCount} filtering={filtering} onClear={clearFilters} />
|
||||
<div>
|
||||
{roots.map((tk) => (
|
||||
<TaskRow key={tk.id} task={tk} depth={0} ctx={visibleSet ? visibleSet.ctx.has(tk.id) : false}
|
||||
expandedId={expandedId} setExpandedId={setExpandedId} openIds={openIds} toggleOpen={toggleOpen} t={t}
|
||||
byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MaestroKitTaskSection: TaskSection });
|
||||
@@ -1,172 +0,0 @@
|
||||
// 顶栏动作:默认仅图标,hover 横向展开文字(与 CountBadge 同交互)
|
||||
const maestroIconActionCss = `
|
||||
.m-iact{display:inline-flex;align-items:center;height:28px;padding:0 8px;cursor:pointer;font-family:var(--mono);font-size:11.5px;font-weight:600;letter-spacing:.08em;line-height:1;background:transparent;color:var(--muted);border:1px solid var(--line);border-radius:var(--radius-sm,4px);transition:color .12s,border-color .12s;white-space:nowrap}
|
||||
.m-iact svg{flex:none}
|
||||
.m-iact .m-iact-label{max-width:0;opacity:0;overflow:hidden;transition:max-width .28s ease,opacity .22s ease,margin-left .28s ease}
|
||||
.m-iact:hover{color:var(--green);border-color:var(--green-dim)}
|
||||
.m-iact:hover .m-iact-label{max-width:9em;opacity:1;margin-left:6px}
|
||||
`;
|
||||
function ensureIconActionCss() {
|
||||
if (document.getElementById('m-iact-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-iact-css'; s.textContent = maestroIconActionCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
function IconAction({ icon, label, title, onClick }) {
|
||||
ensureIconActionCss();
|
||||
return (
|
||||
<button type="button" className="m-iact" title={title || label} onClick={onClick}>
|
||||
{icon}
|
||||
<span className="m-iact-label">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
const iactSvg = { fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', width: 14, height: 14, viewBox: '0 0 24 24' };
|
||||
function SyncIcon() {
|
||||
return <svg {...iactSvg}><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /></svg>;
|
||||
}
|
||||
function ConfigIcon() {
|
||||
return <svg {...iactSvg}><line x1="3" y1="6" x2="12" y2="6" /><circle cx="15" cy="6" r="2.5" /><line x1="18" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="5.5" y2="12" /><circle cx="9" cy="12" r="2.5" /><line x1="12.5" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="12.5" y2="18" /><circle cx="16" cy="18" r="2.5" /><line x1="19.5" y1="18" x2="21" y2="18" /></svg>;
|
||||
}
|
||||
|
||||
function LangIcon() {
|
||||
return <svg {...iactSvg}><circle cx="12" cy="12" r="9" /><path d="M3 12h18" /><path d="M12 3a14 14 0 0 1 0 18a14 14 0 0 1 0-18" /></svg>;
|
||||
}
|
||||
function SunIcon() {
|
||||
return <svg {...iactSvg}><circle cx="12" cy="12" r="4" /><line x1="12" y1="2" x2="12" y2="5" /><line x1="12" y1="19" x2="12" y2="22" /><line x1="2" y1="12" x2="5" y2="12" /><line x1="19" y1="12" x2="22" y2="12" /><line x1="4.9" y1="4.9" x2="7" y2="7" /><line x1="17" y1="17" x2="19.1" y2="19.1" /><line x1="4.9" y1="19.1" x2="7" y2="17" /><line x1="17" y1="7" x2="19.1" y2="4.9" /></svg>;
|
||||
}
|
||||
function MoonIcon() {
|
||||
return <svg {...iactSvg}><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg>;
|
||||
}
|
||||
|
||||
function LangMenu({ t, lang, onSelectLang }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
return (
|
||||
<span ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<span onClick={() => setOpen(!open)}>
|
||||
<IconAction icon={<LangIcon />} label={t.lang} title={t.langTip} onClick={() => {}} />
|
||||
</span>
|
||||
{open ? (
|
||||
<div style={{
|
||||
position: 'absolute', top: 'calc(100% + 5px)', right: 0, zIndex: 60,
|
||||
display: 'grid', minWidth: 124,
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md, 6px)',
|
||||
padding: 4, boxShadow: '0 10px 30px rgba(0,0,0,.65)', animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
{window.MAESTRO_LANGS.map(([code, name]) => {
|
||||
const active = code === lang;
|
||||
return (
|
||||
<button key={code} onClick={() => { onSelectLang(code); setOpen(false); }} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 12, textAlign: 'left',
|
||||
background: active ? 'var(--panel-2)' : 'transparent',
|
||||
color: active ? 'var(--green)' : 'var(--ink)',
|
||||
border: 'none', borderRadius: 4, padding: '7px 10px', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
|
||||
<span style={{ width: 12, color: 'var(--green)' }}>{active ? '▍' : ''}</span>{name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// 顶栏:项目标题 + 徽章组 + 动作;agent 执行面板
|
||||
function Topbar({ project, counts, onSync, onToggleConfig, t, theme, onToggleTheme, lang, onSelectLang }) {
|
||||
const { Button, CountBadge } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<header style={{ display: 'flex', alignItems: 'flex-end', gap: 14, padding: '22px 0 14px', borderBottom: '1px solid var(--line)' }}>
|
||||
<div style={{ minWidth: 0, flex: '0 1 auto' }}>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, letterSpacing: '.04em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{project.name}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={project.path + ' · ' + project.branch + ' · ' + project.autonomy}>{project.path} · {project.branch} · {project.autonomy}</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<CountBadge kind="gate" count={counts.gate} label={t.badgeGate} title={t.badgeGateTip} />
|
||||
<CountBadge kind="ready" count={counts.ready} label={t.badgeReady} title={t.badgeReadyTip} />
|
||||
<CountBadge kind="run" count={counts.run} label={t.badgeRun} title={t.badgeRunTip} />
|
||||
<CountBadge kind="blocked" count={counts.blocked} label={t.badgeBlocked} title={t.badgeBlockedTip} />
|
||||
<CountBadge kind="total" count={counts.total} label={t.badgeTotal} title={t.badgeTotalTip} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<IconAction icon={<ConfigIcon />} label={t.config} title={t.configTip} onClick={onToggleConfig} />
|
||||
<LangMenu t={t} lang={lang} onSelectLang={onSelectLang} />
|
||||
<IconAction icon={theme === 'light' ? <MoonIcon /> : <SunIcon />} label={theme === 'light' ? t.themeDark : t.themeLight} title={t.themeTip} onClick={onToggleTheme} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigPanel({ project, onSave, onClose, onSync, t }) {
|
||||
const { Button, Input, Select } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<section style={{ position: 'relative', zIndex: 30, background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: '12px 14px', marginTop: 12, animation: 'maestro-rise .18s ease both' }}>
|
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<Input label={t.maxConcurrency} type="number" defaultValue={String(project.concurrency)} width={76} />
|
||||
<Select label={t.workMode} defaultValue={project.autonomy} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)', letterSpacing: '.06em', marginLeft: 'auto', paddingBottom: 7 }}>
|
||||
{t.current}:{project.concurrency} · {project.autonomy}
|
||||
</span>
|
||||
<Button variant="solid" onClick={onSave}>{t.save}</Button>
|
||||
<Button onClick={onClose}>{t.collapse}</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap', marginTop: 12 }}>
|
||||
<span style={{ flex: 2, minWidth: 220, display: 'flex' }}><Input label={t.projLogo} placeholder={t.logoPh} style={{ width: '100%' }} /></span>
|
||||
<span style={{ flex: 1, minWidth: 170, display: 'flex' }}><Input label={t.verifyCmd} placeholder={t.verifyPh} style={{ width: '100%' }} /></span>
|
||||
<span style={{ flex: 1, minWidth: 170, display: 'flex' }}><Input label={t.model} placeholder={t.modelPh} style={{ width: '100%' }} /></span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 10, paddingTop: 10, borderTop: '1px dashed var(--line-soft)' }}>
|
||||
<span style={{ fontSize: 10.5, color: 'var(--faint)', letterSpacing: '.06em' }}>{t.lastSync}</span>
|
||||
<Button size="xs" onClick={onSync}>⟳ {t.sync}</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentSection({ agents, quota, t }) {
|
||||
const { SectionHead, QuotaMeter } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<section>
|
||||
<SectionHead mark="cyan" title={<span>{t.agentSection}<span style={{ color: 'var(--cyan)', letterSpacing: '.08em', marginLeft: 8 }}>{agents.length > 0 ? agents.length : ''}</span></span>} sticky />
|
||||
{quota ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 22, flexWrap: 'wrap', fontFamily: 'var(--mono)', margin: '0 0 12px' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.08em' }}>{t.quota}</span>
|
||||
<QuotaMeter label="5h" pct={quota.five.pct} detail={t.quotaReset.replace('{t}', quota.five.reset)} />
|
||||
<QuotaMeter label={t.quotaWeek} pct={quota.week.pct} detail={t.quotaReset.replace('{t}', quota.week.reset)} />
|
||||
</div>
|
||||
) : null}
|
||||
{agents.length === 0 ? (
|
||||
<div style={{ padding: '12px 14px', color: 'var(--faint)', fontSize: 12, border: '1px dashed var(--line)', borderRadius: 6 }}>{t.agentEmpty}</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: 20, alignItems: 'stretch', background: 'var(--panel)', border: '1px solid var(--cyan-dim)', borderRadius: 6, padding: '12px 16px', animation: 'maestro-rise .2s ease both' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minWidth: 64, padding: '4px 8px', borderRight: '1px solid var(--line-soft)' }}>
|
||||
<span style={{ fontSize: 40, fontWeight: 700, lineHeight: 1, color: 'var(--cyan)', textShadow: '0 0 18px rgba(89,200,216,.5)' }}>{agents.length}</span>
|
||||
<span style={{ fontSize: 9, fontWeight: 600, letterSpacing: '.3em', color: 'var(--muted)', marginTop: 6 }}>RUNNING</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'grid', gap: 10, minWidth: 0, alignContent: 'center' }}>
|
||||
{agents.map((a) => (
|
||||
<div key={a.id} style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 12, padding: '3px 0' }}>
|
||||
<span style={{ flex: 'none', width: 6, height: 6, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 8px var(--cyan)', animation: 'maestro-pulse .9s infinite' }}></span>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.title}</span>
|
||||
<span style={{ flex: 'none', fontSize: 10, letterSpacing: '.1em', color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', padding: '0 6px', borderRadius: 3 }}>{a.kind}</span>
|
||||
<span style={{ flex: 'none', marginLeft: 'auto', fontSize: 10.5, color: 'var(--faint)' }}>{a.meta} · {a.time} {t.since}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MaestroKitTopbar: Topbar, MaestroKitConfigPanel: ConfigPanel, MaestroKitAgentSection: AgentSection });
|
||||
@@ -1,99 +0,0 @@
|
||||
// MAESTRO 调度台 · UI kit 假数据(与 src/model 状态机对齐)
|
||||
window.MAESTRO_MOCK = {
|
||||
projects: [
|
||||
{ id: 'p1', name: 'maestro', path: '~/dev/maestro', branch: 'main', autonomy: 'auto-easy', concurrency: 2, hue: 140, state: 'running', pending: 1, agents: 1 },
|
||||
{ id: 'p2', name: 'blog-engine', path: '~/dev/blog-engine', branch: 'main', autonomy: 'manual', concurrency: 1, hue: 38, state: 'paused', pending: 0, agents: 0 },
|
||||
{ id: 'p3', name: 'dotfiles', path: '~/.dotfiles', branch: 'master', autonomy: 'auto-approved', concurrency: 1, hue: 265, state: 'blocked', pending: 0, agents: 0 },
|
||||
],
|
||||
agents: [
|
||||
{ id: 'r1', project: 'maestro', title: '把轮询改为 WS 推送', kind: 'EXECUTOR', time: '06:12', meta: 'wt-127 · claude-sonnet' },
|
||||
],
|
||||
// 全局:跨项目 agent 概览 / daemon 配置 / 当前用户
|
||||
global: {
|
||||
projectCount: 3, runningProjects: 1, runningAgents: 1, maxAgents: 4,
|
||||
autonomy: 'auto-easy', daemon: 'v0.4.2', port: 4517, uptime: '3d 04h',
|
||||
},
|
||||
// Agent 汇总:本周窗口,按项目 + 总结
|
||||
agentSummary: {
|
||||
tokensWeek: 12400000, runsWeek: 47, costWeek: 38.6, activeNow: 1,
|
||||
byProject: [
|
||||
{ id: 'p1', name: 'maestro', hue: 140, active: 1, runs: 32, tokens: 8900000 },
|
||||
{ id: 'p2', name: 'blog-engine', hue: 38, active: 0, runs: 9, tokens: 2100000 },
|
||||
{ id: 'p3', name: 'dotfiles', hue: 265, active: 0, runs: 6, tokens: 1400000 },
|
||||
],
|
||||
},
|
||||
user: { name: 'jun', handle: '@jun', plan: 'Claude Max', initial: 'J', hue: 200 },
|
||||
quota: {
|
||||
five: { pct: 50, reset: '3h 8m' },
|
||||
week: { pct: 58, reset: '16h 38m' },
|
||||
},
|
||||
approvals: [
|
||||
{
|
||||
id: 'a1', gate: 'spec', taskId: 't4', title: '重构 sync 模块', meta: 'maestro · MED',
|
||||
docLabel: '改动方案(SPEC)',
|
||||
doc: '把轮询改为 WS 推送:\n1. store 层加 events 订阅接口\n2. daemon 广播 status.changed\n3. web 端断线重连 + 指数退避\n\n为什么:轮询 2s 间隔在多项目下放大为 N 路请求;WS 已有依赖,无新增包。',
|
||||
},
|
||||
],
|
||||
tasks: [
|
||||
{ id: 't1', title: '接入 Claude Agent SDK', cplx: 'hard', status: 'decomposed', prio: 'P0', children: [
|
||||
{ id: 't1a', title: 'PoC:worktree 起 headless CC', cplx: 'medium', status: 'done', prio: 'P0' },
|
||||
{ id: 't1b', title: 'executor 封装 + verify 钩子', cplx: 'medium', status: 'executing', prio: 'P0' },
|
||||
{ id: 't1c', title: '失败重试 n 次 → needs_attention', cplx: 'easy', status: 'blocked', prio: 'P1', deps: ['t1b'] },
|
||||
]},
|
||||
{ id: 't4', title: '重构 sync 模块', cplx: 'medium', status: 'spec_review', prio: 'P1',
|
||||
doc: '把轮询改为 WS 推送:store 层加 events 订阅…' },
|
||||
{ id: 't5', title: '看板移动端单栏适配', cplx: 'easy', status: 'ready', prio: 'P2',
|
||||
ops: '将执行的操作:@media 1100px 断点改单栏;侧栏折叠为顶部条。' },
|
||||
{ id: 't6', title: '事件流 append-only 审计导出', cplx: 'easy', status: 'done', prio: 'P2' },
|
||||
],
|
||||
archived: [
|
||||
{ id: 't6', title: '事件流 append-only 审计导出', cplx: 'easy', status: 'done', subs: 0, time: '2 小时前', timeFull: '2026-06-13 04:21:09',
|
||||
detail: {
|
||||
attrs: [['id', 't6'], ['复杂度', 'EASY'], ['优先级', 'P2'], ['状态', '完成'], ['深度', '1'], ['创建时间', '2026-06-12 22:03:44'], ['最后更新', '2026-06-13 04:21:09']],
|
||||
ops: '将执行的操作:store 层加 events 导出接口(JSONL);CLI 加 maestro events export。',
|
||||
runs: [{ kind: '执行', status: '成功', span: '2026-06-13 03:58:12 → 2026-06-13 04:11:47 · 时长 13 分 35 秒', ref: '转录 runs/r-0613-0358.jsonl' }],
|
||||
result: { branch: 'maestro/t6-events-export', commits: ['a3f9c21 feat(store): events export JSONL'], diff: '+182 −12 · 4 files' },
|
||||
approvals: [{ gate: '结果评审', action: 'accept', actor: 'you', at: '2026-06-13 04:21:09' }],
|
||||
timeline: [
|
||||
{ time: '06-12 22:03', text: '任务创建', who: 'you' },
|
||||
{ time: '06-13 03:58', text: 'run 开始(执行)', who: '编排器', color: 'var(--cyan)' },
|
||||
{ time: '06-13 04:11', text: '执行中 → 待审/合', who: '编排器', color: 'var(--violet)' },
|
||||
{ time: '06-13 04:21', text: '审批通过(结果评审)· 合并', who: 'you', color: 'var(--green)' },
|
||||
],
|
||||
} },
|
||||
{ id: 't3', title: '旧 todo.json 导入器', cplx: 'medium', status: 'done', subs: 2, time: '昨天', timeFull: '2026-06-12 09:14:02',
|
||||
detail: {
|
||||
attrs: [['id', 't3'], ['复杂度', 'MED'], ['优先级', 'P1'], ['状态', '完成'], ['深度', '1'], ['创建时间', '2026-06-10 11:40:12'], ['最后更新', '2026-06-12 09:14:02']],
|
||||
spec: '读旧 todo/todo.json,tier1/2/3 → hard/medium/easy,导入为一个项目;重复标题跳过。',
|
||||
runs: [{ kind: '执行', status: '成功', span: '2026-06-12 08:31:00 → 2026-06-12 08:54:18 · 时长 23 分 18 秒', ref: '转录 runs/r-0612-0831.jsonl' }],
|
||||
approvals: [
|
||||
{ gate: '方案评审', action: 'reject', actor: 'you', at: '2026-06-11 19:02:51', reason: '未处理重复导入' },
|
||||
{ gate: '方案评审', action: 'accept', actor: 'you', at: '2026-06-12 07:48:20' },
|
||||
{ gate: '结果评审', action: 'accept', actor: 'you', at: '2026-06-12 09:14:02' },
|
||||
],
|
||||
timeline: [
|
||||
{ time: '06-10 11:40', text: '任务创建', who: 'you' },
|
||||
{ time: '06-11 19:02', text: '审批驳回(方案评审):未处理重复导入', who: 'you', color: 'var(--red)' },
|
||||
{ time: '06-12 07:48', text: '审批通过(方案评审)', who: 'you', color: 'var(--green)' },
|
||||
{ time: '06-12 08:31', text: 'run 开始(执行)', who: '编排器', color: 'var(--cyan)' },
|
||||
{ time: '06-12 09:14', text: '审批通过(结果评审)· 合并', who: 'you', color: 'var(--green)' },
|
||||
],
|
||||
} },
|
||||
{ id: 't2', title: '远程看板(可选)调研', cplx: 'easy', status: 'cancelled', subs: 0, time: '3 天前', timeFull: '2026-06-10 16:02:33',
|
||||
detail: {
|
||||
attrs: [['id', 't2'], ['复杂度', 'EASY'], ['优先级', 'P2'], ['状态', '取消'], ['深度', '1'], ['创建时间', '2026-06-09 10:12:00'], ['最后更新', '2026-06-10 16:02:33']],
|
||||
timeline: [
|
||||
{ time: '06-09 10:12', text: '任务创建', who: 'you' },
|
||||
{ time: '06-10 16:02', text: '可执行 → 取消', who: 'you', color: 'var(--faint)' },
|
||||
],
|
||||
} },
|
||||
],
|
||||
events: [
|
||||
{ type: 'run.started', time: '06:12:09', detail: 'executor · 把轮询改为 WS 推送 · wt-127' },
|
||||
{ type: 'approval.requested', time: '06:10:44', detail: 'spec_review · 重构 sync 模块' },
|
||||
{ type: 'approval.rejected', time: '05:58:02', detail: '重构 sync 模块 ↳ 缺少回滚方案' },
|
||||
{ type: 'status.changed', time: '05:31:18', detail: 'PoC:worktree 起 headless CC:执行中 → 完成' },
|
||||
{ type: 'task.created', time: '05:02:51', detail: '看板移动端单栏适配' },
|
||||
{ type: 'project.synced', time: '04:48:00', detail: 'maestro · 3 项导入' },
|
||||
],
|
||||
};
|
||||
@@ -1,231 +0,0 @@
|
||||
// MAESTRO 调度台 · 界面语言包(zh / en / es / ja / fr)— UI chrome 文案;任务/事件内容为用户数据不翻译
|
||||
window.MAESTRO_LANGS = [
|
||||
['zh', '中文'], ['en', 'English'], ['es', 'Español'], ['ja', '日本語'], ['fr', 'Français'],
|
||||
];
|
||||
window.MAESTRO_I18N = {
|
||||
zh: {
|
||||
logoSub: '多项目任务调度台',
|
||||
projects: '项目', newProject: '+ 新建', connected: '已连接 · :4517',
|
||||
expandSide: '展开侧栏', collapseSide: '折叠侧栏',
|
||||
badgeGate: '项待审批', badgeReady: '待执行', badgeRun: '执行中', badgeBlocked: '被阻塞', badgeTotal: '总量',
|
||||
badgeGateTip: '等待你裁决的审批闸', badgeReadyTip: '可执行(依赖已满足)+ 排队中',
|
||||
badgeRunTip: 'agent 正在执行', badgeBlockedTip: '被依赖阻塞(依赖完成后自动放行)',
|
||||
badgeTotalTip: '未完成任务总量(不含 done / 已拆解容器 / 取消)',
|
||||
sync: '同步 todo', lastSync: '上次同步 06:12', config: '配置', configTip: '项目配置(并发 / 工作模式 / todo 同步)',
|
||||
archive: '已归档', archiveTip: '点击查看完整详情', archiveSubs: '含 {n} 子任务', archiveDetail: '归档详情',
|
||||
pagePrev: '‹ 上一页', pageNext: '下一页 ›', pageOf: '第 {a} / {b} 页', perPage: '每页',
|
||||
attrsLabel: '属性', runsLabel: '执行历史', resultLabel: '执行结果', approvalsLabel: '审批记录', timelineLabel: '状态流转时间线',
|
||||
branch: '分支', accepted: '接受', rejected: '驳回', approver: '审批人', opinion: '意见', closeTip: '关闭(Esc)',
|
||||
lang: '中文', langTip: '切换语言 / Language', themeLight: '浅色', themeDark: '深色', themeTip: '切换 light/dark',
|
||||
maxConcurrency: '最大并发', workMode: '工作模式', current: '当前', save: '保存配置', collapse: '收起',
|
||||
autonomy: { manual: '手动', 'auto-easy': '自动 · Easy', 'auto-approved': '自动 · 已批准' },
|
||||
agentSection: 'Agent 执行', agentEmpty: '无运行中的 agent', running: 'RUNNING', since: '起',
|
||||
projRunning: '运行中', projPaused: '暂停', projBlocked: '阻塞', projIdle: '空闲',
|
||||
projPendingTip: '{n} 项待你审批',
|
||||
gAgents: '全局 Agent', gAgentsDetail: '{p}/{P} 个项目运行 · {a} 个 agent',
|
||||
apActive: '运行', apRuns: '次运行', apTokens: 'token', apWeek: '本周', apCost: '花费', apTotal: '所有项目', apPerProj: '按项目', apIdle: '空闲',
|
||||
gConfig: '全局配置', gConfigDetail: '并发上限 {n} · daemon {v}',
|
||||
gUserPlan: '订阅', gUserSignOut: '退出登录', gUserSettings: '账户设置',
|
||||
gateExpand: '展开', gateCollapse: '折叠', gateDblTip: '双击全屏阅读',
|
||||
quota: '额度', quotaWeek: '周', quotaReset: '{t} 后重置',
|
||||
projLogo: '项目 Logo', logoPh: '图片 URL 或仓库内相对路径;留空=自动', verifyCmd: '校验命令', verifyPh: 'npm test · go test ./...(可空)', model: '模型', modelPh: 'claude-opus-4-5(可空)',
|
||||
searchPh: '⌕ 搜索标题…', clearFilter: '✕ 清除筛选', matchCount: '{n} 项匹配',
|
||||
fgroups: { todo: '待办', doing: '进行中', gate: '待审批', bad: '异常', hold: '挂起', done: '完成' },
|
||||
fullRead: '全屏阅读', depsLabel: 'DEPS · 依赖(全部完成才可执行)', depDone: '完成', depWait: '等待',
|
||||
gateSection: '审批闸 · 等待裁决', accept: '✓ 通过', reject: '✕ 驳回', confirmReject: '确认驳回',
|
||||
rejectPlaceholder: '改进意见(必填)',
|
||||
taskSection: '任务树', newTask: '+ 新建任务', titleLabel: '标题', titlePlaceholder: '要做什么',
|
||||
complexity: '复杂度', cplxAuto: '智能', parentTask: '父任务', topLevel: '(顶层)', priority: '优先级',
|
||||
p0: 'P0 · 高', p1: 'P1 · 中', p2: 'P2 · 低', create: '创建任务', cancel: '取消',
|
||||
depsWait: '待依赖', specLabel: '改动方案(SPEC)', opsLabel: '将执行的操作(OPERATIONS)',
|
||||
pendingDoc: '待 Claude Code 产出(经 MCP 写入并提交评审)',
|
||||
emptyTasks: '该项目暂无任务 —— 点「+ 新建任务」或经 MCP 创建', empty: '空',
|
||||
events: '事件流', expandEvents: '展开事件流', collapseEvents: '折叠事件流',
|
||||
toastCreated: '任务已创建 · init', toastAccepted: '已通过 · ', toastRejected: '已驳回 · 退回重做',
|
||||
toastSynced: 'todo 已同步 · 0 项变更', toastSaved: '配置已保存', toastModal: '原型:新建项目模态未接',
|
||||
gatePassed: ' 通过',
|
||||
status: { init: '新建', analyzing: '分析拆解中', plan_review: '待确认拆解', decomposed: '已拆解', speccing: '写方案中', spec_review: '待确认方案', ready: '可执行', blocked: '被依赖阻塞', queued: '排队中', executing: '执行中', exec_review: '待审/合', failed: '失败', needs_attention: '需人工', done: '完成', paused: '暂停', cancelled: '取消' },
|
||||
gates: { plan: '拆解评审', spec: '方案评审', exec: '结果评审' },
|
||||
eventTypes: { 'task.created': '任务创建', 'task.updated': '任务更新', 'status.changed': '状态变更', 'approval.requested': '请求审批', 'approval.granted': '审批通过', 'approval.rejected': '审批驳回', 'run.started': '运行开始', 'run.finished': '运行结束', 'project.synced': 'todo 同步' },
|
||||
},
|
||||
en: {
|
||||
logoSub: 'MULTI-PROJECT ORCHESTRATOR',
|
||||
projects: 'PROJECTS', newProject: '+ New', connected: 'Connected · :4517',
|
||||
expandSide: 'Expand sidebar', collapseSide: 'Collapse sidebar',
|
||||
badgeGate: 'pending review', badgeReady: 'to run', badgeRun: 'running', badgeBlocked: 'blocked', badgeTotal: 'total',
|
||||
badgeGateTip: 'Approval gates awaiting your decision', badgeReadyTip: 'Ready (deps met) + queued',
|
||||
badgeRunTip: 'Agents currently executing', badgeBlockedTip: 'Blocked (auto-released when deps complete)',
|
||||
badgeTotalTip: 'Open tasks (excl. done / decomposed containers / cancelled)',
|
||||
sync: 'Sync todo', lastSync: 'Last sync 06:12', config: 'Config', configTip: 'Project config (concurrency / autonomy / todo sync)',
|
||||
archive: 'Archive', archiveTip: 'Click for full detail', archiveSubs: '{n} subtasks', archiveDetail: 'ARCHIVE',
|
||||
pagePrev: '‹ Prev', pageNext: 'Next ›', pageOf: 'Page {a} / {b}', perPage: 'Per page',
|
||||
attrsLabel: 'ATTRIBUTES', runsLabel: 'RUN HISTORY', resultLabel: 'RESULT', approvalsLabel: 'APPROVALS', timelineLabel: 'STATUS TIMELINE',
|
||||
branch: 'Branch', accepted: 'accepted', rejected: 'rejected', approver: 'Approver', opinion: 'Feedback', closeTip: 'Close (Esc)',
|
||||
lang: 'English', langTip: 'Switch language', themeLight: 'Light', themeDark: 'Dark', themeTip: 'Toggle light/dark',
|
||||
maxConcurrency: 'Max concurrency', workMode: 'Autonomy', current: 'Current', save: 'Save config', collapse: 'Close',
|
||||
autonomy: { manual: 'Manual', 'auto-easy': 'Auto · Easy', 'auto-approved': 'Auto · Approved' },
|
||||
agentSection: 'Agent runs', agentEmpty: 'No running agents', running: 'RUNNING', since: '',
|
||||
projRunning: 'Running', projPaused: 'Paused', projBlocked: 'Blocked', projIdle: 'Idle',
|
||||
projPendingTip: '{n} awaiting your review',
|
||||
gAgents: 'Agents', gAgentsDetail: '{p}/{P} projects · {a} agents',
|
||||
apActive: 'active', apRuns: 'runs', apTokens: 'tokens', apWeek: 'this week', apCost: 'cost', apTotal: 'All projects', apPerProj: 'BY PROJECT', apIdle: 'idle',
|
||||
gConfig: 'Settings', gConfigDetail: 'Max concurrency {n} · daemon {v}',
|
||||
gUserPlan: 'Plan', gUserSignOut: 'Sign out', gUserSettings: 'Account settings',
|
||||
gateExpand: 'Expand', gateCollapse: 'Collapse', gateDblTip: 'Double-click for fullscreen',
|
||||
quota: 'Quota', quotaWeek: 'week', quotaReset: 'resets in {t}',
|
||||
projLogo: 'Project logo', logoPh: 'Image URL or repo-relative path; empty = auto', verifyCmd: 'Verify command', verifyPh: 'npm test · go test ./... (optional)', model: 'Model', modelPh: 'claude-opus-4-5 (optional)',
|
||||
searchPh: '⌕ Search titles…', clearFilter: '✕ Clear filters', matchCount: '{n} matches',
|
||||
fgroups: { todo: 'To do', doing: 'In progress', gate: 'Pending review', bad: 'Issues', hold: 'On hold', done: 'Done' },
|
||||
fullRead: 'Read fullscreen', depsLabel: 'DEPS (all must complete to run)', depDone: 'done', depWait: 'waiting',
|
||||
gateSection: 'Approval gates · awaiting decision', accept: '✓ Accept', reject: '✕ Reject', confirmReject: 'Confirm reject',
|
||||
rejectPlaceholder: 'Improvement feedback (required)',
|
||||
taskSection: 'Task tree', newTask: '+ New task', titleLabel: 'Title', titlePlaceholder: 'What needs doing',
|
||||
complexity: 'Complexity', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(top level)', priority: 'Priority',
|
||||
p0: 'P0 · high', p1: 'P1 · medium', p2: 'P2 · low', create: 'Create task', cancel: 'Cancel',
|
||||
depsWait: 'deps', specLabel: 'CHANGE SPEC', opsLabel: 'PLANNED OPERATIONS',
|
||||
pendingDoc: 'Awaiting Claude Code output (written via MCP, then submitted for review)',
|
||||
emptyTasks: 'No tasks in this project — hit "+ New task" or create via MCP', empty: 'EMPTY',
|
||||
events: 'Events', expandEvents: 'Expand events', collapseEvents: 'Collapse events',
|
||||
toastCreated: 'Task created · init', toastAccepted: 'Accepted · ', toastRejected: 'Rejected · sent back',
|
||||
toastSynced: 'Todo synced · 0 changes', toastSaved: 'Config saved', toastModal: 'Prototype: new-project modal not wired',
|
||||
gatePassed: ' approved',
|
||||
status: { init: 'New', analyzing: 'Analyzing', plan_review: 'Plan review', decomposed: 'Decomposed', speccing: 'Drafting spec', spec_review: 'Spec review', ready: 'Ready', blocked: 'Blocked', queued: 'Queued', executing: 'Executing', exec_review: 'Review & merge', failed: 'Failed', needs_attention: 'Needs attention', done: 'Done', paused: 'Paused', cancelled: 'Cancelled' },
|
||||
gates: { plan: 'PLAN REVIEW', spec: 'SPEC REVIEW', exec: 'EXEC REVIEW' },
|
||||
eventTypes: { 'task.created': 'Task created', 'task.updated': 'Task updated', 'status.changed': 'Status changed', 'approval.requested': 'Approval requested', 'approval.granted': 'Approval granted', 'approval.rejected': 'Approval rejected', 'run.started': 'Run started', 'run.finished': 'Run finished', 'project.synced': 'Todo synced' },
|
||||
},
|
||||
es: {
|
||||
logoSub: 'ORQUESTADOR MULTIPROYECTO',
|
||||
projects: 'PROYECTOS', newProject: '+ Nuevo', connected: 'Conectado · :4517',
|
||||
expandSide: 'Expandir panel', collapseSide: 'Plegar panel',
|
||||
badgeGate: 'por revisar', badgeReady: 'por ejecutar', badgeRun: 'en curso', badgeBlocked: 'bloqueadas', badgeTotal: 'total',
|
||||
badgeGateTip: 'Puertas de aprobación esperando tu decisión', badgeReadyTip: 'Listas (deps cumplidas) + en cola',
|
||||
badgeRunTip: 'Agents en ejecución', badgeBlockedTip: 'Bloqueadas (se liberan al completar deps)',
|
||||
badgeTotalTip: 'Tareas abiertas (sin done / contenedores / canceladas)',
|
||||
sync: 'Sincronizar todo', lastSync: 'Última sinc. 06:12', config: 'Config', configTip: 'Configuración (concurrencia / modo / sinc. todo)',
|
||||
archive: 'Archivo', archiveTip: 'Clic para ver detalle', archiveSubs: '{n} subtareas', archiveDetail: 'ARCHIVO',
|
||||
pagePrev: '‹ Anterior', pageNext: 'Siguiente ›', pageOf: 'Página {a} / {b}', perPage: 'Por página',
|
||||
attrsLabel: 'ATRIBUTOS', runsLabel: 'HISTORIAL DE RUNS', resultLabel: 'RESULTADO', approvalsLabel: 'APROBACIONES', timelineLabel: 'LÍNEA DE TIEMPO',
|
||||
branch: 'Rama', accepted: 'aceptada', rejected: 'rechazada', approver: 'Aprobador', opinion: 'Comentario', closeTip: 'Cerrar (Esc)',
|
||||
lang: 'Español', langTip: 'Cambiar idioma', themeLight: 'Claro', themeDark: 'Oscuro', themeTip: 'Cambiar tema',
|
||||
maxConcurrency: 'Concurrencia máx.', workMode: 'Modo', current: 'Actual', save: 'Guardar', collapse: 'Cerrar',
|
||||
autonomy: { manual: 'manual', 'auto-easy': 'auto · Easy', 'auto-approved': 'auto · aprob.' },
|
||||
agentSection: 'Ejecución de agents', agentEmpty: 'Sin agents en ejecución', running: 'RUNNING', since: '',
|
||||
projRunning: 'En curso', projPaused: 'Pausado', projBlocked: 'Bloqueado', projIdle: 'Inactivo',
|
||||
projPendingTip: '{n} esperan tu revisión',
|
||||
gAgents: 'Agents globales', gAgentsDetail: '{p}/{P} proyectos · {a} agents',
|
||||
apActive: 'activos', apRuns: 'ejecuciones', apTokens: 'tokens', apWeek: 'esta semana', apCost: 'coste', apTotal: 'Todos los proyectos', apPerProj: 'POR PROYECTO', apIdle: 'inactivo',
|
||||
gConfig: 'Config global', gConfigDetail: 'Concurrencia máx {n} · daemon {v}',
|
||||
gUserPlan: 'Plan', gUserSignOut: 'Cerrar sesión', gUserSettings: 'Ajustes de cuenta',
|
||||
gateExpand: 'Expandir', gateCollapse: 'Plegar', gateDblTip: 'Doble clic para pantalla completa',
|
||||
quota: 'Cuota', quotaWeek: 'semana', quotaReset: 'reinicio en {t}',
|
||||
projLogo: 'Logo del proyecto', logoPh: 'URL o ruta relativa; vacío = auto', verifyCmd: 'Comando de verificación', verifyPh: 'npm test · go test ./... (opcional)', model: 'Modelo', modelPh: 'claude-opus-4-5 (opcional)',
|
||||
searchPh: '⌕ Buscar títulos…', clearFilter: '✕ Limpiar filtros', matchCount: '{n} coincidencias',
|
||||
fgroups: { todo: 'Pendientes', doing: 'En curso', gate: 'Por revisar', bad: 'Incidencias', hold: 'En pausa', done: 'Hechas' },
|
||||
fullRead: 'Pantalla completa', depsLabel: 'DEPS (todas deben completarse)', depDone: 'hecha', depWait: 'esperando',
|
||||
gateSection: 'Puertas de aprobación · pendientes', accept: '✓ Aceptar', reject: '✕ Rechazar', confirmReject: 'Confirmar rechazo',
|
||||
rejectPlaceholder: 'Comentario de mejora (obligatorio)',
|
||||
taskSection: 'Árbol de tareas', newTask: '+ Nueva tarea', titleLabel: 'Título', titlePlaceholder: 'Qué hay que hacer',
|
||||
complexity: 'Complejidad', cplxAuto: 'AUTO', parentTask: 'Padre', topLevel: '(raíz)', priority: 'Prioridad',
|
||||
p0: 'P0 · alta', p1: 'P1 · media', p2: 'P2 · baja', create: 'Crear tarea', cancel: 'Cancelar',
|
||||
depsWait: 'deps', specLabel: 'SPEC DE CAMBIOS', opsLabel: 'OPERACIONES PREVISTAS',
|
||||
pendingDoc: 'Esperando salida de Claude Code (vía MCP, luego a revisión)',
|
||||
emptyTasks: 'Sin tareas — pulsa "+ Nueva tarea" o crea vía MCP', empty: 'VACÍO',
|
||||
events: 'Eventos', expandEvents: 'Expandir eventos', collapseEvents: 'Plegar eventos',
|
||||
toastCreated: 'Tarea creada · init', toastAccepted: 'Aceptada · ', toastRejected: 'Rechazada · devuelta',
|
||||
toastSynced: 'Todo sincronizado · 0 cambios', toastSaved: 'Config guardada', toastModal: 'Prototipo: modal no conectado',
|
||||
gatePassed: ' aprobada',
|
||||
status: { init: 'Nueva', analyzing: 'Analizando', plan_review: 'Revisión de plan', decomposed: 'Descompuesta', speccing: 'Redactando spec', spec_review: 'Revisión de spec', ready: 'Lista', blocked: 'Bloqueada', queued: 'En cola', executing: 'Ejecutando', exec_review: 'Revisar y fusionar', failed: 'Fallida', needs_attention: 'Requiere atención', done: 'Hecha', paused: 'Pausada', cancelled: 'Cancelada' },
|
||||
gates: { plan: 'REVISIÓN DE PLAN', spec: 'REVISIÓN DE SPEC', exec: 'REVISIÓN DE RESULTADO' },
|
||||
eventTypes: { 'task.created': 'Tarea creada', 'task.updated': 'Tarea actualizada', 'status.changed': 'Cambio de estado', 'approval.requested': 'Aprobación solicitada', 'approval.granted': 'Aprobación concedida', 'approval.rejected': 'Aprobación rechazada', 'run.started': 'Run iniciado', 'run.finished': 'Run terminado', 'project.synced': 'Todo sincronizado' },
|
||||
},
|
||||
ja: {
|
||||
logoSub: 'マルチプロジェクト・オーケストレーター',
|
||||
projects: 'プロジェクト', newProject: '+ 新規', connected: '接続済み · :4517',
|
||||
expandSide: 'サイドバーを開く', collapseSide: 'サイドバーを畳む',
|
||||
badgeGate: '件 承認待ち', badgeReady: '実行待ち', badgeRun: '実行中', badgeBlocked: 'ブロック中', badgeTotal: '合計',
|
||||
badgeGateTip: 'あなたの裁定を待つ承認ゲート', badgeReadyTip: '実行可能(依存解決済み)+ キュー',
|
||||
badgeRunTip: 'agent が実行中', badgeBlockedTip: '依存でブロック(解決後に自動開放)',
|
||||
badgeTotalTip: '未完了タスク数(done / 分解済み / キャンセル除く)',
|
||||
sync: 'todo 同期', lastSync: '最終同期 06:12', config: '設定', configTip: 'プロジェクト設定(並列数 / モード / todo 同期)',
|
||||
archive: 'アーカイブ', archiveTip: 'クリックで詳細表示', archiveSubs: 'サブタスク {n} 件', archiveDetail: 'アーカイブ詳細',
|
||||
pagePrev: '‹ 前へ', pageNext: '次へ ›', pageOf: '{a} / {b} ページ', perPage: '表示件数',
|
||||
attrsLabel: '属性', runsLabel: '実行履歴', resultLabel: '実行結果', approvalsLabel: '承認記録', timelineLabel: 'ステータス・タイムライン',
|
||||
branch: 'ブランチ', accepted: '承認', rejected: '却下', approver: '承認者', opinion: 'コメント', closeTip: '閉じる(Esc)',
|
||||
lang: '日本語', langTip: '言語を切替', themeLight: 'ライト', themeDark: 'ダーク', themeTip: 'テーマ切替',
|
||||
maxConcurrency: '最大並列数', workMode: 'モード', current: '現在', save: '保存', collapse: '閉じる',
|
||||
autonomy: { manual: '手動', 'auto-easy': '自動 · Easy', 'auto-approved': '自動 · 承認済' },
|
||||
agentSection: 'Agent 実行', agentEmpty: '実行中の agent はありません', running: 'RUNNING', since: '開始',
|
||||
projRunning: '実行中', projPaused: '一時停止', projBlocked: 'ブロック', projIdle: 'アイドル',
|
||||
projPendingTip: '{n} 件があなたの承認待ち',
|
||||
gAgents: 'グローバル Agent', gAgentsDetail: '{p}/{P} プロジェクト · {a} 個の agent',
|
||||
apActive: '実行中', apRuns: '回実行', apTokens: 'token', apWeek: '今週', apCost: 'コスト', apTotal: '全プロジェクト', apPerProj: 'プロジェクト別', apIdle: 'アイドル',
|
||||
gConfig: 'グローバル設定', gConfigDetail: '並列上限 {n} · daemon {v}',
|
||||
gUserPlan: 'プラン', gUserSignOut: 'ログアウト', gUserSettings: 'アカウント設定',
|
||||
gateExpand: '展開', gateCollapse: '折り畳む', gateDblTip: 'ダブルクリックで全画面',
|
||||
quota: 'クォータ', quotaWeek: '週', quotaReset: '{t} 後にリセット',
|
||||
projLogo: 'プロジェクトロゴ', logoPh: '画像 URL またはリポジトリ相対パス;空=自動', verifyCmd: '検証コマンド', verifyPh: 'npm test · go test ./...(任意)', model: 'モデル', modelPh: 'claude-opus-4-5(任意)',
|
||||
searchPh: '⌕ タイトルを検索…', clearFilter: '✕ フィルタをクリア', matchCount: '{n} 件一致',
|
||||
fgroups: { todo: '未着手', doing: '進行中', gate: '承認待ち', bad: '異常', hold: '保留', done: '完了' },
|
||||
fullRead: '全画面で読む', depsLabel: 'DEPS · 依存(全完了で実行可)', depDone: '完了', depWait: '待機',
|
||||
gateSection: '承認ゲート · 裁定待ち', accept: '✓ 承認', reject: '✕ 却下', confirmReject: '却下を確定',
|
||||
rejectPlaceholder: '改善フィードバック(必須)',
|
||||
taskSection: 'タスクツリー', newTask: '+ 新規タスク', titleLabel: 'タイトル', titlePlaceholder: '何をしますか',
|
||||
complexity: '複雑度', cplxAuto: '智能', parentTask: '親タスク', topLevel: '(トップ)', priority: '優先度',
|
||||
p0: 'P0 · 高', p1: 'P1 · 中', p2: 'P2 · 低', create: 'タスク作成', cancel: 'キャンセル',
|
||||
depsWait: '依存待ち', specLabel: 'SPEC · 変更案', opsLabel: 'OPERATIONS · 操作',
|
||||
pendingDoc: 'Claude Code の出力待ち(MCP 経由で書き込み後レビューへ)',
|
||||
emptyTasks: 'タスクなし — 「+ 新規タスク」または MCP で作成', empty: '空',
|
||||
events: 'イベント', expandEvents: 'イベントを開く', collapseEvents: 'イベントを畳む',
|
||||
toastCreated: 'タスク作成済み · init', toastAccepted: '承認済み · ', toastRejected: '却下 · 差し戻し',
|
||||
toastSynced: 'todo 同期完了 · 変更 0 件', toastSaved: '設定を保存しました', toastModal: 'プロトタイプ:新規モーダル未接続',
|
||||
gatePassed: ' 承認',
|
||||
status: { init: '新規', analyzing: '分析・分解中', plan_review: '分解確認待ち', decomposed: '分解済み', speccing: '方針作成中', spec_review: '方針確認待ち', ready: '実行可能', blocked: '依存ブロック', queued: 'キュー待ち', executing: '実行中', exec_review: 'レビュー/マージ', failed: '失敗', needs_attention: '要対応', done: '完了', paused: '一時停止', cancelled: 'キャンセル' },
|
||||
gates: { plan: '分解レビュー', spec: '方針レビュー', exec: '結果レビュー' },
|
||||
eventTypes: { 'task.created': 'タスク作成', 'task.updated': 'タスク更新', 'status.changed': 'ステータス変更', 'approval.requested': '承認リクエスト', 'approval.granted': '承認', 'approval.rejected': '却下', 'run.started': 'run 開始', 'run.finished': 'run 終了', 'project.synced': 'todo 同期' },
|
||||
},
|
||||
fr: {
|
||||
logoSub: 'ORCHESTRATEUR MULTI-PROJETS',
|
||||
projects: 'PROJETS', newProject: '+ Nouveau', connected: 'Connecté · :4517',
|
||||
expandSide: 'Déplier le panneau', collapseSide: 'Replier le panneau',
|
||||
badgeGate: 'à approuver', badgeReady: 'à exécuter', badgeRun: 'en cours', badgeBlocked: 'bloquées', badgeTotal: 'total',
|
||||
badgeGateTip: "Portes d'approbation en attente de décision", badgeReadyTip: 'Prêtes (deps OK) + en file',
|
||||
badgeRunTip: 'Agents en cours d’exécution', badgeBlockedTip: 'Bloquées (libérées quand les deps aboutissent)',
|
||||
badgeTotalTip: 'Tâches ouvertes (hors done / conteneurs / annulées)',
|
||||
sync: 'Synchroniser todo', lastSync: 'Dernière sync 06:12', config: 'Config', configTip: 'Config projet (concurrence / mode / sync todo)',
|
||||
archive: 'Archives', archiveTip: 'Cliquer pour le détail', archiveSubs: '{n} sous-tâches', archiveDetail: 'ARCHIVE',
|
||||
pagePrev: '‹ Préc.', pageNext: 'Suiv. ›', pageOf: 'Page {a} / {b}', perPage: 'Par page',
|
||||
attrsLabel: 'ATTRIBUTS', runsLabel: 'HISTORIQUE DES RUNS', resultLabel: 'RÉSULTAT', approvalsLabel: 'APPROBATIONS', timelineLabel: 'CHRONOLOGIE',
|
||||
branch: 'Branche', accepted: 'acceptée', rejected: 'rejetée', approver: 'Approbateur', opinion: 'Retour', closeTip: 'Fermer (Échap)',
|
||||
lang: 'Français', langTip: 'Changer de langue', themeLight: 'Clair', themeDark: 'Sombre', themeTip: 'Basculer le thème',
|
||||
maxConcurrency: 'Concurrence max', workMode: 'Mode', current: 'Actuel', save: 'Enregistrer', collapse: 'Fermer',
|
||||
autonomy: { manual: 'manual', 'auto-easy': 'auto · Easy', 'auto-approved': 'auto · approuv.' },
|
||||
agentSection: 'Exécutions agent', agentEmpty: 'Aucun agent en cours', running: 'RUNNING', since: '',
|
||||
projRunning: 'En cours', projPaused: 'En pause', projBlocked: 'Bloqué', projIdle: 'Inactif',
|
||||
projPendingTip: '{n} en attente de votre revue',
|
||||
gAgents: 'Agents globaux', gAgentsDetail: '{p}/{P} projets · {a} agents',
|
||||
apActive: 'actifs', apRuns: 'exécutions', apTokens: 'tokens', apWeek: 'cette semaine', apCost: 'coût', apTotal: 'Tous les projets', apPerProj: 'PAR PROJET', apIdle: 'inactif',
|
||||
gConfig: 'Config globale', gConfigDetail: 'Concurrence max {n} · daemon {v}',
|
||||
gUserPlan: 'Forfait', gUserSignOut: 'Se déconnecter', gUserSettings: 'Paramètres du compte',
|
||||
gateExpand: 'Déplier', gateCollapse: 'Replier', gateDblTip: 'Double-clic pour le plein écran',
|
||||
quota: 'Quota', quotaWeek: 'semaine', quotaReset: 'reset dans {t}',
|
||||
projLogo: 'Logo du projet', logoPh: 'URL ou chemin relatif ; vide = auto', verifyCmd: 'Commande de vérification', verifyPh: 'npm test · go test ./... (optionnel)', model: 'Modèle', modelPh: 'claude-opus-4-5 (optionnel)',
|
||||
searchPh: '⌕ Rechercher…', clearFilter: '✕ Effacer les filtres', matchCount: '{n} résultats',
|
||||
fgroups: { todo: 'À faire', doing: 'En cours', gate: 'À approuver', bad: 'Anomalies', hold: 'En attente', done: 'Terminées' },
|
||||
fullRead: 'Plein écran', depsLabel: 'DEPS (toutes requises)', depDone: 'terminée', depWait: 'en attente',
|
||||
gateSection: "Portes d'approbation · en attente", accept: '✓ Accepter', reject: '✕ Rejeter', confirmReject: 'Confirmer le rejet',
|
||||
rejectPlaceholder: "Retour d'amélioration (obligatoire)",
|
||||
taskSection: 'Arbre des tâches', newTask: '+ Nouvelle tâche', titleLabel: 'Titre', titlePlaceholder: 'Que faut-il faire',
|
||||
complexity: 'Complexité', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(racine)', priority: 'Priorité',
|
||||
p0: 'P0 · haute', p1: 'P1 · moyenne', p2: 'P2 · basse', create: 'Créer la tâche', cancel: 'Annuler',
|
||||
depsWait: 'deps', specLabel: 'SPEC DES CHANGEMENTS', opsLabel: 'OPÉRATIONS PRÉVUES',
|
||||
pendingDoc: 'En attente de la sortie de Claude Code (via MCP, puis revue)',
|
||||
emptyTasks: 'Aucune tâche — « + Nouvelle tâche » ou via MCP', empty: 'VIDE',
|
||||
events: 'Événements', expandEvents: 'Déplier les événements', collapseEvents: 'Replier les événements',
|
||||
toastCreated: 'Tâche créée · init', toastAccepted: 'Acceptée · ', toastRejected: 'Rejetée · renvoyée',
|
||||
toastSynced: 'Todo synchronisé · 0 changement', toastSaved: 'Config enregistrée', toastModal: 'Prototype : modale non câblée',
|
||||
gatePassed: ' approuvée',
|
||||
status: { init: 'Nouvelle', analyzing: 'Analyse', plan_review: 'Revue du plan', decomposed: 'Décomposée', speccing: 'Rédaction spec', spec_review: 'Revue de spec', ready: 'Prête', blocked: 'Bloquée', queued: 'En file', executing: 'En exécution', exec_review: 'Revue & fusion', failed: 'Échouée', needs_attention: 'À traiter', done: 'Terminée', paused: 'En pause', cancelled: 'Annulée' },
|
||||
gates: { plan: 'REVUE DU PLAN', spec: 'REVUE DE SPEC', exec: 'REVUE DU RÉSULTAT' },
|
||||
eventTypes: { 'task.created': 'Tâche créée', 'task.updated': 'Tâche mise à jour', 'status.changed': "Changement d'état", 'approval.requested': 'Approbation demandée', 'approval.granted': 'Approbation accordée', 'approval.rejected': 'Approbation rejetée', 'run.started': 'Run démarré', 'run.finished': 'Run terminé', 'project.synced': 'Todo synchronisé' },
|
||||
},
|
||||
};
|
||||
@@ -1,183 +0,0 @@
|
||||
<!-- @dsCard group="Console" viewport="1440x900" name="Web 调度台" subtitle="三栏看板整屏复刻:项目 · 审批闸+任务树 · 事件流(可点击)" -->
|
||||
<!-- @startingPoint section="Screens" subtitle="Maestro Web 调度台整屏" viewport="1440x900" -->
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MAESTRO · 任务调度台</title>
|
||||
<link rel="stylesheet" href="../../styles.css">
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script src="../../_ds_bundle.js"></script>
|
||||
<script src="data.js"></script>
|
||||
<script src="i18n.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body { background: var(--bg); color: var(--ink); font-family: var(--mono); font-size: 13px; line-height: 1.55; overflow: hidden; }
|
||||
body::before {
|
||||
content: ''; position: fixed; inset: 0; z-index: 999; pointer-events: none;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px),
|
||||
radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0,0,0,.5));
|
||||
opacity: .5;
|
||||
}
|
||||
[data-theme="light"] body::before { opacity: .12; }
|
||||
@keyframes maestro-locate {
|
||||
0%, 35% { background: var(--amber-dim); box-shadow: inset 2px 0 0 var(--amber); }
|
||||
100% { background: transparent; box-shadow: none; }
|
||||
}
|
||||
::selection { background: var(--green-dim); color: #fff; }
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--green-dim); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="Sidebar.jsx"></script>
|
||||
<script type="text/babel" src="EventPanel.jsx"></script>
|
||||
<script type="text/babel" src="Topbar.jsx"></script>
|
||||
<script type="text/babel" src="GateSection.jsx"></script>
|
||||
<script type="text/babel" src="TaskTree.jsx"></script>
|
||||
<script type="text/babel" src="ArchiveSection.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { Toast } = window.MaestroDesignSystem_a6a290;
|
||||
const M = window.MAESTRO_MOCK;
|
||||
const I18N = window.MAESTRO_I18N;
|
||||
|
||||
function App() {
|
||||
const [currentId, setCurrentId] = React.useState('p1');
|
||||
const [approvals, setApprovals] = React.useState(M.approvals);
|
||||
const [events, setEvents] = React.useState(M.events);
|
||||
const [showConfig, setShowConfig] = React.useState(false);
|
||||
const [archiveItem, setArchiveItem] = React.useState(null);
|
||||
const [toasts, setToasts] = React.useState([]);
|
||||
const [lang, setLang] = React.useState(() => {
|
||||
const saved = localStorage.getItem('maestro-kit-lang');
|
||||
return I18N[saved] ? saved : 'zh';
|
||||
});
|
||||
const [theme, setTheme] = React.useState(() => localStorage.getItem('maestro-kit-theme') || 'dark');
|
||||
const t = I18N[lang];
|
||||
React.useEffect(() => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
localStorage.setItem('maestro-kit-theme', theme);
|
||||
}, [theme]);
|
||||
React.useEffect(() => { localStorage.setItem('maestro-kit-lang', lang); }, [lang]);
|
||||
const [collapsed, setCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-sidebar') === 'collapsed');
|
||||
const [evCollapsed, setEvCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-events') === 'collapsed');
|
||||
const toggleEvents = () => setEvCollapsed((c) => {
|
||||
localStorage.setItem('maestro-kit-events', c ? 'open' : 'collapsed');
|
||||
return !c;
|
||||
});
|
||||
const toggleCollapse = () => setCollapsed((c) => {
|
||||
localStorage.setItem('maestro-kit-sidebar', c ? 'open' : 'collapsed');
|
||||
return !c;
|
||||
});
|
||||
// 栏宽可拖拽(带最小/最大约束,持久化);中栏保证 ≥ CENTER_MIN,避免被两侧挤压
|
||||
const SIDE_MIN = 200, SIDE_MAX = 420, EV_MIN = 240, EV_MAX = 520, CENTER_MIN = 480;
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
const sideMax = () => Math.min(SIDE_MAX, window.innerWidth - (evCollapsed ? 52 : evW) - CENTER_MIN);
|
||||
const evMax = () => Math.min(EV_MAX, window.innerWidth - (collapsed ? 52 : sideW) - CENTER_MIN);
|
||||
const [sideW, setSideW] = React.useState(() => clamp(Number(localStorage.getItem('maestro-kit-sidew')) || 232, SIDE_MIN, SIDE_MAX));
|
||||
const [evW, setEvW] = React.useState(() => clamp(Number(localStorage.getItem('maestro-kit-evw')) || 320, EV_MIN, EV_MAX));
|
||||
const [dragging, setDragging] = React.useState(null);
|
||||
React.useEffect(() => { localStorage.setItem('maestro-kit-sidew', sideW); }, [sideW]);
|
||||
React.useEffect(() => { localStorage.setItem('maestro-kit-evw', evW); }, [evW]);
|
||||
const startDrag = (which) => (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(which);
|
||||
const startX = e.clientX;
|
||||
const startW = which === 'side' ? sideW : evW;
|
||||
const onMove = (ev) => {
|
||||
if (which === 'side') setSideW(clamp(startW + (ev.clientX - startX), SIDE_MIN, Math.max(SIDE_MIN, sideMax())));
|
||||
else setEvW(clamp(startW - (ev.clientX - startX), EV_MIN, Math.max(EV_MIN, evMax())));
|
||||
};
|
||||
const onUp = () => {
|
||||
setDragging(null);
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
document.body.style.userSelect = 'none';
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
};
|
||||
const Resizer = ({ which }) => (
|
||||
<div onPointerDown={startDrag(which)} title="拖拽调整宽度"
|
||||
style={{
|
||||
position: 'absolute', top: 0, bottom: 0, width: 9, cursor: 'col-resize', zIndex: 40,
|
||||
...(which === 'side' ? { left: sideW - 4 } : { right: evW - 4 }),
|
||||
display: 'flex', justifyContent: 'center',
|
||||
}}>
|
||||
<span style={{ width: 1, background: dragging === which ? 'var(--green)' : 'var(--line-soft)', boxShadow: dragging === which ? '0 0 8px var(--green)' : 'none', transition: 'background .12s' }}></span>
|
||||
</div>
|
||||
);
|
||||
const project = M.projects.find((p) => p.id === currentId);
|
||||
const isMain = currentId === 'p1';
|
||||
const tasks = isMain ? M.tasks : [];
|
||||
const agents = isMain ? M.agents : [];
|
||||
const gates = isMain ? approvals : [];
|
||||
|
||||
const toast = (kind, text) => {
|
||||
const id = Date.now();
|
||||
setToasts((t) => [...t, { id, kind, text }]);
|
||||
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2600);
|
||||
};
|
||||
const now = () => new Date().toTimeString().slice(0, 8);
|
||||
const decide = (a, action, reason) => {
|
||||
setApprovals((list) => list.filter((x) => x.id !== a.id));
|
||||
if (action === 'accept') {
|
||||
setEvents((ev) => [{ type: 'approval.granted', time: now(), detail: a.title + ' · ' + a.gate + '_review' + t.gatePassed }, ...ev]);
|
||||
toast('ok', t.toastAccepted + a.title);
|
||||
} else {
|
||||
setEvents((ev) => [{ type: 'approval.rejected', time: now(), detail: a.title + ' ↳ ' + reason }, ...ev]);
|
||||
toast('warn', t.toastRejected);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: (collapsed ? '52px' : sideW + 'px') + ' minmax(0,1fr) ' + (evCollapsed ? '52px' : evW + 'px'), height: '100vh' }}>
|
||||
{!collapsed ? <Resizer which="side" /> : null}
|
||||
{!evCollapsed ? <Resizer which="ev" /> : null}
|
||||
<window.MaestroKitSidebar projects={M.projects} currentId={currentId} t={t}
|
||||
global={M.global} user={M.user} summary={M.agentSummary} onGlobalConfig={() => toast('warn', t.toastModal)}
|
||||
collapsed={collapsed} onToggleCollapse={toggleCollapse}
|
||||
onSelect={setCurrentId} onNewProject={() => toast('warn', t.toastModal)} />
|
||||
<main style={{ overflowY: 'auto', padding: '0 22px 60px' }}>
|
||||
<window.MaestroKitTopbar project={project} t={t} theme={theme}
|
||||
onToggleTheme={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
lang={lang} onSelectLang={setLang}
|
||||
counts={{ gate: gates.length, ready: isMain ? 1 : 0, run: agents.length, blocked: isMain ? 1 : 0, total: isMain ? 5 : 0 }}
|
||||
onSync={() => toast('ok', t.toastSynced)}
|
||||
onToggleConfig={() => setShowConfig(!showConfig)} />
|
||||
{showConfig ? <window.MaestroKitConfigPanel project={project} t={t}
|
||||
onSave={() => { setShowConfig(false); toast('ok', t.toastSaved); }}
|
||||
onSync={() => toast('ok', t.toastSynced)}
|
||||
onClose={() => setShowConfig(false)} /> : null}
|
||||
<window.MaestroKitAgentSection agents={agents} quota={isMain ? M.quota : null} t={t} />
|
||||
<window.MaestroKitGateSection approvals={gates} onDecide={decide} t={t} />
|
||||
{tasks.length ? (
|
||||
<window.MaestroKitTaskSection tasks={tasks} onToast={toast} t={t} />
|
||||
) : (
|
||||
<div style={{ padding: '46px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, marginTop: 18 }}>
|
||||
<b style={{ display: 'block', fontSize: 15, color: 'var(--muted)', marginBottom: 6, letterSpacing: '.2em' }}>{t.empty}</b>
|
||||
{t.emptyTasks}
|
||||
</div>
|
||||
)}
|
||||
{isMain ? <window.MaestroKitArchiveSection items={M.archived} t={t} onOpen={setArchiveItem} /> : null}
|
||||
</main>
|
||||
<window.MaestroKitEventPanel events={events} collapsed={evCollapsed} onToggleCollapse={toggleEvents} t={t} />
|
||||
{archiveItem ? <window.MaestroKitArchiveModal item={archiveItem} t={t} onClose={() => setArchiveItem(null)} /> : null}
|
||||
<div style={{ position: 'fixed', bottom: 18, left: '50%', transform: 'translateX(-50%)', zIndex: 1200, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
|
||||
{toasts.map((t) => <Toast key={t.id} kind={t.kind}>{t.text}</Toast>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,93 +0,0 @@
|
||||
// 移动版 · 头部 + 底部 Tab 栏
|
||||
const mIcon = { fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', viewBox: '0 0 24 24' };
|
||||
function MIconTask({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><rect x="4" y="4" width="16" height="16" rx="2.5" /><polyline points="8 12 11 15 16 9" /></svg>;
|
||||
}
|
||||
function MIconGate({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><path d="M12 3 L22 21 L2 21 Z" /><line x1="12" y1="10" x2="12" y2="14" /><line x1="12" y1="17.5" x2="12" y2="17.51" /></svg>;
|
||||
}
|
||||
function MIconEvent({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><polyline points="2 12 7 12 10 5 14 19 17 12 22 12" /></svg>;
|
||||
}
|
||||
function MIconGit({ s = 22 }) {
|
||||
return <svg {...mIcon} width={s} height={s}><line x1="6" y1="3" x2="6" y2="15" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M18 9a9 9 0 0 1-9 9" /></svg>;
|
||||
}
|
||||
|
||||
function MobileMark({ scale = 1.6 }) {
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
const MAP = ['.....LLLLL.....','...LLLLLLLLL...','..GGGGGGGGGGG..','..GGEEGGGEEGG..','..GGEEGGGEEGG..','..GGGGGGGGGGG..','...GGGGGGGGG...','...G..G.G..G...','...G..G.G..G...','..G...G.G...G..','..D...D.D...D..','.D...D...D...D.'];
|
||||
const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' };
|
||||
const ctx = ref.current.getContext('2d');
|
||||
MAP.forEach((row, y) => [...row].forEach((ch, x) => {
|
||||
if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); }
|
||||
}));
|
||||
}, []);
|
||||
return <canvas ref={ref} width="15" height="12"
|
||||
style={{ width: 15 * scale, height: 12 * scale, imageRendering: 'pixelated', filter: 'drop-shadow(0 0 5px rgba(95,221,125,.4))' }} />;
|
||||
}
|
||||
|
||||
function MobileHeader({ project, counts }) {
|
||||
const { CountBadge } = window.MaestroDesignSystem_a6a290;
|
||||
return (
|
||||
<header style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px',
|
||||
borderBottom: '1px solid var(--line)', background: 'var(--bg-deep)',
|
||||
position: 'sticky', top: 0, zIndex: 20,
|
||||
}}>
|
||||
<MobileMark />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, letterSpacing: '.04em', whiteSpace: 'nowrap' }}>{project.name}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{project.branch} · {project.autonomy}</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
|
||||
<CountBadge kind="gate" count={counts.gate} />
|
||||
<CountBadge kind="run" count={counts.run} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileTabBar({ tab, setTab, gateCount }) {
|
||||
const tabs = [
|
||||
{ id: 'tasks', label: '任务', icon: MIconTask },
|
||||
{ id: 'gates', label: '审批', icon: MIconGate, badge: gateCount },
|
||||
{ id: 'events', label: '事件', icon: MIconEvent },
|
||||
{ id: 'projects', label: '项目', icon: MIconGit },
|
||||
];
|
||||
return (
|
||||
<nav style={{
|
||||
display: 'grid', gridTemplateColumns: 'repeat(4,1fr)',
|
||||
borderTop: '1px solid var(--line)', background: 'var(--bg-deep)',
|
||||
position: 'sticky', bottom: 0, zIndex: 20,
|
||||
}}>
|
||||
{tabs.map((t) => {
|
||||
const active = tab === t.id;
|
||||
const Icon = t.icon;
|
||||
return (
|
||||
<button key={t.id} onClick={() => setTab(t.id)} style={{
|
||||
fontFamily: 'var(--mono)', background: 'transparent', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
|
||||
padding: '9px 0 10px', minHeight: 56, position: 'relative',
|
||||
color: active ? 'var(--green)' : 'var(--faint)',
|
||||
textShadow: active ? '0 0 10px rgba(95,221,125,.45)' : 'none',
|
||||
}}>
|
||||
<span style={{ position: 'relative', filter: active ? 'drop-shadow(0 0 6px rgba(95,221,125,.5))' : 'none' }}>
|
||||
<Icon />
|
||||
{t.badge ? (
|
||||
<span style={{
|
||||
position: 'absolute', top: -4, right: -8, minWidth: 15, height: 15,
|
||||
fontSize: 9.5, fontWeight: 700, lineHeight: '15px', textAlign: 'center',
|
||||
background: 'var(--violet)', color: 'var(--bg-deep)', borderRadius: 8, padding: '0 3px',
|
||||
}}>{t.badge}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span style={{ fontSize: 10, letterSpacing: '.12em' }}>{t.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MaestroMobHeader: MobileHeader, MaestroMobTabBar: MobileTabBar });
|
||||