feat: add fluxer upstream source and self-hosting documentation

- Clone of github.com/fluxerapp/fluxer (official upstream)
- SELF_HOSTING.md: full VM rebuild procedure, architecture overview,
  service reference, step-by-step setup, troubleshooting, seattle reference
- dev/.env.example: all env vars with secrets redacted and generation instructions
- dev/livekit.yaml: LiveKit config template with placeholder keys
- fluxer-seattle/: existing seattle deployment setup scripts
This commit is contained in:
Vish
2026-03-13 00:55:14 -07:00
parent 5ceda343b8
commit 3b9d759b4b
5859 changed files with 1923440 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {getAlertClasses} from '@fluxer/ui/src/utils/ColorVariants';
import type {FC, PropsWithChildren} from 'hono/jsx';
export type AlertVariant = 'error' | 'warning' | 'success' | 'info';
export interface AlertProps {
variant: AlertVariant;
title?: string;
}
export const Alert: FC<PropsWithChildren<AlertProps>> = ({variant, title, children}) => {
const classes = ['rounded-lg border px-4 py-3 text-sm', getAlertClasses(variant)].filter(Boolean).join(' ');
return (
<div class={classes}>
{title && <p class="mb-1 font-semibold">{title}</p>}
{children && <div>{children}</div>}
</div>
);
};

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {LabelProps, TextProps} from '@fluxer/ui/src/types/Common';
import {type ColorIntensity, type ColorTone, getColorClasses} from '@fluxer/ui/src/utils/ColorVariants';
export type BadgeVariant = 'default' | 'info' | 'success' | 'warning' | 'danger';
export interface BadgeProps extends TextProps {
variant?: BadgeVariant;
intensity?: ColorIntensity;
}
export function Badge({text, variant = 'default', intensity = 'normal'}: BadgeProps) {
const toneMapping: Record<BadgeVariant, ColorTone> = {
default: 'neutral',
info: 'info',
success: 'success',
warning: 'warning',
danger: 'danger',
};
const tone = toneMapping[variant];
const colorClasses = getColorClasses(tone, intensity);
return (
<span class={`inline-flex items-center rounded-full px-2.5 py-0.5 font-medium text-xs ${colorClasses}`}>
{text}
</span>
);
}
export interface UnifiedBadgeProps extends LabelProps {
tone: ColorTone;
intensity?: ColorIntensity;
rounded?: 'full' | 'default';
}
export function UnifiedBadge({label, tone, intensity = 'normal', rounded = 'full'}: UnifiedBadgeProps) {
const colorClasses = getColorClasses(tone, intensity);
const roundedClass = rounded === 'full' ? 'rounded-full' : 'rounded';
return (
<span class={`inline-flex items-center ${roundedClass} px-2 py-1 font-medium text-xs ${colorClasses}`}>
{label}
</span>
);
}
export interface PillProps extends Omit<UnifiedBadgeProps, 'rounded'> {}
export function Pill({label, tone, intensity = 'normal'}: PillProps) {
return <UnifiedBadge label={label} tone={tone} intensity={intensity} rounded="full" />;
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {createCompoundVariantClasses} from '@fluxer/ui/src/utils/VariantClasses';
import type {Child, FC, PropsWithChildren} from 'hono/jsx';
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'info' | 'ghost' | 'brand';
export type ButtonSize = 'small' | 'medium' | 'large' | 'xl';
export type ButtonIconPosition = 'left' | 'right';
const {getVariant: getVariantClasses, getSize: getSizeClasses} = createCompoundVariantClasses(
{
primary: 'bg-neutral-900 text-white hover:bg-neutral-800',
secondary:
'bg-neutral-50 text-neutral-700 hover:text-neutral-900 border border-neutral-300 hover:border-neutral-400',
danger: 'bg-red-600 text-white hover:bg-red-700',
success: 'bg-blue-600 text-white hover:bg-blue-700',
info: 'bg-blue-50 text-blue-700 hover:bg-blue-100',
ghost: 'text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100',
brand: 'bg-brand-primary text-white shadow-sm hover:bg-[color-mix(in_srgb,var(--brand-primary)_80%,black)]',
},
{
small: 'px-3 py-1.5 text-sm',
medium: 'px-4 py-2 text-base',
large: 'px-6 py-3 text-lg',
xl: 'px-8 py-4 text-xl',
},
'inline-flex items-center justify-center gap-2 font-medium rounded-lg transition-all duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2',
);
export interface ButtonProps {
type?: 'button' | 'submit' | 'reset';
variant?: ButtonVariant;
size?: ButtonSize;
fullWidth?: boolean;
name?: string;
value?: string;
disabled?: boolean;
loading?: boolean;
href?: string;
icon?: Child;
iconPosition?: ButtonIconPosition;
onclick?: string;
class?: string;
target?: '_blank' | '_self' | '_parent' | '_top';
rel?: string;
id?: string;
ariaLabel?: string;
}
export const Button: FC<PropsWithChildren<ButtonProps>> = ({
type = 'button',
variant = 'primary',
size = 'medium',
fullWidth = false,
name,
value,
disabled = false,
loading = false,
href,
icon,
iconPosition = 'left',
onclick,
class: extraClass,
target,
rel,
id,
ariaLabel,
children,
}) => {
const baseClasses = getVariantClasses(variant);
const sizeClasses = getSizeClasses(size);
const stateClasses = [
fullWidth ? 'w-full sm:w-fit' : 'w-fit',
disabled || loading ? 'opacity-50 cursor-not-allowed' : '',
loading ? 'pointer-events-none' : '',
variant === 'primary' || variant === 'danger' || variant === 'success' || variant === 'brand'
? 'focus:ring-offset-white'
: '',
].filter(Boolean);
const classes = [baseClasses, sizeClasses, ...stateClasses, extraClass || ''].filter(Boolean).join(' ');
const iconElement = icon ? <span class="flex-shrink-0">{icon}</span> : null;
const content = (
<>
{icon && iconPosition === 'left' && iconElement}
{loading && <span class="mr-2 inline-block animate-spin"></span>}
<span>{children}</span>
{icon && iconPosition === 'right' && iconElement}
</>
);
const commonProps = {
class: classes,
id,
'aria-label': ariaLabel,
};
if (href) {
return (
<a
{...commonProps}
href={href}
target={target}
rel={rel || (target === '_blank' ? 'noopener noreferrer' : undefined)}
role="button"
>
{content}
</a>
);
}
return (
<button {...commonProps} type={type} name={name} value={value} disabled={disabled || loading} onclick={onclick}>
{content}
</button>
);
};

View File

@@ -0,0 +1,151 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {ChildrenProps, TextProps} from '@fluxer/ui/src/types/Common';
import type {PropsWithChildren} from 'hono/jsx';
export type CardPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl';
export type CardVariant = 'default' | 'elevated' | 'empty' | 'marketing';
export type CardShadow = 'none' | 'sm' | 'md' | 'lg' | 'xl';
export interface CardProps {
variant?: CardVariant;
padding?: CardPadding;
hoverable?: boolean;
centerContent?: boolean;
heading?: string;
description?: string;
shadow?: CardShadow;
border?: boolean;
class?: string;
id?: string;
}
const cardPaddingClasses: Record<CardPadding, string> = {
none: 'p-0',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
xl: 'p-12',
};
const cardShadowClasses: Record<CardShadow, string> = {
none: '',
sm: 'shadow-sm',
md: 'shadow-md',
lg: 'shadow-lg',
xl: 'shadow-xl',
};
export function Card({
variant = 'default',
padding = 'md',
hoverable = false,
centerContent = false,
heading,
description,
shadow,
border = true,
class: extraClass,
id,
children,
}: PropsWithChildren<CardProps>) {
const baseClasses = ['rounded-lg', 'bg-white', 'transition-all'];
const variantClasses: Record<CardVariant, string> = {
default: border ? 'border border-neutral-200' : '',
elevated: border ? 'border border-neutral-200' : '',
empty: border ? 'border border-neutral-200' : '',
marketing: 'border-2 border-white/20 bg-white/5 backdrop-blur-sm',
};
const shadowValue = shadow !== undefined ? shadow : variant === 'elevated' ? 'sm' : 'none';
const shadowClass = cardShadowClasses[shadowValue];
const stateClasses = [
hoverable ? 'hover:shadow-lg hover:-translate-y-1 cursor-pointer' : '',
centerContent ? 'text-center' : '',
].filter(Boolean);
const classes = [
...baseClasses,
variantClasses[variant],
shadowClass,
cardPaddingClasses[padding],
...stateClasses,
extraClass || '',
]
.filter(Boolean)
.join(' ');
const content = (
<>
{heading && (
<div class="mb-4 space-y-1">
<h3 class="font-medium text-lg text-neutral-900">{heading}</h3>
{description && <p class="text-neutral-500 text-sm">{description}</p>}
</div>
)}
{children}
</>
);
return (
<div class={classes} id={id}>
{content}
</div>
);
}
export function CardElevated({padding = 'md', children, ...props}: PropsWithChildren<CardProps>) {
return (
<Card variant="elevated" padding={padding} {...props}>
{children}
</Card>
);
}
export function CardEmpty({children}: ChildrenProps) {
return (
<Card variant="empty" centerContent padding="xl">
{children}
</Card>
);
}
export interface HeadingCardProps extends Omit<CardProps, 'heading'>, TextProps {
description?: string;
}
export function HeadingCard({
text,
description,
padding = 'md',
children,
...props
}: PropsWithChildren<HeadingCardProps>) {
return (
<Card heading={text} description={description} padding={padding} {...props}>
{children}
</Card>
);
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {Button} from '@fluxer/ui/src/components/Button';
import {Checkbox} from '@fluxer/ui/src/components/Form';
import type {Child, FC} from 'hono/jsx';
export interface CheckboxFormProps {
id: string;
action?: string;
method?: 'post' | 'get';
saveButtonId?: string;
autoReveal?: boolean;
saveButtonLabel?: string;
children?: Child;
}
function getRevealScript(saveButtonId: string | undefined): string | undefined {
if (!saveButtonId) {
return undefined;
}
return `document.getElementById('${saveButtonId}')?.classList.remove('hidden');`;
}
export const CheckboxForm: FC<CheckboxFormProps> = ({
id,
action,
method = 'post',
saveButtonId,
autoReveal = false,
saveButtonLabel = 'Save Changes',
children,
}) => {
const actualSaveButtonId = saveButtonId ?? `${id}-save-button`;
return (
<form method={method} action={action} id={id}>
{children}
<div class={`mt-6 border-neutral-200 border-t pt-6 ${autoReveal ? '' : 'hidden'}`} id={actualSaveButtonId}>
<Button type="submit" variant="primary" size="medium">
{saveButtonLabel}
</Button>
</div>
</form>
);
};
export interface CheckboxItemProps {
name: string;
value: string;
label: string;
checked: boolean;
saveButtonId?: string;
}
export const CheckboxItem: FC<CheckboxItemProps> = ({name, value, label, checked, saveButtonId}) => {
const revealScript = getRevealScript(saveButtonId);
return <Checkbox name={name} value={value} label={label} checked={checked} onChange={revealScript} />;
};
export interface NativeCheckboxItemProps {
name: string;
value: string | number;
label: string;
checked: boolean;
saveButtonId?: string;
}
export const NativeCheckboxItem: FC<NativeCheckboxItemProps> = ({name, value, label, checked, saveButtonId}) => {
return (
<Checkbox
name={name}
value={String(value)}
label={label}
checked={checked}
onChange={getRevealScript(saveButtonId)}
/>
);
};

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {CSRF_FORM_FIELD} from '@fluxer/constants/src/Cookies';
import type {FC} from 'hono/jsx';
export interface CsrfInputProps {
token: string;
}
export const CsrfInput: FC<CsrfInputProps> = ({token}) => <input type="hidden" name={CSRF_FORM_FIELD} value={token} />;

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {CardEmpty} from '@fluxer/ui/src/components/Card';
import {TextMuted, TextSmallMuted} from '@fluxer/ui/src/components/Typography';
import type {Child, FC} from 'hono/jsx';
export interface EmptyStateProps {
title?: string;
message?: string;
icon?: Child;
action?: Child;
}
export const EmptyState: FC<EmptyStateProps> = ({title, message, icon, action}) => {
return (
<CardEmpty>
<div class="flex flex-col items-center gap-4">
{icon && <div class="text-neutral-400">{icon}</div>}
{title && <TextMuted text={title} />}
{message && <TextSmallMuted text={message} />}
{action && <div>{action}</div>}
</div>
</CardEmpty>
);
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {type Flash, parseFlash} from '@fluxer/hono/src/Flash';
import {getAlertClasses} from '@fluxer/ui/src/utils/ColorVariants';
export interface FlashMessageProps {
flash: Flash;
}
export function FlashMessage({flash}: FlashMessageProps) {
return (
<div class={`rounded-lg border px-4 py-3 text-sm ${getAlertClasses(flash.type)}`}>
<div>{flash.message}</div>
{flash.detail && (
<div class="mt-2 break-all rounded border border-current/20 bg-white/60 px-3 py-2 font-mono text-xs">
{flash.detail}
</div>
)}
</div>
);
}
export function parseFlashFromCookie(cookieHeader: string | null): Flash | undefined {
if (cookieHeader === null) {
return undefined;
}
return parseFlash(cookieHeader) ?? undefined;
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {
FORM_CONTROL_INPUT_CLASS,
FORM_CONTROL_SELECT_CLASS,
FORM_CONTROL_TEXTAREA_CLASS,
FORM_FIELD_CLASS,
FORM_HELPER_CLASS,
FORM_LABEL_CLASS,
FORM_SELECT_ICON_CLASS,
} from '@fluxer/ui/src/styles/FormControls';
import type {BaseFormProps, SelectOption} from '@fluxer/ui/src/types/Common';
import {cn} from '@fluxer/ui/src/utils/ClassNames';
export type InputType = 'text' | 'email' | 'password' | 'tel' | 'number' | 'date' | 'url';
function toInputId(name: string): string {
return name.replace(/[^a-zA-Z0-9_-]/g, '_');
}
function toHelperId(id: string, helper: string | undefined): string | undefined {
if (!helper) {
return undefined;
}
return `${id}-helper`;
}
export interface InputProps extends BaseFormProps {
type?: InputType;
value?: string | undefined;
autocomplete?: string;
step?: string;
min?: string | number;
max?: string | number;
readonly?: boolean;
}
export function Input({
label,
helper,
name,
id,
type = 'text',
value,
required,
placeholder,
disabled,
autocomplete,
step,
min,
max,
readonly,
}: InputProps) {
const inputId = id ?? toInputId(name);
const helperId = toHelperId(inputId, helper);
return (
<div class={FORM_FIELD_CLASS}>
{label && (
<label for={inputId} class={FORM_LABEL_CLASS}>
{label}
</label>
)}
<input
id={inputId}
type={type}
name={name}
value={value}
required={required}
placeholder={placeholder}
disabled={disabled}
readonly={readonly}
autocomplete={autocomplete}
step={step}
min={min}
max={max}
aria-describedby={helperId}
class={FORM_CONTROL_INPUT_CLASS}
/>
{helper && (
<p id={helperId} class={FORM_HELPER_CLASS}>
{helper}
</p>
)}
</div>
);
}
export interface TextareaProps extends BaseFormProps {
value?: string;
rows?: number;
}
export function Textarea({label, helper, name, id, value, required, placeholder, disabled, rows = 4}: TextareaProps) {
const textareaId = id ?? toInputId(name);
const helperId = toHelperId(textareaId, helper);
return (
<div class={FORM_FIELD_CLASS}>
{label && (
<label for={textareaId} class={FORM_LABEL_CLASS}>
{label}
</label>
)}
<textarea
id={textareaId}
name={name}
rows={rows}
required={required}
placeholder={placeholder}
disabled={disabled}
aria-describedby={helperId}
class={FORM_CONTROL_TEXTAREA_CLASS}
>
{value}
</textarea>
{helper && (
<p id={helperId} class={FORM_HELPER_CLASS}>
{helper}
</p>
)}
</div>
);
}
export interface SelectProps extends BaseFormProps {
value?: string;
options: Array<SelectOption>;
}
export function Select({label, helper, name, id, value, required, disabled, options}: SelectProps) {
const selectId = id ?? toInputId(name);
const helperId = toHelperId(selectId, helper);
return (
<div class={FORM_FIELD_CLASS}>
{label && (
<label for={selectId} class={FORM_LABEL_CLASS}>
{label}
</label>
)}
<div class="relative">
<select
id={selectId}
name={name}
required={required}
disabled={disabled}
aria-describedby={helperId}
class={FORM_CONTROL_SELECT_CLASS}
>
{options.map((option) => (
<option key={option.value} value={option.value} selected={option.value === value}>
{option.label}
</option>
))}
</select>
<svg class={FORM_SELECT_ICON_CLASS} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="m6 8 4 4 4-4"
/>
</svg>
</div>
{helper && (
<p id={helperId} class={FORM_HELPER_CLASS}>
{helper}
</p>
)}
</div>
);
}
export interface CheckboxProps {
name: string;
value: string;
label: string;
checked?: boolean;
onChange?: string;
}
export function Checkbox({name, value, label, checked, onChange}: CheckboxProps) {
return (
<label class="group flex w-full cursor-pointer items-center gap-2">
<input
type="checkbox"
name={name}
value={value}
checked={checked}
class="hidden"
{...(onChange ? {onchange: onChange} : {})}
/>
<div class="checkbox-custom flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
class="h-[18px] w-[18px]"
style="stroke-width: 32;"
>
<polyline
points="40 144 96 200 224 72"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div class="min-w-0 flex-1">
<span class={cn('block text-neutral-900 text-sm', 'leading-5')}>{label}</span>
</div>
</label>
);
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {PropsWithChildren, ReactNode} from 'hono/jsx';
export interface FormFieldGroupProps {
label: string;
helperText?: string;
error?: string;
required?: boolean;
children: ReactNode;
id?: string;
}
export function FormFieldGroup({
label,
helperText,
error,
required = false,
children,
id,
}: PropsWithChildren<FormFieldGroupProps>) {
const fieldId = id || label.toLowerCase().replace(/\s+/g, '-');
const helperId = `${fieldId}-helper`;
const errorId = `${fieldId}-error`;
const labelClass = error ? 'text-red-700 font-medium' : 'text-neutral-700 font-medium';
const helperClass = error ? 'text-red-600' : 'text-neutral-500';
return (
<div class="space-y-1.5">
<label for={fieldId} class={`block text-sm ${labelClass}`}>
{label}
{required && <span class="ml-1 text-red-600">*</span>}
</label>
{children}
{helperText && !error && (
<p id={helperId} class={`text-xs ${helperClass}`}>
{helperText}
</p>
)}
{error && (
<p id={errorId} class="font-medium text-red-600 text-xs">
{error}
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {PropsWithChildren, ReactNode} from 'hono/jsx';
export interface FormModalProps {
id: string;
title: string;
action: string;
method?: 'post' | 'get';
children: ReactNode;
submitText?: string;
cancelText?: string;
size?: 'small' | 'medium' | 'large';
footer?: ReactNode;
}
const sizeClasses = {
small: 'max-w-md',
medium: 'max-w-lg',
large: 'max-w-2xl',
};
export function FormModal({
id,
title,
action,
method = 'post',
children,
submitText = 'Submit',
cancelText = 'Cancel',
size = 'medium',
footer,
}: PropsWithChildren<FormModalProps>) {
const modalClass = sizeClasses[size];
const closeScript = `document.getElementById('${id}').classList.add('hidden')`;
return (
<div id={id} class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="flex min-h-screen items-center justify-center p-4">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick={closeScript} aria-hidden="true" />
<div class={`relative rounded-lg bg-white shadow-xl ${modalClass} w-full`}>
<div class="flex items-center justify-between border-neutral-200 border-b p-4">
<h2 class="font-semibold text-lg text-neutral-900">{title}</h2>
<button
type="button"
class="p-1 text-neutral-400 transition-colors hover:text-neutral-600"
onclick={closeScript}
aria-label="Close"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form action={action} method={method}>
<div class="p-4">{children}</div>
<div class="flex items-center justify-end gap-3 rounded-b-lg border-neutral-200 border-t bg-neutral-50 p-4">
{footer || (
<>
<button
type="button"
class="rounded border border-neutral-300 bg-white px-4 py-2 font-medium text-neutral-700 text-sm transition-colors hover:bg-neutral-50"
onclick={closeScript}
>
{cancelText}
</button>
<button
type="submit"
class="rounded bg-neutral-900 px-4 py-2 font-medium text-sm text-white transition-colors hover:bg-neutral-800"
>
{submitText}
</button>
</>
)}
</div>
</form>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {PropsWithChildren, ReactNode} from 'hono/jsx';
export type FormSectionPadding = 'none' | 'small' | 'medium' | 'large';
export interface FormSectionProps {
title?: string;
bordered?: boolean;
padding?: FormSectionPadding;
children: ReactNode;
class?: string;
}
const paddingClasses: Record<FormSectionPadding, string> = {
none: '',
small: 'p-3',
medium: 'p-4',
large: 'p-6',
};
export function FormSection({
title,
bordered = false,
padding = 'medium',
children,
class: className,
}: PropsWithChildren<FormSectionProps>) {
const containerClass = bordered
? `border border-neutral-200 rounded-lg bg-white ${paddingClasses[padding]}`
: paddingClasses[padding];
const content = <div class={`space-y-4 ${containerClass} ${className ?? ''}`}>{children}</div>;
if (title) {
return (
<div class={className ?? ''}>
<h3 class="mb-3 font-semibold text-lg text-neutral-900">{title}</h3>
{bordered ? content : <div class={containerClass}>{children}</div>}
</div>
);
}
return content;
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {PropsWithChildren, ReactNode} from 'hono/jsx';
export type InputGroupGap = 'small' | 'medium' | 'large';
export interface InputGroupProps {
children: ReactNode;
gap?: InputGroupGap;
direction?: 'vertical' | 'horizontal';
align?: 'start' | 'center' | 'end' | 'stretch';
class?: string;
}
const gapClasses: Record<InputGroupGap, string> = {
small: 'gap-3',
medium: 'gap-4',
large: 'gap-6',
};
const directionClasses: Record<'vertical' | 'horizontal', string> = {
vertical: 'flex-col',
horizontal: 'flex-row',
};
const alignClasses: Record<'start' | 'center' | 'end' | 'stretch', string> = {
start: 'items-start',
center: 'items-center',
end: 'items-end',
stretch: 'items-stretch',
};
export function InputGroup({
children,
gap = 'medium',
direction = 'vertical',
align = 'stretch',
class: className,
}: PropsWithChildren<InputGroupProps>) {
return (
<div class={`flex ${directionClasses[direction]} ${gapClasses[gap]} ${alignClasses[align]} ${className ?? ''}`}>
{children}
</div>
);
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {ChildrenProps, GapProps, GridProps, InfoItemProps} from '@fluxer/ui/src/types/Common';
import type {PropsWithChildren} from 'hono/jsx';
export interface FlexRowProps extends GapProps {}
export interface StackProps extends GapProps {}
export function FlexRow({gap = '3', children}: PropsWithChildren<FlexRowProps>) {
return <div class={`flex items-center gap-${gap}`}>{children}</div>;
}
export function FlexRowBetween({children}: ChildrenProps) {
return <div class="flex flex-wrap items-center justify-between gap-3">{children}</div>;
}
export function Stack({gap = '4', children}: PropsWithChildren<StackProps>) {
return <div class={`space-y-${gap}`}>{children}</div>;
}
export function Grid({cols = '2', gap = '4', children}: PropsWithChildren<GridProps>) {
return <div class={`grid grid-cols-${cols} gap-${gap}`}>{children}</div>;
}
export function InfoItem({label, value}: InfoItemProps) {
return (
<div>
<div class="mb-1 font-medium text-neutral-600 text-sm">{label}</div>
<div class="text-neutral-900 text-sm">{value ?? '-'}</div>
</div>
);
}
export function InfoGrid({children}: ChildrenProps) {
return <div class="grid grid-cols-2 gap-x-6 gap-y-3 md:grid-cols-3">{children}</div>;
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {Card} from '@fluxer/ui/src/components/Card';
import type {FC} from 'hono/jsx';
interface BackButtonProps {
href: string;
label: string;
}
export function BackButton({href, label}: BackButtonProps) {
return (
<a
href={href}
class="inline-flex items-center gap-2 text-neutral-900 text-sm underline decoration-neutral-300 hover:text-neutral-600 hover:decoration-neutral-500"
>
&larr; {label}
</a>
);
}
export const NotFoundView: FC<{resourceName: string; backUrl: string; backLabel: string}> = ({
resourceName,
backUrl,
backLabel,
}) => (
<div class="mx-auto max-w-2xl">
<Card padding="lg">
<div class="space-y-4 text-center">
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-neutral-100">
<span class="font-semibold text-2xl text-neutral-400">?</span>
</div>
<h2 class="font-semibold text-lg text-neutral-900">{resourceName} Not Found</h2>
<p class="text-neutral-600">
The {resourceName} you're looking for doesn't exist or you don't have permission to view it.
</p>
<div class="pt-4">
<BackButton href={backUrl} label={backLabel} />
</div>
</div>
</Card>
</div>
);

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export interface PaginationProps {
currentPage: number;
totalPages: number;
basePath?: string;
showPageNumbers?: boolean;
previousLabel?: string;
nextLabel?: string;
pageInfo?: string;
buildUrlFn?: (page: number) => string;
}
export function Pagination({
currentPage,
totalPages,
basePath = '',
showPageNumbers = true,
previousLabel = '← Previous',
nextLabel = 'Next →',
pageInfo,
buildUrlFn,
}: PaginationProps) {
const hasPrevious = currentPage > 0;
const hasNext = currentPage < totalPages - 1;
const getPageUrl = (page: number) => {
if (buildUrlFn) {
return `${basePath}${buildUrlFn(page)}`;
}
return `${basePath}?page=${page}`;
};
const previousButton = hasPrevious ? (
<a
href={getPageUrl(currentPage - 1)}
class="rounded-lg border border-neutral-300 bg-white px-6 py-2 font-medium text-neutral-900 text-sm no-underline transition-colors hover:bg-neutral-50"
>
{previousLabel}
</a>
) : (
<div class="cursor-not-allowed rounded-lg border border-neutral-200 bg-neutral-100 px-6 py-2 font-medium text-neutral-400 text-sm">
{previousLabel}
</div>
);
const nextButton = hasNext ? (
<a
href={getPageUrl(currentPage + 1)}
class="rounded-lg bg-neutral-900 px-6 py-2 font-medium text-sm text-white no-underline transition-colors hover:bg-neutral-800"
>
{nextLabel}
</a>
) : (
<div class="cursor-not-allowed rounded-lg bg-neutral-100 px-6 py-2 font-medium text-neutral-400 text-sm">
{nextLabel}
</div>
);
const pageIndicator = pageInfo ?? `Page ${currentPage + 1} of ${totalPages}`;
return (
<div class="mt-6 flex items-center justify-center gap-3">
{previousButton}
{showPageNumbers && <span class="text-neutral-600 text-sm">{pageIndicator}</span>}
{nextButton}
</div>
);
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export type RadioGroupOrientation = 'vertical' | 'horizontal';
export interface RadioOption {
value: string;
label: string;
disabled?: boolean;
}
export interface RadioGroupProps {
name: string;
label?: string;
value: string;
options: Array<RadioOption>;
orientation?: RadioGroupOrientation;
disabled?: boolean;
helperText?: string;
error?: string;
onChangeScript?: string;
}
const orientationClasses: Record<RadioGroupOrientation, string> = {
vertical: 'flex-col gap-3',
horizontal: 'flex-row gap-6',
};
export function RadioGroup({
name,
label,
value,
options,
orientation = 'vertical',
disabled = false,
helperText,
error,
onChangeScript,
}: RadioGroupProps) {
const groupId = `${name}-group`;
return (
<div class="space-y-1.5">
{label && <span class="block font-medium text-neutral-700 text-sm">{label}</span>}
<div id={groupId} class={`flex ${orientationClasses[orientation]}`}>
{options.map((option) => {
const optionId = `${name}-${option.value}`;
const isOptionDisabled = disabled || option.disabled;
return (
<label
key={option.value}
for={optionId}
class={`flex cursor-pointer items-center gap-2 ${
isOptionDisabled ? 'cursor-not-allowed opacity-50' : ''
}`}
>
<div class="relative">
<input
type="radio"
id={optionId}
name={name}
value={option.value}
checked={value === option.value}
disabled={isOptionDisabled}
class="sr-only"
onchange={onChangeScript}
/>
<div
class={`h-4 w-4 rounded-full border-2 transition-colors ${
value === option.value ? 'border-neutral-900' : 'border-neutral-300'
}`}
>
{value === option.value && <div class="mt-0.5 ml-0.5 h-2 w-2 rounded-full bg-neutral-900" />}
</div>
</div>
<span class="text-neutral-700 text-sm">{option.label}</span>
</label>
);
})}
</div>
{helperText && !error && <p class="text-neutral-500 text-xs">{helperText}</p>}
{error && <p class="font-medium text-red-600 text-xs">{error}</p>}
</div>
);
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {Button} from '@fluxer/ui/src/components/Button';
import {Card} from '@fluxer/ui/src/components/Card';
import {
FORM_CONTROL_INPUT_CLASS,
FORM_CONTROL_SELECT_CLASS,
FORM_SELECT_ICON_CLASS,
} from '@fluxer/ui/src/styles/FormControls';
import {cn} from '@fluxer/ui/src/utils/ClassNames';
import type {FC} from 'hono/jsx';
export type SearchFieldType = 'text' | 'select' | 'number';
export interface SearchFieldOption {
value: string;
label: string;
}
export interface SearchField {
name: string;
type: SearchFieldType;
label?: string;
placeholder?: string;
value?: string | number | undefined;
options?: Array<SearchFieldOption>;
autocomplete?: string;
}
export interface SearchFormProps {
action: string;
method?: 'get' | 'post';
fields: Array<SearchField>;
submitLabel?: string;
showClear?: boolean;
clearHref?: string;
clearLabel?: string;
helperText?: string;
layout?: 'vertical' | 'horizontal';
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
basePath?: string;
}
function withBasePath(basePath: string, path: string): string {
return `${basePath}${path}`;
}
function getSearchInputClass(): string {
return cn(FORM_CONTROL_INPUT_CLASS, 'h-10');
}
function getSearchSelectClass(): string {
return cn(FORM_CONTROL_SELECT_CLASS, 'h-10');
}
export const SearchForm: FC<SearchFormProps> = ({
action,
method = 'get',
fields,
submitLabel = 'Search',
showClear = true,
clearHref,
clearLabel = 'Clear',
helperText,
layout = 'vertical',
padding = 'sm',
basePath = '',
}) => {
const isHorizontal = layout === 'horizontal';
const actionUrl = withBasePath(basePath, action);
const formClass = isHorizontal ? 'flex flex-col gap-3 sm:flex-row sm:items-center' : 'space-y-4';
const fieldGroupClass = isHorizontal ? 'flex flex-1 flex-col gap-2 sm:flex-row' : 'space-y-4';
const actionGroupClass = isHorizontal ? 'flex flex-col gap-2 sm:shrink-0 sm:flex-row' : 'flex flex-wrap gap-2';
const clearUrl = clearHref ? withBasePath(basePath, clearHref) : undefined;
return (
<Card padding={padding}>
<form method={method} action={actionUrl} class={formClass}>
<div class={fieldGroupClass}>
{fields.map((field) => (
<SearchFieldInput key={field.name} field={field} layout={layout} />
))}
</div>
<div class={actionGroupClass}>
<Button type="submit" variant="primary" fullWidth={isHorizontal}>
{submitLabel}
</Button>
{showClear && clearUrl && (
<Button type="button" href={clearUrl} variant="secondary" fullWidth={isHorizontal} ariaLabel={clearLabel}>
{clearLabel}
</Button>
)}
</div>
{helperText && <p class={cn('text-neutral-500 text-xs', isHorizontal && 'sm:pt-1')}>{helperText}</p>}
</form>
</Card>
);
};
interface SearchFieldInputProps {
field: SearchField;
layout: 'vertical' | 'horizontal';
}
function SearchFieldInput({field, layout}: SearchFieldInputProps) {
const controlId = `search-${field.name}`;
const isVertical = layout === 'vertical';
const containerClass = isVertical ? 'w-full' : 'flex-1';
const labelClass = 'mb-2 block font-medium text-neutral-700 text-sm';
if (field.type === 'select') {
return (
<div class={containerClass}>
{isVertical && field.label && (
<label for={controlId} class={labelClass}>
{field.label}
</label>
)}
<div class="relative">
<select id={controlId} name={field.name} class={getSearchSelectClass()} autocomplete={field.autocomplete}>
{field.options?.map((option) => (
<option key={option.value} value={option.value} selected={String(field.value) === option.value}>
{option.label}
</option>
))}
</select>
<svg class={FORM_SELECT_ICON_CLASS} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="m6 8 4 4 4-4"
/>
</svg>
</div>
</div>
);
}
return (
<div class={containerClass}>
{isVertical && field.label && (
<label for={controlId} class={labelClass}>
{field.label}
</label>
)}
<input
id={controlId}
type={field.type}
name={field.name}
value={field.value ?? ''}
placeholder={field.placeholder}
class={getSearchInputClass()}
autocomplete={field.autocomplete}
/>
</div>
);
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {FC} from 'hono/jsx';
export interface SliderInputProps {
id: string;
name: string;
label: string;
min: number;
max: number;
value: number;
step?: number;
rangeText?: string;
displayId?: string;
disabled?: boolean;
}
function getSliderPercent(value: number, min: number, max: number): number {
const range = max - min;
if (range <= 0) {
return 0;
}
return ((value - min) / range) * 100;
}
export const SliderInput: FC<SliderInputProps> = ({
id,
name,
label,
min,
max,
value: initialValue,
step = 1,
rangeText,
displayId,
disabled = false,
}) => {
const actualDisplayId = displayId ?? `${id}-value`;
const rangeTextId = rangeText ? `${id}-range-text` : undefined;
const sliderPercent = getSliderPercent(initialValue, min, max);
const sliderStyle = `--slider-percent: ${sliderPercent}%;`;
const onInputScript = `const value=Number(this.value);const min=${min};const max=${max};const percent=max>min?((value-min)/(max-min))*100:0;this.style.setProperty('--slider-percent', percent + '%');const output=document.getElementById('${actualDisplayId}');if(output){output.textContent=String(value);}`;
return (
<>
<div class="flex items-center justify-between">
<label for={id} class="font-medium text-neutral-800 text-sm">
{label}
</label>
{rangeText && (
<span id={rangeTextId} class="text-neutral-500 text-xs">
{rangeText}
</span>
)}
</div>
<div class="flex items-center gap-4">
<input
type="range"
id={id}
name={name}
min={min}
max={max}
step={step}
value={initialValue}
disabled={disabled}
aria-describedby={rangeTextId}
oninput={onInputScript}
style={sliderStyle}
class="slider-input w-full flex-1 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
/>
<output
id={actualDisplayId}
for={id}
aria-live="polite"
class={`w-12 text-right font-medium text-sm tabular-nums ${disabled ? 'text-neutral-400' : 'text-neutral-900'}`}
>
{initialValue}
</output>
</div>
</>
);
};

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {ChildrenProps, LabelProps, MutedProps} from '@fluxer/ui/src/types/Common';
import type {PropsWithChildren} from 'hono/jsx';
export interface TableHeaderCellProps extends LabelProps {}
export interface TableCellProps extends MutedProps {
colSpan?: number;
}
export function TableContainer({children}: ChildrenProps) {
return <div class="overflow-hidden overflow-x-auto rounded-lg border border-neutral-200 bg-white">{children}</div>;
}
export function Table({children}: ChildrenProps) {
return <table class="min-w-full divide-y divide-neutral-200">{children}</table>;
}
export function TableHead({children}: ChildrenProps) {
return <thead class="bg-neutral-50">{children}</thead>;
}
export function TableBody({children}: ChildrenProps) {
return <tbody class="divide-y divide-neutral-200 bg-white">{children}</tbody>;
}
export function TableRow({children}: ChildrenProps) {
return <tr class="transition-colors hover:bg-neutral-50">{children}</tr>;
}
export function TableHeaderCell({label}: TableHeaderCellProps) {
return <th class="px-6 py-3 text-left text-neutral-600 text-xs uppercase tracking-wider">{label}</th>;
}
export function TableCell({muted, colSpan, children}: PropsWithChildren<TableCellProps>) {
return (
<td class={`px-6 py-4 text-sm ${muted ? 'text-neutral-600' : 'text-neutral-900'}`} colspan={colSpan}>
{children}
</td>
);
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export type ToggleSwitchSize = 'small' | 'medium' | 'large';
export interface ToggleSwitchProps {
name: string;
label?: string;
checked: boolean;
disabled?: boolean;
size?: ToggleSwitchSize;
helperText?: string;
id?: string;
onChangeScript?: string;
}
const sizeClasses: Record<ToggleSwitchSize, {track: string; thumb: string}> = {
small: {
track: 'w-8 h-5',
thumb: 'w-3 h-3 translate-x-3',
},
medium: {
track: 'w-11 h-6',
thumb: 'w-4 h-4 translate-x-5',
},
large: {
track: 'w-14 h-7',
thumb: 'w-5 h-5 translate-x-7',
},
};
export function ToggleSwitch({
name,
label,
checked,
disabled = false,
size = 'medium',
helperText,
id,
onChangeScript,
}: ToggleSwitchProps) {
const switchId = id || name;
const {track, thumb} = sizeClasses[size];
const trackClass = checked ? `bg-neutral-900` : `bg-neutral-300`;
const thumbPosition = checked ? thumb : 'translate-x-0.5';
return (
<div class="space-y-1">
<label
for={switchId}
class={`flex items-center gap-3 ${disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
>
<div class="relative">
<input
type="checkbox"
id={switchId}
name={name}
checked={checked}
disabled={disabled}
class="sr-only"
onchange={onChangeScript}
/>
<div class={`${track} ${trackClass} rounded-full transition-colors duration-200 ease-in-out`}>
<div
class={`${thumb} ${thumbPosition} absolute top-0.5 rounded-full bg-white shadow-sm transition-transform duration-200 ease-in-out`}
/>
</div>
</div>
{label && <span class="font-medium text-neutral-700 text-sm">{label}</span>}
</label>
{helperText && <p class="pl-14 text-neutral-500 text-xs">{helperText}</p>}
</div>
);
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
interface TextProps {
text: string;
}
export function HeadingPage({text}: TextProps) {
return <h1 class="font-bold text-lg text-neutral-900">{text}</h1>;
}
export function HeadingSection({text}: TextProps) {
return <h2 class="font-bold text-base text-neutral-900">{text}</h2>;
}
export function HeadingCard({text}: TextProps) {
return <h3 class="font-semibold text-base text-neutral-900">{text}</h3>;
}
export function HeadingCardWithMargin({text}: TextProps) {
return (
<div class="mb-4">
<HeadingCard text={text} />
</div>
);
}
export function TextMuted({text}: TextProps) {
return <p class="text-neutral-600 text-sm">{text}</p>;
}
export function TextSmallMuted({text}: TextProps) {
return <p class="text-neutral-500 text-xs">{text}</p>;
}