@lowdefy/blocks-antd 5.0.0 → 5.1.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.
@@ -40,10 +40,16 @@ const AutoCompleteInput = ({ blockId, classNames = {}, components, events, loadi
40
40
  backfill: properties.backfill,
41
41
  variant: properties.bordered === false ? 'borderless' : properties.variant,
42
42
  className: classNames.element,
43
+ classNames: {
44
+ content: classNames.selector
45
+ },
43
46
  style: {
44
47
  width: '100%',
45
48
  ...styles.element
46
49
  },
50
+ styles: {
51
+ content: styles.selector
52
+ },
47
53
  defaultOpen: properties.defaultOpen,
48
54
  disabled: properties.disabled || loading,
49
55
  placeholder: properties.placeholder ?? 'Type or select item',
@@ -23,6 +23,7 @@ export default {
23
23
  valueType: 'string',
24
24
  cssKeys: {
25
25
  element: 'The AutoComplete element.',
26
+ selector: 'The inner value container of the AutoComplete (antd `content` semantic slot).',
26
27
  label: 'The AutoComplete label.',
27
28
  extra: 'The AutoComplete extra content.',
28
29
  feedback: 'The AutoComplete validation feedback.',
@@ -22,21 +22,58 @@ import withTheme from '../withTheme.js';
22
22
  import disabledDate from '../../disabledDate.js';
23
23
  dayjs.extend(utc);
24
24
  function buildDateMap(dateCellData) {
25
- if (!type.isArray(dateCellData)) return {};
25
+ if (!type.isArray(dateCellData)) return {
26
+ map: {},
27
+ earliest: undefined
28
+ };
26
29
  const map = {};
30
+ let earliest;
27
31
  dateCellData.forEach((item)=>{
28
32
  if (type.isNone(item?.date)) return;
29
- const key = dayjs(item.date).format('YYYY-MM-DD');
33
+ const parsed = dayjs(item.date);
34
+ if (!parsed.isValid()) return;
35
+ const key = parsed.format('YYYY-MM-DD');
30
36
  if (!map[key]) map[key] = [];
31
37
  map[key].push(item);
38
+ if (!earliest || parsed.isBefore(earliest)) earliest = parsed;
32
39
  });
33
- return map;
40
+ return {
41
+ map,
42
+ earliest
43
+ };
44
+ }
45
+ // Pick a default month to open on when `value` is unset: earliest event date
46
+ // from dateCellData, or the earliest specific disabled date, or the min of a
47
+ // disabledDates range. Without this, a calendar whose data lives in a past or
48
+ // future month opens on "today" and shows nothing, confusing the user.
49
+ function pickDefaultValue({ earliestCellData, disabledDates }) {
50
+ if (earliestCellData) return earliestCellData;
51
+ if (type.isObject(disabledDates)) {
52
+ if (type.isArray(disabledDates.dates) && disabledDates.dates.length > 0) {
53
+ let earliest;
54
+ disabledDates.dates.forEach((d)=>{
55
+ const parsed = dayjs(d);
56
+ if (parsed.isValid() && (!earliest || parsed.isBefore(earliest))) earliest = parsed;
57
+ });
58
+ if (earliest) return earliest;
59
+ }
60
+ if (!type.isNone(disabledDates.min)) {
61
+ const parsed = dayjs(disabledDates.min);
62
+ if (parsed.isValid()) return parsed;
63
+ }
64
+ }
65
+ return undefined;
34
66
  }
35
67
  const CalendarBlock = ({ blockId, classNames = {}, events, methods, properties, styles = {}, value })=>{
36
- const dateMap = buildDateMap(properties.dateCellData);
68
+ const { map: dateMap, earliest: earliestCellData } = buildDateMap(properties.dateCellData);
37
69
  const hasDateData = Object.keys(dateMap).length > 0;
38
- return /*#__PURE__*/ React.createElement(Calendar, {
39
- id: blockId,
70
+ const defaultPanel = pickDefaultValue({
71
+ earliestCellData,
72
+ disabledDates: properties.disabledDates
73
+ });
74
+ return /*#__PURE__*/ React.createElement("div", {
75
+ id: blockId
76
+ }, /*#__PURE__*/ React.createElement(Calendar, {
40
77
  className: classNames.element,
41
78
  style: styles.element,
42
79
  fullscreen: properties.fullscreen !== false,
@@ -47,21 +84,24 @@ const CalendarBlock = ({ blockId, classNames = {}, events, methods, properties,
47
84
  dayjs(properties.validRange[1])
48
85
  ] : undefined,
49
86
  value: type.isDate(value) ? dayjs(value) : undefined,
87
+ defaultValue: !type.isDate(value) ? defaultPanel : undefined,
50
88
  onSelect: (date, selectInfo)=>{
51
89
  // Wrap with our dayjs — antd v6's internal dayjs may lack the utc plugin.
52
90
  const d = dayjs(date);
53
91
  const val = d.toDate();
54
- if (selectInfo?.source === 'date') {
55
- methods.setValue(val);
56
- methods.triggerEvent({
57
- name: 'onSelect',
58
- event: {
59
- value: val,
60
- date: d.format('YYYY-MM-DD'),
61
- source: selectInfo.source
62
- }
63
- });
64
- }
92
+ const source = selectInfo?.source ?? 'date';
93
+ // Only fire for date cell clicks — year/month panel navigation emits
94
+ // onSelect too but shouldn't push a value through the event chain.
95
+ if (source !== 'date' && source !== 'customize') return;
96
+ methods.setValue(val);
97
+ methods.triggerEvent({
98
+ name: 'onSelect',
99
+ event: {
100
+ value: val,
101
+ date: d.format('YYYY-MM-DD'),
102
+ source
103
+ }
104
+ });
65
105
  },
66
106
  onChange: (date)=>{
67
107
  const d = dayjs(date);
@@ -90,6 +130,8 @@ const CalendarBlock = ({ blockId, classNames = {}, events, methods, properties,
90
130
  if (info.type !== 'date') return info.originNode;
91
131
  const key = dayjs(current).format('YYYY-MM-DD');
92
132
  const items = dateMap[key];
133
+ // cellRender output is appended to the default date cell — return
134
+ // null for days with no events to avoid double-rendering the date.
93
135
  if (!items) return null;
94
136
  return /*#__PURE__*/ React.createElement("ul", {
95
137
  className: "ant-picker-calendar-events",
@@ -106,6 +148,6 @@ const CalendarBlock = ({ blockId, classNames = {}, events, methods, properties,
106
148
  text: item.content
107
149
  }))));
108
150
  } : undefined
109
- });
151
+ }));
110
152
  };
111
153
  export default withTheme('Calendar', withBlockDefaults(CalendarBlock));
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import disabledDates from '../../schemas/disabledDates.js';
16
16
  export default {
17
- category: 'display',
17
+ category: 'input',
18
18
  icons: [],
19
19
  valueType: 'date',
20
20
  cssKeys: {
@@ -112,6 +112,7 @@ const DrawerBlock = ({ blockId, classNames = {}, content, properties, methods, r
112
112
  id: blockId,
113
113
  closable: properties.closable,
114
114
  extra: content.extra && content.extra(),
115
+ footer: content.footer && content.footer(),
115
116
  getContainer: properties.getContainer,
116
117
  mask: properties.mask,
117
118
  maskClosable: properties.maskClosable,
@@ -145,6 +146,7 @@ const DrawerBlock = ({ blockId, classNames = {}, content, properties, methods, r
145
146
  styles: {
146
147
  header: styles.header,
147
148
  body: styles.body,
149
+ footer: styles.footer,
148
150
  mask: styles.mask,
149
151
  wrapper: styles.wrapper,
150
152
  content: styles.content
@@ -18,7 +18,8 @@
18
18
  valueType: null,
19
19
  slots: {
20
20
  content: 'Main Drawer body.',
21
- extra: 'Extra content in the header.'
21
+ extra: 'Extra content in the header.',
22
+ footer: 'The Drawer footer.'
22
23
  },
23
24
  cssKeys: {
24
25
  element: 'The Drawer element.',
@@ -12,9 +12,10 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import React, { useCallback } from 'react';
16
- import { Menu } from 'antd';
15
+ */ import React, { useCallback, useContext } from 'react';
16
+ import { Layout, Menu } from 'antd';
17
17
  import { type, get } from '@lowdefy/helpers';
18
+ const SiderContext = Layout._InternalSiderContext;
18
19
  import { withBlockDefaults } from '@lowdefy/block-utils';
19
20
  import withTheme from '../withTheme.js';
20
21
  import useItemShortcuts from '../useItemShortcuts.js';
@@ -133,6 +134,8 @@ function MenuComp({ blockId, classNames = {}, components: { Icon, Link, Shortcut
133
134
  }
134
135
  const menu = getDefaultMenu(menus, properties.menuId, properties.links);
135
136
  const theme = properties.theme;
137
+ const { siderCollapsed } = useContext(SiderContext) ?? {};
138
+ const isCollapsed = properties.collapsed === true || siderCollapsed === true;
136
139
  const items = buildMenuItems({
137
140
  links: menu,
138
141
  events,
@@ -185,7 +188,7 @@ function MenuComp({ blockId, classNames = {}, components: { Icon, Link, Shortcut
185
188
  mode: properties.mode,
186
189
  selectable: true,
187
190
  theme: theme,
188
- defaultOpenKeys: properties.defaultOpenKeys ?? (properties.mode === 'inline' && properties.collapsed !== true && [
191
+ defaultOpenKeys: properties.defaultOpenKeys ?? (properties.mode === 'inline' && !isCollapsed && [
189
192
  (menu.find((link)=>(link.links || []).map((subLink)=>subLink.links ? subLink.links.map((subSubLink)=>subSubLink.pageId) : [
190
193
  subLink.pageId
191
194
  ]).flat().some((link)=>(properties.selectedKeys ?? [
@@ -18,7 +18,8 @@ import { withBlockDefaults } from '@lowdefy/block-utils';
18
18
  import Button from '../Button/Button.js';
19
19
  import Drawer from '../Drawer/Drawer.js';
20
20
  import Menu from '../Menu/Menu.js';
21
- const MobileMenu = ({ basePath, blockId, classNames = {}, components, events, methods, menus, pageId, properties, rename, styles = {} })=>{
21
+ import { getDarkMode } from '../headerActions.js';
22
+ const MobileMenu = ({ basePath, blockId, classNames = {}, components, content, events, methods, menus, pageId, properties, rename, styles = {} })=>{
22
23
  const [openState, setOpen] = useState(false);
23
24
  useEffect(()=>{
24
25
  methods.registerMethod(get(rename, 'methods.toggleOpen', {
@@ -88,7 +89,18 @@ const MobileMenu = ({ basePath, blockId, classNames = {}, components, events, me
88
89
  default: 'toggleOpen'
89
90
  })](),
90
91
  content: {
91
- content: ()=>/*#__PURE__*/ React.createElement(Menu, {
92
+ extra: properties.logo ? ()=>/*#__PURE__*/ React.createElement("div", {
93
+ style: {
94
+ flex: '1 0 auto'
95
+ }
96
+ }, /*#__PURE__*/ React.createElement(components.Link, {
97
+ home: true
98
+ }, /*#__PURE__*/ React.createElement("img", {
99
+ src: properties.logo?.srcMobile ?? properties.logo?.src ?? `${basePath}/logo-square-${getDarkMode() ? 'dark' : 'light'}-theme.png`,
100
+ alt: properties.logo?.alt ?? 'Lowdefy',
101
+ style: properties.logo?.style
102
+ }))) : undefined,
103
+ content: ()=>/*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(Menu, {
92
104
  basePath: basePath,
93
105
  components: components,
94
106
  blockId: `${blockId}_menu`,
@@ -113,7 +125,8 @@ const MobileMenu = ({ basePath, blockId, classNames = {}, components, events, me
113
125
  onSelect: 'onMenuItemSelect'
114
126
  }
115
127
  }
116
- })
128
+ }), content?.drawerContent && content.drawerContent()),
129
+ footer: content?.drawerFooter ? ()=>content.drawerFooter() : undefined
117
130
  }
118
131
  }));
119
132
  };
@@ -13,12 +13,16 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ export default {
16
- category: 'display',
16
+ category: 'container',
17
17
  icons: [
18
18
  'AiOutlineMenuUnfold',
19
19
  'AiOutlineMenuFold'
20
20
  ],
21
21
  valueType: null,
22
+ slots: {
23
+ drawerContent: 'Additional content below the menu in the drawer.',
24
+ drawerFooter: 'Footer content in the drawer.'
25
+ },
22
26
  cssKeys: {
23
27
  element: 'The MobileMenu element.'
24
28
  },
@@ -48,6 +52,33 @@
48
52
  displayType: 'yaml'
49
53
  }
50
54
  },
55
+ logo: {
56
+ type: 'object',
57
+ description: 'Logo settings for the mobile menu drawer header.',
58
+ additionalProperties: false,
59
+ properties: {
60
+ src: {
61
+ type: 'string',
62
+ description: 'Logo source url.'
63
+ },
64
+ srcMobile: {
65
+ type: 'string',
66
+ description: 'Mobile logo source url.'
67
+ },
68
+ alt: {
69
+ type: 'string',
70
+ default: 'Lowdefy',
71
+ description: 'Logo alternative text.'
72
+ },
73
+ style: {
74
+ type: 'object',
75
+ description: 'Css style object to apply to logo.',
76
+ docs: {
77
+ displayType: 'yaml'
78
+ }
79
+ }
80
+ }
81
+ },
51
82
  menuId: {
52
83
  type: 'string',
53
84
  description: 'App menu id used to get menu links.'
@@ -41,7 +41,7 @@ const tagRender = (props, option, methods, components)=>{
41
41
  }
42
42
  });
43
43
  };
44
- const MultipleSelector = ({ blockId, classNames = {}, components: { Icon }, events, loading, methods, properties, required, styles = {}, validation, value })=>{
44
+ const MultipleSelector = ({ blockId, classNames = {}, components: { Icon, ShortcutBadge }, events, loading, methods, properties, required, styles = {}, validation, value })=>{
45
45
  const [fetchState, setFetch] = useState(false);
46
46
  const [elementId] = useState((0 | Math.random() * 9e2) + 1e2);
47
47
  const uniqueValueOptions = getUniqueValues(properties.options ?? []);
@@ -73,15 +73,22 @@ const MultipleSelector = ({ blockId, classNames = {}, components: { Icon }, even
73
73
  autoFocus: properties.autoFocus,
74
74
  variant: properties.bordered === false ? 'borderless' : properties.variant,
75
75
  className: classNames.element,
76
+ classNames: {
77
+ content: classNames.selector
78
+ },
76
79
  style: {
77
80
  width: '100%',
78
81
  ...styles.element
79
82
  },
83
+ styles: {
84
+ content: styles.selector
85
+ },
80
86
  disabled: properties.disabled || loading,
81
87
  getPopupContainer: ()=>document.getElementById(`${blockId}_${elementId}_popup`),
82
88
  mode: "multiple",
83
89
  tagRender: properties.renderTags && ((props)=>tagRender(props, uniqueValueOptions[props.value], methods, {
84
- Icon
90
+ Icon,
91
+ ShortcutBadge
85
92
  })),
86
93
  maxTagCount: properties.maxTagCount,
87
94
  notFoundContent: fetchState ? properties.loadingPlaceholder || 'Loading' : properties.notFoundContent || 'Not found',
@@ -27,6 +27,7 @@ export default {
27
27
  valueType: 'array',
28
28
  cssKeys: {
29
29
  element: 'The MultipleSelector element.',
30
+ selector: 'The inner tag/value container of the MultipleSelector (antd `content` semantic slot). Use for capping the tag area height and enabling internal scroll.',
30
31
  clearIcon: 'The clear icon in the MultipleSelector.',
31
32
  label: 'The MultipleSelector label.',
32
33
  extra: 'The MultipleSelector extra content.',