AI content components

Six Artifacts / content components from the tool-ui port — message drafts, social post cards, an item carousel, and a geo map. Each is a native ES module in site/frontend/ai/components/content/ that exports a single def object following the component contract. No runtime imports, no build step, zero dependencies beyond the existing design-system CSS.


Contract

Every component lives at site/frontend/ai/components/content/<name>.js and exports:

export const def = {
  name,                         // string — unique kebab-case identifier
  category: "Artifacts",        // fixed
  title,                        // human-readable label
  match(payload),               // returns true when this component owns the payload
  render(payload, ctx),         // returns a DOM node — pure, no side effects outside ctx
  example,                      // a concrete payload object for previews / docs
};

ctx (optional, feature-detected) — the generative-UI runtime context:

All components XSS-safe: user-supplied strings go through esc() before touching innerHTML. All components degrade gracefully when ctx is absent.


message_draft

An editable draft email card with Edit and Send/Approve actions.

Payload

| Field | Type | Required | Description | |---|---|---|---| | body | string | yes | Message body text. | | to | string | no | Recipient address. | | subject | string | no | Email subject line. |

Actions (ctx)

| Action | Fired when | |---|---| | draft:edit | Edit/Done button toggled. Data: { editing: boolean }. | | draft:send | Send button clicked. Data: { to, subject, body }. |

sendReceipt is called on Send with { type: "message_draft", action: "send", to, subject, body }.

Example

import { def } from "/ai/components/content/message_draft.js";
const node = def.render(def.example, ctx);
document.body.appendChild(node);
{
  "to": "team@example.com",
  "subject": "Sprint recap",
  "body": "Hi team,\n\nHere's a quick recap of this week's sprint.\n\nBest,\nOli"
}

x_post

An X / Twitter-style post card — avatar, handle, text, and engagement stats.

Payload

| Field | Type | Required | Description | |---|---|---|---| | handle | string | yes | @handle (without @). | | name | string | no | Display name (defaults to handle). | | text | string | yes | Post body. | | avatar | string | no | Avatar image URL. | | stats | object | no | { likes?, reposts?, replies? } — all numbers. |

Example

{
  "handle": "olibuijr",
  "name": "Ólafur Búi",
  "text": "Just shipped the AI component library — native ESM, zero deps. 🚀",
  "stats": { "likes": 142, "reposts": 38, "replies": 17 }
}

linkedin_post

A LinkedIn-style post card — name, optional headline, post text, and reaction/comment counts.

Payload

| Field | Type | Required | Description | |---|---|---|---| | name | string | yes | Author display name. | | headline | string | no | Professional headline (role/company). | | text | string | yes | Post body. | | stats | object | no | { likes?, comments? } — all numbers. |

Example

{
  "name": "Ólafur Búi Ólafsson",
  "headline": "Founder at AkurAI · AI solutions for Icelandic businesses",
  "text": "Excited to announce: AkurAI Framework ships a native-ESM AI component library. No React, no bundler.\n\n#AI #OpenSource",
  "stats": { "likes": 310, "comments": 42 }
}

instagram_post

An Instagram-style media post card — avatar, image (or SVG placeholder), caption, and like count.

Payload

| Field | Type | Required | Description | |---|---|---|---| | user | string | yes | Username / @handle. | | image | string | no | Image URL. Omit or leave blank for an SVG placeholder — honest, no fake images. | | caption | string | no | Post caption text. | | likes | number | no | Like count. |

Example

{
  "user": "akurai.is",
  "image": "",
  "caption": "Building AI tools for Icelandic businesses. 🇮🇸",
  "likes": 874
}

item_carousel

A horizontal scroll-snap carousel with prev/next buttons. Items can be linked or plain cards. Images are optional (SVG placeholder if absent).

Payload

| Field | Type | Required | Description | |---|---|---|---| | items | array | yes | Non-empty list of item objects (see below). |

Item shape:

| Field | Type | Required | |---|---|---| | title | string | yes | | image | string | no — URL | | subtitle | string | no | | href | string | no — must start with https:// or / |

URLs that do not start with https:// or / are silently dropped to prevent injection.

Example

{
  "items": [
    { "title": "AkurAI Framework", "subtitle": "Native ESM, zero deps", "href": "https://github.com/olibuijr/AkurAI-Framework" },
    { "title": "Streaming Chat", "subtitle": "SSE-based token streaming" },
    { "title": "Tool UI", "subtitle": "30+ generative components" }
  ]
}

geo_map

A lightweight map with no external library. Always renders a labelled coordinate panel and an SVG pin canvas. An optional tileUrl adds a static tile image grid from any OSM XYZ-scheme tile server (no key auto-fetching — you supply the URL template).

Payload

| Field | Type | Required | Description | |---|---|---|---| | lat | number | yes | Latitude (decimal degrees). | | lng | number | yes | Longitude (decimal degrees). | | zoom | number | no | Zoom level (default 13). Used for tile grid and pin positioning. | | markers | array | no | Additional pins: [{ lat, lng, label? }]. | | tileUrl | string | no | Tile URL template, e.g. "https://yourserver/{z}/{x}/{y}.png". Major tile providers (OSM, MapTiler, MapBox) require API keys — this field exists for self-hosted or pre-approved endpoints. Omit to show the honest SVG placeholder. |

Pin behaviour

The centre coordinate is always pinned. Any markers entries get secondary pins. Pin positions use Web Mercator projection, so they are proportionally accurate on the SVG canvas and on a real tile overlay.

Example

{
  "lat": 65.6835,
  "lng": -18.1002,
  "zoom": 13,
  "markers": [{ "lat": 65.684, "lng": -18.098, "label": "AkurAI HQ" }]
}

CSS

All component styles live under the tc-content-* prefix in site/frontend/styles.css (appended at the end). Components reference only the design-system tokens declared on :root — no hard-coded colours or radii. No other CSS files are touched.