All notable changes to the Tyrell web components library will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
One size ladder for every field โ
styles/field-size.ts. Each field component used to carry its own height/padding/font table, so "the same size" meant something slightly different on each of them. There is now a single set of--ty-field-*custom properties per rung, and every field consumes them: heights are 28 / 32 / 36 / 40 / 44px for xs โ xl, with padding, font size, line box, label type scale, label gap, in-field control box and icon size all stepping together. Narrow viewports droplgandxlone rung each (640px and 480px breakpoints) โ same ladder, no component-specific overrides. Covered bye2e/field-size-ladder.spec.ts, which asserts input, copy, select and date-picker share a height and a label scale at every rung. -
DEFAULT_SIZEintypes/common.ts. The defaultsizefor field components is declared once instead of repeated in ten components. -
valueattribute onty-calendar. The component observedyear/month/daybut notvalue, so<ty-calendar value="2026-07-15">was silently ignored and the calendar rendered the current month. The.valueproperty setter already handled everything; the attribute now routes into it. This also unblocks<TyCalendar value="โฆ" />in tyrell-react, which passesvalueas an attribute. -
display-year/display-monthattributes onty-calendar-month. The year/month API was properties-only.ty-calendardrives its child by property, which hid it โ but standalone use, the documentation site's own examples, and tyrell-react'sTyCalendarMonth(which mapsdisplayYear/displayMonthonto exactly these attribute names) all set attributes, and all silently rendered today's month instead. Invalid values are ignored rather than throwing. -
Size showcases for
ty-selectandty-radio-groupon the documentation site. Every other ladder component demonstrated xs โ xl; these two demonstrated none.
-
BREAKING: the default
sizefor every field component moves frommdtosm. Affectsty-button,ty-checkbox,ty-radio,ty-radio-group,ty-copy,ty-date-picker,ty-input,ty-select,ty-switch,ty-tagandty-textarea. A field left without asizeattribute is now 32px tall rather than 36px. Setsize="md"explicitly to keep the old dimensions. Calendar components are unaffected โ they use their ownCalendarSizescale. -
BREAKING (types):
TyInput,TySelectandTyDatePickerin tyrell-react accept the full ladder. Theirsizeprop was typed'sm' | 'md' | 'lg'and documented as "fields come in exactly three; legacy xs/xl map to sm/lg" โ no such mapping exists, and the underlying components have always supported all five rungs. Now'xs' | 'sm' | 'md' | 'lg' | 'xl'. -
Input borders in dark mode are drawn from the border family, not the neutral ink ramp.
--ty-input-border/--ty-input-border-hoveraliased--ty-color-neutral-faint/-soft, which are multiplied by--ty-neutral-l-factor(0.8in dark). That put the resting border at L 0.24 against an L 0.18 input surface โ half the separation light mode gets (0.88 on 1.00). The border ladder is exempt from that factor by design. Dark now takes it one rung harder than light (--ty-borderat rest,--ty-border-boldon hover) because a small L delta at the dark end of the scale reads much weaker than the same delta mid-scale. Light mode rest is byte-identical; light hover moves L 0.72 โ 0.76.
-
Label and error text were misaligned with the value they describe โ by 1px, on every field, at every size. The value sits inside the wrapper's 1px border plus
--ty-field-pad-x; the label and error message sit outside that box and only ever cleared the padding. Two hardcoded insets, one missing a term. There is now a single--ty-field-outer-pad-xin the ladder (calc(var(--ty-field-pad-x) + 1px)) that all outside-the-box text uses, so it resolves per rung and cannot drift again. Verified at 0px delta acrossty-input,ty-copy,ty-select,ty-date-pickerandty-textareaat all five sizes. -
ty-textareakept a second, parallel size table that had drifted from the ladder. Its horizontal padding was10pxatxswhere the ladder says8px(a 2px label/value stagger), and its error message was pinned at a hardcoded12px, off the ladder entirely. Horizontal padding now reads--ty-field-pad-xin all eleven rules, including the container- and media-query step-downs. Vertical padding and min-heights are unchanged โ a multi-line box legitimately needs its own. -
The narrow-viewport step-down didn't carry the whole rung.
lg/xldropped their height, padding, font and leading below 640px/480px but kept--ty-field-controlat the original rung. That variable sizes the in-field button box, which drives the wrapper's content height โ soty-copyatxlrendered 38px on a phone while every other field rendered 36.--ty-field-controland--ty-field-iconnow step down with everything else.
- The site's chrome identity colour is specified as a finished OKLCH swatch per mode (
oklch(0.69 0.15 42.1)light,oklch(0.71 0.13 42.2)dark), with the seeds solved back through the primary formula and the derivation written down next to them. - Nine component API tables still documented the
sizedefault asmd. - Removed the table-of-contents' left rule, which ran from the header down to wherever the TOC happened to end and had no counterpart on the left sidebar.
- The desktop header no longer repeats the current page's title โ the sidebar already highlights it. The component breadcrumb stays (it shows depth the sidebar doesn't), and the plain title still renders on mobile, where the navigation is hidden.
fixedonty-tabsโ Material's "fixed tabs": the bar is divided equally between the tabs instead of scrolling them. The default (scrollable) mode gives every button amin-width: 120pxfloor, so five short labels claim 600px whether they need it or not, and a 595px bar overflows for no good reason.<ty-tabs fixed>drops the floor, gives each button an equal share, and skips overflow entirely โ no scrolling, no edge fades, no "โฆ" jump menu. Labels ellipsize if squeezed โ plain text labels are now wrapped in a<span class="tab-label">inside the button to make that possible (the ellipsis rule applies only infixedmode; slottedlabel-{id}content is untouched). Suits 2โ5 tabs โ equal shares only read as tabs while each share fits its label, so past that everything ellipsizes and the scrollable default is the right tool. Leave it off when the tab set is open-ended or user-generated. Exposed on tyrell-react'sTyTabsas the presence booleanfixed.
-
BREAKING: the scrollable tab-button floor drops from
min-width: 120pxto72px, now overridable as--ty-tab-min-width. 120px is Material's fixed-tab figure (90dp mobile / 160dp desktop); for scrollable tabs the recommended floor is 72dp, and using the fixed-tab number on a scrolling bar manufactured overflow โ five short labels claimed 600px whether they needed it or not, so a 595px bar scrolled for no reason. Equal-width rhythm is what the newfixedattribute is for. Visual change: scrollable tab bars get narrower, label-dependent buttons; some bars that scrolled will now fit outright. Set--ty-tab-min-width: 120pxto keep the old look. -
BREAKING:
ty-tabs' transition tokens are namespaced.--transition-duration/--transition-easingwere generic enough that any ancestor setting them for its own purposes silently retimed the tabs โ and the marker glide, the carousel slide andty-tab's panel fade all read them. They are now--ty-tabs-transition-duration/--ty-tabs-transition-easing, matching the--ty-wizard-transition-*precedent. Rename any override; the defaults (300ms /ease-in-out) are unchanged. The unreleased--tabs-separatoris likewise--ty-tabs-separator.
- A dead
ty-tabsstyle rule..marker-wrapper:has(::slotted([slot="marker"])) .default-markernever reached the CSSOM โ pseudo-elements are invalid inside:has(), so the parser dropped the whole rule. Hiding the default marker when one is slotted was, and remains, handled in JS.
-
Scrollable
ty-tabsno longer squeeze their buttons or ellipsize their labels. Tab buttons are flex items and had the defaultflex-shrink: 1, so a narrow bar collapsed them toward themin-widthfloor โ "Overview" became "Overโฆ" โ before the strip ever overflowed. That inverts the contract: a scrollable tab is never narrower than its label, and hiding text the user could simply scroll to helps no one. Buttons are nowflex-shrink: 0, so they keep their natural width (floored at--ty-tab-min-width) and the strip scrolls, as the mode always promised. Label ellipsis is scoped tofixed, which is the mode that genuinely squeezes. Previously masked by the 120px floor, which most labels fit inside. -
ty-tabsno longer jerks the strip backwards when you activate a tab. Changingactiveran a full render, which rebuilds the buttons and the "โฆ" trigger. Taking the trigger out momentarily widens the strip, so the browser clampsscrollLeftdown by the trigger's width (~52px) โ and putting the trigger back does not undo that. The ensure-visible glide then animated forward from the wrong place, reading as a twitch on every click near the end of the bar. Which tab is active says nothing about how wide the tabs are, so activation now updates in place โ marker, ARIA, panels, the "โฆ" menu's active mark and the scroll โ with no rebuild and no overflow re-measure. Structural changes still re-render via the existing childList observer.updateOverflowalso saves and restoresscrollLeftaround its own trigger churn, for the resize path. -
ty-tabsno longer shows the "โฆ" jump menu when the tabs overflow by less than the trigger costs. The trigger is ~52px of the bar, taken out of the scrollable strip. It appeared on any overflow, so a 5px sliver โ five short tabs against a 595px bar, wheremin-width: 120pxalone makes 600px of content โ summoned a control that hid another 52px, turning a barely-clipped last tab into a genuinely hidden one. The trigger's real footprint is now measured after it renders and it is backed out when the overflow was smaller than that. Large overflows keep the jump menu. -
The tab strip's edge fade never veils more than is actually hidden.
--fade-left/rightwere a flat 28px whenever any scroll distance remained, so a 5px overflow was covered by a 28px gradient โ the hint obscured five times what it was hinting at. Both are nowmin(28px, distance remaining).
--tabs-separatoronty-tabsโ the color of the line between the tab bar and the panel, defaulting to--ty-border. Set it totransparentto drop the line entirely, which is what you usually want alongside a customslot="marker"that already carries the active state.
-
ty-tabswithplacement="bottom"draws the active marker on the top edge of the tab bar. The marker used to stay pinned to the bottom of the button, leaving it stranded at the far edge of the screen with the whole bar between it and the panel it marks. It now sits against the content edge in both placements โ top tabs underline, bottom tabs overline. A slotted<div slot="marker">fills the marker wrapper and is unaffected. -
ty-tabsonly clips its tab strip when the tabs actually overflow. The strip carriesoverflow-x: autoso tabs can scroll, which (per spec) forces vertical clipping too, and its edge-fademask-imageclips as well โ together they cut off anything a customslot="marker"painted outside its box, most visibly abox-shadowglow. When every tab fits there is nothing to scroll, so the strip now drops both. Overflow detection moved fromscrollWidthto the buttons' own extent, since an unclipped element has no scrolling box andscrollWidthwould just echoclientWidth. Clipping remains the pre-measurement default, so overflowing tabs still can't spill on first render. -
The default
ty-tabsactive marker is 3px tall (was 2px) โ it reads as a deliberate indicator rather than a second border now that it overlaps the separator line. -
The
ty-tabsseparator line is now a pseudo-element, so the active marker sits on it rather than beside it. The line wasborder-bottomon the button bar โ a border lives outside the strip's box, so the absolutely-positioned marker could only ever stop just above it, reading as two stacked lines. It's now.tab-buttons::afterwith the strip lifted above it.::part(buttons-container)no longer carries a border; a consumer who overrodeborder-bottom/border-topthere to restyle or remove the separator should target the::afterinstead.
- A
slot="label-{id}"element nested inside aty-tabno longer blanks that tab's button. Rich labels must be direct children ofty-tabsโ a slot in the shadow root can only be filled by a direct child of its host, so a nested one never renders. Detection used a descendant query, so it found the nested element, switched the button into slot mode, and produced an empty button (the element itself stayed invisible too โty-tabexposes only an unnamed slot). Detection is now scoped to direct children, so a misplaced label falls back to thelabelattribute instead of vanishing.
Promotion of the TC48โTC50 test-candidate line to the release-candidate channel (npm latest). All changes below under TC48, TC49 and TC50 ship in this version; nothing new beyond them. Highlights: modal/select/popup event fixes, brandโprimary theme flatten, native form-reset fidelity, enter motion for all floating surfaces, placement flush contract, prevent-escape / prevent-outside-click.
preventEscape/preventOutsideClickprops on tyrell-react'sTyModal. TC49 added the attributes to the core component but the React wrapper never exposed them โ React users had no idiomatic path to the new API. Presence semantics: the bare attribute is rendered only when the prop is true. The deprecatedcloseOnEscape/closeOnOutsideClickprops keep working and now carry@deprecatedJSDoc (as do the core element's property mirrors), so IDEs flag the migration.
prevent-escape/prevent-outside-clickonty-modalโ presence booleans that guard the close paths. Closing on ESC and backdrop click is the default, so the switch names the deviation, HTML-style:<ty-modal prevent-outside-click>. Property mirrorspreventEscape/preventOutsideClicktake real booleans (true sets the attribute, false removes it, like nativedisabled). A fully guarded modal has no built-in โ โ consumers must render their own keyboard-reachable close control (WCAG 2.1.2).
- Guarded modals no longer force-close on a second consecutive ESC. Browsers allow only one close request to be canceled (the CloseWatcher anti-trap); the second ESC bypassed
canceland closed the modal withreason: "native". With ESC-close disabled the modal now consumes the Escape keydown, so no close request is ever generated. The Android back gesture deliberately remains force-closable as the last-resort escape hatch.
close-on-escape/close-on-outside-clickattributes (and thecloseOnEscape/closeOnOutsideClickproperties). Still fully working โprevent-*wins when both forms are present โ but string-valued booleans ("false") were the root of a whole bug class and these will be removed in a future release. Migrate:close-on-escape="false"โprevent-escape;close-on-outside-click="false"โprevent-outside-click.
-
The default primary seed moved from
--ty-primary-hue: 285/--ty-primary-chroma: 0.1to252/0.08(light),226/0.065(dark). Anyone consumingtyrell-theme.csswithout setting their own seeds gets a different default violet โ a two-line override (--ty-primary-hue,--ty-primary-chroma) restores the old look. -
--ty-brand-hue/--ty-brand-chromaare gone โ renamed to--ty-primary-hue/--ty-primary-chroma, and every semantic flavor's chroma is now a literal number instead of a multiplier on it. "Brand" and "primary" were two names circling the same knob: in the shipped default,--ty-brand-hueonly ever drove primary (semantic hues are fixed constants, surface tint defaults to 0, neutral is achromatic), while thecalc(var(--ty-brand-chroma) * N)coupling on success/warning/danger was overridden with a different multiplier by core light, core dark, the docs-site chrome, and the docs-site playground โ every consumer already treated the "shared" dial as a fiction. Flattened: every flavor (primary/success/warning/danger) is now one standalone hue + one standalone chroma, matching how neutral already worked.Migration:
Old New --ty-brand-hue--ty-primary-hue--ty-brand-chroma--ty-primary-chroma--ty-success-chroma: calc(var(--ty-brand-chroma) * 1.08)(light) /* 1.42(dark)--ty-success-chroma: 0.086(light) /0.092(dark)--ty-warning-chroma: calc(var(--ty-brand-chroma) * 2.22)(light) /* 3(dark)--ty-warning-chroma: 0.178(light) /0.195(dark)--ty-danger-chroma: calc(var(--ty-brand-chroma) * 1.68)(light) /* 1.56(dark)--ty-danger-chroma: 0.134(light) /0.101(dark)--ty-{flavor}-seed(full color override, any flavor) is unaffected. Anyone who only ever touched--ty-brand-huefor a one-line rebrand just renames it; anyone who pinned a semantic chroma multiplier moves to the literal number. The Theming page playground's per-flavor "chroma ร brand" sliders are now plain absolute chroma sliders. -
--ty-bg-{flavor}moved from a lightness formula to an alpha composite. Previously each background tint had its own--ty-l-bg-{base,bold,soft}lightness stop and--ty-c-bg-{base,bold,soft}-multchroma multiplier, computed the same way as the ink track:L = l-stop ร l-factor. That formula is only well-behaved mid-scale โ the bg stops sit at the ends of the L range (0.92โ0.98 light, 0.19โ0.26 dark), so a flavor withl-factorfar from 1 either clamped to the page background (tint disappeared) or collapsed to a mid-tone fill (no longer read as a background at all).--ty-bg-{flavor}is nowoklch(from var(--ty-color-{flavor}) l c h / var(--ty-a-bg-{base,bold,soft}))โ the flavor's own mid-scale ink, composited at low alpha over whatever surface it sits on. The ink stays in the gamut-widest part of the L range regardless ofl-factor, direction (lighten vs. darken) is automatic per surface instead of requiring a hand-picked absolute lightness, and the tint now correctly adapts to any surface level (content, elevated, floating), not just a single nominal one.Removed:
--ty-l-bg-base,--ty-l-bg-bold,--ty-l-bg-soft,--ty-c-bg-base-mult,--ty-c-bg-bold-mult,--ty-c-bg-soft-mult(both modes). Added:--ty-a-bg-base(9% light / 13% dark),--ty-a-bg-bold(17% / 22%),--ty-a-bg-soft(4% / 7%). A custom flavor pack's--ty-bg-Xlines need the same swap โ see the flavor pack builder on the Theming page for the current template. Visual change: background tints render slightly differently (generally cleaner at extremel-factorvalues); anyone who pinned the removed--ty-l-bg-*/--ty-c-bg-*-multdials directly needs to move that override to the corresponding--ty-a-bg-*alpha instead.
-
Enter motion for every floating surface, on one shared recipe.
ty-select,ty-date-pickerandty-popupenter with a fade +scale(0.96)+ 6px directional slide from the anchor side, driven by two tokens:--ty-popup-duration(180ms) and--ty-popup-ease(slight-overshootcubic-bezier(0.34, 1.56, 0.64, 1)).ty-tooltipgets a calmer variant (120ms fade + 4px slide, no overshoot,--ty-tooltip-duration).ty-modal's panel now zooms in (scale(0.93)+ fade) in sync with its backdrop. Under the hood select/date-picker use@starting-styleโ CSS transitions never start on an element that wasdisplay:nonethe previous frame, which is why ty-select had never animated at all:showModal()and the open class land in the same frame, and the single-select skin additionally disabled the inner panel's transition in favor of a dialog fade that could never run. -
backdrop-zoomattribute onty-modal(opt-in). While open, the page body scales to 0.99 (Vaul-style); the modal itself renders in the top layer and is unaffected. Derived state โ nested modals keep it, last one out clears it. Caveat: a scaled<body>becomes the containing block forposition: fixeddescendants; apps with fixed chrome should skip it.
-
-start/-endplacements now align the VISIBLE surface, not the shadow-room wrapper.ty-popup's dialog carries a 16px transparent wrap; cross-axis alignment ignored it ontop-*/bottom-*(panel 16px off-flush) and had the compensation sign flipped onleft-*/right-*(32px off). Tooltip was already flush (no wrap) โ popup and tooltip now share one geometry contract, pinned by e2e: 8px gap on the chosen side, aligned edges exactly flush, bare side centered. Also:ty-select's triggerโpopup gap went 4px โ 8px, matching date-picker and popup. -
Native form fidelity (from the 2026-08-19 audit).
form.reset()now restores every form control to its attribute-declared default (nativedefaultValue/defaultCheckedsemantics) โ previously checkboxes/switches stayed toggled, inputs reset to''instead of theirvalueattribute, and resetting a form wiped thevalueof everyty-radioin a group (the base class reset the wrong property on non-form children). Also fixed: re-enabling a disabledty-radio-groupre-enables its radios;ty-file-uploadapplies itsacceptfilter to drag-and-drop (the picker dialog was the only thing filtering) and unnamed uploads are excluded fromFormDatalike native inputs;ty-tabs/ty-wizardrender dynamically addedty-tab/ty-stepchildren (light DOM is now observed);ty-resize-observermigrates its registry entry on id rename;ty-selected-tagsstamps text-only<template>s;parseNumericValuetreats a repeated separator as thousands grouping ("1.234.567"โ 1234567) while a lone separator stays decimal. -
Placements gain cross-axis alignment โ
ty-popup,ty-tooltip,ty-selectandty-date-pickernow share one 12-value vocabulary. A placement is a side plus an optional alignment: the bare side centers on the anchor,-startputs the leading edges flush,-endthe trailing edges. Fortop/bottomthat axis is horizontal (bottom-start= below, left edges flush); forleft/rightit is vertical (left-start= to the left, top edges flush). All twelve combinations are valid.The
Placementtype already declared all twelve, but nothing honored them.popup.tsandtooltip.tseach hand-wrote a four-entry fallback chain matching only the bare sides, soplacement="top-start"typechecked, fell through theif-chain to the default, and was silently discarded. Replaced both with a sharedpreferenceChain()(utils/positioning.ts).Fallback flips before it re-aligns. When a placement overflows it is the side axis that ran out of room, and re-aligning on the same side cannot change the fit โ
bottom-startandbottom-endneed identical vertical space. So the requested alignment is carried across the flip first (left-startโright-start, still top-aligned), and only then are other alignments tried, perpendicular axis last. This also preserves the historic chains exactly: every bare side still degrades straight to its opposite as the very next candidate, so existing markup cannot shift on overflow. Locked by regression test. -
ty-date-pickergets aplacementattribute;ty-select'saligngainscenter. The date picker previously had no positioning control at all โ the calendar was always start-anchored below/above with no way to influence it. Dropdowns only live above or below their trigger, so both components acceptbottom*/top*directly and degradeleft-*/right-*to auto-side while keeping the alignment. Implemented via a newsideoption andcenteralignment oncomputeAnchoredPosition(), plusplacementToAnchored()to translate aPlacementinto that engine's inputs.ty-select's existingalignkeeps working standalone;placementtakes precedence when both are set.
-
left-startandright-startwere dead values โ identical to bareleft/right. Both were configuredvertical: 'center'in theplacementstable, andVerticalAlignhad no'start'member to express anything else, so neither could ever top-align. MeanwhilecalculatePlacement()already contained a working start-alignment branch that was unreachable โ nothing ever setverticalto something outside'center'/'end'. Added'start'to the union, pointed both entries at it, and gave that branch the samecontainerPaddinginset itsendsibling already had so the two alignments are symmetric about the anchor. Visual change: markup usingleft-start/right-startmoves from centered to top-aligned. Use bareleft/rightfor the old centered behavior. -
ty-iconrendered as an oval inside shrink-wrapping wrappers (regression in TC46). TC46 changed:hostfromdisplay: flextoinline-flexso an icon placed inline in running text would not force a line break. As an inline box the icon generates a line box in its parent, so any wrapper that shrink-wraps it โ a.rounded-fullbadge, a chip โ inherits the parent'sline-heightstrut instead of hugging the icon: a 12px icon in a 24px-line-height badge measured 22ร34 instead of 22ร22. Badges are far more common than icons in running text, so this reverts todisplay: flex(and restores the:host([slot])inline-flex rule). The inline-in-text case is solved correctly on the consumer side instead โ make the text wrapperinline-flex, which makes the icon a flex item. Documented in the CSS so it is not "fixed" again the same way. -
Every component rendered in a different typeface than the page. Component styles used four inconsistent approaches to
font-family:var(--ty-font-sans)(ร13), a hardcodedsystem-ui, sans-serif(ร3),inherit(ร3), and nothing at all. Since--ty-font-sansresolves to a system stack, a consumer setting their own body font got Tyrell components in the system font regardless โ contradicting the library's "typography stays yours" contract. All 19 declarations are nowinherit.Additionally,
font-family: inheriton:hostdoes not reach native form controls inside the shadow root: the UA stylesheet sets a font onbutton/input/textarea, and that beats inheritance. Soty-button's inner<button>had always rendered in the system font even when the host resolved correctly. Explicitfont-family: inheritadded to the nativebutton,input,textareaand both select search inputs. -
secondarywas still advertised in the React package. All 14tyrell-reactcomponents declaredtype BuiltinFlavor = 'primary' | 'secondary' | โฆ, so TypeScript accepted and autocompleted a flavor removed from the core in TC37 โ it silently degraded toneutralat runtime, meaning the published.d.tsmisrepresented the API. Stripped from all 14 and from the dead union documented inpackages/core/src/README.md. -
Solid auto-contrast put white text on
warning+in both modes โ below AA, and the worse of the two choices.--ty-solid-fg-thresholdshipped at0.62, described in the source as the "~0.58-0.62 sRGB break-even." Measured in a browser at stock dials, the L at which black and white give equal WCAG contrast is 0.553 (success) / 0.564 (neutral) / 0.569 (warning) / 0.570 (primary) / 0.578 (danger) โ so0.62sat above every flavor's real crossover and biased the decision toward white for any fill in the 0.55โ0.62 band.warning+is the one built-in fill that lands there, and it failed in both modes: light (L 0.588) took white at 4.23:1 where black gives 4.97, dark (L 0.604) took white at 3.95:1 where black gives 5.31. Threshold retuned to0.57. Swept across all 30 fills (15 ร light/dark): AA failures 2 โ 0, worst case 3.95:1 โ 4.83:1, and onlywarning+flips โ the other 28 foregrounds are unchanged.The 0.025 hue spread means one global dial can't be exactly right for every hue, but the previous error was ~0.05 in the same direction for all five flavors, so per-hue thresholds would add 15 tokens to buy nothing a retune doesn't. The window where every fill gets the better color is
(0.544, 0.588].Visual change:
flavor="warning+"now renders black label text instead of white, in both modes. Pin--ty-solid-warning-strong-fg: whiteto keep the old look, or set--ty-solid-fg-threshold: 1to force white everywhere. Same value applied toty-wizard's step circles, which carried their own0.6fallback for the brand-layer-not-loaded case.Regression-locked by
test/solid-contrast.test.ts, which asserts the invariant (every fill's computed foreground clears AA and beats the alternative) rather than the dial โ so retuning the L-curve or rebranding stays free, and only an actually-unreadable button fails.
-
Published package is 13% smaller โ 5.02 MB โ 4.65 MB packed, 25.8 MB โ 23.7 MB unpacked, 359 โ 181 files. Three separate causes, only one of which was comments:
Sourcemaps were dead weight.
lib/**/*.map(~2 MB) shipped via thefilesfield but referenced../../src/*.ts, which is never published, with nosourcesContentembedded โ consumers could not resolve them under any circumstance.sourceMapanddeclarationMapare now off for the published build.tscwas keeping comments. Unlike the CDN bundle (Terser,format.comments: false) the NPMlib/build had noremoveComments, so every source comment shipped. Now a two-pass build:tsconfig.lib.jsonemits.jswithremoveComments, and a newtsconfig.lib.dts.jsonemits declarations with JSDoc intact, so consumer hover documentation is unaffected. (tsconfig.lib.jsonalso had duplicateoutDiranddeclarationMapkeys; cleaned up.)CSS comments were shipping in the CDN bundle. Component styles are CSS inside template literals, which makes those comments string data โ Terser never stripped them. Removing them took 15 KB off
dist/tyrell.js(now 343 KB / 76.8 KB gzipped). -
Redundant comments removed across the source tree โ 139 files, 4,623 deletions, ~142 KB. Targeted duplicated file-header feature lists and
@exampleblocks (already inguides/TY_GUIDE.mdand the docs site), pure restatement,// ====banner rules, dead commented-out code, and stalePORTED FROM:/(Phase N)scaffolding. Rationale comments were preserved; the emitted.jsfor all 59 non-style modules is byte-identical to the previous build, and the 26 style modules differ only in removed CSS comments. Several stale comments were corrected in passing โscroll-lock.tsclaimedposition: fixedwhen the implementation usesoverflow: hiddenon<html>, andinput.tshad a/* No gap by default */note directly abovegap: 0.5rem.
ty-iconrenders as an oval inside shrink-wrapping wrappers such as.rounded-fullbadges. Fixed in TC47.
-
ty-selectgets analignattribute for horizontal popup anchoring. The popup already auto-flips vertically (below/above, whichever has more room) via the sharedcomputeAnchoredPosition()engine, but horizontally it was hardcoded to the trigger's left edge โ fine for a full-width field, but wrong for aslot="trigger"custom trigger positioned near the right edge of its container (e.g. a toolbar icon button), where the popup would grow rightward off past the trigger instead of hugging it.align="start"(default, unchanged behavior) anchors the trigger's left edge;align="end"anchors its right edge โ either way still clamped into the viewport. Implemented as a newalignoption oncomputeAnchoredPosition()(utils/positioning.ts), shared withty-date-picker's positioning engine though not yet exposed there. Also added to the React wrapper (TySelectProps.align). -
ty-selectgets a publicclear()method and aclearableattribute.clear()empties the selection programmatically and fireschange(action: 'clear') โ added forslot="trigger"consumers whose fully custom trigger chrome hides any built-in field, so they can wire their own clear icon's click handler (e.g. aty-buttonend-slot icon) directly to it. No-op if nothing is selected, same convention the existingdeselectValue()already follows.clearable(boolean, defaulttrueโ matchesty-date-picker) renders a built-in ร button in the default field andcompactskins once something is selected; it's automatically absent underslot="trigger"โ the button lives in the trigger slot's fallback content, the same mechanism that already hides the chevron there, not a special case โ and is suppressed whiledisabled/readonly. Usenot-clearableto opt out. The"clear"change-event action was already a defined type member but dead code (never dispatched) until now;TySelectEventDetail.action's missing'create'value was also fixed while touching that type. Also added to the React wrapper (TySelectProps.clearable), plus a typedTySelectElementref interface exposingclear().Caught along the way:
.select-stubisflex-wrap: wrap, and.dropdown-placeholder(the joined-labels/selected-text element) usedflex: 1 1 autoโflex-wrap's line-assignment uses each item's hypothetical (pre-shrink) size, which forflex-basis: autoonwhite-space: nowraptext is its full untruncated content width, not its visually-truncated width. With only the absolutely-positioned chevron as a sibling this never surfaced; adding.select-clearas a real flex sibling exposed it โ a long multi-select value would push the clear button onto a second line, growing the field's height, even though the text was already truncating with an ellipsis and there was visibly room. Fixed by giving the field skin's placeholderflex-basis: 0%instead ofauto(truncation behavior is identical; only the wrap-line-fit calculation changes). The.compactskin needed the opposite treatment โ it deliberately relies onflex-basis: autofor its own content-hugging sizing (flex-grow: 0, so with a0%basis and no way to grow back out, its text collapsed to zero width) โ so it now setsflex: 0 1 autoexplicitly rather than inheriting the field skin's basis. -
ty-date-pickergets a publicclear()method, for parity withty-select. Its existingclearablebutton (already shipped) called a privateclearValue()โ no way to trigger it except by clicking that exact button.clear()is a thin public wrapper around the same logic: clears the value and fireschange(detail.source: 'clear'), same as clicking the button. Also added the React wrapper's typedTyDatePickerElementref interface exposingclear(), and documentedclearableand thechangeevent payload inTY_GUIDE.mdโ both were previously undocumented there despiteclearablealready existing.
ty-select's fused search-header + options panel had a mismatched border and background in dark mode. The popup is built as one seamless card: a search-input header sits directly on top of the options list, borders/backgrounds meeting at the seam with no divider. The base panel border was already fixed (TC41) to use--ty-floating-border/--ty-surface-floatinginstead of--ty-input-border/--ty-input-bgโ deliberately faint tokens tuned for resting form fields, not floating panels โ but four spots inselect.ts(the header's own outer border/background, and the options panel's outer-facing top edge whenever it's exposed: search hidden, orposition-above) and one inselect-base.ts(the loading-state panel) never got that fix. In dark mode--ty-input-border's L (~0.24โ0.30) sits close enough to the panel's own background that the affected edge read as missing entirely, and the header's--ty-input-bgbackground was visibly darker than the options list below it. All five now consistently use--ty-floating-border/--ty-surface-floating, matching the rest of the panel โ the actual<input>element itself correctly keeps--ty-input-bg/--ty-input-borderfor its own inset field look. Verified live: border color and background are now uniform across the whole panel in both the search-visible and search-hidden layouts, light and dark.
- BREAKING:
tyrell.cssno longer hardcodes any color tokens. Every--ty-color-*,--ty-bg-{flavor}-*,--ty-border-*,--ty-surface-*,--ty-text-*,--ty-input-{color}-*and--ty-solid-*custom property tyrell.css used to define directly (lighthtml {}+ darkhtml.dark/[data-theme="dark"]) moved to a new file,tyrell-colors-static.css. Reason:tyrell-theme.css, loaded aftertyrell.cssin every documented setup, already redefines every one of those same token names via its OKLCH formulas and always won the cascade by source order โ so the hardcoded block was 100% dead weight in the normal "load both" path, and showed up in DevTools as a wall of struck-through declarations under every color property.tyrell.cssalone now has zero color tokens โ pair it withtyrell-theme.css(dynamic, recommended) or the newtyrell-colors-static.css(static, no theme engine) for colors. Every published example and guide that loadedtyrell.cssalone now also loadstyrell-theme.css; see CSS_GUIDE.md โ Color Customization.
Solid, outlined, and ghost buttons all got a pass โ more restrained default weight, tactile-but-quiet solid depth, and consistent interaction/emphasis rules across all three appearances and both modes. Summary of the final, locked design:
-
Mode-flipped typography weight dials โ bold is now opt-in.
ty-buttonhardcodedfont-weight: 590(semibold) andty-tag510, heavier than every comparable library (Material/Shoelace/Radix buttons: 500) and doubly wrong in dark mode, where light-on-dark text optically blooms and reads even heavier. Two new semantic dials intyrell.css, consumed by the components and themable at any scope:--ty-weight-action(buttons; 510 light / 400 dark) and--ty-weight-label(tags; 440 light / 400 dark). The old look is one line away (--ty-weight-action: var(--ty-font-semibold)). Registered as typed numbers in the theme layer, so with a variable font the text weight crossfades during theme transitions along with the colors. The tag's internal count badge stays semibold (a 400-weight number at xs size in a badge loses legibility). -
Solid buttons get depth chrome โ a fill-derived border and drop shadow, both optional and both quiet by default. Previously flat fills with
border: none. Now a 1px border derives from the tone's resting fill via relative color (oklch(from โฆ)) โ resting, not current, so on hover/active the fill steps up while the border stays put and visibly rises toward it, rather than the two moving together. Scaled by--ty-button-depth(default0.35;0= pixel-flat, border color matches the fill exactly with geometry unchanged). Offset direction/magnitude is--ty-button-border-l, mode-flipped and tuned per surface: colored solids get no border at all in dark mode (0โ their own hue/saturation already separates them from the page) and a subtle darkening in light (-0.08); neutral has no hue to lean on, so it gets its own stronger dial,--ty-button-border-l-neutral(-0.25light /0.5dark), giving dark ink a real lit edge against a near-black canvas. (An earlier pass also added a top-edge sheen โinset 0 1px 0highlight โ scaled by the same depth dial; removed entirely, it read as a distracting bright line rather than a subtle highlight.) Direct escape hatches outrank the derivation:--ty-solid-border-colorpins the border to any color,--ty-solid-border-widthchanges thickness. Internals: solid state/tone rules assign two private variables,--_solid-bg(current fill, swapped by hover/active) and--_solid-rest(resting fill only, untouched by state rules) โ the border formula reads the latter. Browsers without relative color simply drop the chrome and render flat, as before. -
neutral+is full-contrast ink โ the monochrome max-emphasis CTA (Geist/shadcn-style) โ in every appearance. Near-black in light mode, inverted white ink with black text in dark mode, via an absolute dial rather than an "anchor + offset" nudge:--ty-l-solid-neutral-strong(0.15light /0.88dark). Auto-contrast flips the foreground for free, and the inversion animates through theme transitions like every other dial. Outlined and ghostneutral+read the same ink dial โ unlike every other flavor, where each appearance reads its own-strongtext token โ so all three appearances agree on what "+" means for neutral specifically; base/soft neutral (all appearances) stay on the regular per-flavor text ladder, which is what actually needs to invert per mode for page-contrast legibility (the ink ramp doesn't, by design โ pointing weaker tones at it makes them illegible).button.mutedfollows the same split. Hover/active onneutral+move toward mid-lightness (fill-relative:sign(0.5 โ l) ร |dial|) rather than in the mode's fixed direction, since at an L extreme the normal directional dials clip into the ceiling/floor and vanish. -
Warning solid buttons no longer go brown in light mode โ per-flavor solid anchor lift. The solid tone ladder's "darker = stronger" is false for the yellow family: darkened hue-75 is brown, so light-mode
warning+rendered mud and base sat exactly on the auto-contrast threshold. New per-flavor data dials--ty-{flavor}-solid-ladd to the base fill's L before tones/hover/active derive; warning gets+0.1in light (0dark โ dim fills don't brown out there). Result ladder: soft pale amber โ base vivid orange โ strong deep orange, black text throughout, all AAA contrast โ the same special-casing Material applies to amber, expressed as one data dial. Other flavors default to0(no change), but the dial is there for any flavor that needs it. -
Dark-mode solid hover/active offsets raised (
--ty-solid-hover-l0.04 โ 0.07,--ty-solid-active-l0.08 โ 0.11; light mode unchanged). Small L deltas at the dark end of an emissive screen read much weaker than the same delta mid-scale โ hover on dark fills (worst on-tones, which rest another 0.1 lower) was barely perceptible. -
Outlined and ghost buttons now escalate to peak emphasis on hover/active/focus, regardless of starting tone. Previously
-/base/+were static at rest โ hover only tinted the background. Now:hover/:active/:focus-visiblealways resolve text (and, for outlined, border) to the-strongtier: soft and base both rise to the same peak color on interaction;+is already there at rest, so it's a no-op. Composes withmuted's own reveal-on-interaction mechanism rather than fighting it โ a pressed muted button lands on the same escalated color as a pressed plain button of the same flavor.
-
ty-selectopened with two simultaneous focus rings. The stub's deliberate open-state ring (added TC-era to matchty-input's.focusedescalation) applied whenever the popup was open โ but the popup's search row auto-focuses on open and carries its own real:focusring, so searchable selects showed both at once and read as two focused controls. The stub's ring is now suppressed while the search input genuinely holds focus (:not(:has(.dropdown-search-input:focus))); selects whose popup has no search row keep the stub ring exactly as before. Thecustom-triggerskin is explicitly excluded from the rule so its unconditional no-ring reset isn't out-specificity'd. -
ty-select's popup panel border was nearly invisible in dark mode. The panel reused--ty-input-borderโ the deliberately-faint resting-state form-field border, designed to escalate on hover/focus โ as its edge against arbitrary page content, where nothing ever escalates it. Switched to the tokens actually designed for floating panels:--ty-surface-floating+--ty-floating-border. -
ty-select'sslot="trigger"hit area extended well past the visible trigger. The internal.select-stub.custom-triggerrule setdisplay: inline-blockbut never overrode the inheritedwidth: 100%from the base.select-stubrule โ unlike its.compactsibling right above it, which correctly sets both.inline-block+width: 100%still spans the full row, so the cursor showed as a pointer, and clicks landed on, and opened the popup from, empty space well past the actual slotted trigger content. Added the missingwidth: auto. Verified live: the internal stub now sizes to exactly the slotted content (was 558px for a 102px button, now 102px); a click 200px past the visible trigger no longer opens the popup, a click on the trigger itself still does.
.ty-theme-switching(TC40) didn't actually reachty-*components. Its suppression rule was a universal selector (.ty-theme-switching *) in a document-level stylesheet โ Shadow DOM style encapsulation blocks that from ever matching inside a component's own shadow root, so everyty-tag,ty-input,ty-button,ty-switch, etc. kept running its local hover-transition through every theme swap, completely unaffected by TC40's fix. Confirmed live: with.ty-theme-switchingpresent,ty-tagandty-input's internal elements still reportedtransitionDuration: 0.15sand firedtransitionrunevents. Fixed with the mechanism that actually crosses that boundary:.ty-theme-switchingnow also sets an inheriting custom property,--ty-local-transition: noneโ custom properties inherit through Shadow DOM by default, so this reaches every component for free. Every component's own local transition (ty-button,ty-input,ty-textarea,ty-radio,ty-switch,ty-tabs,ty-checkbox,ty-copy,ty-file-upload,ty-calendar-month,ty-calendar-navigation,ty-date-picker,ty-wizard/ty-step,ty-scroll-container's scrollbar) now routes its existing duration/easing throughvar(--ty-local-transition, <original value>)โ a fallback, so nothing changes outside a theme switch. Four shared tokens already used by several components (--ty-transition-all,--ty-transition-colors,--ty-transition-shadow,--ty-transition-transform, defined once intyrell.css) got the same treatment centrally, coveringty-tag,ty-option,ty-modal, and severalty-selectinternals without touching those files individually. Re-verified: the same repro that showed 0 suppressed events onty-tag/ty-inputin TC40 now shows 0transitionrunevents during the switch and correct restoration (0smid-switch โ0.15safter) for both.
.ty-theme-switchingopt-in escape hatch for smooth theme toggles. Interactive components (ty-button,ty-input,ty-switch,ty-tabs, โฆ) each carry their own short hover/focus transition (~0.15โ0.2s onbackground-color/border-color/color), which CSS also fires on any non-interactive value change โ so a theme switch retriggered all of them at once, racing the coordinated 0.45s dial crossfade at a different speed on every element. Measured on our own docs site: a single theme toggle fired ~1,548 separatetransitionrunevents with finish times scattered across a 449ms window, which read as jitter rather than one smooth wash. Addty-theme-switchingto whatever you toggle (<html>or a scoped[data-ty-theme]root) for the duration of the switch to silence local transitions on its descendants only โ the dial crossfade itself is untouched. Purely additive; needs a few lines of JS around the toggle (documented in CSS_GUIDE.md โ Animated theme transitions). Applied to our own docs site's theme toggle: same swap now fires 193 events (mostly the legitimate dial properties themselves) with finish times inside a 184ms window.
Headline: ty-select gets a real display (fixes a ty-selected-tags layout bug), and the chip display is renamed to ty-selected-options (old tag kept forever).
ty-selectnever declared:host { display }, so it defaulted to the browser'sdisplay: inlinefor every skin (default field,compact,slot="trigger"). Inline boxes ignore vertical margin entirely, which is why pairing a select withty-selected-options/ty-selected-tagsbelow it (the documented pattern) rendered the chips flush against the select instead of the intended gap โ not a symptom of the earlier label-gap fix, a separate pre-existing bug that just became visible. Now:host { display: block; width: 100% }, matchingty-input's existing convention;:host([compact]) { display: inline-block; width: auto }keeps the documented "content-hugging toolbar trigger" skin actually content-hugging. Verified: margin-driven gaps now render (was 0px, now the declared value), compact still sizes to its own label, and an input+label sits flush in the same row as a select+label.- The picker's popup could visually collide with its own out-of-band chip row.
ty-select's popup is intentionally independently sized/positioned from the trigger (documented), so when it's wider or narrower than the chip row beneath it, part of that row stayed visible, peeking out beside or through the open popup.ty-selected-options/ty-selected-tagsnow listens for the picker'sopen/closeevents (already emitted byty-select) and hides itself for the duration โ cheaper and more correct than trying to out-z-index or reflow around an overlay whose footprint it doesn't control.
ty-selected-tagsrenamed toty-selected-options. The old name described the chip primitive it happens to render with (ty-tag); the new one describes what drives it โ a picker's selectedty-options โ matching how the rest of the library names things.ty-selected-tagsis re-registered as a subclass and kept working indefinitely (same convention asty-modal/ty-dialog); the JS classTySelectedTags(core and React packages) is likewise kept as an alias export ofTySelectedOptions. No migration required โ this is additive, not a removal.
Headline: tyrell-brand.css renamed to tyrell-theme.css (BREAKING path change), and the static tyrell.css fallback path is now correct-by-default instead of quietly shipping pre-TC35 contrast bugs.
packages/core/css/tyrell-brand.cssrenamed totyrell-theme.css. "Brand" undersold what the file does since TC35โ37 โ it's the auto-contrast engine, the seed-ingestion engine, the named/scoped-theme engine, and the animated-transition engine, not a cosmetic color skin. "Theme" is also the term every comparable library (Radix, Material, Web Awesome) already uses for this concept.--ty-brand-hue/--ty-brand-chromaand the rest of the Tier 1 seed variable names are unchanged โ only the file path moved. Consumers on a pinned CDN URL for a prior TC version are unaffected (those releases keep serving the old filename); anyone updating to this version needs to update their<link>/importpath. Updated everywhere in this repo: build scripts, e2e fixture, all framework guides, README, and the docs-site symlink.
tyrell.cssalone (no theme engine loaded) no longer ships broken contrast. This was worse than a missing enhancement: the single--ty-solid-{flavor}-fgper flavor was already wrong for success/danger/warning at base tone in both modes (as low as 1.58:1, no customization required to trigger it โ the exact bug class TC35 fixed, still live in the fallback path), andtone-plus/tone-minusbuttons had no fg token at all, which resolves to CSS'sinitialvalue rather than failing safe โ measured as black text on a dark navyprimary+fill. Added the missing per-tone-soft-fg/-strong-fgtokens and corrected the existing base tokens; every one of the 29 static solid fills hand-verified against both black and white, worst case now 4.66:1.tyrell-theme.css's computed auto-contrast is unaffected and still wins by source order when loaded.
- Every "load Tyrell" snippet across the guides, README, and framework docs now shows
tyrell-theme.cssalongsidetyrell.cssby default โ 17 files.README.md's prior framing ("Opt-in โ load only if you want to retint") was actively wrong given what's gated behind that file; rewritten to state the trade-off honestly.tyrell.css-only remains fully supported and correct (per the fix above) for consumers who deliberately want the static-color, older-browser-compatible path. - Docs site's own GitHub Pages CDN template (
packages/cljs/resources/index.html.template) still linkedtyrell-brand.cssand set a dead--ty-secondary-offsetseed override โ both leftover from before this rename/removal. Fixed so the published docs site (gersak.github.io/tyrell) loads correctly on this and future releases.
Headline: flavors and themes become user-declarable axes โ secondary removed (BREAKING), color seed ingestion (--ty-primary-seed: #hex), named/scoped themes via [data-ty-theme], animated theme transitions, and a flavor-pack template giving custom flavors full engine parity.
- The
secondaryflavor โ from the brand engine, the base stylesheet (tokens +ty-text-secondary*/ty-bg-secondary*/ty-border-secondary/selection utilities), and the components' built-inFLAVORSlist. The built-in set is now semantic-only:primary/success/danger/warning/neutral. Rationale: secondary was the only non-semantic built-in โ an aesthetic slot defined asbrand + 60ยฐthat most projects had to re-tune anyway. Markup usingflavor="secondary"degrades gracefully to neutral; a real second accent is one flavor pack away (see below). Verified surgical: 21 tokens removed, zero other computed-token changes vs TC36.
- Named + scoped themes โ a theme is a dial pack: any class/attribute that overrides SECTION 1 dials (
html.love { --ty-brand-hue: 340 }). New[data-ty-theme]scope selector makes the engine recompute all derived tokens on that element, so a subtree can carry its own full theme โ including dark-inside-light via<div data-ty-theme class="dark">.darkis now just the built-in reference theme pack. - Color seed ingestion โ the engine now accepts colors, not just numbers. Every flavor resolves one seed color (
--ty-{flavor}-seed: <any color>); ramp formulas read the seed's chroma + hue channels via relative color syntax, while every shade's lightness still comes from the mode-flipped L-curve. The number dials remain the default (the seed falls back to a color constructed from them โ verified 0 pixel differences at defaults, both modes). The ingestion rule is explicit: a seed's lightness is discarded by design โ shade placement is the curve's job, which is what makes dark mode,+/-tones, the input state ladder and auto-contrast correct for ANY seed, light or dark (verified with a dark seed#76467c: dark-mode emphasis direction correct, solid fg 8.7:1). This replaces thecolor-mix()approximation as the custom-flavor path: the flavor pack template, the Theming page's pack builder (now name + one color) and the CSS System brand showcase all run the same seed-ingestion engine โ the showcase's hand-tunedcolor-mix()ramp (which inverted emphasis in dark mode) is gone. Browser floor for the brand layer rises to relative-color support (Chrome 119 / Safari 16.4 / Firefox 128). - Animated theme transitions โ all 63 brand-layer dials are now registered via
@propertyas typed numbers, which makes them interpolable: switching mode (html.dark) or applying any theme pack no longer snaps โ every derived color (buttons, borders, surfaces, text, focus rings) crossfades through OKLCH space, frame by frame, with zero JS. Tune or disable with--ty-theme-transition(default0.45s; set0sto opt out); reduced-motion users never get the animation; browsers without@propertysimply snap as before. Enabled by converting the last per-mode absolutes (surface ladder,--ty-solid-neutral, bg-neutral extremes, focus-ring alpha) into numeric dials โ which also deleted the dark data block entirely: dark mode is now purely dial overrides. Verified: 0 pixel differences vs TC36 at rest in both modes; mid-transition frames measured strictly between endpoints for both mode toggles and brand-hue changes. - Theming playground is now a theme builder โ the export panel emits either
:rootoverrides (as before) or a named theme pack (.my-theme { โฆ }, usable on<html>or any[data-ty-theme]subtree), and a new Flavor pack builder section generates the full flavor-pack template live from a name + two seeds, applying it to the page as you drag so the preview row (solid/outlined/ghost buttons, tones, tag, input โ auto-contrast included) is the real engine at work. - Flavor pack template (CSS_GUIDE โ Custom Flavors) โ the per-flavor formula block published as a copy-paste template. A custom flavor declared this way gets full engine parity: shared L-curve (equal perceived weight with built-ins per shade), saturation curve, dark mode via the same dial flips with no dark block of its own, solid interaction states, auto-contrast foregrounds, and theme-scope support. Verified end-to-end from the documented text: light AA 5.5โ7.2:1, dark 7.0โ13.4:1, scoped recompute working.
- Solid buttons: tone-aware hover/active fills โ the
-hover/-activestates always derived from the base fill, so hovering aflavor-(tone-minus) button abandoned its soft fill and jumped ~0.14 L darker while keeping the soft tone's (black) text โ black-on-dark, unreadable. Tone-plus/-minus states now nudge their own fill by the sameยฑ0.04/0.08interaction dials via relative color (no new tokens; packs and custom flavors get it automatically). Text color deliberately stays pinned to the tone's rest fill through interaction โ re-deriving per state was measured to flicker blackโwhite when a nudge crossed the fg threshold (worst measured state โฅ 4.5:1 either way; stable text wins). ty-input: hover no longer overrides focus โ.input-wrapper:hoverout-ranked.focusedby specificity, so hovering a focused unflavored input swapped its primary focus border for the dimmer neutral hover border (ring stayed, border dimmed โ read as a glitch). Gated with:not(.focused), matchingty-date-picker's existing:hover:not(.open)pattern.ty-select: labelโfield gap now matchesty-inputโ.select-containercarried agap: 0.25remthat stacked on the shared label'smargin-bottom: 6px(flex gaps don't collapse with margins), making select fields 4px taller with a visibly looser label. Removed; select/input/date-picker now measure pixel-identical (6px gap, 63px total).
- Button page "Custom Flavors" demo taught the wrong tool โ it defined flavors via the
--ty-button-*per-instance overrides (chosen for solid), which leak into every appearance: the outlined example rendered invisible white-on-transparent text at rest and turned fully solid on hover (--ty-button-bg-hoveris top of outlined's hover chain too). The demo now defines flavors through the appearance-aware token layer (--ty-solid-X*for solid,--ty-color-X+--ty-bg-X-softfor outlined/ghost) and the prose explains the instance-override vs token distinction, plus the one-line--ty-X-seedpath when the brand layer is loaded.
Headline: brand-layer restructure โ formulas declared once (dark mode keeps only hand-tuned data), text/borders finally engine-wired, and greys decoupled from the brand.
-
Neutral is achromatic by default โ
--ty-neutral-hue/--ty-neutral-chromaare now0/0instead of tracking the brand seeds (brand-hue/brand-chroma ร 0.04). Greys โ text hierarchy, borders, neutral buttons, muted buttons, dividers โ no longer drift when the brand is re-hued. Opt back in to brand-warmed greys with--ty-neutral-hue: var(--ty-brand-hue); --ty-neutral-chroma: calc(var(--ty-brand-chroma) * 0.04). -
--ty-text-*and surface borders now go through the brand engine โ the 5-stop text ladder (--ty-text-strong/bold/base/soft/faint) and--ty-content/elevated/floating-borderwere hardcoded Tailwind hexes in tyrell.css that a rebrand never touched. New Tier 3 dials (--ty-l-text-*,--ty-l-border-*) hold their measured lightness; the brand layer overrides the hexes like every other token. Visible shift: text loses Tailwind's cool blue cast (max ~17/255 per channel) and follows the neutral seeds โ pure grey at the new defaults. -
Generic borders are their own family โ
--ty-border/-strong/-bold/-soft/-faintno longer alias the neutral text ramp; they compute from a dedicated L ladder (--ty-l-border-*, per-mode values preserving the exact colors the aliases produced โ verified 0 pixel diffs). Consequence: tuning the text emphasis curve or--ty-neutral-l-factorno longer moves app borders, and borders are tunable without touching text. Per-flavor accent borders (--ty-border-{flavor}) intentionally remain ink aliases.
tyrell-brand.cssdark-mode formula mirror deleted (~250 lines) โ SECTION 2 formulas are now declared once athtml:root(out-ranking tyrell.css's light tokens by specificity and itshtml.darkhexes by source order), and thehtml.darkblock holds only genuine per-mode data: the surface ladder, focus-ring alpha, the--ty-bg-neutral-strong/-faintpins, and the--ty-solid-neutralink pin. Verified byte-equivalent before the neutral change: 323 computed tokens ร 2 modes, zero differences.
Headline: auto-contrast solid foregrounds โ solid button text is now derived from its own fill's lightness instead of being hardcoded white, closing a contrast hole that affected every flavor- button out of the box. Also new: muted on ty-button.
-
mutedattribute onty-buttonโ suppresses the flavor color at rest (the button renders in neutral tokens) and reveals it on interaction. Hover reveal is gated behind@media (hover: hover) and (pointer: fine)so touch devices, which have no hover, aren't left with a permanently grey button;:activeand:focus-visiblereveal unconditionally, so a tap still shows the real color on press. Works across all three appearances (solid/outlined/ghost) and honors+/-tones via--ty-*-neutral-strong/-soft. Orthogonal toflavorandappearanceโ it's a separate axis, not a fourth appearance. -
Auto-contrast foreground tokens โ
--ty-solid-{flavor}-fgis now computed rather than literal, and gains per-tone siblings--ty-solid-{flavor}-soft-fg/-strong-fg. Each derives black or white from the lightness of the fill it actually sits on, using the sameoklch(from โฆ)relative-color syntax the solid fills already use:--ty-solid-primary-soft-fg: oklch(from var(--ty-solid-primary-soft) clamp(0, (var(--ty-solid-fg-threshold) - l) * 1000, 1) 0 0);
One token per fill, not per flavor:
base/-soft/-strongsit at three different lightnesses, so each needs its own decision. -
--ty-solid-fg-threshold(default0.6) โ the lightness crossover at which foregrounds flip from white to black. Raise it to prefer white, lower it to prefer black, or set it to1to restore the old fixed-white behavior globally.
- Every
flavor-(tone-minus) solid button failed WCAG AA โ--ty-solid-soft-l: 0.1lightens the fill for the-tone, but the foreground was pinned towhite, so contrast collapsed. Measured across 6 flavors ร 3 tones ร 8 brand hues, 48 of 144 combinations fell below 4.5:1 (worst:warning-at 2.53:1) โ on the shipped defaults, with no rebranding involved. Now 0 of 144 fail, in both light and dark; worst case 4.61:1. The.tone-plus/.tone-minusbutton rules also setcolornow, not justbackground. ty-wizardstep circles failed WCAG AA in dark mode (completed 3.48:1, active 3.79:1, error 3.93:1) โ the circles paint--ty-wizard-{state}-accent(which defaults to--ty-color-{flavor}) but took their text color from--ty-solid-{flavor}-fg. Those two coincide in light mode but diverge by 0.25 L in dark, where--ty-solid-ldims solid fills against the dark canvas โ so white text sat on a fill 0.25 lighter than the one the color was chosen for. Each circle now derives its foreground from the accent it actually paints (6.04 / 5.54 / 5.34:1 in dark; light unchanged).
- Solid button text is no longer always white. On pale fills โ the
-tone, high--ty-l-*curves, light brand hues โ labels now render black. This is the intended fix, but it is a visible change for anyone who was relying on white. Opt out per flavor with--ty-solid-primary-fg: white, or globally with--ty-solid-fg-threshold: 1.
- Docs site moved to Tailwind v4 (
@tailwindcss/browser@4, replacing the v3 Play CDN). v3 declared its internal--tw-*state vars on*, ::before, ::after, so DevTools repeated that block once per ancestor when inspecting any element; v4 registers them via@propertyinstead (its universal-selector fallback is gated behind an@supportsthat only matches engines without@propertysupport). Visible--tw-*on a given element dropped from 51 to 11. Site-only โ the Tyrell library itself has no Tailwind dependency. TY_GUIDE.mddocumentsmuted;CSS_GUIDE.mdgains an "Auto-contrast foregrounds" section covering the threshold dial and the opt-out paths.
Headline: ty-select mobile-mode fixes โ stale hidden options, focus/keyboard, and external-search results not appearing.
- Mobile options stayed hidden after a previous search, on open โ
openDropdown()(desktop) has always defensively re-shown the options area on every open ("may have been hidden from previous search");openMobileModal()had no equivalent. Added the mobile-appropriate reset (updateTagVisibilityunhides every option on open, matching desktop's intent via the mechanism mobile actually uses โ per-optionhiddenattribute rather than desktop's container-leveldisplay:none). - Mobile search input's focus was racing
showModal()'s own native focus algorithm โ a manualsearchInput.focus()call right aftershowModal()lost the race;document.activeElementended up back on the host element, so keystrokes weren't reaching the input at all despite it looking focused. Replaced with theautofocusHTML attribute, whichshowModal()itself honors as part of its spec'd focusing steps โ no race, and it correctly falls through to the close button when search is hidden. external-searchresults never appeared on mobile after typing โ.mobile-available-section[data-empty="true"] slot { display: none }hides the entire options slot, not just the empty-state message, and onlyupdateMobileSelectedState()(which setsdata-empty) recomputes it. Internal filtering already called that;external-searchbypasses internal filtering entirely and only theMutationObserverwatching for consumer-driventy-optionswaps was left to catch a change โ and it never calledupdateMobileSelectedState(). Result: the consumer's new options were genuinely in the DOM, correctly slotted, just invisible. Fixed by having the observer refresh mobile state too.
Headline: New brand default โ --ty-brand-hue/--ty-brand-chroma now 45/0.125 (amber/orange) instead of 230/0.12 (indigo).
tyrell-brand.cssdefault brand seed changed from hue230/ chroma0.12to hue45/ chroma0.125. Anyone not overriding--ty-brand-hue/--ty-brand-chromathemselves will see their primary/neutral/surface colors shift on upgrade. Docs site's own OKLCH playground default (theming.cljsdefault-seeds,packages/cljs/public/index.html) synced to match.
Headline: ty-select chrome fixes โ font-family, height, and custom-trigger outline.
ty-select/ty-date-pickerlabels (and other unstyled shadow-DOM text) ignored--ty-font-sansโ neither component's:hostpinnedfont-family, so text silently inherited whatever font the host page happened to be using instead of the library's own token, unlikety-inputwhich already pinned it. Both now setfont-family: var(--ty-font-sans)on:host, matchingty-input.ty-selectwith a custom trigger (slot="trigger") still showed the default field outline โsetupTriggerSlot()toggled acustom-triggerclass on.select-stubwhen a consumer slotted their own trigger content, but no CSS ever consumed that class, so the default border/background/padding and the open/focus ring (box-shadow: 0 0 0 3px ...) kept wrapping the custom content regardless..select-stub.custom-triggernow goes fully bare (no border, background, padding, or ring) in every state, as originally intended.
Headline: flavors everywhere โ custom flavors and +/- tones, previously a ty-button/ty-tag exclusive, now work on every flavored component. Also: field and button sizing reworked onto one shared height scale.
- Custom flavors +
+/-tones for all flavored components โty-switch,ty-radio/ty-radio-group,ty-checkbox,ty-input,ty-select,ty-date-picker,ty-copy,ty-tooltip, andty-calendar/ty-calendar-monthnow use the same mechanism asty-button/ty-tag: any identifier is a validflavor, themed from your--ty-color-X(and where relevant--ty-bg-X/--ty-border-X) tokens, with missing tokens degrading to neutral.+/-shade suffixes map to-strong/-softtokens. - Per-instance color override vars on each component โ flavor colors funnel through local vars (
--switch-track,--radio-color,--checkbox-color[-off],--input-accent[-bold]/--input-ring,--select-accent[-bold]/--select-ring,--date-picker-accent[-bold]/--date-picker-ring,--copy-color[-hover]/--copy-bg-hover,--calendar-month-accent/-selected-bg/-selected-color/-selected-hover-bg), so e.g.ty-switch[flavor="brand"] { --switch-track: โฆ }page rules recolor a single instance. See CSS_GUIDE.md โ Per-Component Color Overrides. ty-selectgets an open-state focus ring โ matchingty-input's.focusedtreatment (bolder border + a 3px 15%-alpha ring in the flavor color), the field previously had zero visual escalation when the dropdown opened.ty-copyflavor actually renders โ the documentedflavorattribute previously had no CSS behind it; it now colors the copy button, its hover, and the wrapper hover tint.ty-selectgained aflavorattribute โ previously had none (no error/danger coloring existed for it at all); now colors the field border + hover likety-input, same built-ins/tones/custom-flavor support.ty-calendar/ty-calendar-monthgained aflavorattribute โ colors the selected day and today's number;ty-calendarforwards its flavor to the nestedty-calendar-monthonly (navigation arrows stay neutral chrome).ty-date-pickerforwards its own flavor to the popup calendar it opens, so the two match automatically with no new API.
ty-tooltipno longer gets stuck on a stale flavor โ the popover element is created once and cached; a flavor change made while the tooltip was closed was silently dropped (attributeChangedCallbackonly restyledif (name === 'flavor' && this._open)). It now restyles on every flavor change regardless of visibility.ty-date-picker's popup calendar no longer gets stuck on a stale flavor โrender()only rebuilds (and re-forwardsflavor) while the dialog is closed; changing the flavor while the popup is already open previously left it showing whatever flavor was active when it was opened.updateDisplay()(the open-dialog partial-update path) now also syncsflavoronto the existingty-calendar.- Several icons/badges were hardcoded to primary regardless of the component's own flavor โ
ty-select's loading spinner and compact-mode count badge,ty-date-picker's calendar trigger icon, andty-input's password-reveal focus outline all ignoredflavorand always rendered primary-colored (or, for the calendar icon, a neutral tone that tracks the brand hue and so visually reads as a washed-out primary on the default theme). All four now follow the component's own accent, matchingty-copy's icon (which already did). ty-checkbox's focus ring was hardcoded to--ty-color-primaryregardless of flavor โ now matches the checked-state color via a new--checkbox-ringvar.ty-date-picker's clear-button hover was silently unstyled โ it referenced--ty-color-negative/--ty-bg-negative-faint, neither of which exists as a token; fixed to--ty-color-danger/--ty-bg-danger-soft(clear stays neutral at rest and always warns danger-red on hover โ a destructive utility action, not the field's own flavor).ty-select's compact-mode count badge referenced--ty-text-primary, a nonexistent token, for its text color (silently fell back to unstyled inherited black) โ fixed alongside making it follow flavor.ty-selectrendered 3-4px taller thanty-input/ty-date-pickerat the samesizeโ the.select-stubsize rules carried vertical padding (0.375rem/0.5rem) that stacked on top ofmin-heightinstead of being absorbed by the stub'salign-items: center; zeroed, matchingty-input's existing zero-vertical-padding approach. Fields now measure pixel-identical at every size.
- Unknown flavors no longer coerce โ components previously rewrote unknown flavors to a default (
primary/neutral/dark) with a console warning. Any plain identifier now passes through as a custom flavor; syntactically invalid values fall back to the default look silently. Matches the existingty-button/ty-tagbehavior. ty-tooltipflavor colors are token-derived โ the per-flavor switch statement collapsed to one formula (--ty-bg-X/--ty-color-X-strong/--ty-border-X);dark/light/infostay hand-tuned, and the defaultdarklook routes through the documented--ty-tooltip-bg/--ty-tooltip-colorescape hatch again (it previously clobbered it). Minor normalization:successtooltips now use--ty-bg-successlike every other flavor (was-bold).ty-inputsecondary/warning flavors gained hover states; flavor CSS is generated from one formula, so all built-ins behave identically.- Flavored field borders follow the neutral emphasis ladder โ a flavored
ty-input/ty-select/ty-date-pickerpreviously rested on the full-strength flavor color, leaving no visible difference between rest, hover, and focus. Fields now rest on the flavor's-softshade and escalate to the full color on hover/focus (+ ring), mirroring neutral's faintโsoftโfocus progression;+/-tones shift the whole ladder. The--ty-input-{success,danger,warning}-bordertokens now alias the-softshades accordingly. - React wrappers:
flavorprop types widened to accept+/-shades and custom flavor strings (TySwitch,TyRadio,TyRadioGroup,TyCheckbox,TyInput,TySelect,TyCopy,TyDatePicker,TyTooltip,TyTag,TyCalendar,TyCalendarMonth). - Field size ladder collapsed from five sizes to three โ
ty-input/ty-select/ty-date-pickernow come in exactlysm/md/lg(32/36/40px) via--ty-size-{sm,md,lg}, always the same height across all three field components at a given size. Legacyxs/xlattribute values still work but coerce tosm/lg. ty-button's size ladder now shares a scale with fields instead of running independently underneath them โxsโxlis 24/28/32/36/40px, and the top three steps land exactly on the field ladder (buttonmd= fieldsm,lg= fieldmd,xl= fieldlg), so a button and a field of the paired size sit flush, same height, in a row.xs/smremain button-only steps (compact toolbars, icon actions).
Headline: React 18 className fix โ utility classes on every tyrell-react wrapper now actually apply.
classNameon React wrappers did nothing under React 18 โ React lowercasesclassNameon custom elements into a literalclassname=""attribute no CSS matches, so host-level utility classes (layout, sizing โ most visible onTyScrollContainer) were silently dropped. All 25 wrappers now normalize props throughhostProps(), re-passingclassNameasclass(verbatim pass-through; harmless on React 19).
latest-channel promotion of the TC27โTC29 line (npm tyrell-components + tyrell-react), and the first matching Clojars release: dev.gersak/tyrell and dev.gersak/tyrell-icons 1.0.0-RC12 โ tyrell.react no longer references the removed TyDropdown/TyMultiselect exports (RC11 fails to compile against tyrell-react โฅ TC27).
Headline: date-picker & calendar restyle โ ghost day cells, one strong element (the selected day), proper time inputs โ plus custom flavors for ty-tag/ty-button and the docs/guides purge of the removed components.
- Custom flavors for
ty-tagandty-buttonโ any identifier works as aflavor: the component generates the same token wiring built-in flavors get, pointed at your design tokens (--ty-bg-X,--ty-color-X,--ty-solid-X, โฆ). Buttons degrade toneutralwhen tokens are missing instead of rendering invisible. Injected as a shadow stylesheet, so page-levelty-tag[flavor="X"] { --tag-bg: โฆ }overrides still win. Flavor CSS is generated per flavor/appearance from one formula (completes the generator that partially shipped in TC28).
- Calendar day grid restyle (
ty-calendar-month, and thereforety-calendar/ty-date-picker): day cells are ghost cells โ no border boxes; hover is a soft neutral rounded pill; the selected day is the single filled element; today is an accent-colored semibold number (no background slab โ never collides with hover, selected pill wins when today is picked); weekends are undifferentiated by default (theme back via--ty-calendar-weekend-color); weekday headers muted; day numbers use tabular numerals. All--ty-calendar-*theming hooks kept โ only defaults changed. - Date-picker time section: gray slab replaced by a transparent row with a hairline separator; hour/minute inputs are now proper mini-fields (border, input background, focus ring, tabular digits).
- Docs & guides: every reference to the removed
ty-dropdown/ty-multiselectpurged from README, all guides, and agent instructions (TYCOMPONENT_GUIDE's case study rewritten around the realTySelectAPI).
ty-calendar-navigationatmd(the default size) had zero padding โ onlysm/lgdefined the size CSS variables, sopadding: var(--nav-padding)collapsed to nothing and the month/year header sat flush against the popup's top edge.mdnow has its own block (0.5rem 0.75rempadding,2rembuttons,280pxdefault width), aligned with the month grid.
Headline: card-style ty-select popup search โ a detached bordered field with the ty-input focus ring instead of the fused search header. (Published mid-stream; the calendar restyle and nav-padding fix that were staged alongside landed in TC29.)
ty-selectpopup search: the fused search header is now a detached bordered field inside the panel โ its own rounded border andty-input-style focus ring, whitespace instead of a divider line, magnifier inside the field.- Groundwork for generated per-flavor button CSS (completed in TC29).
Headline: ty-dropdown and ty-multiselect removed (deprecated in TC26) โ the bundle drops ~18%, and ty-select gets the visual polish pass: a real check icon on selected options and card-style list rows.
ty-dropdownandty-multiselect: components,tyrell-components/dropdown+/multiselectsubpath exports, React wrappers (TyDropdown,TyMultiselectand aliases), ClojureScripttyrell.reactdefs, docs pages, and tests. Migration guide lives on the select docs page (ty-select= single by default,multiplefor multi, chips out-of-band viaty-selected-tags). Minified CDN bundle: 74 kB gzip.
- Selection tick is now the lucide
checkicon rendered insidety-option(right-aligned, primary color) โ replaces the text-glyph "โ" overlay ty-select stamped on selected options. Hidden on the clone displayed in the trigger field. Works everywherety-optionrenders. - Card-style option list: rows are inset from the panel edges with rounded corners; hover and keyboard highlight use the neutral surface (
--ty-bg-neutral-soft) instead of primary โ selection stays expressed by the check + bolder text, not a background slab.
ty-tabsmarker no longer glides in from the top-left corner on first display. The active-tab marker's position transition also ran on its very first positioning โ and on the first re-measure after rendering hidden (modals, drawers, toggled panels), where it had been "positioned" at 0,0 with zero-size rects. The marker now snaps (no animation) when it was never positioned or currently has zero rendered size; tab-to-tab switching still animates.
Headline: ty-select โ the select control that replaces ty-dropdown and ty-multiselect. Single select by default with a form-field look matching ty-input; one attribute each for multi-select and the compact toolbar skin; rich options display intact in the field. Both legacy components are deprecated (kept for compatibility, no new features) โ all site demos and framework guides migrated.
multipleโ native<select multiple>semantics. Absent = single select: scalarvalue, picking replaces the selection and closes the popup, one FormData entry. Present = toggle selection, popup stays open, repeatedname=entries (HTMX-ready).compactโ content-hugging trigger for toolbars/filter bars instead of the default full-width field. Single shows the selected label; multiple shows placeholder + count badge.searchableโ popup search row on demand:auto(default) shows it only for 8+ options โ short lists open as pure option menus;searchable/"true"always,"false"never;external-searchalways (the input is its mechanism). Magnifier icon in the search row.- Rich selected display (single) โ the selected option is cloned into the trigger (
slot="selected"+cloned, same mechanism as ty-dropdown), so rich HTML options (icons, prices, flags) display intact. labelattribute onty-optionโ native<option label>semantics: clean display text for multi-select field summaries andty-selected-tagschips when option content is rich HTML. Honored by ty-select display,changeitems, and chip templates.start/endslots โ field adornments (icons, badges), same convention as ty-input;endsits before the chevron. Works in the compact skin.changedetailvalueโ scalar for single select, array for multiple;valuesalways the array form.- Subpath exports โ
tyrell-components/selectandtyrell-components/selected-tags. - React wrappers โ
TySelect+TySelectedTags(typed props, array value coercion, React-18 property bridge);TyOptiongains typedlabelandflavorprops.
type="password"renders a built-in eye / eye-off toggle after the end slot. Toggles the native input's type only โ componenttype, form value, and API unchanged.aria-pressed/aria-labelswap; focus stays in the field.
- Redesigned as a single tick โ full flavor color when checked, faint when unchecked, dash + mid-opacity when
indeterminate(new attribute, native semantics: visual/ARIA only, clicking resolves to checked), grayscale when disabled.
min/max(ISO dates) onty-calendar,ty-calendar-month,ty-calendar-navigation, andty-date-picker(incl. the native mobile input). Out-of-bounds days are disabled and don't emit; navigation clamps โ year jumps land ON the bound month. Out-of-bounds programmatic selections setrangeUnderflow/rangeOverflowconstraint validity. Set cross-wise on two calendars for period pickers.
- Label-click delegation for
ty-checkbox,ty-switch, andty-radio: the click handler now lives on the host, so the documented<label><ty-checkbox></ty-checkbox> text</label>pattern actually toggles/selects. Previously label-text clicks did nothing (the synthetic click never reached the shadow-internal listener). - Reconnect death: checkbox/switch/radio moved in the DOM lost their listeners permanently; they re-arm on reconnect.
- Parse-order init bugs (plain-HTML/SSR pages):
ty-radio-groupinitialvaluenow marks the matching radio (children announce on connect);ty-selected-tags<template>chips now stamp when the template is parsed after the element connects, and initial picker values render as chips (observes the picker'sselectedattributes). - ty-select popup positioning with hundreds of options: the height estimate used the uncapped content height and blindly flipped the popup above the field (clipping off-screen); now capped, and when neither side fits it takes the side with more room.
- ty-select loading panel styling: continues the fused one-panel silhouette instead of floating as a detached card with a see-through gap.
ty-calendarstandalone import: side-effect imports register its child elements (type-only imports leftty-calendar-navigation/ty-calendar-monthundefined fortyrell-components/calendarconsumers).
- Monochrome surfaces by default โ
tyrell-brand.csssurfaces no longer tint toward brand; new--ty-surface-chromaknob (default0) restores the warmth if wanted. Default--ty-brand-chromalowered0.2โ0.12. - Softer input borders โ
--ty-input-border/-hoverone step fainter in both the base and brand layers. - Aliases:
ty-modalalso registered asty-dialog,ty-copyasty-copy-field(same class; React exportsTyDialog/TyCopyField).
Headline: OKLCH brand layer. Drop tyrell-brand.css in next to tyrell.css, set 1โ2 CSS variables, and the entire library rebrands coherently in light and dark mode. The 186 hexes in tyrell.css stay untouched โ the new layer overrides them via the cascade. Companion site demo at /docs/theming lets you drag sliders and copy a ready-to-paste :root snippet.
A single opt-in CSS file. Load AFTER tyrell.css. Five-tier customisation surface:
- Seeds โ
--ty-brand-hue,--ty-brand-chroma. The two primary knobs. Drive primary, secondary, neutral, surfaces, inputs, solid-button fills, focus rings. - Per-flavor hue anchors โ
--ty-success-hue(default 145ยฐ),--ty-warning-hue(75ยฐ),--ty-danger-hue(25ยฐ). Semantic colors stay green/orange/red across brand changes by default; override to retint. - L-curve โ five variables (
--ty-l-strong / -bold / -base / -soft / -faint) plus three bg L-stops shape the emphasis ladder.:rootis tuned for light mode;html.darkredefines with inverted values. - Saturation curve โ five per-shade chroma multipliers (
--ty-c-strong-mult / -bold-mult / -base-mult / -soft-mult / -faint-mult) plus three bg multipliers reshape the per-shade saturation. - Secondary rotation โ
--ty-secondary-offset(default 60ยฐ) โ secondary rotates from brand by this angle. Set to 30 for a close sibling, 120 for triadic, 180 for complement. Or detach entirely via explicit--ty-secondary-hue.
Every other flavor's chroma defaults to calc(var(--ty-brand-chroma) * <ratio>) (success ร1.08, warning ร1.15, danger ร1.31) so a single chroma slider scales every flavor's saturation proportionally while preserving the emphasis hierarchy. Pin any flavor with a literal --ty-{flavor}-chroma: 0.14.
- Cancellable
beforecloseevent on<ty-modal>โ fires before the modal closes. Consumers can callevent.preventDefault()to abort and render their own confirm UI for unsaved-state flows. Detail carriesreason: 'programmatic' | 'backdrop' | 'escape' | 'close-button' | 'native'. .hide({ force: true })on the modal's imperative API โ bypasses the cancellable event. Use after your own confirm UI captures consent.onBeforeCloseReact prop on<TyModal>โ same target-guarded pattern asonOpen/onClose.
tyrell-componentsandtyrell-reacteach log their loaded version once on first import:[tyrell-components] v1.0.0-RC10. Programmatic access viawindow.tyVersion/window.tyReactVersionand the newVERSIONnamed export from each package.
The second-strongest emphasis token's internal name changed: --ty-color-{flavor}-mild is now --ty-color-{flavor}-bold. The word "mild" fought its position in the emphasis ladder (it was more emphatic than base, not less). The new ladder reads cleanly: strong > bold > base > soft > faint. Same applies to --ty-bg-* and related tokens. The public +/- class suffixes are unchanged โ only the internal token names changed, so most consumers see no breakage.
The native confirm() fallback was inflexible and ugly. New consumers should listen for the cancellable beforeclose event (above) and render their own confirm UI.
<!-- before -->
<ty-modal protected>โฆ</ty-modal>
<!-- after โ for the same native-confirm behavior -->
<ty-modal onbeforeclose="if (!confirm('Discard?')) event.preventDefault();">โฆ</ty-modal>--ty-color-accent-*, --ty-bg-accent-*, --ty-border-accent-* tokens and .ty-text-accent / .ty-bg-accent / etc. utility classes are gone. Accent was visually identical to primary after the brand-layer rewrite; the duplicate namespace added noise. Migrate to primary (29 site files migrated as part of this change).
Every component now follows the brand layer for color. Hardcoded rgba/hex values that previously ignored brand changes are routed through var(--ty-color-โฆ) or color-mix(in oklab, var(--ty-color-โฆ), transparent):
- Focus rings on
<ty-input>(6 sites) and<ty-date-picker>(5 sites) โ previously hardcoded to the original Tyrell blue/violet/green/red/amber rgbas. Nowcolor-mix(var(--ty-color-{flavor}) 10%, transparent)in light mode, 15% in dark. --ty-input-shadow-focusโ was hardcodedrgba(59, 130, 246, 0.1). Now routes throughcolor-mix(var(--ty-color-primary), transparent).<ty-tooltip>default flavor โ was#1f2937literal. Nowvar(--ty-color-neutral-strong)so tooltips invert cleanly with theme.- Custom scrollbar thumb/track โ was
rgba(0,0,0,โฆ). Nowcolor-mix(var(--ty-color-neutral-bold), transparent)at varying opacity per state. <ty-scroll-container>edge shadows โ same treatment.<ty-date-picker>surface shadow โ routes tovar(--ty-shadow-lg).- Solid neutral button was a washy mid-grey at L_base. Now routes to
--ty-color-neutral-strong(light) /--ty-color-neutral-faint(dark) โ a dark-grey "default action" button in both modes.
- Core modal
dialog.oncloseguarded withevent.target === dialog. Child popups (<ty-dropdown>,<ty-multiselect>,<ty-date-picker>) dispatch their own bubblingcloseevents; without the guard, the modal's internal<dialog>element treated those as its own close signal and closed the parent modal whenever a child popup inside it closed. - React wrapper event-bubble leak.
TyModal/TyPopup/TyDatePickerlisteners foropen/closefired on bubbled events from child popups (e.g., a<TyDropdown>inside a modal closing would trigger the modal'sonClose, causing the modal to actually close). All three wrappers now guard withevent.target === element. Companion fix to the core-layer guard above.
tyrell-brand.css html.dark block now explicitly re-declares the color/bg/border tokens it computes from the L-curve. Previously tyrell.css's html.dark definitions out-ranked :root brand-layer definitions on selector specificity (0,1,1 vs 0,1,0), so the brand-hue slider did nothing in dark mode.
tooltip.tsreferenced--ty-color-info(noinfoflavor exists in Tyrell) โ the info-flavor variant was removed entirely.tag.tsreferenced--ty-border-mildstrong(a typo for a token that never existed) โ replaced with--ty-border.
All 24 React wrappers migrated to a shared useBooleanProperty helper that fixes two long-standing bugs in how boolean props are bridged to the underlying custom element:
"false"was treated as truthy. Passingdisabled="false"/open="false"(or any non-empty string) was coerced totrue. The helper now correctly maps"false"and"0"tofalse.true โ falseflips didn't propagate on React 18. React 18 removes the attribute when a boolean prop flips tofalse, but custom-element JS-property state can lag behind. The helper imperatively syncs the JS property inuseEffectso the element actually reflects the new value.
Net effect: controlled state via props now works correctly across the board. The biggest practical consequence is the modal/popup workflow:
// This pattern was unreliable before โ modal could refuse to close.
const [open, setOpen] = useState(false);
<TyModal open={open} onClose={() => setOpen(false)}>โฆ</TyModal>
<button onClick={() => setOpen(true)}>Open</button>
<button onClick={() => setOpen(false)}>Close</button>Parent state owns open. Flip it false and the modal closes. Same applies to <TyPopup open>, <TyDatePicker open>, and every other wrapper with boolean props (disabled, required, clearable, loading, readonly, multiline, spin, pulse, and modal close-on-* flags).
For modals that need a confirm-before-close gate, combine this with the new onBeforeClose prop and .hide({force: true}) imperative escape hatch (see "Added โ Modal beforeclose event" above).
Per-component CSS variable count dropped from 43 to 20; hardcoded literals from 11 to 4 (the four remaining are pure geometry โ header padding, progress height, circle size, border width). Removed per-state -bg / -color indirection; step circles read accent variables directly. Routed radius / shadow / transitions through global scale tokens (--ty-radius-lg, --ty-shadow-md, --ty-transition-duration). Replaced legacy --ty-text-* references with brand-coherent --ty-color-neutral-* so wizard labels retint with brand. color-mix switched from in srgb to in oklab.
- Surface-demo borders softened: outer
ty-canvasno longer has a border (it's the page surface, not a card); innerty-contentusesty-border-softinstead ofty-border. Hierarchy reads from surface tones, not heavy lines. - Site header and right-sidebar (TOC) borders moved from
ty-border-strong/ty-bordertoty-border-soft. - CSS Guide architecture section refreshed to reflect 180+ tokens, 6 flavors ร 5 emphasis levels (was "7 colors ร 5 variants"), and the brand-layer story.
- New interactive Theming playground at
/docs/themingโ drag sliders, watch every component on the page retint live, copy a paste-ready:rootsnippet.
ty-wizard now exposes a complete --ty-wizard-* token family for theming without touching Shadow DOM internals.
Accent aliases โ one variable per state, each falling back to a semantic Tyrell color:
--ty-wizard-active-accent: var(--ty-color-primary)
--ty-wizard-completed-accent: var(--ty-color-success)
--ty-wizard-error-accent: var(--ty-color-danger)
--ty-wizard-pending-accent: var(--ty-color-neutral)Flip all "active" chrome to your brand color with a single variable:
ty-wizard { --ty-wizard-active-accent: #6366f1; }Fine-grained tokens โ per-state circle colors, border, text, glow shadows, and layout:
/* Container */
--ty-wizard-bg, --ty-wizard-border, --ty-wizard-radius, --ty-wizard-shadow
/* Per state: completed / active / pending / error */
--ty-wizard-completed-bg, --ty-wizard-completed-border, --ty-wizard-completed-text
--ty-wizard-completed-glow /* color-mix(โฆaccent 10%, transparent) */
/* โฆactive, pending, error follow the same pattern */
/* Progress line */
--ty-wizard-line-bg, --ty-wizard-line-completed-bg
/* Indicators bar */
--ty-wizard-indicators-bg, --ty-wizard-indicators-border
/* Step labels */
--ty-wizard-label-active, --ty-wizard-label-completed
--ty-wizard-label-pending, --ty-wizard-label-error
/* Transitions */
--ty-wizard-transition-duration, --ty-wizard-transition-easingWidth and height are set by the width/height attributes โ they are not public theming tokens.
ty-dropdown and ty-multiselect now show a loading overlay while an async search is in flight. Set loading (boolean attribute) to activate.
Default slot fallback: a spinner + "Searchingโฆ" label, matching the options popup visually (same background, border, radius, shadow).
Custom content: override via slot="loading":
<ty-dropdown loading>
<span slot="loading">Fetching resultsโฆ</span>
โฆ
</ty-dropdown>CSS variables for the loading overlay:
--ty-loader-bg, --ty-loader-border, --ty-loader-radius, --ty-loader-shadowty-dropdown now attaches a MutationObserver to its light-DOM children. When a consumer replaces ty-option children (the standard external-search refresh pattern), the component re-establishes the visual selection for the current value automatically โ no extra code required. The observer is torn down on disconnect.
ty-dropdownโ fixed fullscreen mobile mode layout regressionsty-multiselectโ fixed tag rendering and selection management in mobile fullscreen mode
NPM packages (tyrell-components, tyrell-react) move from the TC* to the RC* scheme to align with the Clojars release cadence. RC6 is the first coordinated cross-registry release; older TC7โTC11 versions remain on NPM but are superseded.
tyrell-react wrappers now use a single needsPropertyBridge helper (packages/react/src/utils/react-version.ts) to gate the imperative property-sync useEffect that exists to work around React 18's unreliable prop-to-property bridging on custom elements. On React 19+ the gate short-circuits and React's native bridging takes over; the workaround stays active on React 18 with no behavior change.
The same gating was applied to seven affected wrappers: TyDropdown, TyInput, TyTextarea, TyDatePicker, TyMultiselect, TyCheckbox, TyCalendar.
Bug fix: TySwitch and TyRadio previously had no property bridging at all, so flipping checked={true} โ checked={false} could leave the visual state stuck under React 18. They now mirror the TyCheckbox pattern (gated on needsPropertyBridge).
ty-icon now accepts an SVG (or any element) as a light-DOM child. When children are present, the registry/name= fallback is hidden automatically โ the slotted content renders instead. The size scale, animations (spin/pulse/tempo), and ::slotted(ty-icon) sizing contract used by ty-button/ty-input/ty-dropdown/ty-date-picker/ty-tag slots all keep working unchanged because they target the <ty-icon> host element.
This is the recommended pattern for server-rendered HTML stacks (HTMX, Datastar, Flask, Django, Rails, Phoenix, PHP) that already produce inline SVG via template partials. No client-side icon registration, no fetch, no FOUC, no CORS:
<ty-icon size="lg">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="..."/></svg>
</ty-icon>
<ty-button size="lg" flavor="primary">
<ty-icon slot="start">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="..."/></svg>
</ty-icon>
Save
</ty-button>
<ty-icon spin>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/></svg>
</ty-icon>Existing name= registry usage is unchanged. When both are provided (slotted SVG and name=), the slotted SVG wins per standard slot semantics.
The delay attribute on input-style components has been renamed to debounce to align with industry-standard terminology (lodash, RxJS, Material UI, etc.) and to clearly distinguish it from single-shot hover delays.
Affected components:
ty-inputโdelayโdebouncety-dropdownโdelayโdebouncety-multiselectโdelayโdebounce
React props renamed accordingly (TyInput, TyDropdown, TyMultiselect).
Not affected:
ty-tooltipโ keeps itsdelayattribute (semantically a single-shot show-delay, not a debounce of rapid events)ty-resize-observerโ already useddebounce
Migration:
<!-- Before -->
<ty-input delay="300" placeholder="Search..."></ty-input>
<ty-dropdown delay="500" not-searchable></ty-dropdown>
<ty-multiselect delay="150" external-search></ty-multiselect>
<!-- After -->
<ty-input debounce="300" placeholder="Search..."></ty-input>
<ty-dropdown debounce="500" not-searchable></ty-dropdown>
<ty-multiselect debounce="150" external-search></ty-multiselect>// React โ before
<TyInput delay={300} />
<TyDropdown delay={500} notSearchable />
<TyMultiselect delay={150} externalSearch />
// React โ after
<TyInput debounce={300} />
<TyDropdown debounce={500} notSearchable />
<TyMultiselect debounce={150} externalSearch />4 new web components added (19 โ 23 total):
ty-wizard+ty-stepโ Multi-step stepper with progress line, step indicators, completion tracking, and horizontal/vertical orientation. Carousel-based navigation between steps.ty-scroll-containerโ Scroll wrapper with edge shadow indicators showing there's more content above/below. Custom scrollbar styling, configurable max-height, horizontal overflow support.ty-resize-observerโ Self-observing utility element that tracks its own dimensions in a global registry (window.tyResizeObserver). Used bytyrell.layoutfor container-aware responsive layouts.
All 4 components include React wrappers in tyrell-react.
- Calendar โ Fullscreen mobile calendar with touch-optimized scrolling, merged from
feature/mobile-calendarbranch - Dropdown โ Mobile fullscreen mode with improved touch interactions
- Multiselect โ Mobile fullscreen mode matching dropdown behavior
- Numeric inputs โ Mobile support for currency, percent, and compact input types
- Buttons โ
wideattribute for full-width mobile-friendly buttons, responsive size guidelines
Complete restructuring of all documentation into guides/ folder.
guides/clj/ROUTING_GUIDE.md- Standalone tyrell.router guide (extracted from Replicant guide)- Full API:
link,navigate!,rendered?,init!, query params - Authorization with roles/permissions, automatic landing redirects
- Framework-agnostic โ works with any ClojureScript library
- Full API:
guides/clj/I18N_GUIDE.md- Internationalization guide for tyrell.i18n- Keyword-based and string-based translations
- Number formatting (currency, percent, compact) via Intl.NumberFormat
- Date/time formatting (presets, relative time) via Intl.DateTimeFormat
- Async loading from URLs (EDN/JSON), Locale protocol
guides/clj/LAYOUT_GUIDE.md- Responsive layout guide for tyrell.layout- Container-aware breakpoints (vs CSS media queries)
with-window,with-container,with-resize-observermacros- Breakpoint queries, responsive values, grid helpers, aspect ratio
guides/DATASTAR_TY_GUIDE.md- Datastar + Tyrell guide (moved from root)
- All guides moved to
guides/with subdirectories:guides/โ general (TY_GUIDE, CSS_GUIDE, DATASTAR_TY_GUIDE)guides/js/โ JavaScript/React (REACT_TY_GUIDE)guides/clj/โ ClojureScript (REPLICANT_TY, ROUTING, I18N, LAYOUT, COMPONENT, CODE_SPLITTING)
- CSS_GUIDE.md rewritten โ pure class reference, surfaces vs backgrounds distinction, dark mode toggle examples, color customization with
:root.dark - REPLICANT_TY_GUIDE.md rewritten
- Removed: state management, folder structure, philosophy sections, routing (moved to own guide), performance tips, testing patterns
- Added:
^jstype hints on all event handlers, event detail table for every component, per-component event examples, slot examples (start/end)
- REACT_TY_GUIDE.md cleaned โ removed philosophy sections, added slot examples
- DATASTAR_TY_GUIDE.md cleaned โ removed philosophy sections, added slot examples
- TY_GUIDE.md cleaned โ removed "Rules" philosophy framing, kept component reference
- CLAUDE.md updated with grouped guide references
packages/cljs/CLJS_GUIDE.cljs(2223 lines) โ superseded by individual guides inguides/clj/- State management sections from all guides
- Folder structure sections from all guides
- "Golden Rule" / philosophy sections from all guides
- Performance tips, testing patterns, common pitfalls sections
ensureStylesduplicate stylesheet accumulation โensureStyles()appended a stylesheet reference on every call, even if already adopted. Components calling it fromrender()(tabs, calendar, dropdown, wizard, modal, etc.) accumulated duplicateadoptedStyleSheetson every property change. Addedincludes()guard to prevent duplicates.
-
COMPONENT_GUIDE.md - Guide for building Web Components with
tyrell.shim- Replicant, UIx, and Reagent integration examples
- Shadow DOM styling with
defstylesmacro - Form participation patterns
- Properties vs Attributes handling
- Hot reload support for development
-
CODE_SPLITTING.md - Guide for shadow-cljs code splitting
- Module configuration for lazy loading
- Lazy component wrapper patterns
- Preloading strategies
- Bundle analysis tips
tyrell.contextnamespace - Restored for calendar component locale support*locale*dynamic var for locale binding
-
Landing README completely rewritten
- ClojureScript-focused with UIx and Replicant examples
- Accurate
tyrell.routerAPI documentation (usinglink,navigate!,rendered?) - Accurate
tyrell.i18nexamples withtyrell.i18n.numberandtyrell.i18n.timenamespaces - Added "Build Your Own Components" section showcasing
tyrell.shim - Added prominent link to live demo at https://gersak.github.io/tyrell
- Removed "Work in Progress" messaging
- Added links to Vanilla JS and React guides at top
-
packages/cljs/README.md updated
- Added component building examples with
tyrell.shim - Updated event handling syntax to use
(.. % -detail -value)
- Added component building examples with
-
Fixed cljdoc-analyzer failure -
tyrell.components.corenamespace not found- Moved
ty/core.cljsfromsrc/ty/tocomponents/ty/ - This file required component namespaces not included in library JAR
- Moved
-
Fixed deps.edn paths
- Corrected
:ty-libalias path from"src/clj"to"src" - Added
"components"to:devalias extra-paths
- Corrected
-
Added missing dependency
- Added
dev.gersak/timing {:mvn/version "0.7.0"}to:devalias
- Added
This is a major milestone release that represents a complete rewrite of the Tyrell component library in TypeScript while maintaining the powerful ClojureScript infrastructure for advanced features.
- Complete TypeScript port of all 19 web components to
packages/core/src/components/button.ts- Button component with flavor and size variantscalendar.ts- Full calendar orchestration with year/month/day navigationcalendar-month.ts- Month view component with custom day renderingcalendar-navigation.ts- Calendar navigation controlscheckbox.ts- Checkbox form controlcopy.ts- Copy-to-clipboard componentdate-picker.ts- Date picker with calendar integrationdropdown.ts- Desktop and mobile dropdown with searchicon.ts- Icon component with registry supportinput.ts- Text input with formatting and validationmodal.ts- Modal dialog with backdrop and focus trappingmultiselect.ts- Multi-selection dropdown with tagsoption.ts- Option component for dropdownspopup.ts- Popup positioning componenttab.ts- Individual tab component (NEW in this release!)tabs.ts- Tab container component (NEW in this release!)tag.ts- Tag/badge component for multiselecttextarea.ts- Multi-line text inputtooltip.ts- Tooltip component
-
New
TyComponentbase class (packages/core/src/base/ty-component.ts)- Unified property/attribute lifecycle management
- Declarative property configuration
- Automatic type coercion (string, boolean, number, object, array)
- Property validation and custom coercion functions
- Property aliases support (e.g.,
not-searchable,not-clearable) - Smart rendering - only triggers when visual properties change
- Built-in ElementInternals support for form association
- Framework compatibility (React, Vue, Reagent property capture)
-
PropertyManager utility (
packages/core/src/utils/property-manager.ts)- Centralized property storage and lifecycle
- Type-safe property access
- Change tracking and validation
- Event emission for property changes
-
Icon registry (
packages/core/src/utils/icon-registry.ts)- Centralized icon storage with Map-based architecture
- Support for custom SVG icon registration
- Tree-shakeable icon imports
- Global
window.tyAPI for easy integration - Icon watcher system for dynamic updates
-
window.ty API for script tag usage:
window.tyrell.icons.register({ iconName: '<svg>...</svg>' }) window.tyrell.icons.get('iconName') window.tyrell.icons.has('iconName') window.tyrell.icons.list() window.tyrell.version // '0.2.0'
- Vite configuration improvements
vite.config.ts- Main build configurationvite.config.dev.ts- Development server (port 3000)vite.config.cdn.ts- CDN build optimization- HMR (Hot Module Reload) for development
- Source maps for debugging
- Terser minification for production
-
Updated React example (
examples/react-nextjs/)- Icon registration pattern with new icon system
- TypeScript integration examples
- Dashboard example page
-
New icons example page showing icon registration patterns
-
Updated Reagent example (
examples/reagent/)- Integration with TypeScript components
- ClojureScript wrapper usage patterns
-
Updated HTMX-Flask example with new icon loading
- New
packages/cljs/structure with organized component wrappers- Separated ClojureScript wrappers from TypeScript implementation
- Build configuration for ClojureScript package
- Component-specific styling organization
-
Migrated from ClojureScript to TypeScript for all UI components
- Maintained ClojureScript infrastructure for routing, i18n, and site
- Components now in
packages/core/src/instead oflib/ty/components/ - TypeScript provides better type safety and broader ecosystem compatibility
-
Dropdown component major improvements
- Implemented on top of TyComponent base class
- Better separation of desktop and mobile implementations
- Improved search functionality
- Enhanced keyboard navigation
- Better styling and visual feedback
-
Multiselect component refactored
- Adjusted to TyComponent implementation
- Improved tag rendering and management
- Better mobile experience
- Enhanced clear functionality
-
Calendar component enhancements
- Better state management
- Improved locale support (130+ languages)
- Enhanced navigation controls
- Better date formatting
- Improved mobile implementations across components
- Better dropdown mobile menu UX
- Modal improvements for mobile viewports
- Touch-friendly interactions
- Responsive layout adjustments
-
Dropdown styling improvements
- Better visual hierarchy
- Improved focus states
- Enhanced hover effects
- Consistent with design system
-
Tabs component styling (
TABS_STYLING.mddocumentation)- Comprehensive styling guide for tabs
- CSS customization patterns
- Site package updates for compatibility with TypeScript components
- Fixed integration issues
- Updated component demos
- Improved code examples
- Better documentation structure
- NPM package structure optimized
- Better tree-shaking support
- Smaller bundle sizes
- Clearer entry points
- Improved TypeScript definitions
- Highlight rendering in dropdown components
- Replicant integration issues with site
- Icon loading errors in various contexts
- Mobile menu display issues in dropdown
- Only odd tabs rendering bug (weird edge case!)
- Form integration improvements for all form components
- Keyboard navigation edge cases in dropdowns
- Focus management in modal and popup components
- Clear button functionality in searchable dropdowns
Before (v0.1.x - ClojureScript):
(ns my-app
(:require [tyrell.components.button :as button]))After (v0.2.0 - TypeScript):
import { TyButton } from 'tyrell-components'
// or
import 'tyrell-components/css/tyrell.css'Before: Icons were built-in and automatically available
After: Icons must be registered explicitly:
import { check, heart } from 'tyrell-components/icons/lucide'
import { registerIcons } from 'tyrell-components/icons/registry'
registerIcons({ check, heart })OR using the global API:
<script>
window.tyrell.icons.register({
'check': '<svg>...</svg>',
'heart': '<svg>...</svg>'
})
</script>Some components now use TyComponent base class with unified property handling:
- Properties now have declarative configuration
- Validation and coercion are automatic
- Attribute aliases (e.g.,
not-searchable) are supported - Form association is built-in for form controls
- TypeScript components moved to
packages/core/ - ClojureScript wrappers moved to
packages/cljs/ - Root
package.jsonremoved (monorepo structure)
BUILDING_WITH_TYCOMPONENT.md- Comprehensive guide for building components with TyComponent base classCSS_GUIDE.md- Tyrell CSS system usage guide (Tyrell for colors, Tailwind for everything else)TYPESCRIPT_DEV_GUIDE.md- Development workflow for TypeScript componentsPROJECT_SUMMARY.md- Updated with TypeScript architecture detailspackages/cljs/README.md- ClojureScript package documentationpackages/cljs/components/ty/components/TABS_STYLING.md- Tabs styling guide
If you were using v0.1.x ClojureScript components:
-
Install the new package:
;; deps.edn {:deps {dev.gersak/tyrell {:mvn/version "0.2.0"}}}
-
Components still work via ClojureScript wrappers in
packages/cljs/ -
Icon registration now required (see Icon System changes above)
-
Install via NPM:
npm install tyrell-components
-
Import components:
import 'tyrell-components/css/tyrell.css' import { TyButton, TyDropdown } from 'tyrell-components'
-
Register icons you need:
import { check, heart } from 'tyrell-components/icons/lucide' import { registerIcons } from 'tyrell-components/icons/registry' registerIcons({ check, heart })
See updated example in examples/react-nextjs/:
- Use components directly as custom elements
- Register icons at app startup
- TypeScript definitions included for full type safety
Use the global window.ty API:
<script src="https://cdn.jsdelivr.net/npm/tyrell-components/dist/tyrell.js"></script>
<script>
window.tyrell.icons.register({ /* your icons */ })
</script>โ
Complete TypeScript migration - All 19 components ported
โ
TyComponent architecture - Unified, maintainable base class
โ
Icon registry system - Flexible, tree-shakeable, easy to use
โ
Framework compatibility - Works with React, Vue, Reagent, vanilla JS
โ
Production ready - Published to NPM as tyrell-components
โ
Improved mobile UX - Better dropdown and modal experiences
โ
Comprehensive docs - Guides for building, styling, and using components
โ
Zero runtime dependencies - Pure web standards
- Additional components (data table, file upload, progress indicators)
- Enhanced a11y (accessibility) features
- More icon library integrations
- Performance optimizations
- Additional framework adapters
- Component testing utilities
- NPM Package:
tyrell-componentsv1.0.0-rc.4 - Clojars Package:
dev.gersak/tyrellv1.0.0-RC4 - License: MIT
- TypeScript: โ Full type definitions included
- Framework Support: React, Vue, Reagent, HTMX, Vanilla JS
This release represents a significant architectural evolution while maintaining backward compatibility through ClojureScript wrappers. Thank you to everyone who contributed feedback and tested the alpha versions!
For detailed component documentation, visit the documentation site or check the README files in each package.