@atlashub/smartstack-cli 4.4.0 → 4.6.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.
@@ -251,6 +251,7 @@ import { useTranslation } from 'react-i18next';
251
251
  import { useNavigate, useParams } from 'react-router-dom';
252
252
  import { Loader2 } from 'lucide-react';
253
253
  import { DocToggleButton } from '@/components/docs/DocToggleButton';
254
+ import { DataTable } from '@/components/ui/DataTable';
254
255
 
255
256
  // API hook (generated by scaffold_api_client)
256
257
  import { useEntityList } from '@/hooks/useEntity';
@@ -328,7 +329,34 @@ export function EntityListPage() {
328
329
  </div>
329
330
  </div>
330
331
 
331
- {/* Content: SmartTable with row click → detail, edit action → /:id/edit */}
332
+ {/* Content: DataTable with row click → detail */}
333
+ {data.length === 0 ? (
334
+ <div className="text-center py-12 text-[var(--text-secondary)]">
335
+ {t('{module}:empty', 'No items found.')}
336
+ </div>
337
+ ) : (
338
+ <DataTable
339
+ data={data}
340
+ columns={[
341
+ { key: 'name', label: t('{module}:columns.name', 'Name'), sortable: true },
342
+ { key: 'code', label: t('{module}:columns.code', 'Code'), sortable: true },
343
+ { key: 'status', label: t('{module}:columns.status', 'Status'),
344
+ render: (item) => (
345
+ <span className={`px-2 py-0.5 rounded text-xs ${
346
+ item.isActive
347
+ ? 'bg-[var(--success-bg)] text-[var(--success-text)]'
348
+ : 'bg-[var(--error-bg)] text-[var(--error-text)]'
349
+ }`}>
350
+ {item.isActive ? t('common:status.active', 'Active') : t('common:status.inactive', 'Inactive')}
351
+ </span>
352
+ )
353
+ },
354
+ ]}
355
+ searchable
356
+ pagination={{ pageSize: 10 }}
357
+ onRowClick={(item) => navigate(`${item.id}`)}
358
+ />
359
+ )}
332
360
  </div>
333
361
  );
334
362
  }
@@ -434,19 +462,28 @@ const handleTabClick = (tab: TabKey) => {
434
462
  import { useState } from 'react';
435
463
  import { useTranslation } from 'react-i18next';
436
464
  import { useNavigate } from 'react-router-dom';
465
+ import { ArrowLeft } from 'lucide-react';
466
+ // For FK Guid fields: import { EntityLookup } from '@/components/ui/EntityLookup';
437
467
 
438
468
  export function EntityCreatePage() {
439
469
  const { t } = useTranslation(['{module}']);
440
470
  const navigate = useNavigate();
441
471
  const [submitting, setSubmitting] = useState(false);
472
+ const [error, setError] = useState<string | null>(null);
473
+ const [formData, setFormData] = useState<CreateEntityDto>({
474
+ name: '',
475
+ // departmentId: '', ← FK Guid field (use EntityLookup below)
476
+ });
442
477
 
443
- const handleSubmit = async (data: CreateEntityDto) => {
478
+ const handleSubmit = async (e: React.FormEvent) => {
479
+ e.preventDefault();
444
480
  try {
445
481
  setSubmitting(true);
446
- await entityApi.create(data);
482
+ setError(null);
483
+ await entityApi.create(formData);
447
484
  navigate(-1); // Back to list
448
485
  } catch (err: any) {
449
- // Handle validation errors
486
+ setError(err.message || t('{module}:errors.createFailed', 'Creation failed'));
450
487
  } finally {
451
488
  setSubmitting(false);
452
489
  }
@@ -457,8 +494,9 @@ export function EntityCreatePage() {
457
494
  {/* Back button */}
458
495
  <button
459
496
  onClick={() => navigate(-1)}
460
- className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
497
+ className="flex items-center gap-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
461
498
  >
499
+ <ArrowLeft className="w-4 h-4" />
462
500
  {t('common:actions.back', 'Back')}
463
501
  </button>
464
502
 
@@ -467,13 +505,61 @@ export function EntityCreatePage() {
467
505
  {t('{module}:actions.create', 'Create {Entity}')}
468
506
  </h1>
469
507
 
470
- {/* SmartForm NEVER in a modal */}
471
- <SmartForm
472
- fields={formFields}
473
- onSubmit={handleSubmit}
474
- onCancel={() => navigate(-1)}
475
- submitting={submitting}
476
- />
508
+ {/* Error state */}
509
+ {error && (
510
+ <div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)] rounded-[var(--radius-card)]">
511
+ <span className="text-[var(--error-text)]">{error}</span>
512
+ </div>
513
+ )}
514
+
515
+ {/* Form — NEVER in a modal */}
516
+ <form onSubmit={handleSubmit} className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] p-6 space-y-4">
517
+ {/* Text field */}
518
+ <div>
519
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
520
+ {t('{module}:form.name', 'Name')}
521
+ </label>
522
+ <input
523
+ type="text"
524
+ value={formData.name}
525
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
526
+ className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] rounded-[var(--radius-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
527
+ required
528
+ />
529
+ </div>
530
+
531
+ {/* FK Guid field — ALWAYS use EntityLookup, NEVER <select> or <input> */}
532
+ {/* <div>
533
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
534
+ {t('{module}:form.department', 'Department')}
535
+ </label>
536
+ <EntityLookup
537
+ apiEndpoint="/api/{app}/{module}/departments"
538
+ value={formData.departmentId}
539
+ onChange={(id) => setFormData(prev => ({ ...prev, departmentId: id }))}
540
+ mapOption={(dept) => ({ label: dept.name, value: dept.id })}
541
+ placeholder={t('{module}:form.selectDepartment', 'Select a department...')}
542
+ />
543
+ </div> */}
544
+
545
+ {/* Actions */}
546
+ <div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-color)]">
547
+ <button
548
+ type="button"
549
+ onClick={() => navigate(-1)}
550
+ className="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] rounded-[var(--radius-button)]"
551
+ >
552
+ {t('common:actions.cancel', 'Cancel')}
553
+ </button>
554
+ <button
555
+ type="submit"
556
+ disabled={submitting}
557
+ className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded-[var(--radius-button)] hover:bg-[var(--color-accent-600)] disabled:opacity-50"
558
+ >
559
+ {submitting ? t('common:actions.saving', 'Saving...') : t('common:actions.save', 'Save')}
560
+ </button>
561
+ </div>
562
+ </form>
477
563
  </div>
478
564
  );
479
565
  }
@@ -485,21 +571,23 @@ export function EntityCreatePage() {
485
571
  import { useState, useEffect, useCallback } from 'react';
486
572
  import { useTranslation } from 'react-i18next';
487
573
  import { useNavigate, useParams } from 'react-router-dom';
488
- import { Loader2 } from 'lucide-react';
574
+ import { Loader2, ArrowLeft } from 'lucide-react';
575
+ // For FK Guid fields: import { EntityLookup } from '@/components/ui/EntityLookup';
489
576
 
490
577
  export function EntityEditPage() {
491
578
  const { entityId } = useParams<{ entityId: string }>();
492
579
  const { t } = useTranslation(['{module}']);
493
580
  const navigate = useNavigate();
494
- const [entity, setEntity] = useState<Entity | null>(null);
581
+ const [formData, setFormData] = useState<UpdateEntityDto | null>(null);
495
582
  const [loading, setLoading] = useState(true);
496
583
  const [submitting, setSubmitting] = useState(false);
584
+ const [error, setError] = useState<string | null>(null);
497
585
 
498
586
  const loadEntity = useCallback(async () => {
499
587
  try {
500
588
  setLoading(true);
501
589
  const result = await entityApi.getById(entityId!);
502
- setEntity(result);
590
+ setFormData(result);
503
591
  } catch {
504
592
  navigate(-1);
505
593
  } finally {
@@ -509,7 +597,7 @@ export function EntityEditPage() {
509
597
 
510
598
  useEffect(() => { loadEntity(); }, [loadEntity]);
511
599
 
512
- if (loading) {
600
+ if (loading || !formData) {
513
601
  return (
514
602
  <div className="flex items-center justify-center min-h-[400px]">
515
603
  <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
@@ -517,13 +605,15 @@ export function EntityEditPage() {
517
605
  );
518
606
  }
519
607
 
520
- const handleSubmit = async (data: UpdateEntityDto) => {
608
+ const handleSubmit = async (e: React.FormEvent) => {
609
+ e.preventDefault();
521
610
  try {
522
611
  setSubmitting(true);
523
- await entityApi.update(entityId!, data);
612
+ setError(null);
613
+ await entityApi.update(entityId!, formData);
524
614
  navigate(-1); // Back to detail or list
525
615
  } catch (err: any) {
526
- // Handle validation errors
616
+ setError(err.message || t('{module}:errors.updateFailed', 'Update failed'));
527
617
  } finally {
528
618
  setSubmitting(false);
529
619
  }
@@ -534,8 +624,9 @@ export function EntityEditPage() {
534
624
  {/* Back button */}
535
625
  <button
536
626
  onClick={() => navigate(-1)}
537
- className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
627
+ className="flex items-center gap-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
538
628
  >
629
+ <ArrowLeft className="w-4 h-4" />
539
630
  {t('common:actions.back', 'Back')}
540
631
  </button>
541
632
 
@@ -544,14 +635,61 @@ export function EntityEditPage() {
544
635
  {t('{module}:actions.edit', 'Edit {Entity}')}
545
636
  </h1>
546
637
 
547
- {/* SmartForm pre-filled — NEVER in a modal */}
548
- <SmartForm
549
- fields={formFields}
550
- initialValues={entity}
551
- onSubmit={handleSubmit}
552
- onCancel={() => navigate(-1)}
553
- submitting={submitting}
554
- />
638
+ {/* Error state */}
639
+ {error && (
640
+ <div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)] rounded-[var(--radius-card)]">
641
+ <span className="text-[var(--error-text)]">{error}</span>
642
+ </div>
643
+ )}
644
+
645
+ {/* Form pre-filled — NEVER in a modal */}
646
+ <form onSubmit={handleSubmit} className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] p-6 space-y-4">
647
+ {/* Text field (pre-filled) */}
648
+ <div>
649
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
650
+ {t('{module}:form.name', 'Name')}
651
+ </label>
652
+ <input
653
+ type="text"
654
+ value={formData.name}
655
+ onChange={(e) => setFormData(prev => prev ? { ...prev, name: e.target.value } : prev)}
656
+ className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] rounded-[var(--radius-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
657
+ required
658
+ />
659
+ </div>
660
+
661
+ {/* FK Guid field — ALWAYS use EntityLookup, NEVER <select> or <input> */}
662
+ {/* <div>
663
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
664
+ {t('{module}:form.department', 'Department')}
665
+ </label>
666
+ <EntityLookup
667
+ apiEndpoint="/api/{app}/{module}/departments"
668
+ value={formData.departmentId}
669
+ onChange={(id) => setFormData(prev => prev ? { ...prev, departmentId: id } : prev)}
670
+ mapOption={(dept) => ({ label: dept.name, value: dept.id })}
671
+ placeholder={t('{module}:form.selectDepartment', 'Select a department...')}
672
+ />
673
+ </div> */}
674
+
675
+ {/* Actions */}
676
+ <div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-color)]">
677
+ <button
678
+ type="button"
679
+ onClick={() => navigate(-1)}
680
+ className="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] rounded-[var(--radius-button)]"
681
+ >
682
+ {t('common:actions.cancel', 'Cancel')}
683
+ </button>
684
+ <button
685
+ type="submit"
686
+ disabled={submitting}
687
+ className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded-[var(--radius-button)] hover:bg-[var(--color-accent-600)] disabled:opacity-50"
688
+ >
689
+ {submitting ? t('common:actions.saving', 'Saving...') : t('common:actions.save', 'Save')}
690
+ </button>
691
+ </div>
692
+ </form>
555
693
  </div>
556
694
  );
557
695
  }
@@ -636,7 +774,7 @@ const [showCreateModal, setShowCreateModal] = useState(false);
636
774
  <Dialog open={editDialogOpen}><EditForm entity={selected} /></Dialog>
637
775
 
638
776
  // WRONG: drawer for form
639
- <Drawer open={isDrawerOpen}><SmartForm /></Drawer>
777
+ <Drawer open={isDrawerOpen}><form>...</form></Drawer>
640
778
 
641
779
  // WRONG: inline form toggle
642
780
  {isEditing ? <EditForm /> : <DetailView />}
@@ -685,24 +823,25 @@ style={{ backgroundColor: '#ffffff', color: '#1a1a1a' }}
685
823
 
686
824
  ## 5. Component Rules
687
825
 
688
- | Need | Component | Source |
689
- |------|-----------|--------|
690
- | Data table | `SmartTable` | `@/components/SmartTable` |
691
- | Filters | `SmartFilter` | `@/components/SmartFilter` |
692
- | Entity cards | `EntityCard` | `@/components/EntityCard` |
693
- | Forms | `SmartForm` | `@/components/SmartForm` |
694
- | FK field lookup | `EntityLookup` | `@/components/ui/EntityLookup` |
695
- | Statistics | `StatCard` | `@/components/StatCard` |
696
- | Loading spinner | `Loader2` | `lucide-react` |
697
- | Page loader | `PageLoader` | `@/components/ui/PageLoader` |
826
+ | Need | Component | Source | Notes |
827
+ |------|-----------|--------|-------|
828
+ | Data table | `DataTable` | `@/components/ui/DataTable` | Shared component (sorting, pagination, search) |
829
+ | Entity cards | `EntityCard` | `@/components/ui/EntityCard` | Shared component (avatar, badges, actions) |
830
+ | FK field lookup | `EntityLookup` | Generate in `@/components/ui/EntityLookup` | See section 6 for full pattern |
831
+ | KPI statistics | `StatCard` | Generate locally per dashboard | See dashboard-chart.md pattern |
832
+ | Chart wrapper | `ChartCard` | Generate locally per dashboard | See dashboard-chart.md pattern |
833
+ | Loading spinner | `Loader2` | `lucide-react` | Shared |
834
+ | Page loader | `PageLoader` | `@/components/ui/PageLoader` | Shared (Suspense fallback) |
835
+ | Docs toggle | `DocToggleButton` | `@/components/docs/DocToggleButton` | Shared |
698
836
 
699
837
  ### Rules
700
838
 
701
- - **NEVER** use raw `<table>` — use SmartTable
839
+ - **NEVER** use raw `<table>` — use `DataTable` from `@/components/ui/DataTable`
702
840
  - **NEVER** create custom spinners — use `Loader2` from lucide-react
703
841
  - **NEVER** import axios directly — use `@/services/api/apiClient`
704
842
  - **ALWAYS** use `PageLoader` as Suspense fallback
705
843
  - **ALWAYS** use existing shared components before creating new ones
844
+ - **ALWAYS** use `EntityLookup` for FK Guid fields (never `<select>` or `<input>` for GUIDs)
706
845
 
707
846
  ---
708
847
 
@@ -1183,7 +1322,7 @@ Before marking frontend tasks as complete, verify:
1183
1322
  - [ ] Pages follow loading → error → content pattern
1184
1323
  - [ ] Pages use `src/pages/{App}/{Module}/` hierarchy
1185
1324
  - [ ] API calls use generated hooks or `apiClient` (never raw axios)
1186
- - [ ] Components use SmartTable/SmartFilter/EntityCard (never raw HTML tables)
1325
+ - [ ] Components use DataTable/EntityCard (never raw HTML `<table>`)
1187
1326
  - [ ] **FK fields use `EntityLookup` — ZERO plain text inputs for Guid FK fields**
1188
1327
  - [ ] **All FK fields have `mapOption` showing display name, not GUID**
1189
1328
  - [ ] **Backend APIs support `?search=` query parameter for EntityLookup**
@@ -1232,44 +1371,55 @@ export function EntityCreatePage() {
1232
1371
  <div className="space-y-6">
1233
1372
  {/* ... form header ... */}
1234
1373
 
1235
- <SmartForm fields={[
1236
- {
1237
- name: 'name',
1238
- type: 'text',
1239
- label: t('{module}:form.name', 'Name'),
1240
- required: true,
1241
- },
1242
- // Scope selector — binary toggle for optional entities
1243
- {
1244
- name: 'scope',
1245
- type: 'custom',
1246
- label: t('common:scope', 'Scope'),
1247
- render: () => (
1248
- <div className="space-y-2">
1249
- <label className="block text-sm font-medium text-[var(--text-primary)]">
1250
- {t('common:scope', 'Scope')}
1251
- </label>
1252
- <select
1253
- value={formData.isShared ? 'shared' : 'tenant'}
1254
- onChange={(e) => handleScopeChange(e.target.value)}
1255
- className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
1256
- >
1257
- <option value="tenant">
1258
- {t('common:scope.tenant', 'My Organization')}
1259
- </option>
1260
- <option value="shared">
1261
- {t('common:scope.shared', 'Shared (All Organizations)')}
1262
- </option>
1263
- </select>
1264
- <p className="text-xs text-[var(--text-secondary)]">
1265
- {formData.isShared
1266
- ? t('common:scope.shared.hint', 'This data will be accessible to all organizations')
1267
- : t('common:scope.tenant.hint', 'This data will only be visible to your organization')}
1268
- </p>
1269
- </div>
1270
- ),
1271
- },
1272
- ]} />
1374
+ <form onSubmit={handleSubmit} className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] p-6 space-y-4">
1375
+ {/* Name field */}
1376
+ <div>
1377
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
1378
+ {t('{module}:form.name', 'Name')}
1379
+ </label>
1380
+ <input
1381
+ type="text"
1382
+ value={formData.name}
1383
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
1384
+ className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] rounded-[var(--radius-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
1385
+ required
1386
+ />
1387
+ </div>
1388
+
1389
+ {/* Scope selector — binary toggle for optional entities */}
1390
+ <div className="space-y-2">
1391
+ <label className="block text-sm font-medium text-[var(--text-primary)]">
1392
+ {t('common:scope', 'Scope')}
1393
+ </label>
1394
+ <select
1395
+ value={formData.isShared ? 'shared' : 'tenant'}
1396
+ onChange={(e) => handleScopeChange(e.target.value)}
1397
+ className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
1398
+ >
1399
+ <option value="tenant">
1400
+ {t('common:scope.tenant', 'My Organization')}
1401
+ </option>
1402
+ <option value="shared">
1403
+ {t('common:scope.shared', 'Shared (All Organizations)')}
1404
+ </option>
1405
+ </select>
1406
+ <p className="text-xs text-[var(--text-secondary)]">
1407
+ {formData.isShared
1408
+ ? t('common:scope.shared.hint', 'This data will be accessible to all organizations')
1409
+ : t('common:scope.tenant.hint', 'This data will only be visible to your organization')}
1410
+ </p>
1411
+ </div>
1412
+
1413
+ {/* Actions */}
1414
+ <div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-color)]">
1415
+ <button type="button" onClick={() => navigate(-1)} className="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] rounded-[var(--radius-button)]">
1416
+ {t('common:actions.cancel', 'Cancel')}
1417
+ </button>
1418
+ <button type="submit" disabled={submitting} className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded-[var(--radius-button)] disabled:opacity-50">
1419
+ {submitting ? t('common:actions.saving', 'Saving...') : t('common:actions.save', 'Save')}
1420
+ </button>
1421
+ </div>
1422
+ </form>
1273
1423
  </div>
1274
1424
  );
1275
1425
  }
@@ -1288,27 +1438,51 @@ export function EntityCreatePage() {
1288
1438
  });
1289
1439
 
1290
1440
  return (
1291
- <SmartForm fields={[
1292
- {
1293
- name: 'name',
1294
- type: 'text',
1295
- label: t('{module}:form.name', 'Name'),
1296
- required: true,
1297
- },
1298
- {
1299
- name: 'scope',
1300
- type: 'select',
1301
- label: t('common:scope', 'Scope'),
1302
- options: [
1303
- { value: 'Tenant', label: t('common:scope.tenant', 'My Organization') },
1304
- { value: 'Shared', label: t('common:scope.shared', 'Shared') },
1305
- { value: 'Platform', label: t('common:scope.platform', 'Platform (Admin Only)') },
1306
- ],
1307
- default: 'Tenant',
1308
- required: true,
1309
- help: t('common:scope.help', 'Select the visibility level for this data'),
1310
- },
1311
- ]} />
1441
+ <form onSubmit={handleSubmit} className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] p-6 space-y-4">
1442
+ {/* Name field */}
1443
+ <div>
1444
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
1445
+ {t('{module}:form.name', 'Name')}
1446
+ </label>
1447
+ <input
1448
+ type="text"
1449
+ value={formData.name}
1450
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
1451
+ className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] rounded-[var(--radius-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
1452
+ required
1453
+ />
1454
+ </div>
1455
+
1456
+ {/* Scope selector — enum values for scoped entities */}
1457
+ <div>
1458
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
1459
+ {t('common:scope', 'Scope')}
1460
+ </label>
1461
+ <select
1462
+ value={formData.scope}
1463
+ onChange={(e) => setFormData(prev => ({ ...prev, scope: e.target.value }))}
1464
+ className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
1465
+ required
1466
+ >
1467
+ <option value="Tenant">{t('common:scope.tenant', 'My Organization')}</option>
1468
+ <option value="Shared">{t('common:scope.shared', 'Shared')}</option>
1469
+ <option value="Platform">{t('common:scope.platform', 'Platform (Admin Only)')}</option>
1470
+ </select>
1471
+ <p className="text-xs text-[var(--text-secondary)] mt-1">
1472
+ {t('common:scope.help', 'Select the visibility level for this data')}
1473
+ </p>
1474
+ </div>
1475
+
1476
+ {/* Actions */}
1477
+ <div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-color)]">
1478
+ <button type="button" onClick={() => navigate(-1)} className="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] rounded-[var(--radius-button)]">
1479
+ {t('common:actions.cancel', 'Cancel')}
1480
+ </button>
1481
+ <button type="submit" disabled={submitting} className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded-[var(--radius-button)] disabled:opacity-50">
1482
+ {submitting ? t('common:actions.saving', 'Saving...') : t('common:actions.save', 'Save')}
1483
+ </button>
1484
+ </div>
1485
+ </form>
1312
1486
  );
1313
1487
  }
1314
1488
  ```
@@ -1382,7 +1556,7 @@ export function ScopeBadge({ tenantId, scope }: ScopeBadgeProps) {
1382
1556
  }
1383
1557
  ```
1384
1558
 
1385
- ### Using ScopeBadge in SmartTable Columns
1559
+ ### Using ScopeBadge in DataTable Columns
1386
1560
 
1387
1561
  ```tsx
1388
1562
  // In the list page, add a scope column
@@ -1403,10 +1577,11 @@ const columns = [
1403
1577
  ];
1404
1578
 
1405
1579
  return (
1406
- <SmartTable
1580
+ <DataTable
1407
1581
  columns={columns}
1408
1582
  data={data}
1409
- loading={loading}
1583
+ searchable
1584
+ pagination={{ pageSize: 10 }}
1410
1585
  onRowClick={(row) => navigate(`${row.id}`)}
1411
1586
  />
1412
1587
  );
@@ -315,7 +315,7 @@ const [loading, setLoading] = useState(true);
315
315
 
316
316
  ### Components & CSS
317
317
 
318
- **Components:** SmartTable, SmartFilter, EntityCard, SmartForm, StatCard (NEVER raw HTML)
318
+ **Components:** DataTable, EntityCard, EntityLookup (NEVER raw HTML `<table>`)
319
319
  **CSS:** Variables ONLY — hardcoded Tailwind colors are **BLOCKING** in POST-CHECK 9:
320
320
 
321
321
  | Instead of | Use |
@@ -377,7 +377,7 @@ See `references/smartstack-frontend.md` section 6 for the full `EntityLookup` co
377
377
  **FORBIDDEN:**
378
378
  - `src/pages/{Module}/` (flat, missing App)
379
379
  - `import axios` → use `@/services/api/apiClient`
380
- - `<table>` → use SmartTable
380
+ - `<table>` → use DataTable
381
381
  - `<input type="text">` for FK Guid fields → use `EntityLookup`
382
382
  - `<select>` for FK Guid fields → use `EntityLookup` (even with API-loaded options)
383
383
  - Placeholder "Enter ID" or "Enter GUID" → use `EntityLookup`
@@ -299,12 +299,22 @@ test({module}): backend unit and integration tests
299
299
  For each module:
300
300
  - **API client:** MCP scaffold_api_client
301
301
  - **Routes:** MCP scaffold_routes (outputFormat: 'clientRoutes') → generates lazy imports + Suspense
302
+ - **Wire Routes to App.tsx (BLOCKING):** After scaffold_routes, routes MUST be wired into App.tsx:
303
+ → Read App.tsx and detect the routing pattern
304
+ → **Pattern A** (`applicationRoutes: ApplicationRouteExtensions`): add routes to `applicationRoutes['{application_kebab}'][]` with RELATIVE paths
305
+ → **Pattern B** (JSX `<Route>`): nest routes inside `<Route path="/{application}" element={<AppLayout />}>` + duplicate in tenant block
306
+ → **FORBIDDEN:** Adding business routes to `clientRoutes[]` — it is ONLY for non-app routes (`/about`, `/pricing`)
307
+ → ALL business applications use `<AppLayout />` as layout wrapper
308
+ → See `references/frontend-route-wiring-app-tsx.md` for full Pattern A/B detection and examples
309
+ → Verify: `mcp__smartstack__validate_frontend_routes (scope: 'routes')`
302
310
  - **Pages:** use /ui-components skill — MUST follow smartstack-frontend.md patterns:
303
311
  → React.lazy() for all page imports (named export wrapping)
304
312
  → `<Suspense fallback={<PageLoader />}>` around all lazy components
305
313
  → Page structure: hooks → useEffect(load) → loading → error → content
306
314
  → CSS variables only: bg-[var(--bg-card)], text-[var(--text-primary)]
307
- SmartTable/SmartFilter/EntityCard (NEVER raw HTML)
315
+ DataTable/EntityCard (NEVER raw HTML `<table>`)
316
+ → If entity has FK Guid fields: generate EntityLookup component in @/components/ui/EntityLookup (see smartstack-frontend.md section 6)
317
+ → Dashboard pages: generate StatCard + ChartCard locally (see ui-components patterns/dashboard-chart.md)
308
318
  - **FORM PAGES (CRITICAL):** Create/Edit forms are FULL PAGES with own routes:
309
319
  → EntityCreatePage.tsx with route /{module}/create
310
320
  → EntityEditPage.tsx with route /{module}/:id/edit
@@ -368,6 +378,13 @@ For each entity:
368
378
  **MANDATORY: Read references/smartstack-frontend.md FIRST**
369
379
  - API client: MCP scaffold_api_client
370
380
  - Routes: MCP scaffold_routes (outputFormat: 'clientRoutes')
381
+ - Wire Routes to App.tsx (BLOCKING): After scaffold_routes, wire into App.tsx:
382
+ → Read App.tsx, detect Pattern A (applicationRoutes) or Pattern B (JSX Route)
383
+ → Pattern A: add to applicationRoutes['{application_kebab}'][] with RELATIVE paths
384
+ → Pattern B: nest inside <Route path='/{application}' element={<AppLayout />}> + tenant block
385
+ → FORBIDDEN: Adding business routes to clientRoutes[] (ONLY for /about, /pricing)
386
+ → See references/frontend-route-wiring-app-tsx.md for full patterns
387
+ → Verify: mcp__smartstack__validate_frontend_routes (scope: 'routes')
371
388
  - Pages: /ui-components skill (ALL 4 types: List, Detail, Create, Edit)
372
389
  - I18n: 4 JSON files (fr, en, it, de)
373
390
  - FORM PAGES: Full pages with own routes (ZERO modals)
@@ -57,7 +57,7 @@ For each changed file, check:
57
57
  - [ ] Random GUIDs in seed data via Guid.NewGuid() (no deterministic/sequential/fixed)
58
58
  - [ ] 4 languages in translations
59
59
  - [ ] CSS variables (not hardcoded colors)
60
- - [ ] SmartTable/SmartForm (not raw HTML tables/forms)
60
+ - [ ] DataTable (not raw HTML `<table>`) and concrete form markup (not non-existent SmartForm)
61
61
  - [ ] Correct Layout wrapper per application
62
62
 
63
63
  **FK Fields & Forms:**
@@ -77,7 +77,7 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
77
77
  id: taskId,
78
78
  description: `[frontend] Generate COMPLETE frontend for module ${moduleCode} via MCP (${frontendFiles.length} files)`,
79
79
  status: "pending", category: "frontend", dependencies: apiDepId ? [apiDepId] : [],
80
- acceptance_criteria: `Pages in src/pages/${app}/${moduleCode}/; MCP scaffold_api_client + scaffold_routes; SmartTable for lists; CSS variables ONLY; 4-language i18n; npm run typecheck passes`,
80
+ acceptance_criteria: `Pages in src/pages/${app}/${moduleCode}/; MCP scaffold_api_client + scaffold_routes; DataTable for lists; CSS variables ONLY; 4-language i18n; npm run typecheck passes`,
81
81
  started_at: null, completed_at: null, iteration: null, commit_hash: null,
82
82
  files_changed: { created: frontendFiles.map(f => f.path), modified: [] },
83
83
  validation: null, error: null, module: moduleCode,
@@ -136,7 +136,7 @@ function transformPrdJsonToRalphV2(prdJson, moduleCode) {
136
136
  tasks.push({ id: taskId,
137
137
  description: `[frontend] Generate COMPLETE frontend for ${moduleCode} via MCP (${derivedFiles.length} files)`,
138
138
  status: "pending", category: "frontend", dependencies: [lastIdByCategory["api"] || lastIdByCategory["application"]].filter(Boolean),
139
- acceptance_criteria: `Pages in src/pages/${app}/${moduleCode}/; SmartTable for lists; CSS variables ONLY; 4-language i18n; npm run typecheck passes`,
139
+ acceptance_criteria: `Pages in src/pages/${app}/${moduleCode}/; DataTable for lists; CSS variables ONLY; 4-language i18n; npm run typecheck passes`,
140
140
  started_at: null, completed_at: null, iteration: null, commit_hash: null,
141
141
  files_changed: { created: derivedFiles.map(f => f.path), modified: [] },
142
142
  validation: null, error: null, module: moduleCode,