@alepha/ui 0.10.6 → 0.11.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.
@@ -0,0 +1,196 @@
1
+ import { useFormState } from "@alepha/react-form";
2
+ import {
3
+ Autocomplete,
4
+ type AutocompleteProps,
5
+ Flex,
6
+ Input,
7
+ MultiSelect,
8
+ type MultiSelectProps,
9
+ SegmentedControl,
10
+ type SegmentedControlProps,
11
+ Select,
12
+ type SelectProps,
13
+ TagsInput,
14
+ type TagsInputProps,
15
+ } from "@mantine/core";
16
+ import { useEffect, useState } from "react";
17
+ import { type GenericControlProps, parseInput } from "../utils/parseInput.ts";
18
+
19
+ export type SelectValueLabel =
20
+ | string
21
+ | { value: string; label: string; icon?: string };
22
+
23
+ export interface ControlSelectProps extends GenericControlProps {
24
+ select?: boolean | SelectProps;
25
+ multi?: boolean | MultiSelectProps;
26
+ tags?: boolean | TagsInputProps;
27
+ autocomplete?: boolean | AutocompleteProps;
28
+ segmented?: boolean | Partial<SegmentedControlProps>;
29
+
30
+ loader?: () => Promise<SelectValueLabel[]>;
31
+ }
32
+
33
+ /**
34
+ * ControlSelect component for handling Select, MultiSelect, and TagsInput.
35
+ *
36
+ * Features:
37
+ * - Basic Select with enum support
38
+ * - MultiSelect for array of enums
39
+ * - TagsInput for array of strings (no enum)
40
+ * - Future: Lazy loading
41
+ * - Future: Searchable/filterable options
42
+ * - Future: Custom option rendering
43
+ *
44
+ * Automatically detects enum values and array types from schema.
45
+ */
46
+ const ControlSelect = (props: ControlSelectProps) => {
47
+ const form = useFormState(props.input);
48
+ const { inputProps, id, icon } = parseInput(props, form);
49
+
50
+ // Detect if schema is an array type
51
+ const isArray =
52
+ props.input.schema &&
53
+ "type" in props.input.schema &&
54
+ props.input.schema.type === "array";
55
+
56
+ // For arrays, check if items have enum (MultiSelect) or not (TagsInput)
57
+ let itemsEnum: string[] | undefined;
58
+ if (isArray && "items" in props.input.schema && props.input.schema.items) {
59
+ const items: any = props.input.schema.items;
60
+ if ("enum" in items && Array.isArray(items.enum)) {
61
+ itemsEnum = items.enum;
62
+ }
63
+ }
64
+
65
+ // Extract enum values from schema (for non-array select)
66
+ const enumValues =
67
+ props.input.schema &&
68
+ "enum" in props.input.schema &&
69
+ Array.isArray(props.input.schema.enum)
70
+ ? props.input.schema.enum
71
+ : [];
72
+
73
+ const [data, setData] = useState<SelectValueLabel[]>([]);
74
+
75
+ useEffect(() => {
76
+ if (!props.input?.props) {
77
+ return;
78
+ }
79
+
80
+ if (props.loader) {
81
+ props.loader().then(setData);
82
+ } else {
83
+ setData(enumValues);
84
+ }
85
+ }, [props.input, props.loader]);
86
+
87
+ if (!props.input?.props) {
88
+ return null;
89
+ }
90
+
91
+ if (props.segmented) {
92
+ const segmentedControlProps: Partial<SegmentedControlProps> =
93
+ typeof props.segmented === "object" ? props.segmented : {};
94
+
95
+ return (
96
+ <Input.Wrapper {...inputProps}>
97
+ <Flex mt={"calc(var(--mantine-spacing-xs) / 2)"}>
98
+ <SegmentedControl
99
+ disabled={inputProps.disabled}
100
+ defaultValue={String(props.input.props.defaultValue)}
101
+ {...segmentedControlProps}
102
+ onChange={(value) => {
103
+ props.input.set(value);
104
+ }}
105
+ data={data.slice(0, 10)}
106
+ />
107
+ </Flex>
108
+ </Input.Wrapper>
109
+ );
110
+ }
111
+
112
+ if (props.autocomplete) {
113
+ const autocompleteProps =
114
+ typeof props.autocomplete === "object" ? props.autocomplete : {};
115
+
116
+ return (
117
+ <Autocomplete
118
+ {...inputProps}
119
+ id={id}
120
+ leftSection={icon}
121
+ data={data}
122
+ {...props.input.props}
123
+ {...autocompleteProps}
124
+ />
125
+ );
126
+ }
127
+
128
+ // region <TagsInput/> - for array of strings without enum
129
+ if ((isArray && !itemsEnum) || props.tags) {
130
+ const tagsInputProps = typeof props.tags === "object" ? props.tags : {};
131
+ return (
132
+ <TagsInput
133
+ {...inputProps}
134
+ id={id}
135
+ leftSection={icon}
136
+ defaultValue={
137
+ Array.isArray(props.input.props.defaultValue)
138
+ ? props.input.props.defaultValue
139
+ : []
140
+ }
141
+ onChange={(value) => {
142
+ props.input.set(value);
143
+ }}
144
+ {...tagsInputProps}
145
+ />
146
+ );
147
+ }
148
+ // endregion
149
+
150
+ // region <MultiSelect/> - for array of enums
151
+ if ((isArray && itemsEnum) || props.multi) {
152
+ const data =
153
+ itemsEnum?.map((value: string) => ({
154
+ value,
155
+ label: value,
156
+ })) || [];
157
+
158
+ const multiSelectProps = typeof props.multi === "object" ? props.multi : {};
159
+
160
+ return (
161
+ <MultiSelect
162
+ {...inputProps}
163
+ id={id}
164
+ leftSection={icon}
165
+ data={data}
166
+ defaultValue={
167
+ Array.isArray(props.input.props.defaultValue)
168
+ ? props.input.props.defaultValue
169
+ : []
170
+ }
171
+ onChange={(value) => {
172
+ props.input.set(value);
173
+ }}
174
+ {...multiSelectProps}
175
+ />
176
+ );
177
+ }
178
+ // endregion
179
+
180
+ // region <Select/> - for single enum value
181
+ const selectProps = typeof props.select === "object" ? props.select : {};
182
+
183
+ return (
184
+ <Select
185
+ {...inputProps}
186
+ id={id}
187
+ leftSection={icon}
188
+ data={data}
189
+ {...props.input.props}
190
+ {...selectProps}
191
+ />
192
+ );
193
+ // endregion
194
+ };
195
+
196
+ export default ControlSelect;
@@ -0,0 +1,82 @@
1
+ import {
2
+ ActionIcon,
3
+ Flex,
4
+ SegmentedControl,
5
+ useComputedColorScheme,
6
+ useMantineColorScheme,
7
+ } from "@mantine/core";
8
+ import { IconMoon, IconSun } from "@tabler/icons-react";
9
+ import { useEffect, useState } from "react";
10
+
11
+ export interface DarkModeButtonProps {
12
+ mode?: "minimal" | "segmented";
13
+ size?: string | number;
14
+ variant?:
15
+ | "filled"
16
+ | "light"
17
+ | "outline"
18
+ | "default"
19
+ | "subtle"
20
+ | "transparent";
21
+ }
22
+
23
+ const DarkModeButton = (props: DarkModeButtonProps) => {
24
+ const { setColorScheme } = useMantineColorScheme();
25
+ const computedColorScheme = useComputedColorScheme("light");
26
+ const [colorScheme, setColorScheme2] = useState("default");
27
+ const mode = props.mode ?? "minimal";
28
+
29
+ useEffect(() => {
30
+ setColorScheme2(computedColorScheme);
31
+ }, [computedColorScheme]);
32
+
33
+ const toggleColorScheme = () => {
34
+ setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
35
+ };
36
+
37
+ if (mode === "segmented") {
38
+ return (
39
+ <SegmentedControl
40
+ value={colorScheme}
41
+ onChange={(value) => setColorScheme(value as "light" | "dark")}
42
+ data={[
43
+ {
44
+ value: "light",
45
+ label: (
46
+ <Flex h={20} align="center" justify="center">
47
+ <IconSun size={16} />
48
+ </Flex>
49
+ ),
50
+ },
51
+ {
52
+ value: "dark",
53
+ label: (
54
+ <Flex h={20} align="center" justify="center">
55
+ <IconMoon size={16} />
56
+ </Flex>
57
+ ),
58
+ },
59
+ ]}
60
+ />
61
+ );
62
+ }
63
+
64
+ return (
65
+ <ActionIcon
66
+ onClick={toggleColorScheme}
67
+ variant={props.variant ?? "default"}
68
+ size={props.size ?? "lg"}
69
+ aria-label="Toggle color scheme"
70
+ >
71
+ {colorScheme === "dark" ? (
72
+ <IconSun size={20} />
73
+ ) : colorScheme === "light" ? (
74
+ <IconMoon size={20} />
75
+ ) : (
76
+ <Flex h={20} />
77
+ )}
78
+ </ActionIcon>
79
+ );
80
+ };
81
+
82
+ export default DarkModeButton;
@@ -0,0 +1,199 @@
1
+ /* ------------------------------------------------------------------------------------------------------------------ */
2
+ /* DataTable Component Styles */
3
+ /* ------------------------------------------------------------------------------------------------------------------ */
4
+
5
+ .alepha-datatable-container {
6
+ display: flex;
7
+ flex-direction: column;
8
+ height: 100%;
9
+ }
10
+
11
+ .alepha-datatable-toolbar {
12
+ border-radius: var(--mantine-radius-md);
13
+ background: var(--mantine-color-body);
14
+ }
15
+
16
+ .alepha-datatable-search-input {
17
+ min-width: 200px;
18
+ max-width: 300px;
19
+ }
20
+
21
+ .alepha-datatable-page-size-select {
22
+ width: 120px;
23
+ }
24
+
25
+ .alepha-datatable-table {
26
+ position: relative;
27
+ }
28
+
29
+ .alepha-datatable-th {
30
+ font-weight: 600;
31
+ white-space: nowrap;
32
+ user-select: none;
33
+ transition: background-color 0.2s ease;
34
+ }
35
+
36
+ .alepha-datatable-th:hover {
37
+ background-color: var(--mantine-color-gray-0);
38
+ }
39
+
40
+ [data-mantine-color-scheme="dark"] .alepha-datatable-th:hover {
41
+ background-color: var(--mantine-color-dark-6);
42
+ }
43
+
44
+ .alepha-datatable-tr {
45
+ transition: background-color 0.15s ease;
46
+ }
47
+
48
+ .alepha-datatable-tr.alepha-datatable-selected {
49
+ background-color: var(--mantine-color-blue-0) !important;
50
+ }
51
+
52
+ [data-mantine-color-scheme="dark"]
53
+ .alepha-datatable-tr.alepha-datatable-selected {
54
+ background-color: rgba(34, 139, 230, 0.1) !important;
55
+ }
56
+
57
+ .alepha-datatable-checkbox-column {
58
+ width: 40px;
59
+ text-align: center;
60
+ }
61
+
62
+ .alepha-datatable-actions-column {
63
+ width: 100px;
64
+ text-align: center;
65
+ }
66
+
67
+ .alepha-datatable-sort-icon {
68
+ color: var(--mantine-color-blue-6);
69
+ transition: transform 0.2s ease;
70
+ }
71
+
72
+ .alepha-datatable-sort-icon-inactive {
73
+ color: var(--mantine-color-gray-5);
74
+ opacity: 0;
75
+ transition: opacity 0.2s ease;
76
+ }
77
+
78
+ .alepha-datatable-th:hover .alepha-datatable-sort-icon-inactive {
79
+ opacity: 0.5;
80
+ }
81
+
82
+ /* Loading state */
83
+ .alepha-datatable-loading {
84
+ position: relative;
85
+ pointer-events: none;
86
+ opacity: 0.5;
87
+ }
88
+
89
+ .alepha-datatable-loading-overlay {
90
+ position: absolute;
91
+ top: 0;
92
+ left: 0;
93
+ right: 0;
94
+ bottom: 0;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ background: rgba(255, 255, 255, 0.8);
99
+ z-index: 10;
100
+ }
101
+
102
+ [data-mantine-color-scheme="dark"] .alepha-datatable-loading-overlay {
103
+ background: rgba(26, 27, 30, 0.8);
104
+ }
105
+
106
+ /* Sticky header styles */
107
+ .alepha-datatable-sticky-header {
108
+ position: sticky;
109
+ top: 0;
110
+ z-index: 5;
111
+ background: var(--mantine-color-body);
112
+ }
113
+
114
+ /* Zebra striping adjustments */
115
+ .alepha-datatable-striped tbody tr:nth-of-type(odd) {
116
+ background-color: var(--mantine-color-gray-0);
117
+ }
118
+
119
+ [data-mantine-color-scheme="dark"]
120
+ .alepha-datatable-striped
121
+ tbody
122
+ tr:nth-of-type(odd) {
123
+ background-color: var(--mantine-color-dark-7);
124
+ }
125
+
126
+ /* Empty state */
127
+ .alepha-datatable-empty-state {
128
+ padding: 3rem 1rem;
129
+ text-align: center;
130
+ }
131
+
132
+ .alepha-datatable-empty-icon {
133
+ color: var(--mantine-color-gray-5);
134
+ margin-bottom: 1rem;
135
+ }
136
+
137
+ /* Footer */
138
+ .alepha-datatable-footer {
139
+ border-top: 1px solid var(--mantine-color-gray-2);
140
+ padding-top: var(--mantine-spacing-sm);
141
+ margin-top: var(--mantine-spacing-xs);
142
+ }
143
+
144
+ [data-mantine-color-scheme="dark"] .alepha-datatable-footer {
145
+ border-top-color: var(--mantine-color-dark-5);
146
+ }
147
+
148
+ /* Custom scrollbar for better UX */
149
+ .alepha-datatable-container ::-webkit-scrollbar {
150
+ width: 8px;
151
+ height: 8px;
152
+ }
153
+
154
+ .alepha-datatable-container ::-webkit-scrollbar-track {
155
+ background: var(--mantine-color-gray-1);
156
+ border-radius: 4px;
157
+ }
158
+
159
+ [data-mantine-color-scheme="dark"]
160
+ .alepha-datatable-container
161
+ ::-webkit-scrollbar-track {
162
+ background: var(--mantine-color-dark-6);
163
+ }
164
+
165
+ .alepha-datatable-container ::-webkit-scrollbar-thumb {
166
+ background: var(--mantine-color-gray-4);
167
+ border-radius: 4px;
168
+ }
169
+
170
+ [data-mantine-color-scheme="dark"]
171
+ .alepha-datatable-container
172
+ ::-webkit-scrollbar-thumb {
173
+ background: var(--mantine-color-dark-4);
174
+ }
175
+
176
+ .alepha-datatable-container ::-webkit-scrollbar-thumb:hover {
177
+ background: var(--mantine-color-gray-5);
178
+ }
179
+
180
+ [data-mantine-color-scheme="dark"]
181
+ .alepha-datatable-container
182
+ ::-webkit-scrollbar-thumb:hover {
183
+ background: var(--mantine-color-dark-3);
184
+ }
185
+
186
+ /* Responsive */
187
+ @media (max-width: 768px) {
188
+ .alepha-datatable-toolbar {
189
+ padding: var(--mantine-spacing-sm);
190
+ }
191
+
192
+ .alepha-datatable-search-input {
193
+ min-width: 150px;
194
+ }
195
+
196
+ .alepha-datatable-page-size-select {
197
+ width: 100px;
198
+ }
199
+ }