Skip to content

Latest commit

ย 

History

History
1271 lines (830 loc) ยท 120 KB

File metadata and controls

1271 lines (830 loc) ยท 120 KB

Changelog

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.

[1.0.0-TC53] - 2026-09-02

Added

  • 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 drop lg and xl one rung each (640px and 480px breakpoints) โ€” same ladder, no component-specific overrides. Covered by e2e/field-size-ladder.spec.ts, which asserts input, copy, select and date-picker share a height and a label scale at every rung.

  • DEFAULT_SIZE in types/common.ts. The default size for field components is declared once instead of repeated in ten components.

  • value attribute on ty-calendar. The component observed year/month/day but not value, so <ty-calendar value="2026-07-15"> was silently ignored and the calendar rendered the current month. The .value property setter already handled everything; the attribute now routes into it. This also unblocks <TyCalendar value="โ€ฆ" /> in tyrell-react, which passes value as an attribute.

  • display-year / display-month attributes on ty-calendar-month. The year/month API was properties-only. ty-calendar drives its child by property, which hid it โ€” but standalone use, the documentation site's own examples, and tyrell-react's TyCalendarMonth (which maps displayYear/displayMonth onto 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-select and ty-radio-group on the documentation site. Every other ladder component demonstrated xs โ†’ xl; these two demonstrated none.

Changed

  • BREAKING: the default size for every field component moves from md to sm. Affects ty-button, ty-checkbox, ty-radio, ty-radio-group, ty-copy, ty-date-picker, ty-input, ty-select, ty-switch, ty-tag and ty-textarea. A field left without a size attribute is now 32px tall rather than 36px. Set size="md" explicitly to keep the old dimensions. Calendar components are unaffected โ€” they use their own CalendarSize scale.

  • BREAKING (types): TyInput, TySelect and TyDatePicker in tyrell-react accept the full ladder. Their size prop 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-hover aliased --ty-color-neutral-faint / -soft, which are multiplied by --ty-neutral-l-factor (0.8 in 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-border at rest, --ty-border-bold on 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.

Fixed

  • 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-x in 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 across ty-input, ty-copy, ty-select, ty-date-picker and ty-textarea at all five sizes.

  • ty-textarea kept a second, parallel size table that had drifted from the ladder. Its horizontal padding was 10px at xs where the ladder says 8px (a 2px label/value stagger), and its error message was pinned at a hardcoded 12px, off the ladder entirely. Horizontal padding now reads --ty-field-pad-x in 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/xl dropped their height, padding, font and leading below 640px/480px but kept --ty-field-control at the original rung. That variable sizes the in-field button box, which drives the wrapper's content height โ€” so ty-copy at xl rendered 38px on a phone while every other field rendered 36. --ty-field-control and --ty-field-icon now step down with everything else.

Documentation

  • 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 size default as md.
  • 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.

[1.0.0-TC52] - 2026-08-26

Added

  • fixed on ty-tabs โ€” Material's "fixed tabs": the bar is divided equally between the tabs instead of scrolling them. The default (scrollable) mode gives every button a min-width: 120px floor, 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 in fixed mode; slotted label-{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's TyTabs as the presence boolean fixed.

Changed

  • BREAKING: the scrollable tab-button floor drops from min-width: 120px to 72px, 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 new fixed attribute 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: 120px to keep the old look.

  • BREAKING: ty-tabs' transition tokens are namespaced. --transition-duration / --transition-easing were generic enough that any ancestor setting them for its own purposes silently retimed the tabs โ€” and the marker glide, the carousel slide and ty-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-separator is likewise --ty-tabs-separator.

Removed

  • A dead ty-tabs style rule. .marker-wrapper:has(::slotted([slot="marker"])) .default-marker never 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.

Fixed

  • Scrollable ty-tabs no longer squeeze their buttons or ellipsize their labels. Tab buttons are flex items and had the default flex-shrink: 1, so a narrow bar collapsed them toward the min-width floor โ€” "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 now flex-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 to fixed, which is the mode that genuinely squeezes. Previously masked by the 120px floor, which most labels fit inside.

  • ty-tabs no longer jerks the strip backwards when you activate a tab. Changing active ran a full render, which rebuilds the buttons and the "โ€ฆ" trigger. Taking the trigger out momentarily widens the strip, so the browser clamps scrollLeft down 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. updateOverflow also saves and restores scrollLeft around its own trigger churn, for the resize path.

  • ty-tabs no 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, where min-width: 120px alone 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/right were 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 now min(28px, distance remaining).

[1.0.0-TC51] - 2026-08-26

Added

  • --tabs-separator on ty-tabs โ€” the color of the line between the tab bar and the panel, defaulting to --ty-border. Set it to transparent to drop the line entirely, which is what you usually want alongside a custom slot="marker" that already carries the active state.

Changed

  • ty-tabs with placement="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-tabs only clips its tab strip when the tabs actually overflow. The strip carries overflow-x: auto so tabs can scroll, which (per spec) forces vertical clipping too, and its edge-fade mask-image clips as well โ€” together they cut off anything a custom slot="marker" painted outside its box, most visibly a box-shadow glow. When every tab fits there is nothing to scroll, so the strip now drops both. Overflow detection moved from scrollWidth to the buttons' own extent, since an unclipped element has no scrolling box and scrollWidth would just echo clientWidth. Clipping remains the pre-measurement default, so overflowing tabs still can't spill on first render.

  • The default ty-tabs active 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-tabs separator line is now a pseudo-element, so the active marker sits on it rather than beside it. The line was border-bottom on 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::after with the strip lifted above it. ::part(buttons-container) no longer carries a border; a consumer who overrode border-bottom/border-top there to restyle or remove the separator should target the ::after instead.

Fixed

  • A slot="label-{id}" element nested inside a ty-tab no longer blanks that tab's button. Rich labels must be direct children of ty-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-tab exposes only an unnamed slot). Detection is now scoped to direct children, so a misplaced label falls back to the label attribute instead of vanishing.

[1.0.0-RC14] - 2026-08-24

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.

[1.0.0-TC50] - 2026-08-20

Added

  • preventEscape / preventOutsideClick props on tyrell-react's TyModal. 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 deprecated closeOnEscape / closeOnOutsideClick props keep working and now carry @deprecated JSDoc (as do the core element's property mirrors), so IDEs flag the migration.

[1.0.0-TC49] - 2026-08-20

Added

  • prevent-escape / prevent-outside-click on ty-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 mirrors preventEscape / preventOutsideClick take real booleans (true sets the attribute, false removes it, like native disabled). A fully guarded modal has no built-in โœ• โ€” consumers must render their own keyboard-reachable close control (WCAG 2.1.2).

Fixed

  • 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 cancel and closed the modal with reason: "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.

Deprecated

  • close-on-escape / close-on-outside-click attributes (and the closeOnEscape / closeOnOutsideClick properties). 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.

[1.0.0-TC48] - 2026-08-20

Changed

  • The default primary seed moved from --ty-primary-hue: 285 / --ty-primary-chroma: 0.1 to 252 / 0.08 (light), 226 / 0.065 (dark). Anyone consuming tyrell-theme.css without 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-chroma are 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-hue only ever drove primary (semantic hues are fixed constants, surface tint defaults to 0, neutral is achromatic), while the calc(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-hue for 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}-mult chroma 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 with l-factor far 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 now oklch(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 of l-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-X lines 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 extreme l-factor values); anyone who pinned the removed --ty-l-bg-*/--ty-c-bg-*-mult dials directly needs to move that override to the corresponding --ty-a-bg-* alpha instead.

Added

  • Enter motion for every floating surface, on one shared recipe. ty-select, ty-date-picker and ty-popup enter 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-overshoot cubic-bezier(0.34, 1.56, 0.64, 1)). ty-tooltip gets 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 was display:none the 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-zoom attribute on ty-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 for position: fixed descendants; apps with fixed chrome should skip it.

Fixed

  • -start/-end placements now align the VISIBLE surface, not the shadow-room wrapper. ty-popup's dialog carries a 16px transparent wrap; cross-axis alignment ignored it on top-*/bottom-* (panel 16px off-flush) and had the compensation sign flipped on left-*/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 (native defaultValue/defaultChecked semantics) โ€” previously checkboxes/switches stayed toggled, inputs reset to '' instead of their value attribute, and resetting a form wiped the value of every ty-radio in a group (the base class reset the wrong property on non-form children). Also fixed: re-enabling a disabled ty-radio-group re-enables its radios; ty-file-upload applies its accept filter to drag-and-drop (the picker dialog was the only thing filtering) and unnamed uploads are excluded from FormData like native inputs; ty-tabs/ty-wizard render dynamically added ty-tab/ty-step children (light DOM is now observed); ty-resize-observer migrates its registry entry on id rename; ty-selected-tags stamps text-only <template>s; parseNumericValue treats 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-select and ty-date-picker now share one 12-value vocabulary. A placement is a side plus an optional alignment: the bare side centers on the anchor, -start puts the leading edges flush, -end the trailing edges. For top/bottom that axis is horizontal (bottom-start = below, left edges flush); for left/right it is vertical (left-start = to the left, top edges flush). All twelve combinations are valid.

    The Placement type already declared all twelve, but nothing honored them. popup.ts and tooltip.ts each hand-wrote a four-entry fallback chain matching only the bare sides, so placement="top-start" typechecked, fell through the if-chain to the default, and was silently discarded. Replaced both with a shared preferenceChain() (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-start and bottom-end need 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-picker gets a placement attribute; ty-select's align gains center. 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 accept bottom*/top* directly and degrade left-*/right-* to auto-side while keeping the alignment. Implemented via a new side option and center alignment on computeAnchoredPosition(), plus placementToAnchored() to translate a Placement into that engine's inputs. ty-select's existing align keeps working standalone; placement takes precedence when both are set.

Fixed

  • left-start and right-start were dead values โ€” identical to bare left/right. Both were configured vertical: 'center' in the placements table, and VerticalAlign had no 'start' member to express anything else, so neither could ever top-align. Meanwhile calculatePlacement() already contained a working start-alignment branch that was unreachable โ€” nothing ever set vertical to something outside 'center'/'end'. Added 'start' to the union, pointed both entries at it, and gave that branch the same containerPadding inset its end sibling already had so the two alignments are symmetric about the anchor. Visual change: markup using left-start/right-start moves from centered to top-aligned. Use bare left/right for the old centered behavior.

  • ty-icon rendered as an oval inside shrink-wrapping wrappers (regression in TC46). TC46 changed :host from display: flex to inline-flex so 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-full badge, a chip โ€” inherits the parent's line-height strut 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 to display: 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 wrapper inline-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 hardcoded system-ui, sans-serif (ร—3), inherit (ร—3), and nothing at all. Since --ty-font-sans resolves 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 now inherit.

    Additionally, font-family: inherit on :host does not reach native form controls inside the shadow root: the UA stylesheet sets a font on button/input/textarea, and that beats inheritance. So ty-button's inner <button> had always rendered in the system font even when the host resolved correctly. Explicit font-family: inherit added to the native button, input, textarea and both select search inputs.

  • secondary was still advertised in the React package. All 14 tyrell-react components declared type BuiltinFlavor = 'primary' | 'secondary' | โ€ฆ, so TypeScript accepted and autocompleted a flavor removed from the core in TC37 โ€” it silently degraded to neutral at runtime, meaning the published .d.ts misrepresented the API. Stripped from all 14 and from the dead union documented in packages/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-threshold shipped at 0.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) โ€” so 0.62 sat 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 to 0.57. Swept across all 30 fills (15 ร— light/dark): AA failures 2 โ†’ 0, worst case 3.95:1 โ†’ 4.83:1, and only warning+ 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: white to keep the old look, or set --ty-solid-fg-threshold: 1 to force white everywhere. Same value applied to ty-wizard's step circles, which carried their own 0.6 fallback 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.

[1.0.0-TC46] - 2026-08-11

Changed

  • 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 the files field but referenced ../../src/*.ts, which is never published, with no sourcesContent embedded โ€” consumers could not resolve them under any circumstance. sourceMap and declarationMap are now off for the published build.

    tsc was keeping comments. Unlike the CDN bundle (Terser, format.comments: false) the NPM lib/ build had no removeComments, so every source comment shipped. Now a two-pass build: tsconfig.lib.json emits .js with removeComments, and a new tsconfig.lib.dts.json emits declarations with JSDoc intact, so consumer hover documentation is unaffected. (tsconfig.lib.json also had duplicate outDir and declarationMap keys; 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 @example blocks (already in guides/TY_GUIDE.md and the docs site), pure restatement, // ==== banner rules, dead commented-out code, and stale PORTED FROM: / (Phase N) scaffolding. Rationale comments were preserved; the emitted .js for 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.ts claimed position: fixed when the implementation uses overflow: hidden on <html>, and input.ts had a /* No gap by default */ note directly above gap: 0.5rem.

Known issues

  • ty-icon renders as an oval inside shrink-wrapping wrappers such as .rounded-full badges. Fixed in TC47.

[1.0.0-TC45] - 2026-08-08

Added

  • ty-select gets an align attribute for horizontal popup anchoring. The popup already auto-flips vertically (below/above, whichever has more room) via the shared computeAnchoredPosition() engine, but horizontally it was hardcoded to the trigger's left edge โ€” fine for a full-width field, but wrong for a slot="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 new align option on computeAnchoredPosition() (utils/positioning.ts), shared with ty-date-picker's positioning engine though not yet exposed there. Also added to the React wrapper (TySelectProps.align).

  • ty-select gets a public clear() method and a clearable attribute. clear() empties the selection programmatically and fires change (action: 'clear') โ€” added for slot="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. a ty-button end-slot icon) directly to it. No-op if nothing is selected, same convention the existing deselectValue() already follows. clearable (boolean, default true โ€” matches ty-date-picker) renders a built-in ร— button in the default field and compact skins once something is selected; it's automatically absent under slot="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 while disabled/readonly. Use not-clearable to 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 typed TySelectElement ref interface exposing clear().

    Caught along the way: .select-stub is flex-wrap: wrap, and .dropdown-placeholder (the joined-labels/selected-text element) used flex: 1 1 auto โ€” flex-wrap's line-assignment uses each item's hypothetical (pre-shrink) size, which for flex-basis: auto on white-space: nowrap text 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-clear as 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 placeholder flex-basis: 0% instead of auto (truncation behavior is identical; only the wrap-line-fit calculation changes). The .compact skin needed the opposite treatment โ€” it deliberately relies on flex-basis: auto for its own content-hugging sizing (flex-grow: 0, so with a 0% basis and no way to grow back out, its text collapsed to zero width) โ€” so it now sets flex: 0 1 auto explicitly rather than inheriting the field skin's basis.

  • ty-date-picker gets a public clear() method, for parity with ty-select. Its existing clearable button (already shipped) called a private clearValue() โ€” 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 fires change (detail.source: 'clear'), same as clicking the button. Also added the React wrapper's typed TyDatePickerElement ref interface exposing clear(), and documented clearable and the change event payload in TY_GUIDE.md โ€” both were previously undocumented there despite clearable already existing.

[1.0.0-TC44] - 2026-08-07

Fixed

  • 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-floating instead of --ty-input-border/--ty-input-bg โ€” deliberately faint tokens tuned for resting form fields, not floating panels โ€” but four spots in select.ts (the header's own outer border/background, and the options panel's outer-facing top edge whenever it's exposed: search hidden, or position-above) and one in select-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-bg background 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-border for 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.

[1.0.0-TC43] - 2026-08-07

Changed

  • BREAKING: tyrell.css no 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 (light html {} + dark html.dark/[data-theme="dark"]) moved to a new file, tyrell-colors-static.css. Reason: tyrell-theme.css, loaded after tyrell.css in 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.css alone now has zero color tokens โ€” pair it with tyrell-theme.css (dynamic, recommended) or the new tyrell-colors-static.css (static, no theme engine) for colors. Every published example and guide that loaded tyrell.css alone now also loads tyrell-theme.css; see CSS_GUIDE.md โ†’ Color Customization.

[1.0.0-TC42] - 2026-08-07

Added

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-button hardcoded font-weight: 590 (semibold) and ty-tag 510, 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 in tyrell.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 (default 0.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.25 light / 0.5 dark), giving dark ink a real lit edge against a near-black canvas. (An earlier pass also added a top-edge sheen โ€” inset 0 1px 0 highlight โ€” 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-color pins the border to any color, --ty-solid-border-width changes 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.15 light / 0.88 dark). Auto-contrast flips the foreground for free, and the inversion animates through theme transitions like every other dial. Outlined and ghost neutral+ read the same ink dial โ€” unlike every other flavor, where each appearance reads its own -strong text 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.muted follows the same split. Hover/active on neutral+ 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-l add to the base fill's L before tones/hover/active derive; warning gets +0.1 in light (0 dark โ€” 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 to 0 (no change), but the dial is there for any flavor that needs it.

  • Dark-mode solid hover/active offsets raised (--ty-solid-hover-l 0.04 โ†’ 0.07, --ty-solid-active-l 0.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-visible always resolve text (and, for outlined, border) to the -strong tier: soft and base both rise to the same peak color on interaction; + is already there at rest, so it's a no-op. Composes with muted'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.

Fixed

  • ty-select opened with two simultaneous focus rings. The stub's deliberate open-state ring (added TC-era to match ty-input's .focused escalation) applied whenever the popup was open โ€” but the popup's search row auto-focuses on open and carries its own real :focus ring, 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. The custom-trigger skin 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's slot="trigger" hit area extended well past the visible trigger. The internal .select-stub.custom-trigger rule set display: inline-block but never overrode the inherited width: 100% from the base .select-stub rule โ€” unlike its .compact sibling 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 missing width: 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.

[1.0.0-TC41] - 2026-08-06

Fixed

  • .ty-theme-switching (TC40) didn't actually reach ty-* 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 every ty-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-switching present, ty-tag and ty-input's internal elements still reported transitionDuration: 0.15s and fired transitionrun events. Fixed with the mechanism that actually crosses that boundary: .ty-theme-switching now 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 through var(--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 in tyrell.css) got the same treatment centrally, covering ty-tag, ty-option, ty-modal, and several ty-select internals without touching those files individually. Re-verified: the same repro that showed 0 suppressed events on ty-tag/ty-input in TC40 now shows 0 transitionrun events during the switch and correct restoration (0s mid-switch โ†’ 0.15s after) for both.

[1.0.0-TC40] - 2026-08-06

Added

  • .ty-theme-switching opt-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 on background-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 separate transitionrun events with finish times scattered across a 449ms window, which read as jitter rather than one smooth wash. Add ty-theme-switching to 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.

[1.0.0-TC39] - 2026-08-06

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).

Fixed

  • ty-select never declared :host { display }, so it defaulted to the browser's display: inline for every skin (default field, compact, slot="trigger"). Inline boxes ignore vertical margin entirely, which is why pairing a select with ty-selected-options/ty-selected-tags below 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% }, matching ty-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-tags now listens for the picker's open/close events (already emitted by ty-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.

Changed

  • ty-selected-tags renamed to ty-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 selected ty-options โ€” matching how the rest of the library names things. ty-selected-tags is re-registered as a subclass and kept working indefinitely (same convention as ty-modal/ty-dialog); the JS class TySelectedTags (core and React packages) is likewise kept as an alias export of TySelectedOptions. No migration required โ€” this is additive, not a removal.

[1.0.0-TC38] - 2026-08-06

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.

Changed (BREAKING)

  • packages/core/css/tyrell-brand.css renamed to tyrell-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-chroma and 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>/import path. Updated everywhere in this repo: build scripts, e2e fixture, all framework guides, README, and the docs-site symlink.

Fixed

  • tyrell.css alone (no theme engine loaded) no longer ships broken contrast. This was worse than a missing enhancement: the single --ty-solid-{flavor}-fg per 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), and tone-plus/tone-minus buttons had no fg token at all, which resolves to CSS's initial value rather than failing safe โ€” measured as black text on a dark navy primary+ fill. Added the missing per-tone -soft-fg/-strong-fg tokens 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.

Docs

  • Every "load Tyrell" snippet across the guides, README, and framework docs now shows tyrell-theme.css alongside tyrell.css by 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 linked tyrell-brand.css and set a dead --ty-secondary-offset seed 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.

[1.0.0-TC37] - 2026-08-06

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.

Removed (BREAKING)

  • The secondary flavor โ€” from the brand engine, the base stylesheet (tokens + ty-text-secondary*/ty-bg-secondary*/ty-border-secondary/selection utilities), and the components' built-in FLAVORS list. 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 as brand + 60ยฐ that most projects had to re-tune anyway. Markup using flavor="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.

Added

  • 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">. dark is 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 the color-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-tuned color-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 @property as 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 (default 0.45s; set 0s to opt out); reduced-motion users never get the animation; browsers without @property simply 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 :root overrides (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.

Fixed

  • Solid buttons: tone-aware hover/active fills โ€” the -hover/-active states always derived from the base fill, so hovering a flavor- (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.08 interaction 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:hover out-ranked .focused by 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), matching ty-date-picker's existing :hover:not(.open) pattern.
  • ty-select: labelโ†’field gap now matches ty-input โ€” .select-container carried a gap: 0.25rem that stacked on the shared label's margin-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).

Docs

  • 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-hover is 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-soft for outlined/ghost) and the prose explains the instance-override vs token distinction, plus the one-line --ty-X-seed path when the brand layer is loaded.

[1.0.0-TC36] - 2026-08-05

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.

Changed

  • Neutral is achromatic by default โ€” --ty-neutral-hue/--ty-neutral-chroma are now 0/0 instead 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-border were 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/-faint no 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-factor no longer moves app borders, and borders are tunable without touching text. Per-flavor accent borders (--ty-border-{flavor}) intentionally remain ink aliases.

Internal

  • tyrell-brand.css dark-mode formula mirror deleted (~250 lines) โ€” SECTION 2 formulas are now declared once at html:root (out-ranking tyrell.css's light tokens by specificity and its html.dark hexes by source order), and the html.dark block holds only genuine per-mode data: the surface ladder, focus-ring alpha, the --ty-bg-neutral-strong/-faint pins, and the --ty-solid-neutral ink pin. Verified byte-equivalent before the neutral change: 323 computed tokens ร— 2 modes, zero differences.

[1.0.0-TC35] - 2026-08-05

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.

Added

  • muted attribute on ty-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; :active and :focus-visible reveal 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 to flavor and appearance โ€” it's a separate axis, not a fourth appearance.

  • Auto-contrast foreground tokens โ€” --ty-solid-{flavor}-fg is 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 same oklch(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 / -strong sit at three different lightnesses, so each needs its own decision.

  • --ty-solid-fg-threshold (default 0.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 to 1 to restore the old fixed-white behavior globally.

Fixed

  • Every flavor- (tone-minus) solid button failed WCAG AA โ€” --ty-solid-soft-l: 0.1 lightens the fill for the - tone, but the foreground was pinned to white, 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-minus button rules also set color now, not just background.
  • ty-wizard step 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-l dims 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).

Changed

  • 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

  • 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 @property instead (its universal-selector fallback is gated behind an @supports that only matches engines without @property support). Visible --tw-* on a given element dropped from 51 to 11. Site-only โ€” the Tyrell library itself has no Tailwind dependency.
  • TY_GUIDE.md documents muted; CSS_GUIDE.md gains an "Auto-contrast foregrounds" section covering the threshold dial and the opt-out paths.

[1.0.0-TC34] - 2026-07-22

Headline: ty-select mobile-mode fixes โ€” stale hidden options, focus/keyboard, and external-search results not appearing.

Fixed

  • 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 (updateTagVisibility unhides every option on open, matching desktop's intent via the mechanism mobile actually uses โ€” per-option hidden attribute rather than desktop's container-level display:none).
  • Mobile search input's focus was racing showModal()'s own native focus algorithm โ€” a manual searchInput.focus() call right after showModal() lost the race; document.activeElement ended up back on the host element, so keystrokes weren't reaching the input at all despite it looking focused. Replaced with the autofocus HTML attribute, which showModal() 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-search results 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 only updateMobileSelectedState() (which sets data-empty) recomputes it. Internal filtering already called that; external-search bypasses internal filtering entirely and only the MutationObserver watching for consumer-driven ty-option swaps was left to catch a change โ€” and it never called updateMobileSelectedState(). Result: the consumer's new options were genuinely in the DOM, correctly slotted, just invisible. Fixed by having the observer refresh mobile state too.

[1.0.0-TC33] - 2026-07-21

Headline: New brand default โ€” --ty-brand-hue/--ty-brand-chroma now 45/0.125 (amber/orange) instead of 230/0.12 (indigo).

Changed

  • tyrell-brand.css default brand seed changed from hue 230 / chroma 0.12 to hue 45 / chroma 0.125. Anyone not overriding --ty-brand-hue/--ty-brand-chroma themselves will see their primary/neutral/surface colors shift on upgrade. Docs site's own OKLCH playground default (theming.cljs default-seeds, packages/cljs/public/index.html) synced to match.

[1.0.0-TC32] - 2026-07-21

Headline: ty-select chrome fixes โ€” font-family, height, and custom-trigger outline.

Fixed

  • ty-select / ty-date-picker labels (and other unstyled shadow-DOM text) ignored --ty-font-sans โ€” neither component's :host pinned font-family, so text silently inherited whatever font the host page happened to be using instead of the library's own token, unlike ty-input which already pinned it. Both now set font-family: var(--ty-font-sans) on :host, matching ty-input.
  • ty-select with a custom trigger (slot="trigger") still showed the default field outline โ€” setupTriggerSlot() toggled a custom-trigger class on .select-stub when 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-trigger now goes fully bare (no border, background, padding, or ring) in every state, as originally intended.

[1.0.0-TC31] - 2026-07-20

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.

Added

  • 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, and ty-calendar/ty-calendar-month now use the same mechanism as ty-button/ty-tag: any identifier is a valid flavor, 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/-soft tokens.
  • 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-select gets an open-state focus ring โ€” matching ty-input's .focused treatment (bolder border + a 3px 15%-alpha ring in the flavor color), the field previously had zero visual escalation when the dropdown opened.
  • ty-copy flavor actually renders โ€” the documented flavor attribute previously had no CSS behind it; it now colors the copy button, its hover, and the wrapper hover tint.
  • ty-select gained a flavor attribute โ€” previously had none (no error/danger coloring existed for it at all); now colors the field border + hover like ty-input, same built-ins/tones/custom-flavor support.
  • ty-calendar / ty-calendar-month gained a flavor attribute โ€” colors the selected day and today's number; ty-calendar forwards its flavor to the nested ty-calendar-month only (navigation arrows stay neutral chrome). ty-date-picker forwards its own flavor to the popup calendar it opens, so the two match automatically with no new API.

Fixed

  • ty-tooltip no 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 (attributeChangedCallback only restyled if (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-forwards flavor) 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 syncs flavor onto the existing ty-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, and ty-input's password-reveal focus outline all ignored flavor and 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, matching ty-copy's icon (which already did).
  • ty-checkbox's focus ring was hardcoded to --ty-color-primary regardless of flavor โ€” now matches the checked-state color via a new --checkbox-ring var.
  • 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-select rendered 3-4px taller than ty-input/ty-date-picker at the same size โ€” the .select-stub size rules carried vertical padding (0.375rem/0.5rem) that stacked on top of min-height instead of being absorbed by the stub's align-items: center; zeroed, matching ty-input's existing zero-vertical-padding approach. Fields now measure pixel-identical at every size.

Changed

  • 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 existing ty-button/ty-tag behavior.
  • ty-tooltip flavor 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/info stay hand-tuned, and the default dark look routes through the documented --ty-tooltip-bg/--ty-tooltip-color escape hatch again (it previously clobbered it). Minor normalization: success tooltips now use --ty-bg-success like every other flavor (was -bold).
  • ty-input secondary/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-picker previously rested on the full-strength flavor color, leaving no visible difference between rest, hover, and focus. Fields now rest on the flavor's -soft shade 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}-border tokens now alias the -soft shades accordingly.
  • React wrappers: flavor prop 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-picker now come in exactly sm/md/lg (32/36/40px) via --ty-size-{sm,md,lg}, always the same height across all three field components at a given size. Legacy xs/xl attribute values still work but coerce to sm/lg.
  • ty-button's size ladder now shares a scale with fields instead of running independently underneath them โ€” xsโ€“xl is 24/28/32/36/40px, and the top three steps land exactly on the field ladder (button md = field sm, lg = field md, xl = field lg), so a button and a field of the paired size sit flush, same height, in a row. xs/sm remain button-only steps (compact toolbars, icon actions).

[1.0.0-TC30] - 2026-07-10

Headline: React 18 className fix โ€” utility classes on every tyrell-react wrapper now actually apply.

Fixed

  • className on React wrappers did nothing under React 18 โ€” React lowercases className on custom elements into a literal classname="" attribute no CSS matches, so host-level utility classes (layout, sizing โ€” most visible on TyScrollContainer) were silently dropped. All 25 wrappers now normalize props through hostProps(), re-passing className as class (verbatim pass-through; harmless on React 19).

[1.0.0-RC12] - 2026-07-10

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).

[1.0.0-TC29] - 2026-07-10

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.

Added

  • Custom flavors for ty-tag and ty-button โ€” any identifier works as a flavor: 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 to neutral when tokens are missing instead of rendering invisible. Injected as a shadow stylesheet, so page-level ty-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).

Changed

  • Calendar day grid restyle (ty-calendar-month, and therefore ty-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-multiselect purged from README, all guides, and agent instructions (TYCOMPONENT_GUIDE's case study rewritten around the real TySelect API).

Fixed

  • ty-calendar-navigation at md (the default size) had zero padding โ€” only sm/lg defined the size CSS variables, so padding: var(--nav-padding) collapsed to nothing and the month/year header sat flush against the popup's top edge. md now has its own block (0.5rem 0.75rem padding, 2rem buttons, 280px default width), aligned with the month grid.

[1.0.0-TC28] - 2026-07-10

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.)

Changed

  • ty-select popup search: the fused search header is now a detached bordered field inside the panel โ€” its own rounded border and ty-input-style focus ring, whitespace instead of a divider line, magnifier inside the field.
  • Groundwork for generated per-flavor button CSS (completed in TC29).

[1.0.0-TC27] - 2026-07-10

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.

Removed

  • ty-dropdown and ty-multiselect: components, tyrell-components/dropdown + /multiselect subpath exports, React wrappers (TyDropdown, TyMultiselect and aliases), ClojureScript tyrell.react defs, docs pages, and tests. Migration guide lives on the select docs page (ty-select = single by default, multiple for multi, chips out-of-band via ty-selected-tags). Minified CDN bundle: 74 kB gzip.

Changed โ€” ty-select popup

  • Selection tick is now the lucide check icon rendered inside ty-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 everywhere ty-option renders.
  • 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.

Fixed

  • ty-tabs marker 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.

[1.0.0-TC26] - 2026-07-09

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.

Added โ€” ty-select

  • multiple โ€” native <select multiple> semantics. Absent = single select: scalar value, picking replaces the selection and closes the popup, one FormData entry. Present = toggle selection, popup stays open, repeated name= 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-search always (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.
  • label attribute on ty-option โ€” native <option label> semantics: clean display text for multi-select field summaries and ty-selected-tags chips when option content is rich HTML. Honored by ty-select display, change items, and chip templates.
  • start / end slots โ€” field adornments (icons, badges), same convention as ty-input; end sits before the chevron. Works in the compact skin.
  • change detail value โ€” scalar for single select, array for multiple; values always the array form.
  • Subpath exports โ€” tyrell-components/select and tyrell-components/selected-tags.
  • React wrappers โ€” TySelect + TySelectedTags (typed props, array value coercion, React-18 property bridge); TyOption gains typed label and flavor props.

Added โ€” ty-input password reveal

  • type="password" renders a built-in eye / eye-off toggle after the end slot. Toggles the native input's type only โ€” component type, form value, and API unchanged. aria-pressed/aria-label swap; focus stays in the field.

Added โ€” ty-checkbox

  • 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.

Added โ€” calendar bounds

  • min / max (ISO dates) on ty-calendar, ty-calendar-month, ty-calendar-navigation, and ty-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 set rangeUnderflow/rangeOverflow constraint validity. Set cross-wise on two calendars for period pickers.

Fixed

  • Label-click delegation for ty-checkbox, ty-switch, and ty-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-group initial value now 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's selected attributes).
  • 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-calendar standalone import: side-effect imports register its child elements (type-only imports left ty-calendar-navigation/ty-calendar-month undefined for tyrell-components/calendar consumers).

Changed

  • Monochrome surfaces by default โ€” tyrell-brand.css surfaces no longer tint toward brand; new --ty-surface-chroma knob (default 0) restores the warmth if wanted. Default --ty-brand-chroma lowered 0.2 โ†’ 0.12.
  • Softer input borders โ€” --ty-input-border/-hover one step fainter in both the base and brand layers.
  • Aliases: ty-modal also registered as ty-dialog, ty-copy as ty-copy-field (same class; React exports TyDialog/TyCopyField).

[1.0.0-RC10] - 2026-06-02

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.

Added โ€” Brand layer (packages/core/css/tyrell-brand.css)

A single opt-in CSS file. Load AFTER tyrell.css. Five-tier customisation surface:

  1. Seeds โ€” --ty-brand-hue, --ty-brand-chroma. The two primary knobs. Drive primary, secondary, neutral, surfaces, inputs, solid-button fills, focus rings.
  2. 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.
  3. L-curve โ€” five variables (--ty-l-strong / -bold / -base / -soft / -faint) plus three bg L-stops shape the emphasis ladder. :root is tuned for light mode; html.dark redefines with inverted values.
  4. 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.
  5. 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.

Added โ€” Modal beforeclose event

  • Cancellable beforeclose event on <ty-modal> โ€” fires before the modal closes. Consumers can call event.preventDefault() to abort and render their own confirm UI for unsaved-state flows. Detail carries reason: '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.
  • onBeforeClose React prop on <TyModal> โ€” same target-guarded pattern as onOpen/onClose.

Added โ€” Version banners

  • tyrell-components and tyrell-react each log their loaded version once on first import: [tyrell-components] v1.0.0-RC10. Programmatic access via window.tyVersion / window.tyReactVersion and the new VERSION named export from each package.

Changed (breaking) โ€” mild โ†’ bold rename

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.

Removed (breaking) โ€” protected attribute on <ty-modal>

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>

Removed (breaking) โ€” accent flavor

--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).

Fixed โ€” Brand-coherence

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. Now color-mix(var(--ty-color-{flavor}) 10%, transparent) in light mode, 15% in dark.
  • --ty-input-shadow-focus โ€” was hardcoded rgba(59, 130, 246, 0.1). Now routes through color-mix(var(--ty-color-primary), transparent).
  • <ty-tooltip> default flavor โ€” was #1f2937 literal. Now var(--ty-color-neutral-strong) so tooltips invert cleanly with theme.
  • Custom scrollbar thumb/track โ€” was rgba(0,0,0,โ€ฆ). Now color-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 to var(--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.

Fixed โ€” Modal & popup event leaks

  • Core modal dialog.onclose guarded with event.target === dialog. Child popups (<ty-dropdown>, <ty-multiselect>, <ty-date-picker>) dispatch their own bubbling close events; 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 / TyDatePicker listeners for open/close fired on bubbled events from child popups (e.g., a <TyDropdown> inside a modal closing would trigger the modal's onClose, causing the modal to actually close). All three wrappers now guard with event.target === element. Companion fix to the core-layer guard above.

Fixed โ€” Dark mode brand layer

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.

Fixed โ€” Ghost token references

  • tooltip.ts referenced --ty-color-info (no info flavor exists in Tyrell) โ€” the info-flavor variant was removed entirely.
  • tag.ts referenced --ty-border-mildstrong (a typo for a token that never existed) โ€” replaced with --ty-border.

Fixed โ€” React boolean props (controlled-state workflow)

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:

  1. "false" was treated as truthy. Passing disabled="false" / open="false" (or any non-empty string) was coerced to true. The helper now correctly maps "false" and "0" to false.
  2. true โ†’ false flips didn't propagate on React 18. React 18 removes the attribute when a boolean prop flips to false, but custom-element JS-property state can lag behind. The helper imperatively syncs the JS property in useEffect so 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).

Internal โ€” Wizard CSS

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.

Internal โ€” Site polish

  • Surface-demo borders softened: outer ty-canvas no longer has a border (it's the page surface, not a card); inner ty-content uses ty-border-soft instead of ty-border. Hierarchy reads from surface tones, not heavy lines.
  • Site header and right-sidebar (TOC) borders moved from ty-border-strong / ty-border to ty-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 :root snippet.

[1.0.0-RC9] - 2026-05-13

Wizard CSS variable system

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-easing

Width and height are set by the width/height attributes โ€” they are not public theming tokens.


Dropdown & Multiselect โ€” loading state

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-shadow

Dropdown โ€” external-search selection recovery

ty-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.


Mobile fixes

  • ty-dropdown โ€” fixed fullscreen mobile mode layout regressions
  • ty-multiselect โ€” fixed tag rendering and selection management in mobile fullscreen mode

[1.0.0-RC6] - 2026-05-05

Version scheme

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.

React 18 prop-to-property bridging

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).

[1.0.0-TC8] - 2026-05-04

ty-icon slot mode

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.


[1.0.0-TC6] - 2026-05-01

๐Ÿ’ฅ Breaking Changes

delay attribute renamed to debounce

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 โ†’ debounce
  • ty-dropdown โ€” delay โ†’ debounce
  • ty-multiselect โ€” delay โ†’ debounce

React props renamed accordingly (TyInput, TyDropdown, TyMultiselect).

Not affected:

  • ty-tooltip โ€” keeps its delay attribute (semantically a single-shot show-delay, not a debounce of rapid events)
  • ty-resize-observer โ€” already used debounce

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 />

[1.0.0-RC4] - 2026-03-25

New Components

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 by tyrell.layout for container-aware responsive layouts.

All 4 components include React wrappers in tyrell-react.

Mobile Enhancements

  • Calendar โ€” Fullscreen mobile calendar with touch-optimized scrolling, merged from feature/mobile-calendar branch
  • 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 โ€” wide attribute for full-width mobile-friendly buttons, responsive size guidelines

Documentation Overhaul

Complete restructuring of all documentation into guides/ folder.

Added

  • 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
  • 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-observer macros
    • Breakpoint queries, responsive values, grid helpers, aspect ratio
  • guides/DATASTAR_TY_GUIDE.md - Datastar + Tyrell guide (moved from root)

Changed

  • 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: ^js type 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

Removed

  • packages/cljs/CLJS_GUIDE.cljs (2223 lines) โ€” superseded by individual guides in guides/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

Fixed

  • ensureStyles duplicate stylesheet accumulation โ€” ensureStyles() appended a stylesheet reference on every call, even if already adopted. Components calling it from render() (tabs, calendar, dropdown, wizard, modal, etc.) accumulated duplicate adoptedStyleSheets on every property change. Added includes() guard to prevent duplicates.

[0.3.1] - 2026-02-XX

โœจ Added

Documentation

  • COMPONENT_GUIDE.md - Guide for building Web Components with tyrell.shim

    • Replicant, UIx, and Reagent integration examples
    • Shadow DOM styling with defstyles macro
    • 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

ClojureScript Package

  • tyrell.context namespace - Restored for calendar component locale support
    • *locale* dynamic var for locale binding

Changed

README Improvements

  • Landing README completely rewritten

    • ClojureScript-focused with UIx and Replicant examples
    • Accurate tyrell.router API documentation (using link, navigate!, rendered?)
    • Accurate tyrell.i18n examples with tyrell.i18n.number and tyrell.i18n.time namespaces
    • 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)

Fixed

cljdoc Analysis

  • Fixed cljdoc-analyzer failure - tyrell.components.core namespace not found

    • Moved ty/core.cljs from src/ty/ to components/ty/
    • This file required component namespaces not included in library JAR
  • Fixed deps.edn paths

    • Corrected :ty-lib alias path from "src/clj" to "src"
    • Added "components" to :dev alias extra-paths
  • Added missing dependency

    • Added dev.gersak/timing {:mvn/version "0.7.0"} to :dev alias

[0.2.0] - 2024-01-XX

๐ŸŽ‰ Major Release - TypeScript Migration Complete

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.


โœจ Added

TypeScript Components (NEW!)

  • Complete TypeScript port of all 19 web components to packages/core/src/components/
    • button.ts - Button component with flavor and size variants
    • calendar.ts - Full calendar orchestration with year/month/day navigation
    • calendar-month.ts - Month view component with custom day rendering
    • calendar-navigation.ts - Calendar navigation controls
    • checkbox.ts - Checkbox form control
    • copy.ts - Copy-to-clipboard component
    • date-picker.ts - Date picker with calendar integration
    • dropdown.ts - Desktop and mobile dropdown with search
    • icon.ts - Icon component with registry support
    • input.ts - Text input with formatting and validation
    • modal.ts - Modal dialog with backdrop and focus trapping
    • multiselect.ts - Multi-selection dropdown with tags
    • option.ts - Option component for dropdowns
    • popup.ts - Popup positioning component
    • tab.ts - Individual tab component (NEW in this release!)
    • tabs.ts - Tab container component (NEW in this release!)
    • tag.ts - Tag/badge component for multiselect
    • textarea.ts - Multi-line text input
    • tooltip.ts - Tooltip component

TyComponent Base Class Architecture

  • New TyComponent base 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 System

  • 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.ty API 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'

Build System & Tooling

  • Vite configuration improvements
    • vite.config.ts - Main build configuration
    • vite.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

Documentation & Examples

  • 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

ClojureScript Components Package

  • New packages/cljs/ structure with organized component wrappers
    • Separated ClojureScript wrappers from TypeScript implementation
    • Build configuration for ClojureScript package
    • Component-specific styling organization

๐Ÿ”„ Changed

Architecture Changes

  • Migrated from ClojureScript to TypeScript for all UI components

    • Maintained ClojureScript infrastructure for routing, i18n, and site
    • Components now in packages/core/src/ instead of lib/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

Mobile Experience

  • Improved mobile implementations across components
    • Better dropdown mobile menu UX
    • Modal improvements for mobile viewports
    • Touch-friendly interactions
    • Responsive layout adjustments

Styling System

  • Dropdown styling improvements

    • Better visual hierarchy
    • Improved focus states
    • Enhanced hover effects
    • Consistent with design system
  • Tabs component styling (TABS_STYLING.md documentation)

    • Comprehensive styling guide for tabs
    • CSS customization patterns

Documentation Site

  • Site package updates for compatibility with TypeScript components
    • Fixed integration issues
    • Updated component demos
    • Improved code examples
    • Better documentation structure

Build & Distribution

  • NPM package structure optimized
    • Better tree-shaking support
    • Smaller bundle sizes
    • Clearer entry points
    • Improved TypeScript definitions

๐Ÿ› Fixed

  • 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

๐Ÿ’ฅ Breaking Changes

Import Paths Changed

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'

Icon System Changed

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>

Component Property Changes

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

Package Structure

  • TypeScript components moved to packages/core/
  • ClojureScript wrappers moved to packages/cljs/
  • Root package.json removed (monorepo structure)

๐Ÿ“š Documentation Added

  • BUILDING_WITH_TYCOMPONENT.md - Comprehensive guide for building components with TyComponent base class
  • CSS_GUIDE.md - Tyrell CSS system usage guide (Tyrell for colors, Tailwind for everything else)
  • TYPESCRIPT_DEV_GUIDE.md - Development workflow for TypeScript components
  • PROJECT_SUMMARY.md - Updated with TypeScript architecture details
  • packages/cljs/README.md - ClojureScript package documentation
  • packages/cljs/components/ty/components/TABS_STYLING.md - Tabs styling guide

๐Ÿš€ Migration Guide

For ClojureScript Projects

If you were using v0.1.x ClojureScript components:

  1. Install the new package:

    ;; deps.edn
    {:deps {dev.gersak/tyrell {:mvn/version "0.2.0"}}}
  2. Components still work via ClojureScript wrappers in packages/cljs/

  3. Icon registration now required (see Icon System changes above)

For New TypeScript Projects

  1. Install via NPM:

    npm install tyrell-components
  2. Import components:

    import 'tyrell-components/css/tyrell.css'
    import { TyButton, TyDropdown } from 'tyrell-components'
  3. Register icons you need:

    import { check, heart } from 'tyrell-components/icons/lucide'
    import { registerIcons } from 'tyrell-components/icons/registry'
    
    registerIcons({ check, heart })

For React Projects

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

For Vanilla JS / HTMX

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>

๐ŸŽฏ Version 0.2.0 Goals Achieved

โœ… 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


๐Ÿ”ฎ Future Plans (v0.3.0+)

  • Additional components (data table, file upload, progress indicators)
  • Enhanced a11y (accessibility) features
  • More icon library integrations
  • Performance optimizations
  • Additional framework adapters
  • Component testing utilities

๐Ÿ“ฆ Package Information

  • NPM Package: tyrell-components v1.0.0-rc.4
  • Clojars Package: dev.gersak/tyrell v1.0.0-RC4
  • License: MIT
  • TypeScript: โœ… Full type definitions included
  • Framework Support: React, Vue, Reagent, HTMX, Vanilla JS

๐Ÿ™ Acknowledgments

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.