Skip to content

v0.13.0

Choose a tag to compare

@angel-ip angel-ip released this 30 Sep 15:00
· 736 commits to main since this release

@kanzo-tech/ui

Every single-line control is now the same height at the same size. A form that stacked an
input, a combobox, a tags input and a date picker at md had them 32, 46, 38 and 46px tall; they are
all 32px now (sm 28, lg 36 — 28, 32 and 36 times --size-field scaled by density, as Input
and Button always were). What moved, at the default density:

Control Was → is
Combobox, DatePickerInput, DatePickerTimer, InputGroup, PasswordInput, LanguagePicker md 46 → 32; sm 42 → 28 (PasswordInput 32 → 28); lg 50–54 → 36
TagsInputControl with one tag or none md 38 → 32; sm 34 → 28; lg 38 → 36. It still grows with a second row.
SegmentGroup default 34 → 32, solid 44 → 32, at md; it takes a new size prop
NativeSelect its wrapper sm 29 → 28 (an inline gap under the select)
Switch, Checkbox and RadioGroupItem with a label 18–28 → 32 (the indicator is unchanged, centred in the row); with no children they are unchanged
Checkbox and RadioGroupItem labels 16 → 14px, the size the Switch label always was
InputGroupButton (sm, icon-sm), the combobox and calendar triggers 32 → 24px. It is 24 CSS pixels, the WCAG 2.5.8 floor, so it is the same at every density.

Input, Select, NumberInput, PinInput, Toggle and Button did not change height. If you sized
a group with h-* or padded it with p-* to make room, delete that; if you passed h-8 or size-8
to a button inside a group, drop it. A group holding a button at sm in compact density is 26px, not
24.5 — the button's 24px floor wins. The rule and the live example are on Controls
(/docs/forms/controls#one-height-per-size).

New: LanguagePicker and SegmentGroup take size.

DatePickerTimer puts your props on the <input>, shows minutes unless asked, and reads its
Field.
It used to send every prop except id to the group around the input, so onChange,
aria-label, name and disabled went nowhere useful, and it always showed seconds. Now name,
onChange, aria-label and the rest reach the input (size is the group's; the group is w-full,
so size it with its parent), step is a prop in seconds — 60 by default, hours and minutes — and
disabled, invalid, readOnly and required come from an ancestor Field. If you relied on
seconds, pass step={1}; if you passed className for the group's width, wrap the timer instead —
className has always gone to the input. You can also drop a sr-only label workaround: pass
aria-label.

A date and a time is one field. DatePicker takes granularity="minute" (or "second"): the
input is a segmented field showing both in the locale's order and clock (3/14/2026, 6:30 PM, 14/3/2026, 18:30) that you can type into from empty or paste a timestamp into, the popover stays open after a day
is picked, and a DatePickerTimer placed under the calendar in DatePickerContent sets the time of
the same value. The value is a CalendarDateTime; parseDateTime("2026-03-14T18:30") is now exported
next to parseDate. Replace the date input with a separate time input beside it:

<DatePicker granularity="minute" value={[parseDateTime(iso)]} onValueChange={({ value }) => setIso(value[0]?.toString().slice(0, 16) ?? null)}>
  <DatePickerInput />
  <DatePickerContent>
    <CalendarView view="day">…</CalendarView>
    <DatePickerTimer aria-label="Time" />
  </DatePickerContent>
</DatePicker>

Every table takes a relation in another catalog. ChartRoot, a mark's table, ChartStat,
ChartFilter, ChartSearch, ChartSlider and IdSetClient now take a TableExpr: a string, as
before, or a mosaic-sql node, which goes into the SQL as written. A string still means one
identifier in the default catalog — "Person" is FROM "Person" — so nothing you pass today
changes. For a relation a backend names already qualified, such as a fossil corpus's
relation.sql, pass verbatim(relation.sql); for a path you hold in parts,
asTableRef([catalog, name]). Both come from @uwdata/mosaic-sql, and TableExpr is on
@kanzo-tech/ui/analytics and @kanzo-tech/mosaic.

useChartContext().table is typed TableExpr to match: code that read it as a string narrows
it (typeof table === "string") or passes it on to Query.from, which takes both.

@kanzo-tech/ai

CompleteHint takes acceptLabel and dismissLabel, like CompleteKeys — it drew the English
"accept" and "dismiss" whatever the host's language, though the docs said every word is a prop.

ConversationEmpty is removed; a conversation's zero state is @kanzo-tech/ui's Empty parts.
It was a centred column with a measure, which is what EmptyRoot and EmptyHeader already are.
Replace it inside ConversationContent:

// before
<ConversationEmpty>
  <SparklesIcon />
  Ask about your data.
</ConversationEmpty>

// after — EmptyRoot, EmptyHeader, EmptyIndicator, EmptyDescription from "@kanzo-tech/ui"
<EmptyRoot>
  <EmptyHeader>
    <EmptyIndicator>
      <SparklesIcon />
    </EmptyIndicator>
    <EmptyDescription>Ask about your data.</EmptyDescription>
  </EmptyHeader>
</EmptyRoot>

A selector on [data-slot=conversation-empty] becomes [data-slot=empty].

@kanzo-tech/graph

A failure found while GraphRoot mounts no longer updates your component during render. A corpus
with no drawable type, or a scan fossil refused, was reported to onFailure while React was still
rendering the root, so an onFailure={setError} logged "Cannot update a component while rendering a
different component". It is now reported once the root has mounted. Nothing to change on your side.