@clyrex-digital/clyrex-controls-dev 0.8.1-dev.20260923043912 → 0.8.1-dev.20260923103910

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.
@@ -762,6 +762,11 @@ var flattenNodeText = (nodes) => {
762
762
  if (isTextNode(node)) return node.text ?? "";
763
763
  const element = asElementNode(node);
764
764
  if (element.type === "linebreak" || element.type === "line-break") return "\n";
765
+ if (element.type === "bulletList" || element.type === "numberedList") {
766
+ const items = Array.isArray(element.items) ? element.items : [];
767
+ return items.map((item) => `${flattenNodeText(item.children)}
768
+ `).join("");
769
+ }
765
770
  const inner = flattenNodeText(element.children);
766
771
  return element.type === "paragraph" ? `${inner}
767
772
  ` : inner;
@@ -865,6 +870,18 @@ var CommentNodeRenderer = ({ node, assetBaseUrl }) => {
865
870
  case "listitem":
866
871
  case "list-item":
867
872
  return /* @__PURE__ */ jsx6("li", { className: "comment-node-listitem", children: renderChildren(element.children, key, assetBaseUrl) });
873
+ // ContentComposer's shape: items are paragraph-like nodes (their own
874
+ // `children`), not wrapped in a separate "listitem" node.
875
+ case "bulletList":
876
+ case "numberedList": {
877
+ const ListTag = element.type === "numberedList" ? "ol" : "ul";
878
+ const items = Array.isArray(element.items) ? element.items : [];
879
+ return /* @__PURE__ */ jsx6(ListTag, { className: "comment-node-list", children: items.map((item, index) => {
880
+ const itemChildren = item.children ?? [];
881
+ const isEmptyItem = itemChildren.length === 0 || itemChildren.every((child) => isTextNode(child) && (child.text ?? "").length === 0);
882
+ return /* @__PURE__ */ jsx6("li", { className: "comment-node-listitem", children: isEmptyItem ? null : renderChildren(item.children, key, assetBaseUrl) }, item.id ?? `${key}-${index}`);
883
+ }) });
884
+ }
868
885
  case "quote":
869
886
  case "blockquote":
870
887
  return /* @__PURE__ */ jsx6("blockquote", { className: "comment-node-quote", children: renderChildren(element.children, key, assetBaseUrl) });
@@ -764,6 +764,11 @@ var flattenNodeText = (nodes) => {
764
764
  if (isTextNode(node)) return node.text ?? "";
765
765
  const element = asElementNode(node);
766
766
  if (element.type === "linebreak" || element.type === "line-break") return "\n";
767
+ if (element.type === "bulletList" || element.type === "numberedList") {
768
+ const items = Array.isArray(element.items) ? element.items : [];
769
+ return items.map((item) => `${flattenNodeText(item.children)}
770
+ `).join("");
771
+ }
767
772
  const inner = flattenNodeText(element.children);
768
773
  return element.type === "paragraph" ? `${inner}
769
774
  ` : inner;
@@ -867,6 +872,18 @@ var CommentNodeRenderer = ({ node, assetBaseUrl }) => {
867
872
  case "listitem":
868
873
  case "list-item":
869
874
  return /* @__PURE__ */ jsx6("li", { className: "comment-node-listitem", children: renderChildren(element.children, key, assetBaseUrl) });
875
+ // ContentComposer's shape: items are paragraph-like nodes (their own
876
+ // `children`), not wrapped in a separate "listitem" node.
877
+ case "bulletList":
878
+ case "numberedList": {
879
+ const ListTag = element.type === "numberedList" ? "ol" : "ul";
880
+ const items = Array.isArray(element.items) ? element.items : [];
881
+ return /* @__PURE__ */ jsx6(ListTag, { className: "comment-node-list", children: items.map((item, index) => {
882
+ const itemChildren = item.children ?? [];
883
+ const isEmptyItem = itemChildren.length === 0 || itemChildren.every((child) => isTextNode(child) && (child.text ?? "").length === 0);
884
+ return /* @__PURE__ */ jsx6("li", { className: "comment-node-listitem", children: isEmptyItem ? null : renderChildren(item.children, key, assetBaseUrl) }, item.id ?? `${key}-${index}`);
885
+ }) });
886
+ }
870
887
  case "quote":
871
888
  case "blockquote":
872
889
  return /* @__PURE__ */ jsx6("blockquote", { className: "comment-node-quote", children: renderChildren(element.children, key, assetBaseUrl) });
@@ -0,0 +1,377 @@
1
+ "use client";
2
+
3
+ "use client";
4
+ import {
5
+ InputControl_default
6
+ } from "./chunk-XDTWUEVR.mjs";
7
+ import {
8
+ Button_default
9
+ } from "./chunk-LSKD5U3S.mjs";
10
+ import {
11
+ InputControlType_default
12
+ } from "./chunk-F4DNLPFN.mjs";
13
+ import "./chunk-IMNQO57B.mjs";
14
+
15
+ // src/components/pageRenderingEngine/nodes/DataBindingControls.tsx
16
+ import { useCallback, useEffect, useRef, useState } from "react";
17
+ import { useRouter } from "next/navigation";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ var ACTIVE_QUERY_CONTROLS_EVENT = "data-binding-active-query-controls";
20
+ var humanize = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
21
+ var escapeODataString = (value) => value.replace(/'/g, "''");
22
+ var parseFacetSelections = (filter) => {
23
+ const selections = /* @__PURE__ */ new Map();
24
+ const groupedExpression = /\(\s*([^()]+?)\s+eq\s+'((?:''|[^'])*)'\s*\)/gi;
25
+ for (const match of filter.matchAll(groupedExpression)) {
26
+ selections.set(match[1].trim(), match[2].replace(/''/g, "'"));
27
+ }
28
+ if (selections.size === 0) {
29
+ const ungroupedExpression = /(?:^|\s+and\s+)\s*([^()]+?)\s+eq\s+'((?:''|[^'])*)'\s*(?=\s+and\s+|$)/gi;
30
+ for (const match of filter.matchAll(ungroupedExpression)) {
31
+ selections.set(
32
+ match[1].trim(),
33
+ match[2].replace(/''/g, "'")
34
+ );
35
+ }
36
+ }
37
+ return selections;
38
+ };
39
+ var buildFacetFilter = (selections) => Array.from(selections.entries()).map(([field, value]) => `${field} eq '${escapeODataString(value)}'`).join(" and ");
40
+ var parseODataSearch = (value) => {
41
+ if (!value) return "";
42
+ const trimmedValue = value.trim();
43
+ if (trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) {
44
+ return trimmedValue.slice(1, -1).replace(/''/g, "'");
45
+ }
46
+ return trimmedValue;
47
+ };
48
+ var SearchIcon = ({ className = "" }) => /* @__PURE__ */ jsxs(
49
+ "svg",
50
+ {
51
+ "aria-hidden": "true",
52
+ className,
53
+ fill: "none",
54
+ viewBox: "0 0 24 24",
55
+ stroke: "currentColor",
56
+ strokeWidth: "2",
57
+ children: [
58
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
59
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "m20 20-4-4" })
60
+ ]
61
+ }
62
+ );
63
+ var ResetIcon = ({ className = "" }) => /* @__PURE__ */ jsxs(
64
+ "svg",
65
+ {
66
+ "aria-hidden": "true",
67
+ className,
68
+ fill: "none",
69
+ viewBox: "0 0 24 24",
70
+ stroke: "currentColor",
71
+ strokeWidth: "2",
72
+ children: [
73
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4 4v6h6" }),
74
+ /* @__PURE__ */ jsx(
75
+ "path",
76
+ {
77
+ strokeLinecap: "round",
78
+ strokeLinejoin: "round",
79
+ d: "M5.5 15a8 8 0 1 0 .6-7.8L4 10"
80
+ }
81
+ )
82
+ ]
83
+ }
84
+ );
85
+ var normalizeSortOption = (option) => {
86
+ const direction = option.direction.trim();
87
+ return {
88
+ label: option.title || humanize(option.name),
89
+ value: [option.name, direction].filter(Boolean).join(" ")
90
+ };
91
+ };
92
+ var DataBindingControls = ({
93
+ path,
94
+ query,
95
+ facets,
96
+ sortOptions,
97
+ showFacets,
98
+ showSortOptions,
99
+ showSearchBar,
100
+ mode = "toolbar"
101
+ }) => {
102
+ const router = useRouter();
103
+ const facetGroups = Object.entries(facets ?? {}).filter(
104
+ ([, values]) => Array.isArray(values) && values.length > 0
105
+ );
106
+ const normalizedSortOptions = (sortOptions ?? []).filter((option) => option?.name && option?.direction).map(normalizeSortOption).filter((option) => option.value);
107
+ const queryFilter = query?.["$filter"] ?? query?.filter ?? "";
108
+ const querySort = query?.["$orderby"] ?? query?.orderBy ?? "";
109
+ const [currentFilter, setCurrentFilter] = useState(queryFilter);
110
+ const [currentSort, setCurrentSort] = useState(querySort);
111
+ const selectedFacetValues = parseFacetSelections(currentFilter);
112
+ const currentSearch = parseODataSearch(query?.["$search"]);
113
+ const [searchText, setSearchText] = useState(currentSearch);
114
+ const lastNavigationUrl = useRef(null);
115
+ useEffect(() => {
116
+ setSearchText(currentSearch);
117
+ }, [currentSearch]);
118
+ useEffect(() => {
119
+ setCurrentFilter(queryFilter);
120
+ }, [queryFilter]);
121
+ useEffect(() => {
122
+ setCurrentSort(querySort);
123
+ }, [querySort]);
124
+ useEffect(() => {
125
+ const syncActiveQueryControls2 = (event) => {
126
+ const { filter, sort } = event.detail;
127
+ if (filter !== void 0) {
128
+ setCurrentFilter(filter);
129
+ }
130
+ if (sort !== void 0) {
131
+ setCurrentSort(sort);
132
+ }
133
+ };
134
+ window.addEventListener(
135
+ ACTIVE_QUERY_CONTROLS_EVENT,
136
+ syncActiveQueryControls2
137
+ );
138
+ return () => window.removeEventListener(
139
+ ACTIVE_QUERY_CONTROLS_EVENT,
140
+ syncActiveQueryControls2
141
+ );
142
+ }, []);
143
+ const syncActiveQueryControls = useCallback(
144
+ (updates) => {
145
+ window.dispatchEvent(
146
+ new CustomEvent(ACTIVE_QUERY_CONTROLS_EVENT, {
147
+ detail: updates
148
+ })
149
+ );
150
+ },
151
+ []
152
+ );
153
+ const getQueryUrl = useCallback(
154
+ (updates) => {
155
+ const params = new URLSearchParams(query ?? {});
156
+ Object.entries(updates).forEach(([key, value]) => {
157
+ if (value) {
158
+ params.set(key, value);
159
+ } else {
160
+ params.delete(key);
161
+ }
162
+ });
163
+ params.delete("$skip");
164
+ params.delete("skip");
165
+ const queryString = Array.from(params.entries()).map(
166
+ ([key, value]) => key === "$filter" || key === "filter" ? `${key}=${encodeURIComponent(value)}` : `${key}=${value}`
167
+ ).join("&");
168
+ return queryString ? `${path}?${queryString}` : path;
169
+ },
170
+ [path, query]
171
+ );
172
+ const getFacetUrl = (field, value) => {
173
+ const updatedSelections = new Map(selectedFacetValues);
174
+ if (value) {
175
+ updatedSelections.set(field, value);
176
+ } else {
177
+ updatedSelections.delete(field);
178
+ }
179
+ const updatedFilter = buildFacetFilter(updatedSelections);
180
+ syncActiveQueryControls({ filter: updatedFilter });
181
+ return getQueryUrl({
182
+ $filter: updatedFilter || void 0,
183
+ filter: void 0
184
+ });
185
+ };
186
+ const navigateWithoutReload = useCallback(
187
+ (url) => {
188
+ if (lastNavigationUrl.current === url) {
189
+ return;
190
+ }
191
+ if (typeof window !== "undefined") {
192
+ const targetUrl = new URL(url, window.location.origin);
193
+ if (targetUrl.pathname === window.location.pathname && targetUrl.search === window.location.search) {
194
+ return;
195
+ }
196
+ }
197
+ lastNavigationUrl.current = url;
198
+ router.push(url, { scroll: false });
199
+ },
200
+ [router]
201
+ );
202
+ const handleSearch = useCallback(async () => {
203
+ const trimmedSearchText = searchText.trim();
204
+ if (trimmedSearchText === currentSearch.trim()) {
205
+ return { isSuccessful: true };
206
+ }
207
+ navigateWithoutReload(
208
+ getQueryUrl({
209
+ "$search": trimmedSearchText || void 0
210
+ })
211
+ );
212
+ return { isSuccessful: true };
213
+ }, [currentSearch, getQueryUrl, navigateWithoutReload, searchText]);
214
+ const handleClearAll = useCallback(async () => {
215
+ syncActiveQueryControls({ filter: "", sort: "" });
216
+ navigateWithoutReload(
217
+ getQueryUrl({
218
+ $filter: void 0,
219
+ filter: void 0,
220
+ $orderby: void 0,
221
+ orderBy: void 0
222
+ })
223
+ );
224
+ return { isSuccessful: true };
225
+ }, [getQueryUrl, navigateWithoutReload, syncActiveQueryControls]);
226
+ const hasFacetControls = showFacets && facetGroups.length > 0;
227
+ const hasSortControls = showSortOptions && normalizedSortOptions.length > 0;
228
+ const hasActiveFiltersOrSort = Boolean(
229
+ currentFilter.trim() || currentSort.trim()
230
+ );
231
+ const shouldShowToolbarClearAll = hasActiveFiltersOrSort && !hasFacetControls;
232
+ const shouldShowToolbar = mode === "toolbar" && (showSearchBar || hasSortControls || shouldShowToolbarClearAll);
233
+ const shouldShowFacets = mode === "facets" && hasFacetControls;
234
+ if (!shouldShowToolbar && !shouldShowFacets) {
235
+ return null;
236
+ }
237
+ const renderFacetLinks = (field, values, stacked = false) => /* @__PURE__ */ jsxs("div", { className: stacked ? "flex flex-col gap-0.5" : "flex flex-wrap gap-1.5", children: [
238
+ /* @__PURE__ */ jsx(
239
+ "button",
240
+ {
241
+ type: "button",
242
+ onClick: () => navigateWithoutReload(getFacetUrl(field)),
243
+ "aria-pressed": !selectedFacetValues.has(field),
244
+ className: `cursor-pointer px-3 py-1.5 text-left text-sm transition-all duration-150 ease-out hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${stacked ? "w-full" : "border"} ${selectedFacetValues.has(field) ? "hover:bg-info-soft" : "bg-primary-base font-semibold hover:brightness-95"}`,
245
+ children: "All"
246
+ }
247
+ ),
248
+ values.map((facet) => {
249
+ const isSelected = selectedFacetValues.get(field) === facet.value;
250
+ return /* @__PURE__ */ jsxs(
251
+ "button",
252
+ {
253
+ type: "button",
254
+ onClick: () => navigateWithoutReload(getFacetUrl(field, facet.value)),
255
+ "aria-pressed": isSelected,
256
+ className: `cursor-pointer px-3 py-1.5 text-left text-sm transition-all duration-150 ease-out hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${stacked ? "w-full" : "border"} ${isSelected ? "bg-primary-base font-semibold hover:brightness-95" : "hover:bg-info-soft"}`,
257
+ children: [
258
+ facet.value,
259
+ " (",
260
+ facet.count,
261
+ ")"
262
+ ]
263
+ },
264
+ `${field}-${facet.value}`
265
+ );
266
+ })
267
+ ] });
268
+ return /* @__PURE__ */ jsxs(
269
+ "div",
270
+ {
271
+ className: mode === "toolbar" ? "my-6" : "flex flex-col gap-2 border bg-default rounded-[5px] p-3",
272
+ children: [
273
+ shouldShowToolbar && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
274
+ showSearchBar && /* @__PURE__ */ jsxs(
275
+ "form",
276
+ {
277
+ role: "search",
278
+ className: "flex w-full flex-col gap-3 rounded-xl border border-info bg-info-soft p-3 sm:flex-row sm:items-center md:min-w-0 md:flex-1",
279
+ onSubmit: (event) => {
280
+ event.preventDefault();
281
+ void handleSearch();
282
+ },
283
+ children: [
284
+ /* @__PURE__ */ jsxs("div", { className: "relative bg-default rounded-xl min-w-0 flex-1", children: [
285
+ /* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-4 top-1/2 z-10 h-5 w-5 -translate-y-1/2" }),
286
+ /* @__PURE__ */ jsx(
287
+ InputControl_default,
288
+ {
289
+ name: "searchText",
290
+ controlType: InputControlType_default.lineTextInput,
291
+ value: searchText,
292
+ callback: (updatedValues) => setSearchText(updatedValues.value),
293
+ attributes: {
294
+ placeholder: "Search discussions, topics, or keywords..."
295
+ },
296
+ inputClasses: "!mt-0 !rounded-xl !py-3 !pl-11 !pr-4 !shadow-sm"
297
+ }
298
+ )
299
+ ] }),
300
+ /* @__PURE__ */ jsxs(
301
+ Button_default,
302
+ {
303
+ ButtonType: "Primary" /* Solid */,
304
+ onClick: handleSearch,
305
+ className: "!rounded-xl !px-6 !py-3 sm:self-stretch",
306
+ children: [
307
+ /* @__PURE__ */ jsx(SearchIcon, { className: "h-5 w-5" }),
308
+ /* @__PURE__ */ jsx("span", { children: "Search" })
309
+ ]
310
+ }
311
+ )
312
+ ]
313
+ }
314
+ ),
315
+ hasSortControls && /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-3 rounded-2xl border border-info bg-info-soft p-3 text-sm md:ml-auto md:w-auto md:min-w-72 md:self-stretch", children: [
316
+ /* @__PURE__ */ jsx(
317
+ "label",
318
+ {
319
+ htmlFor: "sortOptions",
320
+ className: "shrink-0 whitespace-nowrap font-medium",
321
+ children: "Sort by"
322
+ }
323
+ ),
324
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 rounded-xl bg-default md:min-w-48", children: /* @__PURE__ */ jsx(
325
+ InputControl_default,
326
+ {
327
+ name: "sortOptions",
328
+ controlType: InputControlType_default.select,
329
+ value: currentSort,
330
+ dataset: normalizedSortOptions,
331
+ dataKeyFieldName: "value",
332
+ dataTextFieldName: "label",
333
+ callback: (updatedValues) => {
334
+ const updatedSort = updatedValues.value || "";
335
+ syncActiveQueryControls({ sort: updatedSort });
336
+ navigateWithoutReload(
337
+ getQueryUrl({
338
+ $orderby: updatedSort || void 0,
339
+ orderBy: void 0
340
+ })
341
+ );
342
+ },
343
+ attributes: { placeholder: "Default" },
344
+ inputClasses: "!mt-0 !rounded-xl !border-info !bg-default !px-4 !py-3 !text-body !shadow-sm focus:!border-primary focus:!ring-primary-base"
345
+ }
346
+ ) })
347
+ ] })
348
+ ] }),
349
+ shouldShowFacets && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b pb-2", children: [
350
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Filters" }),
351
+ hasActiveFiltersOrSort && /* @__PURE__ */ jsxs(
352
+ "button",
353
+ {
354
+ type: "button",
355
+ onClick: () => void handleClearAll(),
356
+ className: "group inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors duration-150 hover:bg-info-soft hover:text-primary-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
357
+ "aria-label": "Clear all filters and sorting",
358
+ children: [
359
+ /* @__PURE__ */ jsx(ResetIcon, { className: "h-3.5 w-3.5 transition-transform duration-200 group-hover:-rotate-45" }),
360
+ /* @__PURE__ */ jsx("span", { children: "Clear all" })
361
+ ]
362
+ }
363
+ )
364
+ ] }),
365
+ shouldShowFacets && facetGroups.length === 1 && /* @__PURE__ */ jsx("nav", { "aria-label": `${humanize(facetGroups[0][0])} filters`, children: renderFacetLinks(facetGroups[0][0], facetGroups[0][1]) }),
366
+ shouldShowFacets && facetGroups.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: facetGroups.map(([field, values]) => /* @__PURE__ */ jsxs("details", { className: "border-b py-1 last:border-b-0", open: true, children: [
367
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer rounded-md px-2 py-1.5 font-semibold transition-colors duration-150 hover:bg-info-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: humanize(field) }),
368
+ /* @__PURE__ */ jsx("div", { className: "px-1 pb-1 pt-0.5", children: renderFacetLinks(field, values, true) })
369
+ ] }, field)) })
370
+ ]
371
+ }
372
+ );
373
+ };
374
+ var DataBindingControls_default = DataBindingControls;
375
+ export {
376
+ DataBindingControls_default as default
377
+ };