update from cdrl

This commit is contained in:
2026-07-15 11:20:47 -04:00
parent 0f9f42be5a
commit f1745b8a1b
8 changed files with 195 additions and 353 deletions

View File

@@ -324,12 +324,12 @@ export interface AutoTablePagination {
CurrentPage: number; CurrentPage: number;
NextPage?: number; NextPage?: number;
PreviousPage?: number; PreviousPage?: number;
TotalPages: number; TotalPages?: number;
TotalItems: number; TotalItems?: number;
MaxItemsPerPage: number; MaxItemsPerPage: number;
ItemsThisPage?: number; ItemsThisPage?: number;
ViewRangeLower: number; ViewRangeLower?: number;
ViewRangeUpper: number; ViewRangeUpper?: number;
} }
export interface AutoTableOrderBy { export interface AutoTableOrderBy {
@@ -449,7 +449,7 @@ export function AutoTableDateSearch(props: AutoTableDateSearchProps) {
<DatePicker <DatePicker
small={true} small={true}
clearable={true} clearable={true}
value={liveValue} value={liveValue()}
onchange={props.onchange} onchange={props.onchange}
placeholder={props.placeholder} placeholder={props.placeholder}
/> />
@@ -4181,33 +4181,27 @@ function PaginationButton(props: JSX.ButtonHTMLAttributes<HTMLButtonElement>) {
</button>; </button>;
} }
interface TdProps { export function TdLeft(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
class?: string;
style?: string;
children: any;
}
export function TdLeft(props: TdProps) {
return <td class={"text-left " + (props.class || "")} style={props.style}>{props.children}</td>; return <td class={"text-left " + (props.class || "")} style={props.style}>{props.children}</td>;
} }
export function TdRight(props: TdProps) { export function TdRight(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
return <td class={"text-right " + (props.class || "")} style={props.style}>{props.children}</td>; return <td class={"text-right " + (props.class || "")} style={props.style}>{props.children}</td>;
} }
export function TdCenter(props: TdProps) { export function TdCenter(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
return <td class={"text-center " + (props.class || "")} style={props.style}>{props.children}</td>; return <td class={"text-center " + (props.class || "")} style={props.style}>{props.children}</td>;
} }
export function TdUniformLeft(props: TdProps) { export function TdUniformLeft(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
return <td class={"text-left " + (props.class || "")} style={props.style}>{props.children}</td>; return <td class={"text-left " + (props.class || "")} style={props.style}>{props.children}</td>;
} }
export function TdUniformRight(props: TdProps) { export function TdUniformRight(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
return <td class={"text-right " + (props.class || "")} style={props.style}>{props.children}</td>; return <td class={"text-right " + (props.class || "")} style={props.style}>{props.children}</td>;
} }
export function TdUniformCenter(props: TdProps) { export function TdUniformCenter(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
return <td class={"text-center " + (props.class || "")} style={props.style}>{props.children}</td>; return <td class={"text-center " + (props.class || "")} style={props.style}>{props.children}</td>;
} }

View File

@@ -1,4 +1,4 @@
import { JSX, JSXElement } from "solid-js"; import { JSX, JSXElement, splitProps } from "solid-js";
import { Icon } from "./Icons.tsx"; import { Icon } from "./Icons.tsx";
export const BUTTON_COLOR_NEUTRAL = "neutral"; export const BUTTON_COLOR_NEUTRAL = "neutral";
@@ -63,37 +63,36 @@ interface ButtonUIProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
} }
export function ButtonUI(props: ButtonUIProps) { export function ButtonUI(props: ButtonUIProps) {
const hasText = () => props.text !== undefined ? !!props.text : !props.icon; const [local, buttonProps] = splitProps(props, ["text", "icon", "outline", "color", "small", "class", "type", "children"]);
const hasText = () => local.text !== undefined ? !!local.text : !local.icon;
const cls = () => { const cls = () => {
let c = BASE; let c = BASE;
if (props.outline) { if (local.outline) {
c += " " + OUTLINE_BASE + " " + (OUTLINE_COLORS[props.color!] || OUTLINE_COLORS["neutral"]); c += " " + OUTLINE_BASE + " " + (OUTLINE_COLORS[local.color!] || OUTLINE_COLORS["neutral"]);
} else { } else {
c += " " + (COLORS[props.color!] || COLORS["neutral"]); c += " " + (COLORS[local.color!] || COLORS["neutral"]);
} }
if (props.small) { if (local.small) {
c += props.icon ? " py-1 px-3" : " py-1 px-4"; c += local.icon ? " py-1 px-3" : " py-1 px-4";
} else if (props.icon && !hasText()) { } else if (local.icon && !hasText()) {
c += " py-2 px-3"; c += " py-2 px-3";
} else if (props.icon) { } else if (local.icon) {
c += " py-2 px-5"; c += " py-2 px-5";
} else { } else {
c += " py-2 px-8"; c += " py-2 px-8";
} }
if (local.class) c += " " + local.class;
return c; return c;
}; };
return ( return (
<button <button
type={props.type || "button"} type={local.type || "button"}
onclick={(e) => typeof props.onclick === "function" && props.onclick(e)}
onmousedown={(e) => typeof props.onmousedown === "function" && props.onmousedown(e)}
disabled={props.disabled}
title={props.title}
class={cls()} class={cls()}
{...buttonProps}
> >
{props.children} {local.children}
</button> </button>
); );
} }

View File

@@ -1,4 +1,4 @@
import { createSignal, createEffect, Show, onMount, onCleanup, For } from "solid-js"; import { createSignal, createEffect, Show, onMount, onCleanup, For, JSX } from "solid-js";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import { Icon } from "./Icons.tsx"; import { Icon } from "./Icons.tsx";
import { FormInput } from "./Forms.tsx"; import { FormInput } from "./Forms.tsx";
@@ -179,7 +179,10 @@ const DATE_PICKER_ICON_BTN = "absolute inset-y-0 right-0 z-[1] flex items-center
const DATE_PICKER_CLEAR_BTN = "absolute inset-y-0 right-8 z-[1] flex items-center justify-center bg-transparent border-0 px-1.5 cursor-pointer text-ink-muted leading-none hover:text-ink pointer-events-auto"; const DATE_PICKER_CLEAR_BTN = "absolute inset-y-0 right-8 z-[1] flex items-center justify-center bg-transparent border-0 px-1.5 cursor-pointer text-ink-muted leading-none hover:text-ink pointer-events-auto";
interface DatePickerProps { interface DatePickerProps {
value?: MaybeAccessor<string>; id?: string;
name?: string;
value?: string;
oninput?: JSX.InputEventHandlerUnion<HTMLInputElement, InputEvent>;
onchange?: (value: string) => void; onchange?: (value: string) => void;
placeholder?: string; placeholder?: string;
small?: boolean; small?: boolean;

View File

@@ -107,8 +107,9 @@ export interface FormInputProps
extends extends
FormInputCommonProps, FormInputCommonProps,
Pick<JSX.InputHTMLAttributes<HTMLInputElement>, CommonInputAttrs | "type" | "max" | "min" | "step" | "accept"> { Pick<JSX.InputHTMLAttributes<HTMLInputElement>, CommonInputAttrs | "type" | "max" | "min" | "step" | "accept"> {
small?: boolean;
ref?: (el: HTMLInputElement) => void; ref?: (el: HTMLInputElement) => void;
prefix?: JSXElement;
small?: boolean;
class?: string; class?: string;
onDark?: boolean; onDark?: boolean;
} }
@@ -121,10 +122,6 @@ export interface FormTextareaProps
onDark?: boolean; onDark?: boolean;
} }
export interface FormInputGroupProps extends FormInputProps {
prefix?: JSXElement;
}
export interface FormNumberInputProps extends FormInputProps { export interface FormNumberInputProps extends FormInputProps {
int?: boolean; int?: boolean;
unsigned?: boolean; unsigned?: boolean;
@@ -171,7 +168,7 @@ function _comboboxRootCls(className?: string, fieldWidth: ComboboxFieldWidth = "
} }
export interface FormComboboxProps extends Pick<JSX.SelectHTMLAttributes<HTMLSelectElement>, CommonSelectAttrs> { export interface FormComboboxProps extends Pick<JSX.SelectHTMLAttributes<HTMLSelectElement>, CommonSelectAttrs> {
options: FormSelectOption[]; options?: FormSelectOption[];
placeholder?: string; placeholder?: string;
searchable?: boolean; searchable?: boolean;
searchPlaceholder?: string; searchPlaceholder?: string;
@@ -227,90 +224,104 @@ export interface FormMultiSelectTriggerProps {
class?: string; class?: string;
} }
export function FormInput(props: FormInputProps) { export function FormInputOld(props: FormInputProps) {
const cls = () => _inputCls(props.small, props.error, props.success, props.class, props.onDark); const [local, inputProps] = splitProps(props, [
const inputValue = () => readAccessor(props.value, ""); "prefix",
return <div class="ui-form"> "small",
<input "error",
ref={(el) => typeof props.ref === "function" && props.ref(el)} "success",
class={cls()} "class",
type={props.type} "onDark",
value={inputValue()} "ref",
placeholder={props.placeholder} "autocomplete",
autocomplete={props.autocomplete || (props.passwordManagerIgnore ? "off" : undefined)} "passwordManagerIgnore",
inputmode={props.inputmode} ]);
data-1p-ignore={props.passwordManagerIgnore}
data-lpignore={props.passwordManagerIgnore ? "other" : undefined}
data-form-type={props.passwordManagerIgnore}
data-bwignore={props.passwordManagerIgnore}
data-protonpass-ignore={props.passwordManagerIgnore}
disabled={props.disabled}
maxlength={props.maxlength}
min={props.min}
max={props.max}
step={props.step}
name={props.name}
id={props.id}
oninput={props.oninput}
onchange={props.onchange}
onBlur={props.onblur}
onFocus={props.onfocus}
onKeyDown={props.onkeydown}
return (
<div class="ui-form">
<input
ref={(el) => typeof local.ref === "function" && local.ref(el)}
class={_inputCls(local.small, local.error, local.success, local.class, local.onDark)}
autocomplete={local.autocomplete || (local.passwordManagerIgnore ? "off" : undefined)}
data-1p-ignore={local.passwordManagerIgnore}
data-lpignore={local.passwordManagerIgnore ? "other" : undefined}
data-form-type={local.passwordManagerIgnore}
data-bwignore={local.passwordManagerIgnore}
data-protonpass-ignore={local.passwordManagerIgnore}
{...inputProps}
/> />
<Show when={props.error}> <Show when={local.error}>
<span class={FORM_ERROR}>{props.error}</span> <span class={FORM_ERROR}>{local.error}</span>
</Show> </Show>
<Show when={props.success}> <Show when={local.success}>
<span class={FORM_SUCCESS}>{props.success}</span> <span class={FORM_SUCCESS}>{local.success}</span>
</Show> </Show>
</div>; </div>
);
} }
export function FormInputGroup(props: FormInputGroupProps) { export function FormInput(props: FormInputProps) {
const pfxCls = () => _prefixCls(props.small, props.error, props.onDark); const [local, inputProps] = splitProps(props, [
const inputCls = () => _inputCls(props.small, props.error, props.success, "rounded-l-none" + (props.class ? " " + props.class : ""), props.onDark); "prefix",
const inputValue = () => readAccessor(props.value, ""); "small",
return <div class="ui-form"> "error",
<div class={INPUT_GROUP}> "success",
<span class={pfxCls()}>{props.prefix}</span> "class",
<span class="grow flex"> "onDark",
"ref",
"autocomplete",
"passwordManagerIgnore",
]);
const cls = local.prefix ? ("rounded-l-none" + (local.class ? " " + local.class : "")) : local.class;
const InputEl = () => (
<input <input
ref={(el) => typeof props.ref === "function" && props.ref(el)} ref={(el) => typeof local.ref === "function" && local.ref(el)}
class={inputCls()} class={_inputCls(local.small, local.error, local.success, cls, local.onDark)}
type={props.type} autocomplete={local.autocomplete || (local.passwordManagerIgnore ? "off" : undefined)}
value={inputValue()} data-1p-ignore={local.passwordManagerIgnore}
placeholder={props.placeholder} data-lpignore={local.passwordManagerIgnore ? "other" : undefined}
autocomplete={props.autocomplete || (props.passwordManagerIgnore ? "off" : undefined)} data-form-type={local.passwordManagerIgnore}
inputmode={props.inputmode} data-bwignore={local.passwordManagerIgnore}
data-1p-ignore={props.passwordManagerIgnore} data-protonpass-ignore={local.passwordManagerIgnore}
data-lpignore={props.passwordManagerIgnore ? "other" : undefined} {...inputProps}
data-form-type={props.passwordManagerIgnore}
data-bwignore={props.passwordManagerIgnore}
data-protonpass-ignore={props.passwordManagerIgnore}
disabled={props.disabled}
maxlength={props.maxlength}
name={props.name}
id={props.id}
oninput={props.oninput}
onchange={props.onchange}
onBlur={props.onblur}
onFocus={props.onfocus}
onKeyDown={props.onkeydown}
/> />
</span> );
return (
<Show
when={local.prefix}
fallback={
<div class="ui-form">
<InputEl />
<Show when={local.error}>
<span class={FORM_ERROR}>{local.error}</span>
</Show>
<Show when={local.success}>
<span class={FORM_SUCCESS}>{local.success}</span>
</Show>
</div> </div>
<Show when={props.error}> }
<span class={FORM_ERROR}>{props.error}</span> >
<div class="ui-form">
<div class={INPUT_GROUP}>
<span class={_prefixCls(local.small, local.error, local.onDark)}>{local.prefix}</span>
<span class="grow flex"><InputEl /></span>
</div>
<Show when={local.error}>
<span class={FORM_ERROR}>{local.error}</span>
</Show> </Show>
<Show when={props.success}> <Show when={local.success}>
<span class={FORM_SUCCESS}>{props.success}</span> <span class={FORM_SUCCESS}>{local.success}</span>
</Show> </Show>
</div>; </div>
</Show>
);
} }
export function FormNumberInput(props: FormNumberInputProps) { export function FormNumberInput(props: FormNumberInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput", "int", "unsigned"]); const [local, inputProps] = splitProps(props, ["oninput", "int", "unsigned"]);
var regex: RegExp; var regex: RegExp;
if (local.int) { if (local.int) {
@@ -339,45 +350,11 @@ export function FormNumberInput(props: FormNumberInputProps) {
} }
}; };
return <FormInput return <FormInput inputMode="decimal" oninput={handleInput} {...inputProps} />;
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
inputMode="decimal"
oninput={handleInput}
class={local.class}
small={local.small}
{...inputProps}
/>;
} }
export function FormCarNumberInput(props: FormInputProps) { export function FormCurrencyInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]); const [local, inputProps] = splitProps(props, ["oninput", "showIcon"]);
const handleInput = (e) => {
if (!e || !e.currentTarget) return;
const input = e.currentTarget;
const raw = input.value.toUpperCase().replace(/[^0-9A-Z]/g, "");
const match = raw.match(/^([0-9]{0,2})([A-Z]{0,2})/);
input.value = match ? match[1] + match[2] : "";
if (typeof local.oninput === "function") {
local.oninput(e);
}
};
return <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
maxLength="4"
oninput={handleInput}
class={local.class}
small={local.small}
placeholder="e.g. 28"
{...inputProps}
/>;
}
export function FormCurrencyInput(props: {hideSymbol?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput", "hideSymbol"]);
const handleInput = (e) => { const handleInput = (e) => {
if (!e || !e.currentTarget) return; if (!e || !e.currentTarget) return;
@@ -396,32 +373,12 @@ export function FormCurrencyInput(props: {hideSymbol?: boolean} & FormInputProp
} }
}; };
if (local.hideSymbol) { return <FormInput prefix={local.showIcon ? "$" : null} inputMode="decimal" oninput={handleInput} {...inputProps} />;
return <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
inputMode="decimal"
oninput={handleInput}
class={local.class}
small={local.small}
{...inputProps}
/>;
} else {
return <FormInputGroup
ref={(el) => typeof local.ref === "function" && local.ref(el)}
prefix="$"
type="text"
inputMode="decimal"
oninput={handleInput}
class={local.class}
small={local.small}
{...inputProps}
/>;
}
} }
export function FormPercentInput(props: FormInputProps) { export function FormPercentInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]); const [local, inputProps] = splitProps(props, ["oninput", "showIcon"]);
const handleInput = (e) => { const handleInput = (e) => {
if (!e || !e.currentTarget) return; if (!e || !e.currentTarget) return;
@@ -436,20 +393,11 @@ export function FormPercentInput(props: FormInputProps) {
} }
}; };
return <FormInputGroup return <FormInput prefix={local.showIcon ? "%" : null} inputMode="decimal" oninput={handleInput} {...inputProps} />;
ref={(el) => typeof local.ref === "function" && local.ref(el)}
prefix="%"
type="text"
inputMode="decimal"
oninput={handleInput}
class={local.class}
small={local.small}
{...inputProps}
/>;
} }
export function FormPhoneInput(props: FormInputProps) { export function FormPhoneInput(props: {showIcon?: boolean; icon?: string} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]); const [local, inputProps] = splitProps(props, ["showIcon", "icon", "oninput"]);
const handleInput = (e) => { const handleInput = (e) => {
if (!e || !e.currentTarget) return; if (!e || !e.currentTarget) return;
@@ -473,138 +421,45 @@ export function FormPhoneInput(props: FormInputProps) {
} }
}; };
return <FormInput return (
ref={(el) => typeof local.ref === "function" && local.ref(el)} <FormInput
type="text" prefix={local.showIcon !== false ? <IconInline icon={local.icon || "phone"} size={16} /> : null}
inputMode="numeric" inputMode="numeric"
oninput={handleInput} oninput={handleInput}
class={local.class}
small={local.small}
placeholder="(555) 555-5555" placeholder="(555) 555-5555"
{...inputProps} {...inputProps}
/>; />
} );
// @TODO replace this with showIcon prop
export function FormPhoneInputWithIcon(props: {icon?: string} & FormInputGroupProps) {
const [local, inputProps] = splitProps(props, ["icon", "small", "ref", "class", "oninput"]);
const handleInput = (e) => {
if (!e || !e.currentTarget) return;
const input = e.currentTarget;
let digits = input.value.replace(/\D/g, "");
digits = digits.slice(0, 10);
let formatted = "";
if (digits.length > 0) {
formatted = "(" + digits.slice(0, 3);
}
if (digits.length > 3) {
formatted += ") " + digits.slice(3, 6);
}
if (digits.length > 6) {
formatted += "-" + digits.slice(6, 10);
}
input.value = formatted;
if (typeof local.oninput === "function") {
local.oninput(e);
}
};
const iconEl = <IconInline icon={local.icon || "phone"} size={16} />;
return <FormInputGroup
prefix={iconEl}
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
inputMode="numeric"
oninput={handleInput}
class={local.class}
small={local.small}
placeholder="(555) 555-5555"
{...inputProps}
/>;
} }
export function FormEmailInput(props: {showIcon?: boolean} & FormInputProps) { export function FormEmailInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["showIcon", "small", "ref", "class", "error", "success", "onDark"]); const [local, inputProps] = splitProps(props, ["showIcon"]);
const showIcon = () => local.showIcon !== false;
const pfxCls = () => _prefixCls(local.small, local.error, local.onDark);
const inputCls = () => _inputCls(local.small, local.error, local.success, "rounded-l-none" + (local.class ? " " + local.class : ""), local.onDark);
return <Show when={showIcon()} fallback={ return (
<FormInput <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)} prefix={local.showIcon !== false ? <IconInline icon="envelope" size={16} /> : null}
type="email" type="email"
inputMode="email" inputMode="email"
class={local.class}
small={local.small}
error={local.error}
success={local.success}
onDark={local.onDark}
{...inputProps}
/>}>
<div class="ui-form">
<div class={INPUT_GROUP}>
<span class={pfxCls()}>
<IconInline icon="envelope" size={16} />
</span>
<span class="grow flex">
<input
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="email"
inputMode="email"
class={inputCls()}
{...inputProps} {...inputProps}
/> />
</span> );
</div>
<Show when={local.error}>
<span class={FORM_ERROR}>{local.error}</span>
</Show>
<Show when={local.success}>
<span class={FORM_SUCCESS}>{local.success}</span>
</Show>
</div>
</Show>;
} }
export function FormURLInput(props: {showIcon: boolean} & FormInputProps) { export function FormURLInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["showIcon", "small", "ref", "class", "onDark"]); const [local, inputProps] = splitProps(props, ["showIcon"]);
const showIcon = () => local.showIcon !== false;
const pfxCls = () => _prefixCls(local.small, false, local.onDark);
const inputCls = () => _inputCls(local.small, false, false, "rounded-l-none" + (local.class ? " " + local.class : ""), local.onDark);
return <Show when={showIcon()} fallback={ return (
<FormInput <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)} prefix={local.showIcon !== false ? <IconInline icon="globe" size={16} /> : null}
type="text" type="text"
inputMode="url" inputMode="url"
class={local.class}
small={local.small}
onDark={local.onDark}
{...inputProps}
/>}>
<div class="ui-form">
<div class={INPUT_GROUP}>
<span class={pfxCls()}>
<IconInline icon="globe" size={16} />
</span>
<span class="grow flex">
<input
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
inputMode="url"
class={inputCls()}
{...inputProps} {...inputProps}
/> />
</span> );
</div>
</div>
</Show>;
} }
export function FormZipCodeInput(props: FormInputProps) { export function FormZipCodeInput(props: FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]); const [local, inputProps] = splitProps(props, ["oninput"]);
const handleInput = (e) => { const handleInput = (e) => {
if (!e || !e.currentTarget) return; if (!e || !e.currentTarget) return;
@@ -617,18 +472,15 @@ export function FormZipCodeInput(props: FormInputProps) {
return b ? a + "-" + b : a; return b ? a + "-" + b : a;
}); });
if (typeof props.oninput === "function") { if (typeof local.oninput === "function") {
props.oninput(e); local.oninput(e);
} }
}; };
return <FormInput return <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text" type="text"
inputMode="numeric" inputMode="numeric"
oninput={handleInput} oninput={handleInput}
class={local.class}
small={local.small}
placeholder="12345" placeholder="12345"
{...inputProps} {...inputProps}
/>; />;
@@ -807,7 +659,6 @@ export function FormSignaturePad(props: { value?: () => string; onchange?: (svg:
// @TODO Fix this // @TODO Fix this
export function FormSelect(props: FormSelectProps & { children?: JSXElement }) { export function FormSelect(props: FormSelectProps & { children?: JSXElement }) {
const [local, selectProps] = splitProps(props, ["small", "ref", "class", "children", "value", "onDark"]); const [local, selectProps] = splitProps(props, ["small", "ref", "class", "children", "value", "onDark"]);
const cls = () => _inputCls(local.small, false, false, local.class, local.onDark);
let selectEl; let selectEl;
// Set value via deferred effect so it runs AFTER children (<option>s) are in the DOM. // Set value via deferred effect so it runs AFTER children (<option>s) are in the DOM.
// The spread's createRenderEffect runs immediately (before children), so select.value // The spread's createRenderEffect runs immediately (before children), so select.value
@@ -823,7 +674,7 @@ export function FormSelect(props: FormSelectProps & { children?: JSXElement }) {
return <div class="ui-form"> return <div class="ui-form">
<select <select
ref={(el) => { selectEl = el; if (local.ref) local.ref(el); }} ref={(el) => { selectEl = el; if (local.ref) local.ref(el); }}
class={cls()} class={_inputCls(local.small, false, false, local.class, local.onDark)}
{...selectProps} {...selectProps}
>{local.children}</select> >{local.children}</select>
</div>; </div>;
@@ -967,24 +818,23 @@ export function FormSearchableSelect(props: {ref: string} & FormComboboxProps) {
} }
export function FormTextarea(props: FormTextareaProps) { export function FormTextarea(props: FormTextareaProps) {
const cls = () => _textareaCls(props.small, props.error, props.class, props.onDark); const [local, inputProps] = splitProps(props, [
const inputValue = () => readAccessor(props.value, ""); "small",
"error",
"class",
"onDark",
"ref",
]);
const cls = () => _textareaCls(local.small, local.error, local.class, local.onDark);
return <div class="ui-form"> return <div class="ui-form">
<textarea <textarea
ref={(el) => typeof props.ref === "function" && props.ref(el)} ref={(el) => typeof local.ref === "function" && local.ref(el)}
class={cls()} class={cls()}
value={inputValue()} {...inputProps}
placeholder={props.placeholder}
rows={props.rows}
disabled={props.disabled}
name={props.name}
id={props.id}
oninput={props.oninput}
onchange={props.onchange}
onblur={props.onblur}
/> />
<Show when={props.error}> <Show when={local.error}>
<span class={FORM_ERROR}>{props.error}</span> <span class={FORM_ERROR}>{local.error}</span>
</Show> </Show>
</div>; </div>;
} }

View File

@@ -6,11 +6,11 @@ interface PageContainerProps {
children?: JSXElement; children?: JSXElement;
} }
export function PageContainer(props: PageContainerProps) { export function PageContainer(props: PageContainerProps): JSXElement {
return <div class="admin-page-container">{props.children}</div>; return <div class="admin-page-container">{props.children}</div>;
} }
export function Divider() { export function Divider(): JSXElement {
return <hr class="text-line mt-1 mb-3"/>; return <hr class="text-line mt-1 mb-3"/>;
} }
@@ -19,7 +19,7 @@ interface CodeBoxProps {
class?: string; class?: string;
} }
export function CodeBox(props: CodeBoxProps) { export function CodeBox(props: CodeBoxProps): JSXElement {
return ( return (
<div class={"text-xs p-3 bg-neutral-800 text-neutral-100 border border-neutral-700 rounded-default " + (props.class || "")}> <div class={"text-xs p-3 bg-neutral-800 text-neutral-100 border border-neutral-700 rounded-default " + (props.class || "")}>
<pre><code>{props.code}</code></pre> <pre><code>{props.code}</code></pre>
@@ -28,11 +28,11 @@ export function CodeBox(props: CodeBoxProps) {
} }
interface PageHeaderProps { interface PageHeaderProps {
text: string; text: string | JSXElement;
class?: string; class?: string;
} }
export function PageHeader(props: PageHeaderProps) { export function PageHeader(props: PageHeaderProps): JSXElement {
return ( return (
<header class={props.class || ""}> <header class={props.class || ""}>
<div class="mt-1"> <div class="mt-1">
@@ -50,7 +50,7 @@ interface PageLinkProps {
children?: JSXElement; children?: JSXElement;
} }
export function PageLink(props: PageLinkProps) { export function PageLink(props: PageLinkProps): JSXElement {
return ( return (
<a <a
href={props.href} href={props.href}
@@ -60,7 +60,7 @@ export function PageLink(props: PageLinkProps) {
); );
} }
export function Loader(props: { class?: string; style?: string }) { export function Loader(props: { class?: string; style?: string }): JSXElement {
return ( return (
<div class={"flex items-center justify-center p-8 " + (props.class || "")} style={props.style}> <div class={"flex items-center justify-center p-8 " + (props.class || "")} style={props.style}>
<div class="h-8 w-8 border-4 border-sky-700 border-t-transparent rounded-full animate-spin"></div> <div class="h-8 w-8 border-4 border-sky-700 border-t-transparent rounded-full animate-spin"></div>
@@ -77,7 +77,7 @@ interface BreadcrumbsProps {
items: BreadcrumbItem[]; items: BreadcrumbItem[];
} }
export function Breadcrumbs(props: BreadcrumbsProps) { export function Breadcrumbs(props: BreadcrumbsProps): JSXElement {
return ( return (
<div class="flex flex-row items-center text-ink-faint text-xs"> <div class="flex flex-row items-center text-ink-faint text-xs">
<For each={props.items}>{(crumb, index) => ( <For each={props.items}>{(crumb, index) => (
@@ -102,7 +102,7 @@ interface ManagerPageHeaderProps {
action?: JSXElement; action?: JSXElement;
} }
export function ManagerPageHeader(props: ManagerPageHeaderProps) { export function ManagerPageHeader(props: ManagerPageHeaderProps): JSXElement {
return ( return (
<div class="page-header"> <div class="page-header">
<div> <div>

View File

@@ -65,16 +65,16 @@ function resolveFAIcon(name: string, prefix: string): FAEntry | undefined {
return FA_ICONS[prefix + ":" + name]; return FA_ICONS[prefix + ":" + name];
} }
export function Icon(props: IconProps) { export function Icon(props: IconProps): JSXElement {
const size = () => props.size ?? 16; const size = props.size ?? 16;
const custom = () => customIcons[props.icon]; const custom = customIcons[props.icon];
// Style resolution: an explicit `prefix` wins; then a per-icon `solid` // Style resolution: an explicit `prefix` wins; then a per-icon `solid`
// override (true→solid, false→regular); otherwise the app-wide default set // override (true→solid, false→regular); otherwise the app-wide default set
// by FA_DEFAULT_SOLID. The fallback chain lets any icon resolve regardless // by FA_DEFAULT_SOLID. The fallback chain lets any icon resolve regardless
// of which style it actually ships in, so opting into solid never blanks. // of which style it actually ships in, so opting into solid never blanks.
const faDef = (): FAEntry | undefined => { const faDef = (): FAEntry | undefined => {
if (custom()) return undefined; if (custom) return undefined;
let want: string; let want: string;
if (props.prefix) want = props.prefix; if (props.prefix) want = props.prefix;
else if (props.solid === true) want = FA_PREFIX_SOLID; else if (props.solid === true) want = FA_PREFIX_SOLID;
@@ -88,7 +88,7 @@ export function Icon(props: IconProps) {
// viewBox as [minX, minY, width, height]: custom icons are 0-origin; FA icons // viewBox as [minX, minY, width, height]: custom icons are 0-origin; FA icons
// carry a viewBox cropped to the glyph so they render at their intended size. // carry a viewBox cropped to the glyph so they render at their intended size.
const box = (): [number, number, number, number] => { const box = (): [number, number, number, number] => {
const c = custom(); const c = custom;
if (c) return [0, 0, c.viewBox[0], c.viewBox[1]]; if (c) return [0, 0, c.viewBox[0], c.viewBox[1]];
const fa = faDef(); const fa = faDef();
return fa ? [fa[0], fa[1], fa[2], fa[3]] : [0, 0, 512, 512]; return fa ? [fa[0], fa[1], fa[2], fa[3]] : [0, 0, 512, 512];
@@ -96,18 +96,18 @@ export function Icon(props: IconProps) {
const vw = () => box()[2]; const vw = () => box()[2];
const vh = () => box()[3]; const vh = () => box()[3];
const svgContent = () => { const svgContent = () => {
const c = custom(); const c = custom;
if (c) return c.content; if (c) return c.content;
const fa = faDef(); const fa = faDef();
return fa ? '<path d="' + fa[4] + '"/>' : ""; return fa ? '<path d="' + fa[4] + '"/>' : "";
}; };
const h = (): number => { const h = (): number => {
const s = size(); const s = size;
return Array.isArray(s) ? (s[1] ?? s[0]) : s; return Array.isArray(s) ? (s[1] ?? s[0]) : s;
}; };
const w = (): number => { const w = (): number => {
const s = size(); const s = size;
return Array.isArray(s) ? s[0] : Math.round(h() * (vw() / vh())); return Array.isArray(s) ? s[0] : Math.round(h() * (vw() / vh()));
}; };
@@ -116,22 +116,18 @@ export function Icon(props: IconProps) {
); );
} }
export function IconInline(props: IconProps) { export function IconInline(props: IconProps): JSXElement {
return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " " + (props.class || "")}/>; return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " " + (props.class || "")}/>;
} }
export function IconSuccess(props: IconProps) { export function IconSuccess(props: IconProps): JSXElement {
return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " text-green-600 dark:text-green-400 " + (props.class || "")}/>; return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " text-green-600 dark:text-green-400 " + (props.class || "")}/>;
} }
export function IconError(props: IconProps) { export function IconError(props: IconProps): JSXElement {
return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " text-red-600 dark:text-red-400 " + (props.class || "")}/>; return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " text-red-600 dark:text-red-400 " + (props.class || "")}/>;
} }
interface IconContainerProps { export function IconContainer(props: {children?: JSXElement}): JSXElement {
children?: JSXElement;
}
export function IconContainer(props: IconContainerProps) {
return <span class="flex flex-row items-center gap-2">{props.children}</span>; return <span class="flex flex-row items-center gap-2">{props.children}</span>;
} }

View File

@@ -15,7 +15,7 @@ const LAYOUT_STICKY_FULL = "sticky top-0 h-screen overflow-y-auto";
// viewport edge. Scoped to lg, where the sidebar/gap exists. // viewport edge. Scoped to lg, where the sidebar/gap exists.
const LAYOUT_MAIN = "col-span-1 min-w-0 lg:col-span-10 lg:pr-8"; const LAYOUT_MAIN = "col-span-1 min-w-0 lg:col-span-10 lg:pr-8";
interface SidebarNavItem { export interface SidebarNavItem {
id: string; id: string;
label: string; label: string;
icon?: JSXElement; icon?: JSXElement;

View File

@@ -21,7 +21,7 @@ export function createValidation(validation: Validation<string>): Accessor<strin
return createMemo(() => { return createMemo(() => {
if (!touched()[id] || (field() && !fieldBlur()) || (isValidFunc && isValidFunc(field()))) return ""; if (!touched()[id] || (field() && !fieldBlur()) || (isValidFunc && isValidFunc(field()))) return "";
if (required && !field()) return `${name} is required`; if (!field()) return required ? `${name} is required` : "";
if (isValidFunc && !isValidFunc(fieldBlur())) return invalidMsg ?? `${name} is invalid`; if (isValidFunc && !isValidFunc(fieldBlur())) return invalidMsg ?? `${name} is invalid`;
return ""; return "";
}); });