Component playground

Live, hand-testable examples of every component in @f-ewald/components.

address-autocomplete

Form-associated address input with a suggestion dropdown — backed here by a local array (try "St", "Ave", or "London"), or point endpoint at a geocoding API instead.

import "@f-ewald/components/address-autocomplete.js";

<address-autocomplete placeholder="Start typing an address…"></address-autocomplete>
<script type="module">
  // Local mode: filters client-side, no network request.
  document.querySelector("address-autocomplete").suggestions = [
    { address: "1 Infinite Loop, Cupertino, CA", lat: 37.3318, lng: -122.0312 },
    { address: "10 Downing Street, London", lat: 51.5034, lng: -0.1276 },
  ];

  // API mode: omit `suggestions` and set `endpoint` + `access-token` instead.
</script>

animate-confetti

Fullscreen confetti burst overlay.

import "@f-ewald/components/animate-confetti.js";

<animate-confetti duration="6000"></animate-confetti>

autocomplete-input

Generic form-associated autocomplete for any {key, value} option list — backed here by a local array (try "Type" or "Java"), or point endpoint at an API that returns [{key, value}] for a query string instead.

import "@f-ewald/components/autocomplete-input.js";

<form>
  <autocomplete-input name="language" placeholder="Start typing a language…"></autocomplete-input>
</form>
<script type="module">
  // Local mode: filters client-side, no network request.
  document.querySelector("autocomplete-input").options = [
    { key: "ts", value: "TypeScript" },
    { key: "py", value: "Python" },
  ];

  // API mode: omit `options` and set `endpoint` instead — it's queried as
  // `${endpoint}?${queryParam}=` and must respond with [{key, value}].
</script>

calendar-month

One month as a top-to-bottom day list. Weekends are highlighted, today is marked, and overlapping declarative calendar-entry children stack into aligned lanes rather than hiding one another. Named title/detail slots fill one line per visible day, with overflow details hidden automatically.

Vacation Out of office Road trip along the California coast with several scenic stops Return July 19 at 6 PM Talks and workshops Closing keynote · July 20 Dentist Bring insurance card Appointment complete
import "@f-ewald/components/calendar-month.js";

<calendar-month year="2026" month="7">
  <calendar-entry start="2026-07-10" end="2026-07-18" label="Vacation" color="success">
    <span slot="title">Vacation</span>
    <span slot="detail">Out of office</span>
    <span slot="detail">Road trip along the California coast with several scenic stops</span>
    <span slot="footer">Return July 19 at 6 PM</span>
  </calendar-entry>
  <calendar-entry start="2026-07-15" end="2026-07-20" label="Conference" color="warning" href="#conf">
    <span slot="detail">Talks and workshops</span>
    <span slot="footer">Closing keynote · July 20</span>
  </calendar-entry>
  <calendar-entry start="2026-07-23" end="2026-07-23" label="Appointment" color="info">
    <span slot="title">Dentist</span>
    <span slot="detail">Hidden because this entry only spans one day</span>
    <span slot="footer">Also hidden for a one-day entry</span>
  </calendar-entry>
</calendar-month>

calendar-year

A full year of calendar-month blocks generated from declarative calendar-entry children — including entries that cross a month boundary or extend past the edges of the displayed year.

Offsite New York Team workshops Closing dinner Friday Product launch Coordinate the release across engineering, design, support, and marketing. Monitor adoption and production health throughout the rollout. Public launch · March 18 at 9 AM
import "@f-ewald/components/calendar-year.js";

<calendar-year year="2026">
  <calendar-entry start="2026-01-28" end="2026-02-03" label="Offsite" color="primary" href="#offsite">
    <span slot="detail">New York</span>
    <span slot="detail">Team workshops</span>
    <span slot="footer">Closing dinner Friday</span>
  </calendar-entry>
  <calendar-entry start="2026-03-05" end="2026-03-18" label="Product launch" color="success" href="#launch">
    <span slot="detail">Coordinate the release across engineering, design, support, and marketing.</span>
    <span slot="detail">Monitor adoption and production health throughout the rollout.</span>
    <span slot="footer">Public launch · March 18 at 9 AM</span>
  </calendar-entry>
  <calendar-entry start="2026-07-10" end="2026-07-18" label="Vacation" color="success"></calendar-entry>
</calendar-year>

chat-message

One conversation entry — tool calls and thinking traces are dimmed, collapsible variants of the same component.

Write notes.md containing a haiku. Done — I wrote a haiku about the changing seasons to notes.md.
directory: .
filename: notes.md
content: |
  Autumn leaves falling...
I should pick a seasonal image that reads naturally in 5-7-5 syllables. Delegated to Researcher

import "@f-ewald/components/chat-message.js";

<chat-message role="user" author="Freddy" timestamp="2026-07-19T12:00:00Z">
  Write notes.md containing a haiku.
</chat-message>
<chat-message role="agent" variant="tool" collapsible collapsed summary='file_write · {"filename": "notes.md"}'>
  directory: .
  filename: notes.md
</chat-message>

confirm-dialog

Overlay confirmation dialog with Cancel/Confirm actions.

confirm: 0 · cancel: 0

Are you sure you want to delete this item? This cannot be undone.
import "@f-ewald/components/confirm-dialog.js";

<confirm-dialog open confirm-label="Delete" cancel-label="Cancel">
  Are you sure you want to delete this item?
</confirm-dialog>

data-table

Generic table shell: a header from columns, one row per rows entry, cell content from renderCell. Optional rowHref makes whole rows clickable.

import "@f-ewald/components/data-table.js";

const table = document.querySelector("data-table");
table.columns = [
  { key: "title", label: "Title" },
  { key: "state", label: "State" },
];
table.rows = [
  { id: "tsk_1", title: "Write onboarding docs", state: "Backlog" },
  { id: "tsk_2", title: "Fix the login bug", state: "Done" },
];
table.rowHref = (row) => `#/tasks/${row.id}`;

distance-value

Inline distance display, switching units at sensible thresholds.

import "@f-ewald/components/distance-value.js";

<distance-value miles="5"></distance-value>

distribution-chart

KDE distribution curve for a named metric with value markers.

import "@f-ewald/components/distribution-chart.js";

<distribution-chart metric="sqft"></distribution-chart>
<script>
  document.querySelector("distribution-chart").values = [{ label: "", value: 1450 }];
</script>

editable-text

Click-to-edit text: a display span that turns into an input/textarea on click.

Single-line (title)

Multiline (description)

import "@f-ewald/components/editable-text.js";

<editable-text value="Write the quarterly report" label="Title"></editable-text>
<editable-text multiline placeholder="Add a description…" label="Description"></editable-text>

form-select

Styled dropdown select: a trigger button opening a listbox popover, firing change with { value }. Enable searchable for case-insensitive infix filtering while still requiring an explicit option selection.

Searchable (try gress or VIEW)

import "@f-ewald/components/form-select.js";
import {
  iconArrowPath,
  iconCheckCircle,
  iconEye,
  iconListBullet,
} from "@f-ewald/components/icons.js";

const select = document.querySelector("form-select");
select.options = [
  { value: "backlog", label: "Backlog", icon: iconListBullet(14), iconSize: 14 },
  { value: "open", label: "Open" },
  { value: "in_progress", label: "In progress", icon: iconArrowPath(16), iconSize: 16 },
  { value: "review", label: "Needs review", icon: iconEye(18), iconSize: 18 },
  { value: "done", label: "Done", icon: iconCheckCircle(16), iconSize: 16 },
];
select.value = "open";
select.searchable = true;
select.addEventListener("change", (e) => console.log(e.detail.value));

frame-box

A titled frame around a slot — a gray border with a small uppercase, muted label overlapping the top edge. Generic; the label text is up to the consumer.

Framed content goes here.

import "@f-ewald/components/frame-box.js";

<frame-box label="Debug">
  Framed content goes here.
</frame-box>

icon-button

A borderless button wrapping a passed-in icon, with a rounded hover-highlight background — a low-emphasis affordance next to content it acts on.

import "@f-ewald/components/icon-button.js";
import { iconPencil } from "@f-ewald/components/icons.js";

const btn = document.querySelector("icon-button");
btn.icon = iconPencil(16);
btn.label = "Edit";
btn.addEventListener("click", () => console.log("edit clicked"));

kbd-hint

Platform-aware keyboard shortcut hints rendered as compact keycaps. Use Mod for Command on macOS and Control elsewhere, or override platform for deterministic output.

Auto-detected:
Forced macOS:
Forced other:
Navigation:
import "@f-ewald/components/kbd-hint.js";

<kbd-hint keys="Mod+K"></kbd-hint>
<kbd-hint keys="Mod+Shift+Enter" platform="mac"></kbd-hint>

live-timer

Per-second ticking count-up timer.

Sleeping:
Running:
import "@f-ewald/components/live-timer.js";

<live-timer since="2026-07-19T12:00:00Z" prefix="Sleeping for "></live-timer>
<live-timer since="2026-07-19T12:00:00Z" format="compact" prefix="running for "></live-timer>

map-circle

Plain circular marker — a light-to-dark gradient fill with a white outer ring, no point/tail. Use it as an individual DOM marker with optional badge content, or rasterize a small instance once for a dense point layer. No mapping-library dependency.

1

Configurable ring-width and size

import "@f-ewald/components/map-circle.js";

<map-circle color="#6b7280"></map-circle>
<map-circle color="#0099D8" size="14" ring-width="3"></map-circle>
<map-circle color="#1a73e8" size="24" ring-width="5" highlighted>1</map-circle>

map-pin

Circular "Apple Maps"-style pin — a light-to-dark gradient fill with a slight point at the bottom. No mapping-library dependency; the consumer positions it (e.g. via a mapboxgl.Marker).

1 2 🏠 💼 🎓
3
import "@f-ewald/components/map-pin.js";

<map-pin color="#1a73e8" size="30">3</map-pin>
<map-pin color="#22c55e" size="26" highlighted>🏠</map-pin>

percent-bar-chart

Horizontal bar chart of labeled percentage rows.

import "@f-ewald/components/percent-bar-chart.js";

document.querySelector("percent-bar-chart").groups = [
  { key: "a", label: "White", pct: 45.2, color: "#4f46e5" },
  { key: "b", label: "Asian", pct: 28.1, color: "#0d9488" },
];

popover-panel

Anchored floating popover — like slide-panel, but positioned relative to its trigger instead of docked to the screen edge. Closes on outside click or Escape.

Full page ↗
Popover body content goes here.
Popover body content goes here.
import "@f-ewald/components/popover-panel.js";

<div style="position: relative; display: inline-block;">
  <button>New task</button>
  <popover-panel open heading="New task">
    <a slot="actions" href="#/tasks/new">Full page ↗</a>
    Popover body content goes here.
  </popover-panel>
</div>

<!-- Screen-centered modal variant -->
<popover-panel centered open heading="New task">
  Popover body content goes here.
</popover-panel>

price-history-chart

D3-powered SVG line chart for a property's price history.

import "@f-ewald/components/price-history-chart.js";

const el = document.querySelector("price-history-chart");
el.history = [
  { date: "2023-01-01", price: 620000, eventType: "Listed" },
  { date: "2023-06-01", price: 645000, eventType: "Price change" },
  { date: "2024-02-01", price: 680000, eventType: "Sold" },
];

radio-cards

Single-select cards with a label and optional description.

Selected:

import "@f-ewald/components/radio-cards.js";

const el = document.querySelector("radio-cards");
el.options = [
  { value: "simple", label: "Simple", description: "Quick-ranking view" },
  { value: "detailed", label: "Detailed", description: "Every section and layer" },
];
el.value = "simple";
el.addEventListener("change", (e) => console.log(e.detail.value));

radio-pills

Single-select compact pills for many short, same-shaped options.

Selected:

import "@f-ewald/components/radio-pills.js";

const el = document.querySelector("radio-pills");
el.options = [
  { value: "light", label: "Light" },
  { value: "streets", label: "Streets" },
  { value: "satellite", label: "Satellite" },
];
el.value = "light";
el.addEventListener("change", (e) => console.log(e.detail.value));

relative-time

Inline relative-time display (e.g. "3 hours ago").

import "@f-ewald/components/relative-time.js";

<relative-time datetime="2026-07-17T07:00:00Z"></relative-time>

reveal-button

Button that reveals hidden slotted content when clicked.

Surprise! This content was hidden.
import "@f-ewald/components/reveal-button.js";

<reveal-button label="Show the secret">
  Surprise! This content was hidden.
</reveal-button>

roman-numeral

Converts an integer to a roman numeral inline.

import "@f-ewald/components/roman-numeral.js";

<roman-numeral value="2004"></roman-numeral>

slide-panel

Sliding panel shell with header chrome and a close button.

Panel body content goes here.
import "@f-ewald/components/slide-panel.js";

<slide-panel open heading="Property details">
  Panel body content goes here.
</slide-panel>

stat-meter

Compact labeled meter for a single percentage reading — CPU/memory usage in a dashboard header, for example. percent accepts null for "no reading yet", rendering an empty bar and a "—" instead of "0%".

Custom fill color

import "@f-ewald/components/stat-meter.js";

<stat-meter label="CPU" percent="42"></stat-meter>
<stat-meter label="MEM" percent="76"></stat-meter>
<stat-meter label="I/O"></stat-meter> <!-- percent unset -> null -> renders "—" -->
<stat-meter label="GPU" percent="88" color="#dc2626"></stat-meter>

status-pill

Colored status pill, optionally with a spinning icon.

import "@f-ewald/components/status-pill.js";

<status-pill label="Running" color="primary" spinner></status-pill>
<status-pill label="Blocked" color="danger"></status-pill>

tile-grid

Generic grid shell: one bordered tile per items entry, content from renderTile. Optional itemHref makes whole tiles clickable. Optional file-icon prefixes each tile with a decorative "document" icon.

import "@f-ewald/components/tile-grid.js";

const grid = document.querySelector("tile-grid");
grid.items = [
  { name: "notes.txt" },
  { name: "photo.jpg" },
];
grid.renderTile = (item) => item.name;
grid.fileIcon = true; // or the `file-icon` attribute

toast-notification

Fixed-position stack of dismissible notifications.

import "@f-ewald/components/toast-notification.js";
import { notifySuccess } from "@f-ewald/components/toast-notification.js";

<toast-notification></toast-notification>
<script>notifySuccess("Saved!");</script>

ui-button

Button (or link) with an optional icon, in three visual weights.

+ New property Cancel Delete Saving Edit (as link)
Submit (type="submit")
import "@f-ewald/components/ui-button.js";

<ui-button variant="primary">
  <span slot="icon">...</span>
  New property
</ui-button>
<ui-button variant="danger">Delete</ui-button>
<ui-button variant="secondary" href="/properties?edit=42">Edit</ui-button>

user-avatar

Circular avatar — shows an image, falling back to an initial, falling back further to a generic icon.

import "@f-ewald/components/user-avatar.js";

<user-avatar src="https://example.com/photo.jpg" name="Freddy" size="40"></user-avatar>
<user-avatar name="Freddy" size="sm"></user-avatar>

weight-bar-chart

Sorted horizontal bar chart of normalized weights.

import "@f-ewald/components/weight-bar-chart.js";

document.querySelector("weight-bar-chart").items = [
  { id: "price", label: "Price", value: 0.4 },
  { id: "schools", label: "Schools", value: 0.35 },
  { id: "commute", label: "Commute", value: 0.25 },
];