diff --git a/CHANGELOG.md b/CHANGELOG.md index a261484..fcdad66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Added +- `skills/DesignReview/SKILL.md` — capture-agnostic reviewer that verifies built UI against the `DesignPrinciples` canon from rendered artifacts (screenshot + axe-core JSON per state and viewport); hard numbers come only from axe, and axe `incomplete` or absent states report not-verified rather than pass +- `docs/decisions/ARCH-0015 Rendered Design Review.md` — records the decision to own the review contract rather than a browser, delegating capture to a tool such as a Playwright MCP +- `skills/DesignPrinciples/SKILL.md` — canonical cross-cutting design canon (durable foundations, WCAG-anchored hard checks, advisory non-failing thresholds), `disable-model-invocation` so it serves as a reference the other design skills cite rather than an invocable skill +- `skills/DesignDashboard/SKILL.md` — data-UI design discipline (density, state completeness, chart-to-data-shape matching, progressive disclosure) for dashboards, admin panels, and analytics views, distinct from `DesignFrontend`'s aesthetic scope - `skills/BashConventions/` + `BashPatterns.md` companion — Bash pitfalls (BSD vs GNU, `set -euo pipefail` traps, subprocess env) relocated from forge-dev - `skills/VersionControl/GitWorktrees.md` — companion covering parallel feature work via `git worktree`, with SLSA provenance (adapted from [davila7/claude-code-templates](https://github.com/davila7/claude-code-templates) MIT under EUPL-1.2) - `VersionControl` now serves as the canonical home for git conventions and platform-specific repo governance (absorbing the retired forge-dev `Git` skill). forge-dev's `GitHub` skill remains distinct — it covers CI, Actions, releases, and operational `gh` workflows ### Changed +- `skills/DesignFrontend/SKILL.md` — USE WHEN tightened to marketing/landing/brand-aesthetic surfaces; dashboards and analytics UI now route to `DesignDashboard`. Carries a foundations digest and cites the `DesignPrinciples` canon. - `skills/AdoptArtifact/SKILL.md` — removed the 3–5 initial cap and the skills-only restriction; agents are now eligible for adoption - `skills/ForgeAdopt/` renamed to `skills/AdoptArtifact/` for brand-neutral naming - `skills/RefinePrompt/` consolidates the former Prompt cluster (`AlignPrompt`, `DebrandPrompt`, `ExtractPrompt`, `MinimizePrompt`, `RescopePrompt`, `AdaptPrompts`) into one skill with six companions. Adoption sidecars collapse the per-transform `resolvedDependencies` entries to a single `RefinePrompt` SHA pin. diff --git a/README.md b/README.md index 7ecc0d7..73b8904 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ Skills teach AI coding tools new capabilities. Invoke them with `/SkillName` in | [**SystemCheck**](skills/SystemCheck/SKILL.md) | Ecosystem staleness and version drift checks | | [**RTK**](skills/RTK/SKILL.md) | Token-optimized CLI proxy | | [**HtmlPlayground**](skills/HtmlPlayground/SKILL.md) | Generate single-file HTML demos comparing techniques | +| [**VisualIdentity**](skills/VisualIdentity/SKILL.md) | Create shared visual profiles, previews, and CSS tokens | +| [**DesignPrinciples**](skills/DesignPrinciples/SKILL.md) | Shared token, accessibility, and rendered-design canon | +| [**DesignFrontend**](skills/DesignFrontend/SKILL.md) | Build distinctive profile-driven frontend interfaces | +| [**DesignDashboard**](skills/DesignDashboard/SKILL.md) | Build profile-driven data-dense interfaces | | [**Brainstorming**](skills/Brainstorming/SKILL.md) | Collaborative ideation before implementation | | [**DesignSpec**](skills/DesignSpec/SKILL.md) | Formal design spec from brainstorming output | | [**WritePlan**](skills/WritePlan/SKILL.md) | Bite-sized implementation plans with plan mode | diff --git a/defaults.yaml b/defaults.yaml index d931de6..b56f11d 100644 --- a/defaults.yaml +++ b/defaults.yaml @@ -9,6 +9,15 @@ adr: schema: templates/forge-adr.json mdschema: docs/decisions/.mdschema +visual_identity: + schema: skills/VisualIdentity/visual-profile.schema.yaml + example: skills/VisualIdentity/visual-profile.example.yaml + css_prefix: forge + contrast: + normal_text: 4.5 + large_text: 3.0 + ui_and_focus: 3.0 + agents: DataAnalyst: model: fast diff --git a/docs/decisions/ARCH-0015 Rendered Design Review.md b/docs/decisions/ARCH-0015 Rendered Design Review.md new file mode 100644 index 0000000..02785f3 --- /dev/null +++ b/docs/decisions/ARCH-0015 Rendered Design Review.md @@ -0,0 +1,93 @@ +--- +title: "Rendered Design Review" +description: "Verify built UI against the DesignPrinciples canon from rendered artifacts, owning the review contract rather than the browser that produces them" +type: adr +category: architecture +tags: + - architecture + - design + - review + - accessibility +status: accepted +created: 2026-07-07 +updated: 2026-07-08 +author: "@N4M3Z" +project: forge-core +related: [] +responsible: ["@N4M3Z"] +accountable: ["@N4M3Z"] +consulted: [] +informed: [] +upstream: [] +--- + +# Rendered Design Review + +## Context and Problem Statement + +The `DesignPrinciples` canon defines spec-anchored hard checks (WCAG contrast, target size) and durable foundations, but nothing verifies a built interface against them. Source-scanning cannot do it: regex over CSS-in-JS, styled-components, Tailwind arbitrary values, and React Native sees little, and no static scan computes rendered contrast, which semi-transparency, hover, and dark mode all change. A reviewer must judge the rendered result. The open question is how much infrastructure forge-core should own to make that possible without turning a markdown module into a browser-automation project. + +## Decision Drivers + +- Hard checks (contrast, target size) need computed values, not pixels judged by eye or source literals. +- forge-core deploys markdown only; executables and browsers are a maintenance liability it has so far avoided. +- The reviewer must not commit the flaw it polices: a happy-path-only screenshot cannot verify empty, loading, or error states. +- Users likely already run a screenshot and accessibility tool (Playwright MCP); forge should compose with it, not duplicate it. + +## Considered Options + +1. **Source-grep reviewer** — regex or AST checks in a skill or binary. Blind on CSS-in-JS and React Native, cannot compute contrast, fires on a minority of stacks. +2. **Own the capture** — ship a `bin/ui-shot` Playwright wrapper plus INSTALL.md producing a PNG and axe-core JSON per target. Turnkey, but inherits Chromium version drift, cross-platform browser installs, and INSTALL maintenance, and forge does not auto-deploy `bin/`. A markdown module takes on a browser dependency. +3. **Own the contract, not the browser** — a single capture-agnostic `DesignReview` skill that consumes artifacts (PNG plus axe-core JSON per state and viewport) produced by whatever tool the user has, maps axe rule IDs to DesignPrinciples checks, and reports pass, fail, or not-verified. Pure markdown, forge-native, no dependency to maintain, composes with an existing Playwright MCP. The user must produce the artifacts; capture is not turnkey. + +**Risk Assessment** (option 3): + +- **Technical Risk**: low — a markdown skill with no runtime. +- **Schedule Risk**: low — one skill. +- **Ecosystem Risk**: low — depends on an external capture tool by contract, not by code. + +## Decision Outcome + +Chosen option: **own the contract, not the browser (option 3)**, because it delivers rendered verification without forge-core taking on browser automation, and it composes with capture tooling the user already runs. + +### Ideal implementation + +A `DesignReview` skill in forge-core that: + +- Defines a strict input contract: for each review target, a screenshot PNG plus an axe-core JSON report, named so a file maps to its target, state, and viewport, listed in a small manifest of targets by state by viewport. +- Sources hard checks only from the axe JSON (computed contrast, target size), never from the image, with an explicit prohibition on estimating hard numbers by eye. +- Maps axe rule IDs to DesignPrinciples checks (`color-contrast` to contrast, `target-size` to touch target). +- Treats axe `incomplete` (contrast over gradients, images, or transformed nodes, or an unresolved background) as not-verified, never as pass. +- Uses the screenshots for vision judgment only: structure-before-style, anti-slop tells, and state coverage. +- Requires state targets. Given only the happy path, it reports empty, loading, and error as not-verified rather than passing. +- Emits structured findings citing DesignPrinciples, each tagged verified-fail, verified-pass, or not-verified. + +### What is missing today + +- The `DesignReview` skill itself: the input contract, the axe-rule-to-principle map, and the reporting semantics. +- A named artifact manifest convention (targets, states, viewports, and file naming). +- A documented reference capture path (Playwright MCP) cited, not bundled. + +### Dangers + +- **False confidence through silence.** axe `incomplete` or a missing state reads as pass unless explicitly reported not-verified. This is the primary risk; the reporting semantics exist to close it. +- **Vision measurement leakage.** The model may estimate contrast from the image. The skill must forbid it and route "looks wrong but axe is silent" to a not-verified suspicion, not a number. +- **Non-determinism.** Font loading, subpixel rendering, and mid-flight animation make screenshots non-reproducible. Capture must fix the viewport, disable animation, and wait for fonts and network idle; the contract states this requirement even though a different tool satisfies it. +- **Token cost.** States by viewports multiplies images per review; keep the default target set small. +- **Capture gap.** With no configured capture tool the skill has nothing to read; it must degrade to an explicit no-op, not a silent pass. + +### Consequences + +- [+] Rendered verification of the DesignPrinciples canon with no browser dependency in forge-core. +- [+] Composes with an existing Playwright MCP or any tool honoring the contract. +- [-] Not turnkey: the user wires a capture tool and supplies targets. Mitigated by documenting one reference path and failing loudly when artifacts are absent. + +### Scope for the first cut + +Ship the `DesignReview` skill (contract, axe map, not-verified semantics), two viewports (mobile and desktop) as advisory, caller-supplied targets required. Defer bundled capture, multi-theme review, auto-discovery of states, and interaction or video review. + +## Links + +- [DesignPrinciples](../../skills/DesignPrinciples/SKILL.md) — the canon this reviewer verifies against +- [axe-core rule descriptions](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) — rule IDs mapped to checks +- [WCAG 2.2](https://www.w3.org/TR/WCAG22/) — source of the hard checks diff --git a/skills/DesignDashboard/SKILL.md b/skills/DesignDashboard/SKILL.md new file mode 100644 index 0000000..43c49b7 --- /dev/null +++ b/skills/DesignDashboard/SKILL.md @@ -0,0 +1,78 @@ +--- +name: DesignDashboard +description: "Build data-dense product UI — dashboards, admin panels, analytics views, data tables, KPI screens. USE WHEN the user asks for a dashboard, admin panel, analytics UI, data table, metrics/KPI layout, or reporting interface. For marketing, landing, or brand-aesthetic pages use DesignFrontend instead." +version: 0.1.0 +allowed-tools: Read, Grep, Write, Edit +--- + +# DesignDashboard + +Design data UI as a systems discipline: restraint, legibility, and behavior across data shapes and non-happy-path states. This is the opposite of marketing design, density and clarity win over atmosphere and distinctiveness. + +## Foundations + +- **Structure before style.** Generic layout, not palette, is what makes UI read as templated. Fix hierarchy first. +- **Tokens before values.** Colors, radii, spacing trace to semantic tokens; no raw hex in component code. +- **State completeness.** Every interactive element covers default, hover, focus-visible, active, disabled, loading; every data view covers empty, loading, error. +- **Color never alone.** Status in color is always paired with an icon, label, or shape. + +Hard accessibility self-checks before shipping (verify against the rendered UI, not the source): text contrast ≥ 4.5:1, UI/focus indicators ≥ 3:1, touch targets ≥ 24px (WCAG 2.2 §1.4.3, §1.4.11, §2.5.8 [WCAG]). `DesignPrinciples` holds the full cross-cutting canon, including advisory thresholds and the named-law vocabulary. + +## Structure first + +Let the data's shape drive the component, not the reverse. Prototype against real or realistic data before choosing a pattern: a single value is a stat card, a metric with 50 categories is a table, not a pie. Component choice is a data-shape decision, not an aesthetic one. + +If `visual-profile.yaml` exists, use its mode colors, typography, spacing, radii, focus, and motion tokens. Compile it with `VisualIdentity` when CSS is missing. Dashboard components reference `--forge-*` variables without literal identity fallbacks; changing profile must not change markup or selectors. + +## Layout + +- The sidebar is the product's spine: group nav by relevance, sink low-frequency items (settings) to the bottom, pair icons with short labels, and always mark the active item. +- Primary metrics go where the eye lands first; secondary detail sits behind tabs, drawers, or drill-downs. +- Cap the default view at roughly 5-9 top-level metrics. Beyond that, split — do not stack everything on one screen. +- Use denser typography and tighter hierarchy steps than a marketing page; dashboards trade whitespace for information. + +## Charts + +Match chart type to data shape. Default to boring-but-legible over novel. + +| Data shape | Chart | +| ------------------- | ------------------------- | +| Trend over time | Line | +| Category comparison | Bar | +| Part of whole | Pie / stacked bar | +| Single value | Stat card / sparkline | + +A pie with more than ~6 slices, a line chart on categorical data, or a single stat rendered as a table are mismatches, treat them as defects. + +## States + +"UI is what you can't see." The polish of a dashboard lives outside the populated screenshot, and these are the most verifiable gap in any dashboard PR. + +- **Empty:** context (why is this empty) + guidance (the next action) + a CTA when the user can resolve it. +- **Loading:** skeletons that do not break screen-reader navigation. +- **Error:** a recoverable message, not a blank panel. +- **Overflow:** truncation and wrapping behavior for long labels and large numbers. + +## Interaction + +Optimistic updates (apply instantly, reconcile with the backend after) keep a dashboard feeling responsive. Surface confirmations and failures through toasts. Use modals for complex actions, popovers for lightweight settings. + +## Red Flags + +| Anti-pattern | Fix | +| ----------------------------------------------- | ------------------------------------------------ | +| Only the populated happy-path state is designed | Design empty, loading, error, and overflow first | +| Chart type does not match the data's shape | Re-pick from the chart table by data shape | +| Everything visible at once, no hierarchy | Progressive disclosure: glance view + drill-down | +| Sidebar has no active-item indicator | Mark the current nav item | + +## Constraints + +- Produce real, working code, not mockups. +- Keep identity-specific values in `visual-profile.yaml` or generated profile CSS, never component code. +- Self-check the hard accessibility thresholds from the shared principles before shipping; contrast against the rendered UI, not the source literal. +- Advisory thresholds (spacing grid, metric count, motion) parameterize to the target design system, do not hard-fail on them. + +## Sources + +[WCAG]: https://www.w3.org/TR/WCAG22/ "Web Content Accessibility Guidelines (WCAG) 2.2, W3C Recommendation" diff --git a/skills/DesignFrontend/SKILL.md b/skills/DesignFrontend/SKILL.md index 2eb09c5..b3c8ff9 100644 --- a/skills/DesignFrontend/SKILL.md +++ b/skills/DesignFrontend/SKILL.md @@ -1,6 +1,6 @@ --- name: DesignFrontend -description: "Build distinctive, production-grade frontend interfaces that avoid generic AI aesthetics. USE WHEN the user asks for web components, pages, landing pages, dashboards, React / HTML / CSS layouts, or styling of any web UI. Not for backend or non-UI work." +description: "Build distinctive, production-grade frontend interfaces that avoid generic AI aesthetics. USE WHEN the user asks for a landing page, marketing site, hero, brand or premium aesthetic, or the styling and visual direction of web components. For data-dense dashboards, admin panels, and analytics UI use DesignDashboard instead. Not for backend or non-UI work." version: 0.1.0 allowed-tools: Read, Grep, Write, Edit upstream: https://github.com/davila7/claude-code-templates/blob/main/cli-tool/components/skills/creative-design/frontend-design/SKILL.md @@ -8,12 +8,23 @@ upstream: https://github.com/davila7/claude-code-templates/blob/main/cli-tool/co # DesignFrontend -Create distinctive, production-grade web interfaces. Commit to an aesthetic direction and execute with precision — intentionality wins over intensity, whether the direction is bold maximalism or refined minimalism. +Create distinctive, production-grade web interfaces. Commit to an aesthetic direction and execute with precision: intentionality wins over intensity, whether the direction is bold maximalism or refined minimalism. + +## Foundations + +- **Structure before style.** Generic layout, not palette, is what makes UI read as templated. Fix hierarchy first. +- **Tokens before values.** Colors, radii, spacing trace to semantic tokens; no raw hex in component code. +- **State completeness.** Every interactive element covers default, hover, focus-visible, active, disabled, loading. +- **Color never alone.** Status in color is always paired with an icon, label, or shape. + +Hard accessibility self-checks before shipping (verify against the rendered UI, not the source): text contrast ≥ 4.5:1, UI/focus indicators ≥ 3:1, touch targets ≥ 24px (WCAG 2.2 §1.4.3, §1.4.11, §2.5.8 [WCAG]). `DesignPrinciples` holds the full cross-cutting canon, including advisory thresholds and the named-law vocabulary. ## Direction Before coding, decide on a clear aesthetic direction. Pick one and stay true to it: +If `visual-profile.yaml` exists, load it first and treat it as authoritative. Compile it with `VisualIdentity` when CSS is missing. Components reference `--forge-*` semantic variables without literal identity fallbacks. Invoke `VisualIdentity` when the request needs a new or revised cross-output identity. + - Brutally minimal, maximalist chaos, retro-futuristic, organic, luxury, playful, editorial, brutalist, art deco, pastel, industrial — or a point of your own choosing. Name the context, the audience, and the one thing someone will remember about the result. @@ -45,6 +56,11 @@ No two designs should be the same. Vary themes (light/dark), fonts, aesthetics a ## Constraints - Produce real, working code — HTML/CSS/JS, React, Vue, etc. — not mockups +- Keep identity-specific values in `visual-profile.yaml` or generated profile CSS, never component code - Maintain a single aesthetic point-of-view across the whole output; do not mix brutalist typography with pastel gradients - Accessibility and performance are not aesthetic concessions; they are part of production-grade - Iterate one visual variable per round when refining a design with the user. Bundling changes (illustration + color logic + label edits in one push) forces the user to accept or reject the whole package; they can't keep the parts that landed and discard the parts that didn't. This costs at least one extra round per bundle. + +## Sources + +[WCAG]: https://www.w3.org/TR/WCAG22/ "Web Content Accessibility Guidelines (WCAG) 2.2, W3C Recommendation" diff --git a/skills/DesignPrinciples/SKILL.md b/skills/DesignPrinciples/SKILL.md new file mode 100644 index 0000000..07da149 --- /dev/null +++ b/skills/DesignPrinciples/SKILL.md @@ -0,0 +1,64 @@ +--- +name: DesignPrinciples +description: "Canonical cross-cutting design principles that DesignFrontend and DesignDashboard build on: durable framework-agnostic foundations, WCAG-anchored hard checks, and advisory thresholds. Reference library that other design skills cite, not invoked directly." +version: 0.1.0 +disable-model-invocation: true +--- + +# Design Principles + +The single source for design foundations shared across `DesignFrontend` (distinctive/premium aesthetics) and `DesignDashboard` (data-dense product UI). Those skills derive their inline digests from this canon and cite it by name; keep thresholds here so they do not drift. + +## Foundations + +Framework-agnostic and durable. These hold regardless of stack (Tailwind, CSS-in-JS, vanilla, React Native). + +- **Structure before style.** Generic *layout* is what makes AI-generated UI read as templated, not the palette. Fix information hierarchy (what is dominant, what is grouped, what is deferred) before touching color or type. +- **Tokens before values.** Every color, radius, and spacing value traces to a semantic token, so retheming is a config edit, not a find-replace. No raw hex in component code. +- **State completeness.** Every interactive element defines default, hover, focus-visible, active, disabled, and (if async) loading. Every data-bearing view defines empty, loading, and error, not just the populated happy path. +- **Color never alone.** Status encoded in color is always paired with an icon, label, or shape. One semantic color-to-meaning mapping, applied everywhere. +- **Progressive disclosure.** The default view shows what a user checks at a glance; detail is revealed on demand. +- **Reference, don't invent.** Study real top-tier examples for the surface being built, then adapt. A blank page converges on the statistical-average output. + +## Shared visual profile + +Use `skills/VisualIdentity/visual-profile.schema.yaml` v1 when one identity must span multiple outputs. `VisualIdentity` creates the profile and compiled CSS. `DesignFrontend`, `DesignDashboard`, `HtmlPlayground`, `PublicationFigures`, and presentation modules consume the same semantic contract. + +- Identity values live in `visual-profile.yaml`; generated CSS exposes `--forge-*` custom properties. +- Components use semantic variables without literal fallbacks. Profile switching changes CSS or `data-theme`, never markup or selectors. +- Data and state palettes come from active `modes.*.colors` and retain redundant non-color encodings. +- If no profile exists, create one before implementing reusable components. One-off exploratory sketches may use temporary tokens, but must normalize them into a profile before delivery. + +## Hard checks + +Spec-defined, verifiable against rendered/computed output. Run as a self-check before shipping. These are the only thresholds worth failing on. + +| Check | Threshold | Source | +| ----------------------- | -------------------------------- | ------------------------------- | +| Text contrast | ≥ 4.5:1 (normal), ≥ 3:1 (large) | WCAG 2.2 §1.4.3 AA [WCAG] | +| UI / focus indicator | ≥ 3:1 against adjacent color | WCAG 2.2 §1.4.11, §2.4.11 [WCAG]| +| Touch target | ≥ 24px (AA), prefer ≥ 44px (AAA) | WCAG 2.2 §2.5.8, §2.5.5 [WCAG] | + +Contrast needs the *rendered* color (semi-transparency, hover, and dark mode change it), not the source literal. Verify against the running UI, not a grep. + +## Advisory + +Conventions and heuristics, not standards. Parameterize to the design system; suggest, never hard-fail. + +- Spacing on a consistent grid (commonly 4px or 8px; Android leans 4pt). Consistency matters more than the specific base. +- Type scale in `rem` with a consistent ratio; avoid arbitrary one-off sizes. +- Small palette plus one decisive accent. Restraint reads as intentional. +- Roughly 5-9 primary items per view before splitting into tabs or drill-down (Miller's Law). +- Motion around 150-200ms with consistent easing; snappier reads as cheap, but this is preference, not defect. + +## Ages poorly + +Dated 2026 tells, useful as a smell, not a ban. The durable rule is *do not ship the framework default unchanged* and *decide the palette deliberately*, not "never use gradients." Treat these as prompts to check intent, not automatic rejections: emoji used as interface icons, an unmodified default Tailwind/shadcn palette, blue-to-purple gradient hero on white, gradient-filled avatar circles as image placeholders, Inter/Roboto as the only typeface, every chart the same default bar. + +## Named laws + +Advisory vocabulary for reasoning about a design, not pass/fail: Fitts's Law (target size and distance), Hick's Law (choice count and decision time), Miller's Law (7±2 chunks), Jakob's Law (match learned conventions), aesthetic-usability effect, Gestalt grouping (proximity, similarity, closure). + +## Sources + +[WCAG]: https://www.w3.org/TR/WCAG22/ "Web Content Accessibility Guidelines (WCAG) 2.2, W3C Recommendation" diff --git a/skills/DesignReview/SKILL.md b/skills/DesignReview/SKILL.md new file mode 100644 index 0000000..6cc4dc0 --- /dev/null +++ b/skills/DesignReview/SKILL.md @@ -0,0 +1,73 @@ +--- +name: DesignReview +description: "Verify a rendered interface against the DesignPrinciples canon from captured artifacts (screenshot + axe-core JSON per state and viewport). USE WHEN reviewing built UI, design review of a running app or component, checking a screenshot against design principles, or accessibility review of rendered output. Not for reviewing source code or unrendered designs." +version: 0.1.0 +allowed-tools: Read, Glob, Grep +--- + +# DesignReview + +Judge a rendered interface, not its source. Source review is blind on CSS-in-JS and React Native and cannot compute contrast; only rendered output verifies the `DesignPrinciples` canon. This skill is capture-agnostic: it consumes artifacts a separate tool produced and never runs a browser itself. + +## Inputs + +For each review target, expect two artifacts per (state, viewport): + +- a screenshot `PNG`, and +- an `axe-core` JSON report from the same rendered state. + +Name files so each maps to its target, state, and viewport: `____.png` and `____.axe.json`. States cover at least `empty`, `loading`, `error`, and `default`; viewports cover at least `mobile` and `desktop`. A manifest (or the caller's message) lists which targets, states, and viewports were captured. + +If no artifacts are present, stop and say so plainly. Do not review from memory or from source, and do not report a pass. + +## Capture + +Capture is delegated, not bundled. Produce the artifacts with a tool the environment already has, such as a Playwright MCP: navigate to the target in each state, run `axe-core` against the live DOM, and save the screenshot and the axe JSON. The capture must fix the viewport, disable animation, and wait for fonts and network idle so the screenshot is reproducible. + +## Review + +1. **Discover** the artifacts (Glob the capture directory) and confirm which states and viewports exist. +2. **Hard checks come only from the axe JSON**, never from the image. Map rule results to `DesignPrinciples` checks: + + | axe rule | DesignPrinciples check | WCAG | + | --------------- | ------------------------------------------------- | ----------- | + | `color-contrast`| text contrast ≥ 4.5:1 | §1.4.3 | + | `target-size` | touch target ≥ 24px or the spacing exception | §2.5.8 | + + Note on `target-size`: WCAG 2.5.8 exempts undersized targets whose center is ≥ 24px from every adjacent target's center. An isolated small button passes legitimately; a pass is "meets 2.5.8", not "all targets are 24px". Flag sub-44px targets only as advisory (AAA preference), never as a fail when axe passes. + + A rule in axe's `violations` is a verified-fail. A rule in `passes` is a verified-pass. A rule in `incomplete` (contrast over a gradient, image, or transformed node, or an unresolved background) is not-verified, never a pass. +3. **Judge the screenshots** for what a number cannot capture: structure before style, the anti-slop tells, and whether each state actually renders its job (an empty state with context and guidance, a real loading treatment, a recoverable error). +4. **State coverage.** A state with no artifact is not-verified. If only `default` was captured, report `empty`, `loading`, and `error` as not-verified rather than passing. +5. **No eyeballing hard numbers.** If a screenshot looks low-contrast but axe is silent or incomplete, record a not-verified suspicion that needs a computed check, not an estimated ratio. + +## Output + +One finding per issue, each tagged and citing the principle: + +| Field | Values | +| -------- | ----------------------------------------------- | +| verdict | `verified-fail` / `verified-pass` / `judgment-fail` / `not-verified` | +| target | target, state, viewport | +| check | the `DesignPrinciples` check or foundation | +| evidence | axe rule id, or what the screenshot shows | + +`verified-*` is reserved for axe-backed findings. Screenshot judgments (structure, anti-slop, state quality, color-never-alone) use `judgment-fail` — real findings, but a human can overrule them without contradicting a spec. + +Lead with verified-fails, then judgment-fails, then not-verified gaps. A review that verified only the happy path says so. + +## Red Flags + +| Anti-pattern | Fix | +| ----------------------------------------------------- | -------------------------------------------------- | +| Reporting a contrast pass from axe `incomplete` | Report not-verified; contrast was never computed | +| Estimating a contrast ratio from the screenshot | Hard numbers come only from axe | +| Passing states that were never captured | Mark absent states not-verified | +| Reviewing from source because no artifacts were found | Stop and report the capture gap | + +## Constraints + +- Hard numbers come only from the axe JSON; the screenshot is for judgment, never measurement. +- axe `incomplete` and any absent state are not-verified, never a pass. +- With no artifacts the review is a no-op that reports the gap; it never falls back to source or memory. +- Cite `DesignPrinciples` for every finding. diff --git a/skills/DesignReview/fixtures/baddash__default__desktop.axe.json b/skills/DesignReview/fixtures/baddash__default__desktop.axe.json new file mode 100644 index 0000000..392a17e --- /dev/null +++ b/skills/DesignReview/fixtures/baddash__default__desktop.axe.json @@ -0,0 +1,83 @@ +{ + "target": "baddash", + "state": "default", + "viewport": "desktop", + "url": "file:///private/tmp/claude-501/-Users-psvihra-forge/382595d0-8fd9-4975-980f-1f7d9e4d8988/scratchpad/dr-verify/dash.html?state=default&variant=bad", + "violations": [ + { + "id": "color-contrast", + "impact": "serious", + "help": "Elements must meet minimum color contrast ratio thresholds", + "nodes": [ + { + "target": [ + ".card:nth-child(1) > .label" + ], + "summary": "Fix any of the following:\n Element has insufficient color contrast of 1.46 (foreground color: #c9ccd1, background color: #f3f4f6, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1" + }, + { + "target": [ + ".card:nth-child(2) > .label" + ], + "summary": "Fix any of the following:\n Element has insufficient color contrast of 1.46 (foreground color: #c9ccd1, background color: #f3f4f6, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1" + }, + { + "target": [ + ".card:nth-child(3) > .label" + ], + "summary": "Fix any of the following:\n Element has insufficient color contrast of 1.46 (foreground color: #c9ccd1, background color: #f3f4f6, font size: 9.0pt (12px), font weight: normal). Expected contrast ratio of 4.5:1" + } + ] + }, + { + "id": "target-size", + "impact": "serious", + "help": "All touch targets must be 24px large, or leave sufficient space", + "nodes": [ + { + "target": [ + "button[aria-label=\"Refresh\"]" + ], + "summary": "Fix any of the following:\n Target has insufficient size (18px by 18px, should be at least 24px by 24px)\n Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of 16px instead of at least 24px." + }, + { + "target": [ + "button[aria-label=\"Filter\"]" + ], + "summary": "Fix any of the following:\n Target has insufficient size (18px by 18px, should be at least 24px by 24px)\n Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of 16px instead of at least 24px." + }, + { + "target": [ + "button[aria-label=\"More\"]" + ], + "summary": "Fix any of the following:\n Target has insufficient size (18px by 18px, should be at least 24px by 24px)\n Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of 16px instead of at least 24px." + } + ] + } + ], + "incomplete": [], + "passes": [ + "aria-allowed-attr", + "aria-conditional-attr", + "aria-deprecated-role", + "aria-hidden-body", + "aria-hidden-focus", + "aria-prohibited-attr", + "aria-required-attr", + "aria-roles", + "aria-valid-attr-value", + "aria-valid-attr", + "avoid-inline-spacing", + "button-name", + "bypass", + "color-contrast", + "document-title", + "html-has-lang", + "html-lang-valid", + "link-name", + "meta-viewport", + "nested-interactive", + "role-img-alt", + "target-size" + ] +} diff --git a/skills/DesignReview/fixtures/baddash__default__desktop.png b/skills/DesignReview/fixtures/baddash__default__desktop.png new file mode 100644 index 0000000..4afac3d Binary files /dev/null and b/skills/DesignReview/fixtures/baddash__default__desktop.png differ diff --git a/skills/DesignReview/fixtures/baddash__empty__desktop.axe.json b/skills/DesignReview/fixtures/baddash__empty__desktop.axe.json new file mode 100644 index 0000000..ac09963 --- /dev/null +++ b/skills/DesignReview/fixtures/baddash__empty__desktop.axe.json @@ -0,0 +1,27 @@ +{ + "target": "baddash", + "state": "empty", + "viewport": "desktop", + "url": "file:///private/tmp/claude-501/-Users-psvihra-forge/382595d0-8fd9-4975-980f-1f7d9e4d8988/scratchpad/dr-verify/dash.html?state=empty&variant=bad", + "violations": [], + "incomplete": [], + "passes": [ + "aria-allowed-attr", + "aria-conditional-attr", + "aria-hidden-body", + "aria-hidden-focus", + "aria-prohibited-attr", + "aria-valid-attr-value", + "aria-valid-attr", + "button-name", + "bypass", + "color-contrast", + "document-title", + "html-has-lang", + "html-lang-valid", + "link-name", + "meta-viewport", + "nested-interactive", + "target-size" + ] +} diff --git a/skills/DesignReview/fixtures/baddash__empty__desktop.png b/skills/DesignReview/fixtures/baddash__empty__desktop.png new file mode 100644 index 0000000..463deb9 Binary files /dev/null and b/skills/DesignReview/fixtures/baddash__empty__desktop.png differ diff --git a/skills/DesignReview/fixtures/capture.mjs b/skills/DesignReview/fixtures/capture.mjs new file mode 100644 index 0000000..436f645 --- /dev/null +++ b/skills/DesignReview/fixtures/capture.mjs @@ -0,0 +1,44 @@ +// Capture screenshots + axe-core reports per the DesignReview contract: +// ____.png / .axe.json +import { chromium } from 'playwright'; +import AxeBuilder from '@axe-core/playwright'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import { resolve } from 'node:path'; + +const [,, htmlFile, target, variant = 'good', statesArg = 'default,empty,loading,error'] = process.argv; +if (!htmlFile || !target) { + console.error('usage: node capture.mjs [variant] [states]'); + process.exit(1); +} + +const viewports = { mobile: { width: 390, height: 844 }, desktop: { width: 1440, height: 900 } }; +const states = statesArg.split(','); +const outDir = resolve('artifacts'); +mkdirSync(outDir, { recursive: true }); + +const browser = await chromium.launch(); +for (const [viewportName, size] of Object.entries(viewports)) { + const context = await browser.newContext({ viewport: size, reducedMotion: 'reduce' }); + const page = await context.newPage(); + for (const state of states) { + const url = `${pathToFileURL(resolve(htmlFile))}?state=${state}&variant=${variant}`; + await page.goto(url, { waitUntil: 'networkidle' }); + await page.evaluate(() => document.fonts.ready); + const base = `${outDir}/${target}__${state}__${viewportName}`; + await page.screenshot({ path: `${base}.png`, fullPage: true }); + const axeResults = await new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']) + .analyze(); + const compact = { + target, state, viewport: viewportName, url, + violations: axeResults.violations.map(r => ({ id: r.id, impact: r.impact, help: r.help, nodes: r.nodes.map(n => ({ target: n.target, summary: n.failureSummary })) })), + incomplete: axeResults.incomplete.map(r => ({ id: r.id, help: r.help, nodes: r.nodes.map(n => ({ target: n.target })) })), + passes: axeResults.passes.map(r => r.id), + }; + writeFileSync(`${base}.axe.json`, JSON.stringify(compact, null, 2) + '\n'); + console.log(`${target}__${state}__${viewportName}: ${compact.violations.length} violations, ${compact.incomplete.length} incomplete`); + } + await context.close(); +} +await browser.close(); diff --git a/skills/DesignReview/fixtures/dash.html b/skills/DesignReview/fixtures/dash.html new file mode 100644 index 0000000..5e33d9d --- /dev/null +++ b/skills/DesignReview/fixtures/dash.html @@ -0,0 +1,101 @@ + + + + + +Test Dashboard + + + +
+ +
+

Fleet Telemetry

+
+
+
+ + + diff --git a/skills/DesignReview/fixtures/expected-findings.md b/skills/DesignReview/fixtures/expected-findings.md new file mode 100644 index 0000000..ecbfdaa --- /dev/null +++ b/skills/DesignReview/fixtures/expected-findings.md @@ -0,0 +1,39 @@ +# DesignReview Fixture — Expected Findings + +Reference artifacts for the `DesignReview` skill, captured with Playwright + axe-core (`capture.mjs` against `dash.html?variant=bad`, desktop viewport 1440x900, reduced motion, fonts and network idle). A correct review of this fixture produces the findings below; deviations indicate contract drift in the skill. + +## Artifacts + +| File | Contents | +| ----------------------------------- | --------------------------------------------------------------- | +| `baddash__default__desktop.png` | Populated dashboard with seeded defects | +| `baddash__default__desktop.axe.json`| axe report: `color-contrast` + `target-size` violations | +| `baddash__empty__desktop.png` | Empty state (context + guidance + CTA, no violations) | +| `baddash__empty__desktop.axe.json` | axe report: no violations | +| `dash.html` | Source page; `?variant=bad\|good`, `?state=default\|empty\|loading\|error` | +| `capture.mjs` | Capture script (playwright + @axe-core/playwright) | + +## Expected verdicts + +| verdict | state | check | evidence | +| -------------- | ------- | ---------------------------------- | -------------------------------------------------------------------- | +| verified-fail | default | text contrast ≥ 4.5:1 | axe `color-contrast`: `.card .label` at 1.46:1 (#c9ccd1 on #f3f4f6) | +| verified-fail | default | touch target (WCAG 2.5.8) | axe `target-size`: R/F/M buttons 18×18px, clustered under 24px spacing | +| judgment-fail | default | ages-poorly smell / intent check | emoji (🚀📊⚙️) as the only nav iconography — flag intent, not auto-fail | +| verified-pass | empty | state completeness | heading + explanation + CTA, no axe violations | +| not-verified | loading | all checks | state absent from this fixture — must NOT be reported as pass | +| not-verified | error | all checks | state absent from this fixture — must NOT be reported as pass | + +## Contract points this fixture exercises + +- Hard verdicts sourced only from the axe JSON, ratios quoted from it, never estimated off the PNG. +- Absent states (`loading`, `error` here) reported not-verified, never passing. +- `target-size` semantics: WCAG 2.5.8 includes a spacing exception — an isolated small button passes legitimately; only the clustered R/F/M row violates. +- Screenshot findings are `judgment-fail`, reserved axe-backed findings are `verified-*`. + +Regenerate after editing `dash.html`: + +```sh +npm install playwright @axe-core/playwright && npx playwright install chromium +node capture.mjs dash.html baddash bad default,empty +``` diff --git a/skills/HtmlPlayground/SKILL.md b/skills/HtmlPlayground/SKILL.md index 89c9b47..3df6e04 100644 --- a/skills/HtmlPlayground/SKILL.md +++ b/skills/HtmlPlayground/SKILL.md @@ -23,7 +23,9 @@ Before adding CSS to a project (snippets, themes, component styles), render the 3. Generate a single HTML file at `/tmp/css-preview-{topic}.html` with this structure: - - Dark theme by default using CSS variables (adapts if user prefers light) + - Load the active `visual-profile.yaml` through its compiled CSS when present + - Otherwise use temporary semantic `--forge-*` variables and label them as an unselected preview profile + - Dark theme by default when the profile provides it (adapts if user prefers light) - `prefers-color-scheme` media query with light mode fallback - A grid of cards, one per technique, each self-contained - Each card has: title, source link, the rendered demo with realistic data @@ -44,6 +46,7 @@ Each card is a self-contained demo. Use these conventions: - Subtle border, rounded corners (12px) - Title in the card header with source attribution as small text below - Demo content uses the same CSS variable names as the target platform (Obsidian vars for vault snippets, standard custom properties for web projects) +- Reusable web demos use the shared `--forge-*` visual-profile contract; component rules contain no raw identity values or literal fallbacks - Mock data should feel real — use the user's actual project names, task descriptions, and entity types when known from conversation context ## Constraints @@ -52,3 +55,4 @@ Each card is a self-contained demo. Use these conventions: - No JavaScript unless the comparison requires interactivity (hover states, toggles) - File goes to `/tmp/` — never write preview HTML into the project directory - Source attribution is mandatory when the technique has a known origin +- Profile switching must not require markup or selector changes diff --git a/skills/VisualIdentity/SKILL.md b/skills/VisualIdentity/SKILL.md new file mode 100644 index 0000000..96bec53 --- /dev/null +++ b/skills/VisualIdentity/SKILL.md @@ -0,0 +1,63 @@ +--- +name: VisualIdentity +description: "Create or normalize a shared visual identity from seed colors, logos, references, websites, decks, institutional guidelines, or a verbal brief. Produce three previews, a validated visual-profile.yaml v1, and CSS custom properties. USE WHEN create visual identity, brand system, theme tokens, color profile, design tokens, accessible palette, light or dark theme, or restyle multiple Forge outputs from one profile." +version: 0.1.0 +--- + +# VisualIdentity + +Build identity as a portable token contract. Keep appearance in the profile; components consume semantic CSS variables and contain no identity-specific color values. + +## Workflow + +1. Inventory supplied sources. Record every source in `provenance` with kind, source, note, and license when known. Set the profile's output license explicitly. Never copy a third-party identity without permission; extract general principles when reuse rights are unclear. +2. Infer identity goals: audience, tone, medium, accessibility needs, light/dark requirement, and constraints. Ask only when a missing answer changes the design materially. +3. Create three distinct identity directions. Keep content and component markup identical across previews so only identity changes. Render realistic typography, data series, states, focus, and one dense component in each direction. +4. Select or revise one direction. Emit `visual-profile.yaml` matching `skills/VisualIdentity/visual-profile.schema.yaml` v1. Use `skills/VisualIdentity/visual-profile.example.yaml` as shape guidance, not as a default aesthetic. +5. Check normal text contrast at 4.5:1, large text at 3:1, UI/focus at 3:1, and data colors with redundant marker, line, label, or shape encoding. Check every requested theme against rendered colors. +6. Compile CSS custom properties: + + ```sh + ruby skills/VisualIdentity/scripts/compile-profile.rb visual-profile.yaml --output visual-profile.css + ``` + +7. Apply profile without selector or markup changes. Scan component CSS/JS/HTML for raw identity colors. Raw values belong only in profiles, generated CSS, test fixtures, or intentional media assets. +8. Deliver three previews, selected profile, compiled CSS, accessibility results, and provenance notes. + +## Contract + +- `modes.light` is required; `modes.dark` is optional. +- Each mode owns canonical semantic UI colors, eight data-series colors, states, grid, axes, and annotations. +- Global tokens own typography (including math), spacing, radii, focus geometry, and motion. +- Components reference variables such as `var(--forge-color-ink)`, `var(--forge-color-series-1)`, and `var(--forge-color-danger)` without literal fallbacks. +- Profile switching changes only the active profile CSS or `data-theme`; it never requires selector or markup edits. +- Reduced motion uses `motion.reducedMotion`; do not remove information with animation. + +## Output + +```markdown +## Identity directions +1. [name] — [principle and preview path] +2. [name] — [principle and preview path] +3. [name] — [principle and preview path] + +## Selected profile +- Profile: [visual-profile.yaml] +- CSS: [visual-profile.css] +- Themes: [light/dark] +- Provenance: [inputs, sources, and licenses] + +## Verification +- Schema: +- Contrast: +- Data/state redundancy: +- Reduced motion: +- Raw identity scan: +``` + +## Constraints + +- Do not place identity-specific hex, rgb, hsl, named colors, font families, spacing, radii, or motion values in component code. +- Do not claim accessibility from source literals alone; verify rendered combinations. +- Do not use color as the only carrier of state or data meaning. +- Do not silently invent provenance or licensing. diff --git a/skills/VisualIdentity/scripts/compile-profile.rb b/skills/VisualIdentity/scripts/compile-profile.rb new file mode 100755 index 0000000..fdcb6fd --- /dev/null +++ b/skills/VisualIdentity/scripts/compile-profile.rb @@ -0,0 +1,75 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "optparse" +require "yaml" + +options = { prefix: "forge" } +OptionParser.new do |parser| + parser.banner = "Usage: compile-profile.rb PROFILE [--output FILE] [--prefix NAME]" + parser.on("--output FILE", "Write CSS to FILE instead of stdout") { |value| options[:output] = value } + parser.on("--prefix NAME", "CSS custom-property prefix (default: forge)") { |value| options[:prefix] = value } +end.parse! + +profile_path = ARGV.shift +abort "profile path required" unless profile_path +abort "unexpected arguments: #{ARGV.join(' ')}" unless ARGV.empty? +abort "invalid prefix" unless options[:prefix].match?(/\A[a-z][a-z0-9-]*\z/) + +profile = YAML.safe_load(File.read(profile_path), permitted_classes: [], permitted_symbols: [], aliases: false) +abort "profile must be a mapping" unless profile.is_a?(Hash) +abort "unsupported profile version" unless profile["version"] == 1 + +required = %w[id name license provenance modes typography spacing radii focus motion] +missing = required.reject { |key| profile.key?(key) } +abort "missing keys: #{missing.join(', ')}" unless missing.empty? +abort "modes.light.colors required" unless profile.dig("modes", "light", "colors").is_a?(Hash) + +def declarations(value, path = [], output = []) + case value + when Hash + value.each do |key, child| + css_key = key.to_s.gsub(/([a-z0-9])([A-Z])/, "\\1-\\2").downcase + declarations(child, path + [css_key], output) + end + when Array + value.each_with_index { |child, index| declarations(child, path + [(index + 1).to_s], output) } + else + output << [path.join("-"), value.to_s] + end + output +end + +required_colors = %w[ + canvas surface elevated ink muted inverse border rule brand-primary + brand-secondary brand-accent on-primary focus success warning danger info + link series-1 series-2 series-3 series-4 series-5 series-6 series-7 series-8 + grid axis annotation +] +profile.fetch("modes").each do |mode_name, mode| + colors = mode.is_a?(Hash) ? mode["colors"] : nil + abort "modes.#{mode_name}.colors must be a mapping" unless colors.is_a?(Hash) + absent = required_colors.reject { |name| colors.key?(name) } + abort "modes.#{mode_name}.colors missing: #{absent.join(', ')}" unless absent.empty? +end + +def rule(selector, values, prefix) + body = declarations(values).map { |name, value| " --#{prefix}-#{name}: #{value};" }.join("\n") + "#{selector} {\n#{body}\n}" +end + +globals = profile.slice("typography", "spacing", "radii", "focus", "motion") +css = ["/* Generated from #{File.basename(profile_path)}; edit profile, not this file. */"] +css << rule(":root", globals, options[:prefix]) +css << rule(':root, [data-theme="light"]', { "color" => profile.dig("modes", "light", "colors") }, options[:prefix]) +if profile.dig("modes", "dark", "colors") + css << rule('[data-theme="dark"]', { "color" => profile.dig("modes", "dark", "colors") }, options[:prefix]) +end +css << "@media (prefers-reduced-motion: reduce) {\n :root {\n --#{options[:prefix]}-motion-durations-fast: var(--#{options[:prefix]}-motion-reduced-motion);\n --#{options[:prefix]}-motion-durations-base: var(--#{options[:prefix]}-motion-reduced-motion);\n --#{options[:prefix]}-motion-durations-slow: var(--#{options[:prefix]}-motion-reduced-motion);\n }\n}" +output = css.join("\n\n") + "\n" + +if options[:output] + File.write(options[:output], output) +else + $stdout.write(output) +end diff --git a/skills/VisualIdentity/visual-profile.example.yaml b/skills/VisualIdentity/visual-profile.example.yaml new file mode 100644 index 0000000..673ab4d --- /dev/null +++ b/skills/VisualIdentity/visual-profile.example.yaml @@ -0,0 +1,87 @@ +version: 1 +id: forge-editorial +name: Forge Editorial +description: Neutral editorial profile showing the visual-profile v1 contract. +license: CC0-1.0 +provenance: + - kind: brief + source: original-generalized-brief + license: CC0-1.0 + note: General-purpose editorial identity; no external brand assets used. +modes: + light: + colors: + canvas: "#f7f5f0" + surface: "#ffffff" + elevated: "#ffffff" + ink: "#171a1f" + muted: "#565d68" + inverse: "#ffffff" + border: "#b9b4aa" + rule: "#d5d0c7" + brand-primary: "#005fcc" + brand-secondary: "#7b4ab3" + brand-accent: "#a34400" + on-primary: "#ffffff" + focus: "#9b3cff" + success: "#08755f" + warning: "#8a5200" + danger: "#a61e47" + info: "#0057b8" + link: "#0057b8" + series-1: "#005fcc" + series-2: "#a34400" + series-3: "#00876c" + series-4: "#7b4ab3" + series-5: "#b02755" + series-6: "#5f6b00" + series-7: "#6f4b32" + series-8: "#3f6c88" + grid: "#d5d0c7" + axis: "#565d68" + annotation: "#67172f" + dark: + colors: + canvas: "#11151b" + surface: "#1b222c" + elevated: "#252e3a" + ink: "#f5f7fa" + muted: "#b8c0cc" + inverse: "#11151b" + border: "#526071" + rule: "#384454" + brand-primary: "#78b7ff" + brand-secondary: "#c5a1ff" + brand-accent: "#ffad70" + on-primary: "#10243e" + focus: "#d09cff" + success: "#5fd6b6" + warning: "#ffc66d" + danger: "#ff8fae" + info: "#8fc4ff" + link: "#8fc4ff" + series-1: "#78b7ff" + series-2: "#ffad70" + series-3: "#5fd6b6" + series-4: "#c5a1ff" + series-5: "#ff8fae" + series-6: "#c8d76a" + series-7: "#d49a73" + series-8: "#83bedc" + grid: "#384454" + axis: "#b8c0cc" + annotation: "#ffb5c8" +typography: + display: '"Source Serif 4", Georgia, serif' + body: '"IBM Plex Sans", Arial, sans-serif' + mono: '"IBM Plex Mono", "Courier New", monospace' + math: '"STIX Two Math", "Cambria Math", serif' + weights: {regular: 400, medium: 500, bold: 700} +spacing: + scale: {1: 0.5rem, 2: 1rem, 3: 1.5rem, 4: 2rem, 6: 3rem, 8: 4rem} +radii: {sm: 0.25rem, md: 0.5rem, lg: 1rem, pill: 999px} +focus: {width: 3px, offset: 2px} +motion: + durations: {fast: 120ms, base: 180ms, slow: 320ms} + easing: {standard: "cubic-bezier(0.2, 0, 0, 1)", emphasized: "cubic-bezier(0.2, 0.8, 0.2, 1)"} + reducedMotion: 0ms diff --git a/skills/VisualIdentity/visual-profile.schema.yaml b/skills/VisualIdentity/visual-profile.schema.yaml new file mode 100644 index 0000000..840940d --- /dev/null +++ b/skills/VisualIdentity/visual-profile.schema.yaml @@ -0,0 +1,189 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: https://github.com/N4M3Z/forge-core/skills/VisualIdentity/visual-profile.schema.yaml +title: Forge visual profile v1 +type: object +additionalProperties: false +required: [version, id, name, license, provenance, modes, typography, spacing, radii, focus, motion] +properties: + version: + const: 1 + id: + type: string + pattern: ^[a-z][a-z0-9-]*$ + name: + type: string + minLength: 1 + description: + type: string + license: + type: string + minLength: 1 + provenance: + type: array + minItems: 1 + items: + type: object + additionalProperties: false + required: [kind, source, note] + properties: + kind: + enum: [seed-colors, logo, visual-reference, website, deck, guidelines, brief] + source: + type: string + minLength: 1 + license: + type: string + note: + type: string + modes: + type: object + additionalProperties: false + required: [light] + properties: + light: {$ref: "#/$defs/mode"} + dark: {$ref: "#/$defs/mode"} + typography: + type: object + additionalProperties: false + required: [display, body, mono, math, weights] + properties: + display: {type: string, minLength: 1} + body: {type: string, minLength: 1} + mono: {type: string, minLength: 1} + math: {type: string, minLength: 1} + weights: + type: object + additionalProperties: false + required: [regular, medium, bold] + properties: + regular: {$ref: "#/$defs/fontWeight"} + medium: {$ref: "#/$defs/fontWeight"} + bold: {$ref: "#/$defs/fontWeight"} + spacing: + type: object + additionalProperties: false + required: [scale] + properties: + scale: + type: object + minProperties: 5 + additionalProperties: {$ref: "#/$defs/cssLength"} + radii: + type: object + additionalProperties: false + required: [sm, md, lg, pill] + properties: + sm: {$ref: "#/$defs/cssLength"} + md: {$ref: "#/$defs/cssLength"} + lg: {$ref: "#/$defs/cssLength"} + pill: {$ref: "#/$defs/cssLength"} + focus: + type: object + additionalProperties: false + required: [width, offset] + properties: + width: {$ref: "#/$defs/cssLength"} + offset: {$ref: "#/$defs/cssLength"} + motion: + type: object + additionalProperties: false + required: [durations, easing, reducedMotion] + properties: + durations: + type: object + additionalProperties: false + required: [fast, base, slow] + properties: + fast: {$ref: "#/$defs/cssTime"} + base: {$ref: "#/$defs/cssTime"} + slow: {$ref: "#/$defs/cssTime"} + easing: + type: object + additionalProperties: false + required: [standard, emphasized] + properties: + standard: {type: string, minLength: 1} + emphasized: {type: string, minLength: 1} + reducedMotion: {$ref: "#/$defs/cssTime"} +$defs: + cssColor: + type: string + minLength: 1 + cssLength: + type: string + pattern: ^-?(0|[0-9]+(\.[0-9]+)?(px|rem|em|ch|vw|vh|%))$ + cssTime: + type: string + pattern: ^[0-9]+(\.[0-9]+)?(ms|s)$ + fontWeight: + type: integer + minimum: 1 + maximum: 1000 + mode: + type: object + additionalProperties: false + required: [colors] + properties: + colors: + type: object + additionalProperties: false + required: + - canvas + - surface + - elevated + - ink + - muted + - inverse + - border + - rule + - brand-primary + - brand-secondary + - brand-accent + - on-primary + - focus + - success + - warning + - danger + - info + - link + - series-1 + - series-2 + - series-3 + - series-4 + - series-5 + - series-6 + - series-7 + - series-8 + - grid + - axis + - annotation + properties: + canvas: {$ref: "#/$defs/cssColor"} + surface: {$ref: "#/$defs/cssColor"} + elevated: {$ref: "#/$defs/cssColor"} + ink: {$ref: "#/$defs/cssColor"} + muted: {$ref: "#/$defs/cssColor"} + inverse: {$ref: "#/$defs/cssColor"} + border: {$ref: "#/$defs/cssColor"} + rule: {$ref: "#/$defs/cssColor"} + brand-primary: {$ref: "#/$defs/cssColor"} + brand-secondary: {$ref: "#/$defs/cssColor"} + brand-accent: {$ref: "#/$defs/cssColor"} + on-primary: {$ref: "#/$defs/cssColor"} + focus: {$ref: "#/$defs/cssColor"} + success: {$ref: "#/$defs/cssColor"} + warning: {$ref: "#/$defs/cssColor"} + danger: {$ref: "#/$defs/cssColor"} + info: {$ref: "#/$defs/cssColor"} + link: {$ref: "#/$defs/cssColor"} + series-1: {$ref: "#/$defs/cssColor"} + series-2: {$ref: "#/$defs/cssColor"} + series-3: {$ref: "#/$defs/cssColor"} + series-4: {$ref: "#/$defs/cssColor"} + series-5: {$ref: "#/$defs/cssColor"} + series-6: {$ref: "#/$defs/cssColor"} + series-7: {$ref: "#/$defs/cssColor"} + series-8: {$ref: "#/$defs/cssColor"} + grid: {$ref: "#/$defs/cssColor"} + axis: {$ref: "#/$defs/cssColor"} + annotation: {$ref: "#/$defs/cssColor"}