@growthub/cli 0.9.17 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/reference-options/route.js +62 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +13 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolver-templates/route.js +23 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +35 -5
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +15 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +2048 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataTable.jsx +1 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/FieldEditor.jsx +1 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/FieldManager.jsx +9 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ObjectSidebar.jsx +41 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/RecordDrawer.jsx +1 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +244 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +21 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SourceTestPanel.jsx +15 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +13 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +41 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/dm-shared.jsx +99 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +2 -1528
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +66 -5
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/connector-template-authoring.md +8 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/data-model-reference-fields.md +15 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/mcp-chrome-tool-connectors.md +12 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/resolver-template-library.md +17 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +13 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/README.md +12 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/chrome-bridge.js +22 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/custom-http.js +23 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-commerce.js +22 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-crm.js +23 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-project-management.js +22 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-spreadsheet.js +22 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/mcp-tool.js +22 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +50 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/webhook.js +22 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/collect-reference-options.js +133 -0
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/reference-resolver-registry.js +17 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolver-loader.js +6 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/README.md +8 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/local-data-model.js +11 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/source-records.js +34 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/README.md +5 -3
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +203 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +81 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/reference-option-schema.js +59 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/reference-options.js +29 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +527 -23
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +131 -1
  49. package/dist/index.js +3043 -1340
  50. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export { DataModelTableSurface as DataTable } from "./DataModelShell.jsx";
@@ -0,0 +1 @@
1
+ export { RecordFieldEditor as FieldEditor } from "./DataModelShell.jsx";
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Extension surface for governed field visibility / ordering beyond per-row editors.
5
+ * Row-level editing is composed through `FieldEditor` + drawer sections today.
6
+ */
7
+ export function FieldManager() {
8
+ return null;
9
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { Plus } from "lucide-react";
4
+ import { LucideIcon, OBJECT_TYPE_PRESETS, objectTypeBadge, pluralize } from "./dm-shared.jsx";
5
+
6
+ function ObjectRow({ table, selected, onSelect }) {
7
+ const badge = objectTypeBadge(table.objectType);
8
+ const iconName = table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database";
9
+ return (
10
+ <button type="button" className={`dm-obj-row${selected ? " active" : ""}`} onClick={onSelect}>
11
+ <LucideIcon name={iconName} size={13} className="dm-obj-icon" />
12
+ <span className="dm-obj-name">{table.label}</span>
13
+ <span className={`dm-badge ${badge.cls}`}>{badge.label}</span>
14
+ </button>
15
+ );
16
+ }
17
+
18
+ export function ObjectSidebar({ tables, selectedTable, onSelectSource, onAddObject }) {
19
+ return (
20
+ <aside className="dm-obj-col">
21
+ <div className="dm-obj-col-head">
22
+ <span>{pluralize(tables.length, "object")}</span>
23
+ </div>
24
+ <div className="dm-obj-col-body">
25
+ {tables.map((table) => (
26
+ <ObjectRow
27
+ key={`${table.source}-${table.id}`}
28
+ table={table}
29
+ selected={selectedTable?.id === table.id}
30
+ onSelect={() => onSelectSource(table.source)}
31
+ />
32
+ ))}
33
+ </div>
34
+ <div className="dm-obj-col-foot">
35
+ <button type="button" className="dm-obj-add-btn" onClick={onAddObject}>
36
+ <Plus size={13} />New object
37
+ </button>
38
+ </div>
39
+ </aside>
40
+ );
41
+ }
@@ -0,0 +1 @@
1
+ export { DataModelRecordDrawer as RecordDrawer } from "./DataModelShell.jsx";
@@ -0,0 +1,244 @@
1
+ "use client";
2
+
3
+ import { ChevronDown, Search, AlertTriangle } from "lucide-react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { fetchReferenceOptions } from "@/lib/data-model/reference-options";
6
+
7
+ const EMPTY_CONTEXT = Object.freeze({});
8
+
9
+ function SearchableSelect({
10
+ value,
11
+ options,
12
+ disabled,
13
+ placeholder = "Select...",
14
+ onChange,
15
+ pageSize = 8,
16
+ footer,
17
+ loading,
18
+ emptyHint,
19
+ serverDriven,
20
+ onSearchChange
21
+ }) {
22
+ const [open, setOpen] = useState(false);
23
+ const [query, setQuery] = useState("");
24
+ const [page, setPage] = useState(0);
25
+
26
+ useEffect(() => {
27
+ if (!serverDriven || !onSearchChange) return undefined;
28
+ const handle = setTimeout(() => onSearchChange(query), 220);
29
+ return () => clearTimeout(handle);
30
+ }, [query, serverDriven, onSearchChange]);
31
+
32
+ const selected = options.find((option) => option.value === String(value || ""));
33
+ const filtered = useMemo(() => {
34
+ if (serverDriven) return options;
35
+ const needle = query.trim().toLowerCase();
36
+ if (!needle) return options;
37
+ return options.filter((option) =>
38
+ `${option.label} ${option.value} ${option.secondaryLabel || ""}`.toLowerCase().includes(needle)
39
+ );
40
+ }, [options, query, serverDriven]);
41
+ const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
42
+ const currentPage = Math.min(page, pageCount - 1);
43
+ const visibleOptions = filtered.slice(currentPage * pageSize, currentPage * pageSize + pageSize);
44
+
45
+ useEffect(() => {
46
+ setPage(0);
47
+ }, [query, options.length]);
48
+
49
+ return (
50
+ <div
51
+ className={`dm-select${open ? " open" : ""}${disabled ? " disabled" : ""}`}
52
+ onClick={(event) => event.stopPropagation()}
53
+ onBlur={(event) => {
54
+ if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
55
+ }}
56
+ >
57
+ <button
58
+ type="button"
59
+ className="dm-select-trigger"
60
+ disabled={disabled}
61
+ aria-haspopup="listbox"
62
+ aria-expanded={open}
63
+ onClick={() => setOpen((current) => !current)}
64
+ >
65
+ <span className={selected ? "" : "empty"}>{selected?.label || placeholder}</span>
66
+ <ChevronDown size={15} aria-hidden="true" />
67
+ </button>
68
+ {open && (
69
+ <div className="dm-select-popover">
70
+ <label className="dm-select-search">
71
+ <Search size={14} aria-hidden="true" />
72
+ <input
73
+ autoFocus
74
+ value={query}
75
+ placeholder="Search..."
76
+ onChange={(event) => {
77
+ const next = event.target.value;
78
+ setQuery(next);
79
+ }}
80
+ />
81
+ </label>
82
+ {loading && <p className="dm-select-empty" style={{ padding: 8 }}>Loading…</p>}
83
+ {!loading && (
84
+ <div className="dm-select-list" role="listbox">
85
+ <button
86
+ type="button"
87
+ className={`dm-select-option${!value ? " selected" : ""}`}
88
+ role="option"
89
+ aria-selected={!value}
90
+ onClick={() => {
91
+ onChange("");
92
+ setOpen(false);
93
+ }}
94
+ >
95
+ <span>{placeholder}</span>
96
+ </button>
97
+ {visibleOptions.map((option) => (
98
+ <button
99
+ type="button"
100
+ key={`${option.value}:${option.label}`}
101
+ className={`dm-select-option${option.value === String(value || "") ? " selected" : ""}`}
102
+ role="option"
103
+ aria-selected={option.value === String(value || "")}
104
+ onClick={() => {
105
+ onChange(option.value);
106
+ setOpen(false);
107
+ setQuery("");
108
+ if (serverDriven && onSearchChange) onSearchChange("");
109
+ }}
110
+ >
111
+ <span>{option.label}</span>
112
+ {option.secondaryLabel && <em>{option.secondaryLabel}</em>}
113
+ </button>
114
+ ))}
115
+ {visibleOptions.length === 0 && (
116
+ <p className="dm-select-empty">{emptyHint || "No matches"}</p>
117
+ )}
118
+ </div>
119
+ )}
120
+ {filtered.length > pageSize && (
121
+ <div className="dm-select-pager">
122
+ <button type="button" disabled={currentPage === 0} onClick={() => setPage((next) => Math.max(0, next - 1))}>Prev</button>
123
+ <span>{currentPage + 1} / {pageCount}</span>
124
+ <button type="button" disabled={currentPage >= pageCount - 1} onClick={() => setPage((next) => Math.min(pageCount - 1, next + 1))}>Next</button>
125
+ </div>
126
+ )}
127
+ {footer}
128
+ </div>
129
+ )}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export function ReferencePicker({
135
+ objectId,
136
+ field,
137
+ value,
138
+ onChange,
139
+ disabled,
140
+ placeholder = "Select reference…",
141
+ context = EMPTY_CONTEXT
142
+ }) {
143
+ const [options, setOptions] = useState([]);
144
+ const [nextCursor, setNextCursor] = useState(null);
145
+ const [loading, setLoading] = useState(false);
146
+ const [error, setError] = useState("");
147
+ const [liveQuery, setLiveQuery] = useState("");
148
+ const mountedRef = useRef(false);
149
+ const requestIdRef = useRef(0);
150
+
151
+ useEffect(() => {
152
+ mountedRef.current = true;
153
+ return () => {
154
+ mountedRef.current = false;
155
+ };
156
+ }, []);
157
+
158
+ const loadPage = useCallback(async ({ query, cursor, append }) => {
159
+ if (!objectId || !field) return;
160
+ const requestId = requestIdRef.current + 1;
161
+ requestIdRef.current = requestId;
162
+ setLoading(true);
163
+ setError("");
164
+ try {
165
+ const payload = await fetchReferenceOptions({
166
+ objectId,
167
+ field,
168
+ query: query || "",
169
+ cursor,
170
+ limit: 25,
171
+ context
172
+ });
173
+ if (!mountedRef.current || requestId !== requestIdRef.current) return;
174
+ const next = Array.isArray(payload.options) ? payload.options : [];
175
+ setOptions((prev) => (append ? [...prev, ...next] : next));
176
+ setNextCursor(payload.nextCursor || null);
177
+ } catch (err) {
178
+ if (!mountedRef.current || requestId !== requestIdRef.current) return;
179
+ setError(err?.message || "Failed to load options");
180
+ if (!append) setOptions([]);
181
+ } finally {
182
+ if (!mountedRef.current || requestId !== requestIdRef.current) return;
183
+ setLoading(false);
184
+ }
185
+ }, [objectId, field, context]);
186
+
187
+ useEffect(() => {
188
+ loadPage({ query: liveQuery, cursor: null, append: false });
189
+ }, [liveQuery, objectId, field, loadPage]);
190
+
191
+ useEffect(() => {
192
+ setLiveQuery("");
193
+ }, [objectId, field]);
194
+
195
+ const valueInOptions = useMemo(
196
+ () => options.some((o) => String(o.value) === String(value || "")),
197
+ [options, value]
198
+ );
199
+ const showRepair = Boolean(value) && !valueInOptions && !loading;
200
+
201
+ return (
202
+ <div className="dm-reference-picker">
203
+ {error && <p className="dm-field-error" style={{ fontSize: 11 }}>{error}</p>}
204
+ {showRepair && (
205
+ <p className="dm-validation-banner" style={{ fontSize: 11, marginBottom: 6 }}>
206
+ <AlertTriangle size={12} aria-hidden />
207
+ <span>Selected reference is missing or filtered out. Pick a new row or adjust API Registry status.</span>
208
+ </p>
209
+ )}
210
+ <SearchableSelect
211
+ value={value || ""}
212
+ options={options.map((o) => ({
213
+ value: String(o.value),
214
+ label: String(o.label || o.value),
215
+ secondaryLabel: o.secondaryLabel
216
+ }))}
217
+ disabled={disabled}
218
+ placeholder={placeholder}
219
+ pageSize={10}
220
+ loading={loading}
221
+ emptyHint="No matches — try another search"
222
+ onChange={onChange}
223
+ serverDriven
224
+ onSearchChange={setLiveQuery}
225
+ footer={
226
+ nextCursor ? (
227
+ <div style={{ padding: 8, borderTop: "1px solid rgba(255,255,255,0.06)" }}>
228
+ <button
229
+ type="button"
230
+ className="dm-btn-ghost"
231
+ disabled={loading || disabled}
232
+ onClick={() => loadPage({ query: liveQuery, cursor: nextCursor, append: true })}
233
+ >
234
+ Load more
235
+ </button>
236
+ </div>
237
+ ) : null
238
+ }
239
+ />
240
+ </div>
241
+ );
242
+ }
243
+
244
+ export { SearchableSelect };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { Play } from "lucide-react";
4
+ import { StatusPill } from "./StatusPill.jsx";
5
+
6
+ export function SandboxRunPanel({ status, sandboxRunning, sandboxMessage, onRun, disabled, canRun }) {
7
+ return (
8
+ <div className="dm-record-testbar">
9
+ <StatusPill value={status} />
10
+ <button
11
+ type="button"
12
+ className="dm-btn-primary-sm"
13
+ disabled={sandboxRunning || disabled || !canRun}
14
+ onClick={onRun}
15
+ >
16
+ {sandboxRunning ? "Running…" : (<><Play size={13} aria-hidden /> Run sandbox</>)}
17
+ </button>
18
+ {sandboxMessage && <span>{sandboxMessage}</span>}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { StatusPill } from "./StatusPill.jsx";
4
+
5
+ export function SourceTestPanel({ status, testing, testMessage, onTest, disabled }) {
6
+ return (
7
+ <div className="dm-record-testbar">
8
+ <StatusPill value={status} />
9
+ <button type="button" className="dm-btn-primary-sm" disabled={testing || disabled} onClick={onTest}>
10
+ {testing ? "Testing…" : "Test connection"}
11
+ </button>
12
+ {testMessage && <span>{testMessage}</span>}
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ export function StatusPill({ value }) {
4
+ const status = String(value || "untested").toLowerCase();
5
+ const ok = ["connected", "approved", "ok", "success"].includes(status);
6
+ const bad = ["failed", "error", "disconnected"].includes(status);
7
+ return (
8
+ <span className={`dm-db-status ${ok ? "ok" : bad ? "bad" : ""}`}>
9
+ <span />
10
+ {value || "untested"}
11
+ </span>
12
+ );
13
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ export function ToggleField({ checked, disabled, label, onChange, description }) {
4
+ return (
5
+ <label className="dm-check-row">
6
+ <input
7
+ type="checkbox"
8
+ checked={Boolean(checked)}
9
+ disabled={disabled}
10
+ onChange={(event) => onChange(event.target.checked)}
11
+ />
12
+ <span>
13
+ {label}
14
+ {description && <span className="dm-cell-empty" style={{ display: "block", marginTop: 4 }}>{description}</span>}
15
+ </span>
16
+ </label>
17
+ );
18
+ }
19
+
20
+ export function SegmentedToggle({ value, options, disabled, onChange, label, name = "segmented" }) {
21
+ const group = String(name || "segmented");
22
+ return (
23
+ <div className="dm-record-field">
24
+ {label && <span>{label}</span>}
25
+ <div className="dm-radio-row">
26
+ {options.map((opt) => (
27
+ <label key={opt}>
28
+ <input
29
+ type="radio"
30
+ name={group}
31
+ checked={value === opt}
32
+ disabled={disabled}
33
+ onChange={() => onChange(opt)}
34
+ />
35
+ <span>{opt}</span>
36
+ </label>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import {
4
+ Activity,
5
+ BarChart2,
6
+ Box,
7
+ Building2,
8
+ Calendar,
9
+ CheckSquare,
10
+ Code2,
11
+ Database,
12
+ FileText,
13
+ Globe,
14
+ Hash,
15
+ Layers,
16
+ Link2,
17
+ List,
18
+ Mail,
19
+ Plus,
20
+ ShoppingCart,
21
+ Star,
22
+ Tag,
23
+ Terminal,
24
+ ToggleLeft,
25
+ Type,
26
+ Users,
27
+ Zap,
28
+ } from "lucide-react";
29
+ import { OBJECT_TYPE_PRESETS } from "@/lib/workspace-data-model";
30
+
31
+ const LUCIDE_MAP = {
32
+ Activity, BarChart2, Box, Building2, Calendar, CheckSquare, Code2,
33
+ Database, FileText, Globe, Hash, Layers, Link2, List, Mail, Plus,
34
+ ShoppingCart, Star, Tag, Terminal, ToggleLeft, Type, Users, Zap,
35
+ };
36
+
37
+ const ICON_PICKER_SET = [
38
+ "Database", "Globe", "Code2", "Users", "CheckSquare", "Building2",
39
+ "Tag", "Star", "Zap", "FileText", "Mail", "BarChart2",
40
+ "Layers", "Box", "Activity", "ShoppingCart", "Terminal",
41
+ ];
42
+
43
+ const OBJECT_TYPE_BADGE = {
44
+ "data-source": { label: "Data Source", cls: "dm-badge-datasource" },
45
+ "api-registry": { label: "API Registry", cls: "dm-badge-registry" },
46
+ "sandbox-environment": { label: "Sandbox Environment", cls: "dm-badge-sandbox" },
47
+ people: { label: "People", cls: "dm-badge-people" },
48
+ tasks: { label: "Tasks", cls: "dm-badge-tasks" },
49
+ custom: { label: "Custom", cls: "dm-badge-manual" },
50
+ };
51
+
52
+ const FIELD_TYPE_ICON_NAMES = {
53
+ text: "Type", number: "Hash", date: "Calendar", url: "Link2", select: "List", boolean: "ToggleLeft",
54
+ };
55
+
56
+ function LucideIcon({ name, size = 14, className, style }) {
57
+ const Icon = LUCIDE_MAP[name] || Database;
58
+ return <Icon size={size} className={className} style={style} aria-hidden="true" />;
59
+ }
60
+
61
+ function inferFieldType(name) {
62
+ const n = name.toLowerCase();
63
+ if (n.includes("date") || n.includes("_at") || n.includes("created") || n.includes("updated")) return "date";
64
+ if (n.includes("url") || n.includes("link") || n.includes("website") || n === "endpoint" || n === "baseurl") return "url";
65
+ if (n.includes("count") || n.includes("num") || n.includes("amount") || n.includes("arr") || n.includes("price")) return "number";
66
+ if (n === "status" || n === "stage" || n === "type" || n === "icp" || n === "priority" || n === "authtype" || n === "method") return "select";
67
+ if (n.startsWith("is_") || n.includes("active") || n.includes("enabled")) return "boolean";
68
+ return "text";
69
+ }
70
+
71
+ function pluralize(count, word) {
72
+ return `${count} ${count === 1 ? word : `${word}s`}`;
73
+ }
74
+
75
+ function objectTypeBadge(objectType) {
76
+ return OBJECT_TYPE_BADGE[objectType] || OBJECT_TYPE_BADGE.custom;
77
+ }
78
+
79
+ function textColorForAccent(accent) {
80
+ const hex = String(accent || "").replace("#", "");
81
+ if (!/^[0-9a-f]{6}$/i.test(hex)) return "#ffffff";
82
+ const r = parseInt(hex.slice(0, 2), 16);
83
+ const g = parseInt(hex.slice(2, 4), 16);
84
+ const b = parseInt(hex.slice(4, 6), 16);
85
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.62 ? "#252525" : "#ffffff";
86
+ }
87
+
88
+ export {
89
+ FIELD_TYPE_ICON_NAMES,
90
+ ICON_PICKER_SET,
91
+ LUCIDE_MAP,
92
+ LucideIcon,
93
+ OBJECT_TYPE_BADGE,
94
+ OBJECT_TYPE_PRESETS,
95
+ inferFieldType,
96
+ objectTypeBadge,
97
+ pluralize,
98
+ textColorForAccent
99
+ };