addon-ui 0.9.1 → 0.10.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/README.md +51 -12
- package/dist-types/components/Select/Select.d.ts +1 -1
- package/dist-types/components/Select/SelectIcon.d.ts +1 -1
- package/dist-types/components/Select/SelectValue.d.ts +1 -1
- package/dist-types/components/TextField/TextField.d.ts +2 -1
- package/dist-types/components/TextField/utils.d.ts +8 -0
- package/dist-types/components/Truncate/Truncate.d.ts +2 -2
- package/dist-types/components/Truncate/utils.d.ts +2 -0
- package/dist-types/plugin/index.d.ts +28 -2
- package/dist-types/providers/theme/ThemeProvider.d.ts +88 -0
- package/dist-types/providers/ui/UIProvider.d.ts +22 -1
- package/package.json +4 -8
- package/src/components/Select/Select.tsx +1 -1
- package/src/components/Select/SelectIcon.tsx +1 -1
- package/src/components/Select/SelectValue.tsx +1 -1
- package/src/components/TextField/TextField.tsx +66 -13
- package/src/components/TextField/text-field.module.scss +3 -1
- package/src/components/TextField/utils.ts +56 -0
- package/src/components/Truncate/Truncate.tsx +38 -56
- package/src/components/Truncate/truncate.module.scss +14 -15
- package/src/components/Truncate/utils.ts +62 -0
- package/src/plugin/index.ts +223 -9
- package/src/providers/theme/ThemeProvider.tsx +121 -15
- package/src/providers/ui/UIProvider.tsx +42 -15
- package/src/providers/ui/styles/default.scss +1 -1
- package/src/styles/mixins.scss +19 -6
- /package/src/providers/theme/{ThemeStorage.tsx → ThemeStorage.ts} +0 -0
|
@@ -5,16 +5,14 @@ import React, {
|
|
|
5
5
|
memo,
|
|
6
6
|
useImperativeHandle,
|
|
7
7
|
useLayoutEffect,
|
|
8
|
-
useMemo,
|
|
9
8
|
useRef,
|
|
10
9
|
useState,
|
|
11
10
|
} from "react";
|
|
12
|
-
import debounce from "debounce";
|
|
13
11
|
import classnames from "classnames";
|
|
14
12
|
|
|
15
13
|
import {useComponentProps} from "../../providers";
|
|
16
14
|
|
|
17
|
-
import {
|
|
15
|
+
import {calculateMiddleTruncate} from "./utils";
|
|
18
16
|
|
|
19
17
|
import styles from "./truncate.module.scss";
|
|
20
18
|
|
|
@@ -22,81 +20,57 @@ export interface TruncateProps extends ComponentProps<"span"> {
|
|
|
22
20
|
text?: string;
|
|
23
21
|
middle?: boolean;
|
|
24
22
|
separator?: string;
|
|
25
|
-
|
|
23
|
+
contentClassname?: string;
|
|
24
|
+
render?: (text: string) => React.ReactNode;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
const trimMiddle = (el: HTMLElement, text: string, separator: string) => {
|
|
29
|
-
const measure = (txt: string) => {
|
|
30
|
-
el.textContent = txt;
|
|
31
|
-
return el.scrollWidth <= el.clientWidth;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (measure(text)) return text;
|
|
35
|
-
|
|
36
|
-
let low = 0;
|
|
37
|
-
let high = text.length - 2;
|
|
38
|
-
let result = "";
|
|
39
|
-
|
|
40
|
-
while (low <= high) {
|
|
41
|
-
const size = Math.floor((low + high) / 2);
|
|
42
|
-
const left = text.slice(0, Math.ceil(size / 2));
|
|
43
|
-
const right = text.slice(text.length - Math.floor(size / 2));
|
|
44
|
-
const trimmed = left + separator + right;
|
|
45
|
-
|
|
46
|
-
if (measure(trimmed)) {
|
|
47
|
-
result = trimmed;
|
|
48
|
-
low = size + 1;
|
|
49
|
-
} else {
|
|
50
|
-
high = size - 1;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return result || text.charAt(0) + separator + text.charAt(text.length - 1);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
27
|
const Truncate: ForwardRefRenderFunction<HTMLSpanElement, TruncateProps> = (props, ref) => {
|
|
58
28
|
const {
|
|
59
29
|
text = "",
|
|
60
30
|
middle,
|
|
61
31
|
separator = "...",
|
|
62
32
|
className,
|
|
63
|
-
|
|
33
|
+
contentClassname,
|
|
34
|
+
render,
|
|
64
35
|
...other
|
|
65
36
|
} = {...useComponentProps("truncate"), ...props};
|
|
66
37
|
|
|
67
|
-
const
|
|
38
|
+
const containerRef = useRef<HTMLSpanElement>(null);
|
|
68
39
|
const [displayedText, setDisplayedText] = useState(text);
|
|
69
40
|
|
|
70
|
-
|
|
71
|
-
return middle ? displayedText : text;
|
|
72
|
-
}, [displayedText, text, middle]);
|
|
73
|
-
|
|
74
|
-
useImperativeHandle(ref, () => innerRef.current!, []);
|
|
41
|
+
useImperativeHandle(ref, () => containerRef.current!, []);
|
|
75
42
|
|
|
76
43
|
useLayoutEffect(() => {
|
|
77
|
-
const el =
|
|
78
|
-
if (!el || !middle) return;
|
|
44
|
+
const el = containerRef.current;
|
|
79
45
|
|
|
80
|
-
|
|
46
|
+
if (!middle || !el) {
|
|
47
|
+
setDisplayedText(text);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const observer = new ResizeObserver(entries => {
|
|
52
|
+
const entry = entries[0];
|
|
53
|
+
if (!entry) return;
|
|
54
|
+
|
|
55
|
+
const maxWidth = entry.contentRect.width;
|
|
56
|
+
const {fontWeight, fontSize, fontFamily, letterSpacing} = window.getComputedStyle(el);
|
|
81
57
|
|
|
82
|
-
|
|
83
|
-
setDisplayedText(trimMiddle(el, text, separator));
|
|
84
|
-
}, 150);
|
|
58
|
+
const font = [fontWeight, fontSize, fontFamily].join(" ");
|
|
85
59
|
|
|
86
|
-
|
|
60
|
+
const truncated = calculateMiddleTruncate(text, maxWidth, font, letterSpacing, separator);
|
|
87
61
|
|
|
88
|
-
|
|
62
|
+
setDisplayedText(prev => (prev !== truncated ? truncated : prev));
|
|
63
|
+
});
|
|
89
64
|
|
|
90
65
|
observer.observe(el);
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
}, [text, middle, separator]);
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
measureAndTrim.clear();
|
|
94
|
-
observer?.disconnect();
|
|
95
|
-
};
|
|
96
|
-
}, [text, separator, middle]);
|
|
69
|
+
const content = render ? render(displayedText) : displayedText;
|
|
97
70
|
|
|
98
71
|
return (
|
|
99
72
|
<span
|
|
73
|
+
ref={containerRef}
|
|
100
74
|
className={classnames(
|
|
101
75
|
styles["truncate"],
|
|
102
76
|
{
|
|
@@ -104,11 +78,19 @@ const Truncate: ForwardRefRenderFunction<HTMLSpanElement, TruncateProps> = (prop
|
|
|
104
78
|
},
|
|
105
79
|
className
|
|
106
80
|
)}
|
|
81
|
+
title={text}
|
|
107
82
|
{...other}
|
|
108
83
|
>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
{middle ? (
|
|
85
|
+
<>
|
|
86
|
+
<span className={styles["truncate__hidden"]} aria-hidden="true">
|
|
87
|
+
{text}
|
|
88
|
+
</span>
|
|
89
|
+
<span className={classnames(styles["truncate__content"], contentClassname)}>{content}</span>
|
|
90
|
+
</>
|
|
91
|
+
) : (
|
|
92
|
+
content
|
|
93
|
+
)}
|
|
112
94
|
</span>
|
|
113
95
|
);
|
|
114
96
|
};
|
|
@@ -2,30 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
.truncate {
|
|
4
4
|
position: relative;
|
|
5
|
-
display: block;
|
|
6
|
-
|
|
5
|
+
display: inline-block;
|
|
6
|
+
vertical-align: middle;
|
|
7
|
+
width: auto;
|
|
8
|
+
max-width: 100%;
|
|
9
|
+
min-width: 0;
|
|
7
10
|
white-space: nowrap;
|
|
8
11
|
overflow: hidden;
|
|
9
12
|
text-overflow: ellipsis;
|
|
10
13
|
transition: color var(--truncate-speed-color, var(--speed-color));
|
|
11
14
|
|
|
15
|
+
&--middle {
|
|
16
|
+
text-overflow: clip;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
&__hidden {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
display: block;
|
|
21
|
+
height: 0;
|
|
22
|
+
visibility: hidden;
|
|
16
23
|
white-space: nowrap;
|
|
17
24
|
overflow: hidden;
|
|
18
|
-
text-overflow: ellipsis;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
padding-right: var(--truncate-around-space, 8px);
|
|
25
|
-
|
|
26
|
-
@include theme.rtl() {
|
|
27
|
-
padding-right: 0;
|
|
28
|
-
padding-left: var(--truncate-around-space, 8px);
|
|
29
|
-
}
|
|
27
|
+
&__content {
|
|
28
|
+
display: block;
|
|
30
29
|
}
|
|
31
30
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const MAX_CACHE_SIZE = 1000;
|
|
2
|
+
const cache = new Map<string, string>();
|
|
3
|
+
let canvas: HTMLCanvasElement | null = null;
|
|
4
|
+
|
|
5
|
+
const addToCache = (key: string, value: string) => {
|
|
6
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
7
|
+
const oldestKey = cache.keys().next().value;
|
|
8
|
+
if (oldestKey !== undefined) {
|
|
9
|
+
cache.delete(oldestKey);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
cache.set(key, value);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const calculateMiddleTruncate = (
|
|
16
|
+
text: string,
|
|
17
|
+
maxWidth: number,
|
|
18
|
+
font: string,
|
|
19
|
+
letterSpacing: string,
|
|
20
|
+
separator: string
|
|
21
|
+
) => {
|
|
22
|
+
const cacheKey = `${text}-${maxWidth}-${font}-${letterSpacing}-${separator}`;
|
|
23
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey)!;
|
|
24
|
+
|
|
25
|
+
if (!canvas) {
|
|
26
|
+
canvas = document.createElement("canvas");
|
|
27
|
+
}
|
|
28
|
+
const context = canvas.getContext("2d");
|
|
29
|
+
if (!context) return text;
|
|
30
|
+
context.font = font;
|
|
31
|
+
context.letterSpacing = letterSpacing;
|
|
32
|
+
|
|
33
|
+
const measure = (txt: string) => context.measureText(txt).width;
|
|
34
|
+
|
|
35
|
+
if (measure(text) <= maxWidth) {
|
|
36
|
+
addToCache(cacheKey, text);
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let low = 0;
|
|
41
|
+
let high = text.length;
|
|
42
|
+
let result = "";
|
|
43
|
+
|
|
44
|
+
while (low <= high) {
|
|
45
|
+
const mid = Math.floor((low + high) / 2);
|
|
46
|
+
const leftHalf = Math.ceil(mid / 2);
|
|
47
|
+
const rightHalf = Math.floor(mid / 2);
|
|
48
|
+
|
|
49
|
+
const trimmed = text.slice(0, leftHalf) + separator + text.slice(text.length - rightHalf);
|
|
50
|
+
|
|
51
|
+
if (measure(trimmed) <= maxWidth) {
|
|
52
|
+
result = trimmed;
|
|
53
|
+
low = mid + 1;
|
|
54
|
+
} else {
|
|
55
|
+
high = mid - 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const finalResult = result || text[0] + separator + text.slice(-1);
|
|
60
|
+
addToCache(cacheKey, finalResult);
|
|
61
|
+
return finalResult;
|
|
62
|
+
};
|
package/src/plugin/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import {definePlugin} from "adnbn";
|
|
3
|
-
import {Configuration as Rspack} from "@rspack/core";
|
|
3
|
+
import {Configuration as Rspack, NormalModule} from "@rspack/core";
|
|
4
4
|
import {RspackVirtualModulePlugin} from "rspack-plugin-virtual-module";
|
|
5
|
+
import kebabCase from "lodash/kebabCase";
|
|
5
6
|
|
|
6
7
|
import StyleBuilder from "./builder/StyleBuilder";
|
|
7
8
|
import ConfigBuilder from "./builder/ConfigBuilder";
|
|
@@ -13,20 +14,52 @@ import ConfigFinder from "./finder/ConfigFinder";
|
|
|
13
14
|
import type {BuilderContract} from "./types";
|
|
14
15
|
|
|
15
16
|
export interface PluginOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Directory path where plugin configuration and style files are located, relative to the project root.
|
|
19
|
+
* @default "."
|
|
20
|
+
*/
|
|
16
21
|
themeDir?: string;
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Name of the configuration file.
|
|
25
|
+
* @default "config.ui"
|
|
26
|
+
*/
|
|
27
|
+
configName?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Name of the style file.
|
|
31
|
+
* @default "style.ui"
|
|
32
|
+
*/
|
|
33
|
+
styleName?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether to merge configuration files from different app directories.
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
19
39
|
mergeConfig?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether to merge style files from different app directories.
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
20
45
|
mergeStyles?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for splitting chunks.
|
|
49
|
+
* Can be a boolean to enable/disable or a callback to customize chunk names.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
splitChunks?: boolean | ((name: string) => string | undefined);
|
|
21
53
|
}
|
|
22
54
|
|
|
23
55
|
export default definePlugin((options: PluginOptions = {}) => {
|
|
24
56
|
const {
|
|
25
57
|
themeDir = ".",
|
|
26
|
-
|
|
27
|
-
|
|
58
|
+
configName = "ui.config",
|
|
59
|
+
styleName = "ui.style",
|
|
28
60
|
mergeConfig = true,
|
|
29
61
|
mergeStyles = true,
|
|
62
|
+
splitChunks = false,
|
|
30
63
|
} = options;
|
|
31
64
|
|
|
32
65
|
let configFinder: Finder;
|
|
@@ -47,14 +80,14 @@ export default definePlugin((options: PluginOptions = {}) => {
|
|
|
47
80
|
path.join(srcDir, sharedDir, ...normalizeThemeDir),
|
|
48
81
|
];
|
|
49
82
|
|
|
50
|
-
configFinder = new ConfigFinder(
|
|
51
|
-
styleFinder = new StyleFinder(
|
|
83
|
+
configFinder = new ConfigFinder(configName, config).setCanMerge(mergeConfig).setSearchDirs(searchDirs);
|
|
84
|
+
styleFinder = new StyleFinder(styleName, config).setCanMerge(mergeStyles).setSearchDirs(searchDirs);
|
|
52
85
|
|
|
53
86
|
configBuilder = new ConfigBuilder(configFinder);
|
|
54
87
|
styleBuilder = new StyleBuilder(styleFinder);
|
|
55
88
|
},
|
|
56
89
|
bundler: () => {
|
|
57
|
-
|
|
90
|
+
const config: Rspack = {
|
|
58
91
|
plugins: [
|
|
59
92
|
new RspackVirtualModulePlugin(
|
|
60
93
|
{
|
|
@@ -64,7 +97,188 @@ export default definePlugin((options: PluginOptions = {}) => {
|
|
|
64
97
|
"addon-ui-virtual"
|
|
65
98
|
),
|
|
66
99
|
],
|
|
67
|
-
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (splitChunks) {
|
|
103
|
+
const splitChunksNameCallback = typeof splitChunks === "function" ? splitChunks : undefined;
|
|
104
|
+
|
|
105
|
+
const toUIChunk = (name: string) => {
|
|
106
|
+
if (splitChunksNameCallback) {
|
|
107
|
+
const finalName = splitChunksNameCallback(name);
|
|
108
|
+
|
|
109
|
+
if (finalName) {
|
|
110
|
+
return finalName;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return `${kebabCase(name)}.ui`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const extractName = (res: string): string | null => {
|
|
118
|
+
if (!res) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const match = res.match(/src[\\/]components[\\/]([^\\/]+)/);
|
|
123
|
+
|
|
124
|
+
if (match && match[1] && !match[1].includes(".") && match[1] !== "index") {
|
|
125
|
+
const componentName = match[1];
|
|
126
|
+
const normalized = componentName.toLowerCase();
|
|
127
|
+
|
|
128
|
+
if (["button", "basebutton", "iconbutton"].includes(normalized)) {
|
|
129
|
+
return "button";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (["list", "listitem"].includes(normalized)) {
|
|
133
|
+
return "list";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (["view", "viewdrawer", "viewmodal", "viewport"].includes(normalized)) {
|
|
137
|
+
return "view";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (["svgsprite", "icon"].includes(normalized)) {
|
|
141
|
+
return "svg";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return componentName;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
config.optimization = {
|
|
151
|
+
splitChunks: {
|
|
152
|
+
cacheGroups: {
|
|
153
|
+
addonUI: {
|
|
154
|
+
test: module => {
|
|
155
|
+
const resource =
|
|
156
|
+
(module as NormalModule).resource ||
|
|
157
|
+
(typeof module.nameForCondition === "function"
|
|
158
|
+
? module.nameForCondition()
|
|
159
|
+
: "");
|
|
160
|
+
|
|
161
|
+
if (!resource) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
resource.includes("addon-ui-virtual") ||
|
|
167
|
+
resource.includes("addon-ui-style.scss") ||
|
|
168
|
+
resource.includes("addon-ui-config") ||
|
|
169
|
+
/providers[\\/]ui[\\/]styles/.test(resource)
|
|
170
|
+
) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isComponent = /src[\\/]components[\\/]/.test(resource);
|
|
175
|
+
|
|
176
|
+
if (isComponent) {
|
|
177
|
+
if (resource.includes("node_modules")) {
|
|
178
|
+
return resource.includes("addon-ui");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return /node_modules[\\/](@radix-ui|radix-ui|autosize|odometer|react-highlight-words|react-responsive-overflow-list)/.test(
|
|
185
|
+
resource
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
name(module) {
|
|
189
|
+
const resource =
|
|
190
|
+
(module as NormalModule).resource ||
|
|
191
|
+
((typeof module.nameForCondition === "function"
|
|
192
|
+
? module.nameForCondition()
|
|
193
|
+
: "") as string);
|
|
194
|
+
|
|
195
|
+
const directName = extractName(resource);
|
|
196
|
+
|
|
197
|
+
if (directName) {
|
|
198
|
+
return toUIChunk(directName);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
resource.includes("addon-ui-virtual") ||
|
|
203
|
+
resource.includes("addon-ui-style.scss") ||
|
|
204
|
+
resource.includes("addon-ui-config") ||
|
|
205
|
+
/providers[\\/]ui[\\/]styles/.test(resource)
|
|
206
|
+
) {
|
|
207
|
+
return toUIChunk("common");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (resource.includes("node_modules")) {
|
|
211
|
+
if (resource.includes("radix-ui")) {
|
|
212
|
+
const match = resource.match(/@radix-ui[\\/]react-([^\\/]+)/);
|
|
213
|
+
|
|
214
|
+
if (match) {
|
|
215
|
+
const radixName = match[1];
|
|
216
|
+
|
|
217
|
+
const mainRadixComponents = [
|
|
218
|
+
"accordion",
|
|
219
|
+
"avatar",
|
|
220
|
+
"checkbox",
|
|
221
|
+
"dialog",
|
|
222
|
+
"dropdown-menu",
|
|
223
|
+
"popover",
|
|
224
|
+
"scroll-area",
|
|
225
|
+
"select",
|
|
226
|
+
"switch",
|
|
227
|
+
"tabs",
|
|
228
|
+
"toast",
|
|
229
|
+
"tooltip",
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
if (mainRadixComponents.includes(radixName)) {
|
|
233
|
+
return toUIChunk(radixName);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return toUIChunk("common");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (resource.includes("odometer")) {
|
|
241
|
+
return toUIChunk("odometer");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (resource.includes("autosize")) {
|
|
245
|
+
return toUIChunk("text-area");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (resource.includes("react-highlight-words")) {
|
|
249
|
+
return toUIChunk("highlight");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (resource.includes("react-responsive-overflow-list")) {
|
|
253
|
+
return toUIChunk("truncate-list");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
258
|
+
let issuer = (module as any).issuer;
|
|
259
|
+
|
|
260
|
+
while (issuer) {
|
|
261
|
+
const nameFromIssuer = extractName(issuer.resource || "");
|
|
262
|
+
|
|
263
|
+
if (nameFromIssuer) {
|
|
264
|
+
return toUIChunk(nameFromIssuer);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
issuer = issuer.issuer;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return toUIChunk("common");
|
|
271
|
+
},
|
|
272
|
+
chunks: "all",
|
|
273
|
+
enforce: true,
|
|
274
|
+
priority: 30,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return config satisfies Rspack;
|
|
68
282
|
},
|
|
69
283
|
manifest: ({manifest}) => {
|
|
70
284
|
manifest.addPermission("storage");
|
|
@@ -14,30 +14,124 @@ const isValid = (theme: Theme | undefined): theme is Theme => {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export interface ThemeProviderProps extends Pick<Config, "components"> {
|
|
17
|
+
/**
|
|
18
|
+
* Theme persistence storage configuration.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* - When `undefined`, theme changes are stored only in component state (memory) and reset on page reload.
|
|
22
|
+
* - When `true`, uses the default `ThemeStorage` implementation (typically localStorage).
|
|
23
|
+
* - When a custom `ThemeStorageContract` object is provided, uses that implementation for theme persistence.
|
|
24
|
+
*
|
|
25
|
+
* The storage is used to save, retrieve, and watch for theme changes across sessions or tabs.
|
|
26
|
+
*
|
|
27
|
+
* @default undefined
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // No persistence (memory only)
|
|
32
|
+
* <ThemeProvider>
|
|
33
|
+
* <App />
|
|
34
|
+
* </ThemeProvider>
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // Use default storage (localStorage)
|
|
40
|
+
* <ThemeProvider storage={true}>
|
|
41
|
+
* <App />
|
|
42
|
+
* </ThemeProvider>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* // Use custom storage implementation
|
|
48
|
+
* const customStorage: ThemeStorageContract = {
|
|
49
|
+
* get: async () => { ... },
|
|
50
|
+
* change: async (theme) => { ... },
|
|
51
|
+
* toggle: async () => { ... },
|
|
52
|
+
* watch: (callback) => { ... }
|
|
53
|
+
* };
|
|
54
|
+
*
|
|
55
|
+
* <ThemeProvider storage={customStorage}>
|
|
56
|
+
* <App />
|
|
57
|
+
* </ThemeProvider>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
17
60
|
storage?: ThemeStorageContract | true;
|
|
61
|
+
/**
|
|
62
|
+
* The DOM element where the provider will set attributes "browser"
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* - When a string is provided, it's used as a CSS selector to find the element via `document.querySelector`.
|
|
66
|
+
* - When an Element is provided, attributes are set directly on that element.
|
|
67
|
+
* - When `false`, no element attributes are set.
|
|
68
|
+
*
|
|
69
|
+
* Attributes are automatically cleaned up when the component unmounts.
|
|
70
|
+
*
|
|
71
|
+
* @default "html"
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* // Use default html element
|
|
76
|
+
* <ThemeProviderProps>
|
|
77
|
+
* <App />
|
|
78
|
+
* </ThemeProviderProps>
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* // Use custom selector
|
|
84
|
+
* <ThemeProviderProps container="#app-root">
|
|
85
|
+
* <App />
|
|
86
|
+
* </ThemeProviderProps>
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* // Use direct element reference
|
|
92
|
+
* <ThemeProviderProps container={document.body}>
|
|
93
|
+
* <App />
|
|
94
|
+
* </ThemeProviderProps>
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* // Disable container attributes
|
|
100
|
+
* <ThemeProviderProps container={false}>
|
|
101
|
+
* <App />
|
|
102
|
+
* </ThemeProviderProps>
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
container?: string | Element | false;
|
|
18
106
|
}
|
|
19
107
|
|
|
20
|
-
const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> =
|
|
108
|
+
const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = props => {
|
|
109
|
+
const {children, components, storage, container} = props;
|
|
110
|
+
|
|
21
111
|
const [theme, setTheme] = useState<Theme>(() => (isDarkMedia() ? Theme.Dark : Theme.Light));
|
|
22
112
|
|
|
23
113
|
const currentStorage: ThemeStorageContract | undefined = useMemo(() => {
|
|
24
|
-
if (!storage)
|
|
114
|
+
if (!storage) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
25
117
|
|
|
26
|
-
if (storage === true)
|
|
118
|
+
if (storage === true) {
|
|
119
|
+
return new ThemeStorage();
|
|
120
|
+
}
|
|
27
121
|
|
|
28
122
|
return storage;
|
|
29
123
|
}, [storage]);
|
|
30
124
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
125
|
+
// prettier-ignore
|
|
126
|
+
const changeTheme = useCallback((theme: Theme) => {
|
|
127
|
+
setTheme(theme);
|
|
128
|
+
|
|
129
|
+
if (currentStorage) {
|
|
130
|
+
currentStorage
|
|
131
|
+
.change(theme)
|
|
132
|
+
.catch(e => console.error("ThemeProvider: set theme to storage error", e));
|
|
133
|
+
}
|
|
134
|
+
}, [currentStorage]);
|
|
41
135
|
|
|
42
136
|
const toggleTheme = useCallback(() => {
|
|
43
137
|
changeTheme(theme === Theme.Dark ? Theme.Light : Theme.Dark);
|
|
@@ -57,8 +151,20 @@ const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({children, com
|
|
|
57
151
|
}, [currentStorage]);
|
|
58
152
|
|
|
59
153
|
useEffect(() => {
|
|
60
|
-
|
|
61
|
-
|
|
154
|
+
if (container === false) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const element = typeof container === "string" ? document.querySelector(container) : container;
|
|
159
|
+
|
|
160
|
+
if (element) {
|
|
161
|
+
element.setAttribute("theme", theme);
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
element.removeAttribute("theme");
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}, [theme, container]);
|
|
62
168
|
|
|
63
169
|
return (
|
|
64
170
|
<ThemeContext.Provider value={{theme, changeTheme, toggleTheme, components}}>{children}</ThemeContext.Provider>
|