@lowdefy/client 4.7.3 → 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.
- package/dist/Client.js +4 -3
- package/dist/Context.js +15 -2
- package/dist/DisplayMessage.js +2 -3
- package/dist/ProgressBarController.js +1 -4
- package/dist/block/CategorySwitch.js +18 -13
- package/dist/block/Container.js +24 -21
- package/dist/block/InputContainer.js +24 -21
- package/dist/block/List.js +23 -20
- package/dist/block/LoadingBlock.js +7 -6
- package/dist/block/LoadingContainer.js +13 -20
- package/dist/block/LoadingList.js +13 -20
- package/dist/{style.less → block/resolveClassNames.js} +9 -14
- package/dist/createHandleError.js +11 -5
- package/dist/createIcon.js +11 -13
- package/dist/createShortcutBadge.js +113 -0
- package/dist/createShortcutManager.js +135 -0
- package/dist/index.js +1 -0
- package/dist/initLowdefyContext.js +6 -1
- package/dist/style.module.css +70 -0
- package/dist/useDarkMode.js +147 -0
- package/package.json +16 -16
|
@@ -17,6 +17,12 @@ import { serializer } from '@lowdefy/helpers';
|
|
|
17
17
|
function createHandleError(lowdefy) {
|
|
18
18
|
const loggedErrors = new Set();
|
|
19
19
|
const logger = lowdefy._internal.logger;
|
|
20
|
+
function logError(error) {
|
|
21
|
+
if (!(error instanceof UserError)) {
|
|
22
|
+
lowdefy._runtimeErrorCallback?.(error);
|
|
23
|
+
}
|
|
24
|
+
logger.error(error);
|
|
25
|
+
}
|
|
20
26
|
return async function handleError(error) {
|
|
21
27
|
const errorKey = `${error.message}:${error.configKey || ''}`;
|
|
22
28
|
if (loggedErrors.has(errorKey)) {
|
|
@@ -25,14 +31,14 @@ function createHandleError(lowdefy) {
|
|
|
25
31
|
loggedErrors.add(errorKey);
|
|
26
32
|
// UserError is client-only — log to browser console, never send to server
|
|
27
33
|
if (error instanceof UserError) {
|
|
28
|
-
|
|
34
|
+
logError(error);
|
|
29
35
|
return;
|
|
30
36
|
}
|
|
31
37
|
// Send known error types to server for logging with location resolution
|
|
32
38
|
if (error.isLowdefyError) {
|
|
33
39
|
// Server-originated errors already have source resolved — just display locally
|
|
34
40
|
if (error.source) {
|
|
35
|
-
|
|
41
|
+
logError(error);
|
|
36
42
|
return;
|
|
37
43
|
}
|
|
38
44
|
// Client-originated errors — send to server for logging + location resolution
|
|
@@ -54,18 +60,18 @@ function createHandleError(lowdefy) {
|
|
|
54
60
|
// If server produced a consolidated ConfigError, log it and return early
|
|
55
61
|
// (cause chain includes original error)
|
|
56
62
|
if (serializedConfigError) {
|
|
57
|
-
|
|
63
|
+
logError(serializer.deserialize(serializedConfigError));
|
|
58
64
|
return;
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
} catch {
|
|
62
68
|
// Server logging failed - continue with local console
|
|
63
69
|
}
|
|
64
|
-
|
|
70
|
+
logError(error);
|
|
65
71
|
return;
|
|
66
72
|
}
|
|
67
73
|
// Other errors - just log locally
|
|
68
|
-
|
|
74
|
+
logError(error);
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
77
|
export default createHandleError;
|
package/dist/createIcon.js
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
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 classNames from 'classnames';
|
|
17
16
|
import { omit, type } from '@lowdefy/helpers';
|
|
18
17
|
import Icon from '@ant-design/icons';
|
|
19
|
-
import {
|
|
18
|
+
import { cn, withBlockDefaults, ErrorBoundary } from '@lowdefy/block-utils';
|
|
19
|
+
import iconStyles from './style.module.css';
|
|
20
20
|
const lowdefyProps = [
|
|
21
21
|
'actionLog',
|
|
22
22
|
'basePath',
|
|
@@ -30,6 +30,7 @@ const lowdefyProps = [
|
|
|
30
30
|
'registerEvent',
|
|
31
31
|
'registerMethod',
|
|
32
32
|
'schemaErrors',
|
|
33
|
+
'styles',
|
|
33
34
|
'validation'
|
|
34
35
|
];
|
|
35
36
|
const createIcon = (Icons)=>{
|
|
@@ -42,22 +43,20 @@ const createIcon = (Icons)=>{
|
|
|
42
43
|
let spacedTitle = title.replace(/([A-Z])/g, ' $1').trim();
|
|
43
44
|
return spacedTitle.substring(spacedTitle.indexOf(' ') + 1);
|
|
44
45
|
};
|
|
45
|
-
const IconBlock = ({ blockId, events, methods, onClick, properties, ...props })=>{
|
|
46
|
+
const IconBlock = ({ blockId, classNames = {}, events, methods, onClick, properties, styles = {}, ...props })=>{
|
|
46
47
|
const propertiesObj = type.isString(properties) ? {
|
|
47
48
|
name: properties
|
|
48
49
|
} : properties;
|
|
49
50
|
const spin = (propertiesObj.spin || events.onClick?.loading) && !propertiesObj.disableLoadingIcon;
|
|
50
51
|
const iconProps = {
|
|
51
52
|
id: blockId,
|
|
52
|
-
className: classNames
|
|
53
|
-
[
|
|
54
|
-
{
|
|
55
|
-
cursor: (onClick || events.onClick) && 'pointer'
|
|
56
|
-
},
|
|
57
|
-
propertiesObj.style
|
|
58
|
-
])]: true,
|
|
59
|
-
'icon-spin': spin
|
|
53
|
+
className: cn(classNames.element, {
|
|
54
|
+
[iconStyles['icon-spin']]: spin
|
|
60
55
|
}),
|
|
56
|
+
style: {
|
|
57
|
+
cursor: onClick || events.onClick ? 'pointer' : undefined,
|
|
58
|
+
...styles.element
|
|
59
|
+
},
|
|
61
60
|
rotate: propertiesObj.rotate,
|
|
62
61
|
color: propertiesObj.color,
|
|
63
62
|
title: propertiesObj.title ?? formatTitle(propertiesObj.name),
|
|
@@ -87,7 +86,6 @@ const createIcon = (Icons)=>{
|
|
|
87
86
|
const AntIcon = (all)=>/*#__PURE__*/ React.createElement(Icon, {
|
|
88
87
|
component: ()=>/*#__PURE__*/ React.createElement(IconBlock, all)
|
|
89
88
|
});
|
|
90
|
-
AntIcon
|
|
91
|
-
return AntIcon;
|
|
89
|
+
return withBlockDefaults(AntIcon);
|
|
92
90
|
};
|
|
93
91
|
export default createIcon;
|
|
@@ -0,0 +1,113 @@
|
|
|
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 styles from './style.module.css';
|
|
17
|
+
const SPECIAL_KEY_DISPLAY = {
|
|
18
|
+
Escape: 'Esc',
|
|
19
|
+
Enter: '↵',
|
|
20
|
+
Backspace: '⌫',
|
|
21
|
+
Delete: '⌦',
|
|
22
|
+
ArrowUp: '↑',
|
|
23
|
+
ArrowDown: '↓',
|
|
24
|
+
ArrowLeft: '←',
|
|
25
|
+
ArrowRight: '→',
|
|
26
|
+
Tab: '⇥',
|
|
27
|
+
Space: '␣'
|
|
28
|
+
};
|
|
29
|
+
const MAC_MODIFIERS = {
|
|
30
|
+
mod: '⌘',
|
|
31
|
+
shift: '⇧',
|
|
32
|
+
alt: '⌥',
|
|
33
|
+
ctrl: '⌃'
|
|
34
|
+
};
|
|
35
|
+
const WIN_MODIFIERS = {
|
|
36
|
+
mod: 'Ctrl',
|
|
37
|
+
shift: 'Shift',
|
|
38
|
+
alt: 'Alt',
|
|
39
|
+
ctrl: 'Ctrl'
|
|
40
|
+
};
|
|
41
|
+
let isMacCached = null;
|
|
42
|
+
function isMac() {
|
|
43
|
+
if (isMacCached !== null) return isMacCached;
|
|
44
|
+
if (typeof navigator !== 'undefined') {
|
|
45
|
+
isMacCached = navigator.userAgentData?.platform === 'macOS' || /Mac|iPhone|iPad|iPod/.test(navigator.platform ?? '');
|
|
46
|
+
} else {
|
|
47
|
+
isMacCached = false;
|
|
48
|
+
}
|
|
49
|
+
return isMacCached;
|
|
50
|
+
}
|
|
51
|
+
function parseShortcut(shortcut) {
|
|
52
|
+
// Sequence: "g i" → ["G", "then", "I"]
|
|
53
|
+
if (shortcut.includes(' ') && !shortcut.includes('+')) {
|
|
54
|
+
const parts = shortcut.split(' ');
|
|
55
|
+
const result = [];
|
|
56
|
+
parts.forEach((part, i)=>{
|
|
57
|
+
if (i > 0) result.push('then');
|
|
58
|
+
result.push(displayKey(part));
|
|
59
|
+
});
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
if (shortcut.includes(' ')) {
|
|
63
|
+
// Sequence with modifiers: "mod+G mod+I"
|
|
64
|
+
const parts = shortcut.split(' ');
|
|
65
|
+
const result = [];
|
|
66
|
+
parts.forEach((part, i)=>{
|
|
67
|
+
if (i > 0) result.push('then');
|
|
68
|
+
result.push(...parsePart(part));
|
|
69
|
+
});
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
return parsePart(shortcut);
|
|
73
|
+
}
|
|
74
|
+
function parsePart(part) {
|
|
75
|
+
const segments = part.split('+');
|
|
76
|
+
if (segments.length === 1) {
|
|
77
|
+
return [
|
|
78
|
+
displayKey(segments[0])
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
const key = segments[segments.length - 1];
|
|
82
|
+
const modifiers = segments.slice(0, -1);
|
|
83
|
+
const modMap = isMac() ? MAC_MODIFIERS : WIN_MODIFIERS;
|
|
84
|
+
const result = modifiers.map((mod)=>modMap[mod.toLowerCase()] || mod);
|
|
85
|
+
result.push(displayKey(key));
|
|
86
|
+
return [
|
|
87
|
+
result.join('\u2009')
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
function displayKey(key) {
|
|
91
|
+
if (SPECIAL_KEY_DISPLAY[key]) return SPECIAL_KEY_DISPLAY[key];
|
|
92
|
+
if (key.length === 1) return key.toUpperCase();
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
function createShortcutBadge() {
|
|
96
|
+
function ShortcutBadge({ shortcut }) {
|
|
97
|
+
if (!shortcut) return null;
|
|
98
|
+
const primary = Array.isArray(shortcut) ? shortcut[0] : shortcut;
|
|
99
|
+
if (!primary) return null;
|
|
100
|
+
const segments = parseShortcut(primary);
|
|
101
|
+
return /*#__PURE__*/ React.createElement("span", {
|
|
102
|
+
className: styles['shortcut-badge']
|
|
103
|
+
}, segments.map((segment, i)=>segment === 'then' ? /*#__PURE__*/ React.createElement("span", {
|
|
104
|
+
key: i,
|
|
105
|
+
className: styles['shortcut-then']
|
|
106
|
+
}, "then") : /*#__PURE__*/ React.createElement("kbd", {
|
|
107
|
+
key: i,
|
|
108
|
+
className: styles['shortcut-kbd']
|
|
109
|
+
}, segment)));
|
|
110
|
+
}
|
|
111
|
+
return ShortcutBadge;
|
|
112
|
+
}
|
|
113
|
+
export default createShortcutBadge;
|
|
@@ -0,0 +1,135 @@
|
|
|
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 { tinykeys } from 'tinykeys';
|
|
16
|
+
const SPECIAL_KEYS = new Set([
|
|
17
|
+
'Escape',
|
|
18
|
+
'Enter',
|
|
19
|
+
'Backspace',
|
|
20
|
+
'Delete',
|
|
21
|
+
'ArrowUp',
|
|
22
|
+
'ArrowDown',
|
|
23
|
+
'ArrowLeft',
|
|
24
|
+
'ArrowRight',
|
|
25
|
+
'Tab',
|
|
26
|
+
'Space'
|
|
27
|
+
]);
|
|
28
|
+
const MODIFIER_MAP = {
|
|
29
|
+
mod: '$mod',
|
|
30
|
+
alt: 'Alt',
|
|
31
|
+
shift: 'Shift',
|
|
32
|
+
ctrl: 'Control'
|
|
33
|
+
};
|
|
34
|
+
const MODIFIER_VALUES = new Set([
|
|
35
|
+
'$mod',
|
|
36
|
+
'Alt',
|
|
37
|
+
'Shift',
|
|
38
|
+
'Control'
|
|
39
|
+
]);
|
|
40
|
+
function normalizeShortcut(shortcut) {
|
|
41
|
+
// Sequences are space-separated parts like "g i"
|
|
42
|
+
if (shortcut.includes(' ') && !shortcut.includes('+')) {
|
|
43
|
+
return shortcut.split(' ').map((part)=>normalizePart(part)).join(' ');
|
|
44
|
+
}
|
|
45
|
+
// Check for sequences that have modifiers: "mod+G mod+I" is invalid in tinykeys,
|
|
46
|
+
// but "g i" (no modifiers) is a sequence. Handle mixed case: split on space.
|
|
47
|
+
if (shortcut.includes(' ')) {
|
|
48
|
+
return shortcut.split(' ').map((part)=>normalizePart(part)).join(' ');
|
|
49
|
+
}
|
|
50
|
+
return normalizePart(shortcut);
|
|
51
|
+
}
|
|
52
|
+
function normalizePart(part) {
|
|
53
|
+
const segments = part.split('+');
|
|
54
|
+
if (segments.length === 1) {
|
|
55
|
+
// Single key, no modifiers
|
|
56
|
+
return normalizeKey(segments[0]);
|
|
57
|
+
}
|
|
58
|
+
const key = segments[segments.length - 1];
|
|
59
|
+
const modifiers = segments.slice(0, -1);
|
|
60
|
+
const normalizedModifiers = modifiers.map((mod)=>MODIFIER_MAP[mod.toLowerCase()] || mod);
|
|
61
|
+
return [
|
|
62
|
+
...normalizedModifiers,
|
|
63
|
+
normalizeKey(key)
|
|
64
|
+
].join('+');
|
|
65
|
+
}
|
|
66
|
+
function normalizeKey(key) {
|
|
67
|
+
if (SPECIAL_KEYS.has(key)) return key;
|
|
68
|
+
if (key.length === 1) return key.toLowerCase();
|
|
69
|
+
return key;
|
|
70
|
+
}
|
|
71
|
+
function hasModifier(normalizedShortcut) {
|
|
72
|
+
// Check the first part (for sequences, only first part matters for suppression)
|
|
73
|
+
const firstPart = normalizedShortcut.split(' ')[0];
|
|
74
|
+
return firstPart.split('+').some((segment)=>MODIFIER_VALUES.has(segment));
|
|
75
|
+
}
|
|
76
|
+
function collectShortcuts(context) {
|
|
77
|
+
const entries = [];
|
|
78
|
+
const blockMap = context._internal.RootSlots.map;
|
|
79
|
+
Object.values(blockMap).forEach((block)=>{
|
|
80
|
+
if (!block.Events || !block.Events.events) return;
|
|
81
|
+
Object.entries(block.Events.events).forEach(([eventName, event])=>{
|
|
82
|
+
if (!event.shortcut) return;
|
|
83
|
+
const shortcuts = Array.isArray(event.shortcut) ? event.shortcut : [
|
|
84
|
+
event.shortcut
|
|
85
|
+
];
|
|
86
|
+
shortcuts.forEach((s)=>entries.push({
|
|
87
|
+
block,
|
|
88
|
+
eventName,
|
|
89
|
+
shortcut: s
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
95
|
+
function createShortcutManager() {
|
|
96
|
+
let unsubscribe = null;
|
|
97
|
+
function init(context) {
|
|
98
|
+
const entries = collectShortcuts(context);
|
|
99
|
+
if (entries.length === 0) return;
|
|
100
|
+
const shortcutMap = {};
|
|
101
|
+
// Last entry wins for duplicates
|
|
102
|
+
entries.forEach(({ block, eventName, shortcut })=>{
|
|
103
|
+
const normalized = normalizeShortcut(shortcut);
|
|
104
|
+
const modded = hasModifier(normalized);
|
|
105
|
+
shortcutMap[normalized] = (event)=>{
|
|
106
|
+
// Visibility gating
|
|
107
|
+
if (block.visibleEval && block.visibleEval.output === false) return;
|
|
108
|
+
// Input field suppression for non-modifier shortcuts
|
|
109
|
+
if (!modded) {
|
|
110
|
+
const tag = event.target?.tagName;
|
|
111
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || event.target?.isContentEditable) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
block.triggerEvent({
|
|
117
|
+
name: eventName
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const win = context._internal.lowdefy._internal.globals.window;
|
|
122
|
+
unsubscribe = tinykeys(win, shortcutMap);
|
|
123
|
+
}
|
|
124
|
+
function destroy() {
|
|
125
|
+
if (unsubscribe) {
|
|
126
|
+
unsubscribe();
|
|
127
|
+
unsubscribe = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
init,
|
|
132
|
+
destroy
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export default createShortcutManager;
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import createAuthMethods from './auth/createAuthMethods.js';
|
|
17
17
|
import createCallRequest from './createCallRequest.js';
|
|
18
18
|
import createIcon from './createIcon.js';
|
|
19
|
+
import createShortcutBadge from './createShortcutBadge.js';
|
|
19
20
|
import createLinkComponent from './createLinkComponent.js';
|
|
20
21
|
import createHandleError from './createHandleError.js';
|
|
21
22
|
import { createBrowserLogger } from '@lowdefy/logger/browser';
|
|
@@ -25,8 +26,10 @@ function initLowdefyContext({ auth, Components, config, lowdefy, router, stage,
|
|
|
25
26
|
lowdefy._internal = {
|
|
26
27
|
actions: types.actions,
|
|
27
28
|
blockComponents: types.blocks,
|
|
29
|
+
blockMetas: types.blockMetas ?? {},
|
|
28
30
|
components: {
|
|
29
|
-
Icon: createIcon(types.icons)
|
|
31
|
+
Icon: createIcon(types.icons),
|
|
32
|
+
ShortcutBadge: createShortcutBadge()
|
|
30
33
|
},
|
|
31
34
|
displayMessage: ({ content })=>{
|
|
32
35
|
console.log(content);
|
|
@@ -54,6 +57,7 @@ function initLowdefyContext({ auth, Components, config, lowdefy, router, stage,
|
|
|
54
57
|
lowdefy.contexts = {};
|
|
55
58
|
lowdefy.inputs = {};
|
|
56
59
|
lowdefy.lowdefyGlobal = config.rootConfig.lowdefyGlobal;
|
|
60
|
+
lowdefy.theme = config.rootConfig.theme ?? {};
|
|
57
61
|
lowdefy._internal.callAPI = createCallAPI(lowdefy);
|
|
58
62
|
lowdefy._internal.auth = createAuthMethods(lowdefy, auth);
|
|
59
63
|
lowdefy._internal.callRequest = createCallRequest(lowdefy);
|
|
@@ -62,6 +66,7 @@ function initLowdefyContext({ auth, Components, config, lowdefy, router, stage,
|
|
|
62
66
|
lowdefy._internal.updateBlock = (blockId)=>lowdefy._internal.updaters[blockId] && lowdefy._internal.updaters[blockId]();
|
|
63
67
|
lowdefy._internal.logger = createBrowserLogger();
|
|
64
68
|
lowdefy._internal.handleError = createHandleError(lowdefy);
|
|
69
|
+
lowdefy._internal.components.handleError = lowdefy._internal.handleError;
|
|
65
70
|
if (stage === 'dev' || stage === 'e2e') {
|
|
66
71
|
window.lowdefy = lowdefy;
|
|
67
72
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
*/
|
|
16
|
+
|
|
17
|
+
@layer components {
|
|
18
|
+
.icon-spin {
|
|
19
|
+
animation: spin 1s infinite linear;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes spin {
|
|
23
|
+
0% {
|
|
24
|
+
transform: rotate(0deg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
100% {
|
|
28
|
+
transform: rotate(360deg);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.shortcut-badge {
|
|
33
|
+
display: inline-flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 2px;
|
|
36
|
+
margin-left: var(--ant-margin-xs);
|
|
37
|
+
font-size: calc(var(--ant-font-size-sm) - 1px);
|
|
38
|
+
line-height: 1;
|
|
39
|
+
vertical-align: middle;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.shortcut-kbd {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
padding: 2px 5px;
|
|
45
|
+
font-size: calc(var(--ant-font-size-sm) - 1px);
|
|
46
|
+
font-family:
|
|
47
|
+
system-ui,
|
|
48
|
+
-apple-system,
|
|
49
|
+
sans-serif;
|
|
50
|
+
background-color: var(--ant-color-fill-quaternary);
|
|
51
|
+
border-radius: var(--ant-border-radius-sm);
|
|
52
|
+
border: 1px solid var(--ant-color-border);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.shortcut-kbd + .shortcut-kbd {
|
|
56
|
+
border-left: none;
|
|
57
|
+
border-top-left-radius: 0;
|
|
58
|
+
border-bottom-left-radius: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.shortcut-kbd:has(+ .shortcut-kbd) {
|
|
62
|
+
border-top-right-radius: 0;
|
|
63
|
+
border-bottom-right-radius: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.shortcut-then {
|
|
67
|
+
margin: 0 2px;
|
|
68
|
+
font-size: calc(var(--ant-font-size-sm) - 2px);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { useCallback, useEffect, useMemo, useState } from 'react';
|
|
16
|
+
import { theme as antdTheme } from 'antd';
|
|
17
|
+
const algorithmMap = {
|
|
18
|
+
default: antdTheme.defaultAlgorithm,
|
|
19
|
+
dark: antdTheme.darkAlgorithm,
|
|
20
|
+
compact: antdTheme.compactAlgorithm
|
|
21
|
+
};
|
|
22
|
+
function resolveAlgorithm(algorithm) {
|
|
23
|
+
if (Array.isArray(algorithm)) {
|
|
24
|
+
return algorithm.map((a)=>algorithmMap[a] || antdTheme.defaultAlgorithm);
|
|
25
|
+
}
|
|
26
|
+
return algorithmMap[algorithm] || antdTheme.defaultAlgorithm;
|
|
27
|
+
}
|
|
28
|
+
function stripDarkFromAlgorithm(algorithm) {
|
|
29
|
+
if (Array.isArray(algorithm)) {
|
|
30
|
+
const filtered = algorithm.filter((a)=>a !== 'dark');
|
|
31
|
+
return filtered.length > 0 ? filtered : 'default';
|
|
32
|
+
}
|
|
33
|
+
if (algorithm === 'dark') return 'default';
|
|
34
|
+
return algorithm;
|
|
35
|
+
}
|
|
36
|
+
function mergeAlgorithm(baseAlgorithm, isDark) {
|
|
37
|
+
if (!isDark) return baseAlgorithm;
|
|
38
|
+
const base = Array.isArray(baseAlgorithm) ? baseAlgorithm : baseAlgorithm ? [
|
|
39
|
+
baseAlgorithm
|
|
40
|
+
] : [];
|
|
41
|
+
return [
|
|
42
|
+
...base,
|
|
43
|
+
'dark'
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function resolveIsDark({ configDarkMode, userPreference, systemIsDark }) {
|
|
47
|
+
if (configDarkMode === 'dark') return true;
|
|
48
|
+
if (configDarkMode === 'light') return false;
|
|
49
|
+
// configDarkMode is 'system' or undefined — user preference decides
|
|
50
|
+
if (userPreference === 'dark') return true;
|
|
51
|
+
if (userPreference === 'light') return false;
|
|
52
|
+
// userPreference is 'system' — follow OS
|
|
53
|
+
return systemIsDark;
|
|
54
|
+
}
|
|
55
|
+
function mergeComponents(shared, mode) {
|
|
56
|
+
if (!shared && !mode) return undefined;
|
|
57
|
+
const keys = new Set([
|
|
58
|
+
...Object.keys(shared ?? {}),
|
|
59
|
+
...Object.keys(mode ?? {})
|
|
60
|
+
]);
|
|
61
|
+
const merged = {};
|
|
62
|
+
keys.forEach((name)=>{
|
|
63
|
+
merged[name] = {
|
|
64
|
+
...(shared ?? {})[name] ?? {},
|
|
65
|
+
...(mode ?? {})[name] ?? {}
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return merged;
|
|
69
|
+
}
|
|
70
|
+
function useDarkMode({ antd, configDarkMode }) {
|
|
71
|
+
const baseAlgorithm = antd?.algorithm;
|
|
72
|
+
const cleanAlgorithm = stripDarkFromAlgorithm(baseAlgorithm);
|
|
73
|
+
const [userPreference, setUserPreference] = useState(()=>{
|
|
74
|
+
return window.localStorage?.getItem('lowdefy_darkMode') ?? 'system';
|
|
75
|
+
});
|
|
76
|
+
const [systemIsDark, setSystemIsDark] = useState(()=>{
|
|
77
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false;
|
|
78
|
+
});
|
|
79
|
+
// Listen for OS theme changes
|
|
80
|
+
useEffect(()=>{
|
|
81
|
+
const mql = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
82
|
+
if (!mql?.addEventListener) return;
|
|
83
|
+
const handler = (e)=>setSystemIsDark(e.matches);
|
|
84
|
+
mql.addEventListener('change', handler);
|
|
85
|
+
return ()=>mql.removeEventListener('change', handler);
|
|
86
|
+
}, []);
|
|
87
|
+
const setPreference = useCallback((newPref)=>{
|
|
88
|
+
window.localStorage?.setItem('lowdefy_darkMode', newPref);
|
|
89
|
+
setUserPreference(newPref);
|
|
90
|
+
}, []);
|
|
91
|
+
window.__lowdefy_setDarkMode = setPreference;
|
|
92
|
+
const isDark = resolveIsDark({
|
|
93
|
+
configDarkMode,
|
|
94
|
+
userPreference,
|
|
95
|
+
systemIsDark
|
|
96
|
+
});
|
|
97
|
+
window.__lowdefy_isDark = isDark;
|
|
98
|
+
const sharedToken = antd?.token;
|
|
99
|
+
const lightToken = antd?.lightToken;
|
|
100
|
+
const darkToken = antd?.darkToken;
|
|
101
|
+
const sharedComponents = antd?.components;
|
|
102
|
+
const lightComponents = antd?.lightComponents;
|
|
103
|
+
const darkComponents = antd?.darkComponents;
|
|
104
|
+
const token = useMemo(()=>{
|
|
105
|
+
const modeToken = isDark ? darkToken : lightToken;
|
|
106
|
+
if (!sharedToken && !modeToken) return undefined;
|
|
107
|
+
return {
|
|
108
|
+
...sharedToken ?? {},
|
|
109
|
+
...modeToken ?? {}
|
|
110
|
+
};
|
|
111
|
+
}, [
|
|
112
|
+
isDark,
|
|
113
|
+
sharedToken,
|
|
114
|
+
lightToken,
|
|
115
|
+
darkToken
|
|
116
|
+
]);
|
|
117
|
+
const components = useMemo(()=>mergeComponents(sharedComponents, isDark ? darkComponents : lightComponents), [
|
|
118
|
+
isDark,
|
|
119
|
+
sharedComponents,
|
|
120
|
+
lightComponents,
|
|
121
|
+
darkComponents
|
|
122
|
+
]);
|
|
123
|
+
// Keep the <html> background in sync with the resolved mode. The _document.js
|
|
124
|
+
// inline script sets an initial background before hydration; this effect takes
|
|
125
|
+
// over once React is active and updates on every dark/light toggle.
|
|
126
|
+
const darkBg = darkToken?.colorBgLayout;
|
|
127
|
+
const lightBg = lightToken?.colorBgLayout;
|
|
128
|
+
useEffect(()=>{
|
|
129
|
+
if (isDark) {
|
|
130
|
+
document.documentElement.style.backgroundColor = darkBg ?? '#000';
|
|
131
|
+
} else if (lightBg) {
|
|
132
|
+
document.documentElement.style.backgroundColor = lightBg;
|
|
133
|
+
} else {
|
|
134
|
+
document.documentElement.style.removeProperty('background-color');
|
|
135
|
+
}
|
|
136
|
+
}, [
|
|
137
|
+
isDark,
|
|
138
|
+
darkBg,
|
|
139
|
+
lightBg
|
|
140
|
+
]);
|
|
141
|
+
return {
|
|
142
|
+
algorithm: resolveAlgorithm(mergeAlgorithm(cleanAlgorithm, isDark)),
|
|
143
|
+
token,
|
|
144
|
+
components
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export default useDarkMode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lowdefy Client",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -33,24 +33,24 @@
|
|
|
33
33
|
"dist/*"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ant-design/icons": "
|
|
37
|
-
"
|
|
38
|
-
"@lowdefy/
|
|
39
|
-
"@lowdefy/
|
|
40
|
-
"@lowdefy/
|
|
41
|
-
"@lowdefy/
|
|
42
|
-
"@lowdefy/
|
|
43
|
-
"
|
|
36
|
+
"@ant-design/icons": "6.1.0",
|
|
37
|
+
"antd": "6.3.1",
|
|
38
|
+
"@lowdefy/block-utils": "5.1.0",
|
|
39
|
+
"@lowdefy/engine": "5.1.0",
|
|
40
|
+
"@lowdefy/errors": "5.1.0",
|
|
41
|
+
"@lowdefy/helpers": "5.1.0",
|
|
42
|
+
"@lowdefy/layout": "5.1.0",
|
|
43
|
+
"@lowdefy/logger": "5.1.0",
|
|
44
44
|
"react": "18.2.0",
|
|
45
|
-
"react-dom": "18.2.0"
|
|
45
|
+
"react-dom": "18.2.0",
|
|
46
|
+
"tinykeys": "^3.0.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@emotion/jest": "11.10.5",
|
|
49
49
|
"@jest/globals": "28.1.3",
|
|
50
|
-
"@lowdefy/jest-yaml-transform": "
|
|
51
|
-
"@swc/cli": "0.
|
|
52
|
-
"@swc/core": "1.
|
|
53
|
-
"@swc/jest": "0.2.
|
|
50
|
+
"@lowdefy/jest-yaml-transform": "5.1.0",
|
|
51
|
+
"@swc/cli": "0.8.0",
|
|
52
|
+
"@swc/core": "1.15.18",
|
|
53
|
+
"@swc/jest": "0.2.39",
|
|
54
54
|
"@testing-library/dom": "8.19.1",
|
|
55
55
|
"@testing-library/react": "13.4.0",
|
|
56
56
|
"@testing-library/user-event": "14.4.3",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"access": "public"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
|
-
"build": "swc src --out-dir dist --config-file ../../.swcrc --
|
|
66
|
+
"build": "swc src --out-dir dist --config-file ../../.swcrc --cli-config-file ../../.swc-cli.json && pnpm copyfiles",
|
|
67
67
|
"clean": "rm -rf dist",
|
|
68
68
|
"copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.yaml\" -e \"./src/**/*.snap\"",
|
|
69
69
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|