addon-ui 0.3.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 (129) hide show
  1. package/README.md +1197 -0
  2. package/package.json +113 -0
  3. package/src/components/Avatar/Avatar.tsx +56 -0
  4. package/src/components/Avatar/avatar.module.scss +78 -0
  5. package/src/components/Avatar/index.ts +2 -0
  6. package/src/components/Avatar/types.ts +11 -0
  7. package/src/components/BaseButton/BaseButton.tsx +36 -0
  8. package/src/components/BaseButton/base-button.module.scss +24 -0
  9. package/src/components/BaseButton/index.ts +1 -0
  10. package/src/components/Button/Button.tsx +51 -0
  11. package/src/components/Button/button.module.scss +140 -0
  12. package/src/components/Button/index.ts +2 -0
  13. package/src/components/Button/types.ts +24 -0
  14. package/src/components/Checkbox/Checkbox.tsx +58 -0
  15. package/src/components/Checkbox/checkbox.module.scss +82 -0
  16. package/src/components/Checkbox/index.ts +2 -0
  17. package/src/components/Checkbox/types.ts +15 -0
  18. package/src/components/Dialog/Dialog.tsx +126 -0
  19. package/src/components/Dialog/dialog.module.scss +55 -0
  20. package/src/components/Dialog/index.ts +1 -0
  21. package/src/components/Drawer/Drawer.tsx +53 -0
  22. package/src/components/Drawer/drawer.module.scss +170 -0
  23. package/src/components/Drawer/index.ts +2 -0
  24. package/src/components/Drawer/types.ts +6 -0
  25. package/src/components/Footer/Footer.tsx +59 -0
  26. package/src/components/Footer/footer.module.scss +45 -0
  27. package/src/components/Footer/index.ts +1 -0
  28. package/src/components/Header/Header.tsx +74 -0
  29. package/src/components/Header/header.module.scss +56 -0
  30. package/src/components/Header/index.ts +1 -0
  31. package/src/components/Highlight/Highlight.tsx +36 -0
  32. package/src/components/Highlight/highlight.module.scss +48 -0
  33. package/src/components/Highlight/index.ts +2 -0
  34. package/src/components/Highlight/types.ts +5 -0
  35. package/src/components/Icon/Icon.tsx +46 -0
  36. package/src/components/Icon/icon.module.scss +17 -0
  37. package/src/components/Icon/index.ts +1 -0
  38. package/src/components/IconButton/IconButton.tsx +50 -0
  39. package/src/components/IconButton/icon-button.module.scss +86 -0
  40. package/src/components/IconButton/index.ts +2 -0
  41. package/src/components/IconButton/types.ts +17 -0
  42. package/src/components/Layout/Provider.tsx +60 -0
  43. package/src/components/Layout/context.ts +24 -0
  44. package/src/components/Layout/index.ts +2 -0
  45. package/src/components/Layout/layout.module.scss +17 -0
  46. package/src/components/List/List.tsx +24 -0
  47. package/src/components/List/index.ts +1 -0
  48. package/src/components/List/list.module.scss +8 -0
  49. package/src/components/ListItem/ListItem.tsx +75 -0
  50. package/src/components/ListItem/index.ts +1 -0
  51. package/src/components/ListItem/list-item.module.scss +37 -0
  52. package/src/components/Modal/Modal.tsx +90 -0
  53. package/src/components/Modal/index.ts +2 -0
  54. package/src/components/Modal/modal.module.scss +97 -0
  55. package/src/components/Modal/types.ts +6 -0
  56. package/src/components/Odometer/Odometer.tsx +45 -0
  57. package/src/components/Odometer/hooks/useOdometer.tsx +24 -0
  58. package/src/components/Odometer/index.ts +2 -0
  59. package/src/components/Odometer/odometer.module.scss +81 -0
  60. package/src/components/Odometer/odometr.d.ts +9 -0
  61. package/src/components/ScrollArea/ScrollArea.tsx +75 -0
  62. package/src/components/ScrollArea/index.ts +1 -0
  63. package/src/components/ScrollArea/scroll-area.module.scss +58 -0
  64. package/src/components/SvgSprite/SvgSprite.tsx +21 -0
  65. package/src/components/SvgSprite/index.ts +1 -0
  66. package/src/components/Switch/Switch.tsx +24 -0
  67. package/src/components/Switch/index.ts +1 -0
  68. package/src/components/Switch/switch.module.scss +65 -0
  69. package/src/components/Tag/Tag.tsx +50 -0
  70. package/src/components/Tag/index.ts +2 -0
  71. package/src/components/Tag/tag.module.scss +118 -0
  72. package/src/components/Tag/types.ts +23 -0
  73. package/src/components/TextArea/TextArea.tsx +126 -0
  74. package/src/components/TextArea/index.ts +2 -0
  75. package/src/components/TextArea/text-area.module.scss +88 -0
  76. package/src/components/TextArea/types.ts +18 -0
  77. package/src/components/TextField/TextField.tsx +139 -0
  78. package/src/components/TextField/index.ts +2 -0
  79. package/src/components/TextField/text-field.module.scss +129 -0
  80. package/src/components/TextField/types.ts +24 -0
  81. package/src/components/Toast/Toast.tsx +124 -0
  82. package/src/components/Toast/index.ts +2 -0
  83. package/src/components/Toast/toast.module.scss +267 -0
  84. package/src/components/Toast/types.ts +20 -0
  85. package/src/components/Tooltip/Tooltip.tsx +82 -0
  86. package/src/components/Tooltip/index.ts +1 -0
  87. package/src/components/Tooltip/tooltip.module.scss +123 -0
  88. package/src/components/View/View.tsx +68 -0
  89. package/src/components/View/index.ts +1 -0
  90. package/src/components/View/view.module.scss +38 -0
  91. package/src/components/ViewDrawer/ViewDrawer.tsx +24 -0
  92. package/src/components/ViewDrawer/index.ts +1 -0
  93. package/src/components/ViewModal/ViewModal.tsx +24 -0
  94. package/src/components/ViewModal/index.ts +1 -0
  95. package/src/components/index.ts +27 -0
  96. package/src/components/types.ts +65 -0
  97. package/src/config/default.ts +3 -0
  98. package/src/config/index.ts +8 -0
  99. package/src/declaration.d.ts +8 -0
  100. package/src/index.ts +3 -0
  101. package/src/plugin/builder/ConfigBuilder.ts +32 -0
  102. package/src/plugin/builder/StyleBuilder.ts +35 -0
  103. package/src/plugin/builder/virtual.config.ts +5 -0
  104. package/src/plugin/finder/ConfigFinder.ts +26 -0
  105. package/src/plugin/finder/Finder.ts +76 -0
  106. package/src/plugin/finder/StyleFinder.ts +23 -0
  107. package/src/plugin/index.ts +73 -0
  108. package/src/plugin/types.ts +8 -0
  109. package/src/providers/extra/ExtraProvider.tsx +13 -0
  110. package/src/providers/extra/context.ts +14 -0
  111. package/src/providers/extra/index.ts +2 -0
  112. package/src/providers/icons/IconsProvider.tsx +35 -0
  113. package/src/providers/icons/context.ts +20 -0
  114. package/src/providers/icons/index.ts +2 -0
  115. package/src/providers/index.ts +4 -0
  116. package/src/providers/theme/ThemeProvider.tsx +60 -0
  117. package/src/providers/theme/ThemeStorage.tsx +36 -0
  118. package/src/providers/theme/context.ts +30 -0
  119. package/src/providers/theme/index.ts +3 -0
  120. package/src/providers/ui/UIProvider.tsx +41 -0
  121. package/src/providers/ui/index.ts +1 -0
  122. package/src/providers/ui/styles/default.scss +95 -0
  123. package/src/providers/ui/styles/reset.scss +127 -0
  124. package/src/styles/mixins.scss +23 -0
  125. package/src/types/config.ts +15 -0
  126. package/src/types/theme.ts +11 -0
  127. package/src/utils/index.ts +2 -0
  128. package/src/utils/react.ts +21 -0
  129. package/src/utils/utils.ts +12 -0
@@ -0,0 +1,65 @@
1
+ import type {
2
+ AvatarProps,
3
+ ButtonProps,
4
+ CheckboxProps,
5
+ DialogProps,
6
+ DrawerProps,
7
+ FooterProps,
8
+ HeaderProps,
9
+ HighlightProps,
10
+ IconProps,
11
+ IconButtonProps,
12
+ ListProps,
13
+ ListItemProps,
14
+ ModalProps,
15
+ OdometerProps,
16
+ ScrollAreaProps,
17
+ SwitchProps,
18
+ TagProps,
19
+ TextAreaProps,
20
+ TextFieldProps,
21
+ ToastProps,
22
+ TooltipProps,
23
+ ViewProps,
24
+ ViewDrawerProps,
25
+ ViewModalProps,
26
+ } from "../components";
27
+
28
+ export interface ComponentsProps {
29
+ avatar?: Pick<AvatarProps, "size" | "radius" | "cursorPointer" | "delayMs">;
30
+ button?: Pick<ButtonProps, "variant" | "color" | "size" | "radius">;
31
+ checkbox?: Pick<CheckboxProps, "variant" | "size" | "radius" | "checkedIcon" | "indeterminateIcon">;
32
+ dialog?: DialogProps;
33
+ drawer?: DrawerProps;
34
+ footer?: FooterProps;
35
+ header?: Pick<HeaderProps, "alignCenter" | "before" | "after">;
36
+ highlight?: HighlightProps;
37
+ icon?: Omit<IconProps, "name">;
38
+ iconButton?: Pick<IconButtonProps, "variant" | "size" | "radius">;
39
+ list?: ListProps;
40
+ listItem?: ListItemProps;
41
+ modal?: ModalProps;
42
+ odometer?: Pick<OdometerProps, "auto" | "format" | "duration">;
43
+ scrollArea?: ScrollAreaProps;
44
+ switch?: SwitchProps;
45
+ tag?: Pick<TagProps, "variant" | "size" | "color" | "radius" | "clickable">;
46
+ textArea?: TextAreaProps;
47
+ textField?: TextFieldProps;
48
+ toast?: Pick<
49
+ ToastProps,
50
+ | "side"
51
+ | "duration"
52
+ | "swipeDirection"
53
+ | "swipeThreshold"
54
+ | "closeProps"
55
+ | "closeIcon"
56
+ | "fullWidth"
57
+ | "sticky"
58
+ | "radius"
59
+ | "color"
60
+ >;
61
+ tooltip?: TooltipProps;
62
+ view?: ViewProps;
63
+ viewDrawer?: ViewDrawerProps;
64
+ viewModal?: ViewModalProps;
65
+ }
@@ -0,0 +1,3 @@
1
+ import {Config} from "./index";
2
+
3
+ export default {} as Config;
@@ -0,0 +1,8 @@
1
+ import {ComponentsProps, Config, ExtraProps, Icons} from "../types/config";
2
+
3
+ export type {ComponentsProps, Config, ExtraProps, Icons};
4
+
5
+ export const defineConfig = (config: Partial<Config>): Config => {
6
+ const {components = {}, extra = {}, icons = {}} = config;
7
+ return {components, extra, icons};
8
+ };
@@ -0,0 +1,8 @@
1
+ declare module "*.module.css" {
2
+ const classes: CSSModuleClasses;
3
+ export default classes;
4
+ }
5
+ declare module "*.module.scss" {
6
+ const classes: CSSModuleClasses;
7
+ export default classes;
8
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./components";
2
+ export * from "./providers";
3
+ export {Theme, type ThemeStorageContract} from "./types/theme";
@@ -0,0 +1,32 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Finder from "../finder/Finder";
4
+
5
+ import type {BuilderContract} from "../types";
6
+
7
+ export default class ConfigBuilder implements BuilderContract {
8
+ protected template: string;
9
+
10
+ public constructor(protected finder: Finder) {
11
+ this.template = fs.readFileSync(path.resolve(__dirname, "virtual.config.ts"), "utf8");
12
+ }
13
+
14
+ public build(): string {
15
+ const files = this.finder.getFiles();
16
+
17
+ const imports = files.map(file => {
18
+ return file.name ? `import ${file.name} from "${file.import}"` : `import "${file.import}"`;
19
+ });
20
+
21
+ // Elements must be reversed for correct merging of configs by priority
22
+ const names = files
23
+ .map(file => file.name)
24
+ .filter(Boolean)
25
+ .reverse();
26
+
27
+ // prettier-ignore
28
+ return this.template
29
+ .replace("//configs imports", imports.join("\n"))
30
+ .replace("{}", names.join(", "));
31
+ }
32
+ }
@@ -0,0 +1,35 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Finder from "../finder/Finder";
4
+
5
+ import type {BuilderContract} from "../types";
6
+
7
+ export default class StyleBuilder implements BuilderContract {
8
+ public constructor(protected finder: Finder) {}
9
+
10
+ public build(): string {
11
+ const files = this.finder.getFiles();
12
+
13
+ const usesLines = new Set<string>();
14
+ const stylesLines: string[] = [];
15
+
16
+ const lines = files
17
+ .map(file => file.import)
18
+ .reverse()
19
+ .reduce((lines, filePath) => {
20
+ return lines + "\n" + fs.readFileSync(path.resolve(filePath), "utf8");
21
+ }, "")
22
+ .split("\n");
23
+
24
+ for (const line of lines) {
25
+ const trimmed = line.trim();
26
+ if (trimmed.startsWith("@use")) {
27
+ usesLines.add(trimmed);
28
+ } else {
29
+ stylesLines.push(line);
30
+ }
31
+ }
32
+
33
+ return [...usesLines].join("\n") + "\n" + stylesLines.join("\n");
34
+ }
35
+ }
@@ -0,0 +1,5 @@
1
+ import {merge} from "ts-deepmerge";
2
+
3
+ //configs imports;
4
+
5
+ export default merge({});
@@ -0,0 +1,26 @@
1
+ import path from "path";
2
+ import Finder from "./Finder";
3
+
4
+ import type {ReadonlyConfig} from "adnbn";
5
+ import type {FileImportInfo} from "../types";
6
+
7
+ export default class ConfigFinder extends Finder {
8
+ protected getAllowedExtensions(): string[] {
9
+ return ["tsx", "ts"];
10
+ }
11
+
12
+ constructor(fileName: string, config: ReadonlyConfig) {
13
+ super(fileName, config);
14
+ }
15
+
16
+ protected getFile(dirPath: string): FileImportInfo | undefined {
17
+ const filePath = this.resolveFileWithExtensions(dirPath, this.fileName);
18
+
19
+ if (!filePath) return;
20
+
21
+ return {
22
+ name: dirPath.replaceAll(path.sep, "").replaceAll("-", ""),
23
+ import: this.toImportPath(filePath),
24
+ };
25
+ }
26
+ }
@@ -0,0 +1,76 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ import type {ReadonlyConfig} from "adnbn";
5
+
6
+ import type {FileImportInfo} from "../types";
7
+
8
+ export interface FinderOptions {
9
+ searchDirs: string[];
10
+ fileName: string;
11
+ canMerge: boolean;
12
+ }
13
+
14
+ export default abstract class Finder {
15
+ protected abstract getAllowedExtensions(): string[];
16
+
17
+ protected abstract getFile(dirPath: string): FileImportInfo | undefined;
18
+
19
+ protected searchDirs: string[] = [];
20
+ protected canMerge: boolean = true;
21
+
22
+ protected constructor(
23
+ protected readonly fileName: string,
24
+ protected readonly config: ReadonlyConfig
25
+ ) {}
26
+
27
+ public setCanMerge(canMerge: boolean): this {
28
+ this.canMerge = canMerge;
29
+ return this;
30
+ }
31
+
32
+ public setSearchDirs(searchDirs: string[]): this {
33
+ this.searchDirs = searchDirs;
34
+ return this;
35
+ }
36
+
37
+ public getFiles(): FileImportInfo[] {
38
+ const files: FileImportInfo[] = [];
39
+
40
+ let isFound = false;
41
+
42
+ this.searchDirs.forEach(dirPath => {
43
+ const file = this.getFile(dirPath);
44
+
45
+ if (file && (this.canMerge || !isFound)) {
46
+ files.push(file);
47
+ isFound = true;
48
+ }
49
+ });
50
+
51
+ return files;
52
+ }
53
+
54
+ protected resolveFileWithExtensions(basePath: string, fileName: string): string | undefined {
55
+ const extname = path.extname(fileName);
56
+
57
+ const baseName = this.getAllowedExtensions().includes(extname.slice(1))
58
+ ? path.basename(fileName, extname)
59
+ : fileName;
60
+
61
+ for (const ext of this.getAllowedExtensions()) {
62
+ const fullPath = path.resolve(basePath, `${baseName}.${ext}`);
63
+ if (fs.existsSync(fullPath)) {
64
+ return fullPath;
65
+ }
66
+ }
67
+
68
+ return undefined;
69
+ }
70
+
71
+ protected toImportPath(fullPath: string, withExt: boolean = false): string {
72
+ const importPath = path.relative(this.config.rootDir, fullPath).split(path.sep).join("/");
73
+
74
+ return withExt ? importPath : importPath.replace(path.extname(importPath), "");
75
+ }
76
+ }
@@ -0,0 +1,23 @@
1
+ import type {ReadonlyConfig} from "adnbn";
2
+
3
+ import Finder from "./Finder";
4
+
5
+ import type {FileImportInfo} from "../types";
6
+
7
+ export default class StyleFinder extends Finder {
8
+ protected getAllowedExtensions(): string[] {
9
+ return ["scss", "css"];
10
+ }
11
+
12
+ constructor(fileName: string, config: ReadonlyConfig) {
13
+ super(fileName, config);
14
+ }
15
+
16
+ protected getFile(dirPath: string): FileImportInfo | undefined {
17
+ const filePath = this.resolveFileWithExtensions(dirPath, this.fileName);
18
+
19
+ if (!filePath) return;
20
+
21
+ return {name: "", import: this.toImportPath(filePath, true)};
22
+ }
23
+ }
@@ -0,0 +1,73 @@
1
+ import path from "path";
2
+ import {definePlugin} from "adnbn";
3
+ import {Configuration as Rspack} from "@rspack/core";
4
+ import {RspackVirtualModulePlugin} from "rspack-plugin-virtual-module";
5
+
6
+ import StyleBuilder from "./builder/StyleBuilder";
7
+ import ConfigBuilder from "./builder/ConfigBuilder";
8
+
9
+ import Finder from "./finder/Finder";
10
+ import StyleFinder from "./finder/StyleFinder";
11
+ import ConfigFinder from "./finder/ConfigFinder";
12
+
13
+ import type {BuilderContract} from "./types";
14
+
15
+ export interface PluginOptions {
16
+ themeDir?: string;
17
+ configFileName?: string;
18
+ styleFileName?: string;
19
+ mergeConfig?: boolean;
20
+ mergeStyles?: boolean;
21
+ }
22
+
23
+ export default definePlugin((options: PluginOptions = {}) => {
24
+ const {
25
+ themeDir = ".",
26
+ configFileName = "ui.config",
27
+ styleFileName = "ui.style",
28
+ mergeConfig = true,
29
+ mergeStyles = true,
30
+ } = options;
31
+
32
+ let configFinder: Finder;
33
+ let styleFinder: Finder;
34
+
35
+ let configBuilder: BuilderContract;
36
+ let styleBuilder: BuilderContract;
37
+
38
+ return {
39
+ name: "adnbn-ui",
40
+ startup: ({config}) => {
41
+ const {srcDir, appsDir, sharedDir, app, appSrcDir} = config;
42
+ const normalizeThemeDir = path.normalize(themeDir).split(path.sep);
43
+
44
+ // Elements should be arranged in descending order of priority
45
+ const searchDirs = [
46
+ path.join(srcDir, appsDir, app, appSrcDir, ...normalizeThemeDir),
47
+ path.join(srcDir, sharedDir, ...normalizeThemeDir),
48
+ ];
49
+
50
+ configFinder = new ConfigFinder(configFileName, config).setCanMerge(mergeConfig).setSearchDirs(searchDirs);
51
+ styleFinder = new StyleFinder(styleFileName, config).setCanMerge(mergeStyles).setSearchDirs(searchDirs);
52
+
53
+ configBuilder = new ConfigBuilder(configFinder);
54
+ styleBuilder = new StyleBuilder(styleFinder);
55
+ },
56
+ bundler: () => {
57
+ return {
58
+ plugins: [
59
+ new RspackVirtualModulePlugin(
60
+ {
61
+ "addon-ui-config": configBuilder.build(),
62
+ "addon-ui-style.scss": styleBuilder.build(),
63
+ },
64
+ "addon-ui-virtual"
65
+ ),
66
+ ],
67
+ } satisfies Rspack;
68
+ },
69
+ manifest: ({manifest}) => {
70
+ manifest.addPermission("storage");
71
+ },
72
+ };
73
+ });
@@ -0,0 +1,8 @@
1
+ export interface FileImportInfo {
2
+ name: string;
3
+ import: string;
4
+ }
5
+
6
+ export interface BuilderContract {
7
+ build(): string;
8
+ }
@@ -0,0 +1,13 @@
1
+ import React, {FC, PropsWithChildren} from "react";
2
+
3
+ import {ExtraContext} from "./context";
4
+
5
+ import {Config} from "../../types/config";
6
+
7
+ const ExtraProvider: FC<PropsWithChildren<Pick<Config, "extra">>> = ({children, extra}) => {
8
+ return <ExtraContext.Provider value={{extra}}>{children}</ExtraContext.Provider>;
9
+ };
10
+
11
+ ExtraProvider.displayName = "ExtraProvider";
12
+
13
+ export default ExtraProvider;
@@ -0,0 +1,14 @@
1
+ import {createContext, useContext} from "react";
2
+ import {ExtraProps} from "../../types/config";
3
+
4
+ export interface ExtraContract {
5
+ extra: ExtraProps;
6
+ }
7
+
8
+ export const ExtraContext = createContext<ExtraContract>({
9
+ extra: {},
10
+ });
11
+
12
+ ExtraContext.displayName = "ExtraContext";
13
+
14
+ export const useExtra = () => useContext(ExtraContext).extra;
@@ -0,0 +1,2 @@
1
+ export {default as ExtraProvider} from "./ExtraProvider";
2
+ export {useExtra} from "./context";
@@ -0,0 +1,35 @@
1
+ import React, {FC, PropsWithChildren, useCallback, useMemo, useState} from "react";
2
+
3
+ import {IconsContext} from "./context";
4
+ import {SvgSprite} from "../../components";
5
+
6
+ import {Config, Icons} from "../../types/config";
7
+
8
+ const IconsProvider: FC<PropsWithChildren<Pick<Config, "icons">>> = ({children, icons}) => {
9
+ const [registeredIconNames, setRegisteredIconNames] = useState<string[]>([]);
10
+
11
+ const registerIcon = useCallback((name: string) => {
12
+ setRegisteredIconNames(prev => (prev.includes(name) ? prev : [...prev, name]));
13
+ }, []);
14
+
15
+ const registeredIcons = useMemo(() => {
16
+ return registeredIconNames.reduce((acc, key) => {
17
+ if (key in icons) {
18
+ acc[key] = icons[key];
19
+ }
20
+
21
+ return acc;
22
+ }, {} as Icons);
23
+ }, [icons, registeredIconNames]);
24
+
25
+ return (
26
+ <IconsContext.Provider value={{icons, registeredIconNames, registerIcon}}>
27
+ {children}
28
+ <SvgSprite icons={registeredIcons} />
29
+ </IconsContext.Provider>
30
+ );
31
+ };
32
+
33
+ IconsProvider.displayName = "IconsProvider";
34
+
35
+ export default IconsProvider;
@@ -0,0 +1,20 @@
1
+ import {createContext, useContext} from "react";
2
+ import {Icons} from "../../types/config";
3
+
4
+ export interface IconsContract {
5
+ icons: Icons;
6
+ registeredIconNames: string[];
7
+ registerIcon: (name: string) => void;
8
+ }
9
+
10
+ export const IconsContext = createContext<IconsContract>({
11
+ icons: {},
12
+
13
+ registeredIconNames: [],
14
+
15
+ registerIcon: () => {},
16
+ });
17
+
18
+ IconsContext.displayName = "IconsContext";
19
+
20
+ export const useIcons = () => useContext(IconsContext);
@@ -0,0 +1,2 @@
1
+ export {default as IconsProvider} from "./IconsProvider";
2
+ export {useIcons} from "./context";
@@ -0,0 +1,4 @@
1
+ export * from "./ui";
2
+ export * from "./theme";
3
+ export * from "./icons";
4
+ export * from "./extra";
@@ -0,0 +1,60 @@
1
+ import React, {FC, PropsWithChildren, useCallback, useEffect, useState} from "react";
2
+
3
+ import {ThemeContext} from "./context";
4
+
5
+ import {Theme, ThemeStorageContract} from "../../types/theme";
6
+ import {Config} from "../../types/config";
7
+
8
+ const isDarkMedia = () => window?.matchMedia("(prefers-color-scheme: dark)")?.matches;
9
+
10
+ const isValid = (theme: Theme | undefined): theme is Theme => {
11
+ return !!theme && [Theme.Light, Theme.Dark].includes(theme);
12
+ };
13
+
14
+ export interface ThemeProviderProps extends Pick<Config, "components"> {
15
+ storage?: ThemeStorageContract;
16
+ }
17
+
18
+ const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({children, components, storage}) => {
19
+ const [theme, setTheme] = useState<Theme>(() => (isDarkMedia() ? Theme.Dark : Theme.Light));
20
+
21
+ const changeTheme = useCallback(
22
+ (theme: Theme) => {
23
+ if (storage) {
24
+ storage.change(theme).catch(e => console.error("ThemeProvider: set theme to storage error", e));
25
+ } else {
26
+ setTheme(theme);
27
+ }
28
+ },
29
+ [storage]
30
+ );
31
+
32
+ const toggleTheme = useCallback(() => {
33
+ changeTheme(theme === Theme.Dark ? Theme.Light : Theme.Dark);
34
+ }, [theme, changeTheme]);
35
+
36
+ useEffect(() => {
37
+ if (!storage) return;
38
+
39
+ storage
40
+ .get()
41
+ .then(newTheme => isValid(newTheme) && setTheme(newTheme))
42
+ .catch(e => console.error("ThemeProvider: get theme from storage error", e));
43
+
44
+ const unsubscribe = storage.watch(newTheme => isValid(newTheme) && setTheme(newTheme));
45
+
46
+ return () => unsubscribe();
47
+ }, [storage]);
48
+
49
+ useEffect(() => {
50
+ document.querySelector("html")?.setAttribute("theme", theme);
51
+ }, [theme]);
52
+
53
+ return (
54
+ <ThemeContext.Provider value={{theme, changeTheme, toggleTheme, components}}>{children}</ThemeContext.Provider>
55
+ );
56
+ };
57
+
58
+ ThemeProvider.displayName = "ThemeProvider";
59
+
60
+ export default ThemeProvider;
@@ -0,0 +1,36 @@
1
+ import {Storage} from "adnbn/storage";
2
+
3
+ import {Theme, ThemeStorageContract} from "../../types/theme";
4
+
5
+ export default class implements ThemeStorageContract {
6
+ private readonly storage = new Storage<Record<string, Theme>>({
7
+ area: "local",
8
+ namespace: "adnbn-ui",
9
+ });
10
+
11
+ private readonly key = "theme";
12
+
13
+ public async get(): Promise<Theme | undefined> {
14
+ return await this.storage.get(this.key);
15
+ }
16
+
17
+ public async change(theme: Theme): Promise<void> {
18
+ return this.storage.set(this.key, theme);
19
+ }
20
+
21
+ public async toggle(): Promise<void> {
22
+ let theme = await this.get();
23
+
24
+ if (!theme) {
25
+ theme = Theme.Dark;
26
+ }
27
+
28
+ await this.change(theme === Theme.Dark ? Theme.Light : Theme.Dark);
29
+ }
30
+
31
+ public watch(callback: (theme: Theme) => void): () => void {
32
+ return this.storage.watch({
33
+ [this.key]: newValue => newValue && callback(newValue),
34
+ });
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ import {createContext, useContext} from "react";
2
+ import {Theme} from "../../types/theme";
3
+ import {ComponentsProps} from "../../types/config";
4
+
5
+ export interface ThemeContract {
6
+ theme: Theme;
7
+
8
+ components: ComponentsProps;
9
+
10
+ changeTheme(theme: Theme): void;
11
+
12
+ toggleTheme(): void;
13
+ }
14
+
15
+ export const ThemeContext = createContext<ThemeContract>({
16
+ theme: Theme.Light,
17
+ components: {},
18
+ changeTheme: () => {},
19
+ toggleTheme: () => {},
20
+ });
21
+
22
+ ThemeContext.displayName = "ThemeContext";
23
+
24
+ export const useTheme = () => useContext(ThemeContext);
25
+
26
+ export const useComponentProps = <K extends keyof ComponentsProps>(key: K): ComponentsProps[K] => {
27
+ const {components} = useTheme();
28
+
29
+ return components[key];
30
+ };
@@ -0,0 +1,3 @@
1
+ export {default as ThemeProvider} from "./ThemeProvider";
2
+ export {default as ThemeStorage} from "./ThemeStorage";
3
+ export {useTheme, useComponentProps} from "./context";
@@ -0,0 +1,41 @@
1
+ import React, {FC, PropsWithChildren, useMemo, useRef} from "react";
2
+ import {merge} from "ts-deepmerge";
3
+
4
+ import {ExtraProvider, IconsProvider, ThemeProvider, ThemeStorage} from "../index";
5
+
6
+ import {ThemeStorageContract} from "../../types/theme";
7
+ import {ComponentsProps, Config, ExtraProps, Icons} from "../../types/config";
8
+
9
+ import "./styles/default.scss";
10
+ import "./styles/reset.scss";
11
+ import "addon-ui-style.scss";
12
+
13
+ import config from "addon-ui-config";
14
+
15
+ export type UIProviderProps = Partial<Config>;
16
+
17
+ const UIProvider: FC<PropsWithChildren<UIProviderProps>> = ({children, components = {}, extra = {}, icons = {}}) => {
18
+ const storageRef = useRef<ThemeStorageContract | null>(null);
19
+
20
+ if (!storageRef.current) {
21
+ storageRef.current = new ThemeStorage();
22
+ }
23
+
24
+ const componentsProps = useMemo<ComponentsProps>(() => merge(config.components || {}, components), [components]);
25
+
26
+ const extraProps = useMemo<ExtraProps>(() => merge(config.extra || {}, extra), [extra]);
27
+
28
+ const svgIcons = useMemo<Icons>(() => merge(config.icons || {}, icons), [icons]);
29
+
30
+ return (
31
+ <ThemeProvider components={componentsProps} storage={storageRef.current}>
32
+ <ExtraProvider extra={extraProps}>
33
+ <IconsProvider icons={svgIcons}>{children}</IconsProvider>
34
+ </ExtraProvider>
35
+ </ThemeProvider>
36
+ );
37
+ };
38
+
39
+ UIProvider.displayName = "UIProvider";
40
+
41
+ export default UIProvider;
@@ -0,0 +1 @@
1
+ export {default as UIProvider, type UIProviderProps} from "./UIProvider";