feat(DatePicker,DateRangePicker,DateInput): add week and quarter section masks - #673
Merged
Conversation
…ion masks Close the last selection-type gap in the pickers rewrite: week and quarter selection now render dedicated masked sections instead of falling back to the underlying day mask. - new week and quarter section types in the format engine: ww (ISO week, 1-53, clamped to the 52/53-week length of the selected year) and q/QQQ (numeric quarter or Q1-Q4 names); uppercase W stays a literal so yyyy-Www can spell the ISO prefix - in week layouts the year section holds the ISO week-numbering year, and the value resolves to the Monday of the selected week; quarter layouts resolve to the first day of the quarter - selectionType week/quarter default masks: yyyy-Www (the native input type=week wire format) and QQQ yyyy in both pickers - week/quarter placeholders, spinbutton labels, and year-dependent week clamping in SectionInput
Coverage Report for CI Build 30772085684Coverage increased (+0.06%) to 93.026%Details
Uncovered Changes
Coverage Regressions1 previously-covered line in 1 file lost coverage.
Coverage Stats💛 - Coveralls |
…e the native week input
The native <input type="week"> presents a fixed, non-editable label before
the editable parts (Week 29, 2026), not the ISO wire format.
- quoted literals in the format parser, date-fns style: text in single
quotes is always a literal even when it contains token letters
('Week' ww renders "Week 29"), '' escapes the quote character
- getWeekLabel resolves the localized week-of-year label via
Intl.DisplayNames (Week / Tydzien / Woche), capitalized, with a Week
fallback; getWeekSectionsFromLocale builds the native-style layout
- selectionType week now defaults to that presentation in both pickers;
an explicit format (e.g. yyyy-Www for the ISO style) still wins
new Date(year, 0, 4) maps years 0-99 to 19xx, so getISOWeekNumberAndYear returned week numbers around ±99000 while a week mask's year section was still being typed (the clamp then wrote the garbage into the week section: "Week -99085, 0022"). Build the January 4th reference by copying the date and calling setMonth(0, 4), which keeps the full year.
… programmatic dates like typed ones update() rebuilt the field without running validation, so a programmatic date outside min/max (setDate, the projected today action, the initial date config) rendered as valid and reached the form value — while typing the same date flagged the field invalid and nulled the value. - extract the validation pass into _applyValidationState() and run it in the constructor and at the end of update(), preserving the previous date so dateChange/errorChange reflect the actual transition - the shells emit dateChange (and update the calendar/time selection) with the validated value, not the raw argument - the validity toggle keeps the config-driven invalid class Projected footer buttons stay userland (the picker never disables them) — documented with the validation contract in the footer docs and the migration guide.
…r): expose isDateSelectable in the slot context Projected footer buttons are userland, so the picker never disables them — but the slot context gave userland no way to compute that state. The field gains isDateSelectable(date): it expresses the date in the field's mask and answers whether it passes min/max/disabledDates validation, so the granularity follows the mask (a day mask checks the calendar day — "now" passes with a maxDate of today; a date-time mask checks the exact time). Every shell exposes it through getContext() (isTimeSelectable on the time picker), and the date picker docs show the pattern: disabling a projected Today button when today is out of range.
…/now action when its target is not selectable A button opting into the today action (now on the time picker) opts into the library's click semantics — so it opts into the state those semantics imply. When today (or the current time) falls outside minDate/maxDate/disabledDates, the button is disabled while the footer template is cloned, matching the v1 built-in button. One-way only: the picker never re-enables a projected button, so a disabled set in the template stays, and buttons with custom actions remain fully userland (isDateSelectable stays in the slot context for those).
…alidation border on the picker frame In a picker the input group is the frame and the field inside it draws no border of its own, so the validation state never reached anything visible: the mixin sets --cui-control-border-color on the field, and custom properties only inherit downwards. The group now picks the state up with :has(), matching the standalone date input exactly (same tokens, same color). Following the styling doctrine for the indicator: the validation icon would land under the indicator button, so it is suppressed inside pickers and the inline-SVG indicator carries the color through currentColor instead (the .form-password block does the same for its action icon).
The rewritten shell has no cleaner button and renders its indicator as an inline SVG on currentColor, and validation state now lives on the field inside the group rather than on the root element — so a set of tokens and variables had no reader left: - .time-picker.is-invalid / .is-valid blocks (frame recoloured from the root with !important overrides; v2 reads the state from the field) - --cui-time-picker-cleaner-* (5), -indicator-icon, -indicator-icon-size, -gap and -placeholder-color, plus their Sass variables; the sm/lg blocks no longer set them either - $input-padding-x-lg, whose last consumer was the dead gap-lg (.form-control-lg reads --cui-btn-input-lg-padding-x) $time-picker-cleaner-icon-size-sm/-lg were declared twice; both copies go. --cui-time-picker-indicator-icon-color stays and now carries the validation color as well. Also drops two imports the file no longer needs (sass:math, sass:color — the latter was already unused). Verified in a browser: invalid/valid borders, indicator color and the sm/lg sizing are unchanged.
mrholek
added a commit
that referenced
this pull request
Aug 3, 2026
…d component onto it Six components draw a frame that has to read as a single control — autocomplete, chip input, multi select, password input and the four picker shells — and each carried its own copy of the styling. That duplication is what produced three v2 bugs already (a double icon, a double frame that broke sizing, one picker reading another's properties) and it is what the styling doctrine set out to end. The primitive ------------- - `.form-control-group` is the frame. It shares one canonical definition with `.form-control` through the new `form-control-frame()` mixin, lays its children out as a flex row, and answers focus on `:focus-within` because a div never fires `:focus`. `form-control-group-focus()` lets a component replay that treatment while its popup holds the focus. - `.form-control-group > .form-control`: the control inside gives up its own frame. One deliberate rule replaces the per-family copies, and it outranks a theme's `.form-control` border so a second frame can never reappear inside a group. There is no "ghost" class — the editable keeps the class it already had. - `.form-control-icon` is an icon and nothing more: passive, and usable either on its own or inside an action. `.form-control-action` is an interactive adornment (a real button) that may hold one; `.form-control-cleaner` is the value-clearing action. - Sizing rides the standard `.form-control-sm`/`-lg` on the frame, so the per-family sizing classes are gone. The group answers those classes itself, since it declares the control tokens and would otherwise win the cascade against them. Adoption -------- - Pickers: frame, indicator and the range separator move to the shared classes; the sizing classes and the indicator tokens disappear. - Password input drops the overlay model entirely — the toggle was an absolutely positioned button over the input, with padding carved out and its size recalculated per size class. As a flex child none of that is needed. Its icon paints in `currentcolor`, so it follows the action through hover and validation instead of needing its own colour rules. - Chip input keeps wrapping, chip spacing and chip disabling; the frame, focus and disabled treatment come from the group. - Autocomplete and multi select take the frame. Their validation, open and disabled states are now written onto the frame rather than inherited from the component root, which also retires their `!important` overrides. Their adornments keep family styling for now: those icons are CSS masks, and converting them to the inline-SVG convention the primitive's actions expect belongs with their Popup pass. - Validation generalizes to one `.form-control-group` selector, replacing the per-family pair added in #673. Compiled CSS drops by 1892 lines. The primitive gets its own docs page and the migration guide carries the class map for the markup users write by hand. Design and rationale: architecture/v6-form-controls-styling.md in the workspace repo.
mrholek
added a commit
that referenced
this pull request
Aug 4, 2026
…d component onto it (#675) * feat(forms): add the form-control-group primitive and move every field component onto it Six components draw a frame that has to read as a single control — autocomplete, chip input, multi select, password input and the four picker shells — and each carried its own copy of the styling. That duplication is what produced three v2 bugs already (a double icon, a double frame that broke sizing, one picker reading another's properties) and it is what the styling doctrine set out to end. The primitive ------------- - `.form-control-group` is the frame. It shares one canonical definition with `.form-control` through the new `form-control-frame()` mixin, lays its children out as a flex row, and answers focus on `:focus-within` because a div never fires `:focus`. `form-control-group-focus()` lets a component replay that treatment while its popup holds the focus. - `.form-control-group > .form-control`: the control inside gives up its own frame. One deliberate rule replaces the per-family copies, and it outranks a theme's `.form-control` border so a second frame can never reappear inside a group. There is no "ghost" class — the editable keeps the class it already had. - `.form-control-icon` is an icon and nothing more: passive, and usable either on its own or inside an action. `.form-control-action` is an interactive adornment (a real button) that may hold one; `.form-control-cleaner` is the value-clearing action. - Sizing rides the standard `.form-control-sm`/`-lg` on the frame, so the per-family sizing classes are gone. The group answers those classes itself, since it declares the control tokens and would otherwise win the cascade against them. Adoption -------- - Pickers: frame, indicator and the range separator move to the shared classes; the sizing classes and the indicator tokens disappear. - Password input drops the overlay model entirely — the toggle was an absolutely positioned button over the input, with padding carved out and its size recalculated per size class. As a flex child none of that is needed. Its icon paints in `currentcolor`, so it follows the action through hover and validation instead of needing its own colour rules. - Chip input keeps wrapping, chip spacing and chip disabling; the frame, focus and disabled treatment come from the group. - Autocomplete and multi select take the frame. Their validation, open and disabled states are now written onto the frame rather than inherited from the component root, which also retires their `!important` overrides. Their adornments keep family styling for now: those icons are CSS masks, and converting them to the inline-SVG convention the primitive's actions expect belongs with their Popup pass. - Validation generalizes to one `.form-control-group` selector, replacing the per-family pair added in #673. Compiled CSS drops by 1892 lines. The primitive gets its own docs page and the migration guide carries the class map for the markup users write by hand. Design and rationale: architecture/v6-form-controls-styling.md in the workspace repo. * fix(Autocomplete,MultiSelect): don't pad the frame twice Both families pad the parts inside the frame, not the frame itself — a wrapped tag has to be able to reach its edge — so the padding the primitive applies came on top of theirs and the control grew from 38px to 50px. Their frames now carry none, and their parts stretch to the full height as they did before. * fix(FormControlGroup): grey the whole frame when the control inside is disabled A control that isn't a real input — the pickers' section field — carries `.disabled` as a class, which `:disabled` never matches. So the frame never learned it was disabled while the field painted the disabled background on itself: a grey block inside a white frame. Both spellings now reach the frame, and the control inside gives up its disabled background the same way it gives up the rest of its chrome. This restores what v1 did through `.date-picker.disabled`, without each family having to wire its root state to the frame. * fix(pickers,FormControlGroup): disable the indicator when the picker is disabled The indicator button was never actually disabled — the shells only had the click handler return early — so a disabled picker still offered a focusable button that silently did nothing, and it kept its enabled colours next to a greyed-out frame. The shells set the attribute now, so it drops out of the tab order and the primitive's :disabled styling applies. Adornments inside a group marked .disabled are muted too, for groups assembled by hand where the author disabled the control but not every button. Direct children only, so an icon inside an action doesn't fade twice. * feat(pickers): restore the inline cleaner on the form-control-group primitive v1 offered a button inside the field that cleared the value; the v2 rewrite dropped it, which is how its styling tokens ended up dead. The primitive already had a place for it, so it comes back properly: - a real `<button>` — v1 used a bare div, so the control it operated could not be reached from the keyboard — labelled, disabled together with the picker, and clearing without opening the popup. - it appears only once the field holds a value. The section field already reports that with `.form-date-time-filled`, so the rule belongs to that family and one selector covers all four shells; no component has to track the state itself. - `cleaner` (on by default, as in v1), `cleanerIcon` and `ariaCleanerLabel` on all four shells. The cross icon was duplicated byte for byte as a data URI in autocomplete and multi select; it becomes one inline SVG in `util/icons`, painted in currentColor so it follows the action through hover, validation and disabled. Building an adornment moves to `util/form-control-group` — the JavaScript half of the primitive's markup contract, and the same consolidation the SCSS just went through. Budgets: the JS ones move up for the restored feature, and the CSS ones come down, so the 1892 lines this branch removed stay removed. * fix(pickers): draw the cleaner cross on the same grid as the other icons The cross came from a 16-unit grid, where the same shape carries roughly twice the stroke weight of the 512-grid icons the indicators use — at a shared 16px it read noticeably heavier than the calendar and clock next to it. * feat(Autocomplete,MultiSelect): one cleaner across the library The cross was drawn three different ways: a data URI duplicated byte for byte in autocomplete and multi select, painted through a CSS mask on a ::before, and a third copy about to appear in the pickers. Both families now use `.form-control-cleaner` with the shared inline SVG, so every cleaner in the library is the same mark at the same size, and follows its button's colour through hover, validation and disabled instead of needing tokens of its own. The primitive gives a cleaner its own icon size (.625rem, the value both families already used): the cross is drawn on a 16-unit grid, so it carries more weight than the 512-grid glyph icons and has to render smaller to read as their equal. 32 declarations go — both families' cleaner width, height, padding, icon, icon colours and icon size. Their indicator keeps its mask-based styling until the Popup pass; merging what was left of the shared base rule into it removes a duplicate selector. * refactor(FormControlGroup): an action and a cleaner are one style The cleaner had its own smaller icon size, inherited from autocomplete, because the cross it used was drawn on a 16-unit grid and carried about twice the stroke weight of the icons beside it. That put a per-role exception into a primitive whose whole point is that these are the same kind of button. The cross is cil-x from the CoreUI set now — drawn on the same 512 grid as the icons it stands next to — so both roles share one rule and one icon size, and the exception goes away with the token behind it. * fix(icons): crop the cleaner cross so it matches the icons beside it cil-x carries 85 units of built-in margin where the calendar and clock carry 16, so on the raw 512 box it painted a third less ink than they did at the same rendered size — the cross looked inset rather than equal. Cropping it to their exact coverage overshoots the other way: a cross that reaches the corners reads larger than an orthogonal glyph of the same box, its diagonal being 1.41x the side. The viewBox sits between the two, which lets every adornment keep one icon size. * build(class-api): register the classes the primitive replaced The guard exists so no public class disappears without a migration note. Ten did: the per-family frames' indicators, separators and sizing classes, and the three cleaners. * feat(Autocomplete,MultiSelect): finish the move onto the primitive Their frames and cleaners already came across; the indicator was the last part still styled by the family, drawn as a CSS mask through a ::before. It is a `.form-control-action` now with the shared inline SVG, so every adornment in the library is the same mark at the same size and takes its colour from the button. The chevron was the second icon duplicated byte for byte between these two — it joins the cross in `util/icons`. Both indicators also become genuinely disabled buttons rather than ones merely dropped from the tab order with tabindex, which is what a disabled control should offer. What is left of these two families' styling is what they actually own: the options listbox, tags, search, and the caret turning when the popup opens. * feat(Autocomplete,MultiSelect): the editable and the adornments are the group's own Two things were still hand-written inside the group. The editable carried `.autocomplete-input`, a copy of the typography, colours, padding and reset that `.form-control` plus the group's neutralization already give it — it is a plain `.form-control` now. And both families wrapped their adornments in a `-buttons` div that existed to give an unpadded frame its height; the group lays its children out itself, so the buttons are its children and the wrapper is gone. With that, the frame pads both families like every other control instead of delegating it inwards: the multi select tag row keeps only its own spacing, and the autocomplete markup is now the same shape as a picker's — control, cleaner, indicator. The neutralization reaches nested editables too, since everything inside a group is part of the control, and the picker's cleaner-visibility rule is scoped to groups actually built around a section field. It was written when their cleaner sat inside a wrapper; once every cleaner became a direct child it would have hidden the ones the other components manage themselves. * feat(MultiSelect): a selection is a Chip, whichever selection type asks for it `selectionType: 'tags'` and `'chips'` were the same idea built twice, and the default was the hand-rolled one: a div with a delete button, styled by its own set of variables, without the roles a Chip carries since #658. Both values render the Chip component now, so a selection looks and behaves the same everywhere in the library and is themed through the chip properties. `'tags'` stays accepted as a synonym. That removes the second implementation entirely — its markup builder, its update pass, its delete handler (chips route their own removal through the selection model) and eighteen variables. * feat(forms): add the .popup primitive — one chrome for every floating panel The pickers' dropdowns and the combobox listbox drew the same box three times: three sets of background, border, radius, shadow and z-index tokens, two of them byte-for-byte copies of each other. The panel now carries `.popup`, the floating-surface primitive of the field components, next to its structural class — which keeps its own shape (min-content for pickers, toggler-width for the listbox) and the parent-driven open state, and keeps carrying the family tokens for the teleported case. Nineteen chrome declarations collapse into six `$popup-*` variables and their custom properties: theme the popup once and every field component's panel follows. Defaults match what the panels drew before, so nothing changes visually. The name is deliberately presentation-neutral — this is the anchored mode; the fullscreen/modal mode for mobile (the v1 pickers had one, the v2 rewrite lost it) is designed at this primitive and lands separately. The popup borrows the look of a menu, never its interaction semantics: the body inside owns those, and Menu itself stays the nav primitive. * feat(ChipInput): controlled mode — chips come from the host, not from typing The combobox family plan makes chip-input the selection surface of the multi select: chips there come from the listbox selection, and typing filters options rather than minting chips. `create: false` is that mode: Enter, separators, paste-splitting and create-on-blur stop producing chips, no hidden form input is rendered (the host owns the value), while `add()` and the whole chip keyboard model keep working. Groundwork for embedding: chip-input already adopts an existing <input> instead of creating its own, so the host can hand it a field carrying its own wiring. * test(visual): state-matrix screenshot suite for the field components Every regression of the primitives cycle — the doubled padding, the white disabled frame, the vanishing cleaner, the oversized cross — was caught by eye, none by the 2950-test unit suite, because a shared rule reaches six components and no unit test sees a pixel. This is the safety net for exactly that class of change. Vitest 4's browser mode ships toMatchScreenshot, so the suite is the same Chromium-through-Playwright the unit specs run in — no new dependency. 21 screenshots cover the form-control-group and .popup surfaces: the pickers (states, sizes, open popups, dark mode), autocomplete, multi select (chips, dark), chip input and password input. Determinism: fixed dates only, transitions and animations off, caret hidden, fixed viewport, one screenshot per test (creating a missing baseline aborts the test, so a second one would never get its baseline on the first pass). Baselines are per platform — the matcher suffixes them with the browser and OS — and macOS ones are committed here; the Visual workflow generates the Linux set as an artifact on its first run and gates once it is committed. Runs via `npm run js-test-visual`; deliberately outside the local pre-push gate, since macOS font rendering would fight the CI baselines. * feat(MultiSelect): the selection area rides ChipSet — arrow-key navigation across chips The chips were individually focusable but not navigable: no roving focus, no keyboard model across the row. The selection area is a role-less ChipSet now (the ChipInput precedent — the container mixes chips with the search input), which brings arrow-key navigation, Home/End, keyboard removal on a focused chip, and a role=status region announcing add and remove — replacing the cruder aria-live on the container, which announced every re-render. The selection model stays the component's own: Chip.getOrCreateInstance makes both sides meet on one instance. * refactor(forms): drop the inert per-family frame layer The frame declares the --cui-control-* properties on itself, so a family's own copy of them - declared on the component root and shadowed on every element inside the group - never had an effect. Each family keeps only what is genuinely its own: placeholder colours, the pickers' footers, the multi select's tag row. Gone with the layer: the frame-mirroring Sass variables, the family-named sizing classes (.autocomplete-sm/-lg, .form-multi-select-sm/-lg - sizing is .form-control-sm/-lg on the group), the multi select's tag-delete tokens orphaned by the move to chips, and the @use lines none of it needed. The visual suite pins the proof: all 21 screenshots unchanged. * refactor(forms): finish the dead-import sweep; restore the frame class in two chip input examples Calendar and the date-time field partial each carried a box-shadow @use nothing consumed. The chip input Sizes examples lost their .form-control-group during the frame migration - the component does not add the class itself, so the small and default variants rendered without a frame. * fix(forms): the action's transition list never covered transform, so the chevron snapped The shared $input-btn-transition moves color, background, border and shadow; the indicator flip on an open combobox rotates. v5 gave the indicator its own transform transition - the action token carries it now, for every family at once. * fix(tests): the visual suite screenshotted unstyled DOM Nothing in the harness ever loaded the stylesheet, so all 21 baselines were pictures of bare markup: no frames, no chips, and the dark-mode shots identical to the light ones. It would have passed any CSS regression, which is the only thing it exists to catch. The spec imports scss/coreui.scss now - the source, so a change shows up on the next run with no build step - and every baseline is regenerated. Three cases join it, one per way a validation state reaches the frame: the component root (autocomplete, multi select) and native constraint validation on the control inside the group. * fix(forms): give every family the same validation treatment, from one rule Autocomplete and Multi Select carried their own copy of the frame's validation state, and it had drifted: it set the border but not the adornment colours, so an invalid multi select kept a grey chevron where an invalid picker got a red one, and its focus ring was mixed from a different source than every other control in the library. Both copies are gone; the shared mixin now names their root classes alongside the group's own state, and the four $*-invalid/valid-border-color variables that shadowed --cui-form-*-border-color go with them. Native constraint validation reaches the frame too, which it never did: a required control inside a group under .was-validated only recoloured itself, and the control inside a group draws no border. The autocomplete's was-validated selector had been looking for a sibling element the component never builds. Locked by three visual cases - one per route the state takes to the frame. Catching them needed the comparator to count antialiased pixels: a 16px icon's stroke is almost all antialiasing, and pixelmatch discounts those by default, so grey-to-red scored as no difference. * refactor(Autocomplete,MultiSelect): delete what the frame primitive already does Autocomplete is down to four rules and no variables of its own. What went: a disabled state the group already reads off the disabled control inside it (and reads more completely - it also sets the cursor and mutes the adornments); a text colour the control inherits from the focused frame; a placeholder colour identical to --cui-control-placeholder-color, which it was shadowing rather than extending; and a selector feeding tokens to a teleported .autocomplete-dropdown, a class the component stopped emitting when the options menu became the shared combobox surface. Multi Select loses the same placeholder mirror and dropdown selector, plus a position: relative on the selection with nothing absolute inside it. Its disabled state moves where the doctrine puts it - on the frame - so the one shared rule covers it: the class lands on the group in JS now, since the search input the group would otherwise read is absent when search is off. The visual suite is what makes this safe to do: the two states without a screenshot got one first, so every removal had to prove itself against a baseline taken before it. * refactor(utilities)!: drop the theme-variant and rtl keys from the utilities API The dark: variants were generated for the colour utilities only, never documented, and duplicated every value under a [data-coreui-theme] selector - 40 classes and ~10 kB of CSS to say what light-dark() already says in the palette. The rtl key had nothing left to do: direction is logical properties now, so rtl: false only emitted RTLCSS /* rtl:remove */ directives and rtl: true carried ltr/rtl value maps for a single utility. Both branches doubled the generator, which is 44 lines lighter for it. translate-middle follows Bootstrap v6-dev and stops flipping under dir="rtl" - transform is not direction-aware, and the flag was the only thing left asking the generator to care about direction. Aligns the API with Bootstrap's v6-dev branch, which we track; their side adds selector types and child-selector on top, which is the next step. * fix(utilities): keep the translate-middle RTL flip after dropping the rtl key transform is not direction-aware: in an RTL document .start-50 resolves to right: 50%, so an element centred on that line has to move the other way. Dropping the generator's rtl key took the flip with it; it comes back as the plain [dir="rtl"] rule it always should have been - one rule for the one utility that needs it, instead of an option every utility carries. * test(Sass): follow the utilities API through its own suite The Sass suite covers the generator directly, and two specs still described the old API: one asserted the RTLCSS removal directives that the dropped rtl key used to emit, and the api spec caught the translate-middle override leaking into a run whose $utilities map never asked for that utility. The override is tied to the utility now, so disabling it takes the override with it - which is what the spec was really reporting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last selection-type gap in the pickers rewrite (gate condition 3):
selectionType="week"andselectionType="quarter"now render dedicated masked sections — the equivalents of the native<input type="week">and of the<input type="quarter">HTML never shipped — instead of falling back to the underlying day mask.Format engine (
util/date-sections)ww(ISO week number, 1–53) andq/QQQ(numeric quarter orQ1–Q4names). UppercaseWdeliberately stays a literal so masks can spell the ISO prefix:yyyy-Wwwrenders2026-W29.'Week' wwrendersWeek 29);''escapes the quote character.2027-01-01edits as week 53 of 2026), and the value resolves to the Monday of the selected week. A quarter layout resolves to the first day of the quarter.getISOWeeksInYear/getDateOfISOWeek/getWeekSectionMaxhelpers, mirroring the day-in-month clamping.Shells
selectionType="week"defaults to the native week input's presentation: a fixed localized label plus week and year sections —Week 29, 2026,Tydzień 29, 2026,Woche 29, 2026(getWeekLabelviaIntl.DisplayNamesdateTimeField: weekOfYear, capitalized,Weekfallback). An explicitformatstill wins —format="yyyy-Www"gives the ISO-style field.selectionType="quarter"defaults toQQQ yyyy(Q4 2026) — in both DatePicker and DateRangePicker.SectionInputgains week/quarter placeholders, spinbutton labels, and year-dependent week clamping.Docs
date-picker/date-range-pickerpages: the "underlying date" warning callouts are gone; the Weeks/Quarters sections document the native-style presentation and the ISO-format opt-in.date-input: quoted-literal syntax and week/quarter mask examples; the "isn't supported" note removed.Tests
40 new cases: token parsing incl. quoted literals, ISO week math (round trips, 52/53-week years, week-year edges), localized week label, build/fill from sections, serialization, paste, and picker integration for both shells (incl.
pl-PLlocalization and the explicit-format override). Full gate green: lint, typecheck, 2922 unit tests, dist, bundlewatch PASS.Programmatic dates validate like typed ones
SectionInput.update()rebuilt the field without running validation, so a programmatic date outside min/max (setDate, the projectedtodayaction, the initialdateconfig) rendered as valid and reached the form value — while typing the same date flagged the field invalid and nulled the value. The validation pass is now extracted into_applyValidationState()and runs in the constructor and at the end ofupdate()(preserving the previous date sodateChange/errorChangereflect the actual transition), and the shells emitdateChange— and update the calendar/time selection — with the validated value.Projected footer buttons stay userland by design (the picker never disables them); the footer docs and the migration guide document that contract.
Slot context:
isDateSelectable(date)A button opting into the
todayaction (nowon the time picker) opts into the library's click semantics — so it now opts into the state those semantics imply: it is disabled while the footer template is cloned when its target falls outsideminDate/maxDate/disabledDates, matching the v1 built-in button. One-way only — the picker never re-enables a projected button, so adisabledset in the template stays.For buttons with custom actions, the field gains
isDateSelectable(date)— it expresses the date in the field's mask and answers whether it passesminDate/maxDate/disabledDatesvalidation. The granularity follows the mask: a day mask checks the calendar day (sonew Date()passes with amaxDateof today — no midnight trap), a week mask the week's Monday, a date-time mask the exact time. Every shell exposes it throughgetContext()(isTimeSelectableon the time picker), and the date picker docs gained a runnable example: a projected Today button disabled via the context when today is out of range.Validation border on the picker frame
Reported on the branch:
date-inputpainted a red border onis-invalid, the pickers did not. The picker's input group is the frame and the field inside it draws no border of its own, so the state never reached anything visible — the validation mixin sets--cui-control-border-coloron the field, and custom properties only inherit downwards.The group now picks the state up with
:has(.form-date-time.is-{valid,invalid}), reusing the same tokens as.form-control, so the color is identical to the standalone field. Per the styling doctrine's "validation icon wrinkle": the icon would land under the indicator button, so it is suppressed inside pickers and the inline-SVG indicator carries the color throughcurrentColorinstead — the same shape as the existing.form-passwordblock in the mixin.Verified in a real browser across all four shells plus the valid state: identical
rgb(229, 83, 83)on the standalone field and every picker frame, green on valid, indicator recolored, background icon suppressed inside pickers only.:has()is already inside the declared browser floor (#667), and the migration guide's:has()rationale now lists the pickers.Time picker: v1 styling leftovers removed
Removing the frame bug uncovered dead code in the same family, so it goes with it. The rewritten shell has no cleaner button and draws its indicator as an inline SVG on
currentColor, and validation now lives on the field rather than the root, so nothing read any of this:.time-picker.is-invalid/.is-valid— recoloured the frame from the root with!importantoverrides. v2 never puts that class on the root.--cui-time-picker-cleaner-*(5 tokens),-indicator-icon,-indicator-icon-size,-gap,-placeholder-colorand their Sass variables, plus thesm/lgblocks that set them.$time-picker-cleaner-icon-size-sm/-lgwere declared twice — both copies go.$input-padding-x-lg, orphaned once the deadgap-lgwent (.form-control-lgreads--cui-btn-input-lg-padding-x).sass:math, andsass:colorwhich was already unused.--cui-time-picker-indicator-icon-colorstays and now carries the validation color too.fusvis green with zero unused variables, and the browser check confirms invalid/valid borders, indicator color andsm/lgsizing (31/38/48 px) are unchanged. Documented under "Design-token maps" in the migration guide.