Skip to content

Instantly share code, notes, and snippets.

@jasonsperske
Created February 21, 2026 20:29
Show Gist options
  • Select an option

  • Save jasonsperske/538c0d3f13869dda4944043e497b6a98 to your computer and use it in GitHub Desktop.

Select an option

Save jasonsperske/538c0d3f13869dda4944043e497b6a98 to your computer and use it in GitHub Desktop.
AI-Tag Example - generated by first creating index.html and example.css, then running generate:spec, then updating some of the README and SPEC files to add more information, and running generate:code and using Claude to improve the generated code and README files.

Chart Component

Description

Documentation for the ai-Chart component. Renders a chart as an SVG using the data that is produced from the data-function or downloaded from the data-src URL. Data arrives as an array of objects that have keys that match the parsed series attribute and values to chart. Each index of the array is a series that is plotted on it's own.

Attributes

  • type: 'bar' describes a bar chart, 'line' describes a line chart.
  • data-function: a globally defined function that when called produces data that this element will render. If the function starts with a * then the funciton is a generator function and will need to have .next() called until it ends. The data format
  • series: An object that define the series that will be charted and how to label each series.
  • width: 'auto' renders the full width of it's container
  • height: 'auto' renders the full height of it's container, othersise it uses CSS units.
  • label-step: Integer interval between rendered x-axis labels. 1 (default) shows every label; 2 shows every other label; 5 shows every 5th, etc. Increase this when labels overlap due to dense data. Overridden by the --chart-label-step CSS custom property when set.
  • x-label-function: Name of a globally defined function used to format x-axis labels. Called with (index: number) and must return a string. If omitted the raw index is shown.
  • y-label-function: Name of a globally defined function used to format y-axis labels. Called with (value: number) and must return a string. If omitted the raw value is shown (decimals trimmed to 1 place).

Both formatter attributes have a JavaScript property equivalent (xLabelFunction, yLabelFunction) that accept a function directly and take priority over the attribute when set.

CSS Custom Properties

  • --chart-series-<key>: Color applied to both fill and stroke for the named series (e.g. --chart-series-A).
  • --chart-label-step: Integer. When set, overrides the label-step attribute. Useful for adjusting label density with media queries without changing HTML.
document.getElementById('myChart').xLabelFunction = i => 'Day ' + i;
document.getElementById('myChart').yLabelFunction = v => v.toFixed(0) + '%';

Examples

<ai-Chart id="myChart" type="line" data-function="*generatorData"
    series='{"A": "Red", "B": "Blue", "C": "Green"}'
    width="auto" height="300px"
    x-label-function="formatXLabel">
</ai-Chart>
ai-chart {
    --chart-label-step: 2;   /* default: show every 2nd label */
}
@media (max-width: 600px) {
    ai-chart {
        --chart-label-step: 5; /* narrow screens: show fewer labels */
    }
}
// Format x labels via attribute-referenced global function
function formatXLabel(i) { return 'Day ' + i; }

// Format y labels via JS property (runs after element is defined)
document.getElementById('myChart').yLabelFunction = v => v.toFixed(0) + '%';

Chart Component Specification

Overview

Specification for the ai-Chart component. Renders a chart as an SVG using the data that is produced from the data-function or downloaded from the data-src URL. Data arrives as an object with keys represeting the series being graphed and array of numeric values for each series. Each series is plotted on it's own. Each <rect> or <path> that it renders includes a series attribute witht he series that it correcponds to (so it can be styled with CSS). The Ledgeneds also include this same series attribute so they are styled with the same selector but the text of the ledgend should be left alone. None of the charts should define a default color to keep their size down. These series colors should be definable using css vars such as: --chart-series-A: rgb(255, 0, 0) in the outer CSS file and have this value applied to series="A". This should extend to series="B" and series="C" etc. A chart can render multiple series on top of each other. Each series has a seperate key name and will need it's own CSS rules so it can be styled.

Attributes

  • type: 'bar'|'line'
  • data-function: a globally defined function that when called produces data that this element will render. If the function starts with a * then the funciton is a generator function and will need to have .next() called until it ends. The data format
  • series: An object that define the series that will be charted and how to label each series.
  • width: 'auto' renders the full width of it's container
  • height: 'auto' renders the full height of it's container, othersise it uses CSS units.
  • label-step: Positive integer. Only renders an x-axis label when index % label-step === 0. Defaults to 1 (every label). Use higher values to reduce label density and prevent overlap. Overridden by --chart-label-step when that CSS custom property is set.
  • x-label-function: Name of a global function (index: number) => string used to format x-axis tick labels. Falls back to the raw index when not set.
  • y-label-function: Name of a global function (value: number) => string used to format y-axis tick labels. Falls back to the raw value (1 decimal place) when not set.

CSS Custom Properties

  • --chart-series-<key>: Sets both fill and stroke color for the named series via a single CSS variable (e.g. --chart-series-A: red).
  • --chart-label-step: Positive integer. When present, takes priority over the label-step attribute. Read via getComputedStyle on each draw, so media query changes take effect automatically on the next resize.

JS Properties

In addition to the HTML attributes above, formatters can be set directly as JavaScript properties on the element. Property assignment takes priority over the corresponding attribute.

  • xLabelFunction: (index: number) => string — setter triggers a redraw.
  • yLabelFunction: (value: number) => string — setter triggers a redraw.

Setting a property does not remove the attribute; the property is checked first during rendering. To revert to the attribute-driven formatter, set the property to null.

Usage

<ai-Chart></ai-Chart>

Examples

<ai-Chart id="myChart" type="line" data-function="*generatorData"
    series='{"A": "Red", "B": "Blue", "C": "Green"}'
    width="auto" height="300px"
    x-label-function="formatXLabel">
</ai-Chart>
ai-chart {
    --chart-label-step: 2;
}
@media (max-width: 600px) {
    ai-chart {
        --chart-label-step: 5;
    }
}
// Global function referenced by x-label-function attribute
function formatXLabel(i) { return 'Day ' + i; }

// JS property approach: y labels set directly on the element
// Must run after the element is upgraded (e.g. in a <script type="module">)
document.getElementById('myChart').yLabelFunction = v => v.toFixed(0) + '%';

CodeSample Component

Description

A tag that tkaes the inner textNode and renders it as syntax highlighted soruce code. highlighted subsections are targetable using css vars (such as literal-color for string and numeric literals, and varible-color for varible names). Text is representied with a monospaced font (which can be adjusted with a css var named code-font) and whitespace and linebreaks are preserved. If a onCopy attribute is defined then a button on the top right with the label "copy code" is added that floats over the code and when clicked will call the function who's name is defiend in the attribute and passes the code to that function.

Attributes

  • language the lanaguage used to apply syntax highlighting rules, support langauges include javascript, csharp, cpp and python
  • onCopy the globally defined function to call when a "copy code" button is clicked that contains the textNode that is inside of this tag.

Examples

<ai-CodeSample language="javascript" onCopy="onCopyHandler">
function* generatorData() {
    let count = 50;
    while (count > 0) {
        yield { A: [Math.random() * 100], B: [Math.random() * 100], C: [Math.random() * 100] };
        count--;
    }
}
</ai-CodeSample>

CodeSample Component Specification

Overview

Specification for the ai-CodeSample component. A tag that tkaes the inner textNode and renders it as syntax highlighted soruce code. highlighted subsections are targetable using css vars (such as literal-color for string and numeric literals, and varible-color for varible names). Text is representied with a monospaced font (which can be adjusted with a css var named code-font) and whitespace and linebreaks are preserved. If a onCopy attribute is defined then a button on the top right with the label "copy code" is added that floats over the code and when clicked will call the function who's name is defiend in the attribute and passes the code to that function.

Attributes

  • language the lanaguage used to apply syntax highlighting rules, support langauges include javascript, csharp, cpp and python
  • onCopy the globally defined function to call when a "copy code" button is clicked that contains the textNode that is inside of this tag.

Usage

<ai-CodeSample></ai-CodeSample>

Examples

<ai-CodeSample language="javascript" onCopy="onCopyHandler">
function* generatorData() {
    let count = 50;
    while (count > 0) {
        yield { A: [Math.random() * 100], B: [Math.random() * 100], C: [Math.random() * 100] };
        count--;
    }
}
</ai-CodeSample>
/* You can target ai-ColumnLayout and ai-ColumnLayoutItem elements here */
ai-RowLayout {
gap: 10px;
}
ai-RowLayoutItem {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
ai-chart {
--chart-series-A: rgba(255, 99, 132, 1);
--chart-series-B: rgba(54, 162, 235, 1);
--chart-series-C: rgb(66, 190, 9);
--chart-label-step: 5;
}
@media (max-width: 700px) {
ai-chart {
--chart-label-step: 25;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example Page</title>
<script src="tags.mjs" defer type="module"></script>
<script type="module">
// Runs after all deferred module scripts — element is upgraded by this point.
document.getElementById('myChart').yLabelFunction = v => v.toFixed(0) + '%';
</script>
<script>
function* generatorData() {
let count = 50;
while (count > 0) {
yield { A: [Math.random() * 20], B: [Math.random() * 60], C: [Math.random() * 100] };
count--;
}
}
function formatXLabel(i) {
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][i % 7];
}
function onCopyHandler(event) {
console.log("Code sample copied!", event);
}
</script>
<link rel="stylesheet" href="example.css">
</head>
<body>
<ai-RowLayout>
<ai-RowLayoutItem role="navigation" width="200px">
<ai-Menu orientation="vertical" label="Example">
<ai-MenuItem href="/" label="Home"></ai-MenuItem>
<ai-MenuItem href="/about" label="About"></ai-MenuItem>
<ai-MenuItem href="/contact" label="Contact"></ai-MenuItem>
</ai-Menu>
</ai-RowLayoutItem>
<ai-RowLayoutItem role="main-content" width="auto">
<div>
<h1>Welcome to the Example Page</h1>
<p>This is a sample page demonstrating the use of AI tags.</p>
<p>Notice how the layout adjusts based on the specified widths.</p>
</div>
<div>
<ai-Chart id="myChart" type="line" data-function="*generatorData" series='{"A": "Red", "B": "Blue", "C": "Green"}'
width="auto" height="300px" x-label-function="formatXLabel"></ai-Chart>
<ai-CodeSample language="javascript" onCopy="onCopyHandler">
function* generatorData() {
let count = 10;
while (count > 0) {
yield {
A: [Math.random() * 20],
B: [Math.random() * 60],
C: [Math.random() * 100]
};
count--;
}
}
</ai-CodeSample>
</div>
</ai-RowLayoutItem>
</ai-RowLayout>
</body>
</html>
class AiChart extends HTMLElement {
static observedAttributes = ['type', 'data-function', 'data-src', 'series', 'width', 'height', 'label-step', 'x-label-function', 'y-label-function'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._data = {};
this._xLabelFn = null;
this._yLabelFn = null;
}
set xLabelFunction(fn) { this._xLabelFn = fn; this._draw(); }
set yLabelFunction(fn) { this._yLabelFn = fn; this._draw(); }
connectedCallback() {
this._style = document.createElement('style');
this.shadowRoot.appendChild(this._style);
this._svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.shadowRoot.appendChild(this._svg);
this._resizeObserver = new ResizeObserver(() => this._draw());
this._resizeObserver.observe(this);
this._fetchData();
}
disconnectedCallback() {
this._resizeObserver?.disconnect();
}
attributeChangedCallback(name, oldVal, newVal) {
if (!this.isConnected || oldVal === newVal) return;
if (name === 'data-function' || name === 'data-src') {
this._fetchData();
} else {
this._draw();
}
}
get _type() { return this.getAttribute('type') || 'bar'; }
get _series() {
try { return JSON.parse(this.getAttribute('series') || '{}'); } catch { return {}; }
}
get _widthAttr() { return this.getAttribute('width') || 'auto'; }
get _heightAttr() { return this.getAttribute('height') || 'auto'; }
get _labelStep() {
const css = parseInt(getComputedStyle(this).getPropertyValue('--chart-label-step').trim(), 10);
if (css > 0) return css;
return Math.max(1, parseInt(this.getAttribute('label-step') || '1', 10) || 1);
}
_getXFormatter() {
if (typeof this._xLabelFn === 'function') return this._xLabelFn;
const name = this.getAttribute('x-label-function');
if (name && typeof window[name] === 'function') return window[name];
return i => i;
}
_getYFormatter() {
if (typeof this._yLabelFn === 'function') return this._yLabelFn;
const name = this.getAttribute('y-label-function');
if (name && typeof window[name] === 'function') return window[name];
return val => val % 1 === 0 ? val : val.toFixed(1);
}
_getSize() {
const rect = this.getBoundingClientRect();
const w = this._widthAttr === 'auto' ? rect.width : parseFloat(this._widthAttr);
const h = this._heightAttr === 'auto' ? rect.height : parseFloat(this._heightAttr);
return { width: w || 400, height: h || 200 };
}
async _fetchData() {
this._data = {};
const fnAttr = this.getAttribute('data-function');
const srcAttr = this.getAttribute('data-src');
if (fnAttr) {
const isGen = fnAttr.startsWith('*');
const fn = window[isGen ? fnAttr.slice(1) : fnAttr];
if (typeof fn !== 'function') return;
if (isGen) {
for (const chunk of fn()) {
this._mergeData(chunk);
this._draw();
}
} else {
this._data = await Promise.resolve(fn());
}
} else if (srcAttr) {
this._data = await fetch(srcAttr).then(r => r.json());
} else {
return;
}
this._draw();
}
_mergeData(chunk) {
for (const key of Object.keys(chunk)) {
if (!this._data[key]) this._data[key] = [];
this._data[key].push(...chunk[key]);
}
}
_seriesKeys() {
const configKeys = Object.keys(this._series);
return configKeys.length ? configKeys : Object.keys(this._data);
}
_draw() {
if (!this._svg) return;
const keys = this._seriesKeys();
if (!keys.length) return;
const { width, height } = this._getSize();
if (width <= 0 || height <= 0) return;
const config = this._series;
const hasLegend = Object.keys(config).length > 0;
const pad = { top: hasLegend ? 35 : 20, right: 20, bottom: 40, left: 50 };
const chartW = width - pad.left - pad.right;
const chartH = height - pad.top - pad.bottom;
const hostW = this._widthAttr === 'auto' ? '100%' : this._widthAttr;
const hostH = this._heightAttr === 'auto' ? '100%' : this._heightAttr;
const seriesCss = keys.map(k =>
`[series="${k}"]{fill:var(--chart-series-${k});stroke:var(--chart-series-${k})}`
).join('');
this._style.textContent =
`:host{display:block;width:${hostW};height:${hostH}}` +
`svg{display:block;width:100%;height:100%}` +
`.axis-label,.legend-text{font:11px sans-serif}` +
`.axis-label{fill:currentColor}` +
seriesCss +
`path[series]{fill:none}` +
`.legend-text{fill:currentColor;stroke:none}`;
this._svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
this._svg.innerHTML = '';
const allVals = keys.flatMap(k => this._data[k] || []);
if (!allVals.length) return;
const maxVal = Math.max(...allVals, 0);
const minVal = Math.min(...allVals, 0);
const range = maxVal - minVal || 1;
const maxPts = Math.max(...keys.map(k => (this._data[k] || []).length));
const toY = v => pad.top + chartH - ((v - minVal) / range) * chartH;
const toX = i => pad.left + (maxPts > 1 ? (i / (maxPts - 1)) * chartW : chartW / 2);
const yFmt = this._getYFormatter();
for (let i = 0; i <= 5; i++) {
const val = minVal + (range * i) / 5;
const y = toY(val);
this._svg.appendChild(this._el('line', {
x1: pad.left, y1: y, x2: pad.left + chartW, y2: y,
stroke: '#e0e0e0', 'stroke-width': 1
}));
const t = this._el('text', { x: pad.left - 5, y: y + 4, 'text-anchor': 'end', class: 'axis-label' });
t.textContent = yFmt(val);
this._svg.appendChild(t);
}
if (this._type === 'bar') {
this._drawBars(keys, pad, chartW, maxPts, toY, height);
} else {
this._drawLines(keys, maxPts, toX, toY, pad, height);
}
if (hasLegend) this._drawLegend(keys, config, pad);
}
_drawBars(keys, pad, chartW, maxPts, toY, height) {
const zeroY = toY(0);
const groupW = chartW / maxPts;
const barPad = groupW * 0.1;
const barW = (groupW - barPad * 2) / keys.length;
keys.forEach((key, si) => {
(this._data[key] || []).forEach((val, di) => {
const x = pad.left + di * groupW + barPad + si * barW;
const y = toY(val);
this._svg.appendChild(this._el('rect', {
x, y: Math.min(y, zeroY), width: barW, height: Math.abs(y - zeroY), series: key
}));
});
});
const step = this._labelStep;
const xFmt = this._getXFormatter();
for (let i = 0; i < maxPts; i++) {
if (i % step !== 0) continue;
const t = this._el('text', {
x: pad.left + i * groupW + groupW / 2,
y: height - pad.bottom + 18,
'text-anchor': 'middle', class: 'axis-label'
});
t.textContent = xFmt(i);
this._svg.appendChild(t);
}
}
_drawLines(keys, maxPts, toX, toY, pad, height) {
keys.forEach(key => {
const vals = this._data[key] || [];
if (!vals.length) return;
const d = vals.map((v, i) => `${i ? 'L' : 'M'}${toX(i)} ${toY(v)}`).join(' ');
this._svg.appendChild(this._el('path', {
d, 'stroke-width': 2, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', series: key
}));
});
const step = this._labelStep;
const xFmt = this._getXFormatter();
for (let i = 0; i < maxPts; i++) {
if (i % step !== 0) continue;
const t = this._el('text', {
x: toX(i), y: height - pad.bottom + 18,
'text-anchor': 'middle', class: 'axis-label'
});
t.textContent = xFmt(i);
this._svg.appendChild(t);
}
}
_drawLegend(keys, config, pad) {
let lx = pad.left;
keys.forEach(key => {
const label = config[key] || key;
this._svg.appendChild(this._el('rect', { x: lx, y: 6, width: 12, height: 12, series: key }));
const t = this._el('text', { x: lx + 16, y: 16, class: 'legend-text', series: key });
t.textContent = label;
this._svg.appendChild(t);
lx += 16 + label.length * 7 + 10;
});
}
_el(tag, attrs) {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
}
customElements.define('ai-chart', AiChart);
const KEYWORDS = {
javascript: /\b(async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|export|extends|finally|for|function|if|import|in|instanceof|let|new|of|return|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/g,
csharp: /\b(abstract|as|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while)\b/g,
cpp: /\b(alignas|alignof|and|and_eq|asm|auto|bitand|bitor|bool|break|case|catch|char|char8_t|char16_t|char32_t|class|compl|concept|const|consteval|constexpr|constinit|const_cast|continue|co_await|co_return|co_yield|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|false|float|for|friend|goto|if|inline|int|long|mutable|namespace|new|noexcept|not|not_eq|nullptr|operator|or|or_eq|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|true|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while|xor|xor_eq)\b/g,
python: /\b(False|None|True|and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b/g,
};
const COMMENT_PATTERNS = {
javascript: /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g,
csharp: /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g,
cpp: /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g,
python: /(#[^\n]*)/g,
};
const STRING_PATTERNS = {
javascript: /(`[\s\S]*?`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
csharp: /(@"(?:[^"]|"")*"|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
cpp: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
python: /("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
};
const NUMBER_PATTERN = /(?<![.\w])\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFdDlLuU]*\b/g;
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function collectRanges(code, pattern, type) {
const ranges = [];
pattern.lastIndex = 0;
let m;
while ((m = pattern.exec(code)) !== null) {
ranges.push({ start: m.index, end: m.index + m[0].length, type, text: m[0] });
}
return ranges;
}
function removeOverlaps(ranges) {
ranges.sort((a, b) => a.start - b.start);
const result = [];
let lastEnd = 0;
for (const r of ranges) {
if (r.start >= lastEnd) {
result.push(r);
lastEnd = r.end;
}
}
return result;
}
function highlightSegment(text, lang) {
const kwPattern = new RegExp(KEYWORDS[lang].source, 'g');
const ranges = removeOverlaps([
...collectRanges(text, kwPattern, 'keyword'),
...collectRanges(text, new RegExp(NUMBER_PATTERN.source, 'g'), 'literal'),
]);
let result = '';
let pos = 0;
for (const r of ranges) {
if (r.start > pos) result += escapeHtml(text.slice(pos, r.start));
result += `<span class="${r.type}">${escapeHtml(r.text)}</span>`;
pos = r.end;
}
if (pos < text.length) result += escapeHtml(text.slice(pos));
return result;
}
function highlight(code, language) {
const lang = language && KEYWORDS[language] ? language : null;
if (!lang) return escapeHtml(code);
const ranges = removeOverlaps([
...collectRanges(code, new RegExp(COMMENT_PATTERNS[lang].source, 'g'), 'comment'),
...collectRanges(code, new RegExp(STRING_PATTERNS[lang].source, 'g'), 'literal'),
]);
let result = '';
let pos = 0;
for (const r of ranges) {
if (r.start > pos) result += highlightSegment(code.slice(pos, r.start), lang);
result += `<span class="${r.type}">${escapeHtml(r.text)}</span>`;
pos = r.end;
}
if (pos < code.length) result += highlightSegment(code.slice(pos), lang);
return result;
}
const css = `
:host {
display: block;
position: relative;
}
pre {
margin: 0;
padding: 1em;
overflow-x: auto;
font-family: var(--code-font, 'Courier New', Courier, monospace);
font-size: 0.9em;
white-space: pre;
background: var(--code-background, #1e1e1e);
color: var(--code-color, #d4d4d4);
border-radius: var(--code-border-radius, 4px);
}
.keyword { color: var(--keyword-color, #569cd6); }
.literal { color: var(--literal-color, #ce9178); }
.comment { color: var(--comment-color, #6a9955); font-style: italic; }
.variable { color: var(--variable-color, #9cdcfe); }
.copy-btn {
position: absolute;
top: 0.5em;
right: 0.5em;
padding: 0.25em 0.6em;
font-size: 0.75em;
cursor: pointer;
background: var(--copy-btn-background, rgba(80,80,80,0.85));
color: var(--copy-btn-color, #ccc);
border: 1px solid var(--copy-btn-border-color, #555);
border-radius: 3px;
opacity: 0.7;
transition: opacity 0.2s;
}
.copy-btn:hover { opacity: 1; }
`;
class AiCodeSample extends HTMLElement {
static get observedAttributes() {
return ['language', 'oncopy'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._copyBtn = null;
}
connectedCallback() {
this._render();
}
attributeChangedCallback() {
if (this.shadowRoot.querySelector('pre')) this._render();
}
_render() {
const code = this.textContent;
const language = this.getAttribute('language') || '';
const onCopyAttr = this.getAttribute('oncopy');
let pre = this.shadowRoot.querySelector('pre');
if (!pre) {
const style = document.createElement('style');
style.textContent = css;
pre = document.createElement('pre');
this.shadowRoot.appendChild(style);
this.shadowRoot.appendChild(pre);
}
pre.innerHTML = highlight(code, language);
if (this._copyBtn) {
this._copyBtn.remove();
this._copyBtn = null;
}
if (onCopyAttr) {
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.type = 'button';
btn.textContent = 'copy code';
btn.addEventListener('click', () => {
if (typeof window[onCopyAttr] === 'function') window[onCopyAttr](code);
});
this.shadowRoot.appendChild(btn);
this._copyBtn = btn;
}
}
}
customElements.define('ai-codesample', AiCodeSample);
class AiMenu extends HTMLElement {
static get observedAttributes() {
return ['orientation', 'label'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
nav {
display: flex;
flex-direction: column;
list-style: none;
margin: 0;
padding: 0;
}
:host([orientation="horizontal"]) nav {
flex-direction: row;
}
</style>
<nav role="menu" part="menu">
<slot></slot>
</nav>
`;
}
connectedCallback() {
const label = this.getAttribute('label');
if (label) {
this.shadowRoot.querySelector('nav').setAttribute('aria-label', label);
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'label') {
this.shadowRoot.querySelector('nav').setAttribute('aria-label', newValue ?? '');
}
}
get orientation() {
return this.getAttribute('orientation');
}
set orientation(value) {
if (value) {
this.setAttribute('orientation', value);
} else {
this.removeAttribute('orientation');
}
}
get label() {
return this.getAttribute('label');
}
set label(value) {
if (value) {
this.setAttribute('label', value);
} else {
this.removeAttribute('label');
}
}
}
customElements.define('ai-menu', AiMenu);
const sheet = new CSSStyleSheet();
sheet.replaceSync(/*css*/`
:host {
display: block;
}
a {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
text-decoration: none;
color: inherit;
border-radius: 0.25rem;
transition: background-color 0.2s;
}
a:hover {
background-color: rgba(0, 0, 0, 0.05);
}
`);
class MenuItem extends HTMLElement {
static observedAttributes = ['href', 'label'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [sheet];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const href = this.getAttribute('href') || '#';
const label = this.getAttribute('label') || '';
this.shadowRoot.innerHTML = `<a href="${href}">${label}</a>`;
}
}
customElements.define('ai-menuitem', MenuItem);
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: flex;
flex-direction: row;
}
</style>
<slot></slot>
`;
class RowLayout extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('ai-rowlayout', RowLayout);
class AiRowLayoutItem extends HTMLElement {
static get observedAttributes() {
return ['role', 'width'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<style>:host{display:block;box-sizing:border-box;}</style><slot></slot>`;
}
connectedCallback() {
this.#applyWidth();
}
attributeChangedCallback(name) {
if (name === 'width') {
this.#applyWidth();
}
}
#applyWidth() {
const width = this.getAttribute('width');
if (width) {
this.style.width = width;
this.style.flexShrink = width === 'auto' ? '1' : '0';
this.style.flexGrow = width === 'auto' ? '1' : '0';
} else {
this.style.width = '';
this.style.flexShrink = '';
this.style.flexGrow = '';
}
}
}
customElements.define('ai-rowlayoutitem', AiRowLayoutItem);
export { AiRowLayoutItem, RowLayout, AiMenu, MenuItem, AiCodeSample, AiChart };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment