@lowdefy/blocks-antd 5.0.0 → 5.2.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.
@@ -0,0 +1,526 @@
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 MobileMenuMeta from '../MobileMenu/meta.js';
16
+ import icon from '../../schemas/icon.js';
17
+ import menuLinks from '../../schemas/menuLinks.js';
18
+ import breadcrumbList from '../../schemas/breadcrumbList.js';
19
+ export default {
20
+ category: 'container',
21
+ icons: [
22
+ 'AiOutlineBell',
23
+ 'AiOutlineLaptop',
24
+ 'AiOutlineMenuFold',
25
+ 'AiOutlineMenuUnfold',
26
+ 'AiOutlineMoon',
27
+ 'AiOutlineSun',
28
+ 'AiOutlineUser',
29
+ ...MobileMenuMeta.icons
30
+ ],
31
+ valueType: null,
32
+ slots: {
33
+ content: 'Main page content.',
34
+ footer: 'Page footer.',
35
+ header: 'Additional header content.',
36
+ mobileDrawerContent: 'Content in the mobile menu drawer.',
37
+ mobileDrawerFooter: 'Footer in the mobile menu drawer.',
38
+ mobileExtra: 'Extra content in the mobile header bar.',
39
+ siderClosed: 'Content shown in the sider when collapsed.',
40
+ siderOpen: 'Content shown in the sider when expanded.'
41
+ },
42
+ cssKeys: {
43
+ element: 'The PageSidebarLayout element.',
44
+ sider: 'The PageSidebarLayout sider.',
45
+ menu: 'The PageSidebarLayout menu.',
46
+ mobileHeader: 'The PageSidebarLayout mobile header.',
47
+ mobileMenu: 'The PageSidebarLayout mobile menu.',
48
+ header: 'The PageSidebarLayout header.',
49
+ headerActions: 'The header actions container (notifications, profile, dark mode toggle).',
50
+ logo: 'The PageSidebarLayout logo.',
51
+ notifications: 'The notification bell button.',
52
+ notificationsBadge: 'The notification badge wrapper.',
53
+ notificationsIcon: 'The notification bell icon.',
54
+ profile: 'The profile avatar and dropdown wrapper.',
55
+ profileAvatar: 'The profile avatar element.',
56
+ profileMenu: 'The profile dropdown menu popup.',
57
+ darkModeToggle: 'The dark mode toggle button.',
58
+ content: 'The PageSidebarLayout content.',
59
+ breadcrumb: 'The PageSidebarLayout breadcrumb.',
60
+ footer: 'The PageSidebarLayout footer.',
61
+ toggleButton: 'The PageSidebarLayout sider toggle button.'
62
+ },
63
+ events: {
64
+ onToggleSider: 'Trigger action when sider toggle button is clicked.',
65
+ onMenuItemClick: 'Trigger action when menu item is clicked.',
66
+ onMenuItemSelect: 'Trigger action when menu item is selected.',
67
+ onToggleMenuGroup: 'Trigger action when menu group is opened.',
68
+ onBreadcrumbClick: 'Trigger action when a breadcrumb item is clicked.',
69
+ onMobileMenuOpen: 'Trigger action when mobile menu is opened.',
70
+ onMobileMenuClose: 'Trigger action when mobile menu is closed.',
71
+ onToggleDrawer: 'Trigger action when mobile menu drawer is toggled.',
72
+ onProfileMenuClick: {
73
+ description: 'Trigger action when a profile dropdown menu item is clicked.',
74
+ event: {
75
+ key: 'The menu item key (id).',
76
+ keyPath: 'The key path of the menu item.',
77
+ pageId: 'The page id of the menu item.',
78
+ url: 'The url of the menu item.'
79
+ }
80
+ },
81
+ onProfileMenuOpen: {
82
+ description: 'Trigger action when the profile dropdown opens or closes.',
83
+ event: {
84
+ open: 'Whether the dropdown is open.'
85
+ }
86
+ }
87
+ },
88
+ properties: {
89
+ type: 'object',
90
+ additionalProperties: false,
91
+ properties: {
92
+ title: {
93
+ type: 'string',
94
+ description: 'Page title. Accepted for compatibility.'
95
+ },
96
+ theme: {
97
+ type: 'object',
98
+ description: 'Antd design token overrides for this block. See <a href="https://ant.design/components/overview#design-token">antd design tokens</a>.',
99
+ docs: {
100
+ displayType: 'yaml'
101
+ }
102
+ },
103
+ logo: {
104
+ type: 'object',
105
+ description: 'Header logo settings. By default, images are served from the app public folder and auto-swap between light and dark variants based on dark mode. See <a href="/hosting-files">Hosting Files</a> for details.',
106
+ additionalProperties: false,
107
+ properties: {
108
+ src: {
109
+ type: 'string',
110
+ description: 'Logo image URL for desktop. Defaults to logo-light-theme.png or logo-dark-theme.png from the public folder (~250x72px), auto-selected based on dark mode.'
111
+ },
112
+ srcMobile: {
113
+ type: 'string',
114
+ description: 'Logo image URL for mobile. Defaults to logo-square-light-theme.png or logo-square-dark-theme.png from the public folder (~125x125px), auto-selected based on dark mode.'
115
+ },
116
+ alt: {
117
+ type: 'string',
118
+ default: 'Lowdefy',
119
+ description: 'Logo alternative text.'
120
+ },
121
+ style: {
122
+ type: 'object',
123
+ description: 'Css style object to apply to logo.',
124
+ docs: {
125
+ displayType: 'yaml'
126
+ }
127
+ }
128
+ }
129
+ },
130
+ sider: {
131
+ type: 'object',
132
+ description: 'Sider properties.',
133
+ additionalProperties: false,
134
+ properties: {
135
+ collapsedWidth: {
136
+ type: 'integer',
137
+ description: 'Width of the collapsed sidebar, by setting to 0 a special trigger will appear.'
138
+ },
139
+ collapsible: {
140
+ type: 'boolean',
141
+ default: true,
142
+ description: 'Whether can be collapsed.'
143
+ },
144
+ initialCollapsed: {
145
+ type: 'boolean',
146
+ default: false,
147
+ description: 'Set the initial collapsed state.'
148
+ },
149
+ width: {
150
+ type: [
151
+ 'string',
152
+ 'number'
153
+ ],
154
+ description: 'Width of the sidebar.',
155
+ docs: {
156
+ displayType: 'string'
157
+ }
158
+ },
159
+ hideToggleButton: {
160
+ type: 'boolean',
161
+ description: 'Hide toggle button in sider.',
162
+ default: false
163
+ }
164
+ }
165
+ },
166
+ siderStorageKey: {
167
+ type: 'string',
168
+ default: 'sider',
169
+ description: "localStorage key suffix for sider state persistence. Produces key 'lf-{siderStorageKey}-open'."
170
+ },
171
+ header: {
172
+ type: 'object',
173
+ description: 'Header properties.',
174
+ additionalProperties: false,
175
+ properties: {
176
+ contentStyle: {
177
+ type: 'object',
178
+ description: 'Header content css style object.',
179
+ docs: {
180
+ displayType: 'yaml'
181
+ }
182
+ }
183
+ }
184
+ },
185
+ toggleSiderButton: {
186
+ type: 'object',
187
+ description: 'Toggle sider button properties.',
188
+ docs: {
189
+ displayType: 'button'
190
+ }
191
+ },
192
+ footer: {
193
+ type: 'object',
194
+ description: 'Footer properties.',
195
+ additionalProperties: false,
196
+ properties: {
197
+ style: {
198
+ type: 'object',
199
+ description: 'Footer css style object.',
200
+ docs: {
201
+ displayType: 'yaml'
202
+ }
203
+ }
204
+ }
205
+ },
206
+ content: {
207
+ type: 'object',
208
+ description: 'Content properties.',
209
+ additionalProperties: false,
210
+ properties: {
211
+ style: {
212
+ type: 'object',
213
+ description: 'Content css style object.',
214
+ docs: {
215
+ displayType: 'yaml'
216
+ }
217
+ }
218
+ }
219
+ },
220
+ breadcrumb: {
221
+ type: 'object',
222
+ description: 'Breadcrumb properties.',
223
+ properties: {
224
+ separator: {
225
+ type: 'string',
226
+ default: '/',
227
+ description: 'Use a custom separator string.'
228
+ },
229
+ list: breadcrumbList
230
+ }
231
+ },
232
+ menu: {
233
+ type: 'object',
234
+ description: 'Menu properties.',
235
+ properties: {
236
+ links: menuLinks
237
+ }
238
+ },
239
+ menuLg: {
240
+ type: 'object',
241
+ description: 'Menu large screen properties. Overwrites menu properties on desktop screen sizes.',
242
+ docs: {
243
+ displayType: 'yaml'
244
+ }
245
+ },
246
+ menuMd: {
247
+ type: 'object',
248
+ description: 'Mobile menu properties. Overwrites menu properties on mobile screen sizes.',
249
+ docs: {
250
+ displayType: 'yaml'
251
+ }
252
+ },
253
+ notifications: {
254
+ type: 'object',
255
+ description: 'Notification bell icon with badge. Shown in the sider on desktop and the mobile header on small screens. Renders when configured. Use the link property to navigate when clicked.',
256
+ additionalProperties: false,
257
+ properties: {
258
+ title: {
259
+ type: 'string',
260
+ default: 'Notifications',
261
+ description: 'Label shown next to the bell icon when the sider is expanded. Hidden on mobile header and collapsed sider.'
262
+ },
263
+ link: {
264
+ type: 'object',
265
+ description: 'Link to navigate to when the notification bell is clicked.',
266
+ additionalProperties: false,
267
+ properties: {
268
+ pageId: {
269
+ type: 'string',
270
+ description: 'Page to link to.'
271
+ },
272
+ url: {
273
+ type: 'string',
274
+ description: 'External URL to link to.'
275
+ },
276
+ newTab: {
277
+ type: 'boolean',
278
+ description: 'Open link in new tab.'
279
+ }
280
+ }
281
+ },
282
+ count: {
283
+ type: 'number',
284
+ description: 'Number to display on the badge. Set to 0 to hide the badge (unless showZero is true).'
285
+ },
286
+ dot: {
287
+ type: 'boolean',
288
+ default: false,
289
+ description: 'Show a dot instead of a count number.'
290
+ },
291
+ showZero: {
292
+ type: 'boolean',
293
+ default: false,
294
+ description: 'Show badge when count is zero.'
295
+ },
296
+ overflowCount: {
297
+ type: 'number',
298
+ default: 99,
299
+ description: 'Max count to show. Values above this display as "N+".'
300
+ },
301
+ color: {
302
+ type: 'string',
303
+ description: 'Badge color.',
304
+ docs: {
305
+ displayType: 'color'
306
+ }
307
+ },
308
+ icon: {
309
+ ...icon,
310
+ description: 'Icon for the notification button. Defaults to AiOutlineBell.'
311
+ },
312
+ size: {
313
+ type: 'string',
314
+ enum: [
315
+ 'small',
316
+ 'default',
317
+ 'large'
318
+ ],
319
+ default: 'small',
320
+ description: 'Size of the notification button.'
321
+ }
322
+ }
323
+ },
324
+ profile: {
325
+ type: 'object',
326
+ description: 'Profile avatar with optional dropdown menu. Shown in the sider on desktop and the mobile header on small screens. Renders when configured. Use with the _user operator to populate from the authenticated user.',
327
+ additionalProperties: false,
328
+ properties: {
329
+ title: {
330
+ type: 'string',
331
+ default: 'Profile',
332
+ description: 'Label shown next to the avatar when the sider is expanded. Hidden on mobile header and collapsed sider.'
333
+ },
334
+ avatar: {
335
+ type: 'object',
336
+ description: 'Avatar display properties.',
337
+ additionalProperties: false,
338
+ properties: {
339
+ src: {
340
+ type: 'string',
341
+ description: 'Image URL for the avatar. Typically bound to _user: image.'
342
+ },
343
+ content: {
344
+ type: 'string',
345
+ description: 'Text content inside the avatar (e.g. user initials). Shown when no src is provided.'
346
+ },
347
+ icon: {
348
+ ...icon,
349
+ description: 'Icon to display in avatar when no src or content is set. Defaults to AiOutlineUser.'
350
+ },
351
+ color: {
352
+ type: 'string',
353
+ description: 'Background color of the avatar when not using src.',
354
+ docs: {
355
+ displayType: 'color'
356
+ }
357
+ },
358
+ size: {
359
+ type: [
360
+ 'string',
361
+ 'number'
362
+ ],
363
+ default: 'small',
364
+ enum: [
365
+ 'default',
366
+ 'small',
367
+ 'large'
368
+ ],
369
+ description: 'Size of the avatar.',
370
+ docs: {
371
+ displayType: 'string'
372
+ }
373
+ },
374
+ shape: {
375
+ type: 'string',
376
+ enum: [
377
+ 'circle',
378
+ 'square'
379
+ ],
380
+ default: 'circle',
381
+ description: 'Shape of the avatar.'
382
+ }
383
+ }
384
+ },
385
+ links: {
386
+ type: 'array',
387
+ description: 'Dropdown menu items. Uses the same MenuLink/MenuGroup/MenuDivider schema as Menu. Compatible with _menu operator output for access-filtered menus.',
388
+ items: {
389
+ type: 'object',
390
+ required: [
391
+ 'id',
392
+ 'type'
393
+ ],
394
+ properties: {
395
+ id: {
396
+ type: 'string',
397
+ description: 'Menu item id.'
398
+ },
399
+ type: {
400
+ type: 'string',
401
+ enum: [
402
+ 'MenuDivider',
403
+ 'MenuLink',
404
+ 'MenuGroup'
405
+ ],
406
+ default: 'MenuLink',
407
+ description: 'Menu item type.'
408
+ },
409
+ pageId: {
410
+ type: 'string',
411
+ description: 'Page to link to.'
412
+ },
413
+ url: {
414
+ type: 'string',
415
+ description: 'External URL to link to.'
416
+ },
417
+ newTab: {
418
+ type: 'boolean',
419
+ description: 'Open link in new tab.'
420
+ },
421
+ style: {
422
+ type: 'object',
423
+ description: 'CSS style applied to the link.',
424
+ docs: {
425
+ displayType: 'yaml'
426
+ }
427
+ },
428
+ properties: {
429
+ type: 'object',
430
+ description: 'Properties for the menu item.',
431
+ properties: {
432
+ title: {
433
+ type: 'string',
434
+ description: 'Menu item title.'
435
+ },
436
+ icon: {
437
+ ...icon,
438
+ description: 'Icon for the menu item.'
439
+ },
440
+ danger: {
441
+ type: 'boolean',
442
+ default: false,
443
+ description: 'Apply danger style to menu item.'
444
+ },
445
+ disabled: {
446
+ type: 'boolean',
447
+ default: false,
448
+ description: 'Disable the menu item.'
449
+ },
450
+ dashed: {
451
+ type: 'boolean',
452
+ default: false,
453
+ description: 'Whether the divider line is dashed.'
454
+ },
455
+ shortcut: {
456
+ type: 'string',
457
+ description: 'Keyboard shortcut. Renders a shortcut badge next to the label. Use "mod" for Cmd/Ctrl.'
458
+ }
459
+ }
460
+ },
461
+ links: {
462
+ type: 'array',
463
+ description: 'Nested menu items for MenuGroup.'
464
+ }
465
+ }
466
+ }
467
+ },
468
+ trigger: {
469
+ type: 'string',
470
+ enum: [
471
+ 'click',
472
+ 'hover'
473
+ ],
474
+ default: 'hover',
475
+ description: 'How the profile dropdown opens.'
476
+ },
477
+ placement: {
478
+ type: 'string',
479
+ enum: [
480
+ 'bottomLeft',
481
+ 'bottom',
482
+ 'bottomRight',
483
+ 'topLeft',
484
+ 'top',
485
+ 'topRight'
486
+ ],
487
+ default: 'bottomRight',
488
+ description: 'Dropdown placement relative to the avatar.'
489
+ },
490
+ arrow: {
491
+ anyOf: [
492
+ {
493
+ type: 'boolean'
494
+ },
495
+ {
496
+ type: 'object',
497
+ properties: {
498
+ pointAtCenter: {
499
+ type: 'boolean'
500
+ }
501
+ }
502
+ }
503
+ ],
504
+ default: false,
505
+ description: 'Show arrow on the dropdown.',
506
+ docs: {
507
+ displayType: 'switch'
508
+ }
509
+ }
510
+ }
511
+ },
512
+ darkModeToggle: {
513
+ type: 'boolean',
514
+ default: false,
515
+ description: 'Show a dark mode toggle button in the sider and mobile header. Toggles the Ant Design dark theme for the entire page. Preference is persisted to localStorage.'
516
+ },
517
+ iconsColor: {
518
+ type: 'string',
519
+ description: 'Color for notification and dark mode toggle icons.',
520
+ docs: {
521
+ displayType: 'color'
522
+ }
523
+ }
524
+ }
525
+ }
526
+ };
@@ -26,21 +26,51 @@ import Menu from '../Menu/Menu.js';
26
26
  import MobileMenu from '../MobileMenu/MobileMenu.js';
27
27
  import Sider from '../Sider/Sider.js';
28
28
  import { getDarkMode, renderHeaderActions, registerDarkModeMethod } from '../headerActions.js';
29
+ function getInitialSiderState({ properties }) {
30
+ const storageKey = `lf-${properties.siderStorageKey ?? 'sider'}-open`;
31
+ try {
32
+ const stored = localStorage.getItem(storageKey);
33
+ if (stored === 'true') return true;
34
+ if (stored === 'false') return false;
35
+ } catch {
36
+ // localStorage unavailable (SSR, privacy mode)
37
+ }
38
+ return !properties.sider?.initialCollapsed;
39
+ }
40
+ function writeSiderState({ properties, open }) {
41
+ const storageKey = `lf-${properties.siderStorageKey ?? 'sider'}-open`;
42
+ try {
43
+ localStorage.setItem(storageKey, String(open));
44
+ } catch {
45
+ // localStorage unavailable
46
+ }
47
+ }
29
48
  const PageSiderMenu = ({ basePath, blockId, classNames = {}, components: { Icon, Link, ShortcutBadge }, events, content, menus, methods, pageId, properties, styles = {} })=>{
30
- const [openSiderState, setSiderOpen] = useState(true);
49
+ const [openSiderState, setSiderOpen] = useState(()=>getInitialSiderState({
50
+ properties
51
+ }));
31
52
  useEffect(()=>{
32
53
  registerDarkModeMethod(methods);
33
54
  methods.registerMethod('toggleSiderOpen', ()=>{
34
- methods._toggleSiderOpen({
35
- open: !openSiderState
55
+ const next = !openSiderState;
56
+ methods._setSiderOpen({
57
+ open: next
58
+ });
59
+ setSiderOpen(next);
60
+ writeSiderState({
61
+ properties,
62
+ open: next
36
63
  });
37
- setSiderOpen(!openSiderState);
38
64
  });
39
65
  methods.registerMethod('setSiderOpen', ({ open })=>{
40
- methods._toggleSiderOpen({
66
+ methods._setSiderOpen({
41
67
  open
42
68
  });
43
69
  setSiderOpen(open);
70
+ writeSiderState({
71
+ properties,
72
+ open
73
+ });
44
74
  });
45
75
  });
46
76
  return /*#__PURE__*/ React.createElement(Layout, {
@@ -180,7 +210,10 @@ const PageSiderMenu = ({ basePath, blockId, classNames = {}, components: { Icon,
180
210
  },
181
211
  events: events,
182
212
  methods: methods,
183
- properties: properties.sider ?? {},
213
+ properties: {
214
+ ...properties.sider ?? {},
215
+ initialCollapsed: !openSiderState
216
+ },
184
217
  classNames: {
185
218
  element: classNames.sider ?? 'hidden lg:block'
186
219
  },
@@ -136,6 +136,11 @@ export default {
136
136
  type: 'integer',
137
137
  description: 'Width of the collapsed sidebar, by setting to 0 a special trigger will appear.'
138
138
  },
139
+ initialCollapsed: {
140
+ type: 'boolean',
141
+ default: false,
142
+ description: 'Set the initial collapsed state. Used as the fallback when no persisted preference exists in localStorage.'
143
+ },
139
144
  reverseArrow: {
140
145
  type: 'boolean',
141
146
  default: false,
@@ -158,6 +163,11 @@ export default {
158
163
  }
159
164
  }
160
165
  },
166
+ siderStorageKey: {
167
+ type: 'string',
168
+ default: 'sider',
169
+ description: "localStorage key suffix for sider state persistence. Produces key 'lf-{siderStorageKey}-open'."
170
+ },
161
171
  toggleSiderButton: {
162
172
  type: 'object',
163
173
  description: 'Toggle sider button properties.',
@@ -52,10 +52,16 @@ const Selector = ({ blockId, classNames = {}, components: { Icon, Link }, events
52
52
  id: `${blockId}_input`,
53
53
  variant: properties.bordered === false ? 'borderless' : properties.variant,
54
54
  className: classNames.element,
55
+ classNames: {
56
+ content: classNames.selector
57
+ },
55
58
  style: {
56
59
  width: '100%',
57
60
  ...styles.element
58
61
  },
62
+ styles: {
63
+ content: styles.selector
64
+ },
59
65
  mode: "single",
60
66
  autoFocus: properties.autoFocus,
61
67
  getPopupContainer: ()=>document.getElementById(`${blockId}_${elementId}_popup`),
@@ -26,6 +26,7 @@ export default {
26
26
  valueType: 'any',
27
27
  cssKeys: {
28
28
  element: 'The Selector element.',
29
+ selector: 'The inner value/tag container of the Selector (antd `content` semantic slot).',
29
30
  clearIcon: 'The clear icon in the Selector.',
30
31
  label: 'The Selector label.',
31
32
  extra: 'The Selector extra content.',
@@ -16,6 +16,7 @@
16
16
  import { get } from '@lowdefy/helpers';
17
17
  import { Layout } from 'antd';
18
18
  import { withBlockDefaults } from '@lowdefy/block-utils';
19
+ import { getDarkMode } from '../headerActions.js';
19
20
  const Sider = Layout.Sider;
20
21
  const triggerSetOpen = async ({ state, setOpen, methods, rename })=>{
21
22
  if (!state) {
@@ -36,6 +37,14 @@ const triggerSetOpen = async ({ state, setOpen, methods, rename })=>{
36
37
  };
37
38
  const SiderBlock = ({ blockId, classNames = {}, properties, content, methods, rename, styles = {} })=>{
38
39
  const [openState, setOpen] = useState(!properties.initialCollapsed);
40
+ // Sync internal state when the parent (e.g. PageSidebarLayout) changes
41
+ // `initialCollapsed` after mount — typically because a hydration-time read
42
+ // from localStorage restored a value different from the SSR default.
43
+ useEffect(()=>{
44
+ setOpen(!properties.initialCollapsed);
45
+ }, [
46
+ properties.initialCollapsed
47
+ ]);
39
48
  useEffect(()=>{
40
49
  methods.registerMethod(get(rename, 'methods.toggleOpen', {
41
50
  default: 'toggleOpen'
@@ -62,6 +71,7 @@ const SiderBlock = ({ blockId, classNames = {}, properties, content, methods, re
62
71
  collapsedWidth: properties.collapsedWidth,
63
72
  collapsible: properties.collapsible,
64
73
  reverseArrow: properties.reverseArrow,
74
+ theme: properties.theme ?? (getDarkMode() ? 'dark' : 'light'),
65
75
  style: {
66
76
  overflow: 'auto',
67
77
  background: 'var(--ant-color-bg-container)',