本文へ移動
cccskills
無料GitHub で公開

design-language

Use when building, modifying, or fixing any frontend UI in the Asyar launcher — new components, new views, layout changes, styling decisions, new built-in features, visual bug fixes, and anything in settings or onboarding. Answers "what do I use here?" for fonts, colour, typography, spacing, motion, components, and layout.

日本語の概要は準備中です。原文の説明を表示しています。

Xoshbin/asyarスター 946(GitHub 946 + cccskills 0)

実行スコア 未検証

インストール方法を見る

含まれるファイル(1)

  • SKILL.md43.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Asyar Design Language — Measure

This file answers one question: given what you are building, what do you use?

Every table below is a lookup. Find your row, use what it says. If your case is not in a table, the answer is at the bottom under When nothing fits — it is never "invent something".

The reasoning behind all of this lives in docs/reference/design-system/design-language.md. Read that once. Work from this.

pnpm check:design enforces the mechanical half and runs in CI. If it passes, you have not broken the system; it does not mean your UI is good. The judgement calls are the tables.


The thesis, in one paragraph

Every value in Asyar is measured, not chosen.

Asyar is 750 × 480, of which 384px is content, and it is on screen for two to six seconds. Nothing in it has room to be arbitrary. A value is either derived from a constraint — a window dimension, a contrast ratio, a scale step, a count of rows that must fit — or it is a guess, and this is a system that does not ship guesses. Four principles, in priority order, and every table below is one of them made operational:

  1. Derived, Not Chosen — every value traces to a measurement or a scale step. "It looked right" is not a derivation.
  2. Subtract First — borders, fills, shadows and animations start at zero and have to be argued in. Remove it and look; if nothing is lost, it was decoration.
  3. One Candidate — exactly one element is the thing ⏎ will act on, and nothing else may compete for the eye.
  4. Every Affordance Names Its Key — if it can be done, its key is visible.

When two rules below seem to conflict, the principle wins, and the lower-numbered principle wins over the higher.

Restraint is the position here, not a fallback applied when nothing better comes to mind. If you are about to make something look more designed, that is the moment Principle II applies, not an exception to it.


The 30-second version

You needYou use
Any UI elementA component from src/components/index.ts — read the barrel first
Any colourA var(--…) token. Never a hex, rgb(), white, or a Tailwind colour
Any accent as text/iconvar(--accent-*) — the voice ramp
Any accent as a fillvar(--accent-*-fill) — the ground ramp. See §2
Any text size or weightA .text-* class, or var(--font-size-*)
Any letter-spacingvar(--tracking-*) — never a raw em
Any padding/margin/gapvar(--space-*) — the gap between things
Any width/height/icon sizevar(--size-*) — the thing itself. See §5
A shell metric--shell-header-h · --shell-footer-h · --shell-row-h
Any cornervar(--radius-*)
Any durationvar(--dur-*)
Any easingvar(--ease-*)
Any stackingvar(--z-*)
A selected rowLet .list-row.selected / .selected-result do it — do not restyle it
A scrolling containerAdd .custom-scrollbar
Something that does not existBuild a reusable component in src/components/, export it from the barrel

Never write a var(--token, fallback). Every token is always defined, so a fallback can only ever hide a typo — that is exactly how a whole subsystem in this app once ended up running on invented token names nobody noticed.


1. Fonts — which font, where

Two faces. There is no third.

WhereTokenRenders as
All UI text — labels, titles, body, buttons, menus, settings, onboardingvar(--font-ui)Satoshi
Code, JSON, file paths, IDs, hashes, shortcuts, timestamps, anything column-alignedvar(--font-mono)JetBrains Mono
  • --font-ui is applied globally to *. You only name it explicitly inside a component that also sets font-family for another reason.
  • Satoshi renders on every platform, macOS included. There is deliberately no per-platform override. Asyar used to fall back to San Francisco on macOS; that gave the app no typographic identity on its primary platform. Do not reintroduce it.
  • Reach for mono when characters need to line up or be read individually. A duration, a count, or a version number in prose stays in --font-ui; the same value in a table column goes mono. .text-mono already carries tabular-nums.

Tracking

TokenWhere
--tracking-displayPage titles — the largest type in the app
--tracking-tightThe command line, section headings
--tracking-normalAll body, label and caption text
--tracking-wideUppercase group labels only

Never write a raw letter-spacing: -0.02em. The .text-* classes already carry the right value, which is the main reason to use them.


2. Colour

The one rule people get wrong

A colour that fills cannot also be the colour that speaks.

There are two accent ramps, and picking the wrong one is a contrast bug that looks fine to you and is unreadable to someone else.

RampTokenUse for
The voice--accent-*Text, icons, strokes, focus rings, borders, and the source for a tint
The ground--accent-*-fillThe background of a filled surface that carries --text-on-accent

The test is mechanical: does this surface have --text-on-accent (or --control-knob) on top of it? Yes → -fill. No → the voice.

/* A filled button, a checked checkbox, a user chat bubble, a coloured tile */
background: var(--accent-primary-fill);
color: var(--text-on-accent);

/* A status dot, a meter bar, a progress fill, a left accent seam — no text */
background: var(--accent-primary);

/* An accent-coloured label, a link, an icon */
color: var(--accent-primary);

check:design fails the build on a voice token used as a background in the same rule as --text-on-accent (voice-as-fill). It cannot catch the inverse, so: never use a -fill token as a text colour — it is too dark to read on an Asyar surface.

Surfaces, from back to front

TokenUse for
--bg-primaryThe window background. You rarely set this yourself
--bg-secondaryCards, sidebars, panels — anything sitting on the window
--bg-tertiaryInputs, wells, and subtle insets inside a card
--bg-hoverHover on any interactive row, button, or tile
--bg-selectedThe selected row — the only surface token carrying chroma
--bg-popupOpaque popups and menus
--surface-canvasOnly where external HTML assumes a white page (URL previews)

Never set an opaque background on the root container — platform overrides (macOS transparency, Windows Acrylic, opaque Linux) handle that automatically. Elevation is a scalar, not a stack: a popup opened from a card is still one popup, not a card plus a popup. If you want a fourth level, what you actually want is --scrim to push the thing beneath it back.

Text

TokenUse for
--text-primaryHeadings, labels, the content the user came for
--text-secondarySubtitles, metadata, supporting detail
--text-tertiaryPlaceholders, hints, disabled text, timestamps
--text-on-accentText or icons on any --accent-*-fill surface

Three tiers. If you want a fourth, you want a different layout.

Colour is never the only channel

Every state must be legible with all hue removed. Simulated for deuteranopia (~6% of men) the three state colours collapse toward the same olive — success against danger sits at a luminance ratio of 1.23. No contrast tuning fixes that, because the problem is hue discrimination.

So a state colour is an accelerant on top of a shape, an icon, or a word, never the sole carrier. A status indicator needs a label or an adjacent word; a severity needs an icon with a different silhouette. Badge is fine — it carries text. A bare coloured dot is not.

Review test: screenshot it, desaturate it, and see if you can still read the state.

Semantic state

TokenMeans
--accent-primaryPrimary action, focus, selection, links
--accent-successSucceeded, healthy, connected
--accent-warningNeeds attention, degraded, in progress
--accent-dangerDestructive action, error, failed
--asyar-brandAsyar's identity. The same blue as the accent — one identity colour, not two

For a tinted background in a state colour, mix it rather than inventing a second token — this is the house formula, and it uses the voice ramp for both the tint and the text so they always agree:

background: color-mix(in srgb, var(--accent-danger) 12%, transparent);
color: var(--accent-danger);

Lines and edges

TokenUse for
--border-colorThe border of an interactive element
--separatorList dividers, section rules, subtle outlines
--divider-softThe faintest hairline (split-view handle)
--rim-lightEdge highlight, inside a shadow or gradient — see §4
--rim-shadeEdge shade, same

Prefer alignment to a border. Two things sharing a left edge already read as one column; a border on top of that is a line the layout has already drawn (Principle II). The launcher shell follows this: there is no divider under the search header and none above the bottom bar. The glass surface flows from one to the next, and the spacing does the separating. Do not reintroduce a hairline there to "tidy" a view. Use a real border when the job is "this is an interactive control with an outline".

Code colour

Only for rendered code and structured data. The Prism markdown theme and JsonTree both read from these, so a snippet looks the same everywhere.

--syntax-comment · --syntax-keyword · --syntax-string · --syntax-number · --syntax-function

Everything else

--scrim / --scrim-opaque for modal backdrops · --control-knob for the Toggle knob · --shadow-color inside shadows only · --kbd-rim and --kbd-border for key chips (the rim is the inset glow, the border is the outline).


3. Typography — which class, where

Use the class. Only drop to raw var(--font-size-*) inside a component that needs a size the classes do not cover.

Where you areClass
The title of a settings page or a full-screen view.text-page-title
A section heading inside a page.text-section
An uppercase group label above a list.section-header
A group label in the root launcher list.section-header.root-section
The name of a list item, card, or result.text-title
A paragraph of running text.text-body
The label on a form field or setting.text-label
The second line under a list-item title.text-subtitle
A hint, a timestamp, a caption.text-caption
Code, a path, an ID.text-mono

Weight rules: 500 (font-medium) is the heaviest weight for body and label text. 600 (font-semibold) is for headings and the command line. 700 does not appear in the product — at display sizes with display tracking, 600 is already maximally emphatic and 700 reads as shouting. Never font-bold.

The command line is the largest text in the app and nothing may match it. If you are adding something at --font-size-2xl or above outside SearchHeader, you are competing with the query. Don't.


4. Elevation

Every drop shadow casts straight down. Every shadow is 0 Ypx.

Height comes from blur radius and negative spread, never from sliding the shadow sideways. This is consistency, not physics: shadows that disagree about their direction read as a rendering mistake, and the scale is easier to reason about when height is the only variable. It binds drop shadows — an inset box-shadow used to draw an edge is a drawing primitive, not a cast.

TokenFor
--shadow-xs … -xlLift, in five steps
--shadow-popupMenus
--shadow-launcher-popupThe launcher's floating surfaces
--shadow-focusFocus rings (applied globally already)

--rim-light / --rim-shade are ingredients inside --shadow-launcher-popup and the filled-button gradients. There are no .rim / .rim-all utility classes — they existed, had zero call sites, and were removed.

Radius — --radius-xs (4) · sm (6) · md (8) · lg (10) · xl (12) · popup (20) · full. Small controls take sm, cards take md, modals take xl, floating launcher surfaces take popup.

Stacking — never a bare number:

TokenValueFor
--z-base1Stacking inside one card or row
--z-raised10An icon or handle over its own container
--z-footer40The launcher bottom bars
--z-dropdown50A list anchored to a field or row
--z-floating60Toasts, search accessories — above dropdowns
--z-header100The launcher search header
--z-overlay200Prompts, banners, the extension inspector
--z-portal9999A fixed popup escaping an overflow: hidden parent

Modal uses native <dialog>, which renders in the browser's top layer and is above all of these regardless of z-index. You never need to out-number a modal.


5. Space and size — which measurement, where

Space is the gap between objects. Size is the object. Two scales, and reaching into the wrong one is how --space-* ended up with values like 11px and 23px — those were an icon tile and a spinner, not rhythm.

You are settingYou use
padding, margin, gap, insetvar(--space-*)
width, height, an icon, a tile, an avatarvar(--size-*)

Space — a 2px grid

var(--space-N), from --space-0-5 (2px) to --space-11 (48px). Whole steps only:

2 · 4 · 6 · 8 · 10 · 12 · 16 · 20 · 24 · 32 · 40 · 48

Four tokens are deprecated. Do not add new uses of them — they are the only off-grid values in the system, and they exist because sizes had no scale:

DeprecatedValueUse instead
--space-1-55px--space-1 or --space-2
--space-2-511px--space-4 or --space-5
--space-5-513px--space-5 or --space-6
--space-7-523px--space-8, or --size-lg if it is an object

Size — a 4px grid

TokenValueCanonical use
--size-xs12pxA glyph inside a dense chip or badge
--size-sm16pxThe default inline icon, beside body text
--size-md20pxA key chip, a small pill, a status pip
--size-lg24pxThe icon tile in a result row
--size-xl32pxAn extension avatar, a settings row icon
--size-2xl48pxAn empty-state or onboarding glyph
--size-3xl64pxThe largest object in the product

The names are t-shirt, not role, so pick by value and use the canonical column as a guide. --size-lg on a spinner is fine; it is 24px, not "a tile".

Launcher-density metrics

The launcher's rows are denser than the 4px size scale allows, so three metrics sit deliberately off it. Each carries a design-ok comment at its definition; do not spread them beyond the places listed.

MetricValueWhere
Row icon22pxThe icon tile / image in LauncherListRow
Key cap18pxKeyboardHint — height and minimum width
Alias chip18pxThe f / cl pill after a row title

The row icon column is also the alignment column: the search header's leading slot is the same 22px wide, so the query text starts on the same x as every row title below it.

The visible-row fit — do not break this by accident

The launcher's content area is 384px and the list insets it by --space-3 top and bottom, leaving 368px. Rows are var(--shell-row-h) (38px) and touch (no gap), so nine full rows take 342px and the tenth row peeks in below them. That peek is intentional: it tells the user the list scrolls.

  • The row height is var(--shell-row-h). Changing it changes how much of Asyar is visible at once, and where the tenth row is cut.
  • Rows have no separation. Spacing between groups comes from the section header's own top padding (--space-4), not from a gap between rows.
  • Never hardcode 56px, 40px, 38px or 384px. Use --shell-header-h, --shell-footer-h, --shell-row-h, or calc() off them.

Density — how fast is this read?

The tokens are identical across all five surfaces; the rhythm is not.

SurfaceRhythmRead at
Launcher--space-2 / --space-3 inside a rowA glance. Density is the feature
Settings--space-5 / --space-6 between rows and sectionsDeliberately, with a pointer
Onboarding--space-7+ between blocksOnce, slowly. One idea per stage

If you are unsure how much space to use, the question is not "what looks right" but "how fast is this read?"

Pointer targets: the launcher's 38px row is below the 44px guideline, which is an accepted trade for showing nine results and a peek of the tenth on a keyboard-first surface. Nothing that is not keyboard-first gets that excuse — Settings and onboarding use the roomier density above.


6. Motion

Four durations, four curves. Every animation is one of each. There is no fifth of either — adding one is a change to the design language, not to your component.

DurationUse for
--dur-instantColour/opacity landing under the pointer
--dur-quickA state change the user caused directly
--dur-travelA panel sliding
--dur-emergeA sheet opening
CurveUse for
--ease-travelThe default. Almost everything.
--ease-emergeSomething arriving
--ease-recedeSomething leaving
--ease-settleScale only. The only curve allowed to overshoot.

Rules that are not negotiable:

  • Arrival and departure use different curves. One curve for both is the fastest way to make an interface feel cheap.
  • --ease-settle may only be applied to scale. Overshoot on position reads as sloppy; on opacity it is incoherent.
  • Before adding any animation, ask: if this were removed, would the user be confused about what happened? If no, don't add it.
  • Reduced motion is handled globally in style.css. You do not need a prefers-reduced-motion block unless your component animates via JS.

Two animations are specifically not wanted. Both are the obvious thing to reach for, and both fail the test above:

  • The selection travelling between rows. Selection snaps. The user pressed ↓; nobody is confused about where the highlight went.
  • The launcher animating on summon or dismiss. The window appears. An arrival animation delays the first usable moment on the one surface whose whole promise is being already there.

--transition-fast/normal/smooth/slow still exist and still work; they are shorthands over the tokens above. New code should pair a duration with a curve directly.


7. Voice — how it reads

Asyar is mostly words, and it writes the way an instrument reports: flatly, in the user's terms, without personality.

RuleDoNot
Sentence case, always"Copy to clipboard""Copy To Clipboard"
A control names its result"Publish" → toast "Published""OK" · "Submit"
Name what the user recognises"Notifications""Webhook config"
Errors say what broke and what next"Extension failed to load — check the manifest""An error occurred"
No apology, no exclamation"Nothing matched 'xyz'""Sorry! Nothing found!"
Second person, active"Choose a shortcut""A shortcut should be chosen"
  • A result is named, never described. Title = what it is called. Subtitle = where it lives or what it does. Neither is a sentence.
  • Truncate at the end, except file paths, where the middle goes.
  • Fixed terms, not synonyms: a command is declared by an extension; an action is what you can do to a selected result; a result is a row; an extension ships them.

8. Components — which component, where

Read src/components/index.ts before deciding anything does not exist. It exports every component in the app except ConfirmDialog and the extension inspector's internals, both documented at the bottom of that file. The checker fails the build if a component is missing from it, so the barrel is trustworthy — treat it as the catalogue.

By what you are building

You are buildingUse
A buttonButton — never a styled <button>
An icon-only buttonIconButton
A button in the launcher's bottom barBottomBarButton
A text fieldInput · multi-line: Textarea
A dropdown of fixed optionsSelect
An on/off settingToggle · in a list of choices: Checkbox
Two-to-four exclusive options, inlineSegmentedControl
Top-level navigation between viewsTabGroup (pills or sidebar)
A keyboard shortcut, displayedKeyboardHint
A keyboard shortcut, being recordedShortcutRecorder
A status word (ready, failed)Badge — variants default/success/warning/danger/info
A status dotStatusDot
A progress or capacity barMeterBar
A single headline numberStatTile
A busy indicatorSpinner (inline beside text, md standalone)
An iconIcon · in a sized container: IconBox
An extension's avatarExtensionAvatar
You are buildingUse
Any dialogModal
A yes/no confirmationQueue it through DialogHost
A destructive confirmationSame, with the danger variant
A row in a listListItem
A long, scrolling result listResultsList (virtualised)
A list with group headersSectionedResultsList
A resizable two-pane layoutSplitView
A master/detail page with its own empty stateSplitListDetail
A cardCard
A page header with a back buttonAppBar
Key-value metadata inspector in a detail paneMetadataList
A ⌘K-style action menuActionListPopup
You are buildingUse
"There is nothing here"EmptyState
…inside a panel that is part of a fuller viewEmptyState with compact
…that doubles as "add the first one"EmptyState with compact bordered + a Button
"This is loading"LoadingState
"This failed"ErrorState · inside a form: InlineError
A non-blocking cautionWarningBanner
A transient confirmationPublish via feedbackService — ToastHost is already mounted and must not be imported
You are buildingUse
A settings pageSettingsForm + SettingsSection
One setting with a label and a controlSettingsRow · in a form: SettingsFormRow
A set of radio choices in settingsSettingsRadioGroup
A numeric slider in settingsSettingsRangeSlider
A labelled form field with hint and errorFormField
An onboarding stepOnboardingStage + GuidanceStep
Onboarding progressStepProgress
A "try it now" box in onboardingTestBox

If two components look like they both fit, pick the more specific one.


9. Layout

The launcher shell

┌───────────────────────────────────┐
│  SearchHeader   .search-header    │  fixed, --shell-header-h, --z-header
├───────────────────────────────────┤
│                                   │
│  .shell-content custom-scrollbar  │  fixed between the two bars, scrolls
│                                   │
├───────────────────────────────────┤
│  BottomActionBar                  │  fixed, --shell-footer-h, --z-footer
└───────────────────────────────────┘
  • The header and footer are already mounted by routes/+page.svelte. A view renders into the content area only.
  • The diagram's ├───┤ rules mark where the bars end, not lines that are drawn: neither bar has a border (see Lines and edges).
  • Never add a second fixed header or footer inside a view.
  • Never hardcode 56px or 40px. Use --shell-header-h / --shell-footer-h, or calc() off them, so the five places that depend on those heights stay in agreement.
  • For a master/detail view inside the content area, use SplitListDetail (or SplitView for a plain resizable split) — not a hand-rolled flex layout.

Launcher row anatomy

Left to right, a root-list row is:

SlotWhat it holds
Icon (22px)The command's icon; also the alignment column for the query
Title.result-title, weight 500
Muted textThe row's subtitle, or when there is none its sourceLabel
Alias chipFilled pill, --bg-hover, never outlined
HotkeyKeyboardHint, inline after the title
Right edgekindLabel — what the row is ("Command", "Application")

typeLabel is not shown. It is the internal grouping label the Scripts and Agents sections route on. Display text comes from kindLabel (the right edge) and sourceLabel (after the title), both set in resolveItemMeta. A built-in's source is "Asyar"; an extension's is its manifest name. The kind labels are translated (search.kind_command, search.kind_application).

Key caps (KeyboardHint) are outlined: a --kbd-border hairline and no fill, so they read the same on glass, on hover and on the selected row. Section headers in the root list are sentence case, 12px, weight 500, --text-secondary, aligned to the icon edge.

Scrolling

Every scrolling container gets .custom-scrollbar. No exceptions — the checker enforces it, because the platform default scrollbar against Asyar's surfaces is immediately obvious.

The five surfaces

The tokens and components are identical across all three. What differs is density and how much chrome is acceptable.

SurfaceDensityNotes
LauncherTightest. --space-2/--space-3 inside rowsKeyboard-first: every action needs a key path and a KeyboardHint. Chrome stays invisible
SettingsRoomier. --space-5/--space-6 between rows and sectionsAlways SettingsSection → SettingsRow. Mouse and keyboard are equal citizens
OnboardingRoomiest. --space-7+ between blocksOne idea per stage. OnboardingStage owns the frame; a step supplies content, never its own layout
HUDA single line, no chromeRead peripherally, mid-task. It is a standalone webview with no theme injection — the one design-ok-file in the repo
StickyLauncher density, but persistentRead repeatedly over minutes, so it takes hover states the launcher does not need

10. Interaction and accessibility

Every interactive element covers every state:

StatePattern
Hoverbackground: var(--bg-hover)
Active / pressed.pressable, or background: var(--bg-selected)
Keyboard focusNothing — the global *:focus-visible ring handles it
Disabledopacity: 0.5; cursor: not-allowed
Selected in listLet .list-row.selected / .selected-result do it

Hover must never be mistakable for selection (Principle III). --bg-hover is neutral; --bg-selected carries chroma. That difference is load-bearing — do not "improve" hover by tinting it.

The selected row is a flat band, everywhere, and it is not yours to restyle. background-color: var(--bg-selected) plus box-shadow: inset 0 0 2px 0.5px var(--kbd-rim), identical in .list-row.selected and .selected-result. It never takes a border, a gradient, or an accent seam — this is the most-seen element in the app and the most tempting one to make expressive, and it is deliberately plain.

Focus is already solved globally. *:focus-visible applies var(--shadow-focus) app-wide. Two things break it, both worth knowing:

  1. A more specific rule that also sets box-shadow (e.g. .item.active) silently wins. If your component sets box-shadow in any state — and the Rim is a box-shadow — restate the ring in a :focus-visible rule after it.
  2. :focus-within does not match when the real input is a sibling rather than a descendant. Drive the ring off the input with :global(.peer:focus-visible) ~ .track.

Use :focus-visible, not :focus, for buttons and rows — :focus also fires on mouse click. Text fields are the exception: they legitimately use :focus, and the global rule already stands aside for input/textarea/select.

Other rules:

  • Anything clickable is a <button> or <a>. A <div onclick> needs a role, a tabindex, and a key handler — and is almost always the wrong answer.
  • svelte-ignore uses underscores in Svelte 5 (a11y_no_static_element_interactions). The Svelte 4 hyphenated names silently suppress nothing.
  • Decorative icons get aria-hidden="true"; meaningful ones get a label.
  • Never hijack text editing shortcuts: In views with a search bar or text input, never bind native text editing shortcuts (⌘⌫ / Cmd+Backspace, ⌥⌫, ⌘A, ⌘C, ⌘V, ⌘X, ⌘Z) to list actions or item deletion.
  • Destructive actions belong in ⌘K: Operations that delete, trash, or uninstall items belong in the ⌘K Action Panel (styled with destructive: true), not bound directly to ⌘⌫.

11. Third-party extensions (Tier 2, iframe sandbox)

Extensions run in two sandboxed iframes — a hidden worker and an on-demand view. Design rules apply to the view.

  • Every CSS token above is injected automatically, along with the real Satoshi and JetBrains Mono files. var(--bg-primary), var(--font-ui) etc. just work. The same "never hardcode a colour" rule applies, and so does the voice/fill split.
  • Icons come from the <asyar-icon> web component: call registerIconElement() once in the view entry, then <asyar-icon name="calculator" size="20">. Icons inherit currentColor.
  • There is no bare asyar-sdk entry point — import from asyar-sdk/view, asyar-sdk/worker, or asyar-sdk/contracts. The icon helpers are DOM-dependent and ship on /view only.
  • For IDE autocomplete outside the running app, import asyar-sdk/tokens.css.

A theme extension may override colour, shadow, radius, font family and duration tokens. It may not override --space-*, --font-size-*, --tracking-* or --ease-* — those are design-system-owned, and applyTheme filters them out. A theme recolours the app; it never resizes or re-times it.


12. When nothing fits

In order. Do not skip a step.

  1. Re-read the barrel. src/components/index.ts exports ~100 components. The thing you want usually exists under a name you did not guess.
  2. Check whether an existing component takes a prop for it. EmptyState grew compact and bordered this way; Badge already has five variants. Extending beats duplicating.
  3. Build a new reusable component in src/components/{category}/, style it with tokens, export it from the barrel, and use it. If it is worth building, it is worth making reusable.
  4. A token genuinely missing? Add it to :root in resources/styles/style.css with a comment saying what it is for, and give it a light and dark value if it is not constant. If it is a colour that belongs to a palette, add it to both copies of that palette — themePalettes.test.ts will fail if you add it to only one. Then add it to THEME_VAR_NAMES in lib/themeVariables.ts so extension iframes get it.

The escape hatch

Some values legitimately are not on a scale: a glyph sized to a fixed tile, a metric pinned to a native platform dimension, a standalone webview with no theme injection. For those, and only those:

/* design-ok: glyph scaled to the fixed 128px box, not UI text */
font-size: 64px;

A reason is required — a bare design-ok does not suppress. For a whole file (there is currently exactly one, the HUD window), use design-ok-file: <reason>.

If you find yourself writing the third design-ok for the same kind of value, that is the system telling you a token is missing. Add the token instead.


13. Before you call it done

Run pnpm check:design. It fails on:

RuleWhat it catches
undefined-tokenvar(--x) where --x is defined nowhere — the declaration is dead
token-fallbackvar(--x, something)
hardcoded-colorA hex, rgb(), white or black in a colour property
voice-as-fill--accent-* as a background under --text-on-accent
space-as-size--space-* on a width or height where a --size-* holds the same value
deprecated-tokenA new use of --space-1-5/-2-5/-5-5/-7-5
tailwind-palettebg-red-500 and friends
raw-pxPixels on font-size, padding, margin, gap, or border-radius
arbitrary-pxmin-h-[56px] and friends
bare-z-indexz-index: 50, z-40, z-[100]
missing-scrollbarA scrolling container with no .custom-scrollbar
unexported-componentA component missing from the barrel
stale-a11y-ignoreSvelte 4 hyphenated svelte-ignore names

Then check the things a script cannot:

  • Did you import components, or write markup? Zero imports from ../components in a new view is a red flag.
  • Does it look right in both light and dark? Toggle and look.
  • Desaturate the screenshot. Can you still read every state? Colour is never the only channel.
  • Can you name where every number came from? Pick the three least obvious values in your diff and say what derives them. If the answer is "it looked right", it is not finished (Principle I).
  • Delete something and look. Take out the border, the background, the icon, the animation — whichever is least load-bearing — and run it. If nothing was lost, leave it out (Principle II).
  • Is there exactly one candidate? Squint at it. If two elements compete for the eye, one of them is wrong.
  • Does anything cast a sideways drop shadow? Every cast is 0 Ypx.
  • Does every interactive element have hover, and does Tab show a focus ring?
  • Does every action show its key?
  • Is there an empty state, a loading state, and an error state?
  • Would this look at home next to the rest of the app, or does it look like a different program?

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

Use before planning or implementing ANY feature, bug fix, refactor, or API change that touches Asyar's architecture — extension system, service layer, IPC contracts, Tauri commands, manifest contributions, or cross-layer data flow. Triggers on new features, new extension APIs, new services, bug fixes involving multiple layers, refactors of infrastructure code, adding new contribution points, and any work that could affect how built-in features or third-party extensions interact with the platform. Also triggers on any task where you need to decide which architectural layer owns the logic, whether Tier 2 extensions can use a capability, or whether a change requires a manifest/permission update. Does NOT trigger for pure CSS/styling fixes (use design-language), isolated unit tests, documentation updates, version bumps, mechanical renames, or answering informational questions about the codebase.

日本語の概要は準備中です。原文の説明を表示しています。

Xoshbin/asyar9462026年10月27日 更新

Reference for the Asyar monorepo structure, SDK workspace linking, lockfile discipline, CI workflows, and release flow. Use this skill whenever working on anything related to pnpm workspace setup, the asyar-sdk dependency, lockfile errors, SDK or launcher version bumps, CI configuration, release workflow, or questions about how local dev differs from CI. Also triggers for "how does the workspace work", "how do I update the SDK version", "where do I run pnpm install", or any confusion about the monorepo layout.

日本語の概要は準備中です。原文の説明を表示しています。

Xoshbin/asyar9462026年10月27日 更新

Never hand-edit or hand-copy generated content. Use when touching permissions.rs, error.rs, AppError/SearchError variants, runtimes/catalog.fallback.json, any `#[derive(specta::Type)]` struct, any file with an AUTO-GENERATED banner (kinds.ts, gatedPermissions.ts, knownRuntimes.ts, bindings.ts, emoji data), or whenever you are about to write a TypeScript list/type that mirrors a Rust source.

日本語の概要は準備中です。原文の説明を表示しています。

Xoshbin/asyar9462026年10月27日 更新

Audit IPC message contracts between extensions and the Asyar host. Use when adding a new SDK service, adding a new proxy method, changing a postMessage type string, or reviewing permission gate coverage.

日本語の概要は準備中です。原文の説明を表示しています。

Xoshbin/asyar9462026年10月27日 更新

tdd

無料

Use when implementing any new feature or fixing any bug in this project — both Rust (src-tauri) and TypeScript/Svelte 5 (src). Triggers on phrases like "add X", "fix Y", "implement Z", "change how W works".

日本語の概要は準備中です。原文の説明を表示しています。

Xoshbin/asyar9462026年10月27日 更新

Xoshbin のスキルをすべて見る

このスキルの問題を報告する