angora-component — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited angora-component (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
@theme) generates no Tailwind utility classes — only semantic tokens produce usable classes. This is structural enforcement for dark mode compatibility.src/components/. If the component already exists, read it first. Understand what's there before making changes. If building a new component, read 2–3 existing project components to learn the established patterns (styling conventions, prop style, layout approach).src/components/Section.astro to understand the Section pattern.Four types: primitives, composites, landmarks, and content components.
Atomic, single-purpose elements. Single file. Take props directly.
Children-first composition. Even for primitives, visual content (icons, badges, decorative elements) composes through <slot /> — not through dedicated props. The consumer controls order by placement. This keeps the API surface minimal and the component maximally flexible.
<!-- Bad — prop for visual content -->
<Button icon={ArrowRight} label="Continue" />
<!-- Good — children control content and order -->
<Button>Continue <ArrowRight /></Button>Only accept behavior/layout props (variant, size, disabled) and a default <slot />.
Examples: Button, TextInput, Toggle, Badge, Section, FormRow, FieldGroup.
Layout shells that arrange content through sub-components, not prop bags. Directory structure groups the shell with its sub-components.
The rule: the shell has zero content props. Content flows exclusively through sub-components. The shell only accepts behavior/layout props (variant, size) and a default <slot />.
Sub-components only — no content props on shells. Every piece of content in a composite must be a sub-component. No string props for headings/text, no arrays for lists of items, no slot-based styled markup on the shell. If a section of a composite has content, it's a sub-component — even if it's "just a string." Sub-components own their own styling, defaults, and structure. The consumer composes what they need, omits what they don't.
Why this is strict: Content props create two problems. First, they couple content to the shell — adding/removing a section means changing the shell's interface. Second, they push decisions about rendering into the shell via conditionals ({dataProtectionText && ...}). Sub-components eliminate both: add a section by composing a sub-component, remove it by not including it.
Reference patterns:
<!-- Bad — prop bag -->
<Card imageSrc="/img.jpg" eyebrow="Engineering" title="Hello" description="..." />
<!-- Bad — content props on shell -->
<Footer newsletterHeading="..." dataProtectionText="..." navLinks={[...]} />
<!-- Good — sub-components only -->
<Card padding={false}>
<CardImage src="/img.jpg" alt="..." />
<CardBody>
<CardEyebrow>Engineering</CardEyebrow>
<CardTitle>Building reliable systems</CardTitle>
<CardDescription>How we scaled our infrastructure.</CardDescription>
</CardBody>
</Card>
<!-- Good — landmark composite with sub-components -->
<Footer>
<FooterNewsletter />
<FooterDataProtection>Legal text here...</FooterDataProtection>
<FooterPartners>
<FooterPartnerLogo src="..." alt="..." width={93} height={40} />
</FooterPartners>
<FooterSocial>
<FooterSocialLink href="..." icon="facebook" label="Facebook" />
</FooterSocial>
<FooterNav>
<FooterNavLink href="..." label="Privacy Policy" />
</FooterNav>
</Footer>The shell owns background, container, and spacing. Each sub-component owns its own heading (defaulted), typography, and layout. Consumers compose only the parts they need — no conditionals in the shell.
A landmark component lives on its own without a Section — it participates directly in section-flow and owns its own semantic HTML element, container behavior, and spacing.
Landmarks can be composites too. A landmark that has multiple distinct content sections (e.g., a Footer with newsletter + data protection + partners + social + nav) should follow the composite pattern — shell + sub-components. Being a landmark (owns its semantic element and flow attributes) doesn't exempt a component from the composite rule. If the landmark has more than 3 content props, break it into sub-components.
Classification questions — ask during the spec step:
<Section>.The "section" word trap. When you hear "section" in a request, separate the pattern from the page context. Use this phrasing: "I'm hearing two things — the pattern (accordion/carousel/grid) and the page context (a section with a heading). I'll build the pattern as a reusable component. When you use it on a page, you wrap it in `<Section>` to give it a heading. That way the component works anywhere, not just as a standalone page section."
Landmark structure:
<section>, <footer>, <nav>, or <header> directly with data-component and flow attributes (data-seamless, data-full-width). Example: Hero renders <section data-component="Hero" data-seamless data-full-width>. The consumer never wraps it in Section.Section reference:
Section is a thin semantic wrapper — it renders <section> with @container, seamless variant (data-seamless for section-flow), fullWidth mode (data-full-width — drops flow width constraints), narrow mode (constrains to --container-narrow), withWrap (re-constrains children of full-width sections to container width), withPadding (adds vertical padding without data-seamless behavior), and title prop (renders <h2> with automatic aria-labelledby). Section does NOT own width or horizontal padding — the flow utility (section-flow/prose-flow) handles all width constraints on direct children.section-flow handles spacing. Seamless sections get standardized vertical padding tied to --section-gap.aria-label via props for sections without a visible heading.A content component sits inside a Section (or another container). It renders its own @container wrapper so container queries work anywhere — inside Section, in a specimen page, standalone. The consumer controls section behavior (seamless, narrow, title) via the wrapping Section.
<!-- Consumer controls the section context -->
<Section title="Our Components">
<MyComponent>
<MyComponentItem ... />
</MyComponent>
</Section>
<!-- Same component works standalone — @container is self-provided -->
<MyComponent>
<MyComponentItem ... />
</MyComponent>Content component structure:
---
const { class: className, ...props } = Astro.props;
---
<div data-component="MyComponent" class:list={["@container", className]} {...props}>
<div class="grid grid-cols-2 @sm:grid-cols-3 @md:grid-cols-4 grid-gap">
<slot />
</div>
</div>The outer div provides @container (so container queries resolve against the component's own width). The inner div holds the layout. This means the component works anywhere without depending on an ancestor @container.
Heading ownership — two-pattern distinction:
title prop. Content components never accept a heading or title prop — the consumer provides it via the wrapping <Section title="...">.title on a parent Section. If the heading is embedded inside the component's layout as part of its content composition, use a sub-component.Internal padding vs. outer spacing — never both on the same boundary.
section-flow for page-level rhythm (--section-gap: 4rem), prose-flow for typography-level rhythm (--prose-gap: 1.25em). Both include ambient prose via @apply prose — typography for raw HTML elements is automatic. Pages get section-flow, posts/articles get prose-flow. Components own zero vertical margin — spacing comes entirely from the parent flow context.section-flow margin provides all inter-section spacing.--section-gap, so the breathing room inside a backgrounded section matches the gap between sections. Consistent rhythm everywhere.margin-top/margin-bottom on their outermost element. That's the flow context's job.global.css) that controls vertical rhythm between sections. Gap controlled by --section-gap. Adjacent [data-seamless] sections get 0 gap so backgrounds butt up.global.css) for blog posts and editorial content. Gap controlled by --prose-gap. Used when paragraphs, headings, images, and inline components flow at reading rhythm.py-* AND sits inside section-flow, you get padding + margin = double whitespace. Non-seamless sections avoid this by having no vertical padding. Seamless sections are safe because adjacent seamless sections get 0 margin from section-flow, and the standardized padding provides consistent internal rhythm.Primitives → single file. Composites → directory.
components/
Button.astro ← primitive, single file
Section.astro ← primitive, single file (the section wrapper)
TextInput.astro ← primitive, single file
Card/ ← composite, directory
Card.astro ← shell (variant/layout props)
CardImage.astro ← image with placeholder fallback
CardBody.astro ← padded content wrapper
CardEyebrow.astro ← uppercase category label
CardTitle.astro ← title (accepts `as` prop for heading level)
CardDescription.astro ← body text
Hero/ ← landmark composite, directory
Hero.astro ← renders own <section> with data-component + flow attrs
HeroTitle.astro
HeroDescription.astro
HeroActions.astro
MyComponent/ ← content composite, directory
MyComponent.astro ← @container wrapper + grid (no Section)
MyComponentItem.astroThe directory groups the shell with its sub-components. Consumers import from the directory: import Card from '../components/Card/Card.astro'.
Every component requires three files:
src/components/<Name>.astrosrc/pages/design-system/<name>.astro (using Layout from _layout/, shows all variants/states)src/pages/design-system/view/_content/<name>.astro (pure markup, no FullScreen wrapper). The dynamic route at view/[theme]/[...slug].astro wraps this in FullScreen and applies the theme. This generates /view/light/<name> always, plus /view/dark/<name> when dark mode is enabled.The sidebar auto-discovers design system pages via import.meta.glob — no manual nav registration needed. Just create the file and it appears. The fullscreenHref prop on Layout should point to /design-system/view/light/<name> (the toggle in the sidebar handles switching to the dark route).
Before speccing anything, verify the user is asking for the right component. Ask: "What does this need to DO?" — not what it looks like.
The "looks like" trap: Designers coming from graphic design think in visual similarity — a collapsed accordion item looks like an input field (both are rectangles with text and a chevron). A card looks like a button (both are clickable rectangles). But components are defined by behavior and semantics, not shape:
If the user says "I want to use X for Y" and the semantic purpose doesn't match, push back immediately: "[Component X] is for [its purpose] — what you're describing is [correct pattern], which is a different component with different semantics and a different accessibility contract. Let me build the right one."
If the user asks for a component by a use-case name (e.g., "FAQ"), redirect to the generic pattern name (e.g., "Accordion") — see naming rule below.
This isn't gatekeeping — it's mentorship. Explain why the distinction matters (accessibility, reusability, semantic HTML) so the designer builds the right mental model.
Activate when the user asks to see multiple options, compare approaches, or explore directions — "show me a few takes on this hero," "I want to see some options," "what are some ways to do this."
Write style definitions before building anything. Each direction gets a written description that's specific enough to build from. Cover how it arranges content, how it uses type (scale contrast, weight, font character), what the color temperature and palette emphasis are, how dense or spacious it feels, whether it uses containers/cards or open space, and what the overall mood is.
Each definition must be a genuinely different design philosophy — not three variations of the same idea with tweaked spacing.
Bad: "Option A: Simple and clean with lots of space"
Good: "Option A: High-contrast typographic — large serif display heading against tight body text, deep vertical rhythm between sections, muted earth tones with one bold accent reserved for the primary action. No cards or containers — hierarchy carried entirely by scale and weight."
The flow:
HeroEditorial.astro, HeroBold.astro)Present a spec to the user covering:
data-component and flow attributes. If content: provides its own @container and sits inside a consumer-provided Sectionstate prop values if form-relatedWait for the user to approve before building.
state prop only for form states that can't be triggered by interaction (error, success, disabled). All color values from semantic token utilities only (bg-card, text-foreground, border-border, etc.) — never raw palette classes<section>, <footer>, <nav>) with data-component and flow attributes (data-seamless, data-full-width)section-flow handles spacing@container wrapper div as its root — container queries work anywhere, no dependency on a parent @container@container<Section> to control title, seamless, narrowheading, title, seamless, or narrow props — those belong to the consumer's SectionVerify the component works at narrow (~320px), medium (~768px), and wide (~1280px) container widths. Typography scales automatically via clamp() tokens (requires a @container ancestor). Check: layout collapses/stacks logically, text doesn't overflow, interactive targets stay tappable (≥44px), images/media scale without breaking, spacing tightens proportionally. If layout doesn't adapt, add the missing @sm:/@md:/@lg: container query variants.
Tell the user: "Running a11y tests next." Then run pnpm test:a11y immediately — this is verification, not a project change. Don't ask permission to run it (dev server must be running). Read the output and interpret every finding for the user:
Present findings and proposed fixes. Wait for approval before applying fixes only.
Tell the user: "Running design system audit next." Then run /angora-design-system-audit immediately on the new component — same as above, verification not a change. The audit skips contrast and ARIA labeling (already covered by the a11y test) and focuses on design rules, token compliance (including semantic token enforcement — no raw palette classes), and responsive behavior. Fix any issues it finds — no confirmation needed for audit-driven fixes.
Show the user what you've built. Reference the design system page URL (e.g., /design-system/buttons) so they can check it — don't tell them to start the dev server.
User reviews in browser. Approves or iterates.
Only if you made a new decision worth recording (added to anti-patterns or decisions log). Most components won't need an update.
Suggested component order: Typography specimens, Navigation, Hero sections, Feature grids, Pricing tables, Testimonials, Logo clouds, Accordion, CTA sections, Footer. (Buttons, icons, cards, grid, Section, and forms are already built during init.)
Every component's root element gets a data-component attribute matching the component name (PascalCase). This makes components instantly identifiable in browser dev tools, where Tailwind class soup otherwise gives no hint which component rendered an element.
<button data-component="Button" class="inline-flex items-center ...">
<div data-component="Card" class="rounded-lg overflow-hidden ...">
<!-- Landmark components add flow attributes -->
<section data-component="Hero" data-seamless data-full-width class="relative min-h-[40.625rem] ...">
<footer data-component="Footer" data-seamless class="...">
<!-- Section uses its props to set flow attributes -->
<section data-component="Section" data-seamless data-full-width class="...">Sub-components get it too: data-component="CardBody", data-component="HeroTitle". The DOM reads like a component tree. The data-component attribute is always first in the attribute list for consistency.
Rules:
sm, md, lg) for scales, standard variant names from major design systems for styles.class prop for additive styling (positioning, extra margin, layout context). Use class:list for merging.{...props} spread.All components use semantic HTML elements styled with Tailwind utility classes. No custom elements, no Shadow DOM.
Landmark component example (renders own element with flow attributes):
---
const { class: className, ...props } = Astro.props;
---
<section
data-component="Hero"
data-seamless
data-full-width
class:list={["relative min-h-[40.625rem]", className]}
{...props}
>
<slot />
</section>Content component example (self-contained with `@container`):
---
const { class: className, ...props } = Astro.props;
---
<div data-component="MyComponent" class:list={["@container", className]} {...props}>
<div class="grid grid-cols-2 @sm:grid-cols-3 @md:grid-cols-4 grid-gap">
<slot />
</div>
</div>Rules:
h1-h6, p, a, img, ul, figure, blockquote, section, nav, footer)flex, grid, max-w-*, p-*, gap-*)prose utility class for sections with flowing editorial text (paragraphs, lists, blockquotes). Components like cards and heroes should NOT use prose — they own their spacing explicitly via gap-* classesprose is applied at the <main> level on layout pages as an ambient typography baseline. Its descendant selectors use :where() for low specificity, so any Tailwind utility class on a component naturally overrides it. Prose targets semantic elements (<h2>, <p>, <a>, <ul>, <ol>, <blockquote>) — components that render these need explicit classes to override prose when the default treatment isn't wanted:data-component (prose targets blockquote:not([data-component])). If a component uses a raw <blockquote> without data-component, add explicit counter-declarations (border-l-0 pl-0 not-italic)list-style-type: disc and padding-left: 1.5em to <ul>/<ol>. Components that render UI lists (navigation, tags, non-content lists) must add list-none p-0 to override. Content lists that should have bullets don't need anything — prose handles themp-8). If no token fits, use rem for sizing and em for prose-relative spacing. Never hard-code arbitrary pixel values in components<section>, <footer>, <nav>) with data-component and flow attributes. They don't use Section — Section is for content sections controlled by consumers@container wrapper — don't depend on Section for container queriesChange the component, don't override from outside. When a component's default appearance needs to change, update the component file itself. Never override baked-in Tailwind classes from the consumer side via the class prop — Tailwind resolves same-specificity utilities by CSS source order, not HTML class attribute order, so bg-highlight passed via class won't reliably beat a component's built-in bg-muted. The class prop is for additive styling (positioning, extra margin, layout context) — not for overriding the component's own visual treatment. If you find yourself reaching for !important (!bg-*), that's a signal you're fighting the component instead of updating it.
Icons: always import from `src/icons/`, never from the icon library directly. Each icon in src/icons/ is a thin Astro wrapper around the underlying library. This keeps the library swappable. import ArrowRight from '@/icons/ArrowRight.astro' — never import from the library package. If a needed icon doesn't exist in src/icons/, read an existing wrapper to learn the pattern and create one. When you add a new icon wrapper, also add it to the icons design system page (src/pages/design-system/icons.astro) so the gallery stays complete.
Images: always use `<img>`, never CSS background images. Use <img> with object-fit: cover (object-cover in Tailwind) for all imagery including hero backgrounds, card covers, and full-bleed sections. Position with Tailwind classes (absolute inset-0) inside a relatively-positioned container when used as a backdrop.
Composites that render as page sections need accessible labels so screen readers can identify them. Primitives generally don't — native semantics are enough.
aria-labelledby pointing to the section's heading, or aria-label if there's no visible heading. Landmark components wire aria-labelledby internally since they control their own heading markup. Section's title prop handles aria-labelledby automatically. Pass aria-label on Section for sections without a visible heading.aria-label (e.g., aria-label="Main", aria-label="Footer"). Critical when multiple navs exist on a pagearia-label describing the group<!-- Section with title handles aria-labelledby automatically -->
<Section title="Pricing">
...
</Section>
<!-- Section without visible heading -->
<Section aria-label="Call to action" seamless>
...
</Section>
<!-- Nav with label -->
<nav aria-label="Main">...</nav>
<!-- Control group -->
<div role="group" aria-label="Plan selection">
<Card>...</Card>
<Card>...</Card>
</div>Accept aria-label as a prop on composites so consumers can override the default label in context.
All responsive behavior uses container queries so components adapt to their container, not the viewport. No viewport-based responsive variants (sm:, md:, lg:) in component markup — only container query variants.
Tailwind v4 container query support:
@container utility sets container-type: inline-size on the parent@sm:, @md:, @lg:, @xl: variants trigger at container breakpoints (640/768/1024/1280px)@container/card → @sm/card: for targeted queries<!-- Content components provide their own @container wrapper -->
<div data-component="FeatureGrid" class="@container">
<div class="grid grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-3 grid-gap">
<!-- cards -->
</div>
</div>
<!-- Section provides @container on its root <section> element — content
components don't depend on it, but it's there for inline content -->
<Section title="Features">
<FeatureGrid>...</FeatureGrid>
</Section>The only @media queries allowed are in design-system.css (tooling, not a deliverable).
Components are interactive by default — they include pseudo-class variants (hover:, active:, focus-visible:) and transitions. No frozen "specimen mode" — this is the advantage of HTML over Figma.
Form controls with native inputs (Checkbox, Radio, Toggle) use hidden <input> elements with sr-only peer and Tailwind's peer-checked: variants. Interactivity, keyboard support, and accessibility come from the native element — no state prop needed. Use boolean props: checked, disabled. Add name/value for form submission and radio grouping.
Form controls without native checked state (TextInput, Textarea, Select) use a state prop for states that can't be triggered by design system interaction:
<!-- Error state — can't be triggered by clicking -->
<TextInput state="error" label="Username" value="ab" hint="Must be at least 3 characters" />
<!-- Renders to: -->
<input class="... border-destructive" data-state="error" />Valid state values: error, success, disabled, dragover (file upload), has-value (search).
Two primitives handle form layout spacing, both using grid-gap (var(--grid-gap)):
flex flex-wrap). Children grow to fill space by default (grow prop, defaults true). Set grow={false} for rows where children should stay at natural width (e.g., buttons). align prop controls vertical alignment (start | center | end).flex flex-col). Wraps multiple FormRows or fields with standard vertical spacing.<FieldGroup>
<FormRow>
<TextInput label="First name" />
<TextInput label="Last name" />
</FormRow>
<FormRow>
<TextInput label="Email" />
</FormRow>
<FormRow grow={false}>
<Button>Submit</Button>
</FormRow>
</FieldGroup>Primitive vs composite: Primitives (button, badge, input) show all their own variants. Composites (hero, pricing, nav) show their own variants but render child primitives in default state only.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.