@lowdefy/blocks-antd 5.3.0 → 5.4.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 (97) hide show
  1. package/dist/blocks/AutoComplete/AutoComplete.js +1 -0
  2. package/dist/blocks/AutoComplete/meta.js +2 -1
  3. package/dist/blocks/ButtonSelector/ButtonSelector.js +74 -27
  4. package/dist/blocks/ButtonSelector/meta.js +18 -4
  5. package/dist/blocks/Carousel/meta.js +16 -0
  6. package/dist/blocks/CheckboxSelector/CheckboxSelector.js +46 -14
  7. package/dist/blocks/CheckboxSelector/meta.js +7 -1
  8. package/dist/blocks/CheckboxSwitch/CheckboxSwitch.js +1 -0
  9. package/dist/blocks/CheckboxSwitch/meta.js +4 -1
  10. package/dist/blocks/ColorSelector/ColorSelector.js +1 -0
  11. package/dist/blocks/ColorSelector/meta.js +2 -1
  12. package/dist/blocks/ConfigProvider/ConfigProvider.js +1 -0
  13. package/dist/blocks/ConfigProvider/meta.js +7 -0
  14. package/dist/blocks/ConfirmModal/ConfirmModal.js +2 -2
  15. package/dist/blocks/ConfirmModal/meta.js +2 -4
  16. package/dist/blocks/DateRangeSelector/DateRangeSelector.js +4 -9
  17. package/dist/blocks/DateRangeSelector/meta.js +4 -8
  18. package/dist/blocks/DateSelector/DateSelector.js +4 -3
  19. package/dist/blocks/DateSelector/meta.js +4 -5
  20. package/dist/blocks/DateTimeSelector/DateTimeSelector.js +4 -3
  21. package/dist/blocks/DateTimeSelector/meta.js +4 -5
  22. package/dist/blocks/DropdownMenu/meta.js +46 -6
  23. package/dist/blocks/Label/Label.js +30 -5
  24. package/dist/blocks/Label/meta.js +8 -2
  25. package/dist/blocks/ListSelector/ListSelector.js +384 -0
  26. package/dist/blocks/ListSelector/e2e.js +40 -0
  27. package/dist/blocks/ListSelector/meta.js +215 -0
  28. package/dist/blocks/Menu/Menu.js +26 -80
  29. package/dist/blocks/Menu/meta.js +160 -64
  30. package/dist/blocks/MobileMenu/meta.js +50 -50
  31. package/dist/blocks/Modal/Modal.js +2 -2
  32. package/dist/blocks/Modal/meta.js +2 -4
  33. package/dist/blocks/MonthSelector/MonthSelector.js +4 -3
  34. package/dist/blocks/MonthSelector/meta.js +4 -5
  35. package/dist/blocks/MultipleSelector/MultipleSelector.js +41 -9
  36. package/dist/blocks/MultipleSelector/meta.js +24 -5
  37. package/dist/blocks/NumberInput/NumberInput.js +3 -1
  38. package/dist/blocks/NumberInput/meta.js +3 -3
  39. package/dist/blocks/PageHeaderMenu/PageHeaderMenu.js +10 -2
  40. package/dist/blocks/PageHeaderMenu/meta.js +8 -1
  41. package/dist/blocks/PageSidebarLayout/PageSidebarLayout.js +2 -1
  42. package/dist/blocks/PageSidebarLayout/meta.js +8 -1
  43. package/dist/blocks/PageSiderMenu/PageSiderMenu.js +2 -1
  44. package/dist/blocks/PageSiderMenu/meta.js +8 -1
  45. package/dist/blocks/PasswordInput/PasswordInput.js +1 -0
  46. package/dist/blocks/PasswordInput/meta.js +2 -1
  47. package/dist/blocks/PhoneNumberInput/PhoneNumberInput.js +1 -0
  48. package/dist/blocks/PhoneNumberInput/meta.js +2 -1
  49. package/dist/blocks/RadioSelector/RadioSelector.js +44 -14
  50. package/dist/blocks/RadioSelector/meta.js +7 -1
  51. package/dist/blocks/RatingSlider/meta.js +2 -1
  52. package/dist/blocks/SegmentedSelector/SegmentedSelector.js +10 -4
  53. package/dist/blocks/SegmentedSelector/meta.js +7 -4
  54. package/dist/blocks/Selector/Selector.js +55 -9
  55. package/dist/blocks/Selector/meta.js +24 -5
  56. package/dist/blocks/Slider/Slider.js +1 -0
  57. package/dist/blocks/Slider/meta.js +2 -1
  58. package/dist/blocks/Switch/Switch.js +1 -0
  59. package/dist/blocks/Switch/meta.js +2 -1
  60. package/dist/blocks/Tabs/Tabs.js +30 -43
  61. package/dist/blocks/Tabs/meta.js +8 -10
  62. package/dist/blocks/TextArea/TextArea.js +1 -0
  63. package/dist/blocks/TextArea/meta.js +2 -1
  64. package/dist/blocks/TextInput/TextInput.js +1 -0
  65. package/dist/blocks/TextInput/meta.js +2 -1
  66. package/dist/blocks/TreeInput/TreeInput.js +91 -0
  67. package/dist/blocks/TreeInput/e2e.js +33 -0
  68. package/dist/blocks/TreeInput/meta.js +68 -0
  69. package/dist/blocks/TreeMultipleSelector/TreeMultipleSelector.js +161 -0
  70. package/dist/blocks/TreeMultipleSelector/e2e.js +46 -0
  71. package/dist/blocks/TreeMultipleSelector/meta.js +128 -0
  72. package/dist/blocks/TreeSelector/TreeSelector.js +127 -88
  73. package/dist/blocks/TreeSelector/e2e.js +20 -9
  74. package/dist/blocks/TreeSelector/meta.js +70 -254
  75. package/dist/blocks/WeekSelector/WeekSelector.js +2 -1
  76. package/dist/blocks/WeekSelector/meta.js +3 -3
  77. package/dist/blocks/buildMenuItems.js +89 -26
  78. package/dist/blocks/headerActions.js +87 -3
  79. package/dist/blocks/normalizeItemClassAndStyle.js +77 -0
  80. package/dist/blocks.js +3 -0
  81. package/dist/e2e.js +3 -0
  82. package/dist/getContrastTextColor.js +45 -0
  83. package/dist/getOptionColorStyle.js +36 -0
  84. package/dist/getSelectedIndex.js +42 -0
  85. package/dist/getSelectorOptions.js +67 -0
  86. package/dist/getTreeData.js +94 -0
  87. package/dist/metas.js +3 -0
  88. package/dist/schemas/dataOptions.js +36 -0
  89. package/dist/schemas/index.js +1 -0
  90. package/dist/schemas/label.js +3 -1
  91. package/dist/schemas/labelTooltip.js +44 -0
  92. package/dist/schemas/options.js +7 -3
  93. package/dist/schemas/treeSelectTheme.js +125 -0
  94. package/dist/serializeSelectorValue.js +38 -0
  95. package/dist/useSelectorOptions.js +38 -0
  96. package/dist/useSetData.js +27 -0
  97. package/package.json +9 -7
@@ -13,34 +13,40 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React from 'react';
16
+ import { cn } from '@lowdefy/block-utils';
17
+ import normalizeItemClassAndStyle from './normalizeItemClassAndStyle.js';
16
18
  function getTitle({ id, properties, pageId, url }) {
17
19
  return properties?.title ?? pageId ?? url ?? id;
18
20
  }
19
- function buildMenuItems({ links, components: { Icon, Link, ShortcutBadge }, classNames, styles, events }) {
21
+ function defaultGetKey(link) {
22
+ return link.id;
23
+ }
24
+ function buildMenuItems({ links, components: { Icon, Link, ShortcutBadge }, classNames, styles, events, isTopLevel = true, wrapGroupLabel, nestedGroupAsGroup = false, getKey = defaultGetKey }) {
20
25
  return (links ?? []).map((link, i)=>{
26
+ const item = normalizeItemClassAndStyle(link);
21
27
  if (link.type === 'MenuDivider') {
22
28
  return {
23
29
  type: 'divider',
24
30
  key: link.id ?? i,
25
31
  dashed: link.properties?.dashed,
26
- style: link.style
32
+ className: cn(classNames?.item, item.class.element) || undefined,
33
+ style: item.style.element
27
34
  };
28
35
  }
29
36
  if (link.type === 'MenuGroup') {
30
- return {
31
- key: link.id,
32
- label: getTitle(link),
33
- icon: link.properties?.icon ? /*#__PURE__*/ React.createElement(Icon, {
34
- blockId: `${link.id}_icon`,
35
- classNames: {
36
- element: classNames.itemIcon
37
- },
38
- events: events,
39
- properties: link.properties.icon,
40
- styles: {
41
- element: styles.itemIcon
42
- }
43
- }) : undefined,
37
+ const labelText = getTitle(link);
38
+ const groupLabel = wrapGroupLabel ? wrapGroupLabel({
39
+ link,
40
+ labelText,
41
+ classNames: item.class.label,
42
+ styles: item.style.label
43
+ }) : labelText;
44
+ const renderAsGroup = !isTopLevel && nestedGroupAsGroup;
45
+ const groupItem = {
46
+ key: getKey(link),
47
+ label: groupLabel,
48
+ className: cn(classNames?.item, item.class.element) || undefined,
49
+ style: item.style.element,
44
50
  children: buildMenuItems({
45
51
  links: link.links,
46
52
  components: {
@@ -50,35 +56,92 @@ function buildMenuItems({ links, components: { Icon, Link, ShortcutBadge }, clas
50
56
  },
51
57
  classNames,
52
58
  styles,
53
- events
59
+ events,
60
+ isTopLevel: false,
61
+ wrapGroupLabel,
62
+ nestedGroupAsGroup,
63
+ getKey
54
64
  })
55
65
  };
66
+ if (renderAsGroup) {
67
+ // antd MenuItemGroupType supports neither icon, disabled, title, nor popupClassName.
68
+ groupItem.type = 'group';
69
+ return groupItem;
70
+ }
71
+ // SubMenu — supports icon, disabled, title (tooltip when collapsed), popupClassName.
72
+ groupItem.disabled = link.properties?.disabled;
73
+ groupItem.title = link.properties?.tooltip;
74
+ if (link.properties?.icon) {
75
+ groupItem.icon = /*#__PURE__*/ React.createElement(Icon, {
76
+ blockId: `${link.id}_icon`,
77
+ classNames: {
78
+ element: cn(classNames?.itemIcon, item.class.icon) || undefined
79
+ },
80
+ events: events,
81
+ properties: link.properties.icon,
82
+ styles: {
83
+ element: {
84
+ ...styles?.itemIcon,
85
+ ...item.style.icon
86
+ }
87
+ }
88
+ });
89
+ }
90
+ if (item.class.popup) groupItem.popupClassName = item.class.popup;
91
+ return groupItem;
56
92
  }
57
93
  // MenuLink (default)
94
+ // Strip class/style from the spread so slot-keyed values don't reach Link as raw props.
95
+ const { class: _omitClass, style: _omitStyle, ...linkRest } = link;
96
+ const extra = link.properties?.extra;
97
+ // antd v6's `extra` prop triggers a `display: inline-flex; width: 100%` layout
98
+ // on `.ant-menu-title-content-with-extra` that interacts badly with the Lowdefy
99
+ // `<Link>` wrapper (collapses the label). Render extra ourselves inside the Link
100
+ // using float, which works regardless of the parent's display mode.
58
101
  return {
59
- key: link.id,
102
+ key: getKey(link),
60
103
  danger: link.properties?.danger,
61
104
  disabled: link.properties?.disabled,
105
+ title: link.properties?.tooltip,
106
+ className: cn(classNames?.item, item.class.element) || undefined,
107
+ style: item.style.element,
62
108
  icon: link.properties?.icon ? /*#__PURE__*/ React.createElement(Icon, {
63
109
  blockId: `${link.id}_icon`,
64
110
  classNames: {
65
- element: classNames.itemIcon
111
+ element: cn(classNames?.itemIcon, item.class.icon) || undefined
66
112
  },
67
113
  events: events,
68
114
  properties: link.properties.icon,
69
115
  styles: {
70
- element: styles.itemIcon
116
+ element: {
117
+ ...styles?.itemIcon,
118
+ ...item.style.icon
119
+ }
71
120
  }
72
121
  }) : undefined,
73
122
  label: /*#__PURE__*/ React.createElement(Link, {
123
+ ...linkRest,
74
124
  id: link.pageId ?? link.id ?? i,
75
- style: link.style,
125
+ className: item.class.label || undefined,
126
+ style: item.style.label,
76
127
  url: link.url ?? link.properties?.url,
77
- newTab: link.newTab ?? link.properties?.newTab,
78
- ...link
79
- }, getTitle(link), /*#__PURE__*/ React.createElement(ShortcutBadge, {
80
- shortcut: link.properties?.shortcut
81
- }))
128
+ newTab: link.newTab ?? link.properties?.newTab
129
+ }, link.properties?.shortcut ? /*#__PURE__*/ React.createElement("span", {
130
+ style: {
131
+ float: 'right',
132
+ marginInlineStart: 12
133
+ }
134
+ }, /*#__PURE__*/ React.createElement(ShortcutBadge, {
135
+ shortcut: link.properties.shortcut
136
+ })) : null, extra ? /*#__PURE__*/ React.createElement("span", {
137
+ style: {
138
+ float: 'right',
139
+ marginInlineStart: 12,
140
+ opacity: 0.65,
141
+ fontSize: '0.9em',
142
+ pointerEvents: 'none'
143
+ }
144
+ }, extra) : null, getTitle(link))
82
145
  };
83
146
  });
84
147
  }
@@ -34,6 +34,17 @@ function getDarkModeLabel() {
34
34
  if (pref === 'light') return 'Light mode';
35
35
  return 'System';
36
36
  }
37
+ function getSupportedLocales() {
38
+ return window.__lowdefy_supported_locales ?? [];
39
+ }
40
+ function getActiveLocaleCode() {
41
+ return window.__lowdefy_locale;
42
+ }
43
+ function getActiveLocaleLabel() {
44
+ const active = getActiveLocaleCode();
45
+ const match = getSupportedLocales().find((l)=>l.code === active);
46
+ return match?.label ?? active ?? '';
47
+ }
37
48
  // Wraps a header action row for the expanded sider. Icon cell has a fixed
38
49
  // basis so bell / avatar / sun share a vertical line regardless of their
39
50
  // own intrinsic size; label fills the remaining width. Row gets a subtle
@@ -280,11 +291,68 @@ function renderDarkModeToggle({ blockId, classNames, styles, methods, events, Ic
280
291
  })
281
292
  }, icon);
282
293
  }
294
+ function renderLocaleSelector({ blockId, classNames, styles, methods, events, Icon, iconsColor, expanded }) {
295
+ const supported = getSupportedLocales();
296
+ if (supported.length === 0) return null;
297
+ const active = getActiveLocaleCode();
298
+ const items = supported.map((locale)=>({
299
+ key: locale.code,
300
+ label: locale.label ?? locale.code
301
+ }));
302
+ const icon = /*#__PURE__*/ React.createElement(Icon, {
303
+ blockId: `${blockId}_locale_selector_icon`,
304
+ events: events,
305
+ properties: {
306
+ name: 'AiOutlineGlobal'
307
+ },
308
+ styles: {
309
+ element: {
310
+ fontSize: 16,
311
+ color: iconsColor
312
+ }
313
+ }
314
+ });
315
+ const trigger = expanded ? /*#__PURE__*/ React.createElement(ExpandedRow, {
316
+ className: classNames.localeSelector,
317
+ style: styles.localeSelector,
318
+ label: getActiveLocaleLabel()
319
+ }, icon) : /*#__PURE__*/ React.createElement("div", {
320
+ className: classNames.localeSelector,
321
+ style: {
322
+ cursor: 'pointer',
323
+ lineHeight: 1,
324
+ ...styles.localeSelector
325
+ }
326
+ }, icon);
327
+ return /*#__PURE__*/ React.createElement(Dropdown, {
328
+ menu: {
329
+ items,
330
+ selectedKeys: active ? [
331
+ active
332
+ ] : [],
333
+ onClick: ({ key })=>{
334
+ methods.triggerEvent({
335
+ name: '__setLocale',
336
+ event: {
337
+ locale: key
338
+ }
339
+ });
340
+ }
341
+ },
342
+ trigger: [
343
+ expanded ? 'click' : 'hover'
344
+ ],
345
+ placement: expanded ? 'topRight' : 'bottomRight',
346
+ popupClassName: classNames.localeSelectorMenu,
347
+ popupStyle: styles.localeSelectorMenu
348
+ }, /*#__PURE__*/ React.createElement("div", null, trigger));
349
+ }
283
350
  function renderHeaderActions({ blockId, classNames = {}, styles = {}, properties, methods, events, components: { Icon, Link, ShortcutBadge }, iconsColor, expanded = false }) {
284
351
  const hasNotifications = !type.isNone(properties.notifications);
285
352
  const hasProfile = !type.isNone(properties.profile);
286
353
  const hasDarkMode = properties.darkModeToggle;
287
- if (!hasNotifications && !hasProfile && !hasDarkMode) return null;
354
+ const hasLocaleSelector = properties.localeSelector && getSupportedLocales().length > 0;
355
+ if (!hasNotifications && !hasProfile && !hasDarkMode && !hasLocaleSelector) return null;
288
356
  const ctx = {
289
357
  blockId,
290
358
  classNames,
@@ -302,7 +370,7 @@ function renderHeaderActions({ blockId, classNames = {}, styles = {}, properties
302
370
  return /*#__PURE__*/ React.createElement("div", {
303
371
  className: classNames.headerActions ?? defaultClassName,
304
372
  style: styles.headerActions
305
- }, hasNotifications && renderNotifications(ctx), hasProfile && renderProfile(ctx), hasDarkMode && renderDarkModeToggle(ctx));
373
+ }, hasNotifications && renderNotifications(ctx), hasProfile && renderProfile(ctx), hasDarkMode && renderDarkModeToggle(ctx), hasLocaleSelector && renderLocaleSelector(ctx));
306
374
  }
307
375
  function registerDarkModeMethod(methods) {
308
376
  methods.registerEvent({
@@ -320,4 +388,20 @@ function registerDarkModeMethod(methods) {
320
388
  });
321
389
  });
322
390
  }
323
- export { getDarkMode, renderHeaderActions, registerDarkModeMethod };
391
+ function registerLocaleMethod(methods) {
392
+ methods.registerEvent({
393
+ name: '__setLocale',
394
+ actions: [
395
+ {
396
+ id: '__set_locale',
397
+ type: 'SetLocale',
398
+ params: {
399
+ locale: {
400
+ _event: 'locale'
401
+ }
402
+ }
403
+ }
404
+ ]
405
+ });
406
+ }
407
+ export { getDarkMode, renderHeaderActions, registerDarkModeMethod, registerLocaleMethod };
@@ -0,0 +1,77 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import { cn } from '@lowdefy/block-utils';
17
+ const ITEM_SLOT_KEYS = [
18
+ 'element',
19
+ 'icon',
20
+ 'label',
21
+ 'popup'
22
+ ];
23
+ function isSlotKeyed(value) {
24
+ return Object.keys(value).some((k)=>k.startsWith('.'));
25
+ }
26
+ function partitionStyle(style) {
27
+ const slots = {
28
+ element: undefined,
29
+ icon: undefined,
30
+ label: undefined,
31
+ popup: undefined
32
+ };
33
+ if (type.isNone(style)) return slots;
34
+ if (!type.isObject(style)) return slots;
35
+ if (!isSlotKeyed(style)) {
36
+ slots.element = style;
37
+ return slots;
38
+ }
39
+ for (const [key, value] of Object.entries(style)){
40
+ if (!key.startsWith('.')) continue;
41
+ const slot = key.slice(1);
42
+ if (ITEM_SLOT_KEYS.includes(slot)) slots[slot] = value;
43
+ }
44
+ return slots;
45
+ }
46
+ function partitionClass(classValue) {
47
+ const slots = {
48
+ element: undefined,
49
+ icon: undefined,
50
+ label: undefined,
51
+ popup: undefined
52
+ };
53
+ if (type.isNone(classValue)) return slots;
54
+ if (type.isString(classValue) || type.isArray(classValue)) {
55
+ slots.element = cn(classValue);
56
+ return slots;
57
+ }
58
+ if (!type.isObject(classValue)) return slots;
59
+ if (!isSlotKeyed(classValue)) {
60
+ slots.element = cn(classValue);
61
+ return slots;
62
+ }
63
+ for (const [key, value] of Object.entries(classValue)){
64
+ if (!key.startsWith('.')) continue;
65
+ const slot = key.slice(1);
66
+ if (ITEM_SLOT_KEYS.includes(slot)) slots[slot] = cn(value);
67
+ }
68
+ return slots;
69
+ }
70
+ function normalizeItemClassAndStyle(link) {
71
+ return {
72
+ class: partitionClass(link?.class),
73
+ style: partitionStyle(link?.style)
74
+ };
75
+ }
76
+ export default normalizeItemClassAndStyle;
77
+ export { partitionClass, partitionStyle };
package/dist/blocks.js CHANGED
@@ -45,6 +45,7 @@ export { default as Footer } from './blocks/Footer/Footer.js';
45
45
  export { default as Header } from './blocks/Header/Header.js';
46
46
  export { default as Label } from './blocks/Label/Label.js';
47
47
  export { default as Layout } from './blocks/Layout/Layout.js';
48
+ export { default as ListSelector } from './blocks/ListSelector/ListSelector.js';
48
49
  export { default as Masonry } from './blocks/Masonry/Masonry.js';
49
50
  export { default as MasonryList } from './blocks/MasonryList/MasonryList.js';
50
51
  export { default as Menu } from './blocks/Menu/Menu.js';
@@ -85,6 +86,8 @@ export { default as TextArea } from './blocks/TextArea/TextArea.js';
85
86
  export { default as TextInput } from './blocks/TextInput/TextInput.js';
86
87
  export { default as Title } from './blocks/Title/Title.js';
87
88
  export { default as TitleInput } from './blocks/TitleInput/TitleInput.js';
89
+ export { default as TreeInput } from './blocks/TreeInput/TreeInput.js';
90
+ export { default as TreeMultipleSelector } from './blocks/TreeMultipleSelector/TreeMultipleSelector.js';
88
91
  export { default as TreeSelector } from './blocks/TreeSelector/TreeSelector.js';
89
92
  export { default as Tooltip } from './blocks/Tooltip/Tooltip.js';
90
93
  export { default as Tour } from './blocks/Tour/Tour.js';
package/dist/e2e.js CHANGED
@@ -41,6 +41,7 @@ export { default as Footer } from './blocks/Footer/e2e.js';
41
41
  export { default as Header } from './blocks/Header/e2e.js';
42
42
  export { default as Label } from './blocks/Label/e2e.js';
43
43
  export { default as Layout } from './blocks/Layout/e2e.js';
44
+ export { default as ListSelector } from './blocks/ListSelector/e2e.js';
44
45
  export { default as Menu } from './blocks/Menu/e2e.js';
45
46
  export { default as Message } from './blocks/Message/e2e.js';
46
47
  export { default as MobileMenu } from './blocks/MobileMenu/e2e.js';
@@ -75,5 +76,7 @@ export { default as TimelineList } from './blocks/TimelineList/e2e.js';
75
76
  export { default as Title } from './blocks/Title/e2e.js';
76
77
  export { default as TitleInput } from './blocks/TitleInput/e2e.js';
77
78
  export { default as Tooltip } from './blocks/Tooltip/e2e.js';
79
+ export { default as TreeInput } from './blocks/TreeInput/e2e.js';
80
+ export { default as TreeMultipleSelector } from './blocks/TreeMultipleSelector/e2e.js';
78
81
  export { default as TreeSelector } from './blocks/TreeSelector/e2e.js';
79
82
  export { default as WeekSelector } from './blocks/WeekSelector/e2e.js';
@@ -0,0 +1,45 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ // Returns '#000' or '#fff' for best contrast against a hex background color.
17
+ // Returns undefined for non-hex values (named colors, rgb(), etc.) so the
18
+ // caller can fall back to antd's default solid text color.
19
+ function getContrastTextColor(color) {
20
+ if (!type.isString(color)) return undefined;
21
+ const hex = color.trim().replace(/^#/, '');
22
+ let r;
23
+ let g;
24
+ let b;
25
+ if (hex.length === 3) {
26
+ r = parseInt(hex[0] + hex[0], 16);
27
+ g = parseInt(hex[1] + hex[1], 16);
28
+ b = parseInt(hex[2] + hex[2], 16);
29
+ } else if (hex.length === 6 || hex.length === 8) {
30
+ r = parseInt(hex.slice(0, 2), 16);
31
+ g = parseInt(hex.slice(2, 4), 16);
32
+ b = parseInt(hex.slice(4, 6), 16);
33
+ } else {
34
+ return undefined;
35
+ }
36
+ if ([
37
+ r,
38
+ g,
39
+ b
40
+ ].some((n)=>Number.isNaN(n))) return undefined;
41
+ // YIQ perceptual brightness: dark text on light backgrounds, light text on dark.
42
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
43
+ return yiq >= 128 ? '#000' : '#fff';
44
+ }
45
+ export default getContrastTextColor;
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import getContrastTextColor from './getContrastTextColor.js';
16
+ // Color styles for a selected selector option, by variant.
17
+ // outline: color the border/text + a low-opacity tint of the color.
18
+ // solid: fill with the color + auto-contrast text.
19
+ // `color` may be a CSS value such as 'var(--ant-color-primary)' for the active
20
+ // primary when no explicit color is set.
21
+ function getOptionColorStyle({ color, isOutline }) {
22
+ if (isOutline) {
23
+ return {
24
+ color,
25
+ borderColor: color,
26
+ backgroundColor: `color-mix(in srgb, ${color} 12%, transparent)`
27
+ };
28
+ }
29
+ const contrast = getContrastTextColor(color);
30
+ return {
31
+ backgroundColor: color,
32
+ borderColor: color,
33
+ color: contrast ?? '#fff'
34
+ };
35
+ }
36
+ export default getOptionColorStyle;
@@ -0,0 +1,42 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { get, type } from '@lowdefy/helpers';
16
+ import serializeSelectorValue from './serializeSelectorValue.js';
17
+ // Maps the current block value to the index (or indices) of the matching entry built by
18
+ // getSelectorOptions. Matching is by identity: when `primaryKey` names a field, that field is
19
+ // projected from both the stored value and each entry's value before comparison; with no
20
+ // `primaryKey` the whole value is compared (the original `value` behaviour, which also matches
21
+ // object-valued options). `valueKey` is only a build-time extraction key in getSelectorOptions —
22
+ // by match time the stored value is already the unwrapped entry value, so it must not drive
23
+ // identity here (reusing it projected a missing `.value` off the bare value and broke matching).
24
+ const serialize = serializeSelectorValue;
25
+ const getSelectedIndex = (value, entries, { properties = {}, multiple } = {})=>{
26
+ const { primaryKey } = properties;
27
+ const project = (val)=>type.isString(primaryKey) && type.isObject(val) ? get(val, primaryKey) : val;
28
+ const idOfValue = (v)=>project(v);
29
+ const idOfEntry = (entry)=>project(type.isPrimitive(entry) ? entry : entry.value);
30
+ const findIndex = (v)=>{
31
+ const target = serialize(idOfValue(v));
32
+ for(let i = 0; i < entries.length; i += 1){
33
+ if (serialize(idOfEntry(entries[i])) === target) return `${i}`;
34
+ }
35
+ return undefined;
36
+ };
37
+ if (multiple) {
38
+ return (type.isArray(value) ? value : []).map((v)=>findIndex(v));
39
+ }
40
+ return findIndex(value);
41
+ };
42
+ export default getSelectedIndex;
@@ -0,0 +1,67 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { get, type } from '@lowdefy/helpers';
16
+ import { nunjucksFunction } from '@lowdefy/nunjucks';
17
+ import serializeSelectorValue from './serializeSelectorValue.js';
18
+ // Selectors can be driven two ways:
19
+ // - `options`: an array of primitives or { label, value } pairs (the original model).
20
+ // - `data` + `html`: raw rows rendered through a Nunjucks template, where `valueKey` names the
21
+ // field stored as the value (omitted -> the whole row).
22
+ // Both are normalised here into the option shape the selectors already render: an array of
23
+ // primitives or objects carrying `label` (an html string) and `value`, plus any per-option extras
24
+ // (color, disabled, style, filterString, icon, tag) the blocks read.
25
+ // The identity used to de-duplicate options, kept symmetric with getSelectedIndex: project
26
+ // `primaryKey` from each entry's value, or compare the whole value when no `primaryKey` is set.
27
+ // `valueKey` has already been applied to produce `entry.value`, so it must not drive identity
28
+ // (projecting it off the wrapper entry yielded `undefined` for every option and collapsed them).
29
+ function identityOf(entry, primaryKey) {
30
+ const value = type.isPrimitive(entry) ? entry : entry.value;
31
+ return type.isString(primaryKey) && type.isObject(value) ? get(value, primaryKey) : value;
32
+ }
33
+ function dedupe(entries, primaryKey) {
34
+ const seen = new Set();
35
+ return entries.filter((entry)=>{
36
+ const id = serializeSelectorValue(identityOf(entry, primaryKey));
37
+ if (seen.has(id)) return false;
38
+ seen.add(id);
39
+ return true;
40
+ });
41
+ }
42
+ const getSelectorOptions = ({ properties })=>{
43
+ const { data, options, valueKey, primaryKey, html } = properties;
44
+ if (type.isArray(data)) {
45
+ const template = type.isString(html) ? nunjucksFunction(html) : null;
46
+ const entries = data.map((item, index)=>{
47
+ const value = type.isString(valueKey) && type.isObject(item) ? get(item, valueKey) : item;
48
+ const label = template ? template({
49
+ item,
50
+ index
51
+ }) : `${value}`;
52
+ return {
53
+ ...type.isObject(item) ? item : {},
54
+ label,
55
+ value
56
+ };
57
+ });
58
+ return dedupe(entries, primaryKey);
59
+ }
60
+ const vk = type.isString(valueKey) ? valueKey : 'value';
61
+ const entries = (options ?? []).map((opt)=>type.isPrimitive(opt) ? opt : {
62
+ ...opt,
63
+ value: get(opt, vk)
64
+ });
65
+ return dedupe(entries, primaryKey);
66
+ };
67
+ export default getSelectorOptions;