@schandlergarcia/sf-web-components 1.9.37 → 1.9.39

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 (109) hide show
  1. package/package.json +4 -1
  2. package/scripts/postinstall.mjs +116 -65
  3. package/src/components/library/cards/ActionList.jsx +38 -0
  4. package/src/components/library/cards/ActivityCard.jsx +56 -0
  5. package/src/components/library/cards/BaseCard.jsx +109 -0
  6. package/src/components/library/cards/CalloutCard.jsx +37 -0
  7. package/src/components/library/cards/ChartCard.jsx +105 -0
  8. package/src/components/library/cards/FeedPanel.jsx +39 -0
  9. package/src/components/library/cards/ListCard.jsx +193 -0
  10. package/src/components/library/cards/MetricCard.jsx +109 -0
  11. package/src/components/library/cards/MetricsStrip.jsx +78 -0
  12. package/src/components/library/cards/SectionCard.jsx +83 -0
  13. package/src/components/library/cards/SemanticMetricCard.jsx +52 -0
  14. package/src/components/library/cards/SemanticMetricCardWithLoading.jsx +23 -0
  15. package/src/components/library/cards/SemanticTableCard.jsx +48 -0
  16. package/src/components/library/cards/SemanticTableCardWithLoading.jsx +22 -0
  17. package/src/components/library/cards/StatusCard.jsx +220 -0
  18. package/src/components/library/cards/TableCard.jsx +337 -0
  19. package/src/components/library/cards/WidgetCard.jsx +90 -0
  20. package/src/components/library/charts/D3Chart.jsx +109 -0
  21. package/src/components/library/charts/D3ChartTemplates.jsx +126 -0
  22. package/src/components/library/charts/GeoMap.jsx +293 -0
  23. package/src/components/library/chat/ChatBar.jsx +256 -0
  24. package/src/components/library/chat/ChatInput.jsx +89 -0
  25. package/src/components/library/chat/ChatMessage.jsx +178 -0
  26. package/src/components/library/chat/ChatMessageList.jsx +73 -0
  27. package/src/components/library/chat/ChatPanel.jsx +97 -0
  28. package/src/components/library/chat/ChatSuggestions.jsx +28 -0
  29. package/src/components/library/chat/ChatToolCall.jsx +100 -0
  30. package/src/components/library/chat/ChatTypingIndicator.jsx +23 -0
  31. package/src/components/library/chat/ChatWelcome.jsx +43 -0
  32. package/src/components/library/chat/index.jsx +10 -0
  33. package/src/components/library/chat/useChatState.jsx +130 -0
  34. package/src/components/library/data/DataModeProvider.jsx +67 -0
  35. package/src/components/library/data/DataModeToggle.jsx +36 -0
  36. package/src/components/library/data/chartDataProvider.jsx +61 -0
  37. package/src/components/library/data/filterUtils.jsx +141 -0
  38. package/src/components/library/data/useDataSource.jsx +33 -0
  39. package/src/components/library/data/usePageFilters.jsx +99 -0
  40. package/src/components/library/filters/FilterBar.jsx +95 -0
  41. package/src/components/library/filters/SearchFilter.jsx +36 -0
  42. package/src/components/library/filters/SelectFilter.jsx +55 -0
  43. package/src/components/library/filters/ToggleFilter.jsx +52 -0
  44. package/src/components/library/filters/index.jsx +4 -0
  45. package/src/components/library/forms/FormField.jsx +291 -0
  46. package/src/components/library/forms/FormModal.jsx +201 -0
  47. package/src/components/library/forms/FormRenderer.jsx +46 -0
  48. package/src/components/library/forms/FormSection.jsx +69 -0
  49. package/src/components/library/forms/index.jsx +5 -0
  50. package/src/components/library/forms/useFormState.jsx +165 -0
  51. package/src/components/library/heroui/Accordion.jsx +26 -0
  52. package/src/components/library/heroui/Alert.jsx +8 -0
  53. package/src/components/library/heroui/Badge.jsx +8 -0
  54. package/src/components/library/heroui/Breadcrumbs.jsx +22 -0
  55. package/src/components/library/heroui/Button.jsx +58 -0
  56. package/src/components/library/heroui/Card.jsx +8 -0
  57. package/src/components/library/heroui/Collapsible.jsx +42 -0
  58. package/src/components/library/heroui/DatePicker.jsx +34 -0
  59. package/src/components/library/heroui/Dialog.jsx +37 -0
  60. package/src/components/library/heroui/Drawer.jsx +32 -0
  61. package/src/components/library/heroui/Dropdown.jsx +28 -0
  62. package/src/components/library/heroui/Field.jsx +51 -0
  63. package/src/components/library/heroui/Input.jsx +6 -0
  64. package/src/components/library/heroui/Kbd.jsx +8 -0
  65. package/src/components/library/heroui/Meter.jsx +8 -0
  66. package/src/components/library/heroui/Modal.jsx +32 -0
  67. package/src/components/library/heroui/Pagination.jsx +8 -0
  68. package/src/components/library/heroui/Popover.jsx +64 -0
  69. package/src/components/library/heroui/ProgressBar.jsx +8 -0
  70. package/src/components/library/heroui/ProgressCircle.jsx +8 -0
  71. package/src/components/library/heroui/ScrollShadow.jsx +8 -0
  72. package/src/components/library/heroui/Select.jsx +37 -0
  73. package/src/components/library/heroui/Separator.jsx +8 -0
  74. package/src/components/library/heroui/Skeleton.jsx +8 -0
  75. package/src/components/library/heroui/Tabs.jsx +26 -0
  76. package/src/components/library/heroui/Toast.jsx +25 -0
  77. package/src/components/library/heroui/Toggle.jsx +14 -0
  78. package/src/components/library/heroui/Tooltip.jsx +21 -0
  79. package/src/components/library/index.jsx +146 -0
  80. package/src/components/library/layout/PageContainer.jsx +11 -0
  81. package/src/components/library/skeletons/CardSkeleton.jsx +30 -0
  82. package/src/components/library/theme/AppThemeProvider.jsx +67 -0
  83. package/src/components/library/theme/tokens.jsx +72 -0
  84. package/src/components/library/ui/Alert.jsx +80 -0
  85. package/src/components/library/ui/Avatar.jsx +44 -0
  86. package/src/components/library/ui/BreadcrumbExtras.tsx +120 -0
  87. package/src/components/library/ui/Button.jsx +61 -0
  88. package/src/components/library/ui/Card.jsx +117 -0
  89. package/src/components/library/ui/Checkbox.jsx +17 -0
  90. package/src/components/library/ui/Chip.jsx +38 -0
  91. package/src/components/library/ui/Collapsible.tsx +31 -0
  92. package/src/components/library/ui/Container.jsx +56 -0
  93. package/src/components/library/ui/DatePicker.tsx +34 -0
  94. package/src/components/library/ui/Dialog.tsx +141 -0
  95. package/src/components/library/ui/EmptyState.jsx +46 -0
  96. package/src/components/library/ui/Field.tsx +82 -0
  97. package/src/components/library/ui/FieldGroup.jsx +17 -0
  98. package/src/components/library/ui/Input.jsx +21 -0
  99. package/src/components/library/ui/Label.jsx +22 -0
  100. package/src/components/library/ui/PaginationExtras.tsx +142 -0
  101. package/src/components/library/ui/Popover.tsx +39 -0
  102. package/src/components/library/ui/Select.tsx +113 -0
  103. package/src/components/library/ui/Spinner.d.ts +10 -0
  104. package/src/components/library/ui/Spinner.jsx +64 -0
  105. package/src/components/library/ui/Text.jsx +46 -0
  106. package/src/components/library/ui/Toggle.jsx +42 -0
  107. package/src/components/workspace/ComponentRegistry.jsx +297 -0
  108. package/src/lib/index.ts +1 -0
  109. package/src/lib/utils.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schandlergarcia/sf-web-components",
3
- "version": "1.9.37",
3
+ "version": "1.9.39",
4
4
  "description": "Reusable Salesforce web components library with Tailwind CSS v4 and shadcn/ui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,6 +28,9 @@
28
28
  "dist",
29
29
  "scripts",
30
30
  "data",
31
+ "src/components",
32
+ "src/lib",
33
+ "src/types",
31
34
  "README.md",
32
35
  ".a4drules"
33
36
  ],
@@ -3,7 +3,11 @@
3
3
  /**
4
4
  * Post-install script for @schandlergarcia/sf-web-components
5
5
  *
6
- * Automatically copies sample data to the consuming project's src/data directory
6
+ * Automatically copies to the consuming project:
7
+ * - Component library source (src/components/library/)
8
+ * - Sample data (src/data/)
9
+ * - Skills (.a4drules/)
10
+ * - Scripts (scripts/)
7
11
  */
8
12
 
9
13
  import fs from 'fs';
@@ -20,97 +24,144 @@ const PACKAGE_ROOT = path.resolve(__dirname, '..');
20
24
  // Go up from node_modules/@schandlergarcia/sf-web-components
21
25
  const PROJECT_ROOT = path.resolve(PACKAGE_ROOT, '../../..');
22
26
 
23
- console.log('\n📦 @schandlergarcia/sf-web-components post-install');
27
+ console.log('\n📦 @schandlergarcia/sf-web-components post-install\n');
24
28
 
25
29
  // Check if we're in a node_modules context (not during package development)
26
30
  if (!PACKAGE_ROOT.includes('node_modules')) {
27
- console.log(' ℹ️ Running in development mode, skipping data copy');
31
+ console.log(' ℹ️ Running in development mode, skipping setup\n');
28
32
  process.exit(0);
29
33
  }
30
34
 
31
- // Find src/data directory in the consuming project
32
- const possiblePaths = [
33
- path.join(PROJECT_ROOT, 'src/data'),
34
- path.join(PROJECT_ROOT, 'force-app/main/default/webapplications/src/data'),
35
- ];
36
-
37
- // Try to find an existing src directory
38
- let targetDir = null;
39
- for (const p of possiblePaths) {
40
- const srcDir = path.dirname(p); // Get src directory
41
- if (fs.existsSync(srcDir)) {
42
- targetDir = p;
43
- break;
35
+ // Find webapp directory
36
+ function findWebappDir() {
37
+ // Try direct src/
38
+ if (fs.existsSync(path.join(PROJECT_ROOT, 'src'))) {
39
+ return PROJECT_ROOT;
44
40
  }
45
- }
46
41
 
47
- // If no src directory found, look for any webapp with src/
48
- const webappPattern = /force-app\/main\/default\/webapplications\/[^/]+$/;
49
- if (!targetDir) {
50
- try {
51
- const webappRoot = path.join(PROJECT_ROOT, 'force-app/main/default/webapplications');
52
- if (fs.existsSync(webappRoot)) {
53
- const webapps = fs.readdirSync(webappRoot, { withFileTypes: true });
54
- for (const webapp of webapps) {
55
- if (webapp.isDirectory()) {
56
- const srcDir = path.join(webappRoot, webapp.name, 'src');
57
- if (fs.existsSync(srcDir)) {
58
- targetDir = path.join(srcDir, 'data');
59
- break;
60
- }
42
+ // Try force-app structure
43
+ const webappRoot = path.join(PROJECT_ROOT, 'force-app/main/default/webapplications');
44
+ if (fs.existsSync(webappRoot)) {
45
+ const webapps = fs.readdirSync(webappRoot, { withFileTypes: true });
46
+ for (const webapp of webapps) {
47
+ if (webapp.isDirectory()) {
48
+ const webappDir = path.join(webappRoot, webapp.name);
49
+ if (fs.existsSync(path.join(webappDir, 'src'))) {
50
+ return webappDir;
61
51
  }
62
52
  }
63
53
  }
64
- } catch (err) {
65
- // Ignore errors
66
54
  }
55
+
56
+ return null;
67
57
  }
68
58
 
69
- if (!targetDir) {
70
- console.log(' ⚠️ Could not find src/ directory in project');
71
- console.log(' ℹ️ To manually copy sample data, run:');
72
- console.log('');
73
- console.log(' mkdir -p src/data');
74
- console.log(' cp node_modules/@schandlergarcia/sf-web-components/data/engine-sample-data.js src/data/');
75
- console.log('');
59
+ const WEBAPP_DIR = findWebappDir();
60
+
61
+ if (!WEBAPP_DIR) {
62
+ console.log(' ⚠️ Could not find webapp directory with src/');
63
+ console.log(' ℹ️ Skipping automated setup\n');
76
64
  process.exit(0);
77
65
  }
78
66
 
79
- // Create target directory if it doesn't exist
67
+ let copiedCount = 0;
68
+
69
+ // Helper: Copy directory recursively
70
+ function copyRecursive(src, dest) {
71
+ if (fs.statSync(src).isDirectory()) {
72
+ if (!fs.existsSync(dest)) {
73
+ fs.mkdirSync(dest, { recursive: true });
74
+ }
75
+ fs.readdirSync(src).forEach(file => {
76
+ copyRecursive(path.join(src, file), path.join(dest, file));
77
+ });
78
+ } else {
79
+ fs.copyFileSync(src, dest);
80
+ }
81
+ }
82
+
83
+ // 1. Copy component library source
80
84
  try {
81
- if (!fs.existsSync(targetDir)) {
82
- fs.mkdirSync(targetDir, { recursive: true });
85
+ const targetComponentsDir = path.join(WEBAPP_DIR, 'src/components');
86
+ const sourceLibraryDir = path.join(PACKAGE_ROOT, 'src/components/library');
87
+
88
+ if (fs.existsSync(sourceLibraryDir)) {
89
+ const targetLibraryDir = path.join(targetComponentsDir, 'library');
90
+
91
+ if (!fs.existsSync(targetLibraryDir)) {
92
+ copyRecursive(sourceLibraryDir, targetLibraryDir);
93
+ console.log(' ✅ Component library → src/components/library/');
94
+ copiedCount++;
95
+ }
83
96
  }
84
97
  } catch (err) {
85
- console.error(' Failed to create data directory:', err.message);
86
- process.exit(0); // Don't fail the install
98
+ console.log(' ⚠️ Could not copy component library:', err.message);
87
99
  }
88
100
 
89
- // Copy sample data file
90
- const sourceFile = path.join(PACKAGE_ROOT, 'data/engine-sample-data.js');
91
- const targetFile = path.join(targetDir, 'engine-sample-data.js');
92
-
101
+ // 2. Copy sample data
93
102
  try {
94
- // Only copy if target doesn't exist (don't overwrite user's modifications)
103
+ const dataDir = path.join(WEBAPP_DIR, 'src/data');
104
+ if (!fs.existsSync(dataDir)) {
105
+ fs.mkdirSync(dataDir, { recursive: true });
106
+ }
107
+
108
+ const sourceFile = path.join(PACKAGE_ROOT, 'data/engine-sample-data.js');
109
+ const targetFile = path.join(dataDir, 'engine-sample-data.js');
110
+
95
111
  if (!fs.existsSync(targetFile)) {
96
112
  fs.copyFileSync(sourceFile, targetFile);
97
- console.log(' ✅ Sample data copied to:', path.relative(PROJECT_ROOT, targetFile));
98
- console.log('');
99
- console.log(' Import in your components:');
100
- console.log('');
101
- console.log(" import { MAP_MARKERS, METRICS } from '@/data/engine-sample-data';");
102
- console.log('');
103
- } else {
104
- console.log(' ℹ️ Sample data already exists (not overwriting)');
105
- console.log(' 📁', path.relative(PROJECT_ROOT, targetFile));
106
- console.log('');
113
+ console.log(' ✅ Sample data src/data/engine-sample-data.js');
114
+ copiedCount++;
107
115
  }
108
116
  } catch (err) {
109
- console.error(' ⚠️ Could not copy sample data:', err.message);
110
- console.log(' ℹ️ To manually copy, run:');
111
- console.log('');
112
- console.log(' cp node_modules/@schandlergarcia/sf-web-components/data/engine-sample-data.js src/data/');
113
- console.log('');
117
+ console.log(' ⚠️ Could not copy sample data:', err.message);
118
+ }
119
+
120
+ // 3. Copy .a4drules (skills)
121
+ try {
122
+ const targetRulesDir = path.join(WEBAPP_DIR, '.a4drules');
123
+ const sourceRulesDir = path.join(PACKAGE_ROOT, '.a4drules');
124
+
125
+ if (!fs.existsSync(targetRulesDir) && fs.existsSync(sourceRulesDir)) {
126
+ copyRecursive(sourceRulesDir, targetRulesDir);
127
+ console.log(' ✅ Skills → .a4drules/');
128
+ copiedCount++;
129
+ }
130
+ } catch (err) {
131
+ console.log(' ⚠️ Could not copy skills:', err.message);
132
+ }
133
+
134
+ // 4. Copy scripts
135
+ try {
136
+ const scriptsDir = path.join(WEBAPP_DIR, 'scripts');
137
+ if (!fs.existsSync(scriptsDir)) {
138
+ fs.mkdirSync(scriptsDir, { recursive: true });
139
+ }
140
+
141
+ const scriptsToCopy = ['reset-command-center.sh', 'validate-dashboard.sh'];
142
+ for (const script of scriptsToCopy) {
143
+ const sourceFile = path.join(PACKAGE_ROOT, 'scripts', script);
144
+ const targetFile = path.join(scriptsDir, script);
145
+
146
+ if (fs.existsSync(sourceFile) && !fs.existsSync(targetFile)) {
147
+ fs.copyFileSync(sourceFile, targetFile);
148
+ fs.chmodSync(targetFile, 0o755); // Make executable
149
+ console.log(` ✅ Script → scripts/${script}`);
150
+ copiedCount++;
151
+ }
152
+ }
153
+ } catch (err) {
154
+ console.log(' ⚠️ Could not copy scripts:', err.message);
155
+ }
156
+
157
+ // Summary
158
+ if (copiedCount > 0) {
159
+ console.log(`\n 🎉 Setup complete! ${copiedCount} items copied\n`);
160
+ console.log(' Next steps:\n');
161
+ console.log(' npm run reset:command-center # Reset to baseline state');
162
+ console.log(' npm run dev # Start dev server\n');
163
+ } else {
164
+ console.log(' ℹ️ All files already exist (not overwriting)\n');
114
165
  }
115
166
 
116
167
  process.exit(0);
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import UIButton from "../ui/Button";
3
+
4
+ /**
5
+ * Row of action buttons — typically used at the bottom of a dashboard section.
6
+ *
7
+ * @param {{ label: string, [key]: any }[] | string[]} actions
8
+ * @param {string} title
9
+ * @param {Function} onAction Called with the action object/string when clicked
10
+ */
11
+ export default function ActionList({
12
+ actions = [],
13
+ title,
14
+ onAction,
15
+ className = "",
16
+ }) {
17
+ return (
18
+ <div className={`rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900 ${className}`}>
19
+ {title && (
20
+ <div className="mb-3 text-sm font-medium text-slate-900 dark:text-slate-50">
21
+ {title}
22
+ </div>
23
+ )}
24
+ <div className="flex flex-wrap gap-2">
25
+ {actions.map((action, i) => (
26
+ <UIButton
27
+ key={i}
28
+ size="sm"
29
+ variant={i === 0 ? "primary" : "outline"}
30
+ onClick={() => onAction?.(action)}
31
+ >
32
+ {typeof action === "string" ? action : action.label}
33
+ </UIButton>
34
+ ))}
35
+ </div>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { ArrowPathIcon, CheckCircleIcon, ExclamationCircleIcon, ClockIcon } from "@heroicons/react/24/outline";
4
+ import UIText from "../ui/Text";
5
+
6
+ const STATUS_ICON = {
7
+ working: { Icon: ArrowPathIcon, color: "text-indigo-500", spin: true },
8
+ pending: { Icon: ClockIcon, color: "text-slate-400", spin: false },
9
+ complete: { Icon: CheckCircleIcon, color: "text-emerald-500", spin: false },
10
+ error: { Icon: ExclamationCircleIcon, color: "text-red-500", spin: false },
11
+ };
12
+
13
+ function ActionItem({ action }) {
14
+ const s = STATUS_ICON[action.status] ?? STATUS_ICON.pending;
15
+ return (
16
+ <motion.div
17
+ initial={{ y: 12, opacity: 0 }}
18
+ animate={{ y: 0, opacity: 1 }}
19
+ exit={{ y: -12, opacity: 0 }}
20
+ className="rounded-lg border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-950/40"
21
+ >
22
+ <div className="flex items-start gap-2">
23
+ <s.Icon className={`mt-0.5 h-4 w-4 shrink-0 ${s.color} ${s.spin ? "animate-spin" : ""}`} />
24
+ <div className="min-w-0">
25
+ <div className="text-xs font-medium text-slate-700 dark:text-slate-200">{action.title ?? action.action}</div>
26
+ {(action.subtitle ?? action.traveler ?? action.timestamp ?? action.startedAt) && (
27
+ <div className="mt-0.5 text-[10px] text-slate-400">
28
+ {[action.subtitle, action.traveler, action.timestamp ?? action.startedAt].filter(Boolean).join(" · ")}
29
+ </div>
30
+ )}
31
+ </div>
32
+ </div>
33
+ </motion.div>
34
+ );
35
+ }
36
+
37
+ export default function ActivityCard({ title = "Activity", actions = [], className = "" }) {
38
+ if (actions.length === 0) return null;
39
+
40
+ return (
41
+ <div className={className}>
42
+ {title && (
43
+ <UIText as="div" size="xs" weight="semibold" muted className="mb-2 uppercase tracking-wider">
44
+ {title}
45
+ </UIText>
46
+ )}
47
+ <div className="space-y-2">
48
+ <AnimatePresence>
49
+ {actions.map(a => (
50
+ <ActionItem key={a.id} action={a} />
51
+ ))}
52
+ </AnimatePresence>
53
+ </div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,109 @@
1
+ import React from "react";
2
+
3
+ const VARIANT_CLASSES = {
4
+ default: "",
5
+ metric: "",
6
+ chart: "",
7
+ table: "",
8
+ widget: "",
9
+ status: ""
10
+ };
11
+
12
+ const SIZE_CLASSES = {
13
+ xs: "",
14
+ sm: "min-h-[80px]",
15
+ md: "min-h-[120px]",
16
+ lg: "min-h-[160px]",
17
+ xl: "min-h-[220px]",
18
+ full: ""
19
+ };
20
+
21
+ const PADDING_CLASSES = {
22
+ none: "p-0",
23
+ xs: "p-2",
24
+ sm: "p-3",
25
+ default: "p-4",
26
+ lg: "p-6",
27
+ xl: "p-8"
28
+ };
29
+
30
+ export default function BaseCard({
31
+ header,
32
+ body,
33
+ footer,
34
+ children,
35
+ variant = "default",
36
+ size = "md",
37
+ padding = "default",
38
+ shadow = true,
39
+ radius = "2xl",
40
+ border = true,
41
+ isHoverable = false,
42
+ isPressable = false,
43
+ isLoading = false,
44
+ isDisabled = false,
45
+ isSelected = false,
46
+ className = "",
47
+ headerClassName = "",
48
+ bodyClassName = "",
49
+ footerClassName = "",
50
+ onPress,
51
+ onHover,
52
+ ...rest
53
+ }) {
54
+ const Comp = isPressable ? "button" : "div";
55
+
56
+ const radiusClass = radius ? `rounded-${radius}` : "rounded-2xl";
57
+ const paddingClass = PADDING_CLASSES[padding] ?? PADDING_CLASSES.default;
58
+ const sizeClass = SIZE_CLASSES[size] ?? SIZE_CLASSES.md;
59
+ const variantClass = VARIANT_CLASSES[variant] ?? VARIANT_CLASSES.default;
60
+
61
+ const interactive = isHoverable || isPressable;
62
+ const disabled = isDisabled || (isPressable && !onPress);
63
+
64
+ return (
65
+ <Comp
66
+ type={isPressable ? "button" : undefined}
67
+ onClick={isPressable ? onPress : undefined}
68
+ onMouseEnter={onHover}
69
+ disabled={isPressable ? disabled : undefined}
70
+ className={[
71
+ "w-full text-left",
72
+ radiusClass,
73
+ border ? "border border-slate-200 dark:border-slate-800" : "border border-transparent",
74
+ "bg-white dark:bg-slate-900",
75
+ shadow ? "shadow-sm" : "",
76
+ sizeClass,
77
+ variantClass,
78
+ interactive ? "transition hover:-translate-y-[1px] hover:shadow-md" : "",
79
+ isSelected ? "ring-2 ring-brand-500" : "",
80
+ disabled ? "opacity-60" : "",
81
+ className
82
+ ]
83
+ .filter(Boolean)
84
+ .join(" ")}
85
+ {...rest}
86
+ >
87
+ <div className={paddingClass}>
88
+ {header != null ? <div className={headerClassName}>{header}</div> : null}
89
+
90
+ {isLoading ? (
91
+ <div className={["mt-3 space-y-3", bodyClassName].filter(Boolean).join(" ")}>
92
+ <div className="h-4 w-1/3 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
93
+ <div className="h-4 w-2/3 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
94
+ <div className="h-4 w-1/2 animate-pulse rounded bg-slate-200 dark:bg-slate-800" />
95
+ </div>
96
+ ) : (
97
+ <>
98
+ {body != null ? <div className={bodyClassName}>{body}</div> : null}
99
+ {children}
100
+ </>
101
+ )}
102
+
103
+ {footer != null ? <div className={footerClassName}>{footer}</div> : null}
104
+ </div>
105
+ </Comp>
106
+ );
107
+ }
108
+
109
+
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+
3
+ const TONE_CLASSES = {
4
+ neutral: "border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-800 dark:bg-slate-950/30 dark:text-slate-200",
5
+ success: "border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900/40 dark:bg-emerald-950/20 dark:text-emerald-200",
6
+ warning: "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/20 dark:text-amber-200",
7
+ danger: "border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-900/40 dark:bg-rose-950/20 dark:text-rose-200",
8
+ info: "border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-900/40 dark:bg-blue-950/20 dark:text-blue-200",
9
+ };
10
+
11
+ /**
12
+ * Highlighted callout box for important inline messages.
13
+ *
14
+ * @param {string} title
15
+ * @param {string|ReactNode} message
16
+ * @param {"neutral"|"success"|"warning"|"danger"|"info"} tone
17
+ * @param {ReactNode} icon Optional leading icon
18
+ */
19
+ export default function CalloutCard({
20
+ title,
21
+ message,
22
+ tone = "neutral",
23
+ icon,
24
+ className = "",
25
+ }) {
26
+ return (
27
+ <div className={`rounded-xl border p-4 ${TONE_CLASSES[tone] ?? TONE_CLASSES.neutral} ${className}`}>
28
+ <div className="flex gap-3">
29
+ {icon && <div className="mt-0.5 shrink-0">{icon}</div>}
30
+ <div>
31
+ {title && <div className="mb-1 text-sm font-semibold">{title}</div>}
32
+ <div className="text-sm">{message}</div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,105 @@
1
+ import React from "react";
2
+ import BaseCard from "./BaseCard";
3
+ import UIText from "../ui/Text";
4
+ import UIChip from "../ui/Chip";
5
+
6
+ export default function ChartCard({
7
+ chart,
8
+ chartType,
9
+ title,
10
+ subtitle,
11
+ filters,
12
+ timeRange,
13
+ actions,
14
+ legend,
15
+ height = 280,
16
+ showGrid,
17
+ showAxes,
18
+ data,
19
+ loading = false,
20
+ error,
21
+ ...cardProps
22
+ }) {
23
+ const header = (
24
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
25
+ <div className="min-w-0">
26
+ {title ? (
27
+ <UIText as="div" size="sm" weight="medium">
28
+ {title}
29
+ </UIText>
30
+ ) : null}
31
+ {subtitle ? (
32
+ <UIText as="div" size="xs" muted className="mt-1">
33
+ {subtitle}
34
+ </UIText>
35
+ ) : null}
36
+ </div>
37
+ <div className="flex flex-wrap items-center justify-end gap-2">
38
+ {filters ? <div className="flex items-center gap-2">{filters}</div> : null}
39
+ {timeRange ? (
40
+ <select
41
+ className="h-9 rounded-lg border border-slate-200 bg-white px-3 text-sm text-slate-900 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-50"
42
+ value={timeRange.current}
43
+ onChange={(e) => timeRange.onChange?.(e.target.value)}
44
+ aria-label="Time range"
45
+ >
46
+ {timeRange.options?.map((opt) => (
47
+ <option key={opt.value ?? opt} value={opt.value ?? opt}>
48
+ {opt.label ?? opt}
49
+ </option>
50
+ ))}
51
+ </select>
52
+ ) : null}
53
+ {actions ? <div className="flex items-center gap-2">{actions}</div> : null}
54
+ </div>
55
+ </div>
56
+ );
57
+
58
+ if (error) {
59
+ return (
60
+ <BaseCard
61
+ variant="chart"
62
+ header={header}
63
+ body={
64
+ <div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900 dark:border-rose-900/40 dark:bg-rose-950/30 dark:text-rose-100">
65
+ {String(error)}
66
+ </div>
67
+ }
68
+ {...cardProps}
69
+ />
70
+ );
71
+ }
72
+
73
+ const hint = chartType ? <UIChip>{chartType}</UIChip> : null;
74
+
75
+ return (
76
+ <BaseCard
77
+ variant="chart"
78
+ header={
79
+ <div className="flex items-center justify-between gap-3">
80
+ <div className="min-w-0">{header}</div>
81
+ {hint}
82
+ </div>
83
+ }
84
+ body={
85
+ <div className="mt-4">
86
+ <div className="w-full" style={{ height }}>
87
+ {chart}
88
+ </div>
89
+ {legend ? <div className="mt-3">{legend}</div> : null}
90
+ {(showGrid != null || showAxes != null) && data ? (
91
+ <div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
92
+ grid: {String(!!showGrid)} · axes: {String(!!showAxes)} · points: {data.length}
93
+ </div>
94
+ ) : null}
95
+ {loading ? (
96
+ <div className="mt-3 text-xs text-slate-500 dark:text-slate-400">Loading…</div>
97
+ ) : null}
98
+ </div>
99
+ }
100
+ {...cardProps}
101
+ />
102
+ );
103
+ }
104
+
105
+
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import UIText from "../ui/Text";
3
+
4
+ export default function FeedPanel({
5
+ title,
6
+ subtitle,
7
+ actions,
8
+ children,
9
+ width,
10
+ className = "",
11
+ headerClassName = "",
12
+ bodyClassName = "",
13
+ ...rest
14
+ }) {
15
+ const widthStyle = width ? (typeof width === "number" ? { width: `${width}px` } : { width }) : undefined;
16
+
17
+ return (
18
+ <div
19
+ className={`flex shrink-0 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900 ${className}`}
20
+ style={widthStyle}
21
+ {...rest}
22
+ >
23
+ {(title || subtitle || actions) && (
24
+ <div className={`shrink-0 border-b border-slate-100 px-4 py-3 dark:border-slate-800 ${headerClassName}`}>
25
+ <div className="flex items-start justify-between gap-3">
26
+ <div className="min-w-0">
27
+ {title && <UIText as="div" size="sm" weight="semibold">{title}</UIText>}
28
+ {subtitle && <UIText as="div" size="xs" muted className="mt-0.5">{subtitle}</UIText>}
29
+ </div>
30
+ {actions && <div className="shrink-0">{actions}</div>}
31
+ </div>
32
+ </div>
33
+ )}
34
+ <div className={`flex-1 overflow-y-auto ${bodyClassName}`}>
35
+ {children}
36
+ </div>
37
+ </div>
38
+ );
39
+ }