id: vela-ds-setup-guide
name: Vela Design System — Setup Guide
description: How to add the design system to an HTML page.

before_you_build_anything: |-
  Before writing custom CSS or markup for any visual primitive (avatar,
  badge, card, modal, table, link styling, KPI tile, vertical nav, etc.),
  fetch the component catalog and check whether the DS already ships it:

    https://ds.velaintelligence.com/ai/component-catalog.yaml

  Grep the catalog for the use-case. If a vi-* component matches, use it.
  Every consumer who skipped this step ended up reinventing a primitive
  (avatar circle, link reset, KPI card) that the DS already provides.

  Companion files:
    - https://ds.velaintelligence.com/ai/tokens.yaml  (all design tokens)
    - https://ds.velaintelligence.com/ai/icons.yaml  (icon roles, names and aliases)
    - https://ds.velaintelligence.com/ai/setup-guide.yaml  (this file)

html_boilerplate: |-
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
    <!-- Prevent flash of unstyled custom elements -->
    <style>:not(:defined) { visibility: hidden; }</style>
    <!-- Load design system (registers all components, injects global styles,
         loads Google Fonts, and applies the default theme automatically) -->
    <script type="module" src="https://ds.velaintelligence.com/vela-ds.js"></script>
  </head>
  <body>
    <!-- Your app markup here -->
  </body>
  </html>

what_the_bundle_does:
  - Registers all vi-* custom elements
  - Injects global styles (typography classes, resets, link styles, list styles)
  - Loads Google Fonts (Manrope, Inter, JetBrains Mono) automatically
  - Applies the default theme and design tokens to :root, in DARK mode
  - Exposes window.VelaDS with applyTheme, applyDensity, resolveTheme, etc.

critical_head_style: |-
  Every page MUST include this style tag in <head> before any body content:

    <style>:not(:defined) { visibility: hidden; }</style>

  This prevents a flash of unstyled content (FOUC) while the JS bundle loads.
  Custom elements are invisible until registered, then appear fully styled.
  Uses visibility:hidden (not display:none) so layout is reserved.

theme_and_mode:
  description: |-
    The bundle auto-applies the default theme in DARK mode. Dark is the
    brand's canonical mode; light is a sanctioned product-mode extension
    that a user opts into explicitly. To offer light, or to switch themes
    at runtime, use window.VelaDS.

    Two APIs on window.VelaDS:
      - applyTheme(theme, target, mode)  — full apply: rewrites all --vi-*
        vars, sets data-vi-theme + data-vi-mode, applies density. Use when
        switching theme OR on first paint.
      - applyMode(mode, target, themeName?)  — lighter: only flips the
        dark-mode override vars. Use for a pure light/dark toggle while
        keeping the current theme.

    Both write data-vi-mode="light"|"dark" on the target.

  do_not_follow_system_preference: |-
    Do NOT wire the mode to `prefers-color-scheme`.

    Switching between dark and light is a brand switch, not a system
    preference, so it must be an explicit in-product choice, persisted per
    user. A page that mirrors the OS flips a user's app to a different brand
    surface because they changed a setting somewhere else.

    Earlier versions of this guide led with a follow-the-system recipe. It
    was wrong, and consumers that copied it are off-brand. Use
    `manual_toggle_with_persistence` below instead.

  manual_toggle_with_persistence: |-
    // Admin-console "Theme" switch. Persist user choice across reloads.
    const ds = window.VelaDS;
    const STORAGE_KEY = 'vi-color-mode';

    function setMode(mode) {
      localStorage.setItem(STORAGE_KEY, mode);
      ds.applyMode(mode, document.documentElement);  // cheap path
    }

    // On boot: explicit user choice, else dark. No system preference in the
    // chain — see do_not_follow_system_preference above.
    const initial = localStorage.getItem(STORAGE_KEY) ?? 'dark';
    setMode(initial);

  zero_flash_on_boot: |-
    // The bundle already boots dark, so a default-dark app needs nothing
    // here. This matters for apps that let users choose light: without it,
    // a light-preferring user gets a dark first paint before their stored
    // choice is applied.
    //
    // Set data-vi-mode on <html> inline BEFORE the bundle <script>:
    //
    //   <script>
    //     (function () {
    //       var m = localStorage.getItem('vi-color-mode') || 'dark';
    //       document.documentElement.setAttribute('data-vi-mode', m);
    //     })();
    //   </script>
    //   <script type="module" src="https://ds.velaintelligence.com/vela-ds.js"></script>
    //
    // Then call ds.applyMode(m, document.documentElement) once the bundle is
    // available. The :not(:defined){visibility:hidden} rule already hides
    // unstyled custom elements during this window.

  native_form_controls: |-
    // Nothing to do. The bundle's global styles set `color-scheme: dark` on
    // :root and flip it to light under [data-vi-mode="light"], so native
    // form controls, scrollbars, and built-in widgets follow the mode
    // already. (Earlier versions required an app-side rule for this; if you
    // still carry one, it is now redundant and can be deleted.)

  theme_picker_caveat: |-
    Not every theme supports dark mode. A theme can declare
    `supportedModes: 'light'` (the built-in 'editorial' theme does this).
    resolveTheme / applyTheme will silently clamp the mode to 'light' for
    those themes regardless of what you pass.

    In an admin console with a theme picker AND a mode toggle, read the
    resolved theme's `supportedModes` and disable the dark toggle when it
    equals 'light':

      const theme = ds.resolveTheme(themeName, 'light');
      const supportsDark = theme.supportedModes !== 'light';
      // disable / hide the dark toggle when !supportsDark

    The mirror case has no flag and needs a label instead: the built-in
    'nocturne' theme changes DARK MODE ONLY — it moves the dark neutral family
    from steel to the cooler glacier ramp, and in light mode it resolves to
    exactly the default theme. Selecting it in light mode is not broken, but it
    looks like nothing happened, so say so in the picker.

    Build the picker from ds.listThemes() rather than a hard-coded list: it
    returns { name, label, supportedModes } for the default and every built-in,
    including the product themes 'brass' (Tax Intelligence), 'boardroom' (Deal
    Intelligence) and 'canopy' (Sustainability Compass). Each carries its own
    accent and a side panel painted in both modes, and survives applyMode with
    nothing to re-apply.

  authoring_rule: |-
    Anything in your own CSS must reference semantic tokens (var(--vi-sem-*))
    rather than literal colors — those are what get swapped on mode change.
    Hard-coded hex/rgb values will not flip with the theme.

    Common ones:
      --vi-sem-surface-canvas, --vi-sem-surface-default, --vi-sem-surface-muted
      --vi-sem-text-default, --vi-sem-text-muted, --vi-sem-text-subtle
      --vi-sem-border-default, --vi-sem-border-subtle, --vi-sem-border-strong
      --vi-sem-action-primary, --vi-sem-action-primary-hover

links_and_anchors:
  description: |-
    Bare <a> is NEUTRAL by default — it inherits the parent's color and has
    no underline. This makes "anchor as a clickable wrapper around DS
    components" (chips, cards, user-row links, brand wordmarks) work without
    any defensive CSS. The reset uses :where(a) so it has zero specificity:
    any class you add wins automatically, no !important needed.

  wrap_freely: |-
    Wrap any DS markup in <a href="..."> and it will route correctly,
    support middle-click / right-click "open in new tab", and inherit the
    surrounding visual styling unchanged:

      <a href="#/settings" aria-label="Account settings">
        <vi-avatar name="Robert Salesas" size="32"></vi-avatar>
        <span>Robert Salesas</span>
      </a>

    No CSS reset needed on the consumer side. Same for vi-card, vi-chip,
    deal rows, sidebar items wrapped in <a>, brand wordmarks, etc.

  prose_links: |-
    For body-copy / content links that SHOULD look like links (blue color,
    hover underline), opt in explicitly with the .vi-link or .vi-prose-link
    class:

      <p class="vi-body">
        Read the <a class="vi-prose-link" href="/docs">full documentation</a>
        for details.
      </p>

    Variants: .vi-link-secondary, .vi-link-info, .vi-link-success,
    .vi-link-warning, .vi-link-danger, .vi-link-accent, .vi-link-sm,
    .vi-link-lg, .vi-link-underline, .vi-link-disabled.

    There is one tone class per tone. .vi-link-accent is NOT redundant with
    .vi-link: .vi-link follows action-primary, which a theme may point away
    from its accent layer (the editorial theme points it at clay while its
    accent is gold), so the two can be different colours.

  caveat: |-
    Body-copy anchors WITHOUT .vi-link/.vi-prose-link will be visually
    indistinguishable from surrounding text. For accessibility, prose links
    inside content pages should always carry the class.

custom_icon_fonts:
  description: |-
    The built-in icon set is Lucide, and it is what every vi-icon draws by
    default — internal ones (nav toggles, combobox indicators, modal close)
    and consumer-created alike. A consumer already standardised on a different
    icon font can point the DS at it instead, per icon or globally.
  setup: |-
    1. Load your icon font's stylesheet in <head> (before the DS bundle).

    2. Either set it per icon:
       <vi-icon system="custom" icon-class="ri-home-line" size="16"></vi-icon>

       or make it the default for everything, including internal icons:
       window.VelaDS.configureIcons({
         defaultSystem: "custom",
         roles: { clear: { iconClass: "ri-close-line" } }
       });
  usage: |-
    <vi-icon name="house" size="16"></vi-icon>          <!-- built-in Lucide -->
    <vi-icon system="custom" icon-class="ri-home-line"></vi-icon>
  notes:
    - configureIcons is on window.VelaDS (standalone bundle) or a named export
    - system="custom" renders an <i class="..."> for icon-font libraries; icon-class wins, falling back to name
    - Roles (clear, search, nav toggles, ...) can be remapped individually, so a custom system does not have to be all-or-nothing
    - The built-in set accepts both its own names and Lucide's — house/home, gear/settings, xmark/x all resolve
    - Every role and every valid name is listed in ai/icons.yaml; a name outside it renders nothing and only warns in the console

app_shell_pattern:
  description: |-
    Use vi-page for full-page app layouts with a sidebar and content area.
    The nav column is flush (no gaps). The right column holds header, main,
    and footer. Banner and aside auto-hide when their slots are empty.
  example: |-
    <vi-page>
      <vi-alert slot="banner" tone="info">System notice</vi-alert>

      <vi-stack slot="navigation" gap="0" justify="space-between" style="background:var(--vi-sem-brand-primary-900);color:var(--vi-ref-steel-100);">
        <vi-vertical-nav>
          <div slot="header"><h1>My App</h1></div>
          <vi-vertical-nav-item value="home" label="Home" active></vi-vertical-nav-item>
          <vi-vertical-nav-item value="settings" label="Settings"></vi-vertical-nav-item>
        </vi-vertical-nav>
        <div style="padding:0.75rem;">Footer content pinned to bottom</div>
      </vi-stack>

      <div slot="header">Header bar</div>

      <div>Main content goes in the default slot</div>

      <div slot="footer">Footer</div>
    </vi-page>
  notes:
    - The slot="navigation" element is made a flex column by vi-page, with flex:1 to fill height
    - vi-vertical-nav keeps its header slots pinned and scrolls its item list internally when content overflows
    - vi-vertical-nav inside it does NOT stretch its items — they pack to the top
    - Put a footer div after vi-vertical-nav inside the navigation slot to pin it to the bottom
    - vi-vertical-nav is transparent by default and reads as the page; to paint it, set --vi-cmp-vertical-nav-surface (the fill) and --vi-cmp-vertical-nav-on-surface (what must be legible on it) on the rail itself, and its item states follow — do not restate the item colours, and do not paint the wrapper instead
    - The nav rail is content-sized by default; set --vi-cmp-page-nav-width on vi-page to pin it to a fixed width
    - Regions butt flush by default; set --vi-cmp-page-gap (e.g. 1px) to draw seams between banner, nav, header, main and footer
