Agent Skills › tabler/tabler › ui-component

ui-component

GitHub

指导构建和扩展共享 Astro UI 组件,涵盖 Props 设计、类组合及文档同步。用于新增组件、修改属性或重构现有组件。

.agents/skills/ui-component/SKILL.md tabler/tabler

Trigger Scenarios

创建新的共享 UI 组件 为现有组件添加新属性、变体或尺寸 重构共享组件结构

Install

npx skills add tabler/tabler --skill ui-component -g -y
More Options

Non-standard path

npx skills add https://github.com/tabler/tabler/tree/dev/.agents/skills/ui-component -g -y

Use without installing

npx skills use tabler/tabler@ui-component

指定 Agent (Claude Code)

npx skills add tabler/tabler --skill ui-component -a claude-code -g -y

安装 repo 全部 skill

npx skills add tabler/tabler --all -g -y

预览 repo 内 skill

npx skills add tabler/tabler --list

SKILL.md

Frontmatter
{
    "name": "ui-component",
    "description": "Build or extend a shared Astro component in `shared\/ui\/*.astro` (or `shared\/components\/**`) — the library that both the preview and the docs render. Use whenever the user asks for a new component, for a new prop, variant or size on an existing one, or for a refactor of a shared component. Covers the Props interface, the prop-name dictionary, class composition, slots, the `as` pitfall, and the follow-ups (demo page, docs, changeset) a new component needs."
}

Build a shared UI component

shared/ui/*.astro is the component library. @tabler/preview and @tabler/docs render the same files, so every change here lands on demo pages and in documentation examples at once. Components are markup: the design lives in core/scss/, and logic longer than a few lines lives in shared/lib/*.ts.

1. Before creating a file

  • Extend, do not fork. Read the Props interface of the closest existing component first. A new look is a prop on the existing component (variant, size, light), never a parallel ButtonAlt.astro.
  • Check the demo page before the component. If preview/pages/<name>.astro hand-writes variants the component cannot express, those are the missing props, and the component has drifted from the markup it is supposed to own. Close that gap instead of adding one more hand-written block.
  • Find the CSS first. A component wraps classes that exist in core/scss/ui/_*.scss. If the class is missing, writing it is part of the task — never invent class names in the component.
  • Where it goes: generic building blocks in shared/ui/; fixed demo content (a dashboard card with its own data) in shared/components/cards/; docs-only components in docs/components/.
  • Imports inside shared/ are relative (./Icon.astro, ../components/CaptureScript.astro). @shared / @ui / @data are for the site packages. Any bare npm import used in shared/ must be declared in shared/package.json.

2. Skeleton

---
import Icon from './Icon.astro'

/** core/scss/ui/_badges.scss `.badge-{size}` modifier classes */
type BadgeSize = 'sm' | 'lg'

interface Props {
  text?: string
  color?: string
  size?: BadgeSize | undefined
  /** -lt colour variant + text-{color}-lt-fg */
  light?: boolean
  class?: string
  icon?: string
  /** remaining attributes (id, data-*, aria-*, …) are forwarded to the root element */
  [key: string]: unknown
}

const { text, color, size, light, class: className, icon, ...rest } = Astro.props

const classes = ['badge', size && `badge-${size}`, color && `bg-${color}${light ? '-lt' : ''}`, className]
---

<span class:list={classes} {...rest}>{icon && <Icon name={icon} />}{text}</span>

Rules the file above follows, and every component should:

  • interface Props + destructuring with defaults. No untyped Astro.props.
  • The class prop keeps its HTML name and is destructured as class: className.
  • A union type for each modifier set ('sm' | 'lg'), with a doc comment naming the SCSS file the classes come from. Export the type (export type IconSize) when a sibling component reuses it.
  • [key: string]: unknown + ...rest spread on the root element, so callers can pass id, data-*, aria-* without a prop for each. Add it unless the component deliberately controls its own attributes.
  • One doc comment per non-obvious prop, saying which class or attribute it emits.

3. Prop-name dictionary

Names are shared across the library so the API stays predictable (and stays portable to React/Vue later). Reuse these before inventing a name:

Prop Meaning
class extra classes, merged last so callers can override
id element id; required for components that a script initialises
color palette colour (primary, azure, …), emitted as -{color}
size sm / md / lg (…), where md emits no class
variant style variant of the same component (outline, ghost, important)
text, title, description, label content passed as a prop instead of a slot
icon, iconEnd icon names, rendered through Icon.astro
href, external link target; external adds target="_blank" rel="noreferrer"
disabled, active, required, invalid state booleans, named after the HTML/CSS state
show… (showClose, showValue) opt-in parts of the markup
ariaLabel accessible name when the visible content is an icon or a colour

Props are camelCase. Booleans default to false — never showClose = true.

4. Class composition

  • Use class:list={[...]} with falsy entries for conditionals; it drops them and merges the caller's className for you. .filter(Boolean).join(' ') is only for a string you have to pass to a helper (iconSvg).
  • Put className last so the caller wins.
  • Do not emit a class for a default (size !== 'md' && \btn-${size}``).
  • No <style> blocks and no CSS imports in the frontmatter. Styling is core/scss/. A <style> makes Astro stamp data-astro-cid-* on every element the component renders, and its custom properties skip the --tblr- prefixing in build-css.ts.

5. Slots and containers

Container components (Card, CardBody, ButtonList, …) take a <slot /> rather than a content prop. Use Astro.slots.has('name') when the markup differs depending on whether a named slot was filled.

Polymorphic containers take an as prop — with one trap:

---
import type { HTMLTag } from 'astro/types'

// `as` must stay out of the Props body: Astro's frontmatter scanner bails on a member
// literally named `as` and silently falls back to untyped props.
type PolymorphicProps = { as?: HTMLTag }

interface Props extends PolymorphicProps {
  class?: string
}

const { as: Element = 'div', class: className }: Props = Astro.props
---

<Element class:list={['card', className]}><slot /></Element>

Heading components pick their own level (CardTitle renders h2, as="h3" for a nested section) so pages cannot skip levels.

6. Escape hatches and Astro traps

  • Content that may contain markup or entities (&hellip;) → set:html / <Fragment set:html={…} />. Entities in attribute strings must be passed as an expression (title={"…&hellip;"}), because JSX decodes them inside string literals.
  • Boolean attributes are inconsistent: selected={true} renders bare, but some (e.g. multiple) render ="true". Use multiple ? '' : undefined when a bare attribute is required, and check the rendered HTML.
  • Client-side behaviour (plugin init, event wiring) does not go in an ad-hoc <script> — see the astro-scripts skill.
  • Icons always through <Icon name="…" />, never inline SVG. An unknown name renders nothing and only warns in the build log ([svg] unknown Tabler icon), so the page looks fine with a blank button. Check the name in shared/data/icons.json before using it: it is player-play, not play.

7. Logic belongs in shared/lib

Anything beyond assembling classes and simple ternaries (formatting, data shaping, SVG handling, deterministic demo data) goes to shared/lib/*.ts and is imported by the component. Every module there has a sibling *.test.ts run by vitest (pnpm --filter @tabler/shared test) — add one for the function you introduce, and keep it pure so it is testable without a DOM.

Use pseudo-random.ts instead of Math.random() for demo data: pages are rebuilt constantly and random output would change the HTML on every build.

8. After the component exists

A new component is not done until it is visible and documented:

  1. Demo page — a page in preview/pages/ showing its variants (demo-pages skill).
  2. Docs page — docs/content/** (write-docs skill), plus the class table (class-reference skill) when the component introduces classes.
  3. Changeset — generate-changeset skill, minor for a new component, patch for a fix.

9. Checklist

  • Extends an existing component instead of duplicating one
  • Classes exist in core/scss/; no invented class names
  • interface Props typed, prop names taken from the dictionary in section 3
  • class: className merged last, ...rest forwarded
  • Accessible: semantic element, ariaLabel where the content is an icon
  • Logic in shared/lib with a test, not in the frontmatter
  • pnpm run format:prettier and pnpm run type-check clean at the repo root (full output, not a tail)
  • Demo page + docs page + changeset

Version History

  • 340f719 Current 2026-09-22 21:47

Same Skill Collection

.agents/skills/astro-dev/SKILL.md
.agents/skills/class-reference/SKILL.md
.agents/skills/core-js/SKILL.md
.agents/skills/core-scss/SKILL.md
.agents/skills/demo-pages/SKILL.md
.agents/skills/generate-changeset/SKILL.md
.agents/skills/mr-description/SKILL.md
.agents/skills/navigation/SKILL.md
.agents/skills/page-layouts/SKILL.md
.agents/skills/screenshots/SKILL.md
.agents/skills/shared-lib/SKILL.md
docs/lib/agent-skills/tabler/SKILL.md
.agents/skills/astro-scripts/SKILL.md
.agents/skills/backward-compat/SKILL.md
.agents/skills/bootstrap-component/SKILL.md
.agents/skills/build-pipeline/SKILL.md
.agents/skills/html-diff/SKILL.md
.agents/skills/release-notes/SKILL.md
.agents/skills/upgrade-guide/SKILL.md
.agents/skills/write-docs/SKILL.md

Metadata

Files
0
Version
6199b80
Hash
4efa88b3
Indexed
2026-09-22 21:47

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-29 08:07
浙ICP备14020137号-1