@schandlergarcia/sf-web-components 1.2.5 → 1.2.7

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 (165) hide show
  1. package/package.json +2 -1
  2. package/scripts/postinstall.mjs +69 -93
  3. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Account.cls +196 -0
  4. package/src/components/library/.sfdx/tools/sobjects/standardObjects/AccountHistory.cls +25 -0
  5. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Asset.cls +138 -0
  6. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Attachment.cls +35 -0
  7. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Case.cls +111 -0
  8. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Contact.cls +167 -0
  9. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Contract.cls +96 -0
  10. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Domain.cls +29 -0
  11. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Lead.cls +128 -0
  12. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Note.cls +32 -0
  13. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Opportunity.cls +113 -0
  14. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Order.cls +127 -0
  15. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Pricebook2.cls +47 -0
  16. package/src/components/library/.sfdx/tools/sobjects/standardObjects/PricebookEntry.cls +47 -0
  17. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Product2.cls +91 -0
  18. package/src/components/library/.sfdx/tools/sobjects/standardObjects/RecordType.cls +35 -0
  19. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Report.cls +47 -0
  20. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Task.cls +79 -0
  21. package/src/components/library/.sfdx/tools/sobjects/standardObjects/User.cls +2318 -0
  22. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Account.json +2952 -0
  23. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/AccountHistory.json +875 -0
  24. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Asset.json +1699 -0
  25. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Attachment.json +362 -0
  26. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Case.json +1371 -0
  27. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Contact.json +2309 -0
  28. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Contract.json +1304 -0
  29. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Domain.json +293 -0
  30. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Lead.json +1977 -0
  31. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Note.json +303 -0
  32. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Opportunity.json +1470 -0
  33. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Order.json +1646 -0
  34. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Pricebook2.json +482 -0
  35. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/PricebookEntry.json +433 -0
  36. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Product2.json +1039 -0
  37. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/RecordType.json +2576 -0
  38. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Report.json +486 -0
  39. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Task.json +4296 -0
  40. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/User.json +30415 -0
  41. package/src/components/library/.sfdx/tools/soqlMetadata/typeNames.json +78 -0
  42. package/src/components/library/.sfdx/typings/lwc/sobjects/Account.d.ts +264 -0
  43. package/src/components/library/.sfdx/typings/lwc/sobjects/AccountHistory.d.ts +44 -0
  44. package/src/components/library/.sfdx/typings/lwc/sobjects/Asset.d.ts +240 -0
  45. package/src/components/library/.sfdx/typings/lwc/sobjects/Attachment.d.ts +76 -0
  46. package/src/components/library/.sfdx/typings/lwc/sobjects/Case.d.ts +172 -0
  47. package/src/components/library/.sfdx/typings/lwc/sobjects/Contact.d.ts +264 -0
  48. package/src/components/library/.sfdx/typings/lwc/sobjects/Contract.d.ts +188 -0
  49. package/src/components/library/.sfdx/typings/lwc/sobjects/Domain.d.ts +52 -0
  50. package/src/components/library/.sfdx/typings/lwc/sobjects/Lead.d.ts +252 -0
  51. package/src/components/library/.sfdx/typings/lwc/sobjects/Note.d.ts +64 -0
  52. package/src/components/library/.sfdx/typings/lwc/sobjects/Opportunity.d.ts +200 -0
  53. package/src/components/library/.sfdx/typings/lwc/sobjects/Order.d.ts +260 -0
  54. package/src/components/library/.sfdx/typings/lwc/sobjects/Pricebook2.d.ts +64 -0
  55. package/src/components/library/.sfdx/typings/lwc/sobjects/PricebookEntry.d.ts +76 -0
  56. package/src/components/library/.sfdx/typings/lwc/sobjects/Product2.d.ts +96 -0
  57. package/src/components/library/.sfdx/typings/lwc/sobjects/RecordType.d.ts +64 -0
  58. package/src/components/library/.sfdx/typings/lwc/sobjects/Report.d.ts +80 -0
  59. package/src/components/library/.sfdx/typings/lwc/sobjects/Task.d.ts +184 -0
  60. package/src/components/library/.sfdx/typings/lwc/sobjects/User.d.ts +752 -0
  61. package/src/components/library/cards/ActionList.jsx +38 -0
  62. package/src/components/library/cards/ActivityCard.jsx +56 -0
  63. package/src/components/library/cards/BaseCard.jsx +109 -0
  64. package/src/components/library/cards/CalloutCard.jsx +37 -0
  65. package/src/components/library/cards/ChartCard.jsx +105 -0
  66. package/src/components/library/cards/FeedPanel.jsx +39 -0
  67. package/src/components/library/cards/ListCard.jsx +193 -0
  68. package/src/components/library/cards/MetricCard.jsx +109 -0
  69. package/src/components/library/cards/MetricsStrip.jsx +78 -0
  70. package/src/components/library/cards/SectionCard.jsx +83 -0
  71. package/src/components/library/cards/SemanticMetricCard.jsx +52 -0
  72. package/src/components/library/cards/SemanticMetricCardWithLoading.jsx +23 -0
  73. package/src/components/library/cards/SemanticTableCard.jsx +48 -0
  74. package/src/components/library/cards/SemanticTableCardWithLoading.jsx +22 -0
  75. package/src/components/library/cards/StatusCard.jsx +220 -0
  76. package/src/components/library/cards/TableCard.jsx +337 -0
  77. package/src/components/library/cards/WidgetCard.jsx +90 -0
  78. package/src/components/library/charts/D3Chart.jsx +109 -0
  79. package/src/components/library/charts/D3ChartTemplates.jsx +126 -0
  80. package/src/components/library/charts/GeoMap.jsx +293 -0
  81. package/src/components/library/chat/ChatBar.jsx +256 -0
  82. package/src/components/library/chat/ChatInput.jsx +89 -0
  83. package/src/components/library/chat/ChatMessage.jsx +178 -0
  84. package/src/components/library/chat/ChatMessageList.jsx +73 -0
  85. package/src/components/library/chat/ChatPanel.jsx +97 -0
  86. package/src/components/library/chat/ChatSuggestions.jsx +28 -0
  87. package/src/components/library/chat/ChatToolCall.jsx +100 -0
  88. package/src/components/library/chat/ChatTypingIndicator.jsx +23 -0
  89. package/src/components/library/chat/ChatWelcome.jsx +43 -0
  90. package/src/components/library/chat/index.jsx +10 -0
  91. package/src/components/library/chat/useChatState.jsx +130 -0
  92. package/src/components/library/data/DataModeProvider.jsx +67 -0
  93. package/src/components/library/data/DataModeToggle.jsx +36 -0
  94. package/src/components/library/data/chartDataProvider.jsx +61 -0
  95. package/src/components/library/data/filterUtils.jsx +141 -0
  96. package/src/components/library/data/useDataSource.jsx +33 -0
  97. package/src/components/library/data/usePageFilters.jsx +99 -0
  98. package/src/components/library/filters/FilterBar.jsx +95 -0
  99. package/src/components/library/filters/SearchFilter.jsx +36 -0
  100. package/src/components/library/filters/SelectFilter.jsx +55 -0
  101. package/src/components/library/filters/ToggleFilter.jsx +52 -0
  102. package/src/components/library/filters/index.jsx +4 -0
  103. package/src/components/library/forms/FormField.jsx +291 -0
  104. package/src/components/library/forms/FormModal.jsx +201 -0
  105. package/src/components/library/forms/FormRenderer.jsx +46 -0
  106. package/src/components/library/forms/FormSection.jsx +69 -0
  107. package/src/components/library/forms/index.jsx +5 -0
  108. package/src/components/library/forms/useFormState.jsx +165 -0
  109. package/src/components/library/heroui/Accordion.jsx +26 -0
  110. package/src/components/library/heroui/Alert.jsx +8 -0
  111. package/src/components/library/heroui/Badge.jsx +8 -0
  112. package/src/components/library/heroui/Breadcrumbs.jsx +22 -0
  113. package/src/components/library/heroui/Button.jsx +58 -0
  114. package/src/components/library/heroui/Card.jsx +8 -0
  115. package/src/components/library/heroui/Collapsible.jsx +42 -0
  116. package/src/components/library/heroui/DatePicker.jsx +34 -0
  117. package/src/components/library/heroui/Dialog.jsx +37 -0
  118. package/src/components/library/heroui/Drawer.jsx +32 -0
  119. package/src/components/library/heroui/Dropdown.jsx +28 -0
  120. package/src/components/library/heroui/Field.jsx +51 -0
  121. package/src/components/library/heroui/Input.jsx +6 -0
  122. package/src/components/library/heroui/Kbd.jsx +8 -0
  123. package/src/components/library/heroui/Meter.jsx +8 -0
  124. package/src/components/library/heroui/Modal.jsx +32 -0
  125. package/src/components/library/heroui/Pagination.jsx +8 -0
  126. package/src/components/library/heroui/Popover.jsx +64 -0
  127. package/src/components/library/heroui/ProgressBar.jsx +8 -0
  128. package/src/components/library/heroui/ProgressCircle.jsx +8 -0
  129. package/src/components/library/heroui/ScrollShadow.jsx +8 -0
  130. package/src/components/library/heroui/Select.jsx +37 -0
  131. package/src/components/library/heroui/Separator.jsx +8 -0
  132. package/src/components/library/heroui/Skeleton.jsx +8 -0
  133. package/src/components/library/heroui/Tabs.jsx +26 -0
  134. package/src/components/library/heroui/Toast.jsx +25 -0
  135. package/src/components/library/heroui/Toggle.jsx +14 -0
  136. package/src/components/library/heroui/Tooltip.jsx +21 -0
  137. package/src/components/library/index.jsx +149 -0
  138. package/src/components/library/layout/PageContainer.jsx +11 -0
  139. package/src/components/library/skeletons/CardSkeleton.jsx +30 -0
  140. package/src/components/library/theme/AppThemeProvider.jsx +67 -0
  141. package/src/components/library/theme/tokens.jsx +72 -0
  142. package/src/components/library/ui/Alert.jsx +80 -0
  143. package/src/components/library/ui/Avatar.jsx +44 -0
  144. package/src/components/library/ui/BreadcrumbExtras.tsx +119 -0
  145. package/src/components/library/ui/Card.jsx +117 -0
  146. package/src/components/library/ui/Checkbox.jsx +17 -0
  147. package/src/components/library/ui/Chip.jsx +38 -0
  148. package/src/components/library/ui/Collapsible.tsx +31 -0
  149. package/src/components/library/ui/Container.jsx +56 -0
  150. package/src/components/library/ui/DatePicker.tsx +34 -0
  151. package/src/components/library/ui/Dialog.tsx +141 -0
  152. package/src/components/library/ui/EmptyState.jsx +46 -0
  153. package/src/components/library/ui/Field.tsx +82 -0
  154. package/src/components/library/ui/FieldGroup.jsx +17 -0
  155. package/src/components/library/ui/Label.jsx +22 -0
  156. package/src/components/library/ui/PaginationExtras.tsx +143 -0
  157. package/src/components/library/ui/Popover.tsx +39 -0
  158. package/src/components/library/ui/Select.tsx +113 -0
  159. package/src/components/library/ui/Spinner.jsx +64 -0
  160. package/src/components/library/ui/Text.jsx +46 -0
  161. package/src/components/library/ui/UIButton.jsx +61 -0
  162. package/src/components/library/ui/UIInput.jsx +21 -0
  163. package/src/components/workspace/ComponentRegistry.jsx +297 -0
  164. package/src/templates/pages/Home.tsx.template +5 -5
  165. package/src/templates/pages/NotFound.tsx.template +2 -2
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+
3
+ const DataModeContext = React.createContext({
4
+ mode: "sample",
5
+ isSample: true,
6
+ isLive: false,
7
+ toggle: () => {},
8
+ setMode: () => {},
9
+ });
10
+
11
+ const STORAGE_KEY = "app-data-mode";
12
+ const VALID_MODES = ["sample", "live"];
13
+
14
+ /**
15
+ * Read the current data mode from any component.
16
+ *
17
+ * @returns {{ mode: "sample"|"live", isSample: boolean, isLive: boolean, toggle: () => void, setMode: (mode) => void }}
18
+ */
19
+ export function useDataMode() {
20
+ return React.useContext(DataModeContext);
21
+ }
22
+
23
+ /**
24
+ * Provides global data-mode state (sample vs live) to the component tree.
25
+ * Persists to localStorage so the choice survives page reloads.
26
+ *
27
+ * Wrap once in _app.js alongside AppThemeProvider.
28
+ */
29
+ export default function DataModeProvider({ initialMode = "sample", children }) {
30
+ const [mode, setModeState] = React.useState(initialMode);
31
+
32
+ React.useEffect(() => {
33
+ try {
34
+ const stored = window.localStorage.getItem(STORAGE_KEY);
35
+ if (VALID_MODES.includes(stored)) setModeState(stored);
36
+ } catch {
37
+ // SSR or storage unavailable
38
+ }
39
+ }, []);
40
+
41
+ React.useEffect(() => {
42
+ try {
43
+ window.localStorage.setItem(STORAGE_KEY, mode);
44
+ } catch {
45
+ // ignore
46
+ }
47
+ }, [mode]);
48
+
49
+ const setMode = React.useCallback((m) => {
50
+ if (VALID_MODES.includes(m)) setModeState(m);
51
+ }, []);
52
+
53
+ const value = React.useMemo(
54
+ () => ({
55
+ mode,
56
+ isSample: mode === "sample",
57
+ isLive: mode === "live",
58
+ toggle: () => setModeState((m) => (m === "sample" ? "live" : "sample")),
59
+ setMode,
60
+ }),
61
+ [mode, setMode]
62
+ );
63
+
64
+ return (
65
+ <DataModeContext.Provider value={value}>{children}</DataModeContext.Provider>
66
+ );
67
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { useDataMode } from "./DataModeProvider";
3
+ import { BeakerIcon, SignalIcon } from "@heroicons/react/24/outline";
4
+
5
+ /**
6
+ * Pill toggle for switching between sample and live data modes.
7
+ * Place in the AppShell header next to the theme toggle.
8
+ */
9
+ export default function DataModeToggle({ className = "" }) {
10
+ const { mode, toggle } = useDataMode();
11
+ const isSample = mode === "sample";
12
+
13
+ return (
14
+ <button
15
+ type="button"
16
+ onClick={toggle}
17
+ className={[
18
+ "inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-semibold shadow-sm transition",
19
+ isSample
20
+ ? "border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-300 dark:hover:bg-amber-950/60"
21
+ : "border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300 dark:hover:bg-emerald-950/60",
22
+ className,
23
+ ]
24
+ .filter(Boolean)
25
+ .join(" ")}
26
+ aria-label={`Data mode: ${mode}. Click to switch to ${isSample ? "live" : "sample"}.`}
27
+ >
28
+ {isSample ? (
29
+ <BeakerIcon className="h-3.5 w-3.5" aria-hidden="true" />
30
+ ) : (
31
+ <SignalIcon className="h-3.5 w-3.5" aria-hidden="true" />
32
+ )}
33
+ {isSample ? "Sample" : "Live"}
34
+ </button>
35
+ );
36
+ }
@@ -0,0 +1,61 @@
1
+ // Minimal semantic data provider (seed data + lookup helpers).
2
+ // This is intentionally local/in-memory for now; later we can swap to API-backed providers.
3
+
4
+ const SEMANTIC_DATASETS = {
5
+ sales_pipeline_qtr: {
6
+ title: "Sales Pipeline (Quarter)",
7
+ metrics: [
8
+ {
9
+ metricId: "pipeline_value",
10
+ title: "Pipeline",
11
+ subtitle: "This quarter",
12
+ value: "$1.28M",
13
+ change: "+6.2%",
14
+ changeType: "positive",
15
+ color: "primary",
16
+ trend: "vs last quarter"
17
+ },
18
+ {
19
+ metricId: "win_rate",
20
+ title: "Win rate",
21
+ subtitle: "Trailing 90 days",
22
+ value: "34%",
23
+ change: "+1.1%",
24
+ changeType: "positive",
25
+ color: "success"
26
+ }
27
+ ],
28
+ table: {
29
+ title: "Opportunities",
30
+ subtitle: "Pipeline opportunities",
31
+ columns: [
32
+ { key: "name", label: "Opportunity", sortable: true },
33
+ { key: "amount", label: "Amount", type: "currency", sortable: true, mono: true },
34
+ { key: "stage", label: "Stage", sortable: true },
35
+ { key: "owner", label: "Owner", sortable: true }
36
+ ],
37
+ rows: [
38
+ { id: 1, name: "Acme Renewal", amount: 125000, stage: "Negotiation", owner: "Sam" },
39
+ { id: 2, name: "Globex Expansion", amount: 420000, stage: "Discovery", owner: "Jules" },
40
+ { id: 3, name: "Initech New Logo", amount: 98000, stage: "Proposal", owner: "Riley" },
41
+ { id: 4, name: "Umbrella Upsell", amount: 56000, stage: "Qualification", owner: "Alex" }
42
+ ]
43
+ }
44
+ }
45
+ };
46
+
47
+ export function listSemanticIds() {
48
+ return Object.keys(SEMANTIC_DATASETS);
49
+ }
50
+
51
+ export function getSemanticDataset(semanticId) {
52
+ if (!semanticId) return null;
53
+ return SEMANTIC_DATASETS[semanticId] ?? null;
54
+ }
55
+
56
+ export function getSemanticMetric(semanticId, metricId) {
57
+ const ds = getSemanticDataset(semanticId);
58
+ if (!ds?.metrics) return null;
59
+ return ds.metrics.find((m) => m.metricId === metricId) ?? null;
60
+ }
61
+
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Pure data utilities for filtering, sorting, and searching.
3
+ * Stateless — combine with usePageFilters hook for state management.
4
+ */
5
+
6
+ /**
7
+ * Text search across multiple keys.
8
+ * @param {Array} data
9
+ * @param {string} query — search string
10
+ * @param {string[]} keys — object keys to search within
11
+ * @returns {Array} filtered data
12
+ */
13
+ export function filterBySearch(data, query, keys = []) {
14
+ if (!query || !query.trim()) return data;
15
+ const q = query.trim().toLowerCase();
16
+ return data.filter((row) =>
17
+ keys.some((key) => {
18
+ const val = row?.[key];
19
+ return val != null && String(val).toLowerCase().includes(q);
20
+ })
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Filter rows where key matches a specific value.
26
+ * Pass "all" or "" to skip filtering.
27
+ * @param {Array} data
28
+ * @param {string} key — object key to match
29
+ * @param {*} value — value to match (exact, case-insensitive for strings)
30
+ * @returns {Array}
31
+ */
32
+ export function filterByValue(data, key, value) {
33
+ if (value == null || value === "" || value === "all") return data;
34
+ return data.filter((row) => {
35
+ const v = row?.[key];
36
+ if (typeof v === "string" && typeof value === "string") {
37
+ return v.toLowerCase() === value.toLowerCase();
38
+ }
39
+ return v === value;
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Filter rows where a boolean condition is met.
45
+ * When toggle is off, returns all data (no filtering).
46
+ * @param {Array} data
47
+ * @param {string} key — object key to check
48
+ * @param {boolean} isActive — whether the toggle is on
49
+ * @param {*} matchValue — value that key should equal when active (default: truthy check)
50
+ * @returns {Array}
51
+ */
52
+ export function filterByToggle(data, key, isActive, matchValue) {
53
+ if (!isActive) return data;
54
+ return data.filter((row) => {
55
+ const v = row?.[key];
56
+ if (matchValue !== undefined) return v === matchValue;
57
+ return Boolean(v);
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Filter rows where a date field falls within a range.
63
+ * @param {Array} data
64
+ * @param {string} key — object key containing date (ISO string or Date)
65
+ * @param {{ start?: Date|string, end?: Date|string }} range
66
+ * @returns {Array}
67
+ */
68
+ export function filterByDateRange(data, key, range) {
69
+ if (!range) return data;
70
+ const start = range.start ? new Date(range.start) : null;
71
+ const end = range.end ? new Date(range.end) : null;
72
+ if (!start && !end) return data;
73
+
74
+ return data.filter((row) => {
75
+ const raw = row?.[key];
76
+ if (raw == null) return false;
77
+ const d = raw instanceof Date ? raw : new Date(raw);
78
+ if (Number.isNaN(d.getTime())) return false;
79
+ if (start && d < start) return false;
80
+ if (end && d > end) return false;
81
+ return true;
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Sort data by a key.
87
+ * @param {Array} data
88
+ * @param {string} key — object key to sort by
89
+ * @param {"asc"|"desc"} direction
90
+ * @returns {Array} new sorted array
91
+ */
92
+ export function sortByKey(data, key, direction = "asc") {
93
+ if (!key) return data;
94
+ const dir = direction === "desc" ? -1 : 1;
95
+ return [...data].sort((a, b) => {
96
+ const av = a?.[key];
97
+ const bv = b?.[key];
98
+ if (av == null && bv == null) return 0;
99
+ if (av == null) return -1 * dir;
100
+ if (bv == null) return 1 * dir;
101
+ if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
102
+ return String(av).localeCompare(String(bv)) * dir;
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Apply a set of filter definitions to data.
108
+ * Each filter in `filters` has { id, type, key/keys } and `values` holds the current state.
109
+ *
110
+ * @param {Array} data
111
+ * @param {Array} filters — filter definitions [{ id, type, key?, keys? }]
112
+ * @param {Object} values — current filter values keyed by filter id
113
+ * @returns {Array} filtered data
114
+ */
115
+ export function applyFilters(data, filters = [], values = {}) {
116
+ let result = data;
117
+
118
+ for (const filter of filters) {
119
+ const val = values[filter.id];
120
+ if (val === undefined || val === null) continue;
121
+
122
+ switch (filter.type) {
123
+ case "search":
124
+ result = filterBySearch(result, val, filter.keys ?? []);
125
+ break;
126
+ case "select":
127
+ result = filterByValue(result, filter.key, val);
128
+ break;
129
+ case "toggle":
130
+ result = filterByToggle(result, filter.key, val, filter.matchValue);
131
+ break;
132
+ case "dateRange":
133
+ result = filterByDateRange(result, filter.key, val);
134
+ break;
135
+ default:
136
+ break;
137
+ }
138
+ }
139
+
140
+ return result;
141
+ }
@@ -0,0 +1,33 @@
1
+ import { useMemo } from "react";
2
+ import { useDataMode } from "./DataModeProvider";
3
+
4
+ /**
5
+ * Select between sample and live data based on the global data mode.
6
+ *
7
+ * Values can be plain data or functions (lazy-evaluated only when active).
8
+ *
9
+ * @param {{ sample: any | () => any, live: any | () => any }} sources
10
+ * @returns {any} the resolved value for the active mode
11
+ *
12
+ * @example
13
+ * // Static data
14
+ * const incidents = useDataSource({
15
+ * sample: sampleIncidents,
16
+ * live: fetchedIncidents,
17
+ * });
18
+ *
19
+ * @example
20
+ * // Lazy — factory only runs when that mode is active
21
+ * const metrics = useDataSource({
22
+ * sample: () => generateSampleMetrics(),
23
+ * live: () => computeFromAPI(apiData),
24
+ * });
25
+ */
26
+ export default function useDataSource({ sample, live }) {
27
+ const { mode } = useDataMode();
28
+
29
+ return useMemo(() => {
30
+ const source = mode === "sample" ? sample : live;
31
+ return typeof source === "function" ? source() : source;
32
+ }, [mode, sample, live]);
33
+ }
@@ -0,0 +1,99 @@
1
+ import { useState, useMemo, useCallback } from "react";
2
+ import { applyFilters, sortByKey } from "./filterUtils";
3
+
4
+ /**
5
+ * Hook for managing page-level filter and sort state.
6
+ *
7
+ * @param {Object} options
8
+ * @param {Array} options.data — raw data array
9
+ * @param {Array} options.filters — filter definitions [{ id, type, key?, keys?, defaultValue? }]
10
+ * @param {Object} options.defaultSort — { key, direction } or null
11
+ * @returns {Object} { values, setFilter, resetFilters, sort, setSort, filteredData, sortedData, activeFilterCount }
12
+ *
13
+ * @example
14
+ * const { values, setFilter, resetFilters, sortedData } = usePageFilters({
15
+ * data: incidents,
16
+ * filters: [
17
+ * { id: "search", type: "search", keys: ["title", "description"] },
18
+ * { id: "severity", type: "select", key: "severity", defaultValue: "all" },
19
+ * { id: "active", type: "toggle", key: "resolved", matchValue: false },
20
+ * ],
21
+ * defaultSort: { key: "timestamp", direction: "desc" },
22
+ * });
23
+ */
24
+ export default function usePageFilters({ data = [], filters = [], defaultSort = null } = {}) {
25
+ const initialValues = useMemo(() => {
26
+ const v = {};
27
+ for (const f of filters) {
28
+ if (f.defaultValue !== undefined) {
29
+ v[f.id] = f.defaultValue;
30
+ } else if (f.type === "search") {
31
+ v[f.id] = "";
32
+ } else if (f.type === "select") {
33
+ v[f.id] = "all";
34
+ } else if (f.type === "toggle") {
35
+ v[f.id] = false;
36
+ } else if (f.type === "dateRange") {
37
+ v[f.id] = null;
38
+ }
39
+ }
40
+ return v;
41
+ }, [filters]);
42
+
43
+ const [values, setValues] = useState(initialValues);
44
+ const [sort, setSortState] = useState(defaultSort);
45
+
46
+ const setFilter = useCallback((id, value) => {
47
+ setValues((prev) => ({ ...prev, [id]: value }));
48
+ }, []);
49
+
50
+ const resetFilters = useCallback(() => {
51
+ setValues(initialValues);
52
+ }, [initialValues]);
53
+
54
+ const setSort = useCallback((key, direction) => {
55
+ setSortState(key ? { key, direction: direction ?? "asc" } : null);
56
+ }, []);
57
+
58
+ const toggleSort = useCallback((key) => {
59
+ setSortState((prev) => {
60
+ if (prev?.key !== key) return { key, direction: "asc" };
61
+ if (prev.direction === "asc") return { key, direction: "desc" };
62
+ return null;
63
+ });
64
+ }, []);
65
+
66
+ const filteredData = useMemo(
67
+ () => applyFilters(data, filters, values),
68
+ [data, filters, values]
69
+ );
70
+
71
+ const sortedData = useMemo(
72
+ () => (sort ? sortByKey(filteredData, sort.key, sort.direction) : filteredData),
73
+ [filteredData, sort]
74
+ );
75
+
76
+ const activeFilterCount = useMemo(() => {
77
+ let count = 0;
78
+ for (const f of filters) {
79
+ const v = values[f.id];
80
+ if (f.type === "search" && v && v.trim()) count++;
81
+ else if (f.type === "select" && v && v !== "all") count++;
82
+ else if (f.type === "toggle" && v) count++;
83
+ else if (f.type === "dateRange" && v) count++;
84
+ }
85
+ return count;
86
+ }, [filters, values]);
87
+
88
+ return {
89
+ values,
90
+ setFilter,
91
+ resetFilters,
92
+ sort,
93
+ setSort,
94
+ toggleSort,
95
+ filteredData,
96
+ sortedData,
97
+ activeFilterCount,
98
+ };
99
+ }
@@ -0,0 +1,95 @@
1
+ import React from "react";
2
+ import SearchFilter from "./SearchFilter";
3
+ import SelectFilter from "./SelectFilter";
4
+ import ToggleFilter from "./ToggleFilter";
5
+ import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline";
6
+
7
+ /**
8
+ * Renders a row of filter controls from a definitions array.
9
+ * Pairs with usePageFilters hook for state management.
10
+ *
11
+ * @param {Array} filters — filter definitions [{ id, type, ... }]
12
+ * @param {Object} values — current filter values keyed by filter id
13
+ * @param {Function} onChange — (filterId, value) => void
14
+ * @param {Function} onReset — () => void
15
+ * @param {number} activeCount — number of active filters (for badge)
16
+ * @param {string} layout — "inline" (default) or "stacked"
17
+ */
18
+ export default function FilterBar({
19
+ filters = [],
20
+ values = {},
21
+ onChange,
22
+ onReset,
23
+ activeCount = 0,
24
+ layout = "inline",
25
+ }) {
26
+ if (!filters.length) return null;
27
+
28
+ const isStacked = layout === "stacked";
29
+
30
+ return (
31
+ <div
32
+ className={[
33
+ "flex gap-3",
34
+ isStacked
35
+ ? "flex-col"
36
+ : "flex-col sm:flex-row sm:flex-wrap sm:items-center",
37
+ ].join(" ")}
38
+ >
39
+ {filters.map((filter) => {
40
+ const val = values[filter.id];
41
+
42
+ switch (filter.type) {
43
+ case "search":
44
+ return (
45
+ <SearchFilter
46
+ key={filter.id}
47
+ value={val ?? ""}
48
+ onChange={(v) => onChange?.(filter.id, v)}
49
+ placeholder={filter.placeholder ?? "Search…"}
50
+ className={filter.className ?? (isStacked ? "w-full" : "w-full sm:w-64")}
51
+ />
52
+ );
53
+
54
+ case "select":
55
+ return (
56
+ <SelectFilter
57
+ key={filter.id}
58
+ value={val ?? "all"}
59
+ onChange={(v) => onChange?.(filter.id, v)}
60
+ options={filter.options ?? []}
61
+ label={filter.label}
62
+ placeholder={filter.placeholder ?? "All"}
63
+ className={filter.className}
64
+ />
65
+ );
66
+
67
+ case "toggle":
68
+ return (
69
+ <ToggleFilter
70
+ key={filter.id}
71
+ value={val ?? false}
72
+ onChange={(v) => onChange?.(filter.id, v)}
73
+ label={filter.label}
74
+ className={filter.className}
75
+ />
76
+ );
77
+
78
+ default:
79
+ return null;
80
+ }
81
+ })}
82
+
83
+ {activeCount > 0 && onReset ? (
84
+ <button
85
+ type="button"
86
+ onClick={onReset}
87
+ className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
88
+ >
89
+ <XMarkIcon className="h-3.5 w-3.5" aria-hidden="true" />
90
+ Clear {activeCount} {activeCount === 1 ? "filter" : "filters"}
91
+ </button>
92
+ ) : null}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
3
+
4
+ export default function SearchFilter({
5
+ value = "",
6
+ onChange,
7
+ placeholder = "Search…",
8
+ className = "",
9
+ }) {
10
+ return (
11
+ <div className={["relative", className].filter(Boolean).join(" ")}>
12
+ <MagnifyingGlassIcon
13
+ className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500"
14
+ aria-hidden="true"
15
+ />
16
+ <input
17
+ type="text"
18
+ value={value}
19
+ onChange={(e) => onChange?.(e.target.value)}
20
+ placeholder={placeholder}
21
+ className="h-9 w-full rounded-lg border border-slate-200 bg-white pl-9 pr-8 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus:ring-offset-slate-950"
22
+ aria-label={placeholder}
23
+ />
24
+ {value ? (
25
+ <button
26
+ type="button"
27
+ onClick={() => onChange?.("")}
28
+ className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300"
29
+ aria-label="Clear search"
30
+ >
31
+ <XMarkIcon className="h-4 w-4" />
32
+ </button>
33
+ ) : null}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { ChevronDownIcon } from "@heroicons/react/24/outline";
3
+
4
+ /**
5
+ * Dropdown select filter.
6
+ *
7
+ * @param {string} value — current selected value
8
+ * @param {Function} onChange — (value) => void
9
+ * @param {Array} options — [{ value, label }] or ["string", ...]
10
+ * @param {string} label — visible label
11
+ * @param {string} placeholder — placeholder when no value selected
12
+ */
13
+ export default function SelectFilter({
14
+ value = "all",
15
+ onChange,
16
+ options = [],
17
+ label,
18
+ placeholder,
19
+ className = "",
20
+ }) {
21
+ const normalizedOptions = options.map((opt) =>
22
+ typeof opt === "string" ? { value: opt, label: opt } : opt
23
+ );
24
+
25
+ return (
26
+ <div className={["relative inline-flex items-center gap-2", className].filter(Boolean).join(" ")}>
27
+ {label ? (
28
+ <span className="shrink-0 text-xs font-medium text-slate-500 dark:text-slate-400">
29
+ {label}
30
+ </span>
31
+ ) : null}
32
+ <div className="relative">
33
+ <select
34
+ value={value}
35
+ onChange={(e) => onChange?.(e.target.value)}
36
+ className="h-9 appearance-none rounded-lg border border-slate-200 bg-white py-0 pl-3 pr-8 text-sm font-medium text-slate-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200 dark:focus:ring-offset-slate-950"
37
+ aria-label={label ?? placeholder ?? "Filter"}
38
+ >
39
+ {placeholder ? (
40
+ <option value="all">{placeholder}</option>
41
+ ) : null}
42
+ {normalizedOptions.map((opt) => (
43
+ <option key={opt.value} value={opt.value}>
44
+ {opt.label}
45
+ </option>
46
+ ))}
47
+ </select>
48
+ <ChevronDownIcon
49
+ className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500"
50
+ aria-hidden="true"
51
+ />
52
+ </div>
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Toggle switch filter.
5
+ *
6
+ * @param {boolean} value — current on/off state
7
+ * @param {Function} onChange — (boolean) => void
8
+ * @param {string} label — visible label
9
+ */
10
+ export default function ToggleFilter({
11
+ value = false,
12
+ onChange,
13
+ label,
14
+ className = "",
15
+ }) {
16
+ return (
17
+ <label
18
+ className={[
19
+ "inline-flex cursor-pointer items-center gap-2",
20
+ className,
21
+ ]
22
+ .filter(Boolean)
23
+ .join(" ")}
24
+ >
25
+ <button
26
+ type="button"
27
+ role="switch"
28
+ aria-checked={value}
29
+ onClick={() => onChange?.(!value)}
30
+ className={[
31
+ "relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:focus:ring-offset-slate-950",
32
+ value
33
+ ? "bg-brand-500"
34
+ : "bg-slate-200 dark:bg-slate-700",
35
+ ].join(" ")}
36
+ >
37
+ <span
38
+ aria-hidden="true"
39
+ className={[
40
+ "pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform",
41
+ value ? "translate-x-4" : "translate-x-0",
42
+ ].join(" ")}
43
+ />
44
+ </button>
45
+ {label ? (
46
+ <span className="text-sm font-medium text-slate-700 dark:text-slate-200">
47
+ {label}
48
+ </span>
49
+ ) : null}
50
+ </label>
51
+ );
52
+ }
@@ -0,0 +1,4 @@
1
+ export { default as FilterBar } from "./FilterBar";
2
+ export { default as SearchFilter } from "./SearchFilter";
3
+ export { default as SelectFilter } from "./SelectFilter";
4
+ export { default as ToggleFilter } from "./ToggleFilter";