@tanstack/cta-ui-base 0.28.0 → 0.29.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.
- package/dist/components/add-on-config-dialog.d.ts +10 -0
- package/dist/components/add-on-config-dialog.js +8 -0
- package/dist/components/add-on-option-select.d.ts +9 -0
- package/dist/components/add-on-option-select.js +9 -0
- package/dist/components/add-on-options-panel.d.ts +9 -0
- package/dist/components/add-on-options-panel.js +14 -0
- package/dist/components/sidebar-items/add-ons.js +18 -8
- package/dist/store/project.d.ts +2 -0
- package/dist/store/project.js +20 -1
- package/package.json +2 -2
- package/src/components/add-on-config-dialog.tsx +51 -0
- package/src/components/add-on-option-select.tsx +63 -0
- package/src/components/add-on-options-panel.tsx +42 -0
- package/src/components/sidebar-items/add-ons.tsx +88 -54
- package/src/store/project.ts +24 -1
- package/src/types.d.ts +2 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AddOnInfo } from '../types';
|
|
2
|
+
interface AddOnConfigDialogProps {
|
|
3
|
+
addOn: AddOnInfo | undefined;
|
|
4
|
+
selectedOptions: Record<string, any>;
|
|
5
|
+
onOptionChange: (optionName: string, value: any) => void;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export default function AddOnConfigDialog({ addOn, selectedOptions, onOptionChange, onClose, disabled, }: AddOnConfigDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from './ui/dialog';
|
|
3
|
+
import AddOnOptionsPanel from './add-on-options-panel';
|
|
4
|
+
export default function AddOnConfigDialog({ addOn, selectedOptions, onOptionChange, onClose, disabled = false, }) {
|
|
5
|
+
if (!addOn)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsx(Dialog, { open: !!addOn, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Configure ", addOn.name] }), _jsx(DialogDescription, { children: "Customize the configuration options for this add-on." })] }), _jsx("div", { className: "py-4", children: _jsx(AddOnOptionsPanel, { addOn: addOn, selectedOptions: selectedOptions, onOptionChange: onOptionChange, disabled: disabled }) })] }) }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AddOnSelectOption } from '@tanstack/cta-engine';
|
|
2
|
+
interface AddOnOptionSelectProps {
|
|
3
|
+
option: AddOnSelectOption;
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export default function AddOnOptionSelect({ option, value, onChange, disabled, }: AddOnOptionSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDownIcon } from 'lucide-react';
|
|
3
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from './ui/dropdown-menu';
|
|
4
|
+
import { Button } from './ui/button';
|
|
5
|
+
import { Label } from './ui/label';
|
|
6
|
+
export default function AddOnOptionSelect({ option, value, onChange, disabled = false, }) {
|
|
7
|
+
const selectedOption = option.options.find((opt) => opt.value === value);
|
|
8
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { className: "text-sm font-medium text-gray-300", children: option.label }), option.description && (_jsx("p", { className: "text-xs text-gray-500", children: option.description })), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: "w-full justify-between", disabled: disabled, children: [_jsx("span", { children: selectedOption?.label || 'Select option...' }), _jsx(ChevronDownIcon, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { className: "w-full min-w-[200px]", children: option.options.map((opt) => (_jsx(DropdownMenuItem, { onClick: () => onChange(opt.value), className: value === opt.value ? 'bg-accent' : '', children: opt.label }, opt.value))) })] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AddOnInfo } from '../types';
|
|
2
|
+
interface AddOnOptionsPanelProps {
|
|
3
|
+
addOn: AddOnInfo;
|
|
4
|
+
selectedOptions: Record<string, any>;
|
|
5
|
+
onOptionChange: (optionName: string, value: any) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export default function AddOnOptionsPanel({ addOn, selectedOptions, onOptionChange, disabled, }: AddOnOptionsPanelProps): import("react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import AddOnOptionSelect from './add-on-option-select';
|
|
3
|
+
export default function AddOnOptionsPanel({ addOn, selectedOptions, onOptionChange, disabled = false, }) {
|
|
4
|
+
if (!addOn.options || Object.keys(addOn.options).length === 0) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
return (_jsx("div", { className: "space-y-3", children: Object.entries(addOn.options).map(([optionName, option]) => {
|
|
8
|
+
if (option && typeof option === 'object' && 'type' in option && option.type === 'select') {
|
|
9
|
+
return (_jsx(AddOnOptionSelect, { option: option, value: selectedOptions[optionName] || option.default, onChange: (value) => onOptionChange(optionName, value), disabled: disabled }, optionName));
|
|
10
|
+
}
|
|
11
|
+
// Future option types can be added here
|
|
12
|
+
return null;
|
|
13
|
+
}) }));
|
|
14
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useMemo, useState } from 'react';
|
|
3
|
-
import { InfoIcon } from 'lucide-react';
|
|
3
|
+
import { InfoIcon, SettingsIcon } from 'lucide-react';
|
|
4
4
|
import { Switch } from '../ui/switch';
|
|
5
5
|
import { Label } from '../ui/label';
|
|
6
|
-
import {
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { useAddOns, useProjectOptions } from '../../store/project';
|
|
7
8
|
import ImportCustomAddOn from '../custom-add-on-dialog';
|
|
8
9
|
import AddOnInfoDialog from '../add-on-info-dialog';
|
|
10
|
+
import AddOnConfigDialog from '../add-on-config-dialog';
|
|
9
11
|
const addOnTypeLabels = {
|
|
10
12
|
toolchain: 'Toolchain',
|
|
11
13
|
'add-on': 'Add-on',
|
|
@@ -13,16 +15,24 @@ const addOnTypeLabels = {
|
|
|
13
15
|
example: 'Example',
|
|
14
16
|
};
|
|
15
17
|
export default function SelectedAddOns() {
|
|
16
|
-
const { availableAddOns, addOnState, toggleAddOn } = useAddOns();
|
|
18
|
+
const { availableAddOns, addOnState, toggleAddOn, setAddOnOption } = useAddOns();
|
|
19
|
+
const addOnOptions = useProjectOptions((state) => state.addOnOptions);
|
|
17
20
|
const sortedAddOns = useMemo(() => {
|
|
18
21
|
return availableAddOns.sort((a, b) => {
|
|
19
22
|
return a.name.localeCompare(b.name);
|
|
20
23
|
});
|
|
21
24
|
}, [availableAddOns]);
|
|
22
25
|
const [infoAddOn, setInfoAddOn] = useState();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const [configAddOn, setConfigAddOn] = useState();
|
|
27
|
+
return (_jsxs(_Fragment, { children: [_jsx(AddOnInfoDialog, { addOn: infoAddOn, onClose: () => setInfoAddOn(undefined) }), _jsx(AddOnConfigDialog, { addOn: configAddOn, selectedOptions: configAddOn ? addOnOptions[configAddOn.id] || {} : {}, onOptionChange: (optionName, value) => {
|
|
28
|
+
if (configAddOn) {
|
|
29
|
+
setAddOnOption(configAddOn.id, optionName, value);
|
|
30
|
+
}
|
|
31
|
+
}, onClose: () => setConfigAddOn(undefined), disabled: configAddOn ? !addOnState[configAddOn.id]?.enabled : false }), _jsx("div", { className: "max-h-[60vh] overflow-y-auto space-y-2", children: Object.keys(addOnTypeLabels).map((type) => (_jsx(Fragment, { children: sortedAddOns.filter((addOn) => addOn.type === type).length > 0 && (_jsxs("div", { className: "block p-4 bg-gray-500/10 hover:bg-gray-500/20 rounded-lg transition-colors space-y-4 active", children: [_jsx("h3", { className: "font-medium", children: addOnTypeLabels[type] }), _jsx("div", { className: "space-y-3", children: _jsx("div", { className: "flex flex-row flex-wrap", children: sortedAddOns
|
|
32
|
+
.filter((addOn) => addOn.type === type)
|
|
33
|
+
.map((addOn) => (_jsx("div", { className: "w-1/2", children: _jsxs("div", { className: "flex flex-row items-center justify-between", children: [_jsxs("div", { className: "p-1 flex flex-row items-center", children: [_jsx(Switch, { id: addOn.id, checked: addOnState[addOn.id].selected, disabled: !addOnState[addOn.id].enabled, onCheckedChange: () => {
|
|
34
|
+
toggleAddOn(addOn.id);
|
|
35
|
+
} }), _jsxs(Label, { htmlFor: addOn.id, className: "pl-2 font-semibold text-gray-300 flex items-center gap-2", children: [addOn.smallLogo && (_jsx("img", { src: `data:image/svg+xml,${encodeURIComponent(addOn.smallLogo)}`, alt: addOn.name, className: "w-5" })), addOn.name] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [addOnState[addOn.id].selected &&
|
|
36
|
+
addOn.options &&
|
|
37
|
+
Object.keys(addOn.options).length > 0 && (_jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0 text-gray-600 hover:text-gray-400", onClick: () => setConfigAddOn(addOn), disabled: !addOnState[addOn.id].enabled, children: _jsx(SettingsIcon, { className: "w-4 h-4" }) })), _jsx(InfoIcon, { className: "w-4 text-gray-600 cursor-pointer hover:text-gray-400", onClick: () => setInfoAddOn(addOn) })] })] }) }, addOn.id))) }) })] }, `${type}-add-ons`)) }, type))) }), _jsx("div", { className: "mt-4", children: _jsx(ImportCustomAddOn, {}) })] }));
|
|
28
38
|
}
|
package/dist/store/project.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export declare const useProjectStarter: import("zustand").UseBoundStore<import("
|
|
|
25
25
|
export declare function addCustomAddOn(addOn: AddOnInfo): void;
|
|
26
26
|
export declare function useAddOns(): {
|
|
27
27
|
toggleAddOn: (addOnId: string) => void;
|
|
28
|
+
setAddOnOption: (addOnId: string, optionName: string, value: any) => void;
|
|
29
|
+
getAddOnOptions: (addOnId: string) => Record<string, any>;
|
|
28
30
|
chosenAddOns: string[];
|
|
29
31
|
availableAddOns: AddOnInfo[];
|
|
30
32
|
userSelectedAddOns: string[];
|
package/dist/store/project.js
CHANGED
|
@@ -14,6 +14,7 @@ export const useProjectOptions = create(() => ({
|
|
|
14
14
|
tailwind: true,
|
|
15
15
|
git: true,
|
|
16
16
|
chosenAddOns: [],
|
|
17
|
+
addOnOptions: {},
|
|
17
18
|
packageManager: 'pnpm',
|
|
18
19
|
}));
|
|
19
20
|
const useInitialData = () => useQuery({
|
|
@@ -137,9 +138,27 @@ export function useAddOns() {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
}, [ready, addOnState]);
|
|
141
|
+
}, [ready, addOnState, availableAddOns]);
|
|
142
|
+
const setAddOnOption = useCallback((addOnId, optionName, value) => {
|
|
143
|
+
if (!ready)
|
|
144
|
+
return;
|
|
145
|
+
useProjectOptions.setState((state) => ({
|
|
146
|
+
addOnOptions: {
|
|
147
|
+
...state.addOnOptions,
|
|
148
|
+
[addOnId]: {
|
|
149
|
+
...state.addOnOptions[addOnId],
|
|
150
|
+
[optionName]: value,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}));
|
|
154
|
+
}, [ready]);
|
|
155
|
+
const getAddOnOptions = useCallback((addOnId) => {
|
|
156
|
+
return useProjectOptions.getState().addOnOptions[addOnId] || {};
|
|
157
|
+
}, []);
|
|
141
158
|
return {
|
|
142
159
|
toggleAddOn,
|
|
160
|
+
setAddOnOption,
|
|
161
|
+
getAddOnOptions,
|
|
143
162
|
chosenAddOns,
|
|
144
163
|
availableAddOns,
|
|
145
164
|
userSelectedAddOns,
|
package/package.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"sonner": "^2.0.3",
|
|
37
37
|
"tailwind-merge": "^3.0.2",
|
|
38
38
|
"zustand": "^5.0.3",
|
|
39
|
-
"@tanstack/cta-engine": "0.
|
|
39
|
+
"@tanstack/cta-engine": "0.29.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/react": "^19.0.8",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"vite-tsconfig-paths": "^5.1.4",
|
|
46
46
|
"vitest": "^3.1.4"
|
|
47
47
|
},
|
|
48
|
-
"version": "0.
|
|
48
|
+
"version": "0.29.1",
|
|
49
49
|
"scripts": {}
|
|
50
50
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog,
|
|
3
|
+
DialogContent,
|
|
4
|
+
DialogDescription,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
} from './ui/dialog'
|
|
8
|
+
|
|
9
|
+
import AddOnOptionsPanel from './add-on-options-panel'
|
|
10
|
+
|
|
11
|
+
import type { AddOnInfo } from '../types'
|
|
12
|
+
|
|
13
|
+
interface AddOnConfigDialogProps {
|
|
14
|
+
addOn: AddOnInfo | undefined
|
|
15
|
+
selectedOptions: Record<string, any>
|
|
16
|
+
onOptionChange: (optionName: string, value: any) => void
|
|
17
|
+
onClose: () => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function AddOnConfigDialog({
|
|
22
|
+
addOn,
|
|
23
|
+
selectedOptions,
|
|
24
|
+
onOptionChange,
|
|
25
|
+
onClose,
|
|
26
|
+
disabled = false,
|
|
27
|
+
}: AddOnConfigDialogProps) {
|
|
28
|
+
if (!addOn) return null
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Dialog open={!!addOn} onOpenChange={(open) => !open && onClose()}>
|
|
32
|
+
<DialogContent className="sm:max-w-md">
|
|
33
|
+
<DialogHeader>
|
|
34
|
+
<DialogTitle>Configure {addOn.name}</DialogTitle>
|
|
35
|
+
<DialogDescription>
|
|
36
|
+
Customize the configuration options for this add-on.
|
|
37
|
+
</DialogDescription>
|
|
38
|
+
</DialogHeader>
|
|
39
|
+
|
|
40
|
+
<div className="py-4">
|
|
41
|
+
<AddOnOptionsPanel
|
|
42
|
+
addOn={addOn}
|
|
43
|
+
selectedOptions={selectedOptions}
|
|
44
|
+
onOptionChange={onOptionChange}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</DialogContent>
|
|
49
|
+
</Dialog>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ChevronDownIcon } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from './ui/dropdown-menu'
|
|
9
|
+
import { Button } from './ui/button'
|
|
10
|
+
import { Label } from './ui/label'
|
|
11
|
+
|
|
12
|
+
import type { AddOnSelectOption } from '@tanstack/cta-engine'
|
|
13
|
+
|
|
14
|
+
interface AddOnOptionSelectProps {
|
|
15
|
+
option: AddOnSelectOption
|
|
16
|
+
value: string
|
|
17
|
+
onChange: (value: string) => void
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function AddOnOptionSelect({
|
|
22
|
+
option,
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
disabled = false,
|
|
26
|
+
}: AddOnOptionSelectProps) {
|
|
27
|
+
const selectedOption = option.options.find((opt: { value: string; label: string }) => opt.value === value)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-2">
|
|
31
|
+
<Label className="text-sm font-medium text-gray-300">
|
|
32
|
+
{option.label}
|
|
33
|
+
</Label>
|
|
34
|
+
{option.description && (
|
|
35
|
+
<p className="text-xs text-gray-500">{option.description}</p>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<DropdownMenu>
|
|
39
|
+
<DropdownMenuTrigger asChild>
|
|
40
|
+
<Button
|
|
41
|
+
variant="outline"
|
|
42
|
+
className="w-full justify-between"
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
>
|
|
45
|
+
<span>{selectedOption?.label || 'Select option...'}</span>
|
|
46
|
+
<ChevronDownIcon className="h-4 w-4" />
|
|
47
|
+
</Button>
|
|
48
|
+
</DropdownMenuTrigger>
|
|
49
|
+
<DropdownMenuContent className="w-full min-w-[200px]">
|
|
50
|
+
{option.options.map((opt: { value: string; label: string }) => (
|
|
51
|
+
<DropdownMenuItem
|
|
52
|
+
key={opt.value}
|
|
53
|
+
onClick={() => onChange(opt.value)}
|
|
54
|
+
className={value === opt.value ? 'bg-accent' : ''}
|
|
55
|
+
>
|
|
56
|
+
{opt.label}
|
|
57
|
+
</DropdownMenuItem>
|
|
58
|
+
))}
|
|
59
|
+
</DropdownMenuContent>
|
|
60
|
+
</DropdownMenu>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import AddOnOptionSelect from './add-on-option-select'
|
|
2
|
+
|
|
3
|
+
import type { AddOnInfo } from '../types'
|
|
4
|
+
|
|
5
|
+
interface AddOnOptionsPanelProps {
|
|
6
|
+
addOn: AddOnInfo
|
|
7
|
+
selectedOptions: Record<string, any>
|
|
8
|
+
onOptionChange: (optionName: string, value: any) => void
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function AddOnOptionsPanel({
|
|
13
|
+
addOn,
|
|
14
|
+
selectedOptions,
|
|
15
|
+
onOptionChange,
|
|
16
|
+
disabled = false,
|
|
17
|
+
}: AddOnOptionsPanelProps) {
|
|
18
|
+
if (!addOn.options || Object.keys(addOn.options).length === 0) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
{Object.entries(addOn.options).map(([optionName, option]) => {
|
|
25
|
+
if (option && typeof option === 'object' && 'type' in option && option.type === 'select') {
|
|
26
|
+
return (
|
|
27
|
+
<AddOnOptionSelect
|
|
28
|
+
key={optionName}
|
|
29
|
+
option={option as any}
|
|
30
|
+
value={selectedOptions[optionName] || (option as any).default}
|
|
31
|
+
onChange={(value) => onOptionChange(optionName, value)}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Future option types can be added here
|
|
38
|
+
return null
|
|
39
|
+
})}
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Fragment, useMemo, useState } from 'react'
|
|
2
|
-
import { InfoIcon } from 'lucide-react'
|
|
2
|
+
import { InfoIcon, SettingsIcon } from 'lucide-react'
|
|
3
3
|
|
|
4
4
|
import { Switch } from '../ui/switch'
|
|
5
5
|
import { Label } from '../ui/label'
|
|
6
|
+
import { Button } from '../ui/button'
|
|
6
7
|
|
|
7
|
-
import { useAddOns } from '../../store/project'
|
|
8
|
+
import { useAddOns, useProjectOptions } from '../../store/project'
|
|
8
9
|
|
|
9
10
|
import ImportCustomAddOn from '../custom-add-on-dialog'
|
|
10
11
|
import AddOnInfoDialog from '../add-on-info-dialog'
|
|
12
|
+
import AddOnConfigDialog from '../add-on-config-dialog'
|
|
11
13
|
|
|
12
14
|
import type { AddOnInfo } from '../../types'
|
|
13
15
|
|
|
@@ -19,7 +21,9 @@ const addOnTypeLabels: Record<string, string> = {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export default function SelectedAddOns() {
|
|
22
|
-
const { availableAddOns, addOnState, toggleAddOn } =
|
|
24
|
+
const { availableAddOns, addOnState, toggleAddOn, setAddOnOption } =
|
|
25
|
+
useAddOns()
|
|
26
|
+
const addOnOptions = useProjectOptions((state) => state.addOnOptions)
|
|
23
27
|
|
|
24
28
|
const sortedAddOns = useMemo(() => {
|
|
25
29
|
return availableAddOns.sort((a, b) => {
|
|
@@ -28,6 +32,7 @@ export default function SelectedAddOns() {
|
|
|
28
32
|
}, [availableAddOns])
|
|
29
33
|
|
|
30
34
|
const [infoAddOn, setInfoAddOn] = useState<AddOnInfo>()
|
|
35
|
+
const [configAddOn, setConfigAddOn] = useState<AddOnInfo>()
|
|
31
36
|
|
|
32
37
|
return (
|
|
33
38
|
<>
|
|
@@ -35,58 +40,87 @@ export default function SelectedAddOns() {
|
|
|
35
40
|
addOn={infoAddOn}
|
|
36
41
|
onClose={() => setInfoAddOn(undefined)}
|
|
37
42
|
/>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
43
|
+
<AddOnConfigDialog
|
|
44
|
+
addOn={configAddOn}
|
|
45
|
+
selectedOptions={configAddOn ? addOnOptions[configAddOn.id] || {} : {}}
|
|
46
|
+
onOptionChange={(optionName, value) => {
|
|
47
|
+
if (configAddOn) {
|
|
48
|
+
setAddOnOption(configAddOn.id, optionName, value)
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
onClose={() => setConfigAddOn(undefined)}
|
|
52
|
+
disabled={configAddOn ? !addOnState[configAddOn.id]?.enabled : false}
|
|
53
|
+
/>
|
|
54
|
+
<div className="max-h-[60vh] overflow-y-auto space-y-2">
|
|
55
|
+
{Object.keys(addOnTypeLabels).map((type) => (
|
|
56
|
+
<Fragment key={type}>
|
|
57
|
+
{sortedAddOns.filter((addOn) => addOn.type === type).length > 0 && (
|
|
58
|
+
<div
|
|
59
|
+
key={`${type}-add-ons`}
|
|
60
|
+
className="block p-4 bg-gray-500/10 hover:bg-gray-500/20 rounded-lg transition-colors space-y-4 active"
|
|
61
|
+
>
|
|
62
|
+
<h3 className="font-medium">{addOnTypeLabels[type]}</h3>
|
|
63
|
+
<div className="space-y-3">
|
|
64
|
+
<div className="flex flex-row flex-wrap">
|
|
65
|
+
{sortedAddOns
|
|
66
|
+
.filter((addOn) => addOn.type === type)
|
|
67
|
+
.map((addOn) => (
|
|
68
|
+
<div key={addOn.id} className="w-1/2">
|
|
69
|
+
<div className="flex flex-row items-center justify-between">
|
|
70
|
+
<div className="p-1 flex flex-row items-center">
|
|
71
|
+
<Switch
|
|
72
|
+
id={addOn.id}
|
|
73
|
+
checked={addOnState[addOn.id].selected}
|
|
74
|
+
disabled={!addOnState[addOn.id].enabled}
|
|
75
|
+
onCheckedChange={() => {
|
|
76
|
+
toggleAddOn(addOn.id)
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
<Label
|
|
80
|
+
htmlFor={addOn.id}
|
|
81
|
+
className="pl-2 font-semibold text-gray-300 flex items-center gap-2"
|
|
82
|
+
>
|
|
83
|
+
{addOn.smallLogo && (
|
|
84
|
+
<img
|
|
85
|
+
src={`data:image/svg+xml,${encodeURIComponent(
|
|
86
|
+
addOn.smallLogo,
|
|
87
|
+
)}`}
|
|
88
|
+
alt={addOn.name}
|
|
89
|
+
className="w-5"
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
{addOn.name}
|
|
93
|
+
</Label>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-1">
|
|
96
|
+
{addOnState[addOn.id].selected &&
|
|
97
|
+
addOn.options &&
|
|
98
|
+
Object.keys(addOn.options).length > 0 && (
|
|
99
|
+
<Button
|
|
100
|
+
variant="ghost"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="h-6 w-6 p-0 text-gray-600 hover:text-gray-400"
|
|
103
|
+
onClick={() => setConfigAddOn(addOn)}
|
|
104
|
+
disabled={!addOnState[addOn.id].enabled}
|
|
105
|
+
>
|
|
106
|
+
<SettingsIcon className="w-4 h-4" />
|
|
107
|
+
</Button>
|
|
108
|
+
)}
|
|
109
|
+
<InfoIcon
|
|
110
|
+
className="w-4 text-gray-600 cursor-pointer hover:text-gray-400"
|
|
111
|
+
onClick={() => setInfoAddOn(addOn)}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
85
119
|
</div>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
)}
|
|
121
|
+
</Fragment>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
90
124
|
<div className="mt-4">
|
|
91
125
|
<ImportCustomAddOn />
|
|
92
126
|
</div>
|
package/src/store/project.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const useProjectOptions = create<
|
|
|
23
23
|
tailwind: true,
|
|
24
24
|
git: true,
|
|
25
25
|
chosenAddOns: [],
|
|
26
|
+
addOnOptions: {},
|
|
26
27
|
packageManager: 'pnpm',
|
|
27
28
|
}))
|
|
28
29
|
|
|
@@ -179,11 +180,33 @@ export function useAddOns() {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
},
|
|
182
|
-
[ready, addOnState],
|
|
183
|
+
[ready, addOnState, availableAddOns],
|
|
183
184
|
)
|
|
184
185
|
|
|
186
|
+
const setAddOnOption = useCallback(
|
|
187
|
+
(addOnId: string, optionName: string, value: any) => {
|
|
188
|
+
if (!ready) return
|
|
189
|
+
useProjectOptions.setState((state) => ({
|
|
190
|
+
addOnOptions: {
|
|
191
|
+
...state.addOnOptions,
|
|
192
|
+
[addOnId]: {
|
|
193
|
+
...state.addOnOptions[addOnId],
|
|
194
|
+
[optionName]: value,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}))
|
|
198
|
+
},
|
|
199
|
+
[ready],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
const getAddOnOptions = useCallback((addOnId: string) => {
|
|
203
|
+
return useProjectOptions.getState().addOnOptions[addOnId] || {}
|
|
204
|
+
}, [])
|
|
205
|
+
|
|
185
206
|
return {
|
|
186
207
|
toggleAddOn,
|
|
208
|
+
setAddOnOption,
|
|
209
|
+
getAddOnOptions,
|
|
187
210
|
chosenAddOns,
|
|
188
211
|
availableAddOns,
|
|
189
212
|
userSelectedAddOns,
|
package/src/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StatusStepType } from '@tanstack/cta-engine'
|
|
1
|
+
import type { StatusStepType, AddOnOption, AddOnOptions } from '@tanstack/cta-engine'
|
|
2
2
|
|
|
3
3
|
export type ApplicationMode = 'add' | 'setup' | 'none'
|
|
4
4
|
|
|
@@ -38,6 +38,7 @@ export type AddOnInfo = {
|
|
|
38
38
|
logo?: string
|
|
39
39
|
link: string
|
|
40
40
|
dependsOn?: Array<string>
|
|
41
|
+
options?: AddOnOptions
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export type FileClass =
|