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;
NextPage?: number;
PreviousPage?: number;
TotalPages: number;
TotalItems: number;
TotalPages?: number;
TotalItems?: number;
MaxItemsPerPage: number;
ItemsThisPage?: number;
ViewRangeLower: number;
ViewRangeUpper: number;
ViewRangeLower?: number;
ViewRangeUpper?: number;
}
export interface AutoTableOrderBy {
@@ -449,7 +449,7 @@ export function AutoTableDateSearch(props: AutoTableDateSearchProps) {
<DatePicker
small={true}
clearable={true}
value={liveValue}
value={liveValue()}
onchange={props.onchange}
placeholder={props.placeholder}
/>
@@ -4181,33 +4181,27 @@ function PaginationButton(props: JSX.ButtonHTMLAttributes<HTMLButtonElement>) {
</button>;
}
interface TdProps {
class?: string;
style?: string;
children: any;
}
export function TdLeft(props: TdProps) {
export function TdLeft(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
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>;
}
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>;
}
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>;
}
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>;
}
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>;
}

View File

@@ -1,4 +1,4 @@
import { JSX, JSXElement } from "solid-js";
import { JSX, JSXElement, splitProps } from "solid-js";
import { Icon } from "./Icons.tsx";
export const BUTTON_COLOR_NEUTRAL = "neutral";
@@ -63,37 +63,36 @@ interface ButtonUIProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
}
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 = () => {
let c = BASE;
if (props.outline) {
c += " " + OUTLINE_BASE + " " + (OUTLINE_COLORS[props.color!] || OUTLINE_COLORS["neutral"]);
if (local.outline) {
c += " " + OUTLINE_BASE + " " + (OUTLINE_COLORS[local.color!] || OUTLINE_COLORS["neutral"]);
} else {
c += " " + (COLORS[props.color!] || COLORS["neutral"]);
c += " " + (COLORS[local.color!] || COLORS["neutral"]);
}
if (props.small) {
c += props.icon ? " py-1 px-3" : " py-1 px-4";
} else if (props.icon && !hasText()) {
if (local.small) {
c += local.icon ? " py-1 px-3" : " py-1 px-4";
} else if (local.icon && !hasText()) {
c += " py-2 px-3";
} else if (props.icon) {
} else if (local.icon) {
c += " py-2 px-5";
} else {
c += " py-2 px-8";
}
if (local.class) c += " " + local.class;
return c;
};
return (
<button
type={props.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}
type={local.type || "button"}
class={cls()}
{...buttonProps}
>
{props.children}
{local.children}
</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 { Icon } from "./Icons.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";
interface DatePickerProps {
value?: MaybeAccessor<string>;
id?: string;
name?: string;
value?: string;
oninput?: JSX.InputEventHandlerUnion<HTMLInputElement, InputEvent>;
onchange?: (value: string) => void;
placeholder?: string;
small?: boolean;

View File

@@ -107,8 +107,9 @@ export interface FormInputProps
extends
FormInputCommonProps,
Pick<JSX.InputHTMLAttributes<HTMLInputElement>, CommonInputAttrs | "type" | "max" | "min" | "step" | "accept"> {
small?: boolean;
ref?: (el: HTMLInputElement) => void;
prefix?: JSXElement;
small?: boolean;
class?: string;
onDark?: boolean;
}
@@ -121,10 +122,6 @@ export interface FormTextareaProps
onDark?: boolean;
}
export interface FormInputGroupProps extends FormInputProps {
prefix?: JSXElement;
}
export interface FormNumberInputProps extends FormInputProps {
int?: boolean;
unsigned?: boolean;
@@ -171,7 +168,7 @@ function _comboboxRootCls(className?: string, fieldWidth: ComboboxFieldWidth = "
}
export interface FormComboboxProps extends Pick<JSX.SelectHTMLAttributes<HTMLSelectElement>, CommonSelectAttrs> {
options: FormSelectOption[];
options?: FormSelectOption[];
placeholder?: string;
searchable?: boolean;
searchPlaceholder?: string;
@@ -227,90 +224,104 @@ export interface FormMultiSelectTriggerProps {
class?: string;
}
export function FormInput(props: FormInputProps) {
const cls = () => _inputCls(props.small, props.error, props.success, props.class, props.onDark);
const inputValue = () => readAccessor(props.value, "");
return <div class="ui-form">
<input
ref={(el) => typeof props.ref === "function" && props.ref(el)}
class={cls()}
type={props.type}
value={inputValue()}
placeholder={props.placeholder}
autocomplete={props.autocomplete || (props.passwordManagerIgnore ? "off" : undefined)}
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}
export function FormInputOld(props: FormInputProps) {
const [local, inputProps] = splitProps(props, [
"prefix",
"small",
"error",
"success",
"class",
"onDark",
"ref",
"autocomplete",
"passwordManagerIgnore",
]);
/>
<Show when={props.error}>
<span class={FORM_ERROR}>{props.error}</span>
</Show>
<Show when={props.success}>
<span class={FORM_SUCCESS}>{props.success}</span>
</Show>
</div>;
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={local.error}>
<span class={FORM_ERROR}>{local.error}</span>
</Show>
<Show when={local.success}>
<span class={FORM_SUCCESS}>{local.success}</span>
</Show>
</div>
);
}
export function FormInputGroup(props: FormInputGroupProps) {
const pfxCls = () => _prefixCls(props.small, props.error, props.onDark);
const inputCls = () => _inputCls(props.small, props.error, props.success, "rounded-l-none" + (props.class ? " " + props.class : ""), props.onDark);
const inputValue = () => readAccessor(props.value, "");
return <div class="ui-form">
<div class={INPUT_GROUP}>
<span class={pfxCls()}>{props.prefix}</span>
<span class="grow flex">
<input
ref={(el) => typeof props.ref === "function" && props.ref(el)}
class={inputCls()}
type={props.type}
value={inputValue()}
placeholder={props.placeholder}
autocomplete={props.autocomplete || (props.passwordManagerIgnore ? "off" : undefined)}
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}
name={props.name}
id={props.id}
oninput={props.oninput}
onchange={props.onchange}
onBlur={props.onblur}
onFocus={props.onfocus}
onKeyDown={props.onkeydown}
/>
</span>
</div>
<Show when={props.error}>
<span class={FORM_ERROR}>{props.error}</span>
export function FormInput(props: FormInputProps) {
const [local, inputProps] = splitProps(props, [
"prefix",
"small",
"error",
"success",
"class",
"onDark",
"ref",
"autocomplete",
"passwordManagerIgnore",
]);
const cls = local.prefix ? ("rounded-l-none" + (local.class ? " " + local.class : "")) : local.class;
const InputEl = () => (
<input
ref={(el) => typeof local.ref === "function" && local.ref(el)}
class={_inputCls(local.small, local.error, local.success, cls, 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}
/>
);
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 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 when={local.success}>
<span class={FORM_SUCCESS}>{local.success}</span>
</Show>
</div>
</Show>
<Show when={props.success}>
<span class={FORM_SUCCESS}>{props.success}</span>
</Show>
</div>;
);
}
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;
if (local.int) {
@@ -339,45 +350,11 @@ export function FormNumberInput(props: FormNumberInputProps) {
}
};
return <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
inputMode="decimal"
oninput={handleInput}
class={local.class}
small={local.small}
{...inputProps}
/>;
return <FormInput inputMode="decimal" oninput={handleInput} {...inputProps} />;
}
export function FormCarNumberInput(props: FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);
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"]);
export function FormCurrencyInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["oninput", "showIcon"]);
const handleInput = (e) => {
if (!e || !e.currentTarget) return;
@@ -396,32 +373,12 @@ export function FormCurrencyInput(props: {hideSymbol?: boolean} & FormInputProp
}
};
if (local.hideSymbol) {
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}
/>;
}
return <FormInput prefix={local.showIcon ? "$" : null} inputMode="decimal" oninput={handleInput} {...inputProps} />;
}
export function FormPercentInput(props: FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);
export function FormPercentInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["oninput", "showIcon"]);
const handleInput = (e) => {
if (!e || !e.currentTarget) return;
@@ -436,20 +393,11 @@ export function FormPercentInput(props: FormInputProps) {
}
};
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}
/>;
return <FormInput prefix={local.showIcon ? "%" : null} inputMode="decimal" oninput={handleInput} {...inputProps} />;
}
export function FormPhoneInput(props: FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);
export function FormPhoneInput(props: {showIcon?: boolean; icon?: string} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["showIcon", "icon", "oninput"]);
const handleInput = (e) => {
if (!e || !e.currentTarget) return;
@@ -473,138 +421,45 @@ export function FormPhoneInput(props: FormInputProps) {
}
};
return <FormInput
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}
/>;
}
// @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}
/>;
return (
<FormInput
prefix={local.showIcon !== false ? <IconInline icon={local.icon || "phone"} size={16} /> : null}
inputMode="numeric"
oninput={handleInput}
placeholder="(555) 555-5555"
{...inputProps}
/>
);
}
export function FormEmailInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["showIcon", "small", "ref", "class", "error", "success", "onDark"]);
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);
const [local, inputProps] = splitProps(props, ["showIcon"]);
return <Show when={showIcon()} fallback={
return (
<FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
prefix={local.showIcon !== false ? <IconInline icon="envelope" size={16} /> : null}
type="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}
/>
</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) {
const [local, inputProps] = splitProps(props, ["showIcon", "small", "ref", "class", "onDark"]);
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);
export function FormURLInput(props: {showIcon?: boolean} & FormInputProps) {
const [local, inputProps] = splitProps(props, ["showIcon"]);
return <Show when={showIcon()} fallback={
return (
<FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
prefix={local.showIcon !== false ? <IconInline icon="globe" size={16} /> : null}
type="text"
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}
/>
</span>
</div>
</div>
</Show>;
/>
);
}
export function FormZipCodeInput(props: FormInputProps) {
const [local, inputProps] = splitProps(props, ["small", "ref", "class", "oninput"]);
const [local, inputProps] = splitProps(props, ["oninput"]);
const handleInput = (e) => {
if (!e || !e.currentTarget) return;
@@ -617,18 +472,15 @@ export function FormZipCodeInput(props: FormInputProps) {
return b ? a + "-" + b : a;
});
if (typeof props.oninput === "function") {
props.oninput(e);
if (typeof local.oninput === "function") {
local.oninput(e);
}
};
return <FormInput
ref={(el) => typeof local.ref === "function" && local.ref(el)}
type="text"
inputMode="numeric"
oninput={handleInput}
class={local.class}
small={local.small}
placeholder="12345"
{...inputProps}
/>;
@@ -807,7 +659,6 @@ export function FormSignaturePad(props: { value?: () => string; onchange?: (svg:
// @TODO Fix this
export function FormSelect(props: FormSelectProps & { children?: JSXElement }) {
const [local, selectProps] = splitProps(props, ["small", "ref", "class", "children", "value", "onDark"]);
const cls = () => _inputCls(local.small, false, false, local.class, local.onDark);
let selectEl;
// 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
@@ -823,7 +674,7 @@ export function FormSelect(props: FormSelectProps & { children?: JSXElement }) {
return <div class="ui-form">
<select
ref={(el) => { selectEl = el; if (local.ref) local.ref(el); }}
class={cls()}
class={_inputCls(local.small, false, false, local.class, local.onDark)}
{...selectProps}
>{local.children}</select>
</div>;
@@ -967,24 +818,23 @@ export function FormSearchableSelect(props: {ref: string} & FormComboboxProps) {
}
export function FormTextarea(props: FormTextareaProps) {
const cls = () => _textareaCls(props.small, props.error, props.class, props.onDark);
const inputValue = () => readAccessor(props.value, "");
const [local, inputProps] = splitProps(props, [
"small",
"error",
"class",
"onDark",
"ref",
]);
const cls = () => _textareaCls(local.small, local.error, local.class, local.onDark);
return <div class="ui-form">
<textarea
ref={(el) => typeof props.ref === "function" && props.ref(el)}
ref={(el) => typeof local.ref === "function" && local.ref(el)}
class={cls()}
value={inputValue()}
placeholder={props.placeholder}
rows={props.rows}
disabled={props.disabled}
name={props.name}
id={props.id}
oninput={props.oninput}
onchange={props.onchange}
onblur={props.onblur}
{...inputProps}
/>
<Show when={props.error}>
<span class={FORM_ERROR}>{props.error}</span>
<Show when={local.error}>
<span class={FORM_ERROR}>{local.error}</span>
</Show>
</div>;
}

View File

@@ -6,11 +6,11 @@ interface PageContainerProps {
children?: JSXElement;
}
export function PageContainer(props: PageContainerProps) {
export function PageContainer(props: PageContainerProps): JSXElement {
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"/>;
}
@@ -19,7 +19,7 @@ interface CodeBoxProps {
class?: string;
}
export function CodeBox(props: CodeBoxProps) {
export function CodeBox(props: CodeBoxProps): JSXElement {
return (
<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>
@@ -28,11 +28,11 @@ export function CodeBox(props: CodeBoxProps) {
}
interface PageHeaderProps {
text: string;
text: string | JSXElement;
class?: string;
}
export function PageHeader(props: PageHeaderProps) {
export function PageHeader(props: PageHeaderProps): JSXElement {
return (
<header class={props.class || ""}>
<div class="mt-1">
@@ -50,7 +50,7 @@ interface PageLinkProps {
children?: JSXElement;
}
export function PageLink(props: PageLinkProps) {
export function PageLink(props: PageLinkProps): JSXElement {
return (
<a
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 (
<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>
@@ -77,7 +77,7 @@ interface BreadcrumbsProps {
items: BreadcrumbItem[];
}
export function Breadcrumbs(props: BreadcrumbsProps) {
export function Breadcrumbs(props: BreadcrumbsProps): JSXElement {
return (
<div class="flex flex-row items-center text-ink-faint text-xs">
<For each={props.items}>{(crumb, index) => (
@@ -102,7 +102,7 @@ interface ManagerPageHeaderProps {
action?: JSXElement;
}
export function ManagerPageHeader(props: ManagerPageHeaderProps) {
export function ManagerPageHeader(props: ManagerPageHeaderProps): JSXElement {
return (
<div class="page-header">
<div>

View File

@@ -65,16 +65,16 @@ function resolveFAIcon(name: string, prefix: string): FAEntry | undefined {
return FA_ICONS[prefix + ":" + name];
}
export function Icon(props: IconProps) {
const size = () => props.size ?? 16;
export function Icon(props: IconProps): JSXElement {
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`
// override (true→solid, false→regular); otherwise the app-wide default set
// 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.
const faDef = (): FAEntry | undefined => {
if (custom()) return undefined;
if (custom) return undefined;
let want: string;
if (props.prefix) want = props.prefix;
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
// carry a viewBox cropped to the glyph so they render at their intended size.
const box = (): [number, number, number, number] => {
const c = custom();
const c = custom;
if (c) return [0, 0, c.viewBox[0], c.viewBox[1]];
const fa = faDef();
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 vh = () => box()[3];
const svgContent = () => {
const c = custom();
const c = custom;
if (c) return c.content;
const fa = faDef();
return fa ? '<path d="' + fa[4] + '"/>' : "";
};
const h = (): number => {
const s = size();
const s = size;
return Array.isArray(s) ? (s[1] ?? s[0]) : s;
};
const w = (): number => {
const s = size();
const s = size;
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 || "")}/>;
}
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 || "")}/>;
}
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 || "")}/>;
}
interface IconContainerProps {
children?: JSXElement;
}
export function IconContainer(props: IconContainerProps) {
export function IconContainer(props: {children?: JSXElement}): JSXElement {
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.
const LAYOUT_MAIN = "col-span-1 min-w-0 lg:col-span-10 lg:pr-8";
interface SidebarNavItem {
export interface SidebarNavItem {
id: string;
label: string;
icon?: JSXElement;

View File

@@ -21,7 +21,7 @@ export function createValidation(validation: Validation<string>): Accessor<strin
return createMemo(() => {
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`;
return "";
});