@contractspec/example.saas-boilerplate 3.7.5 → 3.7.7

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 (115) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/AGENTS.md +50 -27
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +64 -144
  5. package/dist/billing/billing.event.js +1 -1
  6. package/dist/billing/index.d.ts +6 -6
  7. package/dist/billing/index.js +1 -1
  8. package/dist/browser/billing/billing.event.js +1 -1
  9. package/dist/browser/billing/index.js +1 -1
  10. package/dist/browser/index.js +931 -932
  11. package/dist/browser/project/index.js +209 -209
  12. package/dist/browser/project/project.event.js +1 -1
  13. package/dist/browser/ui/SaasDashboard.js +45 -45
  14. package/dist/browser/ui/SaasProjectList.js +7 -7
  15. package/dist/browser/ui/SaasSettingsPanel.js +12 -12
  16. package/dist/browser/ui/hooks/index.js +2 -2
  17. package/dist/browser/ui/hooks/useProjectList.js +1 -1
  18. package/dist/browser/ui/hooks/useProjectMutations.js +1 -1
  19. package/dist/browser/ui/index.js +483 -484
  20. package/dist/browser/ui/modals/CreateProjectModal.js +10 -10
  21. package/dist/browser/ui/modals/ProjectActionsModal.js +13 -13
  22. package/dist/browser/ui/modals/index.js +23 -23
  23. package/dist/browser/ui/renderers/index.js +112 -112
  24. package/dist/browser/ui/renderers/project-list.renderer.js +7 -7
  25. package/dist/handlers/index.d.ts +2 -2
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +931 -932
  28. package/dist/node/billing/billing.event.js +1 -1
  29. package/dist/node/billing/index.js +1 -1
  30. package/dist/node/index.js +931 -932
  31. package/dist/node/project/index.js +209 -209
  32. package/dist/node/project/project.event.js +1 -1
  33. package/dist/node/ui/SaasDashboard.js +45 -45
  34. package/dist/node/ui/SaasProjectList.js +7 -7
  35. package/dist/node/ui/SaasSettingsPanel.js +12 -12
  36. package/dist/node/ui/hooks/index.js +2 -2
  37. package/dist/node/ui/hooks/useProjectList.js +1 -1
  38. package/dist/node/ui/hooks/useProjectMutations.js +1 -1
  39. package/dist/node/ui/index.js +483 -484
  40. package/dist/node/ui/modals/CreateProjectModal.js +10 -10
  41. package/dist/node/ui/modals/ProjectActionsModal.js +13 -13
  42. package/dist/node/ui/modals/index.js +23 -23
  43. package/dist/node/ui/renderers/index.js +112 -112
  44. package/dist/node/ui/renderers/project-list.renderer.js +7 -7
  45. package/dist/presentations/index.d.ts +1 -1
  46. package/dist/project/index.d.ts +7 -7
  47. package/dist/project/index.js +209 -209
  48. package/dist/project/project.event.js +1 -1
  49. package/dist/settings/index.d.ts +1 -1
  50. package/dist/ui/SaasDashboard.js +45 -45
  51. package/dist/ui/SaasProjectList.js +7 -7
  52. package/dist/ui/SaasSettingsPanel.js +12 -12
  53. package/dist/ui/hooks/index.d.ts +2 -2
  54. package/dist/ui/hooks/index.js +2 -2
  55. package/dist/ui/hooks/useProjectList.d.ts +5 -0
  56. package/dist/ui/hooks/useProjectList.js +1 -1
  57. package/dist/ui/hooks/useProjectMutations.d.ts +8 -0
  58. package/dist/ui/hooks/useProjectMutations.js +1 -1
  59. package/dist/ui/index.d.ts +4 -4
  60. package/dist/ui/index.js +483 -484
  61. package/dist/ui/modals/CreateProjectModal.js +10 -10
  62. package/dist/ui/modals/ProjectActionsModal.js +13 -13
  63. package/dist/ui/modals/index.js +23 -23
  64. package/dist/ui/renderers/index.d.ts +1 -1
  65. package/dist/ui/renderers/index.js +112 -112
  66. package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
  67. package/dist/ui/renderers/project-list.renderer.js +7 -7
  68. package/package.json +14 -14
  69. package/src/billing/billing.entity.ts +132 -132
  70. package/src/billing/billing.enum.ts +9 -9
  71. package/src/billing/billing.event.ts +71 -71
  72. package/src/billing/billing.handler.ts +87 -87
  73. package/src/billing/billing.operations.ts +158 -158
  74. package/src/billing/billing.presentation.ts +45 -45
  75. package/src/billing/billing.schema.ts +76 -76
  76. package/src/billing/index.ts +43 -48
  77. package/src/dashboard/dashboard.presentation.ts +45 -45
  78. package/src/dashboard/index.ts +2 -2
  79. package/src/docs/saas-boilerplate.docblock.ts +43 -43
  80. package/src/example.ts +32 -32
  81. package/src/handlers/index.ts +9 -9
  82. package/src/handlers/saas.handlers.ts +250 -249
  83. package/src/index.ts +40 -41
  84. package/src/presentations/index.ts +18 -20
  85. package/src/project/index.ts +45 -50
  86. package/src/project/project.entity.ts +68 -68
  87. package/src/project/project.enum.ts +8 -8
  88. package/src/project/project.event.ts +79 -79
  89. package/src/project/project.handler.ts +103 -103
  90. package/src/project/project.operations.ts +236 -236
  91. package/src/project/project.presentation.ts +46 -46
  92. package/src/project/project.schema.ts +90 -90
  93. package/src/saas-boilerplate.feature.ts +100 -100
  94. package/src/seeders/index.ts +20 -20
  95. package/src/settings/index.ts +2 -3
  96. package/src/settings/settings.entity.ts +65 -65
  97. package/src/settings/settings.enum.ts +4 -4
  98. package/src/shared/mock-data.ts +92 -92
  99. package/src/shared/overlay-types.ts +23 -23
  100. package/src/tests/operations.test-spec.ts +96 -96
  101. package/src/ui/SaasDashboard.tsx +270 -270
  102. package/src/ui/SaasProjectList.tsx +90 -90
  103. package/src/ui/SaasSettingsPanel.tsx +84 -84
  104. package/src/ui/hooks/index.ts +3 -3
  105. package/src/ui/hooks/useProjectList.ts +69 -68
  106. package/src/ui/hooks/useProjectMutations.ts +144 -143
  107. package/src/ui/index.ts +8 -12
  108. package/src/ui/modals/CreateProjectModal.tsx +154 -154
  109. package/src/ui/modals/ProjectActionsModal.tsx +321 -321
  110. package/src/ui/overlays/demo-overlays.ts +49 -49
  111. package/src/ui/renderers/index.ts +5 -4
  112. package/src/ui/renderers/project-list.markdown.ts +204 -204
  113. package/src/ui/renderers/project-list.renderer.tsx +14 -13
  114. package/tsconfig.json +7 -8
  115. package/tsdown.config.js +7 -3
@@ -6,160 +6,161 @@
6
6
  * - UpdateProjectContract
7
7
  * - DeleteProjectContract
8
8
  */
9
- import { useCallback, useState } from 'react';
9
+
10
10
  import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
11
+ import { useCallback, useState } from 'react';
11
12
  import type {
12
- CreateProjectInput,
13
- Project,
14
- UpdateProjectInput,
15
- SaasHandlers,
13
+ CreateProjectInput,
14
+ Project,
15
+ SaasHandlers,
16
+ UpdateProjectInput,
16
17
  } from '../../handlers/saas.handlers';
17
18
 
18
19
  export interface MutationState<T> {
19
- loading: boolean;
20
- error: Error | null;
21
- data: T | null;
20
+ loading: boolean;
21
+ error: Error | null;
22
+ data: T | null;
22
23
  }
23
24
 
24
25
  export interface UseProjectMutationsOptions {
25
- onSuccess?: () => void;
26
- onError?: (error: Error) => void;
26
+ onSuccess?: () => void;
27
+ onError?: (error: Error) => void;
27
28
  }
28
29
 
29
30
  export function useProjectMutations(options: UseProjectMutationsOptions = {}) {
30
- const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
31
- const { saas } = handlers;
32
-
33
- const [createState, setCreateState] = useState<MutationState<Project>>({
34
- loading: false,
35
- error: null,
36
- data: null,
37
- });
38
-
39
- const [updateState, setUpdateState] = useState<MutationState<Project>>({
40
- loading: false,
41
- error: null,
42
- data: null,
43
- });
44
-
45
- const [deleteState, setDeleteState] = useState<
46
- MutationState<{ success: boolean }>
47
- >({
48
- loading: false,
49
- error: null,
50
- data: null,
51
- });
52
-
53
- /**
54
- * Create a new project
55
- */
56
- const createProject = useCallback(
57
- async (input: CreateProjectInput): Promise<Project | null> => {
58
- setCreateState({ loading: true, error: null, data: null });
59
- try {
60
- const result = await saas.createProject(input, {
61
- projectId,
62
- organizationId: 'demo-org',
63
- });
64
- setCreateState({ loading: false, error: null, data: result });
65
- options.onSuccess?.();
66
- return result;
67
- } catch (err) {
68
- const error =
69
- err instanceof Error ? err : new Error('Failed to create project');
70
- setCreateState({ loading: false, error, data: null });
71
- options.onError?.(error);
72
- return null;
73
- }
74
- },
75
- [saas, projectId, options]
76
- );
77
-
78
- /**
79
- * Update a project
80
- */
81
- const updateProject = useCallback(
82
- async (input: UpdateProjectInput): Promise<Project | null> => {
83
- setUpdateState({ loading: true, error: null, data: null });
84
- try {
85
- const result = await saas.updateProject(input);
86
- setUpdateState({ loading: false, error: null, data: result });
87
- options.onSuccess?.();
88
- return result;
89
- } catch (err) {
90
- const error =
91
- err instanceof Error ? err : new Error('Failed to update project');
92
- setUpdateState({ loading: false, error, data: null });
93
- options.onError?.(error);
94
- return null;
95
- }
96
- },
97
- [saas, options]
98
- );
99
-
100
- /**
101
- * Delete a project (soft delete)
102
- */
103
- const deleteProject = useCallback(
104
- async (id: string): Promise<boolean> => {
105
- setDeleteState({ loading: true, error: null, data: null });
106
- try {
107
- await saas.deleteProject(id);
108
- setDeleteState({
109
- loading: false,
110
- error: null,
111
- data: { success: true },
112
- });
113
- options.onSuccess?.();
114
- return true;
115
- } catch (err) {
116
- const error =
117
- err instanceof Error ? err : new Error('Failed to delete project');
118
- setDeleteState({ loading: false, error, data: null });
119
- options.onError?.(error);
120
- return false;
121
- }
122
- },
123
- [saas, options]
124
- );
125
-
126
- /**
127
- * Archive a project (status change)
128
- */
129
- const archiveProject = useCallback(
130
- async (id: string): Promise<Project | null> => {
131
- return updateProject({ id, status: 'ARCHIVED' });
132
- },
133
- [updateProject]
134
- );
135
-
136
- /**
137
- * Activate a project (status change)
138
- */
139
- const activateProject = useCallback(
140
- async (id: string): Promise<Project | null> => {
141
- return updateProject({ id, status: 'ACTIVE' });
142
- },
143
- [updateProject]
144
- );
145
-
146
- return {
147
- // Mutations
148
- createProject,
149
- updateProject,
150
- deleteProject,
151
- archiveProject,
152
- activateProject,
153
-
154
- // State
155
- createState,
156
- updateState,
157
- deleteState,
158
-
159
- // Convenience
160
- isLoading:
161
- createState.loading || updateState.loading || deleteState.loading,
162
- };
31
+ const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
32
+ const { saas } = handlers;
33
+
34
+ const [createState, setCreateState] = useState<MutationState<Project>>({
35
+ loading: false,
36
+ error: null,
37
+ data: null,
38
+ });
39
+
40
+ const [updateState, setUpdateState] = useState<MutationState<Project>>({
41
+ loading: false,
42
+ error: null,
43
+ data: null,
44
+ });
45
+
46
+ const [deleteState, setDeleteState] = useState<
47
+ MutationState<{ success: boolean }>
48
+ >({
49
+ loading: false,
50
+ error: null,
51
+ data: null,
52
+ });
53
+
54
+ /**
55
+ * Create a new project
56
+ */
57
+ const createProject = useCallback(
58
+ async (input: CreateProjectInput): Promise<Project | null> => {
59
+ setCreateState({ loading: true, error: null, data: null });
60
+ try {
61
+ const result = await saas.createProject(input, {
62
+ projectId,
63
+ organizationId: 'demo-org',
64
+ });
65
+ setCreateState({ loading: false, error: null, data: result });
66
+ options.onSuccess?.();
67
+ return result;
68
+ } catch (err) {
69
+ const error =
70
+ err instanceof Error ? err : new Error('Failed to create project');
71
+ setCreateState({ loading: false, error, data: null });
72
+ options.onError?.(error);
73
+ return null;
74
+ }
75
+ },
76
+ [saas, projectId, options]
77
+ );
78
+
79
+ /**
80
+ * Update a project
81
+ */
82
+ const updateProject = useCallback(
83
+ async (input: UpdateProjectInput): Promise<Project | null> => {
84
+ setUpdateState({ loading: true, error: null, data: null });
85
+ try {
86
+ const result = await saas.updateProject(input);
87
+ setUpdateState({ loading: false, error: null, data: result });
88
+ options.onSuccess?.();
89
+ return result;
90
+ } catch (err) {
91
+ const error =
92
+ err instanceof Error ? err : new Error('Failed to update project');
93
+ setUpdateState({ loading: false, error, data: null });
94
+ options.onError?.(error);
95
+ return null;
96
+ }
97
+ },
98
+ [saas, options]
99
+ );
100
+
101
+ /**
102
+ * Delete a project (soft delete)
103
+ */
104
+ const deleteProject = useCallback(
105
+ async (id: string): Promise<boolean> => {
106
+ setDeleteState({ loading: true, error: null, data: null });
107
+ try {
108
+ await saas.deleteProject(id);
109
+ setDeleteState({
110
+ loading: false,
111
+ error: null,
112
+ data: { success: true },
113
+ });
114
+ options.onSuccess?.();
115
+ return true;
116
+ } catch (err) {
117
+ const error =
118
+ err instanceof Error ? err : new Error('Failed to delete project');
119
+ setDeleteState({ loading: false, error, data: null });
120
+ options.onError?.(error);
121
+ return false;
122
+ }
123
+ },
124
+ [saas, options]
125
+ );
126
+
127
+ /**
128
+ * Archive a project (status change)
129
+ */
130
+ const archiveProject = useCallback(
131
+ async (id: string): Promise<Project | null> => {
132
+ return updateProject({ id, status: 'ARCHIVED' });
133
+ },
134
+ [updateProject]
135
+ );
136
+
137
+ /**
138
+ * Activate a project (status change)
139
+ */
140
+ const activateProject = useCallback(
141
+ async (id: string): Promise<Project | null> => {
142
+ return updateProject({ id, status: 'ACTIVE' });
143
+ },
144
+ [updateProject]
145
+ );
146
+
147
+ return {
148
+ // Mutations
149
+ createProject,
150
+ updateProject,
151
+ deleteProject,
152
+ archiveProject,
153
+ activateProject,
154
+
155
+ // State
156
+ createState,
157
+ updateState,
158
+ deleteState,
159
+
160
+ // Convenience
161
+ isLoading:
162
+ createState.loading || updateState.loading || deleteState.loading,
163
+ };
163
164
  }
164
165
 
165
166
  // Note: Types are re-exported from the handlers package
package/src/ui/index.ts CHANGED
@@ -1,18 +1,14 @@
1
1
  // Main dashboard
2
- export * from './SaasDashboard';
3
-
4
- // Standalone components
5
- export * from './SaasProjectList';
6
- export * from './SaasSettingsPanel';
7
-
8
- // Modals
9
- export * from './modals';
10
2
 
11
3
  // Hooks
12
4
  export * from './hooks';
13
-
14
- // Renderers
15
- export * from './renderers';
16
-
5
+ // Modals
6
+ export * from './modals';
17
7
  // Overlays
18
8
  export * from './overlays';
9
+ // Renderers
10
+ export * from './renderers';
11
+ export * from './SaasDashboard';
12
+ // Standalone components
13
+ export * from './SaasProjectList';
14
+ export * from './SaasSettingsPanel';
@@ -1,176 +1,176 @@
1
1
  'use client';
2
2
 
3
+ import { Button, Input } from '@contractspec/lib.design-system';
3
4
  /**
4
5
  * CreateProjectModal - Form for creating a new project
5
6
  *
6
7
  * Wires to CreateProjectContract via useProjectMutations hook.
7
8
  */
8
9
  import { useState } from 'react';
9
- import { Button, Input } from '@contractspec/lib.design-system';
10
10
 
11
11
  // Local type definition for modal props
12
12
  export interface CreateProjectInput {
13
- name: string;
14
- description?: string;
15
- tier: 'FREE' | 'PRO' | 'ENTERPRISE';
13
+ name: string;
14
+ description?: string;
15
+ tier: 'FREE' | 'PRO' | 'ENTERPRISE';
16
16
  }
17
17
 
18
18
  interface CreateProjectModalProps {
19
- isOpen: boolean;
20
- onClose: () => void;
21
- onSubmit: (input: CreateProjectInput) => Promise<void>;
22
- isLoading?: boolean;
19
+ isOpen: boolean;
20
+ onClose: () => void;
21
+ onSubmit: (input: CreateProjectInput) => Promise<void>;
22
+ isLoading?: boolean;
23
23
  }
24
24
 
25
25
  const TIERS: { value: CreateProjectInput['tier']; label: string }[] = [
26
- { value: 'FREE', label: 'Free' },
27
- { value: 'PRO', label: 'Pro' },
28
- { value: 'ENTERPRISE', label: 'Enterprise' },
26
+ { value: 'FREE', label: 'Free' },
27
+ { value: 'PRO', label: 'Pro' },
28
+ { value: 'ENTERPRISE', label: 'Enterprise' },
29
29
  ];
30
30
 
31
31
  export function CreateProjectModal({
32
- isOpen,
33
- onClose,
34
- onSubmit,
35
- isLoading = false,
32
+ isOpen,
33
+ onClose,
34
+ onSubmit,
35
+ isLoading = false,
36
36
  }: CreateProjectModalProps) {
37
- const [name, setName] = useState('');
38
- const [description, setDescription] = useState('');
39
- const [tier, setTier] = useState<CreateProjectInput['tier']>('FREE');
40
- const [error, setError] = useState<string | null>(null);
41
-
42
- const handleSubmit = async (e: React.FormEvent) => {
43
- e.preventDefault();
44
- setError(null);
45
-
46
- // Validation
47
- if (!name.trim()) {
48
- setError('Project name is required');
49
- return;
50
- }
51
-
52
- try {
53
- await onSubmit({
54
- name: name.trim(),
55
- description: description.trim() || undefined,
56
- tier,
57
- });
58
-
59
- // Reset form
60
- setName('');
61
- setDescription('');
62
- setTier('FREE');
63
- onClose();
64
- } catch (err) {
65
- setError(err instanceof Error ? err.message : 'Failed to create project');
66
- }
67
- };
68
-
69
- if (!isOpen) return null;
70
-
71
- return (
72
- <div className="fixed inset-0 z-50 flex items-center justify-center">
73
- {/* Backdrop */}
74
- <div
75
- className="bg-background/80 absolute inset-0 backdrop-blur-sm"
76
- onClick={onClose}
77
- role="button"
78
- tabIndex={0}
79
- onKeyDown={(e) => {
80
- if (e.key === 'Enter' || e.key === ' ') onClose();
81
- }}
82
- aria-label="Close modal"
83
- />
84
-
85
- {/* Modal */}
86
- <div className="bg-card border-border relative z-10 w-full max-w-md rounded-xl border p-6 shadow-xl">
87
- <h2 className="mb-4 text-xl font-semibold">Create New Project</h2>
88
-
89
- <form onSubmit={handleSubmit} className="space-y-4">
90
- {/* Project Name */}
91
- <div>
92
- <label
93
- htmlFor="project-name"
94
- className="text-muted-foreground mb-1 block text-sm font-medium"
95
- >
96
- Project Name *
97
- </label>
98
- <Input
99
- id="project-name"
100
- value={name}
101
- onChange={(e) => setName(e.target.value)}
102
- placeholder="e.g., My Awesome Project"
103
- disabled={isLoading}
104
- />
105
- </div>
106
-
107
- {/* Description */}
108
- <div>
109
- <label
110
- htmlFor="project-description"
111
- className="text-muted-foreground mb-1 block text-sm font-medium"
112
- >
113
- Description
114
- </label>
115
- <textarea
116
- id="project-description"
117
- value={description}
118
- onChange={(e) => setDescription(e.target.value)}
119
- placeholder="Describe what this project is about..."
120
- rows={3}
121
- disabled={isLoading}
122
- className="border-input bg-background focus:ring-ring w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50"
123
- />
124
- </div>
125
-
126
- {/* Tier */}
127
- <div>
128
- <label
129
- htmlFor="project-tier"
130
- className="text-muted-foreground mb-1 block text-sm font-medium"
131
- >
132
- Tier
133
- </label>
134
- <select
135
- id="project-tier"
136
- value={tier}
137
- onChange={(e) =>
138
- setTier(e.target.value as CreateProjectInput['tier'])
139
- }
140
- disabled={isLoading}
141
- className="border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50"
142
- >
143
- {TIERS.map((t) => (
144
- <option key={t.value} value={t.value}>
145
- {t.label}
146
- </option>
147
- ))}
148
- </select>
149
- </div>
150
-
151
- {/* Error Message */}
152
- {error && (
153
- <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
154
- {error}
155
- </div>
156
- )}
157
-
158
- {/* Actions */}
159
- <div className="flex justify-end gap-3 pt-2">
160
- <Button
161
- type="button"
162
- variant="ghost"
163
- onPress={onClose}
164
- disabled={isLoading}
165
- >
166
- Cancel
167
- </Button>
168
- <Button type="submit" disabled={isLoading}>
169
- {isLoading ? 'Creating...' : 'Create Project'}
170
- </Button>
171
- </div>
172
- </form>
173
- </div>
174
- </div>
175
- );
37
+ const [name, setName] = useState('');
38
+ const [description, setDescription] = useState('');
39
+ const [tier, setTier] = useState<CreateProjectInput['tier']>('FREE');
40
+ const [error, setError] = useState<string | null>(null);
41
+
42
+ const handleSubmit = async (e: React.FormEvent) => {
43
+ e.preventDefault();
44
+ setError(null);
45
+
46
+ // Validation
47
+ if (!name.trim()) {
48
+ setError('Project name is required');
49
+ return;
50
+ }
51
+
52
+ try {
53
+ await onSubmit({
54
+ name: name.trim(),
55
+ description: description.trim() || undefined,
56
+ tier,
57
+ });
58
+
59
+ // Reset form
60
+ setName('');
61
+ setDescription('');
62
+ setTier('FREE');
63
+ onClose();
64
+ } catch (err) {
65
+ setError(err instanceof Error ? err.message : 'Failed to create project');
66
+ }
67
+ };
68
+
69
+ if (!isOpen) return null;
70
+
71
+ return (
72
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
73
+ {/* Backdrop */}
74
+ <div
75
+ className="absolute inset-0 bg-background/80 backdrop-blur-sm"
76
+ onClick={onClose}
77
+ role="button"
78
+ tabIndex={0}
79
+ onKeyDown={(e) => {
80
+ if (e.key === 'Enter' || e.key === ' ') onClose();
81
+ }}
82
+ aria-label="Close modal"
83
+ />
84
+
85
+ {/* Modal */}
86
+ <div className="relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl">
87
+ <h2 className="mb-4 font-semibold text-xl">Create New Project</h2>
88
+
89
+ <form onSubmit={handleSubmit} className="space-y-4">
90
+ {/* Project Name */}
91
+ <div>
92
+ <label
93
+ htmlFor="project-name"
94
+ className="mb-1 block font-medium text-muted-foreground text-sm"
95
+ >
96
+ Project Name *
97
+ </label>
98
+ <Input
99
+ id="project-name"
100
+ value={name}
101
+ onChange={(e) => setName(e.target.value)}
102
+ placeholder="e.g., My Awesome Project"
103
+ disabled={isLoading}
104
+ />
105
+ </div>
106
+
107
+ {/* Description */}
108
+ <div>
109
+ <label
110
+ htmlFor="project-description"
111
+ className="mb-1 block font-medium text-muted-foreground text-sm"
112
+ >
113
+ Description
114
+ </label>
115
+ <textarea
116
+ id="project-description"
117
+ value={description}
118
+ onChange={(e) => setDescription(e.target.value)}
119
+ placeholder="Describe what this project is about..."
120
+ rows={3}
121
+ disabled={isLoading}
122
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
123
+ />
124
+ </div>
125
+
126
+ {/* Tier */}
127
+ <div>
128
+ <label
129
+ htmlFor="project-tier"
130
+ className="mb-1 block font-medium text-muted-foreground text-sm"
131
+ >
132
+ Tier
133
+ </label>
134
+ <select
135
+ id="project-tier"
136
+ value={tier}
137
+ onChange={(e) =>
138
+ setTier(e.target.value as CreateProjectInput['tier'])
139
+ }
140
+ disabled={isLoading}
141
+ className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
142
+ >
143
+ {TIERS.map((t) => (
144
+ <option key={t.value} value={t.value}>
145
+ {t.label}
146
+ </option>
147
+ ))}
148
+ </select>
149
+ </div>
150
+
151
+ {/* Error Message */}
152
+ {error && (
153
+ <div className="rounded-md bg-destructive/10 p-3 text-destructive text-sm">
154
+ {error}
155
+ </div>
156
+ )}
157
+
158
+ {/* Actions */}
159
+ <div className="flex justify-end gap-3 pt-2">
160
+ <Button
161
+ type="button"
162
+ variant="ghost"
163
+ onPress={onClose}
164
+ disabled={isLoading}
165
+ >
166
+ Cancel
167
+ </Button>
168
+ <Button type="submit" disabled={isLoading}>
169
+ {isLoading ? 'Creating...' : 'Create Project'}
170
+ </Button>
171
+ </div>
172
+ </form>
173
+ </div>
174
+ </div>
175
+ );
176
176
  }