@adea-ai/ui 0.131.0 → 0.133.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/composites/list-row/list-row-control.d.ts +9 -0
- package/dist/components/composites/list-row/list-row-control.d.ts.map +1 -1
- package/dist/components/composites/list-row/list-row-control.js +5 -1
- package/dist/components/composites/list-row/list-row-control.js.map +1 -1
- package/dist/components/ui/accordion/accordion.d.ts.map +1 -1
- package/dist/components/ui/accordion/accordion.js +1 -1
- package/dist/components/ui/accordion/accordion.js.map +1 -1
- package/dist/components/ui/empty/empty-state.d.ts +21 -2
- package/dist/components/ui/empty/empty-state.d.ts.map +1 -1
- package/dist/components/ui/empty/empty-state.js +62 -18
- package/dist/components/ui/empty/empty-state.js.map +1 -1
- package/dist/components/ui/field/field.d.ts +5 -1
- package/dist/components/ui/field/field.d.ts.map +1 -1
- package/dist/components/ui/field/field.js +5 -1
- package/dist/components/ui/field/field.js.map +1 -1
- package/dist/components/ui/field/number-field.d.ts +18 -2
- package/dist/components/ui/field/number-field.d.ts.map +1 -1
- package/dist/components/ui/field/number-field.js +10 -4
- package/dist/components/ui/field/number-field.js.map +1 -1
- package/dist/components/ui/radio-group/radio-group.d.ts +3 -1
- package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group/radio-group.js +3 -1
- package/dist/components/ui/radio-group/radio-group.js.map +1 -1
- package/dist/r/src/components/composites/list-row/list-row-control.tsx +12 -1
- package/dist/r/src/components/ui/accordion/accordion.tsx +5 -1
- package/dist/r/src/components/ui/empty/empty-state.tsx +59 -21
- package/dist/r/src/components/ui/field/field.tsx +10 -2
- package/dist/r/src/components/ui/field/number-field.tsx +35 -4
- package/dist/r/src/components/ui/radio-group/radio-group.tsx +10 -2
- package/package.json +1 -1
- package/src/components/composites/list-row/list-row-control.tsx +12 -1
- package/src/components/ui/accordion/accordion.tsx +5 -1
- package/src/components/ui/empty/empty-state.tsx +59 -21
- package/src/components/ui/field/field.tsx +10 -2
- package/src/components/ui/field/number-field.tsx +35 -4
- package/src/components/ui/radio-group/radio-group.tsx +10 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.js","names":["RadioGroup","KobalteRadioGroup","Circle","ComponentProps","JSX","splitProps","useFormFieldControl","cn","props","local","rest","field","id","_$createComponent","_$mergeProps","aria-label","aria-labelledby","aria-describedby","aria-invalid","aria-errormessage","class","RadioGroupItemProps","Item","controlClass","label","description","variant","media","Element","RadioGroupItem","card","data-variant","undefined","title","children","ItemInput","aria-busy","ItemControl","ItemIndicator","_$memo","_el$","_tmpl$","_$insert","_el$2","_tmpl$2","_c$","ItemLabel","_c$2","ItemDescription","RadioGroup","_$createComponent","KobalteRadioGroup","_$mergeProps","_$memo"],"sources":["../../../../src/components/ui/radio-group/radio-group.tsx"],"sourcesContent":["import { RadioGroup as KobalteRadioGroup } from '@kobalte/core/radio-group'\nimport { Circle } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { splitProps } from 'solid-js'\nimport { useFormFieldControl } from '#lib/form-field'\nimport { cn } from '#lib/utils'\n\n/**\n * RadioGroup.\n *\n * A single choice from a small, visible set. The group owns the value and the\n * arrow-key navigation, so a caller sets `value` once instead of wiring a\n * `name` and a handler onto every item.\n *\n * Below about five options a Select hides the choices behind a click, which is\n * the wrong trade; above about seven, radios become a wall and the Select wins.\n * That call belongs to the caller, and both controls are drawn from the same\n * tokens so either choice sits correctly next to the other.\n */\nexport function RadioGroup(props: ComponentProps<typeof KobalteRadioGroup>) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'id',\n 'aria-label',\n 'aria-labelledby',\n 'aria-describedby',\n 'aria-invalid',\n 'aria-errormessage',\n ])\n const field = useFormFieldControl('group', {\n id: local.id,\n 'aria-label': local['aria-label'],\n 'aria-labelledby': local['aria-labelledby'],\n 'aria-describedby': local['aria-describedby'],\n 'aria-invalid': local['aria-invalid'],\n 'aria-errormessage': local['aria-errormessage'],\n })\n return (\n <KobalteRadioGroup\n data-slot=\"radio-group\"\n id={field?.id ?? local.id}\n aria-label={local['aria-label']}\n aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}\n aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}\n aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}\n aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}\n class={cn('grid gap-2', local.class)}\n {...rest}\n />\n )\n}\n\nexport type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item> & {\n controlClass?: string\n /**\n * The option's visible label. A radio with no accessible name is unreachable\n * by a screen reader and by voice control, so supply this, a labelled child,\n * `aria-label`, or `aria-labelledby`.\n */\n label?: string\n description?: string\n /**\n * `card` draws the option as a bordered, selectable card — for a choice\n * whose options need a sentence each (a source type, a sync budget), where a\n * bare radio row reads as a form field rather than a decision. The whole card\n * is the hit target, and the selected card takes the primary edge.\n */\n variant?: 'default' | 'card'\n /** A leading icon or `EntityIcon` between the radio and the text, for `card`. */\n media?: JSX.Element\n}\n\n/**\n * A card option's label stretches an invisible layer over the whole card, so a\n * click anywhere on it — padding, media, description — selects the option\n * through the label's native association with the radio input. Wrapping the\n * card in a `<label>` would nest the item's own label inside another.\n */\nexport function RadioGroupItem(props: RadioGroupItemProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'controlClass',\n 'label',\n 'description',\n 'variant',\n 'media',\n 'children',\n 'aria-label',\n 'aria-labelledby',\n 'aria-describedby',\n 'aria-invalid',\n 'aria-busy',\n 'title',\n ])\n const card = () => local.variant === 'card'\n\n return (\n <KobalteRadioGroup.Item\n data-slot=\"radio-group-item\"\n data-variant={card() ? 'card' : undefined}\n title={local.title}\n class={cn(\n 'group/radio flex items-start gap-2.5',\n {\n 'relative rounded-lg border border-border bg-card px-3 py-2.5 transition-[color,box-shadow,border-color,background-color] ease-out hover:bg-surface-hover data-[checked]:border-primary data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-card':\n card(),\n 'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-primary-subtle':\n card(),\n },\n local.class\n )}\n {...rest}\n >\n <KobalteRadioGroup.ItemInput\n aria-label={local['aria-label']}\n aria-labelledby={local['aria-labelledby']}\n aria-describedby={local['aria-describedby']}\n aria-invalid={local['aria-invalid']}\n aria-busy={local['aria-busy']}\n title={local.title}\n />\n <KobalteRadioGroup.ItemControl\n class={cn(\n 'border-input bg-transparent flex size-4 shrink-0 items-center justify-center rounded-full border',\n 'transition-[color,box-shadow,border-color,background-color] ease-out outline-none',\n 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',\n 'data-[checked]:border-primary data-[checked]:text-primary',\n 'data-[invalid]:border-destructive data-[invalid]:ring-3 data-[invalid]:ring-destructive-subtle',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',\n local.controlClass\n )}\n >\n <KobalteRadioGroup.ItemIndicator class=\"flex items-center justify-center\">\n <Circle class=\"size-2 fill-current\" />\n </KobalteRadioGroup.ItemIndicator>\n </KobalteRadioGroup.ItemControl>\n {local.media ? (\n <span data-slot=\"radio-group-item-media\" class=\"flex shrink-0 items-center\">\n {local.media}\n </span>\n ) : null}\n {local.children ?? (\n <div class=\"grid gap-0.5 leading-none\">\n {local.label ? (\n <KobalteRadioGroup.ItemLabel\n class={cn('text-sm font-medium', card() && 'after:absolute after:inset-0')}\n >\n {local.label}\n </KobalteRadioGroup.ItemLabel>\n ) : null}\n {local.description ? (\n <KobalteRadioGroup.ItemDescription class=\"text-muted-foreground text-sm\">\n {local.description}\n </KobalteRadioGroup.ItemDescription>\n ) : null}\n </div>\n )}\n </KobalteRadioGroup.Item>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmBA,SAAgBA,aAAWQ,OAAiD;CAC1E,MAAM,CAACC,OAAOC,QAAQL,WAAWG,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;CAAmB,CACpB;CACD,MAAMG,QAAQL,oBAAoB,SAAS;EACzCM,IAAIH,MAAMG;EACV,cAAcH,MAAM;EACpB,mBAAmBA,MAAM;EACzB,oBAAoBA,MAAM;EAC1B,gBAAgBA,MAAM;EACtB,qBAAqBA,MAAM;CAC7B,CAAC;CACD,OAAAI,gBACGZ,YAAiBa,WAAA;EAAA,aAAA;EAAA,IAEhBF,KAAE;GAAA,OAAED,OAAOC,MAAMH,MAAMG;EAAE;EAAA,KAAA,gBAAA;GAAA,OACbH,MAAM;EAAa;EAAA,KAAA,qBAAA;GAAA,OACdE,QAAQ,sBAAsBF,MAAM;EAAkB;EAAA,KAAA,sBAAA;GAAA,OACrDE,QAAQ,uBAAuBF,MAAM;EAAmB;EAAA,KAAA,kBAAA;GAAA,OAC5DE,QAAQ,mBAAmBF,MAAM;EAAe;EAAA,KAAA,uBAAA;GAAA,OAC3CE,QAAQ,wBAAwBF,MAAM;EAAoB;EAAA,KAAA,WAAA;GAAA,OACtEF,GAAG,cAAcE,MAAMW,KAAK;EAAC;CAAA,GAChCV,IAAI,CAAA;AAGd;;;;;;;AA4BA,SAAgBmB,eAAerB,OAA4B;CACzD,MAAM,CAACC,OAAOC,QAAQL,WAAWG,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAO,CACR;CACD,MAAMsB,aAAarB,MAAMiB,YAAY;CAErC,OAAAb,gBACGZ,WAAkBqB,MAAIR,WAAA;EAAA,aAAA;EAAA,KAAA,kBAAA;GAAA,OAEPgB,KAAK,IAAI,SAASE,KAAAA;EAAS;EAAA,IACzCC,QAAK;GAAA,OAAExB,MAAMwB;EAAK;EAAA,KAAA,WAAA;GAAA,OACX1B,GACL,wCACA;IACE,2PACEuB,KAAK;IACP,wHACEA,KAAK;GACT,GACArB,MAAMW,KACR;EAAC;CAAA,GACGV,MAAI,EAAA,IAAAwB,WAAA;EAAA,OAAA;GAAArB,gBAEPZ,WAAkBkC,WAAS;IAAA,KAAA,gBAAA;KAAA,OACd1B,MAAM;IAAa;IAAA,KAAA,qBAAA;KAAA,OACdA,MAAM;IAAkB;IAAA,KAAA,sBAAA;KAAA,OACvBA,MAAM;IAAmB;IAAA,KAAA,kBAAA;KAAA,OAC7BA,MAAM;IAAe;IAAA,KAAA,eAAA;KAAA,OACxBA,MAAM;IAAY;IAAA,IAC7BwB,QAAK;KAAA,OAAExB,MAAMwB;IAAK;GAAA,CAAA;GAAApB,gBAEnBZ,WAAkBoC,aAAW;IAAA,KAAA,WAAA;KAAA,OACrB9B,GACL,oGACA,qFACA,oFACA,6DACA,kGACA,iEACAE,MAAMc,YACR;IAAC;IAAA,IAAAW,WAAA;KAAA,OAAArB,gBAEAZ,WAAkBqC,eAAa;MAAA,SAAA;MAAA,IAAAJ,WAAA;OAAA,OAAArB,gBAC7BX,QAAM,EAAA,SAAA,sBAAA,CAAA;MAAA;KAAA,CAAA;IAAA;GAAA,CAAA;GAAAqC,WAGVA,WAAA,CAAA,CAAA9B,MAAMkB,KAAK,CAAA,CAAA,WAAA;IAAA,IAAAa,OAAAC,OAAA;IAAAC,OAAAF,YAEP/B,MAAMkB,KAAK;IAAA,OAAAa;GAAA,EAAA,CAAA,IAEZ,IAAI;GAAAD,WACP9B,MAAMyB,mBAAQ;IAAA,IAAAS,QAAAC,QAAA;IAAAF,OAAAC,cAAA;KAAA,IAAAE,MAAAN,WAAA,CAAA,CAEV9B,MAAMe,KAAK;KAAA,aAAXqB,IAAA,IAAAhC,gBACEZ,WAAkB6C,WAAS;MAAA,KAAA,WAAA;OAAA,OACnBvC,GAAG,uBAAuBuB,KAAK,KAAK,8BAA8B;MAAC;MAAA,IAAAI,WAAA;OAAA,OAEzEzB,MAAMe;MAAK;KAAA,CAAA,IAEZ;IAAI,EAAA,CAAA,GAAA,IAAA;IAAAkB,OAAAC,cAAA;KAAA,IAAAI,OAAAR,WAAA,CAAA,CACP9B,MAAMgB,WAAW;KAAA,aAAjBsB,KAAA,IAAAlC,gBACEZ,WAAkB+C,iBAAe;MAAA,SAAA;MAAA,IAAAd,WAAA;OAAA,OAC/BzB,MAAMgB;MAAW;KAAA,CAAA,IAElB;IAAI,EAAA,CAAA,GAAA,IAAA;IAAA,OAAAkB;GAAA,EAAA,CAAA,CAEX;EAAA;CAAA,EAAA,CAAA,CAAA;AAGP"}
|
|
1
|
+
{"version":3,"file":"radio-group.js","names":["RadioGroup","KobalteRadioGroup","Circle","ComponentProps","JSX","splitProps","useFormFieldControl","cn","props","local","rest","field","id","_$createComponent","_$mergeProps","aria-label","aria-labelledby","aria-describedby","aria-invalid","aria-errormessage","class","RadioGroupItemProps","Item","controlClass","label","description","variant","media","Element","RadioGroupItem","card","data-variant","undefined","title","children","ItemInput","aria-busy","ItemControl","ItemIndicator","_$memo","_el$","_tmpl$","_$insert","_el$2","_tmpl$2","_c$","ItemLabel","_c$2","ItemDescription","RadioGroup","_$createComponent","KobalteRadioGroup","_$mergeProps","_$memo"],"sources":["../../../../src/components/ui/radio-group/radio-group.tsx"],"sourcesContent":["import { RadioGroup as KobalteRadioGroup } from '@kobalte/core/radio-group'\nimport { Circle } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { splitProps } from 'solid-js'\nimport { useFormFieldControl } from '#lib/form-field'\nimport { cn } from '#lib/utils'\n\n/**\n * RadioGroup.\n *\n * A single choice from a small, visible set. The group owns the value and the\n * arrow-key navigation, so a caller sets `value` once instead of wiring a\n * `name` and a handler onto every item.\n *\n * Below about five options a Select hides the choices behind a click, which is\n * the wrong trade; above about seven, radios become a wall and the Select wins.\n * That call belongs to the caller, and both controls are drawn from the same\n * tokens so either choice sits correctly next to the other.\n */\nexport function RadioGroup(props: ComponentProps<typeof KobalteRadioGroup>) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'id',\n 'aria-label',\n 'aria-labelledby',\n 'aria-describedby',\n 'aria-invalid',\n 'aria-errormessage',\n ])\n const field = useFormFieldControl('group', {\n id: local.id,\n 'aria-label': local['aria-label'],\n 'aria-labelledby': local['aria-labelledby'],\n 'aria-describedby': local['aria-describedby'],\n 'aria-invalid': local['aria-invalid'],\n 'aria-errormessage': local['aria-errormessage'],\n })\n return (\n <KobalteRadioGroup\n data-slot=\"radio-group\"\n id={field?.id ?? local.id}\n aria-label={local['aria-label']}\n aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}\n aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}\n aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}\n aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}\n class={cn('grid gap-2', local.class)}\n {...rest}\n />\n )\n}\n\nexport type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item> & {\n controlClass?: string\n /**\n * The option's visible label. A radio with no accessible name is unreachable\n * by a screen reader and by voice control, so supply this, a labelled child,\n * `aria-label`, or `aria-labelledby`.\n */\n label?: string\n description?: string\n /**\n * `card` draws the option as a bordered, selectable card — for a choice\n * whose options need a sentence each (a source type, a sync budget), where a\n * bare radio row reads as a form field rather than a decision. The whole card\n * is the hit target, and the selected card takes the primary edge. The radio,\n * `media` and text centre on one axis; the default variant keeps the radio on\n * the first line of its label.\n */\n variant?: 'default' | 'card'\n /** A leading icon or `EntityIcon` between the radio and the text, for `card`. */\n media?: JSX.Element\n}\n\n/**\n * A card option's label stretches an invisible layer over the whole card, so a\n * click anywhere on it — padding, media, description — selects the option\n * through the label's native association with the radio input. Wrapping the\n * card in a `<label>` would nest the item's own label inside another.\n */\nexport function RadioGroupItem(props: RadioGroupItemProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'controlClass',\n 'label',\n 'description',\n 'variant',\n 'media',\n 'children',\n 'aria-label',\n 'aria-labelledby',\n 'aria-describedby',\n 'aria-invalid',\n 'aria-busy',\n 'title',\n ])\n const card = () => local.variant === 'card'\n\n return (\n <KobalteRadioGroup.Item\n data-slot=\"radio-group-item\"\n data-variant={card() ? 'card' : undefined}\n title={local.title}\n class={cn(\n 'group/radio flex gap-2.5',\n {\n 'items-start': !card(),\n // A card centres its radio, media and text on one axis. Top alignment\n // is right for a bare row, where the radio sits on the label's first\n // line, but in a card the media is taller than that line, so a\n // top-aligned radio floats above the icon and the label beside it.\n 'items-center': card(),\n 'relative rounded-lg border border-border bg-card px-3 py-2.5 transition-[color,box-shadow,border-color,background-color] ease-out hover:bg-surface-hover data-[checked]:border-primary data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-card':\n card(),\n 'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-primary-subtle':\n card(),\n },\n local.class\n )}\n {...rest}\n >\n <KobalteRadioGroup.ItemInput\n aria-label={local['aria-label']}\n aria-labelledby={local['aria-labelledby']}\n aria-describedby={local['aria-describedby']}\n aria-invalid={local['aria-invalid']}\n aria-busy={local['aria-busy']}\n title={local.title}\n />\n <KobalteRadioGroup.ItemControl\n class={cn(\n 'border-input bg-transparent flex size-4 shrink-0 items-center justify-center rounded-full border',\n 'transition-[color,box-shadow,border-color,background-color] ease-out outline-none',\n 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',\n 'data-[checked]:border-primary data-[checked]:text-primary',\n 'data-[invalid]:border-destructive data-[invalid]:ring-3 data-[invalid]:ring-destructive-subtle',\n 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',\n local.controlClass\n )}\n >\n <KobalteRadioGroup.ItemIndicator class=\"flex items-center justify-center\">\n <Circle class=\"size-2 fill-current\" />\n </KobalteRadioGroup.ItemIndicator>\n </KobalteRadioGroup.ItemControl>\n {local.media ? (\n <span data-slot=\"radio-group-item-media\" class=\"flex shrink-0 items-center\">\n {local.media}\n </span>\n ) : null}\n {local.children ?? (\n <div class=\"grid gap-0.5 leading-none\">\n {local.label ? (\n <KobalteRadioGroup.ItemLabel\n class={cn('text-sm font-medium', card() && 'after:absolute after:inset-0')}\n >\n {local.label}\n </KobalteRadioGroup.ItemLabel>\n ) : null}\n {local.description ? (\n <KobalteRadioGroup.ItemDescription class=\"text-muted-foreground text-sm\">\n {local.description}\n </KobalteRadioGroup.ItemDescription>\n ) : null}\n </div>\n )}\n </KobalteRadioGroup.Item>\n )\n}\n"],"mappings":"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"}
|
|
@@ -18,6 +18,15 @@ export type ListRowControlProps<T extends ValidComponent = 'div'> = PolymorphicP
|
|
|
18
18
|
trailing?: JSX.Element
|
|
19
19
|
/** A second line that wraps and grows the row to fit its content. */
|
|
20
20
|
description?: string
|
|
21
|
+
/**
|
|
22
|
+
* Keep the description on one line and end it with an ellipsis instead of
|
|
23
|
+
* wrapping, so a described row stays at its minimum height however long the
|
|
24
|
+
* text is. Use it where rows must be a predictable height (a virtualised
|
|
25
|
+
* queue); leave it off where the whole description should be readable. Pair
|
|
26
|
+
* it with a `tooltip` when the full text matters, and with
|
|
27
|
+
* `stackTrailing={false}` to keep a trailing slot beside the text.
|
|
28
|
+
*/
|
|
29
|
+
truncateDescription?: boolean
|
|
21
30
|
/**
|
|
22
31
|
* Whether a described row may drop its trailing slot onto its own line when
|
|
23
32
|
* the row gets narrow (the default). Pass `false` for a short, truncating
|
|
@@ -43,6 +52,7 @@ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowC
|
|
|
43
52
|
'leading',
|
|
44
53
|
'trailing',
|
|
45
54
|
'description',
|
|
55
|
+
'truncateDescription',
|
|
46
56
|
'stackTrailing',
|
|
47
57
|
'type',
|
|
48
58
|
'tabIndex',
|
|
@@ -92,7 +102,8 @@ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowC
|
|
|
92
102
|
<span
|
|
93
103
|
data-slot="list-row-description"
|
|
94
104
|
class={cn(
|
|
95
|
-
'text-xs
|
|
105
|
+
'text-xs',
|
|
106
|
+
{ truncate: local.truncateDescription, 'break-words': !local.truncateDescription },
|
|
96
107
|
local.selected ? 'text-foreground' : 'text-muted-foreground'
|
|
97
108
|
)}
|
|
98
109
|
>
|
|
@@ -37,7 +37,11 @@ export function AccordionTrigger(props: ComponentProps<typeof KobalteAccordion.T
|
|
|
37
37
|
<KobalteAccordion.Header class="flex">
|
|
38
38
|
<KobalteAccordion.Trigger
|
|
39
39
|
class={cn(
|
|
40
|
-
|
|
40
|
+
// `group/accordion` is what the chevron's `group-data-[expanded]/accordion`
|
|
41
|
+
// reads. Kobalte puts `data-expanded` on this trigger, not on the item, so
|
|
42
|
+
// the trigger is the group; without the class the rotation selector has no
|
|
43
|
+
// ancestor to match and the chevron never turns.
|
|
44
|
+
'group/accordion flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-medium',
|
|
41
45
|
'transition-colors ease-out outline-none',
|
|
42
46
|
'hover:text-foreground',
|
|
43
47
|
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
@@ -2,22 +2,45 @@ import { Search } from 'lucide-solid'
|
|
|
2
2
|
import type { ComponentProps, JSX } from 'solid-js'
|
|
3
3
|
import { Show, splitProps } from 'solid-js'
|
|
4
4
|
import { ActionButton } from '../../composites/action-button'
|
|
5
|
+
import { cn } from '../../../lib/utils'
|
|
5
6
|
import { Spinner } from '../spinner'
|
|
6
7
|
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './empty'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
type EmptyStateSecondaryAction =
|
|
10
|
+
| {
|
|
11
|
+
secondaryAction?: undefined
|
|
12
|
+
secondaryActionLabel?: undefined
|
|
13
|
+
secondaryActionTooltip?: never
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
/** A second, quieter way out beside `action`: "Open settings" next to "Retry". */
|
|
17
|
+
secondaryAction: () => void
|
|
18
|
+
/** Required: there is no sensible default name for an arbitrary second action. */
|
|
19
|
+
secondaryActionLabel: string
|
|
20
|
+
secondaryActionTooltip?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type EmptyStateProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'aria-live'> &
|
|
24
|
+
EmptyStateSecondaryAction & {
|
|
25
|
+
title: string
|
|
26
|
+
detail: string
|
|
27
|
+
busy?: boolean
|
|
28
|
+
icon?: JSX.Element
|
|
29
|
+
announceAs?: 'alert' | 'status'
|
|
30
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6
|
|
31
|
+
action?: () => void
|
|
32
|
+
actionLabel?: string
|
|
33
|
+
actionTooltip?: string
|
|
34
|
+
}
|
|
19
35
|
|
|
20
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A semantic empty, loading, or error state built from the shared empty parts.
|
|
38
|
+
*
|
|
39
|
+
* `action` is the primary way forward. `secondaryAction` adds one quieter
|
|
40
|
+
* alternative after it, in a `secondary` button, so the primary stays first in
|
|
41
|
+
* the tab order. Two actions side by side is the limit: a state that needs more
|
|
42
|
+
* should compose the `Empty*` parts directly.
|
|
43
|
+
*/
|
|
21
44
|
export function EmptyState(props: EmptyStateProps) {
|
|
22
45
|
const [local, rest] = splitProps(props, [
|
|
23
46
|
'title',
|
|
@@ -29,6 +52,9 @@ export function EmptyState(props: EmptyStateProps) {
|
|
|
29
52
|
'action',
|
|
30
53
|
'actionLabel',
|
|
31
54
|
'actionTooltip',
|
|
55
|
+
'secondaryAction',
|
|
56
|
+
'secondaryActionLabel',
|
|
57
|
+
'secondaryActionTooltip',
|
|
32
58
|
'class',
|
|
33
59
|
])
|
|
34
60
|
|
|
@@ -55,15 +81,27 @@ export function EmptyState(props: EmptyStateProps) {
|
|
|
55
81
|
</EmptyTitle>
|
|
56
82
|
<EmptyDescription>{local.detail}</EmptyDescription>
|
|
57
83
|
</EmptyHeader>
|
|
58
|
-
<Show when={local.action}>
|
|
59
|
-
<EmptyContent>
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
<Show when={local.action || local.secondaryAction}>
|
|
85
|
+
<EmptyContent class={cn({ 'flex-row flex-wrap justify-center': !!local.secondaryAction })}>
|
|
86
|
+
<Show when={local.action}>
|
|
87
|
+
<ActionButton
|
|
88
|
+
tooltip={local.actionTooltip ?? local.actionLabel ?? 'Try again'}
|
|
89
|
+
aria-label={local.actionLabel ?? 'Try again'}
|
|
90
|
+
onClick={local.action}
|
|
91
|
+
>
|
|
92
|
+
{local.actionLabel ?? 'Try again'}
|
|
93
|
+
</ActionButton>
|
|
94
|
+
</Show>
|
|
95
|
+
<Show when={local.secondaryAction}>
|
|
96
|
+
<ActionButton
|
|
97
|
+
variant="secondary"
|
|
98
|
+
tooltip={local.secondaryActionTooltip ?? local.secondaryActionLabel}
|
|
99
|
+
aria-label={local.secondaryActionLabel}
|
|
100
|
+
onClick={local.secondaryAction}
|
|
101
|
+
>
|
|
102
|
+
{local.secondaryActionLabel}
|
|
103
|
+
</ActionButton>
|
|
104
|
+
</Show>
|
|
67
105
|
</EmptyContent>
|
|
68
106
|
</Show>
|
|
69
107
|
</Empty>
|
|
@@ -195,8 +195,12 @@ export type FieldGroupProps = ComponentProps<'div'> & {
|
|
|
195
195
|
* that needs the full width says `class="col-span-full"`. Below `sm` it is
|
|
196
196
|
* one column again, because two inputs side by side on a phone are each too
|
|
197
197
|
* narrow to read.
|
|
198
|
+
*
|
|
199
|
+
* `3` is the same grid with three columns from `sm`, for a row of short values
|
|
200
|
+
* (a trio of limits, a day count and two thresholds). Each column is a third of
|
|
201
|
+
* the group, so reach for `2` when a field holds a path, a URL or a sentence.
|
|
198
202
|
*/
|
|
199
|
-
columns?: 1 | 2
|
|
203
|
+
columns?: 1 | 2 | 3
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/** A visual stack for related controls that does not imply fieldset semantics. */
|
|
@@ -206,7 +210,11 @@ export function FieldGroup(props: FieldGroupProps) {
|
|
|
206
210
|
<div
|
|
207
211
|
data-slot="field-group"
|
|
208
212
|
class={cn(
|
|
209
|
-
|
|
213
|
+
{
|
|
214
|
+
'grid gap-4 sm:grid-cols-2': local.columns === 2,
|
|
215
|
+
'grid gap-4 sm:grid-cols-3': local.columns === 3,
|
|
216
|
+
'flex flex-col gap-4': local.columns !== 2 && local.columns !== 3,
|
|
217
|
+
},
|
|
210
218
|
local.class
|
|
211
219
|
)}
|
|
212
220
|
{...rest}
|
|
@@ -8,6 +8,20 @@ export type NumberFieldProps = {
|
|
|
8
8
|
value: number
|
|
9
9
|
min: number
|
|
10
10
|
max: number
|
|
11
|
+
/**
|
|
12
|
+
* The increment of the spinner and the arrow keys, and the grid a typed value
|
|
13
|
+
* must sit on: `min` plus a whole number of steps. Use it for stepped or
|
|
14
|
+
* fractional values (`0.05` for a 0-1 score, `5` for a percentage in fives).
|
|
15
|
+
* Without it a field steps by 1, or by `any` when `integer` is `false`, which
|
|
16
|
+
* accepts every decimal but leaves the spinner moving in whole numbers.
|
|
17
|
+
*/
|
|
18
|
+
step?: number
|
|
19
|
+
/**
|
|
20
|
+
* Whether only whole numbers are valid. It defaults to `true`, or, when a
|
|
21
|
+
* `step` is given, to whether both the step and its base `min` are whole
|
|
22
|
+
* numbers, so `step={0.05}` accepts decimals without also saying
|
|
23
|
+
* `integer={false}`, and `min={0.5} step={1}` accepts 0.5, 1.5, 2.5.
|
|
24
|
+
*/
|
|
11
25
|
integer?: boolean
|
|
12
26
|
onChange: (value: number) => void
|
|
13
27
|
class?: string
|
|
@@ -16,13 +30,20 @@ export type NumberFieldProps = {
|
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
32
|
* A controlled numeric field that keeps an editable draft while validating
|
|
19
|
-
* range and
|
|
20
|
-
* to the latest controlled value when focus leaves the input
|
|
33
|
+
* range, integer and step constraints. Invalid drafts are announced and restored
|
|
34
|
+
* to the latest controlled value when focus leaves the input, so `onChange`
|
|
35
|
+
* only ever receives a value the field accepts — a caller never needs a
|
|
36
|
+
* `Number(value) || fallback` of its own.
|
|
21
37
|
*/
|
|
22
38
|
export function NumberField(props: NumberFieldProps) {
|
|
23
39
|
const [draft, setDraft] = createSignal(String(props.value))
|
|
24
40
|
const [error, setError] = createSignal('')
|
|
25
|
-
|
|
41
|
+
// Whole numbers only when nothing on the grid is fractional: a whole step from
|
|
42
|
+
// a fractional `min` (0.5, 1.5, 2.5) puts the spinner's own values off the
|
|
43
|
+
// integers, so inference needs the step and its base to both be whole.
|
|
44
|
+
const integer = () =>
|
|
45
|
+
props.integer ??
|
|
46
|
+
(props.step === undefined || (Number.isInteger(props.step) && Number.isInteger(props.min)))
|
|
26
47
|
|
|
27
48
|
createEffect(() => {
|
|
28
49
|
setDraft(String(props.value))
|
|
@@ -38,6 +59,16 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
38
59
|
if (next < props.min || next > props.max) {
|
|
39
60
|
return `${props.label} must be between ${props.min} and ${props.max}.`
|
|
40
61
|
}
|
|
62
|
+
if (props.step !== undefined && props.step > 0) {
|
|
63
|
+
// Compare in whole steps with a tolerance: 0.15 / 0.05 is 3.0000000000000004,
|
|
64
|
+
// and rejecting it would make the spinner's own values invalid.
|
|
65
|
+
const steps = (next - props.min) / props.step
|
|
66
|
+
if (Math.abs(steps - Math.round(steps)) > 1e-9) {
|
|
67
|
+
return props.min === 0
|
|
68
|
+
? `${props.label} must be a multiple of ${props.step}.`
|
|
69
|
+
: `${props.label} must be ${props.min} plus a multiple of ${props.step}.`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
41
72
|
return ''
|
|
42
73
|
}
|
|
43
74
|
|
|
@@ -48,7 +79,7 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
48
79
|
value={draft()}
|
|
49
80
|
min={props.min}
|
|
50
81
|
max={props.max}
|
|
51
|
-
step={integer() ? 1 : 'any'}
|
|
82
|
+
step={props.step ?? (integer() ? 1 : 'any')}
|
|
52
83
|
required
|
|
53
84
|
onInput={(event) => {
|
|
54
85
|
const raw = event.currentTarget.value
|
|
@@ -63,7 +63,9 @@ export type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item>
|
|
|
63
63
|
* `card` draws the option as a bordered, selectable card — for a choice
|
|
64
64
|
* whose options need a sentence each (a source type, a sync budget), where a
|
|
65
65
|
* bare radio row reads as a form field rather than a decision. The whole card
|
|
66
|
-
* is the hit target, and the selected card takes the primary edge.
|
|
66
|
+
* is the hit target, and the selected card takes the primary edge. The radio,
|
|
67
|
+
* `media` and text centre on one axis; the default variant keeps the radio on
|
|
68
|
+
* the first line of its label.
|
|
67
69
|
*/
|
|
68
70
|
variant?: 'default' | 'card'
|
|
69
71
|
/** A leading icon or `EntityIcon` between the radio and the text, for `card`. */
|
|
@@ -100,8 +102,14 @@ export function RadioGroupItem(props: RadioGroupItemProps) {
|
|
|
100
102
|
data-variant={card() ? 'card' : undefined}
|
|
101
103
|
title={local.title}
|
|
102
104
|
class={cn(
|
|
103
|
-
'group/radio flex
|
|
105
|
+
'group/radio flex gap-2.5',
|
|
104
106
|
{
|
|
107
|
+
'items-start': !card(),
|
|
108
|
+
// A card centres its radio, media and text on one axis. Top alignment
|
|
109
|
+
// is right for a bare row, where the radio sits on the label's first
|
|
110
|
+
// line, but in a card the media is taller than that line, so a
|
|
111
|
+
// top-aligned radio floats above the icon and the label beside it.
|
|
112
|
+
'items-center': card(),
|
|
105
113
|
'relative rounded-lg border border-border bg-card px-3 py-2.5 transition-[color,box-shadow,border-color,background-color] ease-out hover:bg-surface-hover data-[checked]:border-primary data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-card':
|
|
106
114
|
card(),
|
|
107
115
|
'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-primary-subtle':
|
package/package.json
CHANGED
|
@@ -18,6 +18,15 @@ export type ListRowControlProps<T extends ValidComponent = 'div'> = PolymorphicP
|
|
|
18
18
|
trailing?: JSX.Element
|
|
19
19
|
/** A second line that wraps and grows the row to fit its content. */
|
|
20
20
|
description?: string
|
|
21
|
+
/**
|
|
22
|
+
* Keep the description on one line and end it with an ellipsis instead of
|
|
23
|
+
* wrapping, so a described row stays at its minimum height however long the
|
|
24
|
+
* text is. Use it where rows must be a predictable height (a virtualised
|
|
25
|
+
* queue); leave it off where the whole description should be readable. Pair
|
|
26
|
+
* it with a `tooltip` when the full text matters, and with
|
|
27
|
+
* `stackTrailing={false}` to keep a trailing slot beside the text.
|
|
28
|
+
*/
|
|
29
|
+
truncateDescription?: boolean
|
|
21
30
|
/**
|
|
22
31
|
* Whether a described row may drop its trailing slot onto its own line when
|
|
23
32
|
* the row gets narrow (the default). Pass `false` for a short, truncating
|
|
@@ -43,6 +52,7 @@ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowC
|
|
|
43
52
|
'leading',
|
|
44
53
|
'trailing',
|
|
45
54
|
'description',
|
|
55
|
+
'truncateDescription',
|
|
46
56
|
'stackTrailing',
|
|
47
57
|
'type',
|
|
48
58
|
'tabIndex',
|
|
@@ -92,7 +102,8 @@ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowC
|
|
|
92
102
|
<span
|
|
93
103
|
data-slot="list-row-description"
|
|
94
104
|
class={cn(
|
|
95
|
-
'text-xs
|
|
105
|
+
'text-xs',
|
|
106
|
+
{ truncate: local.truncateDescription, 'break-words': !local.truncateDescription },
|
|
96
107
|
local.selected ? 'text-foreground' : 'text-muted-foreground'
|
|
97
108
|
)}
|
|
98
109
|
>
|
|
@@ -37,7 +37,11 @@ export function AccordionTrigger(props: ComponentProps<typeof KobalteAccordion.T
|
|
|
37
37
|
<KobalteAccordion.Header class="flex">
|
|
38
38
|
<KobalteAccordion.Trigger
|
|
39
39
|
class={cn(
|
|
40
|
-
|
|
40
|
+
// `group/accordion` is what the chevron's `group-data-[expanded]/accordion`
|
|
41
|
+
// reads. Kobalte puts `data-expanded` on this trigger, not on the item, so
|
|
42
|
+
// the trigger is the group; without the class the rotation selector has no
|
|
43
|
+
// ancestor to match and the chevron never turns.
|
|
44
|
+
'group/accordion flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-medium',
|
|
41
45
|
'transition-colors ease-out outline-none',
|
|
42
46
|
'hover:text-foreground',
|
|
43
47
|
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
@@ -2,22 +2,45 @@ import { Search } from 'lucide-solid'
|
|
|
2
2
|
import type { ComponentProps, JSX } from 'solid-js'
|
|
3
3
|
import { Show, splitProps } from 'solid-js'
|
|
4
4
|
import { ActionButton } from '../../composites/action-button'
|
|
5
|
+
import { cn } from '#lib/utils'
|
|
5
6
|
import { Spinner } from '../spinner'
|
|
6
7
|
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './empty'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
type EmptyStateSecondaryAction =
|
|
10
|
+
| {
|
|
11
|
+
secondaryAction?: undefined
|
|
12
|
+
secondaryActionLabel?: undefined
|
|
13
|
+
secondaryActionTooltip?: never
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
/** A second, quieter way out beside `action`: "Open settings" next to "Retry". */
|
|
17
|
+
secondaryAction: () => void
|
|
18
|
+
/** Required: there is no sensible default name for an arbitrary second action. */
|
|
19
|
+
secondaryActionLabel: string
|
|
20
|
+
secondaryActionTooltip?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type EmptyStateProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'aria-live'> &
|
|
24
|
+
EmptyStateSecondaryAction & {
|
|
25
|
+
title: string
|
|
26
|
+
detail: string
|
|
27
|
+
busy?: boolean
|
|
28
|
+
icon?: JSX.Element
|
|
29
|
+
announceAs?: 'alert' | 'status'
|
|
30
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6
|
|
31
|
+
action?: () => void
|
|
32
|
+
actionLabel?: string
|
|
33
|
+
actionTooltip?: string
|
|
34
|
+
}
|
|
19
35
|
|
|
20
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A semantic empty, loading, or error state built from the shared empty parts.
|
|
38
|
+
*
|
|
39
|
+
* `action` is the primary way forward. `secondaryAction` adds one quieter
|
|
40
|
+
* alternative after it, in a `secondary` button, so the primary stays first in
|
|
41
|
+
* the tab order. Two actions side by side is the limit: a state that needs more
|
|
42
|
+
* should compose the `Empty*` parts directly.
|
|
43
|
+
*/
|
|
21
44
|
export function EmptyState(props: EmptyStateProps) {
|
|
22
45
|
const [local, rest] = splitProps(props, [
|
|
23
46
|
'title',
|
|
@@ -29,6 +52,9 @@ export function EmptyState(props: EmptyStateProps) {
|
|
|
29
52
|
'action',
|
|
30
53
|
'actionLabel',
|
|
31
54
|
'actionTooltip',
|
|
55
|
+
'secondaryAction',
|
|
56
|
+
'secondaryActionLabel',
|
|
57
|
+
'secondaryActionTooltip',
|
|
32
58
|
'class',
|
|
33
59
|
])
|
|
34
60
|
|
|
@@ -55,15 +81,27 @@ export function EmptyState(props: EmptyStateProps) {
|
|
|
55
81
|
</EmptyTitle>
|
|
56
82
|
<EmptyDescription>{local.detail}</EmptyDescription>
|
|
57
83
|
</EmptyHeader>
|
|
58
|
-
<Show when={local.action}>
|
|
59
|
-
<EmptyContent>
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
<Show when={local.action || local.secondaryAction}>
|
|
85
|
+
<EmptyContent class={cn({ 'flex-row flex-wrap justify-center': !!local.secondaryAction })}>
|
|
86
|
+
<Show when={local.action}>
|
|
87
|
+
<ActionButton
|
|
88
|
+
tooltip={local.actionTooltip ?? local.actionLabel ?? 'Try again'}
|
|
89
|
+
aria-label={local.actionLabel ?? 'Try again'}
|
|
90
|
+
onClick={local.action}
|
|
91
|
+
>
|
|
92
|
+
{local.actionLabel ?? 'Try again'}
|
|
93
|
+
</ActionButton>
|
|
94
|
+
</Show>
|
|
95
|
+
<Show when={local.secondaryAction}>
|
|
96
|
+
<ActionButton
|
|
97
|
+
variant="secondary"
|
|
98
|
+
tooltip={local.secondaryActionTooltip ?? local.secondaryActionLabel}
|
|
99
|
+
aria-label={local.secondaryActionLabel}
|
|
100
|
+
onClick={local.secondaryAction}
|
|
101
|
+
>
|
|
102
|
+
{local.secondaryActionLabel}
|
|
103
|
+
</ActionButton>
|
|
104
|
+
</Show>
|
|
67
105
|
</EmptyContent>
|
|
68
106
|
</Show>
|
|
69
107
|
</Empty>
|
|
@@ -195,8 +195,12 @@ export type FieldGroupProps = ComponentProps<'div'> & {
|
|
|
195
195
|
* that needs the full width says `class="col-span-full"`. Below `sm` it is
|
|
196
196
|
* one column again, because two inputs side by side on a phone are each too
|
|
197
197
|
* narrow to read.
|
|
198
|
+
*
|
|
199
|
+
* `3` is the same grid with three columns from `sm`, for a row of short values
|
|
200
|
+
* (a trio of limits, a day count and two thresholds). Each column is a third of
|
|
201
|
+
* the group, so reach for `2` when a field holds a path, a URL or a sentence.
|
|
198
202
|
*/
|
|
199
|
-
columns?: 1 | 2
|
|
203
|
+
columns?: 1 | 2 | 3
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/** A visual stack for related controls that does not imply fieldset semantics. */
|
|
@@ -206,7 +210,11 @@ export function FieldGroup(props: FieldGroupProps) {
|
|
|
206
210
|
<div
|
|
207
211
|
data-slot="field-group"
|
|
208
212
|
class={cn(
|
|
209
|
-
|
|
213
|
+
{
|
|
214
|
+
'grid gap-4 sm:grid-cols-2': local.columns === 2,
|
|
215
|
+
'grid gap-4 sm:grid-cols-3': local.columns === 3,
|
|
216
|
+
'flex flex-col gap-4': local.columns !== 2 && local.columns !== 3,
|
|
217
|
+
},
|
|
210
218
|
local.class
|
|
211
219
|
)}
|
|
212
220
|
{...rest}
|
|
@@ -8,6 +8,20 @@ export type NumberFieldProps = {
|
|
|
8
8
|
value: number
|
|
9
9
|
min: number
|
|
10
10
|
max: number
|
|
11
|
+
/**
|
|
12
|
+
* The increment of the spinner and the arrow keys, and the grid a typed value
|
|
13
|
+
* must sit on: `min` plus a whole number of steps. Use it for stepped or
|
|
14
|
+
* fractional values (`0.05` for a 0-1 score, `5` for a percentage in fives).
|
|
15
|
+
* Without it a field steps by 1, or by `any` when `integer` is `false`, which
|
|
16
|
+
* accepts every decimal but leaves the spinner moving in whole numbers.
|
|
17
|
+
*/
|
|
18
|
+
step?: number
|
|
19
|
+
/**
|
|
20
|
+
* Whether only whole numbers are valid. It defaults to `true`, or, when a
|
|
21
|
+
* `step` is given, to whether both the step and its base `min` are whole
|
|
22
|
+
* numbers, so `step={0.05}` accepts decimals without also saying
|
|
23
|
+
* `integer={false}`, and `min={0.5} step={1}` accepts 0.5, 1.5, 2.5.
|
|
24
|
+
*/
|
|
11
25
|
integer?: boolean
|
|
12
26
|
onChange: (value: number) => void
|
|
13
27
|
class?: string
|
|
@@ -16,13 +30,20 @@ export type NumberFieldProps = {
|
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
32
|
* A controlled numeric field that keeps an editable draft while validating
|
|
19
|
-
* range and
|
|
20
|
-
* to the latest controlled value when focus leaves the input
|
|
33
|
+
* range, integer and step constraints. Invalid drafts are announced and restored
|
|
34
|
+
* to the latest controlled value when focus leaves the input, so `onChange`
|
|
35
|
+
* only ever receives a value the field accepts — a caller never needs a
|
|
36
|
+
* `Number(value) || fallback` of its own.
|
|
21
37
|
*/
|
|
22
38
|
export function NumberField(props: NumberFieldProps) {
|
|
23
39
|
const [draft, setDraft] = createSignal(String(props.value))
|
|
24
40
|
const [error, setError] = createSignal('')
|
|
25
|
-
|
|
41
|
+
// Whole numbers only when nothing on the grid is fractional: a whole step from
|
|
42
|
+
// a fractional `min` (0.5, 1.5, 2.5) puts the spinner's own values off the
|
|
43
|
+
// integers, so inference needs the step and its base to both be whole.
|
|
44
|
+
const integer = () =>
|
|
45
|
+
props.integer ??
|
|
46
|
+
(props.step === undefined || (Number.isInteger(props.step) && Number.isInteger(props.min)))
|
|
26
47
|
|
|
27
48
|
createEffect(() => {
|
|
28
49
|
setDraft(String(props.value))
|
|
@@ -38,6 +59,16 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
38
59
|
if (next < props.min || next > props.max) {
|
|
39
60
|
return `${props.label} must be between ${props.min} and ${props.max}.`
|
|
40
61
|
}
|
|
62
|
+
if (props.step !== undefined && props.step > 0) {
|
|
63
|
+
// Compare in whole steps with a tolerance: 0.15 / 0.05 is 3.0000000000000004,
|
|
64
|
+
// and rejecting it would make the spinner's own values invalid.
|
|
65
|
+
const steps = (next - props.min) / props.step
|
|
66
|
+
if (Math.abs(steps - Math.round(steps)) > 1e-9) {
|
|
67
|
+
return props.min === 0
|
|
68
|
+
? `${props.label} must be a multiple of ${props.step}.`
|
|
69
|
+
: `${props.label} must be ${props.min} plus a multiple of ${props.step}.`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
41
72
|
return ''
|
|
42
73
|
}
|
|
43
74
|
|
|
@@ -48,7 +79,7 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
48
79
|
value={draft()}
|
|
49
80
|
min={props.min}
|
|
50
81
|
max={props.max}
|
|
51
|
-
step={integer() ? 1 : 'any'}
|
|
82
|
+
step={props.step ?? (integer() ? 1 : 'any')}
|
|
52
83
|
required
|
|
53
84
|
onInput={(event) => {
|
|
54
85
|
const raw = event.currentTarget.value
|
|
@@ -63,7 +63,9 @@ export type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item>
|
|
|
63
63
|
* `card` draws the option as a bordered, selectable card — for a choice
|
|
64
64
|
* whose options need a sentence each (a source type, a sync budget), where a
|
|
65
65
|
* bare radio row reads as a form field rather than a decision. The whole card
|
|
66
|
-
* is the hit target, and the selected card takes the primary edge.
|
|
66
|
+
* is the hit target, and the selected card takes the primary edge. The radio,
|
|
67
|
+
* `media` and text centre on one axis; the default variant keeps the radio on
|
|
68
|
+
* the first line of its label.
|
|
67
69
|
*/
|
|
68
70
|
variant?: 'default' | 'card'
|
|
69
71
|
/** A leading icon or `EntityIcon` between the radio and the text, for `card`. */
|
|
@@ -100,8 +102,14 @@ export function RadioGroupItem(props: RadioGroupItemProps) {
|
|
|
100
102
|
data-variant={card() ? 'card' : undefined}
|
|
101
103
|
title={local.title}
|
|
102
104
|
class={cn(
|
|
103
|
-
'group/radio flex
|
|
105
|
+
'group/radio flex gap-2.5',
|
|
104
106
|
{
|
|
107
|
+
'items-start': !card(),
|
|
108
|
+
// A card centres its radio, media and text on one axis. Top alignment
|
|
109
|
+
// is right for a bare row, where the radio sits on the label's first
|
|
110
|
+
// line, but in a card the media is taller than that line, so a
|
|
111
|
+
// top-aligned radio floats above the icon and the label beside it.
|
|
112
|
+
'items-center': card(),
|
|
105
113
|
'relative rounded-lg border border-border bg-card px-3 py-2.5 transition-[color,box-shadow,border-color,background-color] ease-out hover:bg-surface-hover data-[checked]:border-primary data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-card':
|
|
106
114
|
card(),
|
|
107
115
|
'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-primary-subtle':
|