Ostorya dev docs

Design system

Components

The building blocks of Ostorya apps, live on a wallpaper, with the HTML to copy.

Each example below is rendered with the kit on the default Aurora wallpaper. Switch this site to dark mode (top right) to see the dark values.

Buttons#

Pill shaped, 40px, always an icon and a word. One primary button per area; the rest are secondary.

HTML
<div class="o-card o-row-gap">
  <button class="o-btn o-btn-primary"><i data-lucide="plus"></i>New campaign</button>
  <button class="o-btn"><i data-lucide="download"></i>Export</button>
  <button class="o-btn o-btn-ghost"><i data-lucide="sliders-horizontal"></i>Filters</button>
  <button class="o-btn o-btn-danger"><i data-lucide="trash-2"></i>Delete</button>
  <button class="o-btn o-btn-sm"><i data-lucide="pencil"></i>Edit</button>
  <button class="o-btn o-icon-btn" aria-label="Search"><i data-lucide="search"></i></button>
  <button class="o-btn o-btn-primary" disabled><i data-lucide="sparkles"></i>Choose a product first</button>
</div>
ClassUse for
o-btn o-btn-primaryThe main action of the screen or dialog. Accent gradient.
o-btnSecondary actions. Soft fill.
o-btn o-btn-ghostLow emphasis, in toolbars and rows.
o-btn o-btn-dangerThe confirm button of a delete dialog only.
o-btn-smCompact (36px), inside cards and rows. Add o-icon-btn for a square icon button with an aria-label.

A disabled button says why in its label ("Choose a product first") rather than just going grey.

Text fields#

Only you see this name.

HTML
<div class="o-card o-stack" style="max-width: 420px">
  <div>
    <label class="o-label" for="name">Campaign name</label>
    <input id="name" class="o-field" value="Thigh trainer · sales" />
    <p class="o-help">Only you see this name.</p>
  </div>
  <div class="o-search">
    <i data-lucide="search"></i>
    <input class="o-field" placeholder="Search 39 campaigns" />
  </div>
  <textarea class="o-field" placeholder="Your idea (optional)"></textarea>
</div>
  • Soft fill, no border until focus; focus shows the accent border and a 4px ring.
  • Labels above the field (13px), help text below (12.5px muted). Mark errors with aria-invalid="true" and say what to do.
  • Never a native <input type="date">: build a date picker that matches (a field that opens an o-popover).

Select and menus#

Never use the native <select>. It looks different on every system, ignores dark mode and glass, and can't show icons. Build a button that opens an o-popover list (with a search box past 8 options).

HTML
<div class="o-card o-row-gap" style="align-items: flex-start">
  <button class="o-field" style="display:flex; align-items:center; justify-content:space-between; width:220px" aria-haspopup="listbox" aria-expanded="true">
    Français <i data-lucide="chevron-down"></i>
  </button>
  <div class="o-popover" role="listbox">
    <button class="o-menu-item" role="option" aria-selected="false">العربية</button>
    <button class="o-menu-item" role="option" aria-selected="true">Français</button>
    <button class="o-menu-item" role="option" aria-selected="false">English</button>
  </div>
  <div class="o-popover" role="menu">
    <button class="o-menu-item" role="menuitem"><i data-lucide="pencil"></i>Rename</button>
    <button class="o-menu-item" role="menuitem"><i data-lucide="download"></i>Export</button>
    <button class="o-menu-item o-danger" role="menuitem"><i data-lucide="trash-2"></i>Delete</button>
  </div>
</div>

Switch#

For settings that take effect at once (a campaign on or off). For choices that wait for Save, use a checkbox.

HTML
<div class="o-card o-stack" style="max-width: 420px">
  <label class="o-row-gap"><button role="switch" aria-checked="true" class="o-switch"></button>Stop loss on</label>
  <label class="o-row-gap"><button role="switch" aria-checked="false" class="o-switch"></button>Autopilot</label>
  <label class="o-row-gap"><input type="checkbox" class="o-check" checked />Send the confirmation SMS</label>
  <label class="o-row-gap"><input type="radio" name="r" class="o-check" checked />Patient
    <input type="radio" name="r" class="o-check" />Strict</label>
</div>

Toggle aria-checked on click; the kit animates the knob, and mirrors it in right-to-left pages.

Segmented control#

Periods, views and modes: 2 to 5 short options. The selected one is the dark ink pill (light in dark mode), like the Store's tabs; add o-seg-accent for an accent pill when the choice is the screen's main setting.

HTML
<div class="o-card o-stack" style="align-items: flex-start">
  <div role="radiogroup" class="o-seg">
    <button role="radio" aria-checked="false">Today</button>
    <button role="radio" aria-checked="false">Yesterday</button>
    <button role="radio" aria-checked="true">7 days</button>
    <button role="radio" aria-checked="false"><i data-lucide="calendar"></i>More</button>
  </div>
  <div role="radiogroup" class="o-seg o-seg-accent">
    <button role="radio" aria-checked="false">Patient</button>
    <button role="radio" aria-checked="true">Balanced</button>
    <button role="radio" aria-checked="false">Strict</button>
  </div>
</div>

Badges#

Status pills: 12px, fully rounded, one of the five status tones. Never the accent and never a custom color.

KPI tiles#

Spend
78.06 $
Orders
7
Cost / order
11.15 $
Est. profit
—
HTML
<div class="o-card o-grid o-grid-4" style="padding: 12px">
  <div class="o-kpi"><span class="o-kpi-icon"><i data-lucide="wallet"></i></span>
    <div><div class="o-kpi-label">Spend</div><div class="o-kpi-value">78.06 $</div></div></div>
  <div class="o-kpi"><span class="o-kpi-icon"><i data-lucide="shopping-bag"></i></span>
    <div><div class="o-kpi-label">Orders</div><div class="o-kpi-value">7</div></div></div>
  <div class="o-kpi"><span class="o-kpi-icon"><i data-lucide="trending-up"></i></span>
    <div><div class="o-kpi-label">Cost / order</div><div class="o-kpi-value">11.15 $</div></div></div>
  <div class="o-kpi"><span class="o-kpi-icon"><i data-lucide="package"></i></span>
    <div><div class="o-kpi-label">Est. profit</div><div class="o-kpi-value">—</div></div></div>
</div>

Show — when a number can't be known yet (no cost entered, no data), never a made-up 0 or an estimate.

Lists#

One line per row, 48px, a 36px thumbnail, secondary details muted on the same line (never stacked under the name). A list sits in a flush card.

  • #1007 Mustapha E. · Chichaoua · 139 MADConfirmed
  • #1006 Salma B. · Rabat · 249 MADNo answer
  • #1005 Youssef A. · Fès · 199 MADTo confirm
HTML
<div class="o-card o-flush">
  <ul class="o-list">
    <li role="button"><span class="o-thumb"></span><span class="o-grow"><b>#1007 Mustapha E.</b> <span class="o-muted">· Chichaoua · 139 MAD</span></span><span class="o-badge o-good">Confirmed</span></li>
    <li role="button"><span class="o-thumb"></span><span class="o-grow"><b>#1006 Salma B.</b> <span class="o-muted">· Rabat · 249 MAD</span></span><span class="o-badge o-warn">No answer</span></li>
    <li role="button"><span class="o-thumb"></span><span class="o-grow"><b>#1005 Youssef A.</b> <span class="o-muted">· Fès · 199 MAD</span></span><span class="o-badge">To confirm</span></li>
  </ul>
</div>

Modals#

A modal dims the whole app and stays above the desktop dock: the kit's o-backdrop keeps 92px free at the bottom. Title, one sentence on what happens, then the buttons on the end side: secondary first, the main action last.

HTML
<div style="position: relative; height: 300px">
  <div class="o-backdrop" style="position: absolute; padding-bottom: 16px">
    <div class="o-modal" role="dialog" aria-labelledby="t">
      <h2 class="o-h2" id="t">Delete this campaign?</h2>
      <p class="o-muted" style="margin: 8px 0 0">Its 6 ads stop at once. You can't undo this.</p>
      <div class="o-modal-actions">
        <button class="o-btn"><i data-lucide="x"></i>Cancel</button>
        <button class="o-btn o-btn-danger"><i data-lucide="trash-2"></i>Delete</button>
      </div>
    </div>
  </div>
</div>

Empty states and loading#

No conversations yet

When customers message your store on WhatsApp, their chats appear here.

HTML
<div class="o-grid o-grid-2">
  <div class="o-card o-empty">
    <span class="o-empty-icon"><i data-lucide="inbox"></i></span>
    <h2 class="o-h2">No conversations yet</h2>
    <p>When customers message your store on WhatsApp, their chats appear here.</p>
    <button class="o-btn o-btn-primary"><i data-lucide="plus"></i>Connect a number</button>
  </div>
  <div class="o-card o-stack">
    <span class="o-skeleton" style="height: 18px; width: 40%"></span>
    <span class="o-skeleton" style="height: 40px"></span>
    <span class="o-skeleton" style="height: 40px"></span>
    <span class="o-skeleton" style="height: 40px; width: 70%"></span>
  </div>
</div>

An empty state says what will appear and gives the one action that fills it. While loading, show skeletons in the shape of the content, not a spinner in the middle of the window.