@solidpb/ui-kit 0.3.0 → 0.4.1

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.
@@ -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>);
@@ -1,15 +1,16 @@
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;
@@ -33,5 +34,8 @@ export declare function createForm<T>(): {
33
34
  FileField: (props: FileInputProps & BaseFieldProps<T>) => import("solid-js").JSX.Element;
34
35
  ImageField: (props: ImageProps & BaseFieldProps<T>) => import("solid-js").JSX.Element;
35
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;
36
40
  };
37
41
  export {};
@@ -1,17 +1,18 @@
1
1
  import { splitProps } from "solid-js";
2
2
  import { createStore } from "solid-js/store";
3
- 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";
14
3
  import { tv } from "tailwind-variants";
4
+ import { InternalFormContext, useInternalFormContext } from "./formContext";
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";
15
16
  const formClass = tv({
16
17
  base: "space-y-4 space-x-4",
17
18
  });
@@ -24,7 +25,7 @@ export function createForm() {
24
25
  const getValue = (key) => {
25
26
  return values[key];
26
27
  };
27
- const contextValue = { setValue, getValue, values };
28
+ const contextValue = { setValue, getValue };
28
29
  const handleSubmit = (e) => {
29
30
  e.preventDefault();
30
31
  props.onSave?.(values);
@@ -36,10 +37,10 @@ export function createForm() {
36
37
  {props.children}
37
38
 
38
39
  <div class="flex justify-end gap-2">
39
- {props.onCancel && (<Button appearance="neutral" onClick={props.onCancel} size="sm">
40
+ {props.onCancel && (<Button appearance="neutral" onClick={props.onCancel}>
40
41
  Cancel
41
42
  </Button>)}
42
- {props.onSave && (<Button appearance="success" type="submit" size="sm">
43
+ {props.onSave && (<Button appearance="success" type="submit">
43
44
  Save
44
45
  </Button>)}
45
46
  </div>
@@ -80,15 +81,32 @@ export function createForm() {
80
81
  };
81
82
  const ImageField = (props) => {
82
83
  const form = useInternalFormContext();
83
- return (<Image {...props} editable src={form.getValue(props.field)} onChange={(file) => {
84
- const fileURL = URL.createObjectURL(file);
85
- form.setValue(props.field, fileURL);
84
+ const [local, others] = splitProps(props, ["onChange"]);
85
+ // have to set src manually, when using with pocketbase, the value will be a URL string,
86
+ // but when uploading a new file it will be a File object, so we need to handle both cases
87
+ return (<Image editable {...others} onChange={(file) => {
88
+ form.setValue(props.field, file);
89
+ local.onChange?.(file);
86
90
  }}/>);
87
91
  };
88
92
  const SliderField = (props) => {
89
93
  const form = useInternalFormContext();
90
94
  return (<Slider {...props} value={form.getValue(props.field)} onChange={(v) => form.setValue(props.field, v)}/>);
91
95
  };
96
+ const RelationField = (props) => {
97
+ const form = useInternalFormContext();
98
+ const [local, others] = splitProps(props, ["onChange"]);
99
+ const handleChange = (val) => {
100
+ if (props.multi) {
101
+ form.setValue(props.field, (Array.isArray(val) ? val.map((v) => v.id) : []));
102
+ }
103
+ else {
104
+ form.setValue(props.field, (val?.id || null));
105
+ }
106
+ local.onChange?.(val);
107
+ };
108
+ return <RelationPicker {...others} onChange={handleChange}/>;
109
+ };
92
110
  Form.TextField = TextField;
93
111
  Form.NumberField = NumberField;
94
112
  Form.CheckboxField = CheckboxField;
@@ -98,5 +116,6 @@ export function createForm() {
98
116
  Form.FileField = FileField;
99
117
  Form.ImageField = ImageField;
100
118
  Form.SliderField = SliderField;
119
+ Form.RelationField = RelationField;
101
120
  return Form;
102
121
  }
@@ -4,7 +4,7 @@ import Pencil from "lucide-solid/icons/pencil";
4
4
  import ImageIcon from "lucide-solid/icons/image";
5
5
  import { Button } from "../Button";
6
6
  const image = tv({
7
- base: "rounded-box shadow object-cover",
7
+ base: "rounded-sm object-cover",
8
8
  variants: {
9
9
  size: {
10
10
  xs: "w-16 h-16",
@@ -16,7 +16,7 @@ const image = tv({
16
16
  },
17
17
  });
18
18
  const placeholder = tv({
19
- base: "rounded-box shadow flex items-center justify-center bg-base-200",
19
+ base: "rounded-sm flex items-center justify-center bg-base-200",
20
20
  variants: {
21
21
  size: {
22
22
  xs: "w-16 h-16",
@@ -70,7 +70,7 @@ export const Image = (props) => {
70
70
  <img {...imgProps} src={currentSrc()} alt={props.alt} class={image({ size: props.size, class: props.class })}/>
71
71
  </Show>
72
72
  <Show when={editable}>
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-box">
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">
74
74
  <Button size="sm" modifier="square" variant="ghost" onClick={handleEditClick}>
75
75
  <Pencil class="w-4 h-4"/>
76
76
  </Button>
@@ -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>
@@ -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()}>
@@ -1,4 +1,4 @@
1
- import { createSignal, For } from "solid-js";
1
+ import { For } from "solid-js";
2
2
  import Monitor from "lucide-solid/icons/monitor";
3
3
  import { tv } from "tailwind-variants";
4
4
  import { DropdownMenu } from "../DropdownMenu";
@@ -11,9 +11,8 @@ const SystemOption = () => (<span class="flex items-center gap-1">
11
11
  </span>);
12
12
  export const ThemeSwitch = (props) => {
13
13
  const options = () => props.options;
14
- const [theme, setTheme] = createSignal("system");
14
+ const theme = localStorage.getItem("theme");
15
15
  const handleChange = (val) => {
16
- setTheme(val);
17
16
  if (val === "system") {
18
17
  localStorage.removeItem(THEME_KEY);
19
18
  document.documentElement.removeAttribute("data-theme");
@@ -23,12 +22,11 @@ export const ThemeSwitch = (props) => {
23
22
  document.documentElement.setAttribute("data-theme", val);
24
23
  };
25
24
  const getCurrentLabel = () => {
26
- const current = theme();
27
- if (current === "system") {
25
+ if (theme === "system") {
28
26
  return <SystemOption />;
29
27
  }
30
- const option = options().find((opt) => opt.value === current);
31
- return option?.label || current;
28
+ const option = options().find((opt) => opt.value === theme);
29
+ return option?.label || theme;
32
30
  };
33
31
  return (<DropdownMenu>
34
32
  <DropdownMenu.Trigger class={trigger({ class: props.triggerClass })}>
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.3.0",
3
+ "version": "0.4.1",
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";