@contractspec/example.saas-boilerplate 1.46.0 → 1.47.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 (131) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +183 -108
  2. package/.turbo/turbo-build.log +182 -107
  3. package/CHANGELOG.md +58 -0
  4. package/README.md +0 -1
  5. package/dist/billing/billing.event.d.ts +4 -4
  6. package/dist/billing/billing.event.js +1 -1
  7. package/dist/billing/billing.operations.d.ts +5 -5
  8. package/dist/billing/billing.presentation.d.ts +3 -4
  9. package/dist/billing/billing.presentation.d.ts.map +1 -1
  10. package/dist/billing/billing.presentation.js +5 -5
  11. package/dist/billing/billing.presentation.js.map +1 -1
  12. package/dist/dashboard/dashboard.presentation.d.ts +3 -4
  13. package/dist/dashboard/dashboard.presentation.d.ts.map +1 -1
  14. package/dist/dashboard/dashboard.presentation.js +5 -5
  15. package/dist/dashboard/dashboard.presentation.js.map +1 -1
  16. package/dist/example.d.ts +2 -2
  17. package/dist/example.d.ts.map +1 -1
  18. package/dist/example.js +4 -2
  19. package/dist/example.js.map +1 -1
  20. package/dist/handlers/index.d.ts +2 -1
  21. package/dist/handlers/index.js +2 -1
  22. package/dist/handlers/saas.handlers.d.ts +68 -0
  23. package/dist/handlers/saas.handlers.d.ts.map +1 -0
  24. package/dist/handlers/saas.handlers.js +148 -0
  25. package/dist/handlers/saas.handlers.js.map +1 -0
  26. package/dist/index.d.ts +13 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +13 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/project/project.event.d.ts +5 -5
  31. package/dist/project/project.event.d.ts.map +1 -1
  32. package/dist/project/project.event.js +1 -1
  33. package/dist/project/project.operations.d.ts +6 -6
  34. package/dist/project/project.presentation.d.ts +3 -4
  35. package/dist/project/project.presentation.d.ts.map +1 -1
  36. package/dist/project/project.presentation.js +5 -5
  37. package/dist/project/project.presentation.js.map +1 -1
  38. package/dist/saas-boilerplate.feature.d.ts +2 -2
  39. package/dist/saas-boilerplate.feature.d.ts.map +1 -1
  40. package/dist/saas-boilerplate.feature.js +9 -2
  41. package/dist/saas-boilerplate.feature.js.map +1 -1
  42. package/dist/seeders/index.d.ts +10 -0
  43. package/dist/seeders/index.d.ts.map +1 -0
  44. package/dist/seeders/index.js +19 -0
  45. package/dist/seeders/index.js.map +1 -0
  46. package/dist/shared/overlay-types.d.ts +34 -0
  47. package/dist/shared/overlay-types.d.ts.map +1 -0
  48. package/dist/shared/overlay-types.js +0 -0
  49. package/dist/tests/operations.test-spec.d.ts +10 -0
  50. package/dist/tests/operations.test-spec.d.ts.map +1 -0
  51. package/dist/tests/operations.test-spec.js +123 -0
  52. package/dist/tests/operations.test-spec.js.map +1 -0
  53. package/dist/ui/SaasDashboard.d.ts +7 -0
  54. package/dist/ui/SaasDashboard.d.ts.map +1 -0
  55. package/dist/ui/SaasDashboard.js +298 -0
  56. package/dist/ui/SaasDashboard.js.map +1 -0
  57. package/dist/ui/SaasProjectList.d.ts +14 -0
  58. package/dist/ui/SaasProjectList.d.ts.map +1 -0
  59. package/dist/ui/SaasProjectList.js +76 -0
  60. package/dist/ui/SaasProjectList.js.map +1 -0
  61. package/dist/ui/SaasSettingsPanel.d.ts +7 -0
  62. package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
  63. package/dist/ui/SaasSettingsPanel.js +138 -0
  64. package/dist/ui/SaasSettingsPanel.js.map +1 -0
  65. package/dist/ui/hooks/index.d.ts +3 -0
  66. package/dist/ui/hooks/index.js +6 -0
  67. package/dist/ui/hooks/useProjectList.d.ts +34 -0
  68. package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
  69. package/dist/ui/hooks/useProjectList.js +75 -0
  70. package/dist/ui/hooks/useProjectList.js.map +1 -0
  71. package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
  72. package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
  73. package/dist/ui/hooks/useProjectMutations.js +146 -0
  74. package/dist/ui/hooks/useProjectMutations.js.map +1 -0
  75. package/dist/ui/index.d.ts +14 -0
  76. package/dist/ui/index.js +15 -0
  77. package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
  78. package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
  79. package/dist/ui/modals/CreateProjectModal.js +139 -0
  80. package/dist/ui/modals/CreateProjectModal.js.map +1 -0
  81. package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
  82. package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
  83. package/dist/ui/modals/ProjectActionsModal.js +292 -0
  84. package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
  85. package/dist/ui/modals/index.d.ts +3 -0
  86. package/dist/ui/modals/index.js +4 -0
  87. package/dist/ui/overlays/demo-overlays.d.ts +19 -0
  88. package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
  89. package/dist/ui/overlays/demo-overlays.js +70 -0
  90. package/dist/ui/overlays/demo-overlays.js.map +1 -0
  91. package/dist/ui/overlays/index.d.ts +2 -0
  92. package/dist/ui/overlays/index.js +3 -0
  93. package/dist/ui/renderers/index.d.ts +3 -0
  94. package/dist/ui/renderers/index.js +4 -0
  95. package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
  96. package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
  97. package/dist/ui/renderers/project-list.markdown.js +148 -0
  98. package/dist/ui/renderers/project-list.markdown.js.map +1 -0
  99. package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
  100. package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
  101. package/dist/ui/renderers/project-list.renderer.js +17 -0
  102. package/dist/ui/renderers/project-list.renderer.js.map +1 -0
  103. package/package.json +38 -14
  104. package/src/billing/billing.presentation.ts +5 -6
  105. package/src/dashboard/dashboard.presentation.ts +5 -6
  106. package/src/example.ts +3 -3
  107. package/src/handlers/index.ts +3 -0
  108. package/src/handlers/saas.handlers.ts +300 -0
  109. package/src/index.ts +5 -0
  110. package/src/project/project.presentation.ts +5 -6
  111. package/src/saas-boilerplate.feature.ts +3 -3
  112. package/src/seeders/index.ts +28 -0
  113. package/src/shared/overlay-types.ts +39 -0
  114. package/src/tests/operations.test-spec.ts +109 -0
  115. package/src/ui/SaasDashboard.tsx +325 -0
  116. package/src/ui/SaasProjectList.tsx +113 -0
  117. package/src/ui/SaasSettingsPanel.tsx +96 -0
  118. package/src/ui/hooks/index.ts +10 -0
  119. package/src/ui/hooks/useProjectList.ts +95 -0
  120. package/src/ui/hooks/useProjectMutations.ts +166 -0
  121. package/src/ui/index.ts +18 -0
  122. package/src/ui/modals/CreateProjectModal.tsx +176 -0
  123. package/src/ui/modals/ProjectActionsModal.tsx +346 -0
  124. package/src/ui/modals/index.ts +2 -0
  125. package/src/ui/overlays/demo-overlays.ts +74 -0
  126. package/src/ui/overlays/index.ts +1 -0
  127. package/src/ui/renderers/index.ts +7 -0
  128. package/src/ui/renderers/project-list.markdown.ts +239 -0
  129. package/src/ui/renderers/project-list.renderer.tsx +22 -0
  130. package/tsconfig.json +1 -1
  131. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Hook for SaaS project mutations (commands)
3
+ *
4
+ * Uses runtime-local database-backed handlers for:
5
+ * - CreateProjectContract
6
+ * - UpdateProjectContract
7
+ * - DeleteProjectContract
8
+ */
9
+ import { useCallback, useState } from 'react';
10
+ import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
11
+ import type {
12
+ CreateProjectInput,
13
+ Project,
14
+ UpdateProjectInput,
15
+ SaasHandlers,
16
+ } from '../../handlers/saas.handlers';
17
+
18
+ export interface MutationState<T> {
19
+ loading: boolean;
20
+ error: Error | null;
21
+ data: T | null;
22
+ }
23
+
24
+ export interface UseProjectMutationsOptions {
25
+ onSuccess?: () => void;
26
+ onError?: (error: Error) => void;
27
+ }
28
+
29
+ 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
+ };
163
+ }
164
+
165
+ // Note: Types are re-exported from the handlers package
166
+ // Consumers should import types directly from '@contractspec/example.saas-boilerplate/handlers'
@@ -0,0 +1,18 @@
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
+
11
+ // Hooks
12
+ export * from './hooks';
13
+
14
+ // Renderers
15
+ export * from './renderers';
16
+
17
+ // Overlays
18
+ export * from './overlays';
@@ -0,0 +1,176 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * CreateProjectModal - Form for creating a new project
5
+ *
6
+ * Wires to CreateProjectContract via useProjectMutations hook.
7
+ */
8
+ import { useState } from 'react';
9
+ import { Button, Input } from '@contractspec/lib.design-system';
10
+
11
+ // Local type definition for modal props
12
+ export interface CreateProjectInput {
13
+ name: string;
14
+ description?: string;
15
+ tier: 'FREE' | 'PRO' | 'ENTERPRISE';
16
+ }
17
+
18
+ interface CreateProjectModalProps {
19
+ isOpen: boolean;
20
+ onClose: () => void;
21
+ onSubmit: (input: CreateProjectInput) => Promise<void>;
22
+ isLoading?: boolean;
23
+ }
24
+
25
+ const TIERS: { value: CreateProjectInput['tier']; label: string }[] = [
26
+ { value: 'FREE', label: 'Free' },
27
+ { value: 'PRO', label: 'Pro' },
28
+ { value: 'ENTERPRISE', label: 'Enterprise' },
29
+ ];
30
+
31
+ export function CreateProjectModal({
32
+ isOpen,
33
+ onClose,
34
+ onSubmit,
35
+ isLoading = false,
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
+ );
176
+ }
@@ -0,0 +1,346 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ProjectActionsModal - Actions for a specific project
5
+ *
6
+ * Wires to UpdateProjectContract, DeleteProjectContract
7
+ * via useProjectMutations hook.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+ import { Button, Input } from '@contractspec/lib.design-system';
11
+
12
+ // Local type definitions for modal props
13
+ export interface Project {
14
+ id: string;
15
+ name: string;
16
+ description?: string;
17
+ status: 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
18
+ tier: 'FREE' | 'PRO' | 'ENTERPRISE';
19
+ }
20
+
21
+ export interface UpdateProjectInput {
22
+ id: string;
23
+ name?: string;
24
+ description?: string;
25
+ }
26
+
27
+ type ActionMode = 'menu' | 'edit' | 'archive' | 'delete';
28
+
29
+ interface ProjectActionsModalProps {
30
+ isOpen: boolean;
31
+ project: Project | null;
32
+ onClose: () => void;
33
+ onUpdate: (input: UpdateProjectInput) => Promise<void>;
34
+ onArchive: (projectId: string) => Promise<void>;
35
+ onActivate: (projectId: string) => Promise<void>;
36
+ onDelete: (projectId: string) => Promise<void>;
37
+ isLoading?: boolean;
38
+ }
39
+
40
+ export function ProjectActionsModal({
41
+ isOpen,
42
+ project,
43
+ onClose,
44
+ onUpdate,
45
+ onArchive,
46
+ onActivate,
47
+ onDelete,
48
+ isLoading = false,
49
+ }: ProjectActionsModalProps) {
50
+ const [mode, setMode] = useState<ActionMode>('menu');
51
+ const [name, setName] = useState('');
52
+ const [description, setDescription] = useState('');
53
+ const [error, setError] = useState<string | null>(null);
54
+
55
+ const resetForm = () => {
56
+ setMode('menu');
57
+ setError(null);
58
+ if (project) {
59
+ setName(project.name);
60
+ setDescription(project.description ?? '');
61
+ }
62
+ };
63
+
64
+ const handleClose = () => {
65
+ resetForm();
66
+ onClose();
67
+ };
68
+
69
+ // Initialize form when project changes
70
+ useEffect(() => {
71
+ if (project) {
72
+ setName(project.name);
73
+ setDescription(project.description ?? '');
74
+ }
75
+ }, [project]);
76
+
77
+ const handleEdit = async () => {
78
+ if (!project) return;
79
+ setError(null);
80
+
81
+ if (!name.trim()) {
82
+ setError('Project name is required');
83
+ return;
84
+ }
85
+
86
+ try {
87
+ await onUpdate({
88
+ id: project.id,
89
+ name: name.trim(),
90
+ description: description.trim() || undefined,
91
+ });
92
+ handleClose();
93
+ } catch (err) {
94
+ setError(err instanceof Error ? err.message : 'Failed to update project');
95
+ }
96
+ };
97
+
98
+ const handleArchive = async () => {
99
+ if (!project) return;
100
+ setError(null);
101
+
102
+ try {
103
+ await onArchive(project.id);
104
+ handleClose();
105
+ } catch (err) {
106
+ setError(
107
+ err instanceof Error ? err.message : 'Failed to archive project'
108
+ );
109
+ }
110
+ };
111
+
112
+ const handleActivate = async () => {
113
+ if (!project) return;
114
+ setError(null);
115
+
116
+ try {
117
+ await onActivate(project.id);
118
+ handleClose();
119
+ } catch (err) {
120
+ setError(
121
+ err instanceof Error ? err.message : 'Failed to activate project'
122
+ );
123
+ }
124
+ };
125
+
126
+ const handleDelete = async () => {
127
+ if (!project) return;
128
+ setError(null);
129
+
130
+ try {
131
+ await onDelete(project.id);
132
+ handleClose();
133
+ } catch (err) {
134
+ setError(err instanceof Error ? err.message : 'Failed to delete project');
135
+ }
136
+ };
137
+
138
+ if (!isOpen || !project) return null;
139
+
140
+ return (
141
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
142
+ {/* Backdrop */}
143
+ <div
144
+ className="bg-background/80 absolute inset-0 backdrop-blur-sm"
145
+ onClick={handleClose}
146
+ role="button"
147
+ tabIndex={0}
148
+ onKeyDown={(e) => {
149
+ if (e.key === 'Enter' || e.key === ' ') handleClose();
150
+ }}
151
+ aria-label="Close modal"
152
+ />
153
+
154
+ {/* Modal */}
155
+ <div className="bg-card border-border relative z-10 w-full max-w-md rounded-xl border p-6 shadow-xl">
156
+ {/* Project Header */}
157
+ <div className="border-border mb-4 border-b pb-4">
158
+ <h2 className="text-xl font-semibold">{project.name}</h2>
159
+ <p className="text-muted-foreground text-sm">
160
+ {project.tier} · {project.status}
161
+ </p>
162
+ </div>
163
+
164
+ {/* Main Menu */}
165
+ {mode === 'menu' && (
166
+ <div className="space-y-3">
167
+ <Button
168
+ className="w-full justify-start"
169
+ variant="ghost"
170
+ onPress={() => setMode('edit')}
171
+ >
172
+ <span className="mr-2">✏️</span> Edit Project
173
+ </Button>
174
+
175
+ {project.status === 'ACTIVE' || project.status === 'DRAFT' ? (
176
+ <Button
177
+ className="w-full justify-start"
178
+ variant="ghost"
179
+ onPress={() => setMode('archive')}
180
+ >
181
+ <span className="mr-2">📦</span> Archive Project
182
+ </Button>
183
+ ) : project.status === 'ARCHIVED' ? (
184
+ <Button
185
+ className="w-full justify-start"
186
+ variant="ghost"
187
+ onPress={handleActivate}
188
+ disabled={isLoading}
189
+ >
190
+ <span className="mr-2">🔄</span> Restore Project
191
+ </Button>
192
+ ) : null}
193
+
194
+ <Button
195
+ className="w-full justify-start text-red-500 hover:text-red-600"
196
+ variant="ghost"
197
+ onPress={() => setMode('delete')}
198
+ >
199
+ <span className="mr-2">🗑️</span> Delete Project
200
+ </Button>
201
+
202
+ <div className="border-border border-t pt-3">
203
+ <Button
204
+ className="w-full"
205
+ variant="outline"
206
+ onPress={handleClose}
207
+ >
208
+ Close
209
+ </Button>
210
+ </div>
211
+ </div>
212
+ )}
213
+
214
+ {/* Edit Form */}
215
+ {mode === 'edit' && (
216
+ <div className="space-y-4">
217
+ <div>
218
+ <label
219
+ htmlFor="edit-name"
220
+ className="text-muted-foreground mb-1 block text-sm font-medium"
221
+ >
222
+ Project Name *
223
+ </label>
224
+ <Input
225
+ id="edit-name"
226
+ value={name}
227
+ onChange={(e) => setName(e.target.value)}
228
+ disabled={isLoading}
229
+ />
230
+ </div>
231
+
232
+ <div>
233
+ <label
234
+ htmlFor="edit-description"
235
+ className="text-muted-foreground mb-1 block text-sm font-medium"
236
+ >
237
+ Description
238
+ </label>
239
+ <textarea
240
+ id="edit-description"
241
+ value={description}
242
+ onChange={(e) => setDescription(e.target.value)}
243
+ rows={3}
244
+ disabled={isLoading}
245
+ 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"
246
+ />
247
+ </div>
248
+
249
+ {error && (
250
+ <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
251
+ {error}
252
+ </div>
253
+ )}
254
+
255
+ <div className="flex justify-end gap-3 pt-2">
256
+ <Button
257
+ variant="ghost"
258
+ onPress={() => setMode('menu')}
259
+ disabled={isLoading}
260
+ >
261
+ Back
262
+ </Button>
263
+ <Button onPress={handleEdit} disabled={isLoading}>
264
+ {isLoading ? 'Saving...' : 'Save Changes'}
265
+ </Button>
266
+ </div>
267
+ </div>
268
+ )}
269
+
270
+ {/* Archive Confirmation */}
271
+ {mode === 'archive' && (
272
+ <div className="space-y-4">
273
+ <p className="text-muted-foreground">
274
+ Are you sure you want to archive{' '}
275
+ <span className="text-foreground font-medium">
276
+ {project.name}
277
+ </span>
278
+ ?
279
+ </p>
280
+ <p className="text-muted-foreground text-sm">
281
+ Archived projects can be restored later.
282
+ </p>
283
+
284
+ {error && (
285
+ <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
286
+ {error}
287
+ </div>
288
+ )}
289
+
290
+ <div className="flex justify-end gap-3 pt-2">
291
+ <Button
292
+ variant="ghost"
293
+ onPress={() => setMode('menu')}
294
+ disabled={isLoading}
295
+ >
296
+ Cancel
297
+ </Button>
298
+ <Button onPress={handleArchive} disabled={isLoading}>
299
+ {isLoading ? 'Archiving...' : '📦 Archive'}
300
+ </Button>
301
+ </div>
302
+ </div>
303
+ )}
304
+
305
+ {/* Delete Confirmation */}
306
+ {mode === 'delete' && (
307
+ <div className="space-y-4">
308
+ <p className="text-muted-foreground">
309
+ Are you sure you want to delete{' '}
310
+ <span className="text-foreground font-medium">
311
+ {project.name}
312
+ </span>
313
+ ?
314
+ </p>
315
+ <p className="text-destructive text-sm">
316
+ This action cannot be undone.
317
+ </p>
318
+
319
+ {error && (
320
+ <div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
321
+ {error}
322
+ </div>
323
+ )}
324
+
325
+ <div className="flex justify-end gap-3 pt-2">
326
+ <Button
327
+ variant="ghost"
328
+ onPress={() => setMode('menu')}
329
+ disabled={isLoading}
330
+ >
331
+ Cancel
332
+ </Button>
333
+ <Button
334
+ variant="destructive"
335
+ onPress={handleDelete}
336
+ disabled={isLoading}
337
+ >
338
+ {isLoading ? 'Deleting...' : '🗑️ Delete'}
339
+ </Button>
340
+ </div>
341
+ </div>
342
+ )}
343
+ </div>
344
+ </div>
345
+ );
346
+ }
@@ -0,0 +1,2 @@
1
+ export { CreateProjectModal } from './CreateProjectModal';
2
+ export { ProjectActionsModal } from './ProjectActionsModal';