nca-ai-cms-astro-plugin 1.0.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 (73) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/README.md +87 -0
  3. package/package.json +53 -0
  4. package/src/api/_utils.ts +20 -0
  5. package/src/api/articles/[id]/apply.ts +89 -0
  6. package/src/api/articles/[id]/regenerate-image.ts +49 -0
  7. package/src/api/articles/[id]/regenerate-text.ts +57 -0
  8. package/src/api/articles/[id].ts +53 -0
  9. package/src/api/auth/check.ts +6 -0
  10. package/src/api/auth/login.ts +43 -0
  11. package/src/api/auth/logout.ts +6 -0
  12. package/src/api/generate-content.ts +43 -0
  13. package/src/api/generate-image.ts +33 -0
  14. package/src/api/prompts.ts +45 -0
  15. package/src/api/save-image.ts +38 -0
  16. package/src/api/save.ts +49 -0
  17. package/src/api/scheduler/[id].ts +31 -0
  18. package/src/api/scheduler/generate.ts +94 -0
  19. package/src/api/scheduler/publish.ts +96 -0
  20. package/src/api/scheduler.ts +51 -0
  21. package/src/components/Editor.tsx +115 -0
  22. package/src/components/editor/GenerateTab.tsx +384 -0
  23. package/src/components/editor/PlannerTab.tsx +345 -0
  24. package/src/components/editor/SettingsTab.tsx +185 -0
  25. package/src/components/editor/styles.ts +597 -0
  26. package/src/components/editor/types.ts +49 -0
  27. package/src/components/editor/useTabNavigation.ts +69 -0
  28. package/src/config.d.ts +4 -0
  29. package/src/db/tables.ts +39 -0
  30. package/src/domain/entities/Article.test.ts +138 -0
  31. package/src/domain/entities/Article.ts +90 -0
  32. package/src/domain/entities/ScheduledPost.test.ts +228 -0
  33. package/src/domain/entities/ScheduledPost.ts +152 -0
  34. package/src/domain/entities/Source.test.ts +57 -0
  35. package/src/domain/entities/Source.ts +43 -0
  36. package/src/domain/entities/index.ts +9 -0
  37. package/src/domain/index.ts +16 -0
  38. package/src/domain/value-objects/ArticleFinder.test.ts +104 -0
  39. package/src/domain/value-objects/ArticleFinder.ts +61 -0
  40. package/src/domain/value-objects/SEOMetadata.test.ts +48 -0
  41. package/src/domain/value-objects/SEOMetadata.ts +19 -0
  42. package/src/domain/value-objects/Slug.test.ts +51 -0
  43. package/src/domain/value-objects/Slug.ts +33 -0
  44. package/src/domain/value-objects/index.ts +4 -0
  45. package/src/index.ts +146 -0
  46. package/src/middleware.ts +30 -0
  47. package/src/pages/editor.astro +22 -0
  48. package/src/pages/login.astro +117 -0
  49. package/src/services/ArticleService.test.ts +148 -0
  50. package/src/services/ArticleService.ts +150 -0
  51. package/src/services/AutoPublisher.ts +122 -0
  52. package/src/services/ContentFetcher.ts +89 -0
  53. package/src/services/ContentGenerator.ts +320 -0
  54. package/src/services/FileWriter.test.ts +80 -0
  55. package/src/services/FileWriter.ts +59 -0
  56. package/src/services/ImageConverter.ts +15 -0
  57. package/src/services/ImageGenerator.ts +108 -0
  58. package/src/services/PromptService.ts +84 -0
  59. package/src/services/SchedulerDBAdapter.ts +75 -0
  60. package/src/services/SchedulerService.test.ts +286 -0
  61. package/src/services/SchedulerService.ts +149 -0
  62. package/src/services/index.ts +27 -0
  63. package/src/utils/authUtils.test.ts +60 -0
  64. package/src/utils/authUtils.ts +25 -0
  65. package/src/utils/envUtils.test.ts +40 -0
  66. package/src/utils/envUtils.ts +26 -0
  67. package/src/utils/index.ts +7 -0
  68. package/src/utils/markdown.test.ts +65 -0
  69. package/src/utils/markdown.ts +13 -0
  70. package/src/utils/sanitize.test.ts +180 -0
  71. package/src/utils/sanitize.ts +98 -0
  72. package/tsconfig.json +22 -0
  73. package/vitest.config.ts +14 -0
@@ -0,0 +1,597 @@
1
+ import type React from 'react';
2
+
3
+ const v = (name: string, fallback: string): string =>
4
+ `var(${name}, ${fallback})`;
5
+
6
+ const surface = v('--color-surface', '#141416');
7
+ const border = v('--color-border', '#2a2a2e');
8
+ const text = v('--color-text', '#faf9f7');
9
+ const textMuted = v('--color-text-muted', '#b8b5b0');
10
+ const bg = v('--color-bg', '#0a0a0b');
11
+ const primary = v('--color-primary', '#e63946');
12
+ const success = v('--color-success', '#4ade80');
13
+ const error = v('--color-error', '#f87171');
14
+ const surfaceElevated = v('--color-surface-elevated', '#1a1a1d');
15
+ const surfaceAccent = v('--color-surface-accent', '#222226');
16
+ const borderAccent = v('--color-border-accent', '#3a3a40');
17
+ const textAccent = v('--color-text-accent', '#e8e6e1');
18
+ const fontDisplay = v('--font-display', '"Playfair Display", Georgia, serif');
19
+ const fontMono = v('--font-mono', '"JetBrains Mono", monospace');
20
+
21
+ export const styles: Record<string, React.CSSProperties> = {
22
+ container: {
23
+ display: 'flex',
24
+ flexDirection: 'column',
25
+ gap: '1.5rem',
26
+ },
27
+ panel: {
28
+ background: surface,
29
+ border: `1px solid ${border}`,
30
+ borderRadius: '12px',
31
+ padding: '1.5rem',
32
+ },
33
+ headerRow: {
34
+ display: 'flex',
35
+ justifyContent: 'space-between',
36
+ alignItems: 'center',
37
+ marginBottom: '1rem',
38
+ },
39
+ heading: {
40
+ fontSize: '1.25rem',
41
+ fontWeight: 600,
42
+ color: text,
43
+ margin: 0,
44
+ fontFamily: fontDisplay,
45
+ },
46
+ logoutButton: {
47
+ background: 'transparent',
48
+ border: `1px solid ${border}`,
49
+ color: textMuted,
50
+ padding: '0.4rem 1rem',
51
+ borderRadius: '6px',
52
+ cursor: 'pointer',
53
+ fontSize: '0.85rem',
54
+ },
55
+ field: {
56
+ display: 'flex',
57
+ flexDirection: 'column',
58
+ gap: '0.5rem',
59
+ marginBottom: '1rem',
60
+ },
61
+ label: {
62
+ fontSize: '0.875rem',
63
+ fontWeight: 500,
64
+ color: textAccent,
65
+ },
66
+ input: {
67
+ background: bg,
68
+ border: `1px solid ${border}`,
69
+ borderRadius: '8px',
70
+ padding: '0.75rem 1rem',
71
+ color: text,
72
+ fontSize: '0.95rem',
73
+ outline: 'none',
74
+ width: '100%',
75
+ boxSizing: 'border-box' as const,
76
+ },
77
+ hint: {
78
+ fontSize: '0.8rem',
79
+ color: textMuted,
80
+ },
81
+ button: {
82
+ background: surfaceAccent,
83
+ border: `1px solid ${borderAccent}`,
84
+ color: text,
85
+ padding: '0.6rem 1.2rem',
86
+ borderRadius: '8px',
87
+ cursor: 'pointer',
88
+ fontSize: '0.9rem',
89
+ fontWeight: 500,
90
+ transition: 'all 0.15s ease',
91
+ },
92
+ generateButton: {
93
+ background: primary,
94
+ border: 'none',
95
+ color: '#fff',
96
+ padding: '0.75rem 1.5rem',
97
+ borderRadius: '8px',
98
+ cursor: 'pointer',
99
+ fontSize: '1rem',
100
+ fontWeight: 600,
101
+ width: '100%',
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ gap: '0.5rem',
106
+ transition: 'opacity 0.15s ease',
107
+ },
108
+ buttonContent: {
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ gap: '0.5rem',
112
+ },
113
+ spinner: {
114
+ width: '16px',
115
+ height: '16px',
116
+ border: '2px solid rgba(255,255,255,0.3)',
117
+ borderTopColor: '#fff',
118
+ borderRadius: '50%',
119
+ animation: 'spin 0.6s linear infinite',
120
+ flexShrink: 0,
121
+ },
122
+ actionSection: {
123
+ display: 'flex',
124
+ flexDirection: 'column',
125
+ gap: '0.75rem',
126
+ marginTop: '1rem',
127
+ },
128
+ regenerateRow: {
129
+ display: 'flex',
130
+ gap: '0.5rem',
131
+ },
132
+ secondaryButton: {
133
+ background: surfaceAccent,
134
+ border: `1px solid ${borderAccent}`,
135
+ color: textAccent,
136
+ padding: '0.5rem 1rem',
137
+ borderRadius: '6px',
138
+ cursor: 'pointer',
139
+ fontSize: '0.85rem',
140
+ fontWeight: 500,
141
+ flex: 1,
142
+ },
143
+ publishButton: {
144
+ background: success,
145
+ border: 'none',
146
+ color: '#000',
147
+ padding: '0.75rem 1.5rem',
148
+ borderRadius: '8px',
149
+ cursor: 'pointer',
150
+ fontSize: '1rem',
151
+ fontWeight: 600,
152
+ width: '100%',
153
+ display: 'flex',
154
+ alignItems: 'center',
155
+ justifyContent: 'center',
156
+ gap: '0.5rem',
157
+ },
158
+ successBox: {
159
+ background: surfaceElevated,
160
+ border: `1px solid ${success}`,
161
+ borderRadius: '12px',
162
+ padding: '1.5rem',
163
+ textAlign: 'center' as const,
164
+ },
165
+ successIcon: {
166
+ fontSize: '2rem',
167
+ marginBottom: '0.5rem',
168
+ },
169
+ successTitle: {
170
+ fontSize: '1.1rem',
171
+ fontWeight: 600,
172
+ color: success,
173
+ marginBottom: '0.5rem',
174
+ },
175
+ successPath: {
176
+ fontSize: '0.85rem',
177
+ color: textMuted,
178
+ fontFamily: fontMono,
179
+ wordBreak: 'break-all' as const,
180
+ },
181
+ newButton: {
182
+ background: primary,
183
+ border: 'none',
184
+ color: '#fff',
185
+ padding: '0.6rem 1.2rem',
186
+ borderRadius: '8px',
187
+ cursor: 'pointer',
188
+ fontSize: '0.9rem',
189
+ fontWeight: 500,
190
+ marginTop: '1rem',
191
+ },
192
+ error: {
193
+ background: 'rgba(248,113,113,0.1)',
194
+ border: `1px solid ${error}`,
195
+ borderRadius: '8px',
196
+ padding: '0.75rem 1rem',
197
+ color: error,
198
+ fontSize: '0.9rem',
199
+ marginTop: '0.5rem',
200
+ },
201
+ previewArea: {
202
+ display: 'flex',
203
+ flexDirection: 'column',
204
+ gap: '1.5rem',
205
+ },
206
+ preview: {
207
+ background: surface,
208
+ border: `1px solid ${border}`,
209
+ borderRadius: '12px',
210
+ overflow: 'hidden',
211
+ },
212
+ publishedPreview: {
213
+ background: surface,
214
+ border: `1px solid ${success}`,
215
+ borderRadius: '12px',
216
+ overflow: 'hidden',
217
+ },
218
+ previewHeader: {
219
+ display: 'flex',
220
+ justifyContent: 'space-between',
221
+ alignItems: 'center',
222
+ padding: '1rem 1.25rem',
223
+ borderBottom: `1px solid ${border}`,
224
+ background: surfaceElevated,
225
+ },
226
+ previewTitle: {
227
+ fontSize: '0.9rem',
228
+ fontWeight: 600,
229
+ color: textAccent,
230
+ },
231
+ publishedBadge: {
232
+ background: success,
233
+ color: '#000',
234
+ padding: '0.2rem 0.6rem',
235
+ borderRadius: '4px',
236
+ fontSize: '0.75rem',
237
+ fontWeight: 600,
238
+ },
239
+ filepath: {
240
+ padding: '0.75rem 1.25rem',
241
+ background: bg,
242
+ fontFamily: fontMono,
243
+ fontSize: '0.8rem',
244
+ color: textMuted,
245
+ borderBottom: `1px solid ${border}`,
246
+ },
247
+ frontmatter: {
248
+ padding: '1rem 1.25rem',
249
+ borderBottom: `1px solid ${border}`,
250
+ display: 'flex',
251
+ flexDirection: 'column',
252
+ gap: '0.4rem',
253
+ },
254
+ frontmatterRow: {
255
+ display: 'flex',
256
+ gap: '0.5rem',
257
+ fontSize: '0.85rem',
258
+ },
259
+ frontmatterLabel: {
260
+ color: textMuted,
261
+ fontWeight: 500,
262
+ minWidth: '100px',
263
+ },
264
+ frontmatterValue: {
265
+ color: text,
266
+ flex: 1,
267
+ },
268
+ content: {
269
+ padding: '1.25rem',
270
+ },
271
+ markdown: {
272
+ color: text,
273
+ lineHeight: 1.7,
274
+ fontSize: '0.95rem',
275
+ },
276
+ imagePreview: {
277
+ background: surface,
278
+ border: `1px solid ${border}`,
279
+ borderRadius: '12px',
280
+ overflow: 'hidden',
281
+ },
282
+ image: {
283
+ width: '100%',
284
+ height: 'auto',
285
+ display: 'block',
286
+ },
287
+ imageAlt: {
288
+ padding: '0.75rem 1.25rem',
289
+ fontSize: '0.8rem',
290
+ color: textMuted,
291
+ fontStyle: 'italic',
292
+ borderTop: `1px solid ${border}`,
293
+ },
294
+ tabNav: {
295
+ display: 'flex',
296
+ gap: '0.25rem',
297
+ borderBottom: `1px solid ${border}`,
298
+ marginBottom: '1.5rem',
299
+ },
300
+ tab: {
301
+ background: 'transparent',
302
+ border: 'none',
303
+ borderBottom: '2px solid transparent',
304
+ color: textMuted,
305
+ padding: '0.75rem 1.25rem',
306
+ cursor: 'pointer',
307
+ fontSize: '0.95rem',
308
+ fontWeight: 500,
309
+ transition: 'all 0.15s ease',
310
+ },
311
+ tabActive: {
312
+ background: 'transparent',
313
+ border: 'none',
314
+ borderBottom: `2px solid ${primary}`,
315
+ color: text,
316
+ padding: '0.75rem 1.25rem',
317
+ cursor: 'pointer',
318
+ fontSize: '0.95rem',
319
+ fontWeight: 600,
320
+ transition: 'all 0.15s ease',
321
+ },
322
+ panelFullWidth: {
323
+ background: surface,
324
+ border: `1px solid ${border}`,
325
+ borderRadius: '12px',
326
+ padding: '1.5rem',
327
+ gridColumn: '1 / -1',
328
+ },
329
+ settingsContent: {
330
+ display: 'flex',
331
+ flexDirection: 'column',
332
+ gap: '1rem',
333
+ },
334
+ subTabNav: {
335
+ display: 'flex',
336
+ gap: '0.25rem',
337
+ borderBottom: `1px solid ${border}`,
338
+ paddingBottom: '0',
339
+ marginBottom: '1.25rem',
340
+ flexWrap: 'wrap' as const,
341
+ },
342
+ subTab: {
343
+ background: 'transparent',
344
+ border: 'none',
345
+ borderBottom: '2px solid transparent',
346
+ color: textMuted,
347
+ padding: '0.5rem 1rem',
348
+ cursor: 'pointer',
349
+ fontSize: '0.85rem',
350
+ fontWeight: 500,
351
+ },
352
+ subTabActive: {
353
+ background: 'transparent',
354
+ border: 'none',
355
+ borderBottom: `2px solid ${primary}`,
356
+ color: text,
357
+ padding: '0.5rem 1rem',
358
+ cursor: 'pointer',
359
+ fontSize: '0.85rem',
360
+ fontWeight: 600,
361
+ },
362
+ cardGrid: {
363
+ display: 'grid',
364
+ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
365
+ gap: '1rem',
366
+ },
367
+ settingsCard: {
368
+ background: surfaceElevated,
369
+ border: `1px solid ${border}`,
370
+ borderRadius: '10px',
371
+ overflow: 'hidden',
372
+ },
373
+ settingsCardHeader: {
374
+ padding: '1rem',
375
+ borderBottom: `1px solid ${border}`,
376
+ background: surfaceAccent,
377
+ },
378
+ settingsItemLabel: {
379
+ fontSize: '0.95rem',
380
+ fontWeight: 600,
381
+ color: text,
382
+ margin: 0,
383
+ },
384
+ settingsItemCategory: {
385
+ fontSize: '0.75rem',
386
+ color: textMuted,
387
+ marginTop: '0.25rem',
388
+ },
389
+ settingsItemValue: {
390
+ padding: '1rem',
391
+ fontSize: '0.85rem',
392
+ color: textMuted,
393
+ lineHeight: 1.5,
394
+ maxHeight: '120px',
395
+ overflow: 'auto',
396
+ },
397
+ cardPreview: {
398
+ padding: '1rem',
399
+ fontSize: '0.85rem',
400
+ color: textMuted,
401
+ lineHeight: 1.5,
402
+ maxHeight: '120px',
403
+ overflow: 'auto',
404
+ },
405
+ editArea: {
406
+ padding: '1rem',
407
+ },
408
+ textarea: {
409
+ width: '100%',
410
+ minHeight: '150px',
411
+ background: bg,
412
+ border: `1px solid ${border}`,
413
+ borderRadius: '8px',
414
+ padding: '0.75rem',
415
+ color: text,
416
+ fontSize: '0.85rem',
417
+ fontFamily: fontMono,
418
+ resize: 'vertical' as const,
419
+ outline: 'none',
420
+ boxSizing: 'border-box' as const,
421
+ },
422
+ editButtons: {
423
+ display: 'flex',
424
+ gap: '0.5rem',
425
+ marginTop: '0.75rem',
426
+ },
427
+ smallButton: {
428
+ padding: '0.4rem 0.8rem',
429
+ borderRadius: '6px',
430
+ fontSize: '0.8rem',
431
+ fontWeight: 500,
432
+ cursor: 'pointer',
433
+ border: 'none',
434
+ },
435
+ editButton: {
436
+ background: surfaceAccent,
437
+ border: `1px solid ${borderAccent}`,
438
+ color: textAccent,
439
+ padding: '0.4rem 0.8rem',
440
+ borderRadius: '6px',
441
+ fontSize: '0.8rem',
442
+ fontWeight: 500,
443
+ cursor: 'pointer',
444
+ },
445
+ saveButton: {
446
+ background: primary,
447
+ border: 'none',
448
+ color: '#fff',
449
+ padding: '0.4rem 0.8rem',
450
+ borderRadius: '6px',
451
+ fontSize: '0.8rem',
452
+ fontWeight: 500,
453
+ cursor: 'pointer',
454
+ },
455
+ cancelButton: {
456
+ background: 'transparent',
457
+ border: `1px solid ${border}`,
458
+ color: textMuted,
459
+ padding: '0.4rem 0.8rem',
460
+ borderRadius: '6px',
461
+ fontSize: '0.8rem',
462
+ fontWeight: 500,
463
+ cursor: 'pointer',
464
+ },
465
+ loadingBox: {
466
+ display: 'flex',
467
+ alignItems: 'center',
468
+ justifyContent: 'center',
469
+ padding: '3rem',
470
+ color: textMuted,
471
+ fontSize: '0.95rem',
472
+ },
473
+ plannerContent: {
474
+ display: 'flex',
475
+ flexDirection: 'column',
476
+ gap: '1.5rem',
477
+ },
478
+ plannerForm: {
479
+ display: 'flex',
480
+ flexDirection: 'column',
481
+ gap: '1rem',
482
+ background: surfaceElevated,
483
+ border: `1px solid ${border}`,
484
+ borderRadius: '10px',
485
+ padding: '1.25rem',
486
+ },
487
+ plannerFormRow: {
488
+ display: 'grid',
489
+ gridTemplateColumns: '1fr 1fr auto',
490
+ gap: '0.75rem',
491
+ alignItems: 'end',
492
+ },
493
+ plannerList: {
494
+ display: 'flex',
495
+ flexDirection: 'column',
496
+ gap: '0.75rem',
497
+ },
498
+ plannerCard: {
499
+ background: surfaceElevated,
500
+ border: `1px solid ${border}`,
501
+ borderRadius: '10px',
502
+ overflow: 'hidden',
503
+ },
504
+ plannerCardHeader: {
505
+ display: 'flex',
506
+ justifyContent: 'space-between',
507
+ alignItems: 'center',
508
+ padding: '1rem 1.25rem',
509
+ borderBottom: `1px solid ${border}`,
510
+ background: surfaceAccent,
511
+ },
512
+ plannerCardMeta: {
513
+ display: 'flex',
514
+ alignItems: 'center',
515
+ gap: '0.75rem',
516
+ },
517
+ plannerDate: {
518
+ fontSize: '0.85rem',
519
+ color: textMuted,
520
+ fontFamily: fontMono,
521
+ },
522
+ statusBadge: {
523
+ padding: '0.15rem 0.5rem',
524
+ borderRadius: '4px',
525
+ fontSize: '0.75rem',
526
+ fontWeight: 600,
527
+ textTransform: 'uppercase' as const,
528
+ },
529
+ plannerInputType: {
530
+ fontSize: '0.75rem',
531
+ color: textMuted,
532
+ fontStyle: 'italic',
533
+ },
534
+ plannerCardActions: {
535
+ display: 'flex',
536
+ gap: '0.5rem',
537
+ },
538
+ plannerCardBody: {
539
+ padding: '1rem 1.25rem',
540
+ },
541
+ plannerInput: {
542
+ fontSize: '0.9rem',
543
+ color: text,
544
+ lineHeight: 1.5,
545
+ marginBottom: '0.75rem',
546
+ },
547
+ plannerImagePreview: {
548
+ marginTop: '0.75rem',
549
+ borderRadius: '8px',
550
+ overflow: 'hidden',
551
+ border: `1px solid ${border}`,
552
+ },
553
+ plannerImage: {
554
+ width: '100%',
555
+ maxHeight: '200px',
556
+ objectFit: 'cover' as const,
557
+ display: 'block',
558
+ },
559
+ plannerPreview: {
560
+ marginTop: '0.75rem',
561
+ padding: '0.75rem',
562
+ background: bg,
563
+ borderRadius: '8px',
564
+ fontSize: '0.85rem',
565
+ color: textMuted,
566
+ lineHeight: 1.5,
567
+ },
568
+ plannerPublishedPath: {
569
+ marginTop: '0.5rem',
570
+ fontFamily: fontMono,
571
+ fontSize: '0.8rem',
572
+ color: success,
573
+ },
574
+ previewLink: {
575
+ color: primary,
576
+ textDecoration: 'none',
577
+ fontSize: '0.85rem',
578
+ fontWeight: 500,
579
+ },
580
+ srOnly: {
581
+ position: 'absolute' as const,
582
+ width: '1px',
583
+ height: '1px',
584
+ padding: 0,
585
+ margin: '-1px',
586
+ overflow: 'hidden',
587
+ clip: 'rect(0,0,0,0)',
588
+ whiteSpace: 'nowrap' as const,
589
+ borderWidth: 0,
590
+ },
591
+ emptyState: {
592
+ textAlign: 'center' as const,
593
+ padding: '3rem',
594
+ color: textMuted,
595
+ fontSize: '0.95rem',
596
+ },
597
+ };
@@ -0,0 +1,49 @@
1
+ export type TabType = 'generate' | 'planner' | 'settings';
2
+
3
+ export type SettingsSubTab =
4
+ | 'homepage'
5
+ | 'content-ai'
6
+ | 'analysis-ai'
7
+ | 'image-ai'
8
+ | 'website';
9
+
10
+ export interface Prompt {
11
+ id: string;
12
+ name: string;
13
+ category: string;
14
+ promptText: string;
15
+ }
16
+
17
+ export interface Setting {
18
+ key: string;
19
+ value: string;
20
+ }
21
+
22
+ export interface GeneratedArticle {
23
+ title: string;
24
+ description: string;
25
+ content: string;
26
+ filepath: string;
27
+ }
28
+
29
+ export interface GeneratedImage {
30
+ url: string;
31
+ alt: string;
32
+ filepath: string;
33
+ }
34
+
35
+ export interface ScheduledPostData {
36
+ id: string;
37
+ input: string;
38
+ inputType: string;
39
+ scheduledDate: string;
40
+ status: string;
41
+ generatedTitle?: string;
42
+ generatedDescription?: string;
43
+ generatedContent?: string;
44
+ generatedTags?: string;
45
+ generatedImageData?: string;
46
+ generatedImageAlt?: string;
47
+ publishedPath?: string;
48
+ createdAt: string;
49
+ }
@@ -0,0 +1,69 @@
1
+ import { useCallback, useRef, type RefObject } from 'react';
2
+
3
+ interface UseTabNavigationOptions {
4
+ tabCount: number;
5
+ activeIndex: number;
6
+ onActivate: (index: number) => void;
7
+ }
8
+
9
+ interface UseTabNavigationReturn {
10
+ handleTabKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
11
+ tabListRef: RefObject<HTMLDivElement | null>;
12
+ }
13
+
14
+ export function useTabNavigation({
15
+ tabCount,
16
+ activeIndex,
17
+ onActivate,
18
+ }: UseTabNavigationOptions): UseTabNavigationReturn {
19
+ const tabListRef = useRef<HTMLDivElement | null>(null);
20
+
21
+ const focusTab = useCallback(
22
+ (index: number) => {
23
+ if (!tabListRef.current) return;
24
+ const buttons =
25
+ tabListRef.current.querySelectorAll<HTMLButtonElement>(
26
+ '[role="tab"]',
27
+ );
28
+ if (buttons[index]) {
29
+ buttons[index].focus();
30
+ onActivate(index);
31
+ }
32
+ },
33
+ [onActivate],
34
+ );
35
+
36
+ const handleTabKeyDown = useCallback(
37
+ (event: React.KeyboardEvent<HTMLButtonElement>) => {
38
+ let newIndex = activeIndex;
39
+
40
+ switch (event.key) {
41
+ case 'ArrowRight':
42
+ case 'ArrowDown':
43
+ event.preventDefault();
44
+ newIndex = (activeIndex + 1) % tabCount;
45
+ break;
46
+ case 'ArrowLeft':
47
+ case 'ArrowUp':
48
+ event.preventDefault();
49
+ newIndex = (activeIndex - 1 + tabCount) % tabCount;
50
+ break;
51
+ case 'Home':
52
+ event.preventDefault();
53
+ newIndex = 0;
54
+ break;
55
+ case 'End':
56
+ event.preventDefault();
57
+ newIndex = tabCount - 1;
58
+ break;
59
+ default:
60
+ return;
61
+ }
62
+
63
+ focusTab(newIndex);
64
+ },
65
+ [activeIndex, tabCount, focusTab],
66
+ );
67
+
68
+ return { handleTabKeyDown, tabListRef };
69
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'virtual:nca-ai-cms/config' {
2
+ export const contentPath: string;
3
+ export const autoPublish: boolean;
4
+ }