Something went wrong. Try again.
experimental web port of the Union style engine + adapters
Something went wrong. Try again.
7.0 kB · 211 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212import { useContext, createMemo, createComponent, onCleanup } from 'solid-js';import { createElement, resolveElement, matchRules } from '@union-web/core';import type { StylePropertyGroup } from '@union-web/core';import { UnionIRContext, UnionAncestorContext, PositionerContext } from './context.js';import type { AncestorElementData } from './context.js';
const DEFAULT_STATES = [ 'hovered', 'pressed', 'checked', 'disabled', 'visual-focus', 'active-focus', 'highlighted',];
function getStateCombinations(states: string[]): string[][] { const combos: string[][] = [[]]; for (const s of states) { combos.push([s]); } if (states.includes('checked')) { for (const s of states) { if (s !== 'checked') { combos.push(['checked', s]); } } } return combos;}
export interface UseUnionOptions { role: string; context?: string[] | null; state?: string[] | (() => string[]); hints?: string[] | (() => string[]); attributes?: Record<string, string> | (() => Record<string, string>); ids?: string[] | (() => string[]); debug?: boolean;}
export interface UseUnionResult { styles: () => Record<string, StylePropertyGroup>; current: () => StylePropertyGroup; UnionProvider: (props: { children: import('solid-js').JSX.Element }) => import('solid-js').JSX.Element; insidePositioner: boolean;}
function resolveAccessor<T>(v: T | (() => T)): T { return typeof v === 'function' ? (v as () => T)() : v;}
export function useUnion(options: UseUnionOptions): UseUnionResult { const ir = useContext(UnionIRContext); const ancestorData = useContext(UnionAncestorContext);
const effectiveAncestors = createMemo((): AncestorElementData[] => { if (options.context !== undefined) { if (options.context === null) return []; return options.context.map(role => ({ role })); } return ancestorData.elements; });
const resolvedState = createMemo(() => resolveAccessor(options.state ?? [])); const resolvedHints = createMemo(() => resolveAccessor(options.hints ?? [])); const resolvedAttributes = createMemo(() => resolveAccessor(options.attributes ?? {})); const resolvedIds = createMemo(() => resolveAccessor(options.ids ?? []));
const ancestorElements = createMemo(() => effectiveAncestors() .slice() .reverse() .map(a => createElement(a.role, { states: a.states, hints: a.hints, attributes: a.attributes, ids: a.ids, }), ), );
const styles = createMemo(() => { const resolved: Record<string, StylePropertyGroup> = {}; const combos = getStateCombinations(DEFAULT_STATES); for (const combo of combos) { const key = combo.length === 0 ? 'base' : combo.join('+'); const el = createElement(options.role, { states: combo, hints: resolvedHints(), attributes: resolvedAttributes(), ids: resolvedIds(), }); resolved[key] = resolveElement(ir.rules, el, ancestorElements()); } return resolved; });
const currentKey = createMemo(() => { const s = resolvedState(); return s.length === 0 ? 'base' : s.join('+'); });
const resolvedCurrent = createMemo(() => { const map = styles(); const key = currentKey(); const exactMatch = map[key]; if (exactMatch) return exactMatch;
/* Unknown state combo — resolve on the fly */ const el = createElement(options.role, { states: resolvedState(), hints: resolvedHints(), attributes: resolvedAttributes(), ids: resolvedIds(), }); return resolveElement(ir.rules, el, ancestorElements()); });
const nextElements = createMemo(() => { const currentAncestor: AncestorElementData = { role: options.role, states: resolvedState(), hints: resolvedHints(), attributes: resolvedAttributes(), ids: resolvedIds(), }; if (options.context === null) { return [currentAncestor]; } return [...effectiveAncestors(), currentAncestor]; });
function UnionProvider(p: { children: import('solid-js').JSX.Element }) { return createComponent(UnionAncestorContext.Provider, { value: { elements: nextElements() }, get children() { return p.children; }, }); }
/** Debug logging */ if (options.debug) { const el = createElement(options.role, { states: resolvedState(), hints: resolvedHints(), attributes: resolvedAttributes(), ids: resolvedIds(), }); const matches = matchRules(ir.rules, el, ancestorElements()); const seen = new Set<unknown>(); const uniqueMatches: { selector: string; specificity: string; declarations: Record<string, string> }[] = []; for (const m of matches) { if (!seen.has(m.rule)) { seen.add(m.rule); const atomStr = m.selector.atoms.map(a => { switch (a.type) { case 'type': return a.name; case 'id': return `#${a.name}`; case 'state': return `:${a.name}`; case 'hint': return `.${a.name}`; case 'any': return '*'; case 'attribute': return a.operator ? `[${a.name}${a.operator}${a.value ?? ''}]` : `[${a.name}]`; case 'pseudo-element': return `::${a.name}`; case 'child-combinator': return '>'; case 'descendant-combinator': return ' '; } }).join(' '); uniqueMatches.push({ selector: atomStr, specificity: `${m.selector.specificity.a}.${m.selector.specificity.b}.${m.selector.specificity.c}`, declarations: m.rule.declarations, }); } } const stateLabel = resolvedState().length === 0 ? 'base' : resolvedState().join('+'); console.groupCollapsed( `%c[useUnion] %c${options.role}%c (${stateLabel}) %cancestors: [${effectiveAncestors().map(a => a.role).join(', ')}]`, 'color:#888', 'color:#0ae;font-weight:bold', 'color:#888', 'color:#666', ); for (const m of uniqueMatches) { console.groupCollapsed( `%c✓ %c${m.selector} %c(spec: ${m.specificity})`, 'color:#0c0', 'color:#fff;font-weight:bold', 'color:#888', ); console.table(m.declarations); console.groupEnd(); } console.log('%cresult:', 'color:#08f;font-weight:bold', resolvedCurrent()); console.groupEnd(); }
/* Auto-register with parent Positioner if one exists */ const posCtx = useContext(PositionerContext); if (posCtx) { const info = { horizontal: () => resolvedCurrent().layout?.alignment?.horizontal ?? 'start', vertical: () => resolvedCurrent().layout?.alignment?.vertical ?? 'center', container: () => resolvedCurrent().layout?.alignment?.container ?? 'item', }; posCtx.register(info); onCleanup(() => posCtx.unregister(info)); }
return { styles, current: resolvedCurrent, UnionProvider, insidePositioner: posCtx !== null, };}