@solidpb/ui-kit 0.1.1 → 0.2.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.
Files changed (136) hide show
  1. package/README.md +17 -0
  2. package/dist/components/ActivityFeed/ActivityFeed.d.ts +16 -0
  3. package/dist/components/ActivityFeed/ActivityFeed.jsx +54 -0
  4. package/dist/components/ActivityFeed/index.d.ts +1 -0
  5. package/dist/components/ActivityFeed/index.js +1 -0
  6. package/dist/components/AlertDialog/AlertDialog.d.ts +27 -0
  7. package/dist/components/AlertDialog/AlertDialog.jsx +62 -0
  8. package/dist/components/AlertDialog/alertDialogContext.d.ts +9 -0
  9. package/dist/components/AlertDialog/alertDialogContext.js +2 -0
  10. package/dist/components/AlertDialog/index.d.ts +1 -0
  11. package/dist/components/AlertDialog/index.js +1 -0
  12. package/dist/components/Avatar/Avatar.d.ts +10 -0
  13. package/dist/components/Avatar/Avatar.jsx +30 -0
  14. package/dist/components/Avatar/index.d.ts +1 -0
  15. package/dist/components/Avatar/index.js +1 -0
  16. package/dist/components/BreadCrumbs/BreadCrumbs.d.ts +12 -0
  17. package/dist/components/BreadCrumbs/BreadCrumbs.jsx +26 -0
  18. package/dist/components/BreadCrumbs/index.d.ts +1 -0
  19. package/dist/components/BreadCrumbs/index.js +1 -0
  20. package/dist/components/Button/Button.d.ts +3 -3
  21. package/dist/components/Button/Button.jsx +1 -6
  22. package/dist/components/Card/Card.d.ts +1 -0
  23. package/dist/components/Card/Card.jsx +5 -2
  24. package/dist/components/Checkbox/Checkbox.d.ts +6 -7
  25. package/dist/components/Checkbox/Checkbox.jsx +39 -22
  26. package/dist/components/ColorPicker/ColorPicker.d.ts +10 -0
  27. package/dist/components/ColorPicker/ColorPicker.jsx +61 -0
  28. package/dist/components/ColorPicker/index.d.ts +1 -0
  29. package/dist/components/ColorPicker/index.js +1 -0
  30. package/dist/components/Container/Container.jsx +1 -1
  31. package/dist/components/DateInput/DateInput.d.ts +5 -0
  32. package/dist/components/DateInput/DateInput.jsx +29 -8
  33. package/dist/components/Drawer/Drawer.d.ts +33 -0
  34. package/dist/components/Drawer/Drawer.jsx +49 -0
  35. package/dist/components/Drawer/drawerContext.d.ts +6 -0
  36. package/dist/components/Drawer/drawerContext.js +9 -0
  37. package/dist/components/Drawer/index.d.ts +1 -0
  38. package/dist/components/Drawer/index.js +1 -0
  39. package/dist/components/DropdownMenu/DropdownMenu.d.ts +23 -7
  40. package/dist/components/DropdownMenu/DropdownMenu.jsx +41 -15
  41. package/dist/components/FileInput/FileInput.d.ts +7 -3
  42. package/dist/components/FileInput/FileInput.jsx +35 -9
  43. package/dist/components/FilterBar/AddFilter.d.ts +9 -0
  44. package/dist/components/FilterBar/AddFilter.jsx +14 -0
  45. package/dist/components/FilterBar/AddSortingDropdown.d.ts +9 -0
  46. package/dist/components/FilterBar/AddSortingDropdown.jsx +67 -0
  47. package/dist/components/FilterBar/EditFilters.d.ts +10 -0
  48. package/dist/components/FilterBar/EditFilters.jsx +12 -0
  49. package/dist/components/FilterBar/FilterBar.d.ts +56 -0
  50. package/dist/components/FilterBar/FilterBar.jsx +234 -0
  51. package/dist/components/FilterBar/FilterChip.d.ts +23 -0
  52. package/dist/components/FilterBar/FilterChip.jsx +220 -0
  53. package/dist/components/FilterBar/FilterEdit.d.ts +13 -0
  54. package/dist/components/FilterBar/FilterEdit.jsx +159 -0
  55. package/dist/components/FilterBar/FiltersEdit.d.ts +13 -0
  56. package/dist/components/FilterBar/FiltersEdit.jsx +45 -0
  57. package/dist/components/FilterBar/index.d.ts +1 -0
  58. package/dist/components/FilterBar/index.js +1 -0
  59. package/dist/components/Form/Form.d.ts +36 -2
  60. package/dist/components/Form/Form.jsx +98 -3
  61. package/dist/components/Form/formContext.d.ts +2 -0
  62. package/dist/components/Form/formContext.js +9 -0
  63. package/dist/components/Image/Image.d.ts +6 -7
  64. package/dist/components/Image/Image.jsx +56 -7
  65. package/dist/components/Input/Input.d.ts +4 -4
  66. package/dist/components/Input/Input.jsx +9 -35
  67. package/dist/components/Kanban/Kanban.d.ts +19 -0
  68. package/dist/components/Kanban/Kanban.jsx +66 -0
  69. package/dist/components/Kanban/KanbanCard.d.ts +11 -0
  70. package/dist/components/Kanban/KanbanCard.jsx +94 -0
  71. package/dist/components/Kanban/KanbanColumn.d.ts +18 -0
  72. package/dist/components/Kanban/KanbanColumn.jsx +256 -0
  73. package/dist/components/Kanban/index.d.ts +1 -0
  74. package/dist/components/Kanban/index.js +1 -0
  75. package/dist/components/Link/Link.d.ts +8 -6
  76. package/dist/components/Link/Link.jsx +17 -5
  77. package/dist/components/Modal/Modal.d.ts +17 -8
  78. package/dist/components/Modal/Modal.jsx +32 -39
  79. package/dist/components/Modal/modalContext.d.ts +2 -3
  80. package/dist/components/Navbar/Navbar.d.ts +22 -0
  81. package/dist/components/Navbar/Navbar.jsx +38 -0
  82. package/dist/components/Navbar/index.d.ts +1 -0
  83. package/dist/components/Navbar/index.js +1 -0
  84. package/dist/components/NumberInput/NumberInput.d.ts +8 -6
  85. package/dist/components/NumberInput/NumberInput.jsx +52 -7
  86. package/dist/components/Pagination/Pagination.d.ts +15 -0
  87. package/dist/components/Pagination/Pagination.jsx +67 -0
  88. package/dist/components/Pagination/index.d.ts +1 -0
  89. package/dist/components/Pagination/index.js +1 -0
  90. package/dist/components/SearchInput/SearchInput.d.ts +3 -0
  91. package/dist/components/SearchInput/SearchInput.jsx +38 -3
  92. package/dist/components/Select/Select.d.ts +13 -11
  93. package/dist/components/Select/Select.jsx +78 -32
  94. package/dist/components/Slider/Slider.d.ts +16 -0
  95. package/dist/components/Slider/Slider.jsx +60 -0
  96. package/dist/components/Slider/index.d.ts +1 -0
  97. package/dist/components/Slider/index.js +1 -0
  98. package/dist/components/Switch/Switch.d.ts +6 -8
  99. package/dist/components/Switch/Switch.jsx +38 -19
  100. package/dist/components/{List/List.d.ts → Table/Table.d.ts} +13 -12
  101. package/dist/components/Table/Table.jsx +193 -0
  102. package/dist/components/Table/index.d.ts +1 -0
  103. package/dist/components/Table/index.js +1 -0
  104. package/dist/components/Tabs/Tabs.d.ts +25 -1
  105. package/dist/components/Tabs/Tabs.jsx +42 -1
  106. package/dist/components/Tabs/tabContext.d.ts +9 -0
  107. package/dist/components/Tabs/tabContext.js +2 -0
  108. package/dist/components/Tag/Tag.d.ts +7 -5
  109. package/dist/components/Tag/Tag.jsx +40 -14
  110. package/dist/components/TagArea/TagArea.d.ts +6 -2
  111. package/dist/components/TagArea/TagArea.jsx +89 -35
  112. package/dist/components/TextArea/TextArea.d.ts +9 -8
  113. package/dist/components/TextArea/TextArea.jsx +54 -18
  114. package/dist/components/ThemeSwitch/ThemeSwitch.d.ts +2 -0
  115. package/dist/components/ThemeSwitch/ThemeSwitch.jsx +72 -0
  116. package/dist/components/ThemeSwitch/index.d.ts +1 -0
  117. package/dist/components/ThemeSwitch/index.js +1 -0
  118. package/dist/components/Toast/Toast.d.ts +1 -1
  119. package/dist/components/Toast/Toast.jsx +13 -10
  120. package/dist/components/Toast/Toaster.jsx +1 -2
  121. package/dist/components/Tooltip/Tooltip.d.ts +4 -3
  122. package/dist/components/Tooltip/Tooltip.jsx +19 -8
  123. package/dist/constants.d.ts +7 -0
  124. package/dist/constants.js +7 -0
  125. package/dist/index.d.ts +14 -2
  126. package/dist/index.js +14 -2
  127. package/dist/methods/triggerFlash.d.ts +1 -0
  128. package/dist/methods/triggerFlash.js +7 -0
  129. package/package.json +3 -2
  130. package/dist/components/List/List.jsx +0 -120
  131. package/dist/components/List/index.d.ts +0 -1
  132. package/dist/components/List/index.js +0 -1
  133. package/dist/components/RelationPicker/RelationPicker.d.ts +0 -11
  134. package/dist/components/RelationPicker/RelationPicker.jsx +0 -13
  135. package/dist/components/RelationPicker/index.d.ts +0 -1
  136. package/dist/components/RelationPicker/index.js +0 -1
@@ -0,0 +1,234 @@
1
+ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
2
+ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3
+ import { TextField } from "@kobalte/core/text-field";
4
+ import { tv } from "tailwind-variants";
5
+ import Search from "lucide-solid/icons/search";
6
+ import ListFilter from "lucide-solid/icons/list-filter";
7
+ import ArrowDown from "lucide-solid/icons/arrow-down-narrow-wide";
8
+ import invariant from "tiny-invariant";
9
+ import { FilterChip, FilterGroupChip } from "./FilterChip";
10
+ import AddFilter from "./AddFilter";
11
+ import AddSortingDropdown from "./AddSortingDropdown";
12
+ import EditFilters from "./EditFilters";
13
+ import { DropdownMenu } from "../DropdownMenu";
14
+ import { Modal } from "../Modal";
15
+ import { Button } from "../Button";
16
+ export const filterDefaults = {
17
+ text: "loose_contains",
18
+ bool: "is",
19
+ number: "is",
20
+ select: "is",
21
+ date: "greater_than",
22
+ };
23
+ export const filterLabels = {
24
+ text: {
25
+ loose_contains: "Contains",
26
+ in: "Contains (strict)",
27
+ not_in: "Doesn't contain",
28
+ fuzzy_match: "Fuzzy contains",
29
+ is: "Is",
30
+ is_not: "Is not",
31
+ is_set: "Is set",
32
+ is_not_set: "Is not set",
33
+ },
34
+ bool: {
35
+ is: "Is",
36
+ },
37
+ number: {
38
+ is: "Is",
39
+ greater_than: "Is greater than",
40
+ less_than: "Is less than",
41
+ between: "Is between",
42
+ is_not: "Is not",
43
+ },
44
+ select: {
45
+ is: "Is",
46
+ is_not: "Is not",
47
+ in: "Contains",
48
+ not_in: "Doesn't contain",
49
+ is_set: "Is set",
50
+ is_not_set: "Is not set",
51
+ },
52
+ date: {
53
+ greater_than: "Is after",
54
+ less_than: "Is before",
55
+ between: "Is between",
56
+ is: "Is",
57
+ is_not: "Is not",
58
+ is_set: "Is set",
59
+ is_not_set: "Is not set",
60
+ },
61
+ };
62
+ const filterBarBase = tv({
63
+ base: "relative flex flex-col not-sm:w-screen",
64
+ });
65
+ const filterBar = tv({
66
+ base: "input outline-offset-0 join-item flex-1",
67
+ variants: {
68
+ size: {
69
+ xs: "input-xs",
70
+ sm: "input-sm",
71
+ md: "input-md",
72
+ lg: "input-lg",
73
+ xl: "input-xl",
74
+ },
75
+ },
76
+ defaultVariants: {
77
+ size: "sm",
78
+ },
79
+ });
80
+ const suggestions = tv({
81
+ base: "menu w-full",
82
+ variants: {
83
+ size: {
84
+ xs: "menu-xs",
85
+ sm: "menu-sm",
86
+ md: "menu-md",
87
+ lg: "menu-lg",
88
+ xl: "menu-xl",
89
+ },
90
+ },
91
+ defaultVariants: {
92
+ size: "sm",
93
+ },
94
+ });
95
+ export const FilterBar = (props) => {
96
+ let ref;
97
+ const [dragging, setDragging] = createSignal("idle");
98
+ const [filterDropdownOpen, setFilterDropdownOpen] = createSignal(false);
99
+ const [showFieldDropdown, setShowFieldDropdown] = createSignal(false);
100
+ const textFields = createMemo(() => props.availableFields?.filter((f) => f.type === "text"));
101
+ const handleTextValueChange = (val) => {
102
+ setFocusedIndex(-1); // reset on type
103
+ props.onChangeValue?.(val);
104
+ setShowFieldDropdown(!!val);
105
+ };
106
+ const createTextFilter = (field) => {
107
+ const newFilter = {
108
+ field,
109
+ operator: "loose_contains",
110
+ value: props.value,
111
+ };
112
+ props.onAddFilterGroup([newFilter]);
113
+ handleTextValueChange("");
114
+ };
115
+ const isFilterGroup = (item) => {
116
+ return "filters" in item;
117
+ };
118
+ createEffect(() => {
119
+ const element = ref;
120
+ invariant(element);
121
+ const dispose = dropTargetForElements({
122
+ element,
123
+ canDrop({ source }) {
124
+ if (source.element === element) {
125
+ return false;
126
+ }
127
+ return !!source.data.isFilterChip || !!source.data.isFilterGroupChip;
128
+ },
129
+ onDragEnter() {
130
+ setDragging("dragging-over");
131
+ },
132
+ onDrag() {
133
+ if (dragging() !== "dragging-over") {
134
+ setDragging("dragging-over");
135
+ }
136
+ },
137
+ onDragLeave() {
138
+ setDragging("idle");
139
+ },
140
+ onDrop({ source, location }) {
141
+ setDragging("idle");
142
+ if (location.current.dropTargets[0].element !== ref)
143
+ return;
144
+ props.onGroupDrag(source.data.index, -1, source.data.groupIndex);
145
+ },
146
+ });
147
+ onCleanup(dispose);
148
+ });
149
+ const [focusedIndex, setFocusedIndex] = createSignal(-1);
150
+ const handleKeyDown = (e) => {
151
+ const fields = textFields() ?? [];
152
+ if (!showFieldDropdown() || fields.length === 0)
153
+ return;
154
+ if (e.key === "ArrowDown") {
155
+ e.preventDefault();
156
+ setFocusedIndex((i) => Math.min(i + 1, fields.length - 1));
157
+ }
158
+ else if (e.key === "ArrowUp") {
159
+ e.preventDefault();
160
+ setFocusedIndex((i) => Math.max(i - 1, 0));
161
+ }
162
+ else if (e.key === "Enter" && focusedIndex() >= 0) {
163
+ e.preventDefault();
164
+ createTextFilter(fields[focusedIndex()]);
165
+ setFocusedIndex(-1);
166
+ }
167
+ else if (e.key === "Escape") {
168
+ e.preventDefault();
169
+ setShowFieldDropdown(false);
170
+ setFocusedIndex(-1);
171
+ }
172
+ };
173
+ return (<div class={filterBarBase({ class: props.class })}>
174
+ <div class="flex w-full gap-1">
175
+ <Show when={props.leftAction}>{props.leftAction}</Show>
176
+ <div class="join flex flex-1 relative">
177
+ <TextField value={props.value} onChange={handleTextValueChange} class={filterBar({ size: props.size })} onKeyDown={handleKeyDown}>
178
+ <Search class="w-[1em] h-[1em] opacity-90"/>
179
+ <TextField.Input placeholder={props.placeholder || "Search"} class="grow focus:outline-none" onFocusIn={() => setShowFieldDropdown(!!props.value)} onFocusOut={() => setShowFieldDropdown(false)}/>
180
+ </TextField>
181
+ <Modal title="Add filters" open={filterDropdownOpen()} onOpenChange={setFilterDropdownOpen}>
182
+ <Modal.Trigger as={Button} modifier="square" class="join-item">
183
+ <ListFilter class="w-[1em] h-[1em]"/>
184
+ </Modal.Trigger>
185
+ <Modal.Modal class="bg-base-200">
186
+ <AddFilter size={props.size} availableFields={props.availableFields ?? []} onAddFilters={props.onAddFilterGroup} setOpen={setFilterDropdownOpen}/>
187
+ </Modal.Modal>
188
+ </Modal>
189
+ <Show when={props.setSortBy}>
190
+ <DropdownMenu>
191
+ <div class="indicator">
192
+ {props.sortBy && <span class="indicator-item status status-neutral"></span>}
193
+ <DropdownMenu.Trigger size={props.size} modifier="square" class="join-item">
194
+ <ArrowDown class="w-[1em] h-[1em]"/>
195
+ </DropdownMenu.Trigger>
196
+ </div>
197
+ <AddSortingDropdown size={props.size} availableFields={props.availableFields ?? []} sortBy={props.sortBy} setSortBy={props.setSortBy}/>
198
+ </DropdownMenu>
199
+ </Show>
200
+ {showFieldDropdown() && (<div class="absolute top-full left-0 mt-1 w-full dropdown-content rounded-box bg-base-100 shadow-md z-20">
201
+ <ul class={suggestions({ size: props.size })}>
202
+ <For each={textFields()}>
203
+ {(item, index) => (<li class="menu-item" classList={{ "bg-base-300 rounded-sm": focusedIndex() === index() }} onMouseDown={(e) => {
204
+ e.preventDefault(); // prevent input blur
205
+ createTextFilter(item);
206
+ }} onMouseEnter={() => setFocusedIndex(index())}>
207
+ <p class="flex gap-1">
208
+ <span class="font-bold">{item.label}</span>
209
+ <span class="italic">Contains</span>
210
+ <span>'{props.value}'</span>
211
+ </p>
212
+ </li>)}
213
+ </For>
214
+ </ul>
215
+ </div>)}
216
+ </div>
217
+ </div>
218
+ <div ref={ref} class="w-full flex flex-wrap gap-0.5 mt-1">
219
+ <For each={props.items}>
220
+ {(item, i) => {
221
+ const [itemOpen, setItemOpen] = createSignal(false);
222
+ return (<Modal title="Edit filters" open={itemOpen()} onOpenChange={setItemOpen}>
223
+ <Show when={isFilterGroup(item)} fallback={<Modal.Trigger as={FilterChip} onDelete={() => props.onFilterRemove(i(), item)} filter={item} size={props.size} onGroupDrag={(sourceInd, sourceFilterGroupInd) => props.onGroupDrag(sourceInd, i(), sourceFilterGroupInd)} index={i()} setOpen={setItemOpen}/>}>
224
+ <Modal.Trigger as={FilterGroupChip} filterGroup={item} size={props.size} onGroupDrag={(sourceInd, sourceFilterGroupInd) => props.onGroupDrag(sourceInd, i(), sourceFilterGroupInd)} index={i()} setOpen={setItemOpen}/>
225
+ </Show>
226
+ <Modal.Modal class="bg-base-200">
227
+ <EditFilters size={props.size} availableFields={props.availableFields ?? []} onSaveFilters={(filters) => props.onUpdateFilterGroup(i(), filters)} currentFilters={isFilterGroup(item) ? item.filters : [item]} setOpen={setItemOpen}/>
228
+ </Modal.Modal>
229
+ </Modal>);
230
+ }}
231
+ </For>
232
+ </div>
233
+ </div>);
234
+ };
@@ -0,0 +1,23 @@
1
+ import { Setter } from "solid-js";
2
+ import { Filter, FilterGroup } from "./FilterBar";
3
+ interface FilterChipOrGroupProps {
4
+ onGroupDrag: (sourceInd: number, sourceFilterGroupInd?: number) => void;
5
+ onDelete?: () => void;
6
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
7
+ class?: string;
8
+ index: number;
9
+ setOpen?: Setter<boolean>;
10
+ }
11
+ interface FilterChipProps<T> extends FilterChipOrGroupProps {
12
+ onClick?: (filter: Filter<T>) => void;
13
+ filter: Filter<T>;
14
+ isInGroup?: boolean;
15
+ groupIndex?: number;
16
+ }
17
+ export declare const FilterChip: <T>(props: FilterChipProps<T>) => import("solid-js").JSX.Element;
18
+ interface FilterGroupProps<T> extends FilterChipOrGroupProps {
19
+ filterGroup: FilterGroup<T>;
20
+ onClick?: (filterGroup: FilterGroup<T>) => void;
21
+ }
22
+ export declare const FilterGroupChip: <T>(props: FilterGroupProps<T>) => import("solid-js").JSX.Element;
23
+ export default FilterChip;
@@ -0,0 +1,220 @@
1
+ import { createEffect, createMemo, createSignal, For, onCleanup } from "solid-js";
2
+ import { dropTargetForElements, draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3
+ import invariant from "tiny-invariant";
4
+ import CloseIcon from "lucide-solid/icons/x";
5
+ import { tv } from "tailwind-variants";
6
+ import { filterLabels } from "./FilterBar";
7
+ import { Button } from "../Button";
8
+ const filterChip = tv({
9
+ base: "badge badge-soft cursor-pointer",
10
+ variants: {
11
+ size: {
12
+ xs: "badge-xs",
13
+ sm: "badge-sm",
14
+ md: "badge-md",
15
+ lg: "badge-lg",
16
+ xl: "badge-xl",
17
+ },
18
+ },
19
+ });
20
+ export const FilterChip = (props) => {
21
+ let ref;
22
+ const [dragging, setDragging] = createSignal("idle");
23
+ const filterOperator = createMemo(() => {
24
+ return filterLabels[props.filter.field.type][props.filter.operator] || "has";
25
+ });
26
+ const valueLabel = createMemo(() => {
27
+ let val = props.filter.value;
28
+ if (val === null)
29
+ return "";
30
+ switch (typeof val) {
31
+ case "boolean":
32
+ return val ? "True" : "False";
33
+ case "number":
34
+ return String(val);
35
+ case "string":
36
+ return val;
37
+ case "object":
38
+ if ("label" in val) {
39
+ return val.label;
40
+ }
41
+ else {
42
+ let valText = val.startDate ? String(val.startDate.toLocaleDateString("en-GB")) : "";
43
+ if (val.endDate)
44
+ valText += ` - ${val.endDate.toLocaleDateString("en-GB")}`;
45
+ return valText;
46
+ }
47
+ }
48
+ });
49
+ createEffect(() => {
50
+ const element = ref;
51
+ invariant(element);
52
+ const dispose = dropTargetForElements({
53
+ element,
54
+ canDrop({ source }) {
55
+ // not allowing dropping on yourself
56
+ if (source.element === element) {
57
+ return false;
58
+ }
59
+ return !!source.data.isFilterChip || !!source.data.isFilterGroupChip;
60
+ },
61
+ onDragEnter() {
62
+ setDragging("dragging-over");
63
+ },
64
+ onDrag() {
65
+ if (dragging() !== "dragging-over") {
66
+ setDragging("dragging-over");
67
+ }
68
+ },
69
+ onDragLeave() {
70
+ setDragging("idle");
71
+ },
72
+ onDrop({ source }) {
73
+ setDragging("idle");
74
+ if (source.data.index === props.index)
75
+ return;
76
+ // add the target ind and group source ind if it exists
77
+ props.onGroupDrag(source.data.index, source.data.groupIndex);
78
+ },
79
+ });
80
+ onCleanup(dispose);
81
+ });
82
+ createEffect(() => {
83
+ const element = ref;
84
+ invariant(element);
85
+ const dispose = draggable({
86
+ element,
87
+ getInitialData() {
88
+ return {
89
+ isFilterChip: true,
90
+ index: props.index,
91
+ isInGroup: props.isInGroup || false,
92
+ groupIndex: props.groupIndex,
93
+ };
94
+ },
95
+ onDragStart() {
96
+ setDragging("dragging");
97
+ },
98
+ onDrop() {
99
+ setDragging("idle");
100
+ },
101
+ });
102
+ onCleanup(() => dispose());
103
+ });
104
+ const bgStyle = createMemo(() => {
105
+ if (dragging() === "dragging-over") {
106
+ return { "background-color": "color-mix(in srgb, var(--color-primary) 10%, transparent)" };
107
+ }
108
+ return {};
109
+ });
110
+ return (<div class={filterChip({ size: props.size, class: props.class })} ref={ref} style={{ opacity: dragging() === "dragging" ? 0.2 : 1, ...bgStyle() }} onClick={() => props.setOpen?.((prev) => !prev)}>
111
+ <div class="not-sm:tooltip not-sm:tooltip-bottom not-sm:tooltip-neutral">
112
+ <span class="font-bold">{String(props.filter.field.label)}</span>
113
+ <span class="mx-1 hidden sm:inline">{filterOperator()}</span>
114
+ {valueLabel() && <span class="hidden sm:inline">{valueLabel()}</span>}
115
+ <div class="inline sm:hidden tooltip-content">
116
+ <span class="font-bold">{String(props.filter.field.label)}</span>
117
+ <span class="mx-1 inline sm:hidden">{filterOperator()}</span>
118
+ {valueLabel() && <span class="inline sm:hidden">{valueLabel()}</span>}
119
+ </div>
120
+ </div>
121
+ {props.onDelete && (<Button onClick={props.onDelete} size="xs" variant="ghost" class="p-0.5 m-0 h-min">
122
+ <CloseIcon class="w-[1em] h-[1em]" stroke-width={4}/>
123
+ </Button>)}
124
+ </div>);
125
+ };
126
+ const filterGroupChip = tv({
127
+ base: "badge badge-soft cursor-pointer pl-0 pr-1 gap-0.5",
128
+ variants: {
129
+ size: {
130
+ xs: "badge-xs",
131
+ sm: "badge-sm",
132
+ md: "badge-md",
133
+ lg: "badge-lg",
134
+ xl: "badge-xl",
135
+ },
136
+ },
137
+ });
138
+ export const FilterGroupChip = (props) => {
139
+ let ref;
140
+ const [dragging, setDragging] = createSignal("idle");
141
+ const extraCount = () => props.filterGroup.filters.length - 1;
142
+ createEffect(() => {
143
+ const element = ref;
144
+ invariant(element);
145
+ const dispose = dropTargetForElements({
146
+ element,
147
+ canDrop({ source }) {
148
+ // not allowing dropping on yourself
149
+ if (source.element === element) {
150
+ return false;
151
+ }
152
+ return !!source.data.isFilterChip || !!source.data.isFilterGroupChip;
153
+ },
154
+ onDragEnter() {
155
+ setDragging("dragging-over");
156
+ },
157
+ onDrag() {
158
+ if (dragging() !== "dragging-over") {
159
+ setDragging("dragging-over");
160
+ }
161
+ },
162
+ onDragLeave() {
163
+ setDragging("idle");
164
+ },
165
+ onDrop({ source, location }) {
166
+ setDragging("idle");
167
+ if (location.current.dropTargets[0].element !== ref)
168
+ return;
169
+ // add the target ind and group source ind if it exists
170
+ props.onGroupDrag(source.data.index, source.data.groupIndex);
171
+ },
172
+ });
173
+ onCleanup(dispose);
174
+ });
175
+ createEffect(() => {
176
+ const element = ref;
177
+ invariant(element);
178
+ const dispose = draggable({
179
+ element,
180
+ getInitialData() {
181
+ return {
182
+ isFilterGroupChip: true,
183
+ isInGroup: false,
184
+ index: props.index,
185
+ };
186
+ },
187
+ onDragStart() {
188
+ setDragging("dragging");
189
+ },
190
+ onDrop() {
191
+ setDragging("idle");
192
+ },
193
+ });
194
+ onCleanup(dispose);
195
+ });
196
+ const bgStyle = createMemo(() => {
197
+ if (dragging() === "dragging-over") {
198
+ return { "background-color": "color-mix(in srgb, var(--color-primary) 10%, transparent)" };
199
+ }
200
+ return {};
201
+ });
202
+ return (<div ref={ref} class={filterGroupChip({ size: props.size })} style={{ opacity: dragging() === "dragging" ? 0.2 : 1, ...bgStyle() }} onClick={() => props.setOpen?.((prev) => !prev)}>
203
+ <FilterChip filter={props.filterGroup.filters[0]} onGroupDrag={props.onGroupDrag} isInGroup={true} groupIndex={0} index={props.index}/>
204
+ <For each={props.filterGroup.filters.slice(1)}>
205
+ {(filter, ind) => (<>
206
+ <span class="italic mx-0.5 hidden sm:inline">Or</span>
207
+ <div class="hidden sm:contents">
208
+ <FilterChip filter={filter} onGroupDrag={props.onGroupDrag} isInGroup={true} groupIndex={ind()} index={props.index}/>
209
+ </div>
210
+ </>)}
211
+ </For>
212
+
213
+ <span class="sm:hidden badge badge-ghost badge-xs">+{extraCount()} more</span>
214
+
215
+ <Button onClick={props.onDelete} size="xs" variant="ghost" class="p-0.5 m-0 h-min">
216
+ <CloseIcon class="w-[1em] h-[1em]" stroke-width={4}/>
217
+ </Button>
218
+ </div>);
219
+ };
220
+ export default FilterChip;
@@ -0,0 +1,13 @@
1
+ import { Filter, FilterField, FilterOperator, FilterValue } from "./FilterBar";
2
+ interface FilterEditProps<T> {
3
+ availableFields: FilterField<T>[];
4
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
5
+ filter: Partial<Filter<T>>;
6
+ setField: (field?: FilterField<T>) => void;
7
+ setOperator: (operator?: FilterOperator) => void;
8
+ setValue: (value?: FilterValue) => void;
9
+ setCanConfirm: (v: boolean) => void;
10
+ onDelete?: () => void;
11
+ }
12
+ export declare const FilterEdit: <T>(props: FilterEditProps<T>) => import("solid-js").JSX.Element;
13
+ export default FilterEdit;
@@ -0,0 +1,159 @@
1
+ import { createMemo, createSignal, Match, Show, Switch } from "solid-js";
2
+ import { createStore } from "solid-js/store";
3
+ import X from "lucide-solid/icons/x";
4
+ import { filterDefaults, filterLabels, } from "./FilterBar";
5
+ import { Select } from "../Select";
6
+ import { Input } from "../Input";
7
+ import { NumberInput } from "../NumberInput";
8
+ import { DateInput } from "../DateInput";
9
+ import { Button } from "../Button";
10
+ export const FilterEdit = (props) => {
11
+ const [selectedBoolValue, setSelectedBoolValue] = createSignal(props.filter.field?.type === "bool"
12
+ ? {
13
+ label: props.filter.value ? "Is True" : "Is False",
14
+ value: props.filter.value,
15
+ }
16
+ : null);
17
+ const [selectedTextValue, setSelectedTextValue] = createSignal(props.filter.field?.type === "text" ? props.filter.value : "");
18
+ const [selectedNumberValue, setSelectedNumberValue] = createSignal(props.filter.field?.type === "number" ? props.filter.value : 0);
19
+ const [selectedSelectValue, setSelectedSelectValue] = createSignal(props.filter.field?.type === "select" ? props.filter.value : null);
20
+ const [selectedDateValues, setSelectedDateValues] = createStore(props.filter.field?.type === "date"
21
+ ? props.filter.value
22
+ : { startDate: null, endDate: null });
23
+ const availableOperators = createMemo(() => props.filter.field
24
+ ? Object.entries(filterLabels[props.filter.field.type]).map(([value, label]) => ({
25
+ label,
26
+ value: value,
27
+ }))
28
+ : []);
29
+ const handleOperatorChange = (operator) => {
30
+ props.setOperator(operator);
31
+ if (["is_set", "is_not_set"].includes(operator ?? "")) {
32
+ props.setValue(null);
33
+ props.setCanConfirm(true);
34
+ }
35
+ else {
36
+ switch (props.filter.field?.type) {
37
+ case "number":
38
+ props.setValue(0);
39
+ props.setCanConfirm(true);
40
+ return;
41
+ case "text":
42
+ props.setValue("");
43
+ default:
44
+ props.setValue(undefined);
45
+ }
46
+ props.setCanConfirm(false);
47
+ // hmm, for ux I don't want to clear too much when switching operator. will see how this feels.
48
+ // need to do this for canConfirm behaviour atm
49
+ setSelectedDateValues("endDate", null);
50
+ }
51
+ };
52
+ const handleFieldChange = (f) => {
53
+ props.setField(f);
54
+ const newOperator = f ? filterDefaults[f.type] : undefined;
55
+ handleOperatorChange(newOperator);
56
+ };
57
+ const canConfirm = (val) => {
58
+ if (!props.filter.field || !props.filter.operator || val === undefined || val === "")
59
+ return false;
60
+ if (props.filter.field?.type === "date" &&
61
+ props.filter.operator === "between" &&
62
+ (selectedDateValues.endDate === null || selectedDateValues.startDate === null)) {
63
+ return false;
64
+ }
65
+ return true;
66
+ };
67
+ const handleValueChange = (val) => {
68
+ if (Number.isNaN(val)) {
69
+ props.setValue(0);
70
+ }
71
+ else {
72
+ props.setValue(val);
73
+ }
74
+ props.setCanConfirm(canConfirm(val));
75
+ };
76
+ const selectValue = createMemo(() => {
77
+ if (!props.filter.operator || !props.filter.field)
78
+ return null;
79
+ return {
80
+ value: props.filter.operator,
81
+ label: filterLabels[props.filter.field.type][props.filter.operator] || "",
82
+ };
83
+ });
84
+ return (<div class="bg-base-100 p-2 rounded-box">
85
+ <div class="flex justify-end mb-1">
86
+ <Show when={props.onDelete}>
87
+ <Button variant="ghost" size="xs" appearance="error" modifier="circle" onClick={props.onDelete}>
88
+ <X size={12}/>
89
+ </Button>
90
+ </Show>
91
+ </div>
92
+ <div class="flex flex-col lg:flex-row gap-3">
93
+ <Select label="Field" value={props.filter.field ?? null} labelKey="label" valueKey="name" onChange={(f) => handleFieldChange(f ?? undefined)} options={props.availableFields} size={props.size} class="min-w-50"/>
94
+ <Show when={props.filter.field?.type !== "bool"}>
95
+ <Select value={selectValue()} label="Operator" labelKey="label" valueKey="value" onChange={(v) => handleOperatorChange(v?.value)} options={availableOperators()} disabled={!props.filter.field} size={props.size} class="min-w-50"/>
96
+ </Show>
97
+ <Switch>
98
+ <Match when={props.filter.field?.type === "bool"}>
99
+ <Select label="Value" labelKey="label" valueKey="value" value={selectedBoolValue()} onChange={(val) => {
100
+ setSelectedBoolValue(val);
101
+ handleValueChange(val?.value);
102
+ }} options={[
103
+ { label: "Is True", value: true },
104
+ { label: "Is False", value: false },
105
+ ]} size={props.size} class="min-w-50"/>
106
+ </Match>
107
+ <Match when={props.filter.field?.type === "text" &&
108
+ !["is_set", "is_not_set"].includes(props.filter.operator ?? "")}>
109
+ <Input label="Value" value={selectedTextValue()} onChange={(val) => {
110
+ setSelectedTextValue(val);
111
+ handleValueChange(val);
112
+ }} size={props.size} class="min-w-50"/>
113
+ </Match>
114
+ <Match when={props.filter.field?.type === "number"}>
115
+ <NumberInput label="Value" rawValue={selectedNumberValue()} onRawValueChange={(val) => {
116
+ setSelectedNumberValue(val);
117
+ handleValueChange(val);
118
+ }} inputProps={{ class: "w-full" }} size={props.size} class="min-w-50"/>
119
+ </Match>
120
+ <Match when={props.filter.field?.type === "select" &&
121
+ !["is_set", "is_not_set"].includes(props.filter.operator ?? "")}>
122
+ <Show when={["in", "not_in"].includes(props.filter.operator ?? "")} fallback={<Select value={selectedSelectValue()} label="Value" labelKey="label" valueKey="value" onChange={(val) => {
123
+ setSelectedSelectValue(val);
124
+ handleValueChange(val);
125
+ }} options={props.filter.field?.options ?? []} disabled={!props.filter.operator || !props.filter.field?.options?.length} size={props.size} class="min-w-50"/>}>
126
+ <Input label="Value" value={selectedTextValue()} onChange={(val) => {
127
+ setSelectedTextValue(val);
128
+ handleValueChange(val);
129
+ }} size={props.size} class="min-w-50"/>
130
+ </Show>
131
+ </Match>
132
+ <Match when={props.filter.field?.type === "date" &&
133
+ !["is_set", "is_not_set"].includes(props.filter.operator ?? "")}>
134
+ <DateInput value={selectedDateValues.startDate} onChange={(val) => {
135
+ setSelectedDateValues("startDate", val);
136
+ let newVal = undefined;
137
+ if (val && !(props.filter.operator === "between" && selectedDateValues.endDate !== null)) {
138
+ newVal = { ...selectedDateValues };
139
+ }
140
+ handleValueChange(newVal);
141
+ }} max={selectedDateValues.endDate ? selectedDateValues.endDate.toISOString().slice(0, 10) : undefined} label={props.filter.operator === "between" ? "Start" : ""} size={props.size} class="min-w-50"/>
142
+ <Show when={props.filter.operator === "between"}>
143
+ <DateInput value={selectedDateValues.endDate} label="End" min={selectedDateValues.startDate
144
+ ? selectedDateValues.startDate.toISOString().slice(0, 10)
145
+ : undefined} onChange={(val) => {
146
+ setSelectedDateValues("endDate", val);
147
+ let newVal = undefined;
148
+ if (val && selectedDateValues.startDate !== null) {
149
+ newVal = { ...selectedDateValues };
150
+ }
151
+ handleValueChange(newVal);
152
+ }} size={props.size} class="min-w-50"/>
153
+ </Show>
154
+ </Match>
155
+ </Switch>
156
+ </div>
157
+ </div>);
158
+ };
159
+ export default FilterEdit;
@@ -0,0 +1,13 @@
1
+ import { JSXElement } from "solid-js";
2
+ import { Filter, FilterField } from "./FilterBar";
3
+ interface FiltersEditProps<T> {
4
+ availableFields: FilterField<T>[];
5
+ filters: Partial<Filter<T>>[];
6
+ onSaveFilters: (filters: Filter<T>[]) => void;
7
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
8
+ saveTrigger: JSXElement;
9
+ addConditionTrigger?: JSXElement;
10
+ filtersReady?: boolean;
11
+ }
12
+ export declare const FiltersEdit: <T>(props: FiltersEditProps<T>) => import("solid-js").JSX.Element;
13
+ export default FiltersEdit;