@treasuryspatial/panel-react 0.1.2

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.
@@ -0,0 +1,13 @@
1
+ import type { PanelLayout } from "@treasuryspatial/panel-core";
2
+ export type PanelRendererProps = {
3
+ layout: PanelLayout;
4
+ values: Record<string, unknown>;
5
+ onChange: (path: string, value: unknown) => void;
6
+ onAction?: (actionId: string) => void;
7
+ className?: string;
8
+ tabClassName?: string;
9
+ panelClassName?: string;
10
+ fieldClassName?: string;
11
+ };
12
+ export declare function PanelRenderer({ layout, values, onChange, onAction, className, tabClassName, panelClassName, fieldClassName }: PanelRendererProps): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=PanelRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PanelRenderer.d.ts","sourceRoot":"","sources":["../src/PanelRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAc,WAAW,EAAwB,MAAM,6BAA6B,CAAC;AAEjG,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAobF,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,EAAE,kBAAkB,2CA6BhJ"}
@@ -0,0 +1,216 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
+ const normalizePath = (path) => {
5
+ if (path.startsWith("/")) {
6
+ return path.slice(1).replace(/\//g, ".");
7
+ }
8
+ return path;
9
+ };
10
+ const parsePathSegments = (path) => {
11
+ if (!path)
12
+ return [];
13
+ const segments = [];
14
+ const parts = path.split(".");
15
+ for (const part of parts) {
16
+ if (!part)
17
+ continue;
18
+ const matches = part.matchAll(/([^[\]]+)|\[(\d+)\]/g);
19
+ for (const match of matches) {
20
+ if (match[1]) {
21
+ segments.push(match[1]);
22
+ }
23
+ else if (match[2]) {
24
+ segments.push(Number.parseInt(match[2], 10));
25
+ }
26
+ }
27
+ }
28
+ return segments;
29
+ };
30
+ const getValueByPath = (values, path) => {
31
+ if (!values)
32
+ return undefined;
33
+ if (Object.prototype.hasOwnProperty.call(values, path)) {
34
+ return values[path];
35
+ }
36
+ const normalized = normalizePath(path);
37
+ if (normalized !== path && Object.prototype.hasOwnProperty.call(values, normalized)) {
38
+ return values[normalized];
39
+ }
40
+ const segments = parsePathSegments(normalized);
41
+ if (segments.length === 0)
42
+ return undefined;
43
+ let cursor = values;
44
+ for (const segment of segments) {
45
+ if (cursor == null)
46
+ return undefined;
47
+ if (typeof segment === "number") {
48
+ if (!Array.isArray(cursor))
49
+ return undefined;
50
+ cursor = cursor[segment];
51
+ }
52
+ else {
53
+ cursor = cursor[segment];
54
+ }
55
+ }
56
+ return cursor;
57
+ };
58
+ const evaluateExpr = (expr, values, fallback) => {
59
+ if (!expr)
60
+ return fallback;
61
+ if (typeof expr !== "object")
62
+ return Boolean(expr);
63
+ const record = expr;
64
+ if (Array.isArray(record.all)) {
65
+ return record.all.every((entry) => evaluateExpr(entry, values, fallback));
66
+ }
67
+ if (Array.isArray(record.any)) {
68
+ return record.any.some((entry) => evaluateExpr(entry, values, fallback));
69
+ }
70
+ if (record.not) {
71
+ return !evaluateExpr(record.not, values, fallback);
72
+ }
73
+ const path = record.path ?? record.key;
74
+ if (!path)
75
+ return fallback;
76
+ const actual = getValueByPath(values, path);
77
+ if (Object.prototype.hasOwnProperty.call(record, "truthy")) {
78
+ return Boolean(actual) === Boolean(record.truthy);
79
+ }
80
+ if (Object.prototype.hasOwnProperty.call(record, "equals")) {
81
+ return actual === record.equals;
82
+ }
83
+ if (Object.prototype.hasOwnProperty.call(record, "notEquals")) {
84
+ return actual !== record.notEquals;
85
+ }
86
+ if (Object.prototype.hasOwnProperty.call(record, "gt")) {
87
+ return Number(actual) > Number(record.gt);
88
+ }
89
+ if (Object.prototype.hasOwnProperty.call(record, "gte")) {
90
+ return Number(actual) >= Number(record.gte);
91
+ }
92
+ if (Object.prototype.hasOwnProperty.call(record, "lt")) {
93
+ return Number(actual) < Number(record.lt);
94
+ }
95
+ if (Object.prototype.hasOwnProperty.call(record, "lte")) {
96
+ return Number(actual) <= Number(record.lte);
97
+ }
98
+ if (Array.isArray(record.in)) {
99
+ return record.in.includes(actual);
100
+ }
101
+ return Boolean(actual);
102
+ };
103
+ const resolvePanelsForTab = (panels, tabId) => {
104
+ if (!tabId)
105
+ return panels;
106
+ return panels.filter((panel) => panel.tab === tabId);
107
+ };
108
+ const sortPanels = (panels) => {
109
+ return [...panels].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
110
+ };
111
+ const renderFieldControl = (field, value, onChange, onAction, disabled) => {
112
+ switch (field.control) {
113
+ case "range":
114
+ return (_jsxs("div", { className: "panel-field-control", children: [_jsx("input", { type: "range", min: field.min, max: field.max, step: field.step, value: typeof value === "number" ? value : (field.min ?? 0), onChange: (event) => onChange(Number.parseFloat(event.target.value)), disabled: disabled }), _jsx("input", { type: "number", min: field.min, max: field.max, step: field.step, value: typeof value === "number" ? value : (field.min ?? 0), onChange: (event) => onChange(Number.parseFloat(event.target.value)), disabled: disabled })] }));
115
+ case "number":
116
+ return (_jsx("input", { type: "number", min: field.min, max: field.max, step: field.step, value: typeof value === "number" ? value : "", onChange: (event) => onChange(Number.parseFloat(event.target.value)), disabled: disabled }));
117
+ case "toggle":
118
+ return (_jsx("input", { type: "checkbox", checked: Boolean(value), onChange: (event) => onChange(event.target.checked), disabled: disabled }));
119
+ case "select":
120
+ return (_jsx("select", { value: typeof value === "string" || typeof value === "number" ? String(value) : "", onChange: (event) => onChange(event.target.value), disabled: disabled, children: (field.options ?? []).map((option) => (_jsx("option", { value: String(option.value), children: option.label }, `${field.key}-${option.value}`))) }));
121
+ case "color":
122
+ return (_jsx("input", { type: "color", value: typeof value === "string" ? value : "#ffffff", onChange: (event) => onChange(event.target.value), disabled: disabled }));
123
+ case "textarea":
124
+ return (_jsx("textarea", { rows: field.rows ?? 3, value: typeof value === "string" ? value : "", onChange: (event) => onChange(event.target.value), disabled: disabled }));
125
+ case "file":
126
+ return _jsx("input", { type: "file", onChange: (event) => onChange(event.target.files?.[0] ?? null), disabled: disabled });
127
+ case "action":
128
+ return (_jsx("button", { type: "button", onClick: () => onAction?.(field.actionId ?? field.key), disabled: disabled, children: field.label }));
129
+ case "text":
130
+ default:
131
+ return (_jsx("input", { type: "text", value: typeof value === "string" ? value : "", onChange: (event) => onChange(event.target.value), disabled: disabled }));
132
+ }
133
+ };
134
+ const buildItemDefaults = (field) => {
135
+ if (isRecord(field.itemDefaults)) {
136
+ return { ...field.itemDefaults };
137
+ }
138
+ const defaults = {};
139
+ (field.itemFields ?? []).forEach((itemField) => {
140
+ if (itemField.defaultValue !== undefined) {
141
+ defaults[itemField.key] = itemField.defaultValue;
142
+ }
143
+ });
144
+ return defaults;
145
+ };
146
+ const FieldRow = ({ field, values, onFieldChange, onAction, }) => {
147
+ const visible = field.visibleWhen ? evaluateExpr(field.visibleWhen, values, true) : true;
148
+ if (!visible)
149
+ return null;
150
+ const disabled = field.disabledWhen ? evaluateExpr(field.disabledWhen, values, false) : false;
151
+ const rawValue = getValueByPath(values, field.key);
152
+ const value = rawValue ?? field.defaultValue;
153
+ if (field.control === "array") {
154
+ const arrayValue = Array.isArray(value) ? value : Array.isArray(field.defaultValue) ? field.defaultValue : [];
155
+ const itemFields = field.itemFields ?? [];
156
+ const itemDefaults = buildItemDefaults(field);
157
+ const minItems = field.minItems ?? 0;
158
+ const maxItems = field.maxItems ?? Number.POSITIVE_INFINITY;
159
+ const handleAdd = () => {
160
+ if (disabled || arrayValue.length >= maxItems)
161
+ return;
162
+ const nextItem = isRecord(itemDefaults) ? { ...itemDefaults } : itemDefaults;
163
+ onFieldChange(field.key, [...arrayValue, nextItem]);
164
+ };
165
+ const handleRemove = (index) => {
166
+ if (disabled || arrayValue.length <= minItems)
167
+ return;
168
+ const next = arrayValue.filter((_, idx) => idx !== index);
169
+ onFieldChange(field.key, next);
170
+ };
171
+ return (_jsxs("div", { className: "panel-field panel-field--array", children: [_jsxs("div", { className: "panel-field-label", children: [_jsx("span", { children: field.label }), _jsx("div", { className: "panel-array-actions", children: _jsx("button", { type: "button", onClick: handleAdd, disabled: disabled || arrayValue.length >= maxItems, children: "add" }) })] }), itemFields.length > 0 ? (_jsx("div", { className: "panel-array-items", children: arrayValue.map((_, index) => (_jsxs("div", { className: "panel-array-item", children: [_jsxs("div", { className: "panel-array-item-header", children: [_jsx("span", { children: field.itemLabel ? `${field.itemLabel} ${index + 1}` : `item ${index + 1}` }), _jsx("button", { type: "button", onClick: () => handleRemove(index), disabled: disabled || arrayValue.length <= minItems, children: "remove" })] }), _jsx("div", { className: "panel-array-item-fields", children: itemFields.map((itemField) => {
172
+ const itemKey = `${field.key}[${index}].${itemField.key}`;
173
+ return (_jsx(FieldRow, { field: { ...itemField, key: itemKey }, values: values, onFieldChange: onFieldChange, onAction: onAction }, `${itemKey}`));
174
+ }) })] }, `${field.key}-${index}`))) })) : (_jsx("textarea", { rows: field.rows ?? 4, value: JSON.stringify(arrayValue, null, 2), onChange: (event) => {
175
+ try {
176
+ const parsed = JSON.parse(event.target.value);
177
+ if (Array.isArray(parsed)) {
178
+ onFieldChange(field.key, parsed);
179
+ }
180
+ }
181
+ catch {
182
+ // ignore invalid JSON
183
+ }
184
+ }, disabled: disabled })), field.helpText ? _jsx("div", { className: "panel-field-help", children: field.helpText }) : null] }));
185
+ }
186
+ if (field.control === "oneOf") {
187
+ const variants = field.variants ?? [];
188
+ if (variants.length === 0)
189
+ return null;
190
+ const discriminatorPath = field.discriminator ?? `${field.key}.__variant`;
191
+ const currentId = getValueByPath(values, discriminatorPath) ?? variants[0].id;
192
+ const activeVariant = variants.find((variant) => variant.id === currentId) ?? variants[0];
193
+ return (_jsxs("div", { className: "panel-field panel-field--oneof", children: [_jsx("div", { className: "panel-field-label", children: _jsx("span", { children: field.label }) }), _jsx("select", { value: currentId, onChange: (event) => onFieldChange(discriminatorPath, event.target.value), disabled: disabled, children: variants.map((variant) => (_jsx("option", { value: variant.id, children: variant.label }, `${field.key}-${variant.id}`))) }), _jsx("div", { className: "panel-oneof-fields", children: activeVariant.fields.map((variantField) => {
194
+ const variantKey = `${field.key}.${variantField.key}`;
195
+ return (_jsx(FieldRow, { field: { ...variantField, key: variantKey }, values: values, onFieldChange: onFieldChange, onAction: onAction }, variantKey));
196
+ }) }), field.helpText ? _jsx("div", { className: "panel-field-help", children: field.helpText }) : null] }));
197
+ }
198
+ return (_jsxs("div", { className: "panel-field", children: [_jsxs("div", { className: "panel-field-label", children: [_jsx("span", { children: field.label }), field.unit ? _jsx("span", { className: "panel-field-unit", children: field.unit }) : null] }), renderFieldControl(field, value, (nextValue) => onFieldChange(field.key, nextValue), onAction, disabled), field.helpText ? _jsx("div", { className: "panel-field-help", children: field.helpText }) : null] }));
199
+ };
200
+ const PanelView = ({ panel, values, onFieldChange, onAction, }) => {
201
+ return (_jsxs("section", { className: "panel-section", "data-panel-id": panel.id, children: [_jsxs("header", { className: "panel-section-header", children: [_jsx("h3", { children: panel.title }), panel.description ? _jsx("p", { children: panel.description }) : null] }), panel.sections?.length ? (panel.sections.map((section) => (_jsxs("div", { className: "panel-section-group", children: [_jsx("h4", { children: section.title }), section.description ? _jsx("p", { children: section.description }) : null, _jsx("div", { className: "panel-section-fields", children: section.fields.map((field) => (_jsx(FieldRow, { field: field, values: values, onFieldChange: onFieldChange, onAction: onAction }, field.key))) })] }, section.id)))) : (_jsx("div", { className: "panel-section-fields", children: (panel.fields ?? []).map((field) => (_jsx(FieldRow, { field: field, values: values, onFieldChange: onFieldChange, onAction: onAction }, field.key))) }))] }));
202
+ };
203
+ const Tabs = ({ tabs, activeTab, onTabChange }) => (_jsx("div", { className: "panel-tabs", children: tabs.map((tab) => (_jsx("button", { type: "button", className: `panel-tab ${tab.id === activeTab ? "panel-tab--active" : ""}`, onClick: () => onTabChange(tab.id), children: tab.label }, tab.id))) }));
204
+ export function PanelRenderer({ layout, values, onChange, onAction, className, tabClassName, panelClassName, fieldClassName }) {
205
+ const sortedTabs = useMemo(() => (layout.tabs ? [...layout.tabs].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) : []), [layout.tabs]);
206
+ const defaultTab = sortedTabs[0]?.id;
207
+ const [activeTab, setActiveTab] = useState(defaultTab);
208
+ const panels = useMemo(() => {
209
+ const resolvedPanels = activeTab ? resolvePanelsForTab(layout.panels, activeTab) : layout.panels;
210
+ return sortPanels(resolvedPanels);
211
+ }, [layout.panels, activeTab]);
212
+ const handleFieldChange = (path, value) => {
213
+ onChange(path, value);
214
+ };
215
+ return (_jsxs("div", { className: className ?? "panel-renderer", "data-panel-renderer": true, children: [sortedTabs.length > 0 ? (_jsx("div", { className: tabClassName ?? "panel-tabs-wrapper", children: _jsx(Tabs, { tabs: sortedTabs, activeTab: activeTab, onTabChange: setActiveTab }) })) : null, _jsx("div", { className: panelClassName ?? "panel-panels", children: panels.map((panel) => (_jsx(PanelView, { panel: panel, values: values, onFieldChange: handleFieldChange, onAction: onAction }, panel.id))) }), fieldClassName ? _jsx("style", { children: `.panel-field{ ${fieldClassName} }` }) : null] }));
216
+ }
@@ -0,0 +1,3 @@
1
+ export { PanelRenderer } from "./PanelRenderer";
2
+ export type { PanelRendererProps } from "./PanelRenderer";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { PanelRenderer } from "./PanelRenderer";
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@treasuryspatial/panel-react",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "@treasuryspatial/panel-core": "^0.1.2"
22
+ },
23
+ "peerDependencies": {
24
+ "react": ">=18"
25
+ },
26
+ "devDependencies": {
27
+ "react": "19.1.0",
28
+ "@types/react": "^19",
29
+ "@types/react-dom": "^19"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -b",
33
+ "typecheck": "tsc -b --pretty false --noEmit"
34
+ }
35
+ }