Skip to content

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

desktop-compact

Desktop-only, compact-density UI primitives for Electron/Tauri apps. Framework-neutral custom elements (Lit), zero built-in strings (every user-facing label is consumer-supplied), Shadow DOM + CSS custom-property tokens for theming.

Not a general-purpose or mobile-density kit — it targets the density and interaction patterns of desktop utility software specifically. Layout/structure (sidebars, docking panels, toolbars) belongs in desktop-patterns; native platform integration (file dialogs, tray, window control) belongs in electron-kit. This package knows about neither, and knows nothing about any specific consuming application — it is the lowest layer, with no dependencies of its own.

Install

Requires Node ≥22.

npm install @iyulab/desktop-compact

Import only the components you use — each ships as its own subpath for tree-shaking:

import '@iyulab/desktop-compact/button'
import '@iyulab/desktop-compact/dialog'

Or import everything via the barrel:

import '@iyulab/desktop-compact'

The barrel registers every component when it is imported — that is what importing it does — so a bundle that takes anything from it takes them all, whatever it names. For one helper, use its own subpath: isModalOpen from @iyulab/desktop-compact/modal brings no component along. The package declares no sideEffects: each component's module registers its element, and no list of files would let a bundler drop the components a barrel import evaluates.

Usage

<dc-button variant="primary">Save</dc-button>

<dc-confirm-dialog
  id="confirm"
  heading="Delete item?"
  confirm-label="Delete"
  cancel-label="Cancel"
  danger
>
  This action cannot be undone.
</dc-confirm-dialog>
document.querySelector('dc-confirm-dialog')!.addEventListener('confirm', () => {
  // consumer handles the confirmed action
})

Every component takes its user-facing text as a plain attribute/property or slot — there is no built-in i18n layer, and no component assumes a language. See each component's Storybook story (npm run storybook) for its full API.

Theming

Import tokens.css once in your app's global stylesheet for consistent theming across every component:

import '@iyulab/desktop-compact/tokens.css'

Every component also ships with sane fallback values, so it still renders correctly even without the stylesheet — tokens.css is for consistent cross-component theming and dark-mode support, not a hard dependency. Override any --dc-* custom property to theme; light/dark is handled by switching token values (via your app's own data-theme attribute or a prefers-color-scheme media query) — components themselves have no light/dark awareness. tokens.css also sets color-scheme to match the palette in use, so what the browser draws itself — scrollbars, native form controls — is light or dark with it.

Colour tokens come in two forms. --dc-color-accent, -success, -warning and -danger are fills: borders, solid buttons, the tint behind a badge. Their text forms — --dc-color-accent-text, -success-text, -warning-text, -danger-text — are what a label is painted with: an outline button's label, a tinted badge's label, a danger menu item. A fill that reads well as a border is often under 4.5:1 as small text, so a theme that overrides a fill should override its text form too. A component falls back to the fill when the text form is not set.

Three layers: primitives, roles, components

Tokens come in three layers:

  1. Primitives — the base palette, spacing, type and elevation: --dc-color-surface, --dc-color-accent, --dc-space-*, --dc-font-size-*, --dc-elevation-*. tokens.css gives these values.
  2. Roles — what a part of the interface is made of: --dc-card-bg, --dc-table-header-bg, --dc-color-accent-subtle. Components read roles.
  3. Components — each reads its role tokens, falls back to a primitive, then to a literal: var(--dc-card-bg, var(--dc-color-surface, #f7f7f8)).

Roles are declared, not given a value: tokens.css registers them with @property and sets none. A role that is not set therefore falls back to the primitive where the component sits, so overriding a primitive on the root or on any subtree (a panel with its own --dc-color-surface or --dc-color-accent) reaches every component inside it. To theme a role, set it — usually on :root, or on a wrapping element to scope it. The defaults reproduce the 0.9 look; a test keeps it that way.

Role Falls back to
--dc-color-rule --dc-color-border (hairline rules)
--dc-color-accent-subtle, -secondary-subtle, -success-subtle, -warning-subtle, -danger-subtle 15% tint of the fill
--dc-page-eyebrow-color, -size --dc-color-accent-text, --dc-font-size-xs
--dc-page-title-size, -weight --dc-font-size-2xl, --dc-font-weight-bold
--dc-page-description-color --dc-color-text-secondary
--dc-page-rule --dc-color-rule
--dc-section-title-weight --dc-font-weight-semibold
--dc-section-description-color --dc-color-text-muted
--dc-section-marker-color --dc-color-accent
--dc-callout-size --dc-font-size-sm
--dc-card-bg --dc-color-surface
--dc-card-border 1px solid --dc-color-border
--dc-card-elevation none
--dc-card-radius --dc-radius-md
--dc-card-header-size, -accent --dc-font-size-md, --dc-color-secondary-text
--dc-card-footer-bg --dc-color-surface
--dc-field-label-size, -weight, -color --dc-font-size-sm, --dc-font-weight-medium, --dc-color-text-secondary
--dc-field-hint-color --dc-color-text-secondary
--dc-field-required-color --dc-color-accent-text
--dc-field-gap --dc-space-1
--dc-metric-size --dc-font-size-display
--dc-metric-accent-1, -accent-2 --dc-color-accent, --dc-color-secondary
--dc-indicator-color --dc-color-accent
--dc-selection-bg transparent
--dc-table-header-bg, -header-color --dc-color-surface, --dc-color-text
--dc-table-rule --dc-color-rule
--dc-table-total-rule --dc-color-text
:root {
  --dc-card-border: none;
  --dc-card-elevation: var(--dc-elevation-1);
  --dc-selection-bg: color-mix(in srgb, var(--dc-color-accent) 10%, transparent);
}

The Storybook story Foundations/Hierarchy shows the parts together, and BrandOverride the same screen with roles overridden on a wrapping element.

Checking token references

A var(--dc-…, fallback) whose token no stylesheet defines falls back silently — often to a light default that only looks wrong in the dark scheme. The package ships a command that fails when your code reads a --dc-* token nothing defines:

desktop-compact-check-tokens [--defined <css>]... <dir|file>...

A token registered with @property (the role tokens) counts as defined. This package's tokens.css always counts as defined; each --defined adds another stylesheet (your app's own, or another design-token package's). Directories are scanned recursively for .ts, .js, .mjs and .css files, skipping node_modules and dot-directories. Every undefined token is printed as --dc-x in <path> and the command exits 1; otherwise it prints [tokens] ok — N defined and exits 0. Run it from your test script, for example:

"scripts": { "check:tokens": "desktop-compact-check-tokens --defined src/styles.css src" }

Components (v1 — complete, 17/17)

Component Description
dc-button Button with primary/secondary/ghost/danger/outline variants and sm/md sizes, form-associated (type="submit"/"reset" participate in the owning <form>; the first type="submit" also submits it on Enter in a text field, as a native default button would)
dc-input Text/email/password/number/search input, form-associated with native constraint validation
dc-select Select control, data-driven options property (not slotted <option>s — works around a shadow-DOM <select> HTML spec gap); an option's optional group renders consecutive options under one <optgroup label>
dc-checkbox Checkbox with its label slotted — wraps the native checkbox (role, Space key, indeterminate from the platform), form-associated (submits value under name while checked, required validation), change on user toggle only
dc-textarea Multi-line text input, form-associated
dc-badge Small status/count indicator
dc-card Content container with consistent padding/border
dc-spinner Loading indicator
dc-dialog Modal dialog (native <dialog> + showModal() — focus trap, backdrop, Escape-to-close all come from the platform); placement="end" makes it a full-height sheet along the end edge
dc-confirm-dialog dc-dialog + two dc-buttons composed into a confirm/cancel flow
dc-delete-confirm-button Two-state delete trigger: icon-only button swaps to a visible confirm button on click, resets on blur
dc-empty-state Empty-state placeholder (icon slot, heading, description, actions slot)
dc-section-heading Section heading with consistent typography
dc-field Label, required mark, hint and error around one control; the hint or error, invalid and required reach the control for screen readers
dc-tab-bar Tab list (role="tablist"/"tab" + aria-selected) — roving-tabindex keyboard navigation (Arrow keys wrap, Home/End); activation="manual" for panels slow to show
dc-segmented-control One value out of a few, all visible (role="radiogroup"/"radio" + aria-checked) — single Tab stop, arrow keys move and select (skipping disabled), form-associated, change on user pick only
dc-status-strip Inline idle/loading/error/done status indicator (reuses dc-spinner for the loading state)
dc-toast Single-toast display primitive — info/success/warning/error variants, no built-in stacking/queueing/auto-dismiss (consumer owns that)
dc-paste-rows-zone Paste-only target for bulk Excel/Word row import — parses tab-separated clipboard content, dispatches the parsed rows; no built-in feedback banner (compose with dc-toast for that)
dc-data-table Read-only table of records whose rows open something — sticky header, row header holding a button for keyboard activation, a click anywhere on a row activates it (a click that ends a text selection does not), cells keep their line breaks, optional empty label; dispatches activate with the row id
dc-context-menu Right-click context menu — Popover API for open/close (light-dismiss + top-layer stacking from the platform), roving-tabindex keyboard navigation, viewport-edge clamping

isModalOpen() says whether a modal is open — a dc-dialog, or anything that registers itself with setModalOpen(owner, open) — and document fires dc-modal-change when that changes: an app's keyboard shortcuts stay quiet while it is (if (isModalOpen()) return), with no search through shadow roots.

Run npm run storybook to browse every component interactively, including variants not shown above.

Accessibility

Every component ships with an axe accessibility test as part of its test suite (npm test). Where a component composes an icon-only or otherwise unlabelled control, it forwards a consumer-supplied aria-label to the actual interactive element (not just the host) — see dc-button, dc-input, dc-dialog. dc-button also forwards the states a button announces — aria-haspopup and aria-expanded for a menu or popup trigger, aria-pressed for a toggle — and follows their changes, so set them on the host as you would on a native button. A pressed toggle (aria-pressed="true") is also painted pressed — the accent fill dc-segmented-control uses for its checked segment — in every variant.

Development

npm install
npm test              # @web/test-runner, real Chromium
npm run typecheck
npm run guard:test     # node tests for the scripts and the check-tokens command
npm run build          # per-component ESM output, type declarations
npm run storybook      # interactive component browser

tsc is TypeScript 7 (@typescript/native), while typescript resolves to @typescript/typescript6: TypeScript 7 has no JavaScript compiler API yet, and the declaration build reads one. The alias goes once vite-plugin-dts runs on TypeScript 7 alone.

License

MIT

About

No description, website, or topics provided.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages