Skip to content

dAppCore/ui

Repository files navigation

CoreUI · @dappcore/ui

CoreUI is a brand-neutral Web Component library and design utility kit for the dappco.re polyglot stack. Lit-based, light DOM, oklch-first, Tailwind v4 friendly. Brandable with one attribute.

<body data-brand="lethean" data-mode="dark">
  <button class="bg-brand-500 text-fg-0 rounded-md font-sans shadow-2">
    Save
  </button>
</body>
  • Brand-neutral by default. Three opt-in brands ship in the box (hostuk, lethean, ofm). New brands are one CSS file each — no central registry.
  • Tailwind v4 ready. import '@dappcore/ui/tokens/tailwind' and bg-brand-500, rounded-md, font-sans, shadow-2 work natively, with [data-brand] switching flowing through.
  • Oklch-first colour helpers. Parse, convert, rotate, mix, contrast — agents building canvas/SVG/charts don't reach to npm for one util.
  • ReactiveController patterns. Focus-trap, click-outside, resize/intersection/mutation observers, brand/mode controllers — Lit-aware, lifecycle-correct.
  • a11y baked in. aria-live announcer, focus save/restore, prefers-reduced-motion / contrast / color-scheme reactive controllers.
  • Pipe registry — shared by reference with dappco.re/go/html; byte-identical output across browser + Go server + PHP server.

Install

# npm
npm install @dappcore/ui

# git submodule (dappco.re-native pattern)
git submodule add https://github.com/dAppCore/ui.git external/ui

Identity

  • Source: dappco.re/ui (canonical), github.com/dAppCore/ui (mirror)
  • Docs: https://core.help (end-user facing, un-branded help + technical guides)
  • Package: @dappcore/ui
  • Tag prefix: <core-*>

Sub-imports

import '@dappcore/ui';                        // everything (JS)
import '@dappcore/ui/tokens';                 // CSS tokens — brand-neutral
import '@dappcore/ui/tokens/tailwind';        // Tailwind v4 @theme bridge
import '@dappcore/ui/tokens/brand-lethean';   // one brand on demand

import { parseColour, mix, contrastRatio } from '@dappcore/ui/colour';
import { Easing, interpolate, clamp }      from '@dappcore/ui/math';
import { FocusTrap, matchKey }              from '@dappcore/ui/dom';
import { announce, generateId }             from '@dappcore/ui/a11y';
import { getPlatform, isNativeShell }       from '@dappcore/ui/platform';
import { BrandController, ModeController }  from '@dappcore/ui/brand';

Primitives (v0.5)

Eleven brand-neutral Web Components ready for any UI agent:

<core-button variant="primary" type="submit">Save</core-button>
<core-toggle name="notify" value="yes" checked>Notify me</core-toggle>
<core-status-dot state="good" pulse aria-label="Online"></core-status-dot>
<core-pill state="brand">
  <core-icon slot="leading" name="check" decorative></core-icon>
  Active
</core-pill>
<core-icon name="search" size="lg"></core-icon>
<core-label for="email" required>Email</core-label>
<core-card elevation="raised" interactive>Body content</core-card>
<core-glass dark radius="20px"><p>Floating panel</p></core-glass>
<core-window-controls></core-window-controls>          <!-- auto-detects -->
<core-rail href="/dashboard" active>Dashboard</core-rail>
<core-sparkline kind="area" points="1,3,2,5,4,7,6"></core-sparkline>

Light DOM, ::part()-style hooks via attribute selectors, brandable via [data-brand] (all primitives consume --core-* tokens). Default styles ship as sibling .css files; import the aggregator for one-shot setup:

@import "@dappcore/ui/primitives/index.css";

The <core-icon> registry ships 12 default icons (check, x, chevrons, plus/minus, info/warning/danger, search). Register your own with registerIcon(name, svg) or drop SVG inline via the default slot.

Forms (v0.7)

Six form-input Web Components with native <form> participation:

<form action="/v1/sign-up" method="POST">
  <core-label for="email" required>Email</core-label>
  <core-input id="email" type="email" name="email" required>
    <span slot="hint">Your work email</span>
  </core-input>

  <core-label for="password">Password</core-label>
  <core-input id="password" type="password" name="password" required minlength="8">
    <span slot="error">At least 8 characters.</span>
  </core-input>

  <core-radio-group name="plan" value="free" required>
    <core-radio value="free">Free</core-radio>
    <core-radio value="pro">Pro</core-radio>
    <core-radio value="enterprise">Enterprise</core-radio>
  </core-radio-group>

  <core-checkbox name="terms" required>
    I accept the terms of service
  </core-checkbox>

  <button type="submit">Sign up</button>
</form>

Shadow DOM (RFC §4 exception for slot distribution); full Constraint Validation API surface — setValidity, validity, validationMessage, willValidate, checkValidity, reportValidity, setCustomValidity. Inner native inputs carry real browser validity; the host mirrors to ElementInternals so <form>.checkValidity() walks every custom element correctly.

Skin via real ::part() pseudo-element (Shadow DOM, unlike the v0.5 primitives' attribute-selector workaround). Tokens still cascade — CSS custom properties pierce shadow boundaries.

Icons via attribute lookup: <core-input leading-icon="search"> resolves through the v0.5 icon registry. Hint and error content via <slot name="hint"> and <slot name="error">.

Surfaces (v0.8)

Four overlay Web Components — dialog, drawer, popover, tooltip:

<!-- Modal dialog with header/footer slots -->
<core-button id="delete-trigger">Delete item</core-button>
<core-dialog modal size="md" closedby="closerequest">
  <h2 slot="header">Confirm deletion</h2>
  <p>This action cannot be undone.</p>
  <div slot="footer">
    <core-button data-core-close>Cancel</core-button>
    <core-button onclick="this.closest('core-dialog').close('confirm')">Delete</core-button>
  </div>
</core-dialog>

<!-- Edge drawer, end side -->
<core-drawer modal side="end" closedby="any">
  <h2 slot="header">Cart (3 items)</h2>
  <!-- body content -->
  <div slot="footer"><core-button>Checkout</core-button></div>
</core-drawer>

<!-- Anchored popover (menu) -->
<core-button id="more-btn">More</core-button>
<core-popover anchor="#more-btn" placement="bottom-start" offset="8">
  <ul>
    <li><button>Edit</button></li>
    <li><button>Delete</button></li>
  </ul>
</core-popover>

<!-- Hover/focus tooltip with auto aria-describedby -->
<core-button id="save-btn" aria-label="Save">💾</core-button>
<core-tooltip anchor="#save-btn" placement="top" delay-in="700">
  Save (⌘S)
</core-tooltip>

Shadow DOM. Platform-API-first (<dialog>, Popover API, CSS Anchor Positioning with JS fallback for Safari/Firefox). Zero deps beyond Lit.

State machine (data-state="closed|opening|open|closing") on all four components — CSS targets :host([data-state="opening"]) for transition choreography. prefers-reduced-motion guard resets transitions to none.

closedby="any|closerequest|none" polyfill on all surfaces. [data-core-close] close-button convention — any descendant with that attribute closes the surface on click. Focus restored to pre-open activeElement on close.

Two abstract base classes for extension: CoreOverlayElement (dialog+drawer), CoreAnchoredElement (popover+tooltip).

Data table (v0.3)

<core-data-table> + <core-column> — the data-presentation tier. Shadow DOM host, declarative light-DOM columns, zero deps beyond Lit.

Bare table (no sort, no pagination)

<core-data-table>
  <core-column key="name"  label="Name"></core-column>
  <core-column key="email" label="Email"></core-column>
</core-data-table>

<script>
  document.querySelector('core-data-table').rows = [
    { name: 'Alice', email: 'alice@example.com' },
    { name: 'Bob',   email: 'bob@example.com' },
  ];
</script>

Sortable columns

<core-data-table>
  <core-column key="name"   label="Name"   sortable></core-column>
  <core-column key="score"  label="Score"  sortable type="number" align="end"></core-column>
  <core-column key="joined" label="Joined" sortable type="date"></core-column>
</core-data-table>

Click any sortable header: tri-state cycle asc → desc → unsorted. Cancel the built-in sort by calling event.preventDefault() on core-sort-change and assigning el.rows from server data.

Paginated

<core-data-table page-size="10">
  <core-column key="name" label="Name" sortable></core-column>
</core-data-table>

Default pagination footer: range display + prev/next + windowed page buttons. Replace entirely with <slot name="pagination"> for custom widgets.

Multi-select + density

<core-data-table
  selection="multi"
  page-size="25"
  density="compact"
  key-field="id"
>
  <core-column key="name"   label="Name"   sortable></core-column>
  <core-column key="active" label="Active" type="boolean"></core-column>

  <div slot="empty">No results match your filter.</div>
</core-data-table>

<script>
  const el = document.querySelector('core-data-table');
  el.addEventListener('core-selection-change', (e) => {
    console.log('selected:', e.detail.selected);
  });
</script>

Density: comfortable | cozy (default) | compact. Select-all header checkbox with tri-state (none/some/all). clearSelection(), selectAll(), selectNone() methods. Direct el.selected = new Set([...]) assignment.

Custom render per column

<core-data-table key-field="id">
  <core-column key="name"    label="Name"></core-column>
  <core-column key="actions" label=""></core-column>
</core-data-table>

<script>
  import { html } from 'lit';
  document.querySelector('core-column[key="actions"]').cellRender = (row) =>
    html`<button @click=${() => editUser(row.id)}>Edit</button>`;
</script>

column.cellRender receives (row, { rowIndex, columnIndex, isSelected }). Return a Lit TemplateResult, a string, or undefined (falls back to type-aware default). Custom sort comparator: column.sortFn = (a, b, dir) => ....

Note: The property is cellRender (not render) to avoid colliding with LitElement's render method. Spec uses render for ergonomics but the implementation uses cellRender.

Sticky header is on by default. Loading state: <core-data-table loading>. ARIA: role="table", aria-rowcount, aria-rowindex, aria-sort, aria-selected. Keyboard nav: ArrowUp/Down/Home/End on rows, Space toggles selection, Enter fires core-row-click, Enter/Space on sortable headers triggers sort.

Tabs (v0.4)

<core-tabs> + <core-tab> + <core-tabpanel> — W3C ARIA APG tablist pattern. Auto-wired ARIA, roving tabindex, sliding indicator, keyboard nav.

Minimal horizontal (default)

<core-tabs>
  <core-tab>General</core-tab>
  <core-tab>Account</core-tab>
  <core-tab>Security</core-tab>
  <core-tabpanel>General settings here.</core-tabpanel>
  <core-tabpanel>Account settings.</core-tabpanel>
  <core-tabpanel>Security settings.</core-tabpanel>
</core-tabs>

Explicit for/id pairing

<core-tabs>
  <core-tab for="general">General</core-tab>
  <core-tab for="billing" disabled>Billing</core-tab>
  <core-tabpanel id="general">General settings.</core-tabpanel>
  <core-tabpanel id="billing">Billing (disabled in nav).</core-tabpanel>
</core-tabs>

Vertical orientation

<core-tabs orientation="vertical">
  <core-tab>Profile</core-tab>
  <core-tab>Preferences</core-tab>
  <core-tabpanel>Profile content.</core-tabpanel>
  <core-tabpanel>Preferences content.</core-tabpanel>
</core-tabs>

Manual activation

<core-tabs activation="manual">
  <core-tab>Heavy A</core-tab>
  <core-tab>Heavy B</core-tab>
  <core-tabpanel>Expensive panel A.</core-tabpanel>
  <core-tabpanel>Expensive panel B.</core-tabpanel>
</core-tabs>

Arrow keys move focus only. Space or Enter activates the focused tab. Best for panels with heavy content.

Disabled tabs

<core-tabs>
  <core-tab>Active</core-tab>
  <core-tab disabled>Locked</core-tab>
  <core-tab>Also active</core-tab>
  <core-tabpanel>Panel one.</core-tabpanel>
  <core-tabpanel>Locked panel.</core-tabpanel>
  <core-tabpanel>Panel three.</core-tabpanel>
</core-tabs>

Disabled tabs have aria-disabled="true", are skipped in keyboard nav, and ignore clicks.

Programmatic control

const tabs = document.querySelector('core-tabs');

tabs.selectedIndex;           // current index
tabs.selectedTab;             // CoreTab | null
tabs.selectedPanel;           // CoreTabpanel | null

tabs.select(2);               // activate by index
tabs.select(elTab);           // activate by element ref
tabs.refresh();               // re-read children after dynamic insert

tabs.addEventListener('core-tab-change', (e) => {
  if (needsConfirm) e.preventDefault(); // block activation
});

Import

import '@dappcore/ui/tabs';                     // side-effect, registers all 3 elements
import { CoreTabs } from '@dappcore/ui/tabs';   // typed

Menu (v0.9)

<core-menu> + <core-menuitem> + <core-menu-separator> — W3C ARIA menu pattern. Shadow DOM container, auto-wired ARIA, roving tabindex, keyboard nav (Arrow/Home/End/Enter/Space/Escape), single-char type-ahead, submenu support.

Standalone flat menu

<core-menu>
  <core-menuitem>Dashboard</core-menuitem>
  <core-menuitem>Profile</core-menuitem>
  <core-menuitem disabled>Admin (disabled)</core-menuitem>
</core-menu>

With separator, icon slot, and shortcut slot

<core-menu>
  <core-menuitem value="new">
    <core-icon slot="start" name="plus"></core-icon>
    New file
    <span slot="end" class="shortcut">⌘N</span>
  </core-menuitem>
  <core-menuitem value="open">Open</core-menuitem>
  <core-menu-separator></core-menu-separator>
  <core-menuitem value="save">
    <core-icon slot="start" name="save"></core-icon>
    Save
    <span slot="end" class="shortcut">⌘S</span>
  </core-menuitem>
</core-menu>

Submenu (nested <core-menu> inside <core-menuitem has-submenu>)

<core-menu>
  <core-menuitem>New file</core-menuitem>
  <core-menuitem has-submenu>Export
    <core-menu>
      <core-menuitem value="pdf">As PDF</core-menuitem>
      <core-menuitem value="html">As HTML</core-menuitem>
    </core-menu>
  </core-menuitem>
</core-menu>

Keyboard: ArrowRight opens submenu + focuses first item. ArrowLeft / Escape closes back to parent.

Popover-composed (triggered menu)

<button id="more-btn">More ▾</button>
<core-popover anchor="#more-btn" placement="bottom-start">
  <core-menu>
    <core-menuitem value="edit">Edit</core-menuitem>
    <core-menuitem value="delete">Delete</core-menuitem>
  </core-menu>
</core-popover>

<core-popover> (v0.8) handles anchor positioning + light-dismiss + focus. Consumer listens for core-menu-select (action) and core-popover-close (dismissal). Call popover.hide() from the core-menu-select handler to close after action.

Programmatic focus control

import '@dappcore/ui/menu';
import type { CoreMenu } from '@dappcore/ui/menu';

const menu = document.querySelector('core-menu') as CoreMenu;

// Focus the first item when menu opens
menu.focusFirst();

// Focus a specific item by index
menu.focusItem(2);

// Focus a specific item by element ref
const item = menu.querySelector('core-menuitem[value="save"]');
menu.focusItem(item);

// Listen for selection
menu.addEventListener('core-menu-select', (e) => {
  const { item, index, value } = e.detail;
  console.log(`Selected: ${value} at index ${index}`);
});

// Listen for close request
menu.addEventListener('core-menu-close', (e) => {
  // e.preventDefault() keeps menu open (e.g. for async validation)
  popover.hide();
});

Import paths

import '@dappcore/ui/menu';                       // side-effect: registers all 3 elements
import { CoreMenu } from '@dappcore/ui/menu';      // typed
import { CoreMenuitem } from '@dappcore/ui/menu/menuitem';
import { CoreMenuSeparator } from '@dappcore/ui/menu/menu-separator';

Toast (v0.10)

<core-toast> + <core-toast-region> + toast helper — notification toasts with 4 severity levels, 6 corner positions, auto-dismiss + pause-on-hover, sticky mode, action slot.

Programmatic quickstart

import { toast } from '@dappcore/ui/toast';

// Severity shortcuts — singleton region created automatically in top-right
toast.success('File saved.');
toast.error('Upload failed.');
toast.warning('Unsaved changes will be lost.');
toast.info('New version available.');

Severity shortcuts with options

import { toast } from '@dappcore/ui/toast';

// Auto-dismiss after 3 seconds
toast.success('Saved!', { duration: 3000 });

// Sticky — must be manually dismissed
const id = toast.error('Network error.', { duration: 0 });

// Programmatic dismiss
toast.dismiss(id);

// Clear all
toast.dismissAll();

Declarative markup with <core-toast-region>

<core-toast-region position="top-right">
  <core-toast severity="info" duration="5000">
    Your session expires in 5 minutes.
  </core-toast>
</core-toast-region>

Sticky error with action button (Retry/Undo pattern)

<core-toast-region position="top-right">
  <core-toast severity="error" duration="0">
    Upload failed.
    <button slot="action" onclick="retryUpload()">Retry</button>
  </core-toast>
</core-toast-region>

Action button click does not auto-dismiss the toast — consumer handles dismiss in the click handler if desired.

Custom region position

import { toast } from '@dappcore/ui/toast';

// Target a custom region by reference
const region = document.querySelector('core-toast-region');
toast.show('Message from bottom!', { region, severity: 'info' });
<!-- Six available positions -->
<core-toast-region position="bottom-center"></core-toast-region>
<!-- top-left | top-center | top-right | bottom-left | bottom-center | bottom-right -->

Bottom regions use flex-direction: column-reverse so new toasts appear nearest the viewport edge and stack inward.

Custom severity icon

<core-toast severity="warning" duration="0">
  <svg slot="icon" viewBox="0 0 16 16">
    <!-- your icon markup — inherits currentColor -->
  </svg>
  Custom icon warning.
</core-toast>

Slot icon overrides the built-in severity SVG. Built-in icons: filled circle (info), checkmark circle (success), triangle (warning), × circle (error). All in currentColor.

ARIA summary

Severity role on [part="toast"] Live region
info status polite
success status polite
warning alert assertive
error alert assertive

<core-toast-region> sets role="region" + aria-label="Notifications" automatically.

Import paths

import '@dappcore/ui/toast';                            // side-effect: registers both elements
import { toast } from '@dappcore/ui/toast';             // programmatic helper
import { CoreToast } from '@dappcore/ui/toast/toast';
import { CoreToastRegion } from '@dappcore/ui/toast/toast-region';
import { toast } from '@dappcore/ui/toast/toast-helper';

Design canon

RFC.md — full spec including the pipe registry, component contracts, polyglot story. Read this for the why.

docs/superpowers/specs/ — incremental specs (v0.2 utils, future tracks).

Roadmap

See RFC.md §16. Currently at v0.7 — forms layer (six form-input Web Components with native <form> participation, on top of the v0.5 primitives + v0.2 utils foundations). Next ships the seed <core-data-table> per RFC.md §16.

Licence

EUPL-1.2.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors