@lowdefy/client 6.1.0 → 7.0.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 (51) hide show
  1. package/dist/Client.js +54 -17
  2. package/dist/Context.js +30 -12
  3. package/dist/HtmlOverlay.js +127 -0
  4. package/dist/IconProvider.js +24 -0
  5. package/dist/MountEvents.js +3 -3
  6. package/dist/adapters/createRouter.js +9 -3
  7. package/dist/adapters/url.js +1 -7
  8. package/dist/auth/createAuthMethods.js +1034 -41
  9. package/dist/block/Block.js +6 -3
  10. package/dist/block/CategorySwitch.js +6 -0
  11. package/dist/block/Container.js +3 -0
  12. package/dist/block/InputContainer.js +8 -2
  13. package/dist/block/List.js +3 -0
  14. package/dist/block/LoadingBlock.js +6 -12
  15. package/dist/block/LoadingContainer.js +6 -1
  16. package/dist/block/LoadingList.js +6 -1
  17. package/dist/block/loadingBlockMethods.js +26 -0
  18. package/dist/claimCallerReload.js +38 -0
  19. package/dist/createAntdIconConfig.js +91 -0
  20. package/dist/createCallAPI.js +6 -2
  21. package/dist/createCallRequest.js +6 -2
  22. package/dist/createHandleError.js +6 -4
  23. package/dist/createIcon.js +138 -36
  24. package/dist/createPageLifecycleManager.js +172 -0
  25. package/dist/formatIconTitle.js +26 -0
  26. package/dist/getActiveLocale.js +27 -0
  27. package/dist/getContextKey.js +29 -0
  28. package/dist/getMessageIcon.js +43 -0
  29. package/dist/hasOtherRoles.js +30 -0
  30. package/dist/index.js +1 -0
  31. package/dist/initLowdefyContext.js +103 -4
  32. package/dist/isOtherCaller.js +29 -0
  33. package/dist/recordCallerStamp.js +31 -0
  34. package/dist/renderIconNodes.js +46 -0
  35. package/dist/request.js +72 -5
  36. package/dist/setLowdefyUser.js +32 -0
  37. package/dist/setupLink.js +19 -13
  38. package/dist/shouldReloadForAuthError.js +39 -0
  39. package/dist/shouldReloadForBuild.js +44 -0
  40. package/dist/shouldReloadForCaller.js +50 -0
  41. package/dist/storage/clearUserStorage.js +36 -0
  42. package/dist/storage/clearUserStorageOnOwnerChange.js +41 -0
  43. package/dist/storage/createStorage.js +48 -0
  44. package/dist/storage/listUserStorageKeys.js +36 -0
  45. package/dist/storage/readStorageItem.js +31 -0
  46. package/dist/storage/userStorageKey.js +24 -0
  47. package/dist/storage/userStoragePrefix.js +25 -0
  48. package/dist/useDarkMode.js +17 -4
  49. package/dist/useLocale.js +18 -5
  50. package/dist/websocket/createWebSocketClient.js +234 -98
  51. package/package.json +11 -10
@@ -16,7 +16,7 @@
16
16
  import { ErrorBoundary } from '@lowdefy/block-utils';
17
17
  import CategorySwitch from './CategorySwitch.js';
18
18
  import MountEvents from '../MountEvents.js';
19
- const Block = ({ block, Blocks, context, lowdefy, parentLoading })=>{
19
+ const Block = ({ block, Blocks, context, lowdefy, parentLoading, renderHead })=>{
20
20
  const [updates, setUpdate] = useState(0);
21
21
  const loggedErrorsRef = useRef(new Set());
22
22
  context._internal.updaters[block.id] = ()=>setUpdate(updates + 1);
@@ -48,7 +48,9 @@ const Block = ({ block, Blocks, context, lowdefy, parentLoading })=>{
48
48
  configKey: block.eval?.configKey,
49
49
  onError: handleError,
50
50
  properties: block.eval?.properties
51
- }, /*#__PURE__*/ React.createElement(MountEvents, {
51
+ }, renderHead?.({
52
+ properties: block.eval.properties
53
+ }), /*#__PURE__*/ React.createElement(MountEvents, {
52
54
  context: context,
53
55
  triggerEvent: async ()=>{
54
56
  context._internal.lowdefy._internal.progress.dispatch({
@@ -76,7 +78,8 @@ const Block = ({ block, Blocks, context, lowdefy, parentLoading })=>{
76
78
  lowdefy._internal.progress.dispatch({
77
79
  type: 'done'
78
80
  });
79
- }
81
+ },
82
+ waitForMount: Object.hasOwn(block.Events.events, 'onMount')
80
83
  }, (eventLoading)=>/*#__PURE__*/ React.createElement(CategorySwitch, {
81
84
  block: block,
82
85
  Blocks: Blocks,
@@ -69,7 +69,9 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
69
69
  layout: block.eval.layout
70
70
  }, /*#__PURE__*/ React.createElement(Component, {
71
71
  methods: Object.assign(block.methods, {
72
+ getAppStorageKey: lowdefy._internal.storage.appKey,
72
73
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
74
+ getUserStorageKey: lowdefy._internal.storage.userKey,
73
75
  registerEvent: block.registerEvent,
74
76
  registerMethod: block.registerMethod,
75
77
  setValue: block.setValue,
@@ -85,6 +87,7 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
85
87
  loading: loading,
86
88
  menus: lowdefy.menus,
87
89
  pageId: lowdefy.pageId,
90
+ pathParams: context.pathParams,
88
91
  properties: block.eval.properties,
89
92
  required: block.eval.required,
90
93
  styles: block.eval.style ?? {},
@@ -108,7 +111,9 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
108
111
  layout: block.eval.layout
109
112
  }, /*#__PURE__*/ React.createElement(Component, {
110
113
  methods: Object.assign(block.methods, {
114
+ getAppStorageKey: lowdefy._internal.storage.appKey,
111
115
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
116
+ getUserStorageKey: lowdefy._internal.storage.userKey,
112
117
  registerEvent: block.registerEvent,
113
118
  registerMethod: block.registerMethod,
114
119
  translate: lowdefy._internal.translate,
@@ -123,6 +128,7 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
123
128
  loading: loading,
124
129
  menus: lowdefy.menus,
125
130
  pageId: lowdefy.pageId,
131
+ pathParams: context.pathParams,
126
132
  properties: block.eval.properties,
127
133
  required: block.eval.required,
128
134
  styles: block.eval.style ?? {},
@@ -51,7 +51,9 @@ const Container = ({ block, Blocks, Component, context, loading, lowdefy })=>{
51
51
  layout: block.eval.layout
52
52
  }, /*#__PURE__*/ React.createElement(Component, {
53
53
  methods: Object.assign(block.methods, {
54
+ getAppStorageKey: lowdefy._internal.storage.appKey,
54
55
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
56
+ getUserStorageKey: lowdefy._internal.storage.userKey,
55
57
  registerEvent: block.registerEvent,
56
58
  registerMethod: block.registerMethod,
57
59
  translate: lowdefy._internal.translate,
@@ -67,6 +69,7 @@ const Container = ({ block, Blocks, Component, context, loading, lowdefy })=>{
67
69
  loading: loading,
68
70
  menus: lowdefy.menus,
69
71
  pageId: lowdefy.pageId,
72
+ pathParams: context.pathParams,
70
73
  properties: block.eval.properties,
71
74
  required: block.eval.required,
72
75
  styles: block.eval.style ?? {},
@@ -24,7 +24,10 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
24
24
  const slots = Blocks.subSlots[block.id][0].slots;
25
25
  Object.keys(slots).forEach((slotKey, i)=>{
26
26
  if (slots[slotKey].blocks.length === 0) return;
27
- content[slotKey] = (contentStyle)=>/*#__PURE__*/ React.createElement(Area, {
27
+ // A block may pass the loading its slot's blocks inherit (`{ loading }`), for slots that are
28
+ // its chrome rather than its content: the Table keeps its toolbar and bulk action blocks
29
+ // usable while its own rows load. By default they inherit the block's loading.
30
+ content[slotKey] = (contentStyle, { loading: slotLoading = loading } = {})=>/*#__PURE__*/ React.createElement(Area, {
28
31
  area: block.eval.slots[slotKey],
29
32
  areaKey: slotKey,
30
33
  style: {
@@ -41,7 +44,7 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
41
44
  context: context,
42
45
  key: `co-${bl.blockId}-${k}`,
43
46
  lowdefy: lowdefy,
44
- parentLoading: loading
47
+ parentLoading: slotLoading
45
48
  })));
46
49
  });
47
50
  return /*#__PURE__*/ React.createElement(BlockLayout, {
@@ -51,7 +54,9 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
51
54
  layout: block.eval.layout
52
55
  }, /*#__PURE__*/ React.createElement(Component, {
53
56
  methods: Object.assign(block.methods, {
57
+ getAppStorageKey: lowdefy._internal.storage.appKey,
54
58
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
59
+ getUserStorageKey: lowdefy._internal.storage.userKey,
55
60
  registerEvent: block.registerEvent,
56
61
  registerMethod: block.registerMethod,
57
62
  translate: lowdefy._internal.translate,
@@ -73,6 +78,7 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
73
78
  loading: loading,
74
79
  menus: lowdefy.menus,
75
80
  pageId: lowdefy.pageId,
81
+ pathParams: context.pathParams,
76
82
  properties: block.eval.properties,
77
83
  required: block.eval.required,
78
84
  styles: block.eval.style ?? {},
@@ -56,7 +56,9 @@ const List = ({ block, Blocks, Component, context, loading, lowdefy })=>{
56
56
  layout: block.eval.layout
57
57
  }, /*#__PURE__*/ React.createElement(Component, {
58
58
  methods: Object.assign(block.methods, {
59
+ getAppStorageKey: lowdefy._internal.storage.appKey,
59
60
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
61
+ getUserStorageKey: lowdefy._internal.storage.userKey,
60
62
  moveItemDown: block.moveItemDown,
61
63
  moveItemUp: block.moveItemUp,
62
64
  pushItem: block.pushItem,
@@ -78,6 +80,7 @@ const List = ({ block, Blocks, Component, context, loading, lowdefy })=>{
78
80
  loading: loading,
79
81
  menus: lowdefy.menus,
80
82
  pageId: lowdefy.pageId,
83
+ pathParams: context.pathParams,
81
84
  properties: block.eval.properties,
82
85
  required: block.eval.required,
83
86
  styles: block.eval.style ?? {},
@@ -16,17 +16,7 @@
16
16
  import { BlockLayout } from '@lowdefy/layout';
17
17
  import LoadingContainer from './LoadingContainer.js';
18
18
  import LoadingList from './LoadingList.js';
19
- const blockMethods = {
20
- moveItemDown: ()=>{},
21
- moveItemUp: ()=>{},
22
- pushItem: ()=>{},
23
- registerEvent: ()=>{},
24
- registerMethod: ()=>{},
25
- removeItem: ()=>{},
26
- setValue: ()=>{},
27
- triggerEvent: ()=>{},
28
- unshiftItem: ()=>{}
29
- };
19
+ import loadingBlockMethods from './loadingBlockMethods.js';
30
20
  const LoadingBlock = ({ blockClass, blockId, blockLayout, blockProperties, blockStyle, context, lowdefy, skeleton })=>{
31
21
  let Component = lowdefy._internal.blockComponents[skeleton.type];
32
22
  useEffect(()=>{
@@ -74,7 +64,11 @@ const LoadingBlock = ({ blockClass, blockId, blockLayout, blockProperties, block
74
64
  components: lowdefy._internal.components,
75
65
  key: `s-${blockId}-${skeleton.id}`,
76
66
  menus: lowdefy.menus,
77
- methods: blockMethods,
67
+ methods: {
68
+ ...loadingBlockMethods,
69
+ getAppStorageKey: lowdefy._internal.storage.appKey,
70
+ getUserStorageKey: lowdefy._internal.storage.userKey
71
+ },
78
72
  pageId: lowdefy.pageId,
79
73
  properties: skeleton.properties ?? blockProperties
80
74
  }));
@@ -15,6 +15,7 @@
15
15
  */ import React from 'react';
16
16
  import { Area, BlockLayout } from '@lowdefy/layout';
17
17
  import LoadingBlock from './LoadingBlock.js';
18
+ import loadingBlockMethods from './loadingBlockMethods.js';
18
19
  const LoadingContainer = ({ blockClass, blockId, blockLayout, blockProperties, blockStyle, Component, context, lowdefy, skeleton })=>{
19
20
  const content = {};
20
21
  // eslint-disable-next-line prefer-destructuring
@@ -46,7 +47,11 @@ const LoadingContainer = ({ blockClass, blockId, blockLayout, blockProperties, b
46
47
  content: content,
47
48
  key: skeleton.id,
48
49
  menus: lowdefy.menus,
49
- methods: {},
50
+ methods: {
51
+ ...loadingBlockMethods,
52
+ getAppStorageKey: lowdefy._internal.storage.appKey,
53
+ getUserStorageKey: lowdefy._internal.storage.userKey
54
+ },
50
55
  pageId: lowdefy.pageId,
51
56
  properties: skeleton.properties ?? blockProperties
52
57
  }));
@@ -15,6 +15,7 @@
15
15
  */ import React from 'react';
16
16
  import { Area, BlockLayout } from '@lowdefy/layout';
17
17
  import LoadingBlock from './LoadingBlock.js';
18
+ import loadingBlockMethods from './loadingBlockMethods.js';
18
19
  const LoadingList = ({ blockClass, blockId, blockLayout, blockProperties, blockStyle, Component, context, lowdefy, skeleton })=>{
19
20
  const content = {};
20
21
  const contentList = [];
@@ -50,7 +51,11 @@ const LoadingList = ({ blockClass, blockId, blockLayout, blockProperties, blockS
50
51
  components: lowdefy._internal.components,
51
52
  list: contentList,
52
53
  menus: lowdefy.menus,
53
- methods: {},
54
+ methods: {
55
+ ...loadingBlockMethods,
56
+ getAppStorageKey: lowdefy._internal.storage.appKey,
57
+ getUserStorageKey: lowdefy._internal.storage.userKey
58
+ },
54
59
  pageId: lowdefy.pageId,
55
60
  properties: skeleton.properties ?? blockProperties
56
61
  }));
@@ -0,0 +1,26 @@
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
+ */ const loadingBlockMethods = {
16
+ moveItemDown: ()=>{},
17
+ moveItemUp: ()=>{},
18
+ pushItem: ()=>{},
19
+ registerEvent: ()=>{},
20
+ registerMethod: ()=>{},
21
+ removeItem: ()=>{},
22
+ setValue: ()=>{},
23
+ triggerEvent: ()=>{},
24
+ unshiftItem: ()=>{}
25
+ };
26
+ export default loadingBlockMethods;
@@ -0,0 +1,38 @@
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
+ */ // sessionStorage key holding when this tab last reloaded to follow its caller.
16
+ const RELOADED_FOR_CALLER_KEY = 'lowdefy.reloadedForCaller';
17
+ const RELOAD_INTERVAL_MS = 10000;
18
+ // True when this tab may reload because its caller changed (another person or
19
+ // organization, or the session ended), and records the reload. A tab reloads
20
+ // at most once per 10 seconds for its caller: if the freshly loaded page still
21
+ // disagrees with the server, reloading again would loop. Callers fail the call
22
+ // instead, so no data shows for the wrong caller either way.
23
+ function claimCallerReload({ window }) {
24
+ const now = Date.now();
25
+ try {
26
+ const last = Number(window.sessionStorage.getItem(RELOADED_FOR_CALLER_KEY));
27
+ if (Math.abs(now - last) < RELOAD_INTERVAL_MS) {
28
+ return false;
29
+ }
30
+ window.sessionStorage.setItem(RELOADED_FOR_CALLER_KEY, String(now));
31
+ } catch (error) {
32
+ // Browsers with storage disabled throw on sessionStorage access. Without
33
+ // the record the loop guard cannot work, so do not reload.
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ export default claimCallerReload;
@@ -0,0 +1,91 @@
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 React from 'react';
16
+ import { ConfigProvider } from 'antd';
17
+ // Icon keys for a ConfigProvider around the page tree, so antd's own chrome
18
+ // draws with the app's icon set. Tag is left out: a context closeIcon makes
19
+ // every Tag closable (antd's useClosable), so the Tag block passes its own.
20
+ function createAntdIconConfig({ Icon }) {
21
+ function chromeIcon(name, properties) {
22
+ return /*#__PURE__*/ React.createElement(Icon, {
23
+ properties: {
24
+ name,
25
+ title: '',
26
+ ...properties
27
+ }
28
+ });
29
+ }
30
+ // antd renders a function menu.expandIcon as a component, so it is created
31
+ // once here to keep its identity, and it adds the expand-icon class itself.
32
+ function MenuExpandIcon({ isOpen }) {
33
+ const { getPrefixCls } = React.useContext(ConfigProvider.ConfigContext);
34
+ return /*#__PURE__*/ React.createElement(Icon, {
35
+ className: `${getPrefixCls('menu')}-submenu-expand-icon`,
36
+ properties: {
37
+ name: 'chevron-down',
38
+ rotate: isOpen ? 180 : 0,
39
+ title: ''
40
+ }
41
+ });
42
+ }
43
+ const closeIcon = chromeIcon('close');
44
+ return {
45
+ alert: {
46
+ closeIcon,
47
+ errorIcon: chromeIcon('error'),
48
+ infoIcon: chromeIcon('info'),
49
+ successIcon: chromeIcon('success'),
50
+ warningIcon: chromeIcon('warning')
51
+ },
52
+ breadcrumb: {
53
+ dropdownIcon: chromeIcon('chevron-down')
54
+ },
55
+ button: {
56
+ loadingIcon: chromeIcon('loading', {
57
+ spin: true
58
+ })
59
+ },
60
+ collapse: {
61
+ expandIcon: ({ isActive })=>chromeIcon('chevron-right', {
62
+ rotate: isActive ? 90 : 0
63
+ })
64
+ },
65
+ datePicker: {
66
+ suffixIcon: chromeIcon('calendar')
67
+ },
68
+ drawer: {
69
+ closeIcon
70
+ },
71
+ menu: {
72
+ expandIcon: MenuExpandIcon
73
+ },
74
+ modal: {
75
+ closeIcon
76
+ },
77
+ tabs: {
78
+ moreIcon: chromeIcon('more')
79
+ },
80
+ timePicker: {
81
+ suffixIcon: chromeIcon('clock')
82
+ },
83
+ tour: {
84
+ closeIcon
85
+ },
86
+ treeSelect: {
87
+ switcherIcon: chromeIcon('chevron-down')
88
+ }
89
+ };
90
+ }
91
+ export default createAntdIconConfig;
@@ -13,16 +13,20 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import request from './request.js';
16
- function createCallAPI({ basePath }) {
16
+ function createCallAPI(lowdefy) {
17
+ const { basePath, buildId } = lowdefy;
17
18
  function callAPI({ payload, endpointId, pageId, blockId }) {
18
19
  return request({
20
+ buildId,
19
21
  url: `${basePath}/api/endpoints/${endpointId}`,
20
22
  method: 'POST',
21
23
  body: {
22
24
  payload,
23
25
  pageId,
24
26
  blockId
25
- }
27
+ },
28
+ reloadOnAuthorizationError: false,
29
+ lowdefy
26
30
  });
27
31
  }
28
32
  return callAPI;
@@ -13,16 +13,20 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import request from './request.js';
16
- function createCallRequest({ basePath }) {
16
+ function createCallRequest(lowdefy) {
17
+ const { basePath, buildId } = lowdefy;
17
18
  function callRequest({ actionId, blockId, pageId, payload, requestId }) {
18
19
  return request({
20
+ buildId,
19
21
  url: `${basePath}/api/request/${pageId}/${requestId}`,
20
22
  method: 'POST',
21
23
  body: {
22
24
  actionId,
23
25
  blockId,
24
26
  payload
25
- }
27
+ },
28
+ reloadOnAuthorizationError: true,
29
+ lowdefy
26
30
  });
27
31
  }
28
32
  return callRequest;
@@ -13,7 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { getDevError } from '@lowdefy/engine';
16
- import { UserError } from '@lowdefy/errors';
16
+ import { expectedErrorNames } from '@lowdefy/errors';
17
17
  import { serializer } from '@lowdefy/helpers';
18
18
  function createHandleError(lowdefy) {
19
19
  const loggedErrors = new Set();
@@ -36,9 +36,11 @@ function createHandleError(lowdefy) {
36
36
  return;
37
37
  }
38
38
  loggedErrors.add(errorKey);
39
- // UserError is client-only — log to browser console, never send to server or
40
- // the error bar
41
- if (error instanceof UserError) {
39
+ // An expected outcome - a UserError, or an auth gate's refusal the server
40
+ // already logged as one warning line - logs to the browser console only,
41
+ // never sent to the server or the error bar. Matched by name, not
42
+ // instanceof: plugins bundle their own @lowdefy/errors copy.
43
+ if (expectedErrorNames.has(error?.name)) {
42
44
  logger.error(shown);
43
45
  return;
44
46
  }
@@ -15,7 +15,11 @@
15
15
  */ import React from 'react';
16
16
  import { omit, type } from '@lowdefy/helpers';
17
17
  import Icon from '@ant-design/icons';
18
+ import LucideIcon from 'lucide-react/dist/esm/Icon.mjs';
19
+ import { useLucideContext } from 'lucide-react/dist/esm/context.mjs';
18
20
  import { cn, withBlockDefaults, ErrorBoundary } from '@lowdefy/block-utils';
21
+ import formatIconTitle from './formatIconTitle.js';
22
+ import renderIconNodes from './renderIconNodes.js';
19
23
  import iconStyles from './style.module.css';
20
24
  const lowdefyProps = [
21
25
  'actionLog',
@@ -27,65 +31,163 @@ const lowdefyProps = [
27
31
  'loading',
28
32
  'menus',
29
33
  'pageId',
34
+ 'pathParams',
30
35
  'registerEvent',
31
36
  'registerMethod',
32
37
  'schemaErrors',
33
38
  'styles',
34
39
  'validation'
35
40
  ];
36
- const createIcon = (Icons)=>{
37
- const AiOutlineLoading3Quarters = Icons['AiOutlineLoading3Quarters'];
38
- const AiOutlineExclamationCircle = Icons['AiOutlineExclamationCircle'];
39
- const formatTitle = (title)=>{
40
- if (!title || !type.isString(title)) {
41
- return '';
41
+ // Drawn while every icon loads: an empty svg keeps the icon's box, so nothing
42
+ // shifts and no fallback flashes before the icon arrives.
43
+ const PENDING_ICON = {
44
+ node: []
45
+ };
46
+ function createIcon({ icons, loadAllIcons }) {
47
+ // Without a loader the map already holds every icon the app bundles.
48
+ let allIconsLoaded = type.isNone(loadAllIcons);
49
+ // icons is the live icon map: page loads, dev JIT and loadAllIcons add names
50
+ // to it, so every lookup happens at render.
51
+ function getIconData(name) {
52
+ if (type.isString(name) && Object.hasOwn(icons, name)) {
53
+ return icons[name];
42
54
  }
43
- let spacedTitle = title.replace(/([A-Z])/g, ' $1').trim();
44
- return spacedTitle.substring(spacedTitle.indexOf(' ') + 1);
45
- };
46
- const IconBlock = ({ blockId, classNames = {}, events, methods, onClick, properties, styles = {}, ...props })=>{
55
+ return null;
56
+ }
57
+ // A name missing from the page's icons may be one of the app's, so the first
58
+ // miss waits for every icon to load before it draws the fallback. After a
59
+ // load, a missing name is unknown. A failed load (logged by the loader)
60
+ // draws the fallback, and a later miss tries again.
61
+ function useIconData(name) {
62
+ const data = getIconData(name);
63
+ const [settledName, setSettledName] = React.useState(null);
64
+ const pending = data === null && type.isString(name) && !allIconsLoaded && settledName !== name;
65
+ React.useEffect(()=>{
66
+ if (!pending) return undefined;
67
+ let mounted = true;
68
+ loadAllIcons().then(()=>{
69
+ allIconsLoaded = true;
70
+ }, ()=>undefined).then(()=>{
71
+ if (mounted) setSettledName(name);
72
+ });
73
+ return ()=>{
74
+ mounted = false;
75
+ };
76
+ }, [
77
+ pending,
78
+ name
79
+ ]);
80
+ return {
81
+ data,
82
+ pending
83
+ };
84
+ }
85
+ function IconSvg({ data, nonScalingStroke, svgProps, title }) {
86
+ return /*#__PURE__*/ React.createElement(LucideIcon, {
87
+ icon: {
88
+ node: [],
89
+ size: data.size,
90
+ width: data.width,
91
+ height: data.height
92
+ },
93
+ ...data.attrs,
94
+ ...svgProps
95
+ }, [
96
+ title ? /*#__PURE__*/ React.createElement("title", {
97
+ key: "title"
98
+ }, title) : null,
99
+ ...renderIconNodes({
100
+ node: data.node,
101
+ nonScalingStroke
102
+ })
103
+ ]);
104
+ }
105
+ function IconBlock({ blockId, classNames = {}, events, methods, onClick, properties, styles = {}, ...props }) {
106
+ const lucideContext = useLucideContext();
47
107
  const propertiesObj = type.isString(properties) ? {
48
108
  name: properties
49
109
  } : properties;
110
+ const { data, pending } = useIconData(propertiesObj.name);
50
111
  const spin = (propertiesObj.spin || events.onClick?.loading) && !propertiesObj.disableLoadingIcon;
51
- const iconProps = {
112
+ const title = propertiesObj.title ?? formatIconTitle(propertiesObj.name);
113
+ const labelled = Boolean(title) || !type.isNone(props['aria-label']);
114
+ const nonScalingStroke = propertiesObj.nonScalingStroke ?? lucideContext.nonScalingStroke;
115
+ const triggerClick = events.onClick && (()=>methods.triggerEvent({
116
+ name: 'onClick'
117
+ }));
118
+ const svgProps = {
52
119
  id: blockId,
53
120
  className: cn(classNames.element, {
54
121
  [iconStyles['icon-spin']]: spin
55
122
  }),
56
123
  style: {
57
124
  cursor: onClick || events.onClick ? 'pointer' : undefined,
125
+ // CSS colour and transform, not SVG attributes: every set draws with
126
+ // currentColor, so one colour reaches stroke- and fill-based icons.
127
+ color: propertiesObj.color,
128
+ transform: propertiesObj.rotate ? `rotate(${propertiesObj.rotate}deg)` : undefined,
58
129
  ...styles.element
59
130
  },
60
- rotate: propertiesObj.rotate,
61
- color: propertiesObj.color,
62
- title: propertiesObj.title ?? formatTitle(propertiesObj.name),
63
131
  size: propertiesObj.size,
64
- // twoToneColor: propertiesObj.color, // TODO: track https://github.com/react-icons/react-icons/issues/508
132
+ strokeWidth: propertiesObj.strokeWidth,
133
+ 'aria-hidden': labelled ? undefined : 'true',
134
+ onClick: onClick ?? triggerClick,
65
135
  ...omit(props, lowdefyProps)
66
136
  };
67
- let IconComp = Icons[propertiesObj.name];
68
- if (!IconComp) {
69
- IconComp = AiOutlineExclamationCircle;
137
+ const missingSvgProps = {
138
+ ...svgProps,
139
+ style: {
140
+ ...svgProps.style,
141
+ color: '#F00'
142
+ }
143
+ };
144
+ const missingIcon = /*#__PURE__*/ React.createElement(IconSvg, {
145
+ data: getIconData('icon-missing'),
146
+ nonScalingStroke: nonScalingStroke,
147
+ svgProps: missingSvgProps,
148
+ title: title
149
+ });
150
+ if (spin) {
151
+ return /*#__PURE__*/ React.createElement(IconSvg, {
152
+ data: getIconData('loading'),
153
+ nonScalingStroke: nonScalingStroke,
154
+ svgProps: svgProps,
155
+ title: title
156
+ });
70
157
  }
71
- return /*#__PURE__*/ React.createElement(React.Fragment, null, spin ? /*#__PURE__*/ React.createElement(AiOutlineLoading3Quarters, iconProps) : /*#__PURE__*/ React.createElement(ErrorBoundary, {
72
- fallback: ()=>/*#__PURE__*/ React.createElement(AiOutlineExclamationCircle, {
73
- ...iconProps,
74
- color: '#F00'
75
- })
76
- }, /*#__PURE__*/ React.createElement(IconComp, {
77
- id: blockId,
78
- onClick: onClick || events.onClick && (()=>methods.triggerEvent({
79
- name: 'onClick'
80
- })),
81
- size: propertiesObj.size,
82
- title: propertiesObj.title,
83
- ...iconProps
158
+ if (pending) {
159
+ return /*#__PURE__*/ React.createElement(IconSvg, {
160
+ data: PENDING_ICON,
161
+ nonScalingStroke: nonScalingStroke,
162
+ svgProps: svgProps,
163
+ title: title
164
+ });
165
+ }
166
+ if (!data) {
167
+ return missingIcon;
168
+ }
169
+ return(// Keyed so an icon that failed recovers when its name changes.
170
+ /*#__PURE__*/ React.createElement(ErrorBoundary, {
171
+ key: propertiesObj.name,
172
+ fallback: ()=>missingIcon
173
+ }, /*#__PURE__*/ React.createElement(IconSvg, {
174
+ data: data,
175
+ nonScalingStroke: nonScalingStroke,
176
+ svgProps: svgProps,
177
+ title: title
84
178
  })));
85
- };
86
- const AntIcon = (all)=>/*#__PURE__*/ React.createElement(Icon, {
87
- component: ()=>/*#__PURE__*/ React.createElement(IconBlock, all)
88
- });
179
+ }
180
+ // antd's Icon renders `component` with React.createElement, so the component
181
+ // must keep its identity across renders or React remounts the <svg> every
182
+ // time. The per-render props travel through a render-function child instead.
183
+ const IconHost = ({ children })=>children();
184
+ // antd components add their classes (collapse arrow, tree switcher, menu
185
+ // expand icon) to the icon element they are given; their CSS expects them on
186
+ // the .anticon span, as on antd's own icons.
187
+ const AntIcon = ({ className, ...all })=>/*#__PURE__*/ React.createElement(Icon, {
188
+ className: className,
189
+ component: IconHost
190
+ }, ()=>/*#__PURE__*/ React.createElement(IconBlock, all));
89
191
  return withBlockDefaults(AntIcon);
90
- };
192
+ }
91
193
  export default createIcon;