@f0rbit/ui 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import { type JSX, splitProps, createEffect, onMount } from "solid-js";
2
+
3
+ export interface CheckboxProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, "type"> {
4
+ label?: string;
5
+ description?: string;
6
+ indeterminate?: boolean;
7
+ }
8
+
9
+ export function Checkbox(props: CheckboxProps) {
10
+ const [local, rest] = splitProps(props, ["label", "description", "indeterminate", "class", "disabled", "id"]);
11
+
12
+ let inputRef: HTMLInputElement | undefined;
13
+
14
+ const setIndeterminate = () => {
15
+ if (inputRef) inputRef.indeterminate = local.indeterminate ?? false;
16
+ };
17
+
18
+ onMount(setIndeterminate);
19
+ createEffect(setIndeterminate);
20
+
21
+ const classes = () =>
22
+ `checkbox ${local.disabled ? "checkbox-disabled" : ""} ${local.class ?? ""}`.trim();
23
+
24
+ const inputId = () => local.id ?? (local.label ? `checkbox-${local.label.toLowerCase().replace(/\s+/g, "-")}` : undefined);
25
+
26
+ return (
27
+ <label class={classes()}>
28
+ <input
29
+ ref={inputRef}
30
+ type="checkbox"
31
+ class="checkbox-input"
32
+ id={inputId()}
33
+ disabled={local.disabled}
34
+ {...rest}
35
+ />
36
+ <span class="checkbox-box" aria-hidden="true">
37
+ <svg class="checkbox-check" viewBox="0 0 12 12" fill="none">
38
+ <path d="M2 6L5 9L10 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
39
+ </svg>
40
+ <span class="checkbox-indeterminate" />
41
+ </span>
42
+ {(local.label || local.description) && (
43
+ <span class="checkbox-content">
44
+ {local.label && <span class="checkbox-label">{local.label}</span>}
45
+ {local.description && <span class="checkbox-description">{local.description}</span>}
46
+ </span>
47
+ )}
48
+ </label>
49
+ );
50
+ }
@@ -0,0 +1,36 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+
3
+ export type ChevronFacing = "right" | "down";
4
+
5
+ export interface ChevronProps extends JSX.SvgSVGAttributes<SVGSVGElement> {
6
+ expanded?: boolean;
7
+ /** Which way the chevron points. Default: "right" */
8
+ facing?: ChevronFacing;
9
+ size?: number | string;
10
+ }
11
+
12
+ export function Chevron(props: ChevronProps) {
13
+ const [local, rest] = splitProps(props, ["expanded", "facing", "size", "class"]);
14
+
15
+ const classes = () => {
16
+ const parts = ["chevron"];
17
+ if (local.facing === "down") {
18
+ parts.push("chevron-down");
19
+ }
20
+ if (local.expanded) {
21
+ parts.push("active");
22
+ }
23
+ if (local.class) {
24
+ parts.push(local.class);
25
+ }
26
+ return parts.join(" ");
27
+ };
28
+
29
+ const size = () => local.size ?? "1em";
30
+
31
+ return (
32
+ <svg class={classes()} viewBox="0 0 24 24" width={size()} height={size()} fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" {...rest}>
33
+ <path d="m9 18 6-6-6-6" />
34
+ </svg>
35
+ );
36
+ }
@@ -0,0 +1,51 @@
1
+ import { type JSX, splitProps, createSignal, Show } from "solid-js";
2
+
3
+ export interface ClampProps extends JSX.HTMLAttributes<HTMLDivElement> {
4
+ lines?: number;
5
+ showMoreText?: string;
6
+ showLessText?: string;
7
+ }
8
+
9
+ export function Clamp(props: ClampProps) {
10
+ const [local, rest] = splitProps(props, ["lines", "showMoreText", "showLessText", "class", "children"]);
11
+
12
+ const lines = () => local.lines ?? 3;
13
+ const showMoreText = () => local.showMoreText ?? "show more";
14
+ const showLessText = () => local.showLessText ?? "show less";
15
+
16
+ const [expanded, setExpanded] = createSignal(false);
17
+ const [overflows, setOverflows] = createSignal(false);
18
+
19
+ let contentRef: HTMLDivElement | undefined;
20
+
21
+ const checkOverflow = () => {
22
+ if (!contentRef) return;
23
+ setOverflows(contentRef.scrollHeight > contentRef.clientHeight);
24
+ };
25
+
26
+ const toggle = () => setExpanded(prev => !prev);
27
+
28
+ const wrapperClasses = () => `clamp ${local.class ?? ""}`.trim();
29
+
30
+ const contentClasses = () => (expanded() ? "clamp-content" : "clamp-content clamp-clamped");
31
+
32
+ return (
33
+ <div class={wrapperClasses()} {...rest}>
34
+ <div
35
+ ref={el => {
36
+ contentRef = el;
37
+ requestAnimationFrame(checkOverflow);
38
+ }}
39
+ class={contentClasses()}
40
+ style={`--clamp-lines: ${lines()}`}
41
+ >
42
+ {local.children}
43
+ </div>
44
+ <Show when={overflows() || expanded()}>
45
+ <button class="clamp-toggle" onClick={toggle}>
46
+ {expanded() ? showLessText() : showMoreText()}
47
+ </button>
48
+ </Show>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,39 @@
1
+ import { type JSX, splitProps, createSignal, Show } from "solid-js";
2
+ import { Chevron } from "./Chevron";
3
+
4
+ export interface CollapsibleProps {
5
+ defaultOpen?: boolean;
6
+ open?: boolean;
7
+ onOpenChange?: (open: boolean) => void;
8
+ children: JSX.Element;
9
+ trigger: JSX.Element | string;
10
+ }
11
+
12
+ export function Collapsible(props: CollapsibleProps) {
13
+ const [local, rest] = splitProps(props, ["defaultOpen", "open", "onOpenChange", "children", "trigger"]);
14
+
15
+ const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false);
16
+
17
+ const isControlled = () => local.open !== undefined;
18
+ const isOpen = () => (isControlled() ? local.open! : internalOpen());
19
+
20
+ const toggle = () => {
21
+ const next = !isOpen();
22
+ if (!isControlled()) {
23
+ setInternalOpen(next);
24
+ }
25
+ local.onOpenChange?.(next);
26
+ };
27
+
28
+ return (
29
+ <div class="collapsible">
30
+ <button class="collapsible-trigger" onClick={toggle}>
31
+ {local.trigger}
32
+ <Chevron class="collapsible-chevron" expanded={isOpen()} />
33
+ </button>
34
+ <Show when={isOpen()}>
35
+ <div class="collapsible-content">{local.children}</div>
36
+ </Show>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,114 @@
1
+ import { type JSX, splitProps, createSignal, Show, onMount, onCleanup, createContext, useContext } from "solid-js";
2
+ import { isServer } from "solid-js/web";
3
+
4
+ type DropdownContextValue = {
5
+ open: () => boolean;
6
+ setOpen: (value: boolean) => void;
7
+ close: () => void;
8
+ };
9
+
10
+ const DropdownContext = createContext<DropdownContextValue>();
11
+
12
+ export interface DropdownProps {
13
+ children: JSX.Element;
14
+ }
15
+
16
+ export interface DropdownTriggerProps {
17
+ children: JSX.Element;
18
+ }
19
+
20
+ export interface DropdownMenuProps {
21
+ children: JSX.Element;
22
+ }
23
+
24
+ export interface DropdownItemProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
25
+ onClick?: () => void;
26
+ active?: boolean;
27
+ children: JSX.Element;
28
+ }
29
+
30
+ export function Dropdown(props: DropdownProps) {
31
+ const [open, setOpen] = createSignal(false);
32
+ let containerRef: HTMLDivElement | undefined;
33
+
34
+ const close = () => setOpen(false);
35
+
36
+ const handleClickOutside = (e: MouseEvent) => {
37
+ if (!containerRef?.contains(e.target as Node)) {
38
+ close();
39
+ }
40
+ };
41
+
42
+ const handleKeyDown = (e: KeyboardEvent) => {
43
+ if (e.key === "Escape" && open()) {
44
+ close();
45
+ }
46
+ };
47
+
48
+ onMount(() => {
49
+ if (isServer) return;
50
+ document.addEventListener("click", handleClickOutside);
51
+ document.addEventListener("keydown", handleKeyDown);
52
+ });
53
+
54
+ onCleanup(() => {
55
+ if (isServer) return;
56
+ document.removeEventListener("click", handleClickOutside);
57
+ document.removeEventListener("keydown", handleKeyDown);
58
+ });
59
+
60
+ return (
61
+ <DropdownContext.Provider value={{ open, setOpen, close }}>
62
+ <div ref={el => (containerRef = el)} class="dropdown">
63
+ {props.children}
64
+ </div>
65
+ </DropdownContext.Provider>
66
+ );
67
+ }
68
+
69
+ export function DropdownTrigger(props: DropdownTriggerProps) {
70
+ const ctx = useContext(DropdownContext);
71
+
72
+ const handleClick = (e: MouseEvent) => {
73
+ e.stopPropagation();
74
+ ctx?.setOpen(!ctx.open());
75
+ };
76
+
77
+ return (
78
+ <div class="dropdown-trigger" onClick={handleClick}>
79
+ {props.children}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export function DropdownMenu(props: DropdownMenuProps) {
85
+ const ctx = useContext(DropdownContext);
86
+
87
+ return (
88
+ <Show when={ctx?.open()}>
89
+ <div class="dropdown-menu">{props.children}</div>
90
+ </Show>
91
+ );
92
+ }
93
+
94
+ export function DropdownItem(props: DropdownItemProps) {
95
+ const [local, rest] = splitProps(props, ["onClick", "active", "children"]);
96
+ const ctx = useContext(DropdownContext);
97
+
98
+ const handleClick = () => {
99
+ local.onClick?.();
100
+ ctx?.close();
101
+ };
102
+
103
+ const classes = () => `dropdown-item ${local.active ? "active" : ""}`.trim();
104
+
105
+ return (
106
+ <button type="button" class={classes()} onClick={handleClick} {...rest}>
107
+ {local.children}
108
+ </button>
109
+ );
110
+ }
111
+
112
+ export function DropdownDivider() {
113
+ return <div class="dropdown-divider" />;
114
+ }
@@ -0,0 +1,22 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+
3
+ type EmptyProps = {
4
+ icon?: JSX.Element;
5
+ title?: string;
6
+ description?: string;
7
+ children?: JSX.Element;
8
+ };
9
+
10
+ export function Empty(props: EmptyProps): JSX.Element {
11
+ const [local] = splitProps(props, ["icon", "title", "description", "children"]);
12
+
13
+ return (
14
+ <div class="empty">
15
+ {local.icon && <div class="empty-icon">{local.icon}</div>}
16
+ {local.title && <h3 class="empty-title">{local.title}</h3>}
17
+ {local.description && <p class="empty-description">{local.description}</p>}
18
+ {local.children}
19
+ </div>
20
+ );
21
+ }
22
+ export type { EmptyProps };
@@ -0,0 +1,33 @@
1
+ import { type JSX, splitProps, Show } from "solid-js";
2
+
3
+ export interface FormFieldProps extends JSX.HTMLAttributes<HTMLDivElement> {
4
+ label: string;
5
+ error?: string;
6
+ description?: string;
7
+ required?: boolean;
8
+ children: JSX.Element;
9
+ id?: string;
10
+ }
11
+
12
+ export function FormField(props: FormFieldProps) {
13
+ const [local, rest] = splitProps(props, ["label", "error", "description", "required", "children", "id", "class"]);
14
+ const classes = () => `form-field ${local.error ? "form-field-has-error" : ""} ${local.class ?? ""}`.trim();
15
+
16
+ return (
17
+ <div class={classes()} {...rest}>
18
+ <label class="form-field-label" for={local.id}>
19
+ {local.label}
20
+ <Show when={local.required}>
21
+ <span class="form-field-required" aria-hidden="true">*</span>
22
+ </Show>
23
+ </label>
24
+ <Show when={local.description}>
25
+ <span class="form-field-description">{local.description}</span>
26
+ </Show>
27
+ {local.children}
28
+ <Show when={local.error}>
29
+ <span class="form-field-error" role="alert">{local.error}</span>
30
+ </Show>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,35 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+
3
+ export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
4
+ error?: boolean;
5
+ }
6
+
7
+ export interface TextareaProps extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement> {
8
+ error?: boolean;
9
+ }
10
+
11
+ export interface SelectProps extends JSX.SelectHTMLAttributes<HTMLSelectElement> {
12
+ error?: boolean;
13
+ }
14
+
15
+ export function Input(props: InputProps) {
16
+ const [local, rest] = splitProps(props, ["error", "class", "disabled"]);
17
+ const classes = () => `input ${local.error ? "input-error" : ""} ${local.class ?? ""}`.trim();
18
+ return <input class={classes()} disabled={local.disabled} {...rest} />;
19
+ }
20
+
21
+ export function Textarea(props: TextareaProps) {
22
+ const [local, rest] = splitProps(props, ["error", "class", "disabled"]);
23
+ const classes = () => `textarea ${local.error ? "input-error" : ""} ${local.class ?? ""}`.trim();
24
+ return <textarea class={classes()} disabled={local.disabled} {...rest} />;
25
+ }
26
+
27
+ export function Select(props: SelectProps) {
28
+ const [local, rest] = splitProps(props, ["error", "class", "disabled", "children"]);
29
+ const classes = () => `select ${local.error ? "input-error" : ""} ${local.class ?? ""}`.trim();
30
+ return (
31
+ <select class={classes()} disabled={local.disabled} {...rest}>
32
+ {local.children}
33
+ </select>
34
+ );
35
+ }
@@ -0,0 +1,106 @@
1
+ import { type JSX, splitProps, Show, onMount, onCleanup, createContext, useContext } from "solid-js";
2
+ import { Portal, isServer } from "solid-js/web";
3
+
4
+ type ModalContextValue = { onClose: () => void };
5
+ const ModalContext = createContext<ModalContextValue>();
6
+
7
+ export interface ModalProps extends JSX.DialogHtmlAttributes<HTMLDialogElement> {
8
+ open: boolean;
9
+ onClose: () => void;
10
+ }
11
+
12
+ export interface ModalHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {}
13
+ export interface ModalTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {}
14
+ export interface ModalBodyProps extends JSX.HTMLAttributes<HTMLDivElement> {}
15
+ export interface ModalFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {}
16
+
17
+ export function Modal(props: ModalProps) {
18
+ const [local, rest] = splitProps(props, ["open", "onClose", "class", "children"]);
19
+
20
+ const handleKeyDown = (e: KeyboardEvent) => {
21
+ if (e.key === "Escape" && local.open) {
22
+ local.onClose();
23
+ }
24
+ };
25
+
26
+ const handleOverlayClick = (e: MouseEvent) => {
27
+ if (e.target === e.currentTarget) {
28
+ local.onClose();
29
+ }
30
+ };
31
+
32
+ onMount(() => {
33
+ if (isServer) return;
34
+ document.addEventListener("keydown", handleKeyDown);
35
+ });
36
+
37
+ onCleanup(() => {
38
+ if (isServer) return;
39
+ document.removeEventListener("keydown", handleKeyDown);
40
+ });
41
+
42
+ const classes = () => `modal ${local.class ?? ""}`.trim();
43
+
44
+ return (
45
+ <Show when={local.open}>
46
+ <Portal>
47
+ <ModalContext.Provider value={{ onClose: local.onClose }}>
48
+ <div class="overlay" onClick={handleOverlayClick} onKeyDown={(e: KeyboardEvent) => e.key === "Escape" && local.onClose()} role="presentation">
49
+ <dialog class={classes()} open aria-modal="true" {...rest}>
50
+ {local.children}
51
+ </dialog>
52
+ </div>
53
+ </ModalContext.Provider>
54
+ </Portal>
55
+ </Show>
56
+ );
57
+ }
58
+
59
+ export function ModalHeader(props: ModalHeaderProps) {
60
+ const [local, rest] = splitProps(props, ["class", "children"]);
61
+ const ctx = useContext(ModalContext);
62
+
63
+ return (
64
+ <div class={`modal-header ${local.class ?? ""}`.trim()} {...rest}>
65
+ {local.children}
66
+ <Show when={ctx}>
67
+ <button type="button" class="modal-close" onClick={ctx?.onClose} aria-label="Close">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
69
+ <line x1="18" y1="6" x2="6" y2="18" />
70
+ <line x1="6" y1="6" x2="18" y2="18" />
71
+ </svg>
72
+ </button>
73
+ </Show>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ export function ModalTitle(props: ModalTitleProps) {
79
+ const [local, rest] = splitProps(props, ["class", "children"]);
80
+
81
+ return (
82
+ <h2 class={`modal-title ${local.class ?? ""}`.trim()} {...rest}>
83
+ {local.children}
84
+ </h2>
85
+ );
86
+ }
87
+
88
+ export function ModalBody(props: ModalBodyProps) {
89
+ const [local, rest] = splitProps(props, ["class", "children"]);
90
+
91
+ return (
92
+ <div class={`modal-body ${local.class ?? ""}`.trim()} {...rest}>
93
+ {local.children}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export function ModalFooter(props: ModalFooterProps) {
99
+ const [local, rest] = splitProps(props, ["class", "children"]);
100
+
101
+ return (
102
+ <div class={`modal-footer ${local.class ?? ""}`.trim()} {...rest}>
103
+ {local.children}
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,31 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+
3
+ export type SpinnerSize = "sm" | "md" | "lg";
4
+
5
+ export interface SpinnerProps extends JSX.HTMLAttributes<HTMLSpanElement> {
6
+ size?: SpinnerSize;
7
+ }
8
+
9
+ const sizeClasses: Record<SpinnerSize, string> = {
10
+ sm: "spinner-sm",
11
+ md: "",
12
+ lg: "spinner-lg",
13
+ };
14
+
15
+ export function Spinner(props: SpinnerProps) {
16
+ const [local, rest] = splitProps(props, ["size", "class"]);
17
+
18
+ const classes = () => {
19
+ const parts = ["spinner"];
20
+ const size = local.size ?? "md";
21
+ if (size !== "md") {
22
+ parts.push(sizeClasses[size]);
23
+ }
24
+ if (local.class) {
25
+ parts.push(local.class);
26
+ }
27
+ return parts.join(" ");
28
+ };
29
+
30
+ return <span class={classes()} {...rest} />;
31
+ }
@@ -0,0 +1,18 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+
3
+ type StatProps = {
4
+ value: string | number;
5
+ label: string;
6
+ } & Omit<JSX.HTMLAttributes<HTMLDivElement>, "children">;
7
+
8
+ export function Stat(props: StatProps): JSX.Element {
9
+ const [local, rest] = splitProps(props, ["value", "label", "class"]);
10
+
11
+ return (
12
+ <div class={local.class ? `stat ${local.class}` : "stat"} {...rest}>
13
+ <span class="stat-value">{local.value}</span>
14
+ <span class="stat-label">{local.label}</span>
15
+ </div>
16
+ );
17
+ }
18
+ export type { StatProps };
@@ -0,0 +1,43 @@
1
+ import { type JSX, splitProps } from "solid-js";
2
+
3
+ export type StatusState = "active" | "inactive" | "error" | "pending";
4
+
5
+ export interface StatusProps extends JSX.HTMLAttributes<HTMLSpanElement> {
6
+ state: StatusState;
7
+ label?: string;
8
+ }
9
+
10
+ const stateClasses: Record<StatusState, string> = {
11
+ active: "status-active",
12
+ inactive: "status-inactive",
13
+ error: "status-error",
14
+ pending: "status-pending",
15
+ };
16
+
17
+ const defaultLabels: Record<StatusState, string> = {
18
+ active: "Active",
19
+ inactive: "Inactive",
20
+ error: "Error",
21
+ pending: "Pending",
22
+ };
23
+
24
+ export function Status(props: StatusProps) {
25
+ const [local, rest] = splitProps(props, ["state", "label", "class"]);
26
+
27
+ const classes = () => {
28
+ const parts = ["status", stateClasses[local.state]];
29
+ if (local.class) {
30
+ parts.push(local.class);
31
+ }
32
+ return parts.join(" ");
33
+ };
34
+
35
+ const displayLabel = () => local.label ?? defaultLabels[local.state];
36
+
37
+ return (
38
+ <span class={classes()} {...rest}>
39
+ <span class="status-dot" />
40
+ <span>{displayLabel()}</span>
41
+ </span>
42
+ );
43
+ }