Vitral 0.2
Reference

Contributing

What a component is made of, and where each piece lives.

A component is five pieces, each in its own place. None of them is optional, and the order is the order they are written in.

where things go
packages/themes/src/presets/base/components/toggleswitch.ts   # tokens
packages/styles/src/toggleswitch/{toggleswitch.css,index.ts}   # CSS + class map
packages/vue/src/components/ToggleSwitch/
    types.ts            # the props interface, extending BaseProps
    ToggleSwitch.vue    # the component
    ToggleSwitch.spec.ts# axe + keyboard + ARIA
    index.ts
apps/docs/src/demos/ToggleSwitch.vue                           # the demo page

1. Tokens

A token tree whose values refer to the semantic layer ('{formField.borderColor}', '{control.minHeight}') rather than to a colour. Anything that differs between schemes goes under colorScheme. Every token becomes a --vt-toggleswitch-… variable.

2. Style

  • Classes are vt-toggleswitch, vt-toggleswitch-<part>, vt-toggleswitch-<modifier>.
  • CSS reads only var(--vt-…). A hard-coded colour, radius or duration is a bug.
  • Build on the shared chrome (.vt-field, .vt-overlay, .vt-option, .vt-mask) rather than restyling it.
  • Keyboard focus shows the focus ring; anything that animates stops under prefers-reduced-motion.

3. The component

ToggleSwitch.vue
<script setup lang="ts">
defineOptions({ name: 'VtToggleSwitch' });

const props = withDefaults(defineProps<ToggleSwitchProps>(), {
    unstyled: undefined   // required: an absent boolean prop would be false,
});                       // and false would override the global setting

const { part } = useComponent(toggleswitchStyle, props);
</script>

<template>
    <!-- one call per element: theme classes, pass-through and state -->
    <div v-bind="part('root', state)">
        <span v-bind="part('thumb', state)" />
    </div>
</template>

v-bind="part(…)" on every element is what makes a component themeable, reachable by pass-through and usable unstyled. A component wrapping a native control also uses useSplitAttrs() with inheritAttrs: false, so class dresses the wrapper while name, aria-describedby and the rest reach the control.

4. The demo

A page in apps/docs/src/demos/ exporting meta: DemoMeta from a plain <script> block. This site finds it by glob, and reads the same file again as text for the code beside each example, so a snippet cannot drift from what it documents.

5. Barrels

pnpm gen writes every index.ts list. Nobody edits them by hand.

Conventions worth knowing

  • v-model through defineModel(); ids from Vue's useId().
  • Text comes from the locale, never from a literal in the template.
  • Popups use <Teleport to="body">, the vt-overlay transition and useOverlay(); modals add useFocusTrap() and lockScroll().
  • Behaviour with no DOM in it (navigation, parsing, selection) goes to @vitral/core, with a test there.
  • Reach for an option before a new component: two things that differ only in what they refuse are one thing with a prop.

Checks

terminal
pnpm test                          # vitest + axe
pnpm typecheck                     # vue-tsc over every package and the site
node scripts/gen-index.mjs --check # barrels up to date
pnpm build                         # every package builds, with declarations