@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.
@@ -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 { useAddOns } from '../../store/project';
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
- return (_jsxs(_Fragment, { children: [_jsx(AddOnInfoDialog, { addOn: infoAddOn, onClose: () => setInfoAddOn(undefined) }), 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: "flex flex-row flex-wrap", children: sortedAddOns
24
- .filter((addOn) => addOn.type === type)
25
- .map((addOn) => (_jsx("div", { className: "w-1/2 flex flex-row justify-between pr-4", 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: () => {
26
- toggleAddOn(addOn.id);
27
- } }), _jsxs(Label, { htmlFor: addOn.id, className: "pl-2 font-semibold text-gray-300", children: [addOn.smallLogo && (_jsx("img", { src: `data:image/svg+xml,${encodeURIComponent(addOn.smallLogo)}`, alt: addOn.name, className: "w-5" })), addOn.name] }), _jsx(InfoIcon, { className: "ml-2 w-4 text-gray-600", onClick: () => setInfoAddOn(addOn) })] }) }, addOn.id))) })] }, `${type}-add-ons`)) }, type))), _jsx("div", { className: "mt-4", children: _jsx(ImportCustomAddOn, {}) })] }));
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
  }
@@ -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[];
@@ -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.28.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.28.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 } = useAddOns()
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
- {Object.keys(addOnTypeLabels).map((type) => (
39
- <Fragment key={type}>
40
- {sortedAddOns.filter((addOn) => addOn.type === type).length > 0 && (
41
- <div
42
- key={`${type}-add-ons`}
43
- className="block p-4 bg-gray-500/10 hover:bg-gray-500/20 rounded-lg transition-colors space-y-4 active"
44
- >
45
- <h3 className="font-medium">{addOnTypeLabels[type]}</h3>
46
- <div className="flex flex-row flex-wrap">
47
- {sortedAddOns
48
- .filter((addOn) => addOn.type === type)
49
- .map((addOn) => (
50
- <div
51
- key={addOn.id}
52
- className="w-1/2 flex flex-row justify-between pr-4"
53
- >
54
- <div className="p-1 flex flex-row items-center">
55
- <Switch
56
- id={addOn.id}
57
- checked={addOnState[addOn.id].selected}
58
- disabled={!addOnState[addOn.id].enabled}
59
- onCheckedChange={() => {
60
- toggleAddOn(addOn.id)
61
- }}
62
- />
63
- <Label
64
- htmlFor={addOn.id}
65
- className="pl-2 font-semibold text-gray-300"
66
- >
67
- {addOn.smallLogo && (
68
- <img
69
- src={`data:image/svg+xml,${encodeURIComponent(
70
- addOn.smallLogo,
71
- )}`}
72
- alt={addOn.name}
73
- className="w-5"
74
- />
75
- )}
76
- {addOn.name}
77
- </Label>
78
- <InfoIcon
79
- className="ml-2 w-4 text-gray-600"
80
- onClick={() => setInfoAddOn(addOn)}
81
- />
82
- </div>
83
- </div>
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
- </div>
87
- )}
88
- </Fragment>
89
- ))}
120
+ )}
121
+ </Fragment>
122
+ ))}
123
+ </div>
90
124
  <div className="mt-4">
91
125
  <ImportCustomAddOn />
92
126
  </div>
@@ -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 =