Fix UI for mobile render

This commit is contained in:
2026-07-01 09:37:37 +02:00
parent 8cbc25a601
commit 8e551159fc
13 changed files with 229 additions and 172 deletions

View File

@@ -38,11 +38,11 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise
}
export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: Class, disabled?: Reactive<boolean>)
{
const btn = dom('button', { class: [`inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
const btn = dom('button', { class: [cls, `inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => _disabled || (onClick && onClick.bind(btn)()) } }, Array.isArray(content) ? content : [content]);
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`], listeners: { click: () => _disabled || (onClick && onClick.bind(btn)()) } }, Array.isArray(content) ? content : [content]);
let _disabled = false;
Object.defineProperty(btn, 'disabled', {
get: () => _disabled,
@@ -59,10 +59,9 @@ export function button(content: Node | NodeChildren, onClick?: (this: HTMLElemen
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void })
{
let currentValue = settings?.value;
const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 hover:dark:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-hidden
const elements = options.map(e => dom('div', { class: [settings?.class?.option, `cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 hover:dark:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-hidden
border border-light-40 dark:border-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[selected]:z-10 data-[selected]:border-light-50 dark:data-[selected]:border-dark-50
data-[selected]:shadow-raw transition-[box-shadow] data-[selected]:shadow-light-50 dark:data-[selected]:shadow-dark-50 focus:shadow-raw focus:shadow-light-40 focus:dark:shadow-dark-40`,
settings?.class?.option], text: e.text, attributes: { 'data-selected': settings?.value === e.value }, listeners: { click: function() {
data-[selected]:shadow-raw transition-[box-shadow] data-[selected]:shadow-light-50 dark:data-[selected]:shadow-dark-50 focus:shadow-raw focus:shadow-light-40 focus:dark:shadow-dark-40`], text: e.text, attributes: { 'data-selected': settings?.value === e.value }, listeners: { click: function() {
if(currentValue !== e.value)
{
elements.forEach(e => e.toggleAttribute('data-selected', false));
@@ -74,12 +73,12 @@ export function buttongroup<T extends any>(options: Array<{ text: string, value:
}
}
}}}))
return div(['flex flex-row', settings?.class?.container], elements);
return div([settings?.class?.container, 'flex flex-row'], elements);
}
export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: HTMLElement) => void
{
let close: () => void;
const element = div(['flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100', settings?.class?.container], options.map(e => dom('div', { class: ['flex flex-row px-2 py-1 hover:bg-light-35 hover:dark:bg-dark-35 cursor-pointer', settings?.class?.option], text: e.title, listeners: { click: () => { e.click(); close() } } })));
const element = div([settings?.class?.container, 'flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100'], options.map(e => dom('div', { class: [settings?.class?.option, 'flex flex-row px-2 py-1 hover:bg-light-35 hover:dark:bg-dark-35 cursor-pointer'], text: e.title, listeners: { click: () => { e.click(); close() } } })));
return function(this: HTMLElement, target?: HTMLElement) {
close = followermenu(target ?? this, [ element ], { arrow: true, placement: settings?.position, offset: 8 }).close;
}
@@ -127,8 +126,8 @@ export function select<T extends NonNullable<any>>(options: Reactive<Array<{ tex
if(disabled) return;
window.addEventListener('keydown', handleKeys);
context = followermenu(select, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { 'min-width': `${select.clientWidth}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none outline-hidden cursor-default h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container], attributes: { tabindex: '0' } }, [ span('', valueText), icon('radix-icons:caret-down') ]) as HTMLDivElement & { value: T | undefined, disabled: boolean };
context = followermenu(select, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: [settings?.class?.popup, 'flex flex-col max-h-[320px] overflow-auto'], style: { 'min-width': `${select.clientWidth}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
} }, class: [settings?.class?.container, 'mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none outline-hidden cursor-default h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20'], attributes: { tabindex: '0' } }, [ span('', valueText), icon('radix-icons:caret-down') ]) as HTMLDivElement & { value: T | undefined, disabled: boolean };
Object.defineProperty(select, 'disabled', {
get: () => disabled,
@@ -164,7 +163,7 @@ export function select<T extends NonNullable<any>>(options: Reactive<Array<{ tex
reactivity(options, (o) => {
_options = o.filter(e => !!e);
if(!_options.find(e => e.value === value)) select.value = undefined;
optionElements = _options.map((e, i) => dom('div', { listeners: { click: (_e) => { select.value = e.value ; context.close(); }, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], text: e.text }));
optionElements = _options.map((e, i) => dom('div', { listeners: { click: (_e) => { select.value = e.value ; context.close(); }, mouseenter: (e) => focus(i) }, class: [settings?.class?.option, 'data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer'], text: e.text }));
});
select.disabled = settings?.disabled ?? false;
@@ -212,8 +211,8 @@ export function multiselect<T extends NonNullable<any>>(options: Option<T>[], se
if(disabled) return;
window.addEventListener('keydown', handleKeys);
context = followermenu(select, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { 'min-width': `${select.clientWidth}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none outline-hidden cursor-default h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container], attributes: { tabindex: '0' } }, [ span('', valueText), icon('radix-icons:caret-down') ]) as HTMLDivElement & { value: T[] | undefined, disabled: boolean };
context = followermenu(select, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: [settings?.class?.popup, 'flex flex-col max-h-[320px] overflow-auto'], style: { 'min-width': `${select.clientWidth}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
} }, class: [settings?.class?.container, 'mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none outline-hidden cursor-default h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20'], attributes: { tabindex: '0' } }, [ span('', valueText), icon('radix-icons:caret-down') ]) as HTMLDivElement & { value: T[] | undefined, disabled: boolean };
Object.defineProperty(select, 'disabled', {
get: () => disabled,
@@ -252,7 +251,7 @@ export function multiselect<T extends NonNullable<any>>(options: Option<T>[], se
this.toggleAttribute('data-selected', idx === -1);
select.value = v;
_e.ctrlKey || context.close();
}, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': settings?.defaultValue?.includes(e.value) ?? false } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block' }) ]));
}, mouseenter: (e) => focus(i) }, class: [settings?.class?.option, 'group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer'], attributes: { 'data-selected': settings?.defaultValue?.includes(e.value) ?? false } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block' }) ]));
});
select.disabled = settings?.disabled ?? false;
@@ -364,7 +363,7 @@ export function combobox<T extends NonNullable<any>>(options: RecurrentOption<T>
const box = container.getBoundingClientRect();
focus();
context = followermenu(container, currentOptions.length > 0 ? currentOptions.map(e => e.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": settings?.fill === 'cover' && `${box.width}px`, "max-width": settings?.fill === 'contain' && `${box.width}px` }, blur: hide });
context = followermenu(container, currentOptions.length > 0 ? currentOptions.map(e => e.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: [settings?.class?.popup, 'flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden'], style: { "min-width": settings?.fill === 'cover' && `${box.width}px`, "max-width": settings?.fill === 'contain' && `${box.width}px` }, blur: hide });
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
};
const hide = () => {
@@ -397,12 +396,12 @@ export function combobox<T extends NonNullable<any>>(options: RecurrentOption<T>
{
const children = option.value.map(render).filter(e => !!e);
const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored), mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored), mouseenter: () => focus(option.value) }, class: [settings?.class?.option, 'data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center'] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
return stored;
}
else
{
return { item: option, dom: dom('div', { listeners: { click: (_e) => { container.value = option.value as T; !_e.ctrlKey && hide(); }, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) };
return { item: option, dom: dom('div', { listeners: { click: (_e) => { container.value = option.value as T; !_e.ctrlKey && hide(); }, mouseenter: () => focus(option.value) }, class: [settings?.class?.option, 'data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer'] }, [ option?.render ? option?.render() : text(option.text) ]) };
}
}
const filter = (value: string, option?: StoredRecurrentOption<T>): StoredRecurrentOption<T>[] => {
@@ -453,7 +452,7 @@ export function combobox<T extends NonNullable<any>>(options: RecurrentOption<T>
}
} }, attributes: { type: 'text', }, class: 'flex-1 outline-hidden px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20 w-full' });
const container = dom('label', { class: ['inline-flex outline-hidden px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]) as HTMLLabelElement & { disabled: boolean, value: T | undefined };
const container = dom('label', { class: [settings?.class?.container, 'inline-flex outline-hidden px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20'] }, [ select, icon('radix-icons:caret-down') ]) as HTMLLabelElement & { disabled: boolean, value: T | undefined };
let value: T | undefined = undefined;
Object.defineProperty(container, 'disabled', {
@@ -520,7 +519,7 @@ export function tagpicker<T extends NonNullable<any>>(options: Reactive<Option<T
const box = container.getBoundingClientRect();
focus();
currentOptions = optionElements.map(e => filter(select.value.toLowerCase().trim().normalize(), e)).filter(e => !!e);
context = followermenu(container, currentOptions.length > 0 ? currentOptions.map(e => e.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": settings?.fill === 'cover' && `${box.width}px`, "max-width": settings?.fill === 'contain' && `${box.width}px` }, blur: hide });
context = followermenu(container, currentOptions.length > 0 ? currentOptions.map(e => e.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: [settings?.class?.popup, 'flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden'], style: { "min-width": settings?.fill === 'cover' && `${box.width}px`, "max-width": settings?.fill === 'contain' && `${box.width}px` }, blur: hide });
};
const hide = () => {
context && context.container.parentElement && context.close();
@@ -537,7 +536,7 @@ export function tagpicker<T extends NonNullable<any>>(options: Reactive<Option<T
if(option === undefined)
return;
return { item: option, dom: dom('div', { listeners: { click: (_e) => { add(option.value); !_e.ctrlKey && hide(); }, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) };
return { item: option, dom: dom('div', { listeners: { click: (_e) => { add(option.value); !_e.ctrlKey && hide(); }, mouseenter: () => focus(option.value) }, class: [settings?.class?.option, 'data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer'] }, [ option?.render ? option?.render() : text(option.text) ]) };
}
const filter = (_value: string, option?: StoredOption<T>): StoredOption<T> | undefined => {
return option?.item?.text.toLowerCase().normalize().includes(_value) && !value.includes(option.item.value) ? option : undefined;
@@ -581,7 +580,7 @@ export function tagpicker<T extends NonNullable<any>>(options: Reactive<Option<T
} }, attributes: { type: 'text', }, class: 'flex-1 outline-hidden px-3 leading-none appearance-none py-1 bg-light-10 dark:bg-dark-10 disabled:bg-light-20 dark:disabled:bg-dark-20 w-full' });
let value: T[] = reactive([]);
const container = dom('label', { class: ['inline-flex h-10 w-full outline-hidden px-1 items-center justify-between text-sm font-semibold leading-none border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20 overflow-y-hidden overflow-x-auto no-scroll', settings?.class?.container] }, [ div('flex flex-row gap-2 item-center select-none py-1 shrink-0', { list: () => value, render: (v, _c) => _c ?? div('flex flex-row gap-2 items-center border border-light-35 dark:border-dark-35 py-1 ps-2 pe-1', [ span('text-sm', currentOptions.find(e => e.item?.value === v)?.item?.text), dom('span', { listeners: { click: () => remove(v) } }, [ icon('radix-icons:cross-1', { width: 10, height: 10, class: 'cursor-pointer text-light-60 dark:text-dark-60 hover:text-light-100 hover:dark:text-dark-100' }) ]) ]) }), select ]) as HTMLLabelElement & { disabled: boolean, value: T[] };
const container = dom('label', { class: [settings?.class?.container, 'inline-flex h-10 w-full outline-hidden px-1 items-center justify-between text-sm font-semibold leading-none border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20 overflow-y-hidden overflow-x-auto no-scroll'] }, [ div('flex flex-row gap-2 item-center select-none py-1 shrink-0', { list: () => value, render: (v, _c) => _c ?? div('flex flex-row gap-2 items-center border border-light-35 dark:border-dark-35 py-1 ps-2 pe-1', [ span('text-sm', currentOptions.find(e => e.item?.value === v)?.item?.text), dom('span', { listeners: { click: () => remove(v) } }, [ icon('radix-icons:cross-1', { width: 10, height: 10, class: 'cursor-pointer text-light-60 dark:text-dark-60 hover:text-light-100 hover:dark:text-dark-100' }) ]) ]) }), select ]) as HTMLLabelElement & { disabled: boolean, value: T[] };
Object.defineProperty(container, 'disabled', {
get: () => disabled,
@@ -603,9 +602,9 @@ export function tagpicker<T extends NonNullable<any>>(options: Reactive<Option<T
}
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void | boolean, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
{
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder, type: type }, class: [`mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder, type: type }, class: [settings?.class, `mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 appearance-none outline-hidden px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
border border-light-35 dark:border-dark-35 hover:border-light-50 hover:dark:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`], listeners: {
input: (e) => { if(settings?.input && settings.input(input.value) === false) input.value = value; else value = input.value; },
change: () => settings?.change && settings.change(input.value),
focus: settings?.focus,
@@ -619,13 +618,19 @@ export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', se
export function numberpicker(settings?: { defaultValue?: Reactive<number>, change?: (value: number) => void, input?: (value: number) => void, focus?: (value: number) => void, blur?: (value: number) => void, class?: Class, min?: number, max?: number, disabled?: Reactive<boolean> }): HTMLInputElement
{
let storedValue = settings?.defaultValue ? typeof settings.defaultValue === 'function' ? settings.defaultValue() : settings.defaultValue : 0;
const validateAndChange = (value: number) => {
const parseRaw = () => field.value.trim().toLowerCase().normalize().replace(/[a-z]/g, "").replace(/,/g, ".");
const validateAndChange = (value: number, fromInput = false) => {
const raw = parseRaw();
if(isNaN(value))
field.value = '';
{
if(!fromInput || (raw !== '' && raw !== '.'))
field.value = '';
}
else
{
value = clamp(value, settings?.min ?? -Infinity, settings?.max ?? Infinity);
field.value = value.toString(10);
if(!fromInput || !raw.endsWith('.'))
field.value = value.toString(10);
if(storedValue !== value)
{
storedValue = value;
@@ -634,8 +639,8 @@ export function numberpicker(settings?: { defaultValue?: Reactive<number>, chang
}
return false;
}
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 outline-hidden px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 hover:dark:border-dark-50 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-35 dark:disabled:border-dark-35 disabled:border-2`, settings?.class], listeners: {
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [settings?.class, `w-14 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 outline-hidden px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 hover:dark:border-dark-50 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-35 dark:disabled:border-dark-35 disabled:border-2`], listeners: {
input: () => validateAndChange(parseFloat(parseRaw()), true) && settings?.input && settings.input(storedValue),
keydown: (e: KeyboardEvent) => {
if(field.disabled)
return;
@@ -655,15 +660,16 @@ export function numberpicker(settings?: { defaultValue?: Reactive<number>, chang
settings?.min && validateAndChange(settings.min) && settings?.input && settings.input(storedValue);
break;
case "Enter":
validateAndChange(parseFloat(parseRaw()));
settings?.change && settings.change(storedValue);
break;
default:
return;
}
},
change: () => settings?.change && settings.change(storedValue),
change: () => { validateAndChange(parseFloat(parseRaw())); settings?.change && settings.change(storedValue); },
focus: () => settings?.focus && settings.focus(storedValue),
blur: () => settings?.blur && settings.blur(storedValue),
blur: () => { validateAndChange(parseFloat(parseRaw())); settings?.blur && settings.blur(storedValue); },
}});
if(settings?.defaultValue !== undefined)
{
@@ -690,27 +696,27 @@ export function foldable(content: Reactive<NodeChildren>, title: NodeChildren, s
}
settings?.onFold && settings.onFold(state);
}
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
const fold = div(['group flex w-full flex-col', settings?.class?.container], [
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div(['flex-1', settings?.class?.title], title) ]),
const contentContainer = div([settings?.class?.content, 'hidden group-data-[active]:flex']);
const fold = div([settings?.class?.container, 'group flex w-full flex-col'], [
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: [settings?.class?.icon, 'flex justify-center items-center'] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div([settings?.class?.title, 'flex-1'], title) ]),
contentContainer
]);
display(settings?.open ?? true);
fold.toggleAttribute('data-active', settings?.open ?? true);
return fold;
}
type TableRow = Record<string, (() => HTMLElement) | HTMLElement | string>;
export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } })
type TableRow<T extends string> = Record<T, (() => HTMLElement) | HTMLElement | string>;
export function table<T extends string>(content: TableRow<T>[], headers: TableRow<T>, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } })
{
const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item;
return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]);
return dom('table', { class: [properties?.class?.table] }, [ dom('thead', { class: [properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: [properties?.class?.body] }, content.map(e => dom('tr', { class: [properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: [properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]);
}
export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: Reactive<boolean>, class?: { container?: Class } })
{
let state = settings?.defaultValue ?? false;
const element = dom("div", { class: [`group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-hidden
const element = dom("div", { class: [settings?.class?.container, `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-hidden
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 hover:dark:border-dark-50 focus:shadow-raw focus:shadow-light-40 focus:dark:shadow-dark-40
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
click: function(e: Event) {
if(this.hasAttribute('data-disabled'))
return;
@@ -726,9 +732,9 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void | boolean, disabled?: Reactive<boolean>, class?: { container?: Class, icon?: Class } })
{
let state = settings?.defaultValue ?? false;
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
const element = dom("div", { class: [settings?.class?.container, `group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
cursor-pointer hover:bg-light-30 hover:dark:bg-dark-30 hover:border-light-60 hover:dark:border-dark-60
data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0 hover:data-[disabled]:bg-0 hover:dark:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0 hover:data-[disabled]:bg-0 hover:dark:data-[disabled]:bg-0`], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
click: function(e: Event) {
if(this.hasAttribute('data-disabled'))
return;
@@ -740,7 +746,7 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HT
}
}
}
}, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]);
}, [ icon('radix-icons:check', { width: 14, height: 14, class: [settings?.class?.icon, 'hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50'] }), ]);
return element;
}
export function tabgroup(tabs: Array<Reactive<{ id: string, title: NodeChildren, content: Reactive<NodeChildren> } | undefined>>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): HTMLElement
@@ -749,9 +755,9 @@ export function tabgroup(tabs: Array<Reactive<{ id: string, title: NodeChildren,
const resolveTab = (t: typeof tabs[number]) => typeof t === 'function' ? t() : t;
const tabbar = div(['flex flex-row items-center gap-1', settings?.class?.tabbar]);
const content = div(['', settings?.class?.content]);
const container = div(['flex flex-col', settings?.class?.container], [tabbar, content]);
const tabbar = div([settings?.class?.tabbar, 'flex flex-row items-center gap-1']);
const content = div([settings?.class?.content]);
const container = div([settings?.class?.container, 'flex flex-col'], [tabbar, content]);
const render = () => {
const resolved = tabs.map(resolveTab).filter((t): t is NonNullable<typeof t> => !!t);
@@ -760,7 +766,7 @@ export function tabgroup(tabs: Array<Reactive<{ id: string, title: NodeChildren,
focus = resolved[0]?.id ?? '';
const titles = resolved.map(t => dom('div', {
class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title],
class: [settings?.class?.title, 'px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer'],
attributes: focus === t.id ? { 'data-focus': '' } : {},
listeners: { click: function() {
if (this.hasAttribute('data-focus'))
@@ -952,7 +958,7 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
});
minimized = false;
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
div(['group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', settings?.class], typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
div([settings?.class, 'group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content'], typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
],
viewport,
events: events