@tpitre/story-ui 3.8.0 → 3.9.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.
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { setupCommand, cleanupDefaultStorybookComponents } from './setup.js';
8
8
  import { deployCommand } from './deploy.js';
9
+ import { updateCommand, statusCommand } from './update.js';
9
10
  import net from 'net';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -279,4 +280,25 @@ program
279
280
  mcpServer.kill('SIGINT');
280
281
  });
281
282
  });
283
+ program
284
+ .command('update')
285
+ .description('Update Story UI managed files to the latest version')
286
+ .option('-f, --force', 'Skip confirmation prompts')
287
+ .option('--no-backup', 'Skip creating backups of existing files')
288
+ .option('-n, --dry-run', 'Show what would be updated without making changes')
289
+ .option('-v, --verbose', 'Show detailed output')
290
+ .action(async (options) => {
291
+ await updateCommand({
292
+ force: options.force,
293
+ backup: options.backup,
294
+ dryRun: options.dryRun,
295
+ verbose: options.verbose
296
+ });
297
+ });
298
+ program
299
+ .command('status')
300
+ .description('Show Story UI installation status and version info')
301
+ .action(() => {
302
+ statusCommand();
303
+ });
282
304
  program.parse(process.argv);
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAkCA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AA2VD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBAwsB5D"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBA2sB5D"}
package/dist/cli/setup.js CHANGED
@@ -8,6 +8,23 @@ import net from 'net';
8
8
  import { execSync } from 'child_process';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
+ /**
12
+ * Get the Story UI package version for version tracking
13
+ */
14
+ function getStoryUIVersion() {
15
+ try {
16
+ const pkgRoot = path.resolve(__dirname, '..');
17
+ const packageJsonPath = path.join(pkgRoot, 'package.json');
18
+ if (fs.existsSync(packageJsonPath)) {
19
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
20
+ return packageJson.version || 'unknown';
21
+ }
22
+ }
23
+ catch (error) {
24
+ // Fallback
25
+ }
26
+ return 'unknown';
27
+ }
11
28
  // FIRST_EDIT: helper functions to check for free ports
12
29
  async function isPortAvailable(port) {
13
30
  return new Promise((resolve) => {
@@ -244,10 +261,16 @@ const DESIGN_SYSTEM_CONFIGS = {
244
261
  framework: 'vue'
245
262
  },
246
263
  vuetify: {
247
- packages: ['vuetify'],
264
+ packages: ['vuetify', '@mdi/font', '@fontsource/roboto'],
248
265
  name: 'Vuetify',
249
266
  importPath: 'vuetify',
250
- additionalSetup: 'import "vuetify/styles";',
267
+ additionalSetup: `import "vuetify/styles";
268
+ import "@mdi/font/css/materialdesignicons.css";
269
+ // Roboto font required for proper Vuetify typography
270
+ import "@fontsource/roboto/300.css";
271
+ import "@fontsource/roboto/400.css";
272
+ import "@fontsource/roboto/500.css";
273
+ import "@fontsource/roboto/700.css";`,
251
274
  framework: 'vue'
252
275
  },
253
276
  'element-plus': {
@@ -835,6 +858,9 @@ Material UI (MUI) is a React component library implementing Material Design.
835
858
  config.componentFramework = componentFramework; // react, angular, vue, svelte, or web-components
836
859
  config.storybookFramework = storybookFramework; // e.g., @storybook/react-vite, @storybook/angular
837
860
  config.llmProvider = answers.llmProvider || 'claude'; // claude, openai, or gemini
861
+ // Add version tracking for update command
862
+ config._storyUIVersion = getStoryUIVersion();
863
+ config._lastUpdated = new Date().toISOString();
838
864
  // Create configuration file
839
865
  const configContent = `module.exports = ${JSON.stringify(config, null, 2)};`;
840
866
  const configPath = path.join(process.cwd(), 'story-ui.config.js');
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Story UI Update Command
3
+ *
4
+ * Refreshes managed Story UI files (StoryUIPanel.tsx, StoryUIPanel.mdx, index.tsx)
5
+ * while preserving user configuration files (story-ui.config.js, .env, etc.)
6
+ */
7
+ export interface UpdateOptions {
8
+ force?: boolean;
9
+ backup?: boolean;
10
+ dryRun?: boolean;
11
+ verbose?: boolean;
12
+ }
13
+ export interface UpdateResult {
14
+ success: boolean;
15
+ filesUpdated: string[];
16
+ filesBackedUp: string[];
17
+ errors: string[];
18
+ currentVersion: string;
19
+ newVersion: string;
20
+ }
21
+ /**
22
+ * Main update command
23
+ */
24
+ export declare function updateCommand(options?: UpdateOptions): Promise<UpdateResult>;
25
+ /**
26
+ * Show current Story UI installation status
27
+ */
28
+ export declare function statusCommand(): void;
29
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../cli/update.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AA2SD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkItF;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CA+BpC"}
@@ -0,0 +1,398 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { fileURLToPath } from 'url';
5
+ import inquirer from 'inquirer';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ // Files managed by Story UI that can be safely overwritten
9
+ const MANAGED_FILES = [
10
+ {
11
+ source: 'templates/StoryUI/StoryUIPanel.tsx',
12
+ target: 'src/stories/StoryUI/StoryUIPanel.tsx',
13
+ description: 'Main chat panel component'
14
+ },
15
+ {
16
+ source: 'templates/StoryUI/StoryUIPanel.mdx',
17
+ target: 'src/stories/StoryUI/StoryUIPanel.mdx',
18
+ description: 'Cross-framework MDX wrapper'
19
+ },
20
+ {
21
+ source: 'templates/StoryUI/index.tsx',
22
+ target: 'src/stories/StoryUI/index.tsx',
23
+ description: 'Panel registration'
24
+ }
25
+ ];
26
+ // Files that should NEVER be modified by update
27
+ const USER_CONFIG_FILES = [
28
+ 'story-ui.config.js',
29
+ 'story-ui.config.mjs',
30
+ 'story-ui.config.cjs',
31
+ '.env',
32
+ 'story-ui-considerations.md',
33
+ 'story-ui-docs/'
34
+ ];
35
+ // Directories that should NEVER be touched
36
+ const PROTECTED_DIRECTORIES = [
37
+ 'src/stories/generated/'
38
+ ];
39
+ /**
40
+ * Get the Story UI package version
41
+ */
42
+ function getPackageVersion() {
43
+ try {
44
+ // Try multiple paths to find package.json
45
+ // When running from dist/cli/index.js, we need to go up 2 levels
46
+ const possiblePaths = [
47
+ path.resolve(__dirname, '..', 'package.json'), // From dist/cli
48
+ path.resolve(__dirname, '..', '..', 'package.json'), // From src/cli
49
+ ];
50
+ for (const packageJsonPath of possiblePaths) {
51
+ if (fs.existsSync(packageJsonPath)) {
52
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
53
+ if (packageJson.name === '@tpitre/story-ui' && packageJson.version) {
54
+ return packageJson.version;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ catch (error) {
60
+ // Fallback
61
+ }
62
+ return 'unknown';
63
+ }
64
+ /**
65
+ * Detect if Story UI is initialized in the current directory
66
+ */
67
+ function detectStoryUIInstallation() {
68
+ const cwd = process.cwd();
69
+ // Check for Story UI directory
70
+ const possibleStoryUIDirs = [
71
+ path.join(cwd, 'src', 'stories', 'StoryUI'),
72
+ path.join(cwd, 'stories', 'StoryUI')
73
+ ];
74
+ let storyUIDir;
75
+ for (const dir of possibleStoryUIDirs) {
76
+ if (fs.existsSync(dir)) {
77
+ storyUIDir = dir;
78
+ break;
79
+ }
80
+ }
81
+ // Check for config file
82
+ const configFiles = [
83
+ 'story-ui.config.js',
84
+ 'story-ui.config.mjs',
85
+ 'story-ui.config.cjs'
86
+ ];
87
+ let configPath;
88
+ for (const configFile of configFiles) {
89
+ const fullPath = path.join(cwd, configFile);
90
+ if (fs.existsSync(fullPath)) {
91
+ configPath = fullPath;
92
+ break;
93
+ }
94
+ }
95
+ // Try to read installed version from config
96
+ let installedVersion;
97
+ if (configPath) {
98
+ try {
99
+ const configContent = fs.readFileSync(configPath, 'utf-8');
100
+ const versionMatch = configContent.match(/_storyUIVersion:\s*['"]([^'"]+)['"]/);
101
+ if (versionMatch) {
102
+ installedVersion = versionMatch[1];
103
+ }
104
+ }
105
+ catch (error) {
106
+ // Ignore read errors
107
+ }
108
+ }
109
+ return {
110
+ isInstalled: !!(storyUIDir || configPath),
111
+ storyUIDir,
112
+ configPath,
113
+ installedVersion
114
+ };
115
+ }
116
+ /**
117
+ * Create a backup of a file
118
+ */
119
+ function createBackup(filePath) {
120
+ if (!fs.existsSync(filePath)) {
121
+ return null;
122
+ }
123
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
124
+ const backupPath = `${filePath}.backup-${timestamp}`;
125
+ try {
126
+ fs.copyFileSync(filePath, backupPath);
127
+ return backupPath;
128
+ }
129
+ catch (error) {
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * Get the source path for a template file
135
+ */
136
+ function getSourcePath(relativePath) {
137
+ // First try the dist directory (when installed as package)
138
+ const pkgRoot = path.resolve(__dirname, '..');
139
+ const distPath = path.join(pkgRoot, relativePath);
140
+ if (fs.existsSync(distPath)) {
141
+ return distPath;
142
+ }
143
+ // Fall back to project root (when running in development)
144
+ const projectRoot = path.resolve(__dirname, '..', '..');
145
+ const projectPath = path.join(projectRoot, relativePath);
146
+ if (fs.existsSync(projectPath)) {
147
+ return projectPath;
148
+ }
149
+ throw new Error(`Template file not found: ${relativePath}`);
150
+ }
151
+ /**
152
+ * Compare file contents to check if update is needed
153
+ */
154
+ function filesAreDifferent(sourcePath, targetPath) {
155
+ if (!fs.existsSync(targetPath)) {
156
+ return true;
157
+ }
158
+ try {
159
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
160
+ const targetContent = fs.readFileSync(targetPath, 'utf-8');
161
+ return sourceContent !== targetContent;
162
+ }
163
+ catch (error) {
164
+ return true;
165
+ }
166
+ }
167
+ /**
168
+ * Update a single managed file
169
+ */
170
+ function updateManagedFile(sourceRelative, targetRelative, options) {
171
+ const cwd = process.cwd();
172
+ const targetPath = path.join(cwd, targetRelative);
173
+ try {
174
+ const sourcePath = getSourcePath(sourceRelative);
175
+ // Check if update is needed
176
+ if (!filesAreDifferent(sourcePath, targetPath)) {
177
+ if (options.verbose) {
178
+ console.log(chalk.gray(` ⏭️ ${targetRelative} (already up to date)`));
179
+ }
180
+ return { updated: false };
181
+ }
182
+ if (options.dryRun) {
183
+ console.log(chalk.cyan(` 📋 Would update: ${targetRelative}`));
184
+ return { updated: true };
185
+ }
186
+ // Create backup if enabled and file exists
187
+ let backupPath;
188
+ if (options.backup !== false && fs.existsSync(targetPath)) {
189
+ const backup = createBackup(targetPath);
190
+ if (backup) {
191
+ backupPath = backup;
192
+ if (options.verbose) {
193
+ console.log(chalk.gray(` 💾 Backed up: ${path.basename(backup)}`));
194
+ }
195
+ }
196
+ }
197
+ // Ensure target directory exists
198
+ const targetDir = path.dirname(targetPath);
199
+ if (!fs.existsSync(targetDir)) {
200
+ fs.mkdirSync(targetDir, { recursive: true });
201
+ }
202
+ // Copy the new file
203
+ fs.copyFileSync(sourcePath, targetPath);
204
+ console.log(chalk.green(` ✅ Updated: ${targetRelative}`));
205
+ return { updated: true, backupPath };
206
+ }
207
+ catch (error) {
208
+ return { updated: false, error: error.message };
209
+ }
210
+ }
211
+ /**
212
+ * Update the config file with version tracking
213
+ */
214
+ function updateConfigVersion(configPath, version) {
215
+ try {
216
+ let content = fs.readFileSync(configPath, 'utf-8');
217
+ // Check if version tracking already exists
218
+ const hasVersion = /_storyUIVersion/.test(content);
219
+ const hasLastUpdated = /_lastUpdated/.test(content);
220
+ const timestamp = new Date().toISOString();
221
+ if (hasVersion) {
222
+ // Update existing version
223
+ content = content.replace(/_storyUIVersion:\s*['"][^'"]*['"]/, `_storyUIVersion: '${version}'`);
224
+ }
225
+ if (hasLastUpdated) {
226
+ // Update existing timestamp
227
+ content = content.replace(/_lastUpdated:\s*['"][^'"]*['"]/, `_lastUpdated: '${timestamp}'`);
228
+ }
229
+ // If neither exists, add them before the closing brace
230
+ if (!hasVersion && !hasLastUpdated) {
231
+ // Find the last property and add version tracking
232
+ const insertPosition = content.lastIndexOf('}');
233
+ if (insertPosition !== -1) {
234
+ const versionFields = `
235
+ // Story UI version tracking (auto-generated)
236
+ _storyUIVersion: '${version}',
237
+ _lastUpdated: '${timestamp}',
238
+ `;
239
+ // Check if there's a trailing comma needed
240
+ const beforeInsert = content.substring(0, insertPosition).trim();
241
+ const needsComma = beforeInsert.endsWith(',') || beforeInsert.endsWith('{') ? '' : ',';
242
+ content = content.substring(0, insertPosition - 1) +
243
+ needsComma +
244
+ versionFields +
245
+ '};' +
246
+ content.substring(insertPosition + 1);
247
+ }
248
+ }
249
+ fs.writeFileSync(configPath, content);
250
+ return true;
251
+ }
252
+ catch (error) {
253
+ return false;
254
+ }
255
+ }
256
+ /**
257
+ * Main update command
258
+ */
259
+ export async function updateCommand(options = {}) {
260
+ const result = {
261
+ success: false,
262
+ filesUpdated: [],
263
+ filesBackedUp: [],
264
+ errors: [],
265
+ currentVersion: 'unknown',
266
+ newVersion: getPackageVersion()
267
+ };
268
+ console.log(chalk.bold('\n🔄 Story UI Update\n'));
269
+ // Step 1: Detect installation
270
+ const installation = detectStoryUIInstallation();
271
+ if (!installation.isInstalled) {
272
+ console.log(chalk.red('❌ Story UI is not initialized in this directory.'));
273
+ console.log(chalk.yellow(' Run "npx story-ui init" first to set up Story UI.'));
274
+ result.errors.push('Story UI not initialized');
275
+ return result;
276
+ }
277
+ result.currentVersion = installation.installedVersion || 'unknown';
278
+ console.log(chalk.gray(` Current version: ${result.currentVersion}`));
279
+ console.log(chalk.gray(` New version: ${result.newVersion}`));
280
+ // Step 2: Show what will be updated
281
+ console.log(chalk.bold('\n📦 Managed files to update:'));
282
+ const filesToUpdate = [];
283
+ for (const file of MANAGED_FILES) {
284
+ try {
285
+ const sourcePath = getSourcePath(file.source);
286
+ const targetPath = path.join(process.cwd(), file.target);
287
+ const needsUpdate = filesAreDifferent(sourcePath, targetPath);
288
+ if (needsUpdate) {
289
+ filesToUpdate.push(file);
290
+ console.log(chalk.cyan(` • ${file.target}`));
291
+ console.log(chalk.gray(` ${file.description}`));
292
+ }
293
+ else if (options.verbose) {
294
+ console.log(chalk.gray(` ⏭️ ${file.target} (up to date)`));
295
+ }
296
+ }
297
+ catch (error) {
298
+ console.log(chalk.red(` ❌ ${file.target}: ${error.message}`));
299
+ result.errors.push(`${file.target}: ${error.message}`);
300
+ }
301
+ }
302
+ if (filesToUpdate.length === 0) {
303
+ console.log(chalk.green('\n✅ All files are already up to date!'));
304
+ result.success = true;
305
+ return result;
306
+ }
307
+ // Step 3: Confirm update (unless --force or --dry-run)
308
+ if (!options.force && !options.dryRun) {
309
+ console.log(chalk.yellow('\n⚠️ The following will NOT be modified:'));
310
+ console.log(chalk.gray(' • story-ui.config.js (your configuration)'));
311
+ console.log(chalk.gray(' • .env (your API keys)'));
312
+ console.log(chalk.gray(' • story-ui-docs/ (your documentation)'));
313
+ console.log(chalk.gray(' • src/stories/generated/ (your generated stories)'));
314
+ const { confirm } = await inquirer.prompt([{
315
+ type: 'confirm',
316
+ name: 'confirm',
317
+ message: `Update ${filesToUpdate.length} file(s)?`,
318
+ default: true
319
+ }]);
320
+ if (!confirm) {
321
+ console.log(chalk.yellow('\n⏹️ Update cancelled.'));
322
+ return result;
323
+ }
324
+ }
325
+ // Step 4: Perform updates
326
+ if (options.dryRun) {
327
+ console.log(chalk.bold('\n📋 Dry run - no changes made:'));
328
+ }
329
+ else {
330
+ console.log(chalk.bold('\n🔧 Updating files...'));
331
+ }
332
+ for (const file of filesToUpdate) {
333
+ const updateResult = updateManagedFile(file.source, file.target, options);
334
+ if (updateResult.updated) {
335
+ result.filesUpdated.push(file.target);
336
+ }
337
+ if (updateResult.backupPath) {
338
+ result.filesBackedUp.push(updateResult.backupPath);
339
+ }
340
+ if (updateResult.error) {
341
+ result.errors.push(`${file.target}: ${updateResult.error}`);
342
+ }
343
+ }
344
+ // Step 5: Update config version tracking
345
+ if (!options.dryRun && installation.configPath) {
346
+ if (updateConfigVersion(installation.configPath, result.newVersion)) {
347
+ console.log(chalk.gray(`\n Updated version tracking in ${path.basename(installation.configPath)}`));
348
+ }
349
+ }
350
+ // Step 6: Summary
351
+ console.log(chalk.bold('\n📊 Update Summary:'));
352
+ console.log(chalk.green(` ✅ Files updated: ${result.filesUpdated.length}`));
353
+ if (result.filesBackedUp.length > 0) {
354
+ console.log(chalk.gray(` 💾 Backups created: ${result.filesBackedUp.length}`));
355
+ }
356
+ if (result.errors.length > 0) {
357
+ console.log(chalk.red(` ❌ Errors: ${result.errors.length}`));
358
+ for (const error of result.errors) {
359
+ console.log(chalk.red(` • ${error}`));
360
+ }
361
+ }
362
+ result.success = result.errors.length === 0;
363
+ if (result.success && !options.dryRun) {
364
+ console.log(chalk.green('\n✅ Story UI updated successfully!'));
365
+ console.log(chalk.gray(' Restart Storybook to see the changes.'));
366
+ }
367
+ return result;
368
+ }
369
+ /**
370
+ * Show current Story UI installation status
371
+ */
372
+ export function statusCommand() {
373
+ console.log(chalk.bold('\n📊 Story UI Status\n'));
374
+ const installation = detectStoryUIInstallation();
375
+ const packageVersion = getPackageVersion();
376
+ if (!installation.isInstalled) {
377
+ console.log(chalk.red('❌ Story UI is not initialized in this directory.'));
378
+ console.log(chalk.gray(' Run "npx story-ui init" to set up Story UI.'));
379
+ return;
380
+ }
381
+ console.log(chalk.green('✅ Story UI is installed'));
382
+ console.log(chalk.gray(` Package version: ${packageVersion}`));
383
+ console.log(chalk.gray(` Installed version: ${installation.installedVersion || 'unknown'}`));
384
+ if (installation.configPath) {
385
+ console.log(chalk.gray(` Config: ${path.basename(installation.configPath)}`));
386
+ }
387
+ if (installation.storyUIDir) {
388
+ console.log(chalk.gray(` Panel directory: ${installation.storyUIDir}`));
389
+ }
390
+ // Check for updates
391
+ if (installation.installedVersion && installation.installedVersion !== packageVersion) {
392
+ console.log(chalk.yellow(`\n⚡ Update available: ${installation.installedVersion} → ${packageVersion}`));
393
+ console.log(chalk.gray(' Run "npx story-ui update" to update.'));
394
+ }
395
+ else if (installation.installedVersion === packageVersion) {
396
+ console.log(chalk.green('\n✅ Up to date!'));
397
+ }
398
+ }
@@ -263,6 +263,87 @@ app.post('/story-ui/delete', async (req, res) => {
263
263
  res.status(500).json({ error: 'Failed to delete story' });
264
264
  }
265
265
  });
266
+ // Bulk delete stories
267
+ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
268
+ try {
269
+ const { ids } = req.body;
270
+ if (!ids || !Array.isArray(ids) || ids.length === 0) {
271
+ return res.status(400).json({ error: 'ids array is required' });
272
+ }
273
+ console.log(`🗑️ Bulk deleting ${ids.length} stories`);
274
+ const storiesPath = config.generatedStoriesPath;
275
+ const deleted = [];
276
+ const notFound = [];
277
+ const errors = [];
278
+ for (const id of ids) {
279
+ try {
280
+ const fileName = id.endsWith('.stories.tsx') ? id : `${id}.stories.tsx`;
281
+ const filePath = path.join(storiesPath, fileName);
282
+ if (fs.existsSync(filePath)) {
283
+ fs.unlinkSync(filePath);
284
+ deleted.push(id);
285
+ console.log(`✅ Deleted: ${fileName}`);
286
+ }
287
+ else {
288
+ notFound.push(id);
289
+ }
290
+ }
291
+ catch (err) {
292
+ errors.push(id);
293
+ console.error(`❌ Error deleting ${id}:`, err);
294
+ }
295
+ }
296
+ console.log(`📊 Bulk delete complete: ${deleted.length} deleted, ${notFound.length} not found, ${errors.length} errors`);
297
+ return res.json({
298
+ success: true,
299
+ deleted,
300
+ notFound,
301
+ errors,
302
+ summary: {
303
+ requested: ids.length,
304
+ deleted: deleted.length,
305
+ notFound: notFound.length,
306
+ errors: errors.length
307
+ }
308
+ });
309
+ }
310
+ catch (error) {
311
+ console.error('Error in bulk delete:', error);
312
+ return res.status(500).json({ error: 'Failed to bulk delete stories' });
313
+ }
314
+ });
315
+ // Clear all generated stories
316
+ app.delete('/story-ui/stories', async (req, res) => {
317
+ try {
318
+ const storiesPath = config.generatedStoriesPath;
319
+ console.log(`🗑️ Clearing all stories from: ${storiesPath}`);
320
+ if (!fs.existsSync(storiesPath)) {
321
+ return res.json({ success: true, deleted: 0, message: 'No stories directory found' });
322
+ }
323
+ const files = fs.readdirSync(storiesPath);
324
+ const storyFiles = files.filter(file => file.endsWith('.stories.tsx'));
325
+ let deleted = 0;
326
+ for (const file of storyFiles) {
327
+ try {
328
+ fs.unlinkSync(path.join(storiesPath, file));
329
+ deleted++;
330
+ }
331
+ catch (err) {
332
+ console.error(`Error deleting ${file}:`, err);
333
+ }
334
+ }
335
+ console.log(`✅ Cleared ${deleted} stories`);
336
+ return res.json({
337
+ success: true,
338
+ deleted,
339
+ message: `Cleared ${deleted} stories`
340
+ });
341
+ }
342
+ catch (error) {
343
+ console.error('Error clearing stories:', error);
344
+ return res.status(500).json({ error: 'Failed to clear stories' });
345
+ }
346
+ });
266
347
  // MCP Remote HTTP transport routes (for Claude Desktop remote connections)
267
348
  // Provides Streamable HTTP and legacy SSE endpoints for remote MCP access
268
349
  app.use('/mcp-remote', mcpRemoteRouter);
@@ -1 +1 @@
1
- {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAymDA,iBAAS,YAAY,4CAuyCpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAymDA,iBAAS,YAAY,4CAygDpB;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -1241,6 +1241,8 @@ function StoryUIPanel() {
1241
1241
  const [attachedImages, setAttachedImages] = useState([]);
1242
1242
  const [considerations, setConsiderations] = useState('');
1243
1243
  const [orphanStories, setOrphanStories] = useState([]);
1244
+ const [selectedStoryIds, setSelectedStoryIds] = useState(new Set());
1245
+ const [isBulkDeleting, setIsBulkDeleting] = useState(false);
1244
1246
  const chatEndRef = useRef(null);
1245
1247
  const inputRef = useRef(null);
1246
1248
  const fileInputRef = useRef(null);
@@ -1961,6 +1963,92 @@ function StoryUIPanel() {
1961
1963
  }
1962
1964
  }
1963
1965
  };
1966
+ // Toggle story selection for bulk operations
1967
+ const toggleStorySelection = (storyId) => {
1968
+ setSelectedStoryIds(prev => {
1969
+ const newSet = new Set(prev);
1970
+ if (newSet.has(storyId)) {
1971
+ newSet.delete(storyId);
1972
+ }
1973
+ else {
1974
+ newSet.add(storyId);
1975
+ }
1976
+ return newSet;
1977
+ });
1978
+ };
1979
+ // Select/deselect all stories
1980
+ const toggleSelectAll = () => {
1981
+ if (selectedStoryIds.size === orphanStories.length) {
1982
+ setSelectedStoryIds(new Set());
1983
+ }
1984
+ else {
1985
+ setSelectedStoryIds(new Set(orphanStories.map(s => s.id)));
1986
+ }
1987
+ };
1988
+ // Bulk delete selected stories
1989
+ const handleBulkDelete = async () => {
1990
+ if (selectedStoryIds.size === 0)
1991
+ return;
1992
+ const count = selectedStoryIds.size;
1993
+ if (!confirm(`Delete ${count} selected ${count === 1 ? 'story' : 'stories'}? This action cannot be undone.`)) {
1994
+ return;
1995
+ }
1996
+ setIsBulkDeleting(true);
1997
+ try {
1998
+ const response = await fetch(`${STORIES_API}/delete-bulk`, {
1999
+ method: 'POST',
2000
+ headers: { 'Content-Type': 'application/json' },
2001
+ body: JSON.stringify({ ids: Array.from(selectedStoryIds) }),
2002
+ });
2003
+ if (response.ok) {
2004
+ const result = await response.json();
2005
+ // Remove deleted stories from state
2006
+ setOrphanStories(prev => prev.filter(s => !selectedStoryIds.has(s.id)));
2007
+ setSelectedStoryIds(new Set());
2008
+ console.log(`Deleted ${result.deleted?.length || count} stories`);
2009
+ }
2010
+ else {
2011
+ alert('Failed to delete some stories. Please try again.');
2012
+ }
2013
+ }
2014
+ catch (err) {
2015
+ console.error('Error bulk deleting stories:', err);
2016
+ alert('Failed to delete stories. Please try again.');
2017
+ }
2018
+ finally {
2019
+ setIsBulkDeleting(false);
2020
+ }
2021
+ };
2022
+ // Clear all generated stories
2023
+ const handleClearAll = async () => {
2024
+ if (orphanStories.length === 0)
2025
+ return;
2026
+ if (!confirm(`Delete ALL ${orphanStories.length} generated stories? This action cannot be undone.`)) {
2027
+ return;
2028
+ }
2029
+ setIsBulkDeleting(true);
2030
+ try {
2031
+ const response = await fetch(STORIES_API, {
2032
+ method: 'DELETE',
2033
+ });
2034
+ if (response.ok) {
2035
+ const result = await response.json();
2036
+ setOrphanStories([]);
2037
+ setSelectedStoryIds(new Set());
2038
+ console.log(`Cleared ${result.deleted || 'all'} stories`);
2039
+ }
2040
+ else {
2041
+ alert('Failed to clear stories. Please try again.');
2042
+ }
2043
+ }
2044
+ catch (err) {
2045
+ console.error('Error clearing stories:', err);
2046
+ alert('Failed to clear stories. Please try again.');
2047
+ }
2048
+ finally {
2049
+ setIsBulkDeleting(false);
2050
+ }
2051
+ };
1964
2052
  return (_jsxs("div", { className: "story-ui-panel", style: STYLES.container, children: [_jsxs("div", { style: {
1965
2053
  ...STYLES.sidebar,
1966
2054
  ...(sidebarOpen ? {} : STYLES.sidebarCollapsed),
@@ -2001,28 +2089,107 @@ function StoryUIPanel() {
2001
2089
  if (deleteBtn)
2002
2090
  deleteBtn.style.opacity = '0';
2003
2091
  }, children: [_jsx("div", { style: STYLES.chatItemTitle, children: chat.title }), _jsx("div", { style: STYLES.chatItemTime, children: formatTime(chat.lastUpdated) }), _jsx("button", { className: "delete-btn", onClick: (e) => handleDeleteChat(chat.id, e), style: STYLES.deleteButton, title: "Delete chat", children: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }, chat.id))), orphanStories.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
2004
- color: '#64748b',
2005
- fontSize: '12px',
2092
+ display: 'flex',
2093
+ alignItems: 'center',
2094
+ justifyContent: 'space-between',
2006
2095
  marginTop: '16px',
2007
2096
  marginBottom: '8px',
2008
- fontWeight: '500',
2009
- textTransform: 'uppercase',
2010
- letterSpacing: '0.05em',
2011
- }, children: "Generated Files" }), orphanStories.map(story => (_jsxs("div", { style: {
2097
+ }, children: _jsxs("div", { style: {
2098
+ display: 'flex',
2099
+ alignItems: 'center',
2100
+ gap: '8px',
2101
+ }, children: [_jsx("input", { type: "checkbox", checked: selectedStoryIds.size === orphanStories.length && orphanStories.length > 0, onChange: toggleSelectAll, style: {
2102
+ width: '14px',
2103
+ height: '14px',
2104
+ cursor: 'pointer',
2105
+ accentColor: '#3b82f6',
2106
+ }, title: selectedStoryIds.size === orphanStories.length ? 'Deselect all' : 'Select all' }), _jsxs("span", { style: {
2107
+ color: '#64748b',
2108
+ fontSize: '12px',
2109
+ fontWeight: '500',
2110
+ textTransform: 'uppercase',
2111
+ letterSpacing: '0.05em',
2112
+ }, children: ["Generated Files (", orphanStories.length, ")"] })] }) }), selectedStoryIds.size > 0 && (_jsx("div", { style: {
2113
+ display: 'flex',
2114
+ gap: '8px',
2115
+ marginBottom: '12px',
2116
+ }, children: _jsx("button", { onClick: handleBulkDelete, disabled: isBulkDeleting, style: {
2117
+ flex: 1,
2118
+ padding: '6px 10px',
2119
+ fontSize: '11px',
2120
+ fontWeight: '500',
2121
+ background: 'rgba(239, 68, 68, 0.15)',
2122
+ color: '#f87171',
2123
+ border: '1px solid rgba(239, 68, 68, 0.3)',
2124
+ borderRadius: '6px',
2125
+ cursor: isBulkDeleting ? 'not-allowed' : 'pointer',
2126
+ opacity: isBulkDeleting ? 0.6 : 1,
2127
+ transition: 'all 0.15s ease',
2128
+ }, onMouseEnter: (e) => {
2129
+ if (!isBulkDeleting) {
2130
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.25)';
2131
+ }
2132
+ }, onMouseLeave: (e) => {
2133
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
2134
+ }, children: isBulkDeleting ? 'Deleting...' : `Delete Selected (${selectedStoryIds.size})` }) })), _jsx("div", { style: {
2135
+ display: 'flex',
2136
+ gap: '8px',
2137
+ marginBottom: '12px',
2138
+ }, children: _jsx("button", { onClick: handleClearAll, disabled: isBulkDeleting || orphanStories.length === 0, style: {
2139
+ flex: 1,
2140
+ padding: '6px 10px',
2141
+ fontSize: '11px',
2142
+ fontWeight: '500',
2143
+ background: 'rgba(100, 116, 139, 0.15)',
2144
+ color: '#94a3b8',
2145
+ border: '1px solid rgba(100, 116, 139, 0.3)',
2146
+ borderRadius: '6px',
2147
+ cursor: (isBulkDeleting || orphanStories.length === 0) ? 'not-allowed' : 'pointer',
2148
+ opacity: (isBulkDeleting || orphanStories.length === 0) ? 0.6 : 1,
2149
+ transition: 'all 0.15s ease',
2150
+ }, onMouseEnter: (e) => {
2151
+ if (!isBulkDeleting && orphanStories.length > 0) {
2152
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
2153
+ e.currentTarget.style.color = '#f87171';
2154
+ e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.3)';
2155
+ }
2156
+ }, onMouseLeave: (e) => {
2157
+ e.currentTarget.style.background = 'rgba(100, 116, 139, 0.15)';
2158
+ e.currentTarget.style.color = '#94a3b8';
2159
+ e.currentTarget.style.borderColor = 'rgba(100, 116, 139, 0.3)';
2160
+ }, children: "Clear All Stories" }) }), orphanStories.map(story => (_jsxs("div", { style: {
2012
2161
  ...STYLES.chatItem,
2013
- background: 'rgba(251, 191, 36, 0.1)',
2014
- borderLeft: '3px solid rgba(251, 191, 36, 0.5)',
2162
+ background: selectedStoryIds.has(story.id)
2163
+ ? 'rgba(59, 130, 246, 0.15)'
2164
+ : 'rgba(251, 191, 36, 0.1)',
2165
+ borderLeft: selectedStoryIds.has(story.id)
2166
+ ? '3px solid rgba(59, 130, 246, 0.5)'
2167
+ : '3px solid rgba(251, 191, 36, 0.5)',
2168
+ display: 'flex',
2169
+ alignItems: 'flex-start',
2170
+ gap: '8px',
2015
2171
  }, onMouseEnter: (e) => {
2016
- e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2172
+ if (!selectedStoryIds.has(story.id)) {
2173
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2174
+ }
2017
2175
  const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn');
2018
2176
  if (deleteBtn)
2019
2177
  deleteBtn.style.opacity = '1';
2020
2178
  }, onMouseLeave: (e) => {
2021
- e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2179
+ if (!selectedStoryIds.has(story.id)) {
2180
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2181
+ }
2022
2182
  const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn');
2023
2183
  if (deleteBtn)
2024
2184
  deleteBtn.style.opacity = '0';
2025
- }, children: [_jsx("div", { style: STYLES.chatItemTitle, children: story.title }), _jsx("div", { style: { ...STYLES.chatItemTime, fontSize: '11px' }, children: story.fileName }), _jsx("button", { className: "delete-orphan-btn", onClick: async (e) => {
2185
+ }, children: [_jsx("input", { type: "checkbox", checked: selectedStoryIds.has(story.id), onChange: () => toggleStorySelection(story.id), onClick: (e) => e.stopPropagation(), style: {
2186
+ width: '14px',
2187
+ height: '14px',
2188
+ cursor: 'pointer',
2189
+ accentColor: '#3b82f6',
2190
+ marginTop: '2px',
2191
+ flexShrink: 0,
2192
+ } }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: STYLES.chatItemTitle, children: story.title }), _jsx("div", { style: { ...STYLES.chatItemTime, fontSize: '11px' }, children: story.fileName })] }), _jsx("button", { className: "delete-orphan-btn", onClick: async (e) => {
2026
2193
  e.stopPropagation();
2027
2194
  try {
2028
2195
  const response = await fetch(`${STORIES_API}/${story.id}`, {
@@ -2030,6 +2197,11 @@ function StoryUIPanel() {
2030
2197
  });
2031
2198
  if (response.ok) {
2032
2199
  setOrphanStories(prev => prev.filter(s => s.id !== story.id));
2200
+ setSelectedStoryIds(prev => {
2201
+ const newSet = new Set(prev);
2202
+ newSet.delete(story.id);
2203
+ return newSet;
2204
+ });
2033
2205
  }
2034
2206
  else {
2035
2207
  console.error('Failed to delete orphan story');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1656,6 +1656,8 @@ function StoryUIPanel() {
1656
1656
  const [attachedImages, setAttachedImages] = useState<AttachedImage[]>([]);
1657
1657
  const [considerations, setConsiderations] = useState<string>('');
1658
1658
  const [orphanStories, setOrphanStories] = useState<OrphanStory[]>([]);
1659
+ const [selectedStoryIds, setSelectedStoryIds] = useState<Set<string>>(new Set());
1660
+ const [isBulkDeleting, setIsBulkDeleting] = useState(false);
1659
1661
  const chatEndRef = useRef<HTMLDivElement | null>(null);
1660
1662
  const inputRef = useRef<HTMLInputElement | null>(null);
1661
1663
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -2462,6 +2464,92 @@ function StoryUIPanel() {
2462
2464
  }
2463
2465
  };
2464
2466
 
2467
+ // Toggle story selection for bulk operations
2468
+ const toggleStorySelection = (storyId: string) => {
2469
+ setSelectedStoryIds(prev => {
2470
+ const newSet = new Set(prev);
2471
+ if (newSet.has(storyId)) {
2472
+ newSet.delete(storyId);
2473
+ } else {
2474
+ newSet.add(storyId);
2475
+ }
2476
+ return newSet;
2477
+ });
2478
+ };
2479
+
2480
+ // Select/deselect all stories
2481
+ const toggleSelectAll = () => {
2482
+ if (selectedStoryIds.size === orphanStories.length) {
2483
+ setSelectedStoryIds(new Set());
2484
+ } else {
2485
+ setSelectedStoryIds(new Set(orphanStories.map(s => s.id)));
2486
+ }
2487
+ };
2488
+
2489
+ // Bulk delete selected stories
2490
+ const handleBulkDelete = async () => {
2491
+ if (selectedStoryIds.size === 0) return;
2492
+
2493
+ const count = selectedStoryIds.size;
2494
+ if (!confirm(`Delete ${count} selected ${count === 1 ? 'story' : 'stories'}? This action cannot be undone.`)) {
2495
+ return;
2496
+ }
2497
+
2498
+ setIsBulkDeleting(true);
2499
+ try {
2500
+ const response = await fetch(`${STORIES_API}/delete-bulk`, {
2501
+ method: 'POST',
2502
+ headers: { 'Content-Type': 'application/json' },
2503
+ body: JSON.stringify({ ids: Array.from(selectedStoryIds) }),
2504
+ });
2505
+
2506
+ if (response.ok) {
2507
+ const result = await response.json();
2508
+ // Remove deleted stories from state
2509
+ setOrphanStories(prev => prev.filter(s => !selectedStoryIds.has(s.id)));
2510
+ setSelectedStoryIds(new Set());
2511
+ console.log(`Deleted ${result.deleted?.length || count} stories`);
2512
+ } else {
2513
+ alert('Failed to delete some stories. Please try again.');
2514
+ }
2515
+ } catch (err) {
2516
+ console.error('Error bulk deleting stories:', err);
2517
+ alert('Failed to delete stories. Please try again.');
2518
+ } finally {
2519
+ setIsBulkDeleting(false);
2520
+ }
2521
+ };
2522
+
2523
+ // Clear all generated stories
2524
+ const handleClearAll = async () => {
2525
+ if (orphanStories.length === 0) return;
2526
+
2527
+ if (!confirm(`Delete ALL ${orphanStories.length} generated stories? This action cannot be undone.`)) {
2528
+ return;
2529
+ }
2530
+
2531
+ setIsBulkDeleting(true);
2532
+ try {
2533
+ const response = await fetch(STORIES_API, {
2534
+ method: 'DELETE',
2535
+ });
2536
+
2537
+ if (response.ok) {
2538
+ const result = await response.json();
2539
+ setOrphanStories([]);
2540
+ setSelectedStoryIds(new Set());
2541
+ console.log(`Cleared ${result.deleted || 'all'} stories`);
2542
+ } else {
2543
+ alert('Failed to clear stories. Please try again.');
2544
+ }
2545
+ } catch (err) {
2546
+ console.error('Error clearing stories:', err);
2547
+ alert('Failed to clear stories. Please try again.');
2548
+ } finally {
2549
+ setIsBulkDeleting(false);
2550
+ }
2551
+ };
2552
+
2465
2553
  return (
2466
2554
  <div className="story-ui-panel" style={STYLES.container}>
2467
2555
  {/* Sidebar */}
@@ -2558,40 +2646,173 @@ function StoryUIPanel() {
2558
2646
  {/* Generated Files Section - orphan stories without chat history */}
2559
2647
  {orphanStories.length > 0 && (
2560
2648
  <>
2649
+ {/* Header with Select All and Count */}
2561
2650
  <div style={{
2562
- color: '#64748b',
2563
- fontSize: '12px',
2651
+ display: 'flex',
2652
+ alignItems: 'center',
2653
+ justifyContent: 'space-between',
2564
2654
  marginTop: '16px',
2565
2655
  marginBottom: '8px',
2566
- fontWeight: '500',
2567
- textTransform: 'uppercase',
2568
- letterSpacing: '0.05em',
2569
2656
  }}>
2570
- Generated Files
2657
+ <div style={{
2658
+ display: 'flex',
2659
+ alignItems: 'center',
2660
+ gap: '8px',
2661
+ }}>
2662
+ <input
2663
+ type="checkbox"
2664
+ checked={selectedStoryIds.size === orphanStories.length && orphanStories.length > 0}
2665
+ onChange={toggleSelectAll}
2666
+ style={{
2667
+ width: '14px',
2668
+ height: '14px',
2669
+ cursor: 'pointer',
2670
+ accentColor: '#3b82f6',
2671
+ }}
2672
+ title={selectedStoryIds.size === orphanStories.length ? 'Deselect all' : 'Select all'}
2673
+ />
2674
+ <span style={{
2675
+ color: '#64748b',
2676
+ fontSize: '12px',
2677
+ fontWeight: '500',
2678
+ textTransform: 'uppercase',
2679
+ letterSpacing: '0.05em',
2680
+ }}>
2681
+ Generated Files ({orphanStories.length})
2682
+ </span>
2683
+ </div>
2684
+ </div>
2685
+
2686
+ {/* Bulk Action Buttons */}
2687
+ {selectedStoryIds.size > 0 && (
2688
+ <div style={{
2689
+ display: 'flex',
2690
+ gap: '8px',
2691
+ marginBottom: '12px',
2692
+ }}>
2693
+ <button
2694
+ onClick={handleBulkDelete}
2695
+ disabled={isBulkDeleting}
2696
+ style={{
2697
+ flex: 1,
2698
+ padding: '6px 10px',
2699
+ fontSize: '11px',
2700
+ fontWeight: '500',
2701
+ background: 'rgba(239, 68, 68, 0.15)',
2702
+ color: '#f87171',
2703
+ border: '1px solid rgba(239, 68, 68, 0.3)',
2704
+ borderRadius: '6px',
2705
+ cursor: isBulkDeleting ? 'not-allowed' : 'pointer',
2706
+ opacity: isBulkDeleting ? 0.6 : 1,
2707
+ transition: 'all 0.15s ease',
2708
+ }}
2709
+ onMouseEnter={(e) => {
2710
+ if (!isBulkDeleting) {
2711
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.25)';
2712
+ }
2713
+ }}
2714
+ onMouseLeave={(e) => {
2715
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
2716
+ }}
2717
+ >
2718
+ {isBulkDeleting ? 'Deleting...' : `Delete Selected (${selectedStoryIds.size})`}
2719
+ </button>
2720
+ </div>
2721
+ )}
2722
+
2723
+ {/* Clear All Button (always visible) */}
2724
+ <div style={{
2725
+ display: 'flex',
2726
+ gap: '8px',
2727
+ marginBottom: '12px',
2728
+ }}>
2729
+ <button
2730
+ onClick={handleClearAll}
2731
+ disabled={isBulkDeleting || orphanStories.length === 0}
2732
+ style={{
2733
+ flex: 1,
2734
+ padding: '6px 10px',
2735
+ fontSize: '11px',
2736
+ fontWeight: '500',
2737
+ background: 'rgba(100, 116, 139, 0.15)',
2738
+ color: '#94a3b8',
2739
+ border: '1px solid rgba(100, 116, 139, 0.3)',
2740
+ borderRadius: '6px',
2741
+ cursor: (isBulkDeleting || orphanStories.length === 0) ? 'not-allowed' : 'pointer',
2742
+ opacity: (isBulkDeleting || orphanStories.length === 0) ? 0.6 : 1,
2743
+ transition: 'all 0.15s ease',
2744
+ }}
2745
+ onMouseEnter={(e) => {
2746
+ if (!isBulkDeleting && orphanStories.length > 0) {
2747
+ e.currentTarget.style.background = 'rgba(239, 68, 68, 0.15)';
2748
+ e.currentTarget.style.color = '#f87171';
2749
+ e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.3)';
2750
+ }
2751
+ }}
2752
+ onMouseLeave={(e) => {
2753
+ e.currentTarget.style.background = 'rgba(100, 116, 139, 0.15)';
2754
+ e.currentTarget.style.color = '#94a3b8';
2755
+ e.currentTarget.style.borderColor = 'rgba(100, 116, 139, 0.3)';
2756
+ }}
2757
+ >
2758
+ Clear All Stories
2759
+ </button>
2571
2760
  </div>
2761
+
2762
+ {/* Story List */}
2572
2763
  {orphanStories.map(story => (
2573
2764
  <div
2574
2765
  key={story.id}
2575
2766
  style={{
2576
2767
  ...STYLES.chatItem,
2577
- background: 'rgba(251, 191, 36, 0.1)',
2578
- borderLeft: '3px solid rgba(251, 191, 36, 0.5)',
2768
+ background: selectedStoryIds.has(story.id)
2769
+ ? 'rgba(59, 130, 246, 0.15)'
2770
+ : 'rgba(251, 191, 36, 0.1)',
2771
+ borderLeft: selectedStoryIds.has(story.id)
2772
+ ? '3px solid rgba(59, 130, 246, 0.5)'
2773
+ : '3px solid rgba(251, 191, 36, 0.5)',
2774
+ display: 'flex',
2775
+ alignItems: 'flex-start',
2776
+ gap: '8px',
2579
2777
  }}
2580
2778
  onMouseEnter={(e) => {
2581
- e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2779
+ if (!selectedStoryIds.has(story.id)) {
2780
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
2781
+ }
2582
2782
  const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn') as HTMLElement;
2583
2783
  if (deleteBtn) deleteBtn.style.opacity = '1';
2584
2784
  }}
2585
2785
  onMouseLeave={(e) => {
2586
- e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2786
+ if (!selectedStoryIds.has(story.id)) {
2787
+ e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
2788
+ }
2587
2789
  const deleteBtn = e.currentTarget.querySelector('.delete-orphan-btn') as HTMLElement;
2588
2790
  if (deleteBtn) deleteBtn.style.opacity = '0';
2589
2791
  }}
2590
2792
  >
2591
- <div style={STYLES.chatItemTitle}>{story.title}</div>
2592
- <div style={{ ...STYLES.chatItemTime, fontSize: '11px' }}>
2593
- {story.fileName}
2793
+ {/* Checkbox */}
2794
+ <input
2795
+ type="checkbox"
2796
+ checked={selectedStoryIds.has(story.id)}
2797
+ onChange={() => toggleStorySelection(story.id)}
2798
+ onClick={(e) => e.stopPropagation()}
2799
+ style={{
2800
+ width: '14px',
2801
+ height: '14px',
2802
+ cursor: 'pointer',
2803
+ accentColor: '#3b82f6',
2804
+ marginTop: '2px',
2805
+ flexShrink: 0,
2806
+ }}
2807
+ />
2808
+ {/* Story Info */}
2809
+ <div style={{ flex: 1, minWidth: 0 }}>
2810
+ <div style={STYLES.chatItemTitle}>{story.title}</div>
2811
+ <div style={{ ...STYLES.chatItemTime, fontSize: '11px' }}>
2812
+ {story.fileName}
2813
+ </div>
2594
2814
  </div>
2815
+ {/* Delete Button */}
2595
2816
  <button
2596
2817
  className="delete-orphan-btn"
2597
2818
  onClick={async (e) => {
@@ -2602,6 +2823,11 @@ function StoryUIPanel() {
2602
2823
  });
2603
2824
  if (response.ok) {
2604
2825
  setOrphanStories(prev => prev.filter(s => s.id !== story.id));
2826
+ setSelectedStoryIds(prev => {
2827
+ const newSet = new Set(prev);
2828
+ newSet.delete(story.id);
2829
+ return newSet;
2830
+ });
2605
2831
  } else {
2606
2832
  console.error('Failed to delete orphan story');
2607
2833
  }