import { el, safeUrl, text, variant } from './dom.js';
import { effect } from './signal.js';
import { markup } from './markup.js';
const BASE = {
column: 'zd-col',
row: 'zd-row',
error: 'zd-err',
prose: 'zd-prose',
preformatted: 'zd-pre',
};
function withBase(p, base) {
const given = p.class;
if (given === undefined) {
p.class = base;
} else if (typeof given === 'function') {
p.class = () => `${base} ${given()}`;
} else {
p.class = `${base} ${given}`;
}
return p;
}
function word(value) {
return typeof value === 'function' ? () => String(value()) : String(value);
}
function props(args = {}) {
const out = {};
const style = {};
for (const [name, value] of Object.entries(args)) {
switch (name) {
case 'padding':
style.padding = typeof value === 'function' ? () => `${value()}px` : `${value}px`;
break;
case 'weight':
style['font-weight'] = value;
break;
case 'hint':
out.placeholder = value;
break;
case 'source':
out.src = typeof value === 'function' ? () => safeUrl(value()) : safeUrl(value);
break;
case 'poster':
out.poster = typeof value === 'function' ? () => safeUrl(value()) : safeUrl(value);
break;
case 'src':
case 'href':
out[name] = typeof value === 'function' ? () => safeUrl(value()) : safeUrl(value);
break;
case 'exact':
out.datetime = value;
break;
case 'expansion':
out.title = value;
break;
case 'controls':
out['aria-controls'] = value;
break;
case 'describedBy':
out['aria-describedby'] = value;
break;
case 'labelledBy':
out['aria-labelledby'] = value;
break;
case 'current':
out['aria-current'] = value;
break;
case 'live':
out['aria-live'] = value;
break;
case 'selected':
case 'expanded':
case 'pressed':
case 'checked':
out[`aria-${name}`] = word(value);
break;
case 'disabled':
out['aria-disabled'] = word(value);
break;
case 'decorative':
out['aria-hidden'] = word(value);
break;
case 'label':
out['aria-label'] = value;
break;
case 'least':
out.min = value;
break;
case 'most':
out.max = value;
break;
case 'best':
out.optimum = value;
break;
case 'message':
break; case 'class':
out.class = value;
break;
default:
out[name] = value;
}
}
if (Object.keys(style).length > 0) out.style = style;
return out;
}
export function Column(value, args = {}, children = []) {
return el(
'div',
withBase(props(args), BASE.column),
value === undefined ? children : [text(value), ...children],
);
}
export function Row(value, args = {}, children = []) {
return el(
'div',
withBase(props(args), BASE.row),
value === undefined ? children : [text(value), ...children],
);
}
export function Text(value, args = {}) {
return el('span', props(args), [text(value)]);
}
export function Heading(value, args = {}) {
return el('h1', props(args), [text(value)]);
}
export function Button(label, args = {}, children = []) {
return el('button', { type: 'button', ...props(args) }, [text(label), ...children]);
}
export function Input(binding, args = {}) {
const [get, set] = binding;
return el('input', {
type: 'text',
value: get,
onInput: (e) => set(e.target.value),
...props(args),
});
}
export function TextArea(binding, args = {}) {
const [get, set] = binding;
return el('textarea', {
value: get,
onInput: (e) => set(e.target.value),
...props(args),
});
}
export function PasswordInput(binding, args = {}) {
const [get, set] = binding;
return el('input', {
type: 'password',
autocomplete: 'current-password',
spellcheck: 'false',
value: get,
onInput: (e) => set(e.target.value),
...props(args),
});
}
export function NumberInput(binding, args = {}) {
return numericField('number', binding, args);
}
export function DateInput(binding, args = {}) {
return numericField('date', binding, args);
}
function numericField(type, [get, set], args) {
const node = el('input', {
type,
onInput: (e) => {
const read = e.target.valueAsNumber;
set(Number.isNaN(read) ? variant('None') : variant('Some', read));
},
...props(args),
});
effect(() => {
const held = get();
const shown =
held.tag === 'Some' && Number.isFinite(held.fields[0]) ? held.fields[0] : NaN;
if (!Object.is(node.valueAsNumber, shown)) node.valueAsNumber = shown;
});
return node;
}
export function Slider(binding, args = {}) {
const [get, set] = binding;
return el('input', {
type: 'range',
value: get,
onInput: (e) => set(e.target.valueAsNumber),
...props(args),
});
}
export function Select(binding, variants = [], args = {}) {
const [get, set] = binding;
return el(
'select',
{
value: () => get().tag,
onChange: (e) => set(variant(e.target.value)),
...props(args),
},
variants.map((name) => el('option', { value: name }, [name])),
);
}
export function Radio(binding, group, option, args = {}) {
const [get, set] = binding;
const button = el('input', {
type: 'radio',
name: group,
checked: () => get().tag === option,
onChange: () => set(variant(option)),
});
button.setAttribute('value', option);
if (args.label === undefined) return button;
return el('label', { class: BASE.row }, [button, text(args.label)]);
}
export function Checkbox(binding, args = {}) {
const [get, set] = binding;
const box = el('input', {
type: 'checkbox',
checked: get,
onChange: (e) => set(e.target.checked),
});
if (args.label === undefined) return box;
return el('label', { class: BASE.row }, [box, text(args.label)]);
}
export function Progress(value, args = {}) {
return el('progress', { value, ...props(args) });
}
export function Meter(value, args = {}) {
return el('meter', { value, ...props(args) });
}
export function Spinner(args = {}) {
return el('span', { 'aria-busy': 'true', ...props(args) }, ['…']);
}
export function ErrorBar(args = {}) {
return el('div', withBase({ role: 'alert', ...props(args) }, BASE.error), [
text(args.message ?? ''),
]);
}
function group(tag) {
return (args = {}, children = []) => el(tag, props(args), children);
}
function shown(tag) {
return (value, args = {}, children = []) =>
el(tag, props(args), value === undefined ? children : [text(value), ...children]);
}
function empty(tag) {
return (args = {}) => el(tag, props(args));
}
export const Main = group('main');
export const Section = group('section');
export const Article = group('article');
export const Aside = group('aside');
export const Navigation = group('nav');
export const Header = group('header');
export const Footer = group('footer');
export const Address = group('address');
export const Quote = group('blockquote');
export const List = group('ul');
export const NumberedList = group('ol');
export const Terms = group('dl');
export const HeaderRow = group('tr');
export const TableRow = group('tr');
export const Figure = group('figure');
export const Form = group('form');
export const Fieldset = group('fieldset');
export const Details = group('details');
export const Paragraph = shown('p');
export const Emphasis = shown('em');
export const Strong = shown('strong');
export const Code = shown('code');
export const CodeBlock = shown('pre');
export const Key = shown('kbd');
export const Time = shown('time');
export const Small = shown('small');
export const Mark = shown('mark');
export const Abbreviation = shown('abbr');
export function Label(value, args = {}, children = []) {
const p = props(args);
if ('aria-controls' in p) {
p.for = p['aria-controls'];
delete p['aria-controls'];
}
return el('label', p, value === undefined ? children : [text(value), ...children]);
}
export const Legend = shown('legend');
export const Summary = shown('summary');
export const Superscript = shown('sup');
export const Subscript = shown('sub');
export const Item = shown('li');
export const Term = shown('dt');
export const Description = shown('dd');
export const Caption = shown('figcaption');
export const Cell = shown('td');
export function Table(args = {}, children = []) {
return el('table', props(args), [el('tbody', {}, children)]);
}
export function HeaderCell(value, args = {}, children = []) {
return el(
'th',
{ scope: 'col', ...props(args) },
value === undefined ? children : [text(value), ...children],
);
}
export function Prose(value, args = {}) {
const p = props(args);
withBase(p, BASE.prose);
const node = el('div', p);
markup(node, typeof value === 'function' ? value() : value);
return node;
}
export const Divider = empty('hr');
export const Break = empty('br');
export const Canvas = empty('canvas');
export function Preformatted(value, args = {}, children = []) {
return el(
'pre',
withBase(props(args), BASE.preformatted),
value === undefined ? children : [text(value), ...children],
);
}
export function Image(args = {}) {
return el('img', props(args));
}
export function Video(args = {}) {
return el('video', { controls: '', ...props(args) });
}
export function Audio(args = {}) {
return el('audio', { controls: '', ...props(args) });
}
export function Frame(args = {}) {
return el('iframe', {
sandbox: '',
referrerpolicy: 'no-referrer',
loading: 'lazy',
...props(args),
});
}
export function Link(destination, args = {}, children = []) {
const href =
typeof destination === 'function' ? () => safeUrl(destination()) : safeUrl(destination);
return el('a', { href, ...props(args) }, children);
}