@lowdefy/client 6.0.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 +21 -11
  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 +74 -6
  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 +239 -100
  51. package/package.json +11 -10
package/dist/Client.js CHANGED
@@ -13,14 +13,19 @@
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 { serializer } from '@lowdefy/helpers';
16
+ import { ConfigProvider } from 'antd';
17
+ import { getAppContext } from '@lowdefy/engine';
18
+ import { pageInstanceKey, serializer } from '@lowdefy/helpers';
17
19
  import Block from './block/Block.js';
18
20
  import Context from './Context.js';
19
21
  import DisplayMessage from './DisplayMessage.js';
20
22
  import Head from './Head.js';
23
+ import IconProvider from './IconProvider.js';
21
24
  import ProgressBarController from './ProgressBarController.js';
25
+ import createAntdIconConfig from './createAntdIconConfig.js';
26
+ import getMessageIcon from './getMessageIcon.js';
22
27
  import initLowdefyContext from './initLowdefyContext.js';
23
- const Client = ({ auth, Components, config: rawConfig, jsMap, lowdefy, resetContext = {
28
+ const Client = ({ auth, Components, config: rawConfig, jsMap, loadAllIcons, lowdefy, matchedPath, pathParams, resetContext = {
24
29
  reset: false,
25
30
  setReset: ()=>undefined
26
31
  }, router, stage, types, window })=>{
@@ -32,10 +37,17 @@ const Client = ({ auth, Components, config: rawConfig, jsMap, lowdefy, resetCont
32
37
  rawConfig.pageConfig,
33
38
  rawConfig.rootConfig
34
39
  ]);
35
- // Dynamic pages rebuild their engine context per fetch — remount the page
36
- // tree with the rebuilt context instead of reconciling mounted blocks
37
- // against it, which React can silently drop. Static pages keep their
38
- // context and tree across navigations, so their key stays stable.
40
+ // Each page instance (one per set of path values) has its own engine
41
+ // context, so a new instance remounts the page tree instead of reconciling
42
+ // the previous instance's blocks against it. Dynamic pages also rebuild their
43
+ // context per fetch, which React can silently drop when reconciling, so their
44
+ // key changes with every fetch. Static page instances keep their context and
45
+ // tree across navigations, so their key stays stable.
46
+ const instanceKey = pageInstanceKey({
47
+ pageId: config.pageConfig.pageId,
48
+ path: config.pageConfig.path,
49
+ pathParams
50
+ });
39
51
  const buildRef = React.useRef({
40
52
  config: null,
41
53
  build: 0
@@ -46,18 +58,35 @@ const Client = ({ auth, Components, config: rawConfig, jsMap, lowdefy, resetCont
46
58
  build: buildRef.current.build + 1
47
59
  };
48
60
  }
49
- const contextKey = config.pageConfig.dynamic === true ? `${config.pageConfig.id}:${buildRef.current.build}` : config.pageConfig.id;
61
+ const contextKey = config.pageConfig.dynamic === true ? `${instanceKey}:${buildRef.current.build}` : instanceKey;
50
62
  initLowdefyContext({
51
63
  auth,
52
64
  Components,
53
65
  config,
66
+ loadAllIcons,
54
67
  lowdefy,
68
+ matchedPath,
69
+ pathParams,
55
70
  router,
56
71
  stage,
57
72
  types,
58
73
  window
59
74
  });
60
- return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(ProgressBarController, {
75
+ const appContext = getAppContext({
76
+ events: config.rootConfig.events,
77
+ jsMap,
78
+ lowdefy
79
+ });
80
+ const Icon = lowdefy._internal.components.Icon;
81
+ const antdIconConfig = React.useMemo(()=>createAntdIconConfig({
82
+ Icon
83
+ }), [
84
+ Icon
85
+ ]);
86
+ return(// theme.icons is read at render: createIcon runs before the theme is set.
87
+ /*#__PURE__*/ React.createElement(IconProvider, {
88
+ icons: lowdefy.theme.icons
89
+ }, /*#__PURE__*/ React.createElement(ProgressBarController, {
61
90
  id: "lowdefy-progress-bar",
62
91
  key: `${config.pageConfig.id}-progress-bar`,
63
92
  lowdefy: lowdefy,
@@ -69,27 +98,35 @@ const Client = ({ auth, Components, config: rawConfig, jsMap, lowdefy, resetCont
69
98
  components: lowdefy._internal.components,
70
99
  methods: {
71
100
  registerMethod: (_, method)=>{
72
- lowdefy._internal.displayMessage = method;
101
+ lowdefy._internal.displayMessage = (args)=>method({
102
+ ...args,
103
+ icon: args.icon ?? getMessageIcon({
104
+ status: args.status ?? 'success'
105
+ })
106
+ });
73
107
  }
74
108
  }
75
- }), /*#__PURE__*/ React.createElement(Context, {
109
+ }), /*#__PURE__*/ React.createElement(ConfigProvider, antdIconConfig, /*#__PURE__*/ React.createElement(Context, {
76
110
  key: contextKey,
111
+ appContext: appContext,
77
112
  config: config.pageConfig,
78
113
  jsMap: jsMap,
79
114
  lowdefy: lowdefy,
115
+ pathParams: pathParams,
80
116
  resetContext: resetContext
81
117
  }, (context)=>{
82
118
  if (!context._internal.onInitDone) return '';
83
- return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(Head, {
84
- Component: Components.Head,
85
- properties: context._internal.RootSlots.map[config.pageConfig.blockId].eval.properties
86
- }), /*#__PURE__*/ React.createElement(Block, {
119
+ return /*#__PURE__*/ React.createElement(Block, {
87
120
  block: context._internal.RootSlots.map[config.pageConfig.blockId],
88
121
  Blocks: context._internal.RootSlots,
89
122
  context: context,
90
123
  lowdefy: lowdefy,
91
- parentLoading: false
92
- }));
93
- }));
124
+ parentLoading: false,
125
+ renderHead: ({ properties })=>/*#__PURE__*/ React.createElement(Head, {
126
+ Component: Components.Head,
127
+ properties: properties
128
+ })
129
+ });
130
+ }))));
94
131
  };
95
132
  export default Client;
package/dist/Context.js CHANGED
@@ -14,7 +14,9 @@
14
14
  limitations under the License.
15
15
  */ import React, { useEffect } from 'react';
16
16
  import getContext from '@lowdefy/engine';
17
+ import createPageLifecycleManager from './createPageLifecycleManager.js';
17
18
  import createShortcutManager from './createShortcutManager.js';
19
+ import getContextKey from './getContextKey.js';
18
20
  import MountEvents from './MountEvents.js';
19
21
  const ShortcutEffect = ({ context })=>{
20
22
  useEffect(()=>{
@@ -26,6 +28,16 @@ const ShortcutEffect = ({ context })=>{
26
28
  ]);
27
29
  return null;
28
30
  };
31
+ const PageLifecycleEffect = ({ context })=>{
32
+ useEffect(()=>{
33
+ const manager = createPageLifecycleManager();
34
+ manager.init(context);
35
+ return ()=>manager.destroy();
36
+ }, [
37
+ context
38
+ ]);
39
+ return null;
40
+ };
29
41
  const WebSocketsEffect = ({ context })=>{
30
42
  useEffect(()=>{
31
43
  context._internal.WebSockets.subscribeAll();
@@ -35,33 +47,39 @@ const WebSocketsEffect = ({ context })=>{
35
47
  ]);
36
48
  return null;
37
49
  };
38
- const Context = ({ children, config, jsMap, lowdefy, resetContext })=>{
50
+ const Context = ({ appContext, children, config, jsMap, lowdefy, pathParams, resetContext })=>{
39
51
  const context = getContext({
40
52
  config,
41
53
  jsMap,
42
54
  lowdefy,
55
+ pathParams,
43
56
  resetContext
44
57
  });
58
+ const progress = ()=>{
59
+ lowdefy._internal.progress.dispatch({
60
+ type: 'increment'
61
+ });
62
+ };
45
63
  return /*#__PURE__*/ React.createElement(MountEvents, {
64
+ key: getContextKey(context),
46
65
  context: context,
47
66
  triggerEvent: async ()=>{
48
- await context._internal.runOnInit(()=>{
49
- lowdefy._internal.progress.dispatch({
50
- type: 'increment'
51
- });
52
- });
67
+ // The app events run once per app load, when the first page mounts. The
68
+ // page onInit waits for the app onInit, but not for the app onInitAsync.
69
+ await appContext._internal.runOnInit(progress);
70
+ appContext._internal.runOnInitAsync(progress);
71
+ await context._internal.runOnInit(progress);
53
72
  },
54
73
  triggerEventAsync: ()=>{
55
- context._internal.runOnInitAsync(()=>{
56
- lowdefy._internal.progress.dispatch({
57
- type: 'increment'
58
- });
59
- });
60
- }
74
+ context._internal.runOnInitAsync(progress);
75
+ },
76
+ waitForMount: true
61
77
  }, (loadingOnInit)=>{
62
78
  if (loadingOnInit) return '';
63
79
  return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(ShortcutEffect, {
64
80
  context: context
81
+ }), /*#__PURE__*/ React.createElement(PageLifecycleEffect, {
82
+ context: context
65
83
  }), /*#__PURE__*/ React.createElement(WebSocketsEffect, {
66
84
  context: context
67
85
  }), children(context));
@@ -0,0 +1,127 @@
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, { forwardRef, useEffect, useId, useImperativeHandle, useRef } from 'react';
16
+ import { Popconfirm, Popover, Tooltip } from 'antd';
17
+ // The target lives in HTML React does not own, so antd cannot wrap it. antd's
18
+ // trigger resolves its anchor from the child's ref, so a child that renders
19
+ // nothing and hands back the target element anchors the popup on the target
20
+ // itself, and the trigger follows its scrolling and resizing.
21
+ const TargetAnchor = /*#__PURE__*/ forwardRef(function TargetAnchor({ target }, ref) {
22
+ useImperativeHandle(ref, ()=>target, [
23
+ target
24
+ ]);
25
+ return null;
26
+ });
27
+ // Open popovers and confirms, newest last. Escape and outside clicks close only
28
+ // the newest, so a confirm opened inside a popover closes before the popover,
29
+ // and a click in the confirm is not an outside click for the popover.
30
+ const dismissable = [];
31
+ function isTop(entry) {
32
+ return dismissable[dismissable.length - 1] === entry;
33
+ }
34
+ function ConfirmOverlay({ content, Icon, onClose, onConfirm, popupClassName, target }) {
35
+ const messageId = `${useId().replace(/[^a-zA-Z0-9_-]/g, '')}-message`;
36
+ const cancelRef = useRef(null);
37
+ return /*#__PURE__*/ React.createElement(Popconfirm, {
38
+ afterOpenChange: (open)=>{
39
+ // The least destructive action takes focus (WAI-ARIA alert dialog),
40
+ // once the popup is placed so focusing it cannot scroll the page.
41
+ if (open) cancelRef.current?.focus();
42
+ },
43
+ cancelButtonProps: {
44
+ 'aria-describedby': messageId,
45
+ ref: cancelRef
46
+ },
47
+ icon: /*#__PURE__*/ React.createElement(Icon, {
48
+ properties: {
49
+ name: 'warning',
50
+ title: ''
51
+ }
52
+ }),
53
+ okButtonProps: {
54
+ 'aria-describedby': messageId
55
+ },
56
+ onCancel: ()=>onClose('cancel'),
57
+ onConfirm: onConfirm,
58
+ open: true,
59
+ placement: "top",
60
+ rootClassName: popupClassName,
61
+ title: /*#__PURE__*/ React.createElement("span", {
62
+ id: messageId
63
+ }, content),
64
+ trigger: []
65
+ }, /*#__PURE__*/ React.createElement(TargetAnchor, {
66
+ target: target
67
+ }));
68
+ }
69
+ function HtmlOverlay({ content, Icon, kind, onClose, onConfirm, target }) {
70
+ const popupClassName = `lowdefy-html-${kind}-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
71
+ useEffect(()=>{
72
+ // Tooltips close on Escape but take no clicks, so they are not dismissable.
73
+ const entry = kind === 'tooltip' ? null : {};
74
+ if (entry) dismissable.push(entry);
75
+ // A click on the trigger toggles the overlay itself; one inside the popup
76
+ // is inside the overlay.
77
+ function onPointerDown(event) {
78
+ if (!entry || !isTop(entry)) return;
79
+ if (target.contains(event.target)) return;
80
+ if (event.target.closest?.(`.${popupClassName}`)) return;
81
+ onClose('outside');
82
+ }
83
+ function onKeyDown(event) {
84
+ if (event.key !== 'Escape' || event.isComposing) return;
85
+ // Every tooltip closes on Escape; of popovers and confirms, the newest.
86
+ if (!entry || isTop(entry)) onClose('escape');
87
+ }
88
+ document.addEventListener('mousedown', onPointerDown);
89
+ document.addEventListener('keydown', onKeyDown);
90
+ return ()=>{
91
+ if (entry) dismissable.splice(dismissable.indexOf(entry), 1);
92
+ document.removeEventListener('mousedown', onPointerDown);
93
+ document.removeEventListener('keydown', onKeyDown);
94
+ };
95
+ }, [
96
+ kind,
97
+ onClose,
98
+ popupClassName,
99
+ target
100
+ ]);
101
+ if (kind === 'confirm') {
102
+ return /*#__PURE__*/ React.createElement(ConfirmOverlay, {
103
+ content: content,
104
+ Icon: Icon,
105
+ onClose: onClose,
106
+ onConfirm: onConfirm,
107
+ popupClassName: popupClassName,
108
+ target: target
109
+ });
110
+ }
111
+ const Overlay = kind === 'popover' ? Popover : Tooltip;
112
+ const contentProps = kind === 'popover' ? {
113
+ content
114
+ } : {
115
+ title: content
116
+ };
117
+ return /*#__PURE__*/ React.createElement(Overlay, {
118
+ ...contentProps,
119
+ open: true,
120
+ placement: "top",
121
+ rootClassName: popupClassName,
122
+ trigger: []
123
+ }, /*#__PURE__*/ React.createElement(TargetAnchor, {
124
+ target: target
125
+ }));
126
+ }
127
+ export default HtmlOverlay;
@@ -0,0 +1,24 @@
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 { LucideProvider } from 'lucide-react/dist/esm/context.mjs';
17
+ function IconProvider({ children, icons }) {
18
+ return /*#__PURE__*/ React.createElement(LucideProvider, {
19
+ nonScalingStroke: icons.nonScalingStroke,
20
+ size: icons.size,
21
+ strokeWidth: icons.strokeWidth
22
+ }, children);
23
+ }
24
+ export default IconProvider;
@@ -13,11 +13,11 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React, { useEffect, useState } from 'react';
16
- const MountEvents = ({ children, context, triggerEvent, triggerEventAsync })=>{
17
- const [loading, setLoading] = useState(true);
16
+ const MountEvents = ({ children, context, triggerEvent, triggerEventAsync, waitForMount })=>{
17
+ const [loading, setLoading] = useState(waitForMount);
18
18
  const [error, setError] = useState(null);
19
19
  useEffect(()=>{
20
- setLoading(true);
20
+ setLoading(waitForMount);
21
21
  const mount = async ()=>{
22
22
  try {
23
23
  await triggerEvent();
@@ -12,10 +12,13 @@
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 { createUrl, parsePageId } from './url.js';
15
+ */ import { parsePageId } from '@lowdefy/helpers';
16
+ import { createUrl } from './url.js';
16
17
  // History-API router backing the @lowdefy/client router contract:
17
18
  // push({ pathname, query }), back(), basePath — plus subscribe() for the
18
- // page component and sessionStorage-backed scroll restoration.
19
+ // page component and sessionStorage-backed scroll restoration. A location names
20
+ // no page: the client learns a path's page from the server's page response, and
21
+ // reads it back from the path memory.
19
22
  function createRouter({ basePath = '', window }) {
20
23
  const listeners = new Set();
21
24
  let entryKey = window.history.state?.lowdefyKey ?? `k${Date.now().toString(36)}`;
@@ -52,9 +55,12 @@ function createRouter({ basePath = '', window }) {
52
55
  window.scrollTo(position?.x ?? 0, position?.y ?? 0);
53
56
  });
54
57
  }
58
+ // path is the pathname without basePath, the leading "/" and one trailing "/",
59
+ // still encoded: the form page requests and the path memory use. It is ''
60
+ // at the app root.
55
61
  function getLocation() {
56
62
  return {
57
- pageId: parsePageId(window.location.href, basePath),
63
+ path: parsePageId(window.location.href, basePath) ?? '',
58
64
  pathname: window.location.pathname,
59
65
  search: window.location.search
60
66
  };
@@ -26,10 +26,4 @@ function createUrl({ basePath = '', pathname, query }) {
26
26
  const serializedQuery = serializeQuery(query);
27
27
  return `${basePath}${pathname}${serializedQuery ? `?${serializedQuery}` : ''}`;
28
28
  }
29
- function parsePageId(url, basePath = '') {
30
- const pathname = new URL(url, 'http://localhost').pathname;
31
- const stripped = basePath && pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
32
- const pageId = stripped.replace(/^\//, '').replace(/\/$/, '');
33
- return pageId === '' ? null : pageId;
34
- }
35
- export { createUrl, parsePageId, serializeQuery };
29
+ export { createUrl, serializeQuery };