@solidpb/ui-kit 0.2.0 → 0.4.0

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.
@@ -1,7 +1,6 @@
1
1
  import { ParentComponent } from "solid-js";
2
2
  interface Props {
3
3
  class?: string;
4
- noPadding?: boolean;
5
4
  }
6
5
  export declare const Container: ParentComponent<Props>;
7
6
  export default Container;
@@ -1,15 +1,9 @@
1
1
  import { tv } from "tailwind-variants";
2
2
  const container = tv({
3
- base: "flex-1 bg-base-100 dark:bg-base-900 min-h-[calc(100vh-4rem)]",
4
- variants: {
5
- noPadding: {
6
- true: "",
7
- false: "py-4 px-[2vw]",
8
- },
9
- },
3
+ base: "flex-1 bg-base-100 min-h-[calc(100vh-4rem)] py-4 px-[2vw]",
10
4
  });
11
5
  export const Container = (props) => {
12
- const classes = container({ noPadding: props.noPadding, class: props.class });
6
+ const classes = container({ class: props.class });
13
7
  return <div class={classes}>{props.children}</div>;
14
8
  };
15
9
  export default Container;
@@ -28,6 +28,21 @@ const input = tv({
28
28
  size: "sm",
29
29
  },
30
30
  });
31
+ const label = tv({
32
+ base: "label text-xs",
33
+ variants: {
34
+ size: {
35
+ xs: "text-xs",
36
+ sm: "text-xs",
37
+ md: "text-xs",
38
+ lg: "text-sm",
39
+ xl: "text-sm",
40
+ },
41
+ },
42
+ defaultVariants: {
43
+ size: "sm",
44
+ },
45
+ });
31
46
  export const FileInput = (props) => {
32
47
  const [local, others] = splitProps(props, ["label", "class", "onChange", "saveFunc"]);
33
48
  const handleChange = (files) => {
@@ -36,7 +51,7 @@ export const FileInput = (props) => {
36
51
  };
37
52
  return (<label class="flex flex-col gap-1 w-fit">
38
53
  <Show when={local.label}>
39
- <span class="label">{local.label}</span>
54
+ <span class={label({ size: props.size })}>{local.label}</span>
40
55
  </Show>
41
56
  <input {...others} type="file" class={input({ class: local.class })} accept={props.accept} multiple={props.multiple} onChange={(e) => handleChange(e.currentTarget.files)}/>
42
57
  </label>);
@@ -24,12 +24,10 @@ export interface FilterField<T> {
24
24
  name: keyof T;
25
25
  label: string;
26
26
  type: FieldType;
27
- operators?: FilterOperator[];
28
27
  options?: {
29
28
  label: string;
30
29
  value: string;
31
30
  }[];
32
- searchable?: boolean;
33
31
  }
34
32
  export interface SortOption<T> {
35
33
  field: keyof T;
@@ -1,19 +1,21 @@
1
1
  import { JSXElement } from "solid-js";
2
- import { type SwitchProps } from "../Switch";
3
- import { type SelectProps } from "../Select";
4
- import { type InputRootProps } from "../Input";
5
- import { type TextAreaRootProps } from "../TextArea";
6
- import { type CheckboxProps } from "../Checkbox";
7
- import { type NumberInputRootProps } from "../NumberInput";
8
- import { type SliderProps } from "../Slider";
9
- import { type ImageProps } from "../Image";
10
- import { type FileInputProps } from "../FileInput";
2
+ import { type SwitchProps } from "./Switch";
3
+ import { type SelectProps } from "./Select";
4
+ import { type InputRootProps } from "./Input";
5
+ import { type TextAreaRootProps } from "./TextArea";
6
+ import { type CheckboxProps } from "./Checkbox";
7
+ import { type NumberInputRootProps } from "./NumberInput";
8
+ import { type SliderProps } from "./Slider";
9
+ import { type ImageProps } from "./Image";
10
+ import { type FileInputProps } from "./FileInput";
11
+ import { RelationPickerProps } from "./RelationPicker";
11
12
  export interface FormProps<T> {
12
- data: T;
13
+ data: Partial<T>;
13
14
  title?: string;
14
15
  onSave?: (values: Partial<T>) => Promise<void>;
15
16
  onCancel?: () => void;
16
17
  children: JSXElement;
18
+ class?: string;
17
19
  }
18
20
  type BaseFieldProps<T> = {
19
21
  field: keyof T;
@@ -32,5 +34,8 @@ export declare function createForm<T>(): {
32
34
  FileField: (props: FileInputProps & BaseFieldProps<T>) => import("solid-js").JSX.Element;
33
35
  ImageField: (props: ImageProps & BaseFieldProps<T>) => import("solid-js").JSX.Element;
34
36
  SliderField: (props: SliderProps & BaseFieldProps<T>) => import("solid-js").JSX.Element;
37
+ RelationField: <K extends {
38
+ id: string;
39
+ }>(props: RelationPickerProps<K> & BaseFieldProps<T>) => import("solid-js").JSX.Element;
35
40
  };
36
41
  export {};
@@ -1,41 +1,47 @@
1
1
  import { splitProps } from "solid-js";
2
2
  import { createStore } from "solid-js/store";
3
+ import { tv } from "tailwind-variants";
3
4
  import { InternalFormContext, useInternalFormContext } from "./formContext";
4
- import { Switch } from "../Switch";
5
- import { Select } from "../Select";
6
- import { Input } from "../Input";
7
- import { TextArea } from "../TextArea";
8
- import { Checkbox } from "../Checkbox";
9
- import { NumberInput } from "../NumberInput";
10
- import { Slider } from "../Slider";
11
- import { Image } from "../Image";
12
- import { Button } from "../Button";
13
- import { FileInput } from "../FileInput";
5
+ import { Switch } from "./Switch";
6
+ import { Select } from "./Select";
7
+ import { Input } from "./Input";
8
+ import { TextArea } from "./TextArea";
9
+ import { Checkbox } from "./Checkbox";
10
+ import { NumberInput } from "./NumberInput";
11
+ import { Slider } from "./Slider";
12
+ import { Image } from "./Image";
13
+ import { Button } from "./Button";
14
+ import { FileInput } from "./FileInput";
15
+ import RelationPicker from "./RelationPicker";
16
+ const formClass = tv({
17
+ base: "space-y-4 space-x-4",
18
+ });
14
19
  export function createForm() {
15
20
  const Form = (props) => {
16
21
  const [values, setValues] = createStore({ ...props.data });
17
22
  const setValue = (key, value) => {
23
+ console.log("Setting value", key, value);
18
24
  setValues(key, value);
19
25
  };
20
26
  const getValue = (key) => {
21
27
  return values[key];
22
28
  };
23
- const contextValue = { setValue, getValue, values };
29
+ const contextValue = { setValue, getValue };
24
30
  const handleSubmit = (e) => {
25
31
  e.preventDefault();
26
- props.onSave?.(values);
32
+ props.onSave?.(JSON.parse(JSON.stringify(values))); // deep clone to avoid issues with reactive proxies
27
33
  };
28
34
  return (<InternalFormContext.Provider value={contextValue}>
29
- <form onSubmit={handleSubmit} class="space-y-4 space-x-4">
35
+ <form onSubmit={handleSubmit} class={formClass({ class: props.class })}>
30
36
  {props.title && <h2 class="text-lg font-semibold">{props.title}</h2>}
31
37
 
32
38
  {props.children}
33
39
 
34
40
  <div class="flex justify-end gap-2">
35
- {props.onCancel && (<Button appearance="neutral" onClick={props.onCancel} size="sm">
41
+ {props.onCancel && (<Button appearance="neutral" onClick={props.onCancel}>
36
42
  Cancel
37
43
  </Button>)}
38
- {props.onSave && (<Button appearance="success" type="submit" size="sm">
44
+ {props.onSave && (<Button appearance="success" type="submit">
39
45
  Save
40
46
  </Button>)}
41
47
  </div>
@@ -76,15 +82,32 @@ export function createForm() {
76
82
  };
77
83
  const ImageField = (props) => {
78
84
  const form = useInternalFormContext();
79
- return (<Image {...props} editable src={form.getValue(props.field)} onChange={(file) => {
80
- const fileURL = URL.createObjectURL(file);
81
- form.setValue(props.field, fileURL);
85
+ const [local, others] = splitProps(props, ["onChange"]);
86
+ // have to set src manually, when using with pocketbase, the value will be a URL string,
87
+ // but when uploading a new file it will be a File object, so we need to handle both cases
88
+ return (<Image editable {...others} onChange={(file) => {
89
+ form.setValue(props.field, file);
90
+ local.onChange?.(file);
82
91
  }}/>);
83
92
  };
84
93
  const SliderField = (props) => {
85
94
  const form = useInternalFormContext();
86
95
  return (<Slider {...props} value={form.getValue(props.field)} onChange={(v) => form.setValue(props.field, v)}/>);
87
96
  };
97
+ const RelationField = (props) => {
98
+ const form = useInternalFormContext();
99
+ const [local, others] = splitProps(props, ["onChange"]);
100
+ const handleChange = (val) => {
101
+ if (props.multi) {
102
+ form.setValue(props.field, (Array.isArray(val) ? val.map((v) => v.id) : []));
103
+ }
104
+ else {
105
+ form.setValue(props.field, (val?.id || null));
106
+ }
107
+ local.onChange?.(val);
108
+ };
109
+ return <RelationPicker {...others} onChange={handleChange}/>;
110
+ };
88
111
  Form.TextField = TextField;
89
112
  Form.NumberField = NumberField;
90
113
  Form.CheckboxField = CheckboxField;
@@ -94,5 +117,6 @@ export function createForm() {
94
117
  Form.FileField = FileField;
95
118
  Form.ImageField = ImageField;
96
119
  Form.SliderField = SliderField;
120
+ Form.RelationField = RelationField;
97
121
  return Form;
98
122
  }
@@ -1,9 +1,22 @@
1
1
  import { Show, createSignal } from "solid-js";
2
2
  import { tv } from "tailwind-variants";
3
3
  import Pencil from "lucide-solid/icons/pencil";
4
+ import ImageIcon from "lucide-solid/icons/image";
4
5
  import { Button } from "../Button";
5
6
  const image = tv({
6
- base: "rounded shadow object-cover",
7
+ base: "rounded-sm object-cover",
8
+ variants: {
9
+ size: {
10
+ xs: "w-16 h-16",
11
+ sm: "w-24 h-24",
12
+ md: "w-32 h-32",
13
+ lg: "w-48 h-48",
14
+ xl: "w-64 h-64",
15
+ },
16
+ },
17
+ });
18
+ const placeholder = tv({
19
+ base: "rounded-sm flex items-center justify-center bg-base-200",
7
20
  variants: {
8
21
  size: {
9
22
  xs: "w-16 h-16",
@@ -45,14 +58,19 @@ export const Image = (props) => {
45
58
  };
46
59
  // Destructure onChange and editable so they are not passed to <img>
47
60
  const { onChange, editable, ...imgProps } = props;
61
+ const currentSrc = () => preview() || props.src;
48
62
  return (<label class="flex flex-col gap-1 w-fit">
49
63
  <Show when={props.label}>
50
64
  <span class={label({ size: props.size })}>{props.label}</span>
51
65
  </Show>
52
66
  <div class="relative inline-block group w-fit">
53
- <img {...imgProps} src={preview() || props.src} alt={props.alt} class={image({ size: props.size, class: props.class })}/>
67
+ <Show when={currentSrc()} fallback={<div class={placeholder({ size: props.size, class: props.class })}>
68
+ <ImageIcon class="w-1/2 h-1/2 text-base-300"/>
69
+ </div>}>
70
+ <img {...imgProps} src={currentSrc()} alt={props.alt} class={image({ size: props.size, class: props.class })}/>
71
+ </Show>
54
72
  <Show when={editable}>
55
- <div class="absolute inset-0 flex items-center justify-center bg-black/10 opacity-0 group-hover:opacity-100 transition-opacity z-10">
73
+ <div class="absolute inset-0 flex items-center justify-center bg-black/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 rounded-sm">
56
74
  <Button size="sm" modifier="square" variant="ghost" onClick={handleEditClick}>
57
75
  <Pencil class="w-4 h-4"/>
58
76
  </Button>
@@ -1,6 +1,6 @@
1
1
  import { JSXElement } from "solid-js";
2
2
  export interface KanbanProps<T extends KanbanItem, K extends KanbanState> {
3
- columns: K[];
3
+ columns?: K[];
4
4
  items: T[];
5
5
  renderItem?: (item: T) => JSXElement;
6
6
  onCardClick?: (item: T) => void;
@@ -1,4 +1,4 @@
1
- import { createEffect, createMemo, createSignal, For, onCleanup } from "solid-js";
1
+ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
2
2
  import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
3
3
  import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4
4
  import { tv } from "tailwind-variants";
@@ -11,7 +11,7 @@ export const Kanban = (props) => {
11
11
  const posKey = props.statePositionKey;
12
12
  if (!posKey)
13
13
  return props.columns;
14
- return props.columns.toSorted((a, b) => (Number(a[posKey]) ?? 0) - (Number(b[posKey]) ?? 0));
14
+ return props.columns?.toSorted((a, b) => (Number(a[posKey]) ?? 0) - (Number(b[posKey]) ?? 0));
15
15
  });
16
16
  const colDragEnabled = () => !!props.statePositionKey;
17
17
  const [flashedColId, setFlashedColId] = createSignal(null);
@@ -58,9 +58,11 @@ export const Kanban = (props) => {
58
58
  onCleanup(dispose);
59
59
  });
60
60
  return (<div class={container({ class: props.containerClass })}>
61
- <For each={sortedColumns()}>
62
- {(col) => (<KanbanColumn col={col} dragEnabled={colDragEnabled} cardClass={props.cardClass} items={props.items} class={props.columnClass} onCardClick={props.onCardClick} renderItem={props.renderItem} onCreateItem={props.onCreateItem} onReorderCard={props.onReorderCard} onCollapse={props.onCollapseColumn} flashSignal={() => flashedColId()} itemPositionKey={props.itemPositionKey} itemStateKey={props.itemStateKey}/>)}
63
- </For>
61
+ <Show when={sortedColumns()?.length} fallback={<KanbanColumn dragEnabled={colDragEnabled} cardClass={props.cardClass} items={props.items} class={props.columnClass} onCardClick={props.onCardClick} renderItem={props.renderItem} onCreateItem={props.onCreateItem} onReorderCard={props.onReorderCard} onCollapse={props.onCollapseColumn} flashSignal={() => flashedColId()} itemPositionKey={props.itemPositionKey} itemStateKey={props.itemStateKey}/>}>
62
+ <For each={sortedColumns()}>
63
+ {(col) => (<KanbanColumn col={col} dragEnabled={colDragEnabled} cardClass={props.cardClass} items={props.items} class={props.columnClass} onCardClick={props.onCardClick} renderItem={props.renderItem} onCreateItem={props.onCreateItem} onReorderCard={props.onReorderCard} onCollapse={props.onCollapseColumn} flashSignal={() => flashedColId()} itemPositionKey={props.itemPositionKey} itemStateKey={props.itemStateKey}/>)}
64
+ </For>
65
+ </Show>
64
66
  </div>);
65
67
  };
66
68
  export default Kanban;
@@ -1,6 +1,6 @@
1
1
  import { Accessor, JSXElement } from "solid-js";
2
2
  export interface KanbanColumnProps<T extends KanbanItem, K extends KanbanState> {
3
- col: K;
3
+ col?: K;
4
4
  items: T[];
5
5
  dragEnabled: Accessor<boolean>;
6
6
  class?: string;
@@ -12,7 +12,7 @@ import { Button } from "../Button";
12
12
  import { KanbanCard } from "./KanbanCard";
13
13
  import { Input } from "../Input";
14
14
  const column = tv({
15
- base: "kanban-column flex flex-col gap-1 flex-shrink-0 bg-base-300 p-1.5 rounded-md transition-[width] text-nowrap",
15
+ base: "kanban-column flex flex-col gap-1 bg-base-300 p-1.5 rounded-md transition-[width] text-nowrap",
16
16
  variants: {
17
17
  folded: {
18
18
  true: "w-9",
@@ -35,9 +35,6 @@ const columnHeader = tv({
35
35
  folded: false,
36
36
  },
37
37
  });
38
- const columnContent = tv({
39
- base: "flex flex-col gap-1.5",
40
- });
41
38
  export const KanbanColumn = (props) => {
42
39
  let ref;
43
40
  const [dragging, setDragging] = createSignal("idle");
@@ -51,7 +48,7 @@ export const KanbanColumn = (props) => {
51
48
  const stateKey = props.itemStateKey;
52
49
  const posKey = props.itemPositionKey;
53
50
  if (stateKey) {
54
- items = items.filter((item) => item[stateKey] === props.col.id);
51
+ items = items.filter((item) => item[stateKey] === props.col?.id);
55
52
  }
56
53
  if (!posKey)
57
54
  return items;
@@ -60,7 +57,7 @@ export const KanbanColumn = (props) => {
60
57
  const itemDragEnabled = () => !!props.itemPositionKey;
61
58
  const [flashedCardId, setFlashedCardId] = createSignal(null);
62
59
  createEffect(() => {
63
- if (props.flashSignal?.() === props.col.id && ref) {
60
+ if (props.flashSignal?.() === props.col?.id && ref) {
64
61
  triggerFlash(ref);
65
62
  }
66
63
  });
@@ -77,7 +74,7 @@ export const KanbanColumn = (props) => {
77
74
  return false;
78
75
  }
79
76
  // only allowing same collection for now to be dropped on me
80
- return source.data.item.collectionId == props.col.collectionId;
77
+ return source.data.item.collectionId == props.col?.collectionId;
81
78
  },
82
79
  getData({ input }) {
83
80
  return attachClosestEdge({
@@ -201,7 +198,7 @@ export const KanbanColumn = (props) => {
201
198
  return {};
202
199
  });
203
200
  return (<div data-drop-edge={dragging() === "dragging-over" && !cardDraggedOver() ? (closestEdge() ?? undefined) : undefined} ref={ref} class={column({ class: props.class, folded: folded() })} style={{ opacity: dragging() == "dragging" ? 0.2 : 1, ...bgStyle() }}>
204
- <div>
201
+ <Show when={props.col}>
205
202
  <div class={columnHeader({ folded: folded() })}>
206
203
  <div class="flex items-center gap-2">
207
204
  <Button size="xs" variant="ghost" modifier="square" onClick={() => setFolded(!folded())}>
@@ -224,33 +221,31 @@ export const KanbanColumn = (props) => {
224
221
  <span class="text-xs font-normal text-base-content/50 mt-2">{filteredItems().length}</span>
225
222
  </div>)}
226
223
  </div>
227
- </div>
228
- <div class={columnContent()}>
229
- {!folded() && (<>
230
- {creatingItem() && (<div class="card bg-base-100 p-2 rounded-md space-y-1.5">
231
- <p class="font-medium text-sm">New Item</p>
232
- <Input value={newItemTitle()} onChange={setNewItemTitle} label="Title"/>
233
- <div class="flex justify-end space-x-1.5">
234
- <Button appearance="neutral" size="sm" onClick={() => {
224
+ </Show>
225
+ {!folded() && (<>
226
+ {creatingItem() && props.col && (<div class="card bg-base-100 p-2 rounded-md space-y-1.5">
227
+ <p class="font-medium text-sm">New Item</p>
228
+ <Input value={newItemTitle()} onChange={setNewItemTitle} label="Title"/>
229
+ <div class="flex justify-end space-x-1.5">
230
+ <Button appearance="neutral" size="sm" onClick={() => {
235
231
  setCreatingItem(false);
236
232
  setNewItemTitle("");
237
233
  }}>
238
- Cancel
239
- </Button>
240
- <Button appearance="success" size="sm" onClick={() => {
234
+ Cancel
235
+ </Button>
236
+ <Button appearance="success" size="sm" onClick={() => {
241
237
  setCreatingItem(false);
242
238
  props.onCreateItem?.(newItemTitle(), props.col.id);
243
239
  setNewItemTitle("");
244
240
  }}>
245
- Add
246
- </Button>
247
- </div>
248
- </div>)}
249
- <For each={filteredItems()}>
250
- {(item) => (<KanbanCard item={item} dragEnabled={itemDragEnabled} onCardClick={() => props.onCardClick?.(item)} class={props.cardClass} renderItem={props.renderItem} flashSignal={() => flashedCardId()}/>)}
251
- </For>
252
- </>)}
253
- </div>
241
+ Add
242
+ </Button>
243
+ </div>
244
+ </div>)}
245
+ <For each={filteredItems()}>
246
+ {(item) => (<KanbanCard item={item} dragEnabled={itemDragEnabled} onCardClick={() => props.onCardClick?.(item)} class={props.cardClass} renderItem={props.renderItem} flashSignal={() => flashedCardId()}/>)}
247
+ </For>
248
+ </>)}
254
249
  </div>);
255
250
  };
256
251
  export default KanbanColumn;
@@ -3,7 +3,9 @@ export interface NavbarProps {
3
3
  class?: string;
4
4
  }
5
5
  export interface NavbarComponents {
6
- Brand: ParentComponent;
6
+ Brand: ParentComponent<{
7
+ href?: string;
8
+ }>;
7
9
  Profile: ParentComponent;
8
10
  Menu: ParentComponent;
9
11
  MenuItem: ParentComponent;
@@ -12,7 +14,9 @@ export interface NavbarComponents {
12
14
  }>;
13
15
  }
14
16
  export declare const Navbar: ParentComponent<NavbarProps> & NavbarComponents;
15
- export declare const NavbarBrand: ParentComponent;
17
+ export declare const NavbarBrand: ParentComponent<{
18
+ href?: string;
19
+ }>;
16
20
  export declare const NavbarProfile: ParentComponent;
17
21
  export declare const NavbarMenu: ParentComponent;
18
22
  export declare const NavbarSubmenu: ParentComponent<{
@@ -7,7 +7,9 @@ export const Navbar = (props) => {
7
7
  return <nav class={navbar({ class: props.class })}>{props.children}</nav>;
8
8
  };
9
9
  export const NavbarBrand = (props) => {
10
- return <div class="btn btn-lg btn-ghost">{props.children}</div>;
10
+ return (<a class="btn btn-lg btn-ghost" href={props.href}>
11
+ {props.children}
12
+ </a>);
11
13
  };
12
14
  export const NavbarProfile = (props) => {
13
15
  return <div class="avatar">{props.children}</div>;
@@ -0,0 +1,23 @@
1
+ import { JSXElement } from "solid-js";
2
+ export interface RelationPickerProps<T> {
3
+ value: T | T[] | null;
4
+ options: T[];
5
+ onChange: (val: T | T[] | null) => void;
6
+ labelKey: keyof T;
7
+ valueKey: keyof T;
8
+ disabledKey?: keyof T;
9
+ multi?: boolean;
10
+ label?: string;
11
+ variant?: "ghost";
12
+ appearance?: "neutral" | "primary" | "secondary" | "accent" | "info" | "success" | "warning" | "error";
13
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
14
+ disabled?: boolean;
15
+ placeholder?: string;
16
+ class?: string;
17
+ listboxAction?: JSXElement;
18
+ onTextInputChange?: (text: string) => void;
19
+ defaultFilter?: (option: T[] | Exclude<NonNullable<T>, null>, filter: string) => boolean;
20
+ onLinkClick?: (value: T) => void;
21
+ }
22
+ export declare const RelationPicker: <T>(props: RelationPickerProps<T>) => import("solid-js").JSX.Element;
23
+ export default RelationPicker;
@@ -0,0 +1,144 @@
1
+ import { Combobox } from "@kobalte/core/combobox";
2
+ import Check from "lucide-solid/icons/check";
3
+ import Link from "lucide-solid/icons/link";
4
+ import UpDown from "lucide-solid/icons/chevrons-up-down";
5
+ import { For, Show } from "solid-js";
6
+ import { tv } from "tailwind-variants";
7
+ import { Tag } from "./Tag";
8
+ import { Button } from "./Button";
9
+ import { iconSize } from "../constants";
10
+ const input = tv({
11
+ base: "join-item input outline-offset-0",
12
+ variants: {
13
+ variant: {
14
+ ghost: "input-ghost",
15
+ none: "",
16
+ },
17
+ appearance: {
18
+ neutral: "input-neutral",
19
+ primary: "input-primary",
20
+ secondary: "input-secondary",
21
+ accent: "input-accent",
22
+ info: "input-info",
23
+ success: "input-success",
24
+ warning: "input-warning",
25
+ error: "input-error",
26
+ },
27
+ size: {
28
+ xs: "input-xs",
29
+ sm: "input-sm",
30
+ md: "input-md",
31
+ lg: "input-lg",
32
+ xl: "input-xl",
33
+ },
34
+ tags: {
35
+ true: "h-full py-1.25",
36
+ false: "",
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ size: "sm",
41
+ },
42
+ });
43
+ const menu = tv({
44
+ base: "menu w-full",
45
+ variants: {
46
+ size: {
47
+ xs: "menu-xs",
48
+ sm: "menu-sm",
49
+ md: "menu-base",
50
+ lg: "menu-lg",
51
+ xl: "menu-xl",
52
+ },
53
+ },
54
+ defaultVariants: {
55
+ size: "sm",
56
+ },
57
+ });
58
+ export const RelationPicker = (props) => {
59
+ let inputRef;
60
+ const values = () => {
61
+ if (props.multi) {
62
+ return Array.isArray(props.value) ? props.value : props.value ? [props.value] : [];
63
+ }
64
+ else {
65
+ return props.value && !Array.isArray(props.value) ? props.value : null;
66
+ }
67
+ };
68
+ const options = () => {
69
+ if (props.options.length === 0)
70
+ return [
71
+ { [props.labelKey]: "No Records Found", [props.valueKey]: "__no_options__", disabled: true },
72
+ ];
73
+ return props.options;
74
+ };
75
+ return (<div class="floating-label">
76
+ {props.label && <span>{props.label}</span>}
77
+ <Combobox disabled={props.disabled} multiple={props.multi} value={values()} onChange={props.onChange} options={options()}
78
+ //@ts-ignore, kobalte confusing, just ignore for now...
79
+ optionValue={props.valueKey}
80
+ //@ts-ignore
81
+ optionTextValue={props.labelKey}
82
+ //@ts-ignore
83
+ optionLabel={props.labelKey}
84
+ //@ts-ignore
85
+ optionDisabled={props.disabledKey} placeholder={props.placeholder} onMouseDown={(e) => {
86
+ e.preventDefault();
87
+ inputRef?.focus();
88
+ }} defaultFilter={props.defaultFilter} itemComponent={(itemProps) => (<Combobox.Item item={itemProps.item} class="outline-none focus:bg-base-300 rounded-sm">
89
+ <Combobox.ItemLabel class="flex flex-row justify-between items-center">
90
+ {itemProps.item.textValue}
91
+ <Combobox.ItemIndicator>
92
+ <Check size={16}/>
93
+ </Combobox.ItemIndicator>
94
+ </Combobox.ItemLabel>
95
+ </Combobox.Item>)}>
96
+ <Combobox.Control class="join w-full max-w-[20rem]">
97
+ {(state) => (<>
98
+ <div class={input({
99
+ variant: props.variant,
100
+ appearance: props.appearance,
101
+ size: props.size,
102
+ class: props.class,
103
+ tags: props.multi && Array.isArray(props.value) && props.value.length > 0,
104
+ })}>
105
+ <Show when={props.multi} fallback={<>
106
+ {!props.multi && values() && (<Button variant="ghost" appearance="primary" size="xs" modifier="square" onClick={() => props.onLinkClick?.(props.value)}>
107
+ <Link class="w-[1em] h-[1em]"/>
108
+ </Button>)}
109
+ <Combobox.Input onBlur={(e) => {
110
+ if (!props.value) {
111
+ e.currentTarget.value = "";
112
+ }
113
+ else {
114
+ e.currentTarget.value = String(state.selectedOptions()[0][props.labelKey]);
115
+ }
116
+ }} ref={inputRef} onInput={(e) => props.onTextInputChange?.(e.currentTarget.value)}/>
117
+ </>}>
118
+ <div class="flex flex-wrap gap-1 w-full">
119
+ <For each={state.selectedOptions()}>
120
+ {(option) => (<span onPointerDown={(e) => e.stopPropagation()}>
121
+ <Tag appearance="neutral" variant="soft" title={String(option[props.labelKey])} onDelete={() => state.remove(option)}/>
122
+ </span>)}
123
+ </For>
124
+ <Combobox.Input class="w-[unset]" onBlur={(e) => (e.currentTarget.value = "")} ref={inputRef} onInput={(e) => props.onTextInputChange?.(e.currentTarget.value)}/>
125
+ </div>
126
+ </Show>
127
+ </div>
128
+ <Combobox.Trigger as={Button} size={props.size} modifier="square" class="join-item">
129
+ <Combobox.Icon>
130
+ <UpDown size={iconSize[props.size ?? "sm"]}/>
131
+ </Combobox.Icon>
132
+ </Combobox.Trigger>
133
+ </>)}
134
+ </Combobox.Control>
135
+ <Combobox.Portal>
136
+ <Combobox.Content class="rounded-box bg-base-100 shadow-md border border-base-200 z-20 max-h-100 overflow-auto">
137
+ <Combobox.Listbox class={menu({ size: props.size })}/>
138
+ {props.listboxAction}
139
+ </Combobox.Content>
140
+ </Combobox.Portal>
141
+ </Combobox>
142
+ </div>);
143
+ };
144
+ export default RelationPicker;
@@ -1,9 +1,11 @@
1
1
  import { Select as KSelect } from "@kobalte/core/select";
2
2
  import Check from "lucide-solid/icons/check";
3
- import Down from "lucide-solid/icons/chevron-down";
3
+ import UpDown from "lucide-solid/icons/chevrons-up-down";
4
4
  import { tv } from "tailwind-variants";
5
+ import { Button } from "../Button";
6
+ import { iconSize } from "../../constants";
5
7
  const trigger = tv({
6
- base: "input outline-offset-0 flex justify-between items-center",
8
+ base: "input outline-offset-0 flex justify-between items-center join-item",
7
9
  variants: {
8
10
  variant: {
9
11
  ghost: "input-ghost",
@@ -59,21 +61,23 @@ export const Select = (props) => {
59
61
  </KSelect.ItemLabel>
60
62
  </KSelect.Item>);
61
63
  }}>
62
- <KSelect.Trigger class={trigger({
64
+ <KSelect.Trigger class="join w-full max-w-[20rem]">
65
+ <div class={trigger({
63
66
  variant: props.variant,
64
67
  appearance: props.appearance,
65
68
  size: props.size,
66
69
  class: props.class,
67
70
  })}>
68
- <KSelect.Value>
69
- {(state) => String(props.labelKey ? state.selectedOption()?.[props.labelKey] : "")}
70
- </KSelect.Value>
71
- <KSelect.Icon>
72
- <Down size={16}/>
71
+ <KSelect.Value>
72
+ {(state) => String(props.labelKey ? state.selectedOption()?.[props.labelKey] : "")}
73
+ </KSelect.Value>
74
+ </div>
75
+ <KSelect.Icon as={Button} class="join-item" modifier="square" size={props.size}>
76
+ <UpDown size={iconSize[props.size ?? "sm"]}/>
73
77
  </KSelect.Icon>
74
78
  </KSelect.Trigger>
75
79
  <KSelect.Portal>
76
- <KSelect.Content class="rounded-box bg-base-100 shadow-md border border-base-200 z-20">
80
+ <KSelect.Content class="rounded-box bg-base-100 shadow-md border border-base-200 z-20 max-h-100 overflow-auto">
77
81
  <KSelect.Listbox class={menu({ size: props.size })}/>
78
82
  </KSelect.Content>
79
83
  </KSelect.Portal>
@@ -1,4 +1,4 @@
1
- import { Accessor, JSXElement } from "solid-js";
1
+ import { JSXElement } from "solid-js";
2
2
  import { ColumnDef, Row } from "@tanstack/solid-table";
3
3
  interface TableItem {
4
4
  id: string;
@@ -9,8 +9,8 @@ interface TableProps<T extends TableItem> {
9
9
  data: T[];
10
10
  createFunc?: () => Promise<void>;
11
11
  headerActions?: JSXElement;
12
- columns: Accessor<ColumnDef<T>[]>;
13
- onRowClick: (item: T) => void;
12
+ columns: ColumnDef<T>[];
13
+ onRowClick?: (item: T) => void;
14
14
  loading?: boolean;
15
15
  emptyState?: JSXElement;
16
16
  loadingFallback?: JSXElement;
@@ -116,7 +116,7 @@ export const Table = (props) => {
116
116
  get data() {
117
117
  return props.data || [];
118
118
  },
119
- columns: props.columns(),
119
+ columns: props.columns,
120
120
  getCoreRowModel: getCoreRowModel(),
121
121
  });
122
122
  const rowCount = createMemo(() => table.getRowModel().rows.length);
@@ -164,7 +164,7 @@ export const Table = (props) => {
164
164
  <Loader class="w-9 h-9 animate-spin"/>
165
165
  </div>)}>
166
166
  <Show when={rowCount() > 0} fallback={props.emptyState || <div class="text-center py-4">No results found.</div>}>
167
- <table class={tableClass({ class: props.class })}>
167
+ <table class={tableClass({ class: props.class, size: props.size })}>
168
168
  <Show when={props.headers}>
169
169
  <thead>
170
170
  <For each={table.getHeaderGroups()}>
@@ -182,7 +182,7 @@ export const Table = (props) => {
182
182
 
183
183
  <tbody>
184
184
  <For each={rows()}>
185
- {(row, ind) => (<TableRow row={row} ind={ind()} onRowClick={() => props.onRowClick(row.original)} dragEnabled={dragEnabled} flashSignal={() => flashedRowId()}/>)}
185
+ {(row, ind) => (<TableRow row={row} ind={ind()} onRowClick={() => props.onRowClick?.(row.original)} dragEnabled={dragEnabled} flashSignal={() => flashedRowId()}/>)}
186
186
  </For>
187
187
  </tbody>
188
188
  </table>
@@ -1,2 +1,11 @@
1
- export declare function ThemeSwitch(): import("solid-js").JSX.Element;
1
+ import { Component, JSXElement } from "solid-js";
2
+ export interface ThemeOption {
3
+ value: string;
4
+ label: JSXElement;
5
+ }
6
+ interface ThemeSwitchProps {
7
+ triggerClass?: string;
8
+ options: ThemeOption[];
9
+ }
10
+ export declare const ThemeSwitch: Component<ThemeSwitchProps>;
2
11
  export default ThemeSwitch;
@@ -1,72 +1,47 @@
1
- import { createSignal, onMount } from "solid-js";
2
- import Sun from "lucide-solid/icons/sun";
3
- import Moon from "lucide-solid/icons/moon";
1
+ import { For } from "solid-js";
4
2
  import Monitor from "lucide-solid/icons/monitor";
3
+ import { tv } from "tailwind-variants";
5
4
  import { DropdownMenu } from "../DropdownMenu";
6
- import { Button } from "../Button";
7
- const THEME_KEY = "theme";
8
- function getSystemTheme() {
9
- if (window.matchMedia("(prefers-color-scheme: dark)").matches)
10
- return "dark";
11
- return "light";
12
- }
13
- function applyTheme(theme) {
14
- const html = document.documentElement;
15
- let applied = theme;
16
- if (theme === "system") {
17
- applied = getSystemTheme();
18
- }
19
- html.setAttribute("data-theme", applied);
20
- }
21
- const labelClass = "flex items-center gap-1";
22
- const getThemeValue = (theme) => {
23
- switch (theme) {
24
- case "light":
25
- return (<span class={labelClass}>
26
- <Sun class="w-[1em] h-[1em]"/> Light
27
- </span>);
28
- case "dark":
29
- return (<span class={labelClass}>
30
- <Moon class="w-[1em] h-[1em]"/> Dark
31
- </span>);
32
- case "system":
33
- return (<span class={labelClass}>
34
- <Monitor class="w-[1em] h-[1em]"/> System
35
- </span>);
36
- }
37
- };
38
- export function ThemeSwitch() {
39
- const [theme, setTheme] = createSignal("system");
40
- onMount(() => {
41
- const saved = localStorage.getItem(THEME_KEY);
42
- if (saved) {
43
- setTheme(saved);
44
- applyTheme(saved);
45
- }
46
- else {
47
- applyTheme("system");
48
- }
49
- });
5
+ import { THEME_KEY } from "../../constants";
6
+ const trigger = tv({
7
+ base: "min-w-30",
8
+ });
9
+ const SystemOption = () => (<span class="flex items-center gap-1">
10
+ <Monitor class="w-[1em] h-[1em]"/> System
11
+ </span>);
12
+ export const ThemeSwitch = (props) => {
13
+ const options = () => props.options;
14
+ const theme = localStorage.getItem("theme");
50
15
  const handleChange = (val) => {
51
- setTheme(val);
16
+ if (val === "system") {
17
+ localStorage.removeItem(THEME_KEY);
18
+ document.documentElement.removeAttribute("data-theme");
19
+ return;
20
+ }
52
21
  localStorage.setItem(THEME_KEY, val);
53
- applyTheme(val);
22
+ document.documentElement.setAttribute("data-theme", val);
23
+ };
24
+ const getCurrentLabel = () => {
25
+ if (theme === "system") {
26
+ return <SystemOption />;
27
+ }
28
+ const option = options().find((opt) => opt.value === theme);
29
+ return option?.label || theme;
54
30
  };
55
31
  return (<DropdownMenu>
56
- <DropdownMenu.Trigger>
57
- <Button>{getThemeValue(theme())}</Button>
32
+ <DropdownMenu.Trigger class={trigger({ class: props.triggerClass })}>
33
+ {getCurrentLabel()}
58
34
  </DropdownMenu.Trigger>
59
35
  <DropdownMenu.Content>
60
- <DropdownMenu.MenuItem onSelect={() => handleChange("light")}>
61
- {getThemeValue("light")}
62
- </DropdownMenu.MenuItem>
63
- <DropdownMenu.MenuItem onSelect={() => handleChange("dark")}>
64
- {getThemeValue("dark")}
65
- </DropdownMenu.MenuItem>
36
+ <For each={options()}>
37
+ {(option) => (<DropdownMenu.MenuItem onSelect={() => handleChange(option.value)}>
38
+ {option.label}
39
+ </DropdownMenu.MenuItem>)}
40
+ </For>
66
41
  <DropdownMenu.MenuItem onSelect={() => handleChange("system")}>
67
- {getThemeValue("system")}
42
+ <SystemOption />
68
43
  </DropdownMenu.MenuItem>
69
44
  </DropdownMenu.Content>
70
45
  </DropdownMenu>);
71
- }
46
+ };
72
47
  export default ThemeSwitch;
@@ -5,3 +5,4 @@ export declare const iconSize: {
5
5
  lg: number;
6
6
  xl: number;
7
7
  };
8
+ export declare const THEME_KEY = "theme";
package/dist/constants.js CHANGED
@@ -5,3 +5,4 @@ export const iconSize = {
5
5
  lg: 20,
6
6
  xl: 22,
7
7
  };
8
+ export const THEME_KEY = "theme";
package/dist/index.d.ts CHANGED
@@ -33,4 +33,5 @@ export * from "./components/TextArea";
33
33
  export * from "./components/ThemeSwitch";
34
34
  export * from "./components/Toast";
35
35
  export * from "./components/Tooltip";
36
+ export * from "./components/RelationPicker";
36
37
  export * from "./methods/debounce";
package/dist/index.js CHANGED
@@ -33,4 +33,5 @@ export * from "./components/TextArea";
33
33
  export * from "./components/ThemeSwitch";
34
34
  export * from "./components/Toast";
35
35
  export * from "./components/Tooltip";
36
+ export * from "./components/RelationPicker";
36
37
  export * from "./methods/debounce";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidpb/ui-kit",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1 +0,0 @@
1
- export * from "./Form";
@@ -1 +0,0 @@
1
- export * from "./Form";