@iobroker/gui-components 10.3.15 → 10.4.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 (62) hide show
  1. package/README.md +21 -1
  2. package/build/Components/AnimatedIcons/IconCopyToClipboard.d.ts +25 -0
  3. package/build/Components/AnimatedIcons/IconCopyToClipboard.js +84 -0
  4. package/build/Components/AnimatedIcons/IconCopyToClipboard.js.map +1 -0
  5. package/build/Components/AnimatedIcons/IconFilter.d.ts +23 -0
  6. package/build/Components/AnimatedIcons/IconFilter.js +26 -0
  7. package/build/Components/AnimatedIcons/IconFilter.js.map +1 -0
  8. package/build/Components/AnimatedIcons/IconFolder.d.ts +6 -0
  9. package/build/Components/AnimatedIcons/IconFolder.js +2 -1
  10. package/build/Components/AnimatedIcons/IconFolder.js.map +1 -1
  11. package/build/Components/AnimatedIcons/IconThemeMode.d.ts +22 -0
  12. package/build/Components/AnimatedIcons/IconThemeMode.js +32 -0
  13. package/build/Components/AnimatedIcons/IconThemeMode.js.map +1 -0
  14. package/build/Components/AnimatedIcons/IconUpload.d.ts +24 -0
  15. package/build/Components/AnimatedIcons/IconUpload.js +37 -0
  16. package/build/Components/AnimatedIcons/IconUpload.js.map +1 -0
  17. package/build/Components/AnimatedIcons/common.d.ts +2 -0
  18. package/build/Components/AnimatedIcons/common.js +64 -5
  19. package/build/Components/AnimatedIcons/common.js.map +1 -1
  20. package/build/Components/ColorPicker.js +4 -3
  21. package/build/Components/ColorPicker.js.map +1 -1
  22. package/build/Components/FileBrowser.d.ts +6 -0
  23. package/build/Components/FileBrowser.js +54 -12
  24. package/build/Components/FileBrowser.js.map +1 -1
  25. package/build/Components/FileViewer.js +3 -2
  26. package/build/Components/FileViewer.js.map +1 -1
  27. package/build/Components/ObjectBrowser/ObjectBrowserClass.d.ts +15 -0
  28. package/build/Components/ObjectBrowser/ObjectBrowserClass.js +134 -6
  29. package/build/Components/ObjectBrowser/ObjectBrowserClass.js.map +1 -1
  30. package/build/Components/ObjectBrowser/contextMenu.js +3 -2
  31. package/build/Components/ObjectBrowser/contextMenu.js.map +1 -1
  32. package/build/Components/ObjectBrowser/renderLeaf.js +78 -59
  33. package/build/Components/ObjectBrowser/renderLeaf.js.map +1 -1
  34. package/build/Components/ObjectBrowser/rowAnimation.d.ts +25 -1
  35. package/build/Components/ObjectBrowser/rowAnimation.js +46 -19
  36. package/build/Components/ObjectBrowser/rowAnimation.js.map +1 -1
  37. package/build/Components/ObjectBrowser/skeleton.d.ts +15 -0
  38. package/build/Components/ObjectBrowser/skeleton.js +106 -0
  39. package/build/Components/ObjectBrowser/skeleton.js.map +1 -0
  40. package/build/Components/ObjectBrowser/styles.js +16 -11
  41. package/build/Components/ObjectBrowser/styles.js.map +1 -1
  42. package/build/Components/ObjectBrowser/toolbar.js +18 -14
  43. package/build/Components/ObjectBrowser/toolbar.js.map +1 -1
  44. package/build/Components/ObjectBrowser/utils.d.ts +4 -0
  45. package/build/Components/ObjectBrowser/utils.js +4 -0
  46. package/build/Components/ObjectBrowser/utils.js.map +1 -1
  47. package/build/Components/SaveCloseButtons.d.ts +12 -1
  48. package/build/Components/SaveCloseButtons.js +38 -2
  49. package/build/Components/SaveCloseButtons.js.map +1 -1
  50. package/build/Components/ToggleThemeMenu.js +11 -8
  51. package/build/Components/ToggleThemeMenu.js.map +1 -1
  52. package/build/Components/TreeTable.d.ts +16 -0
  53. package/build/Components/TreeTable.js +106 -18
  54. package/build/Components/TreeTable.js.map +1 -1
  55. package/build/Dialogs/ComplexCron.js +3 -2
  56. package/build/Dialogs/ComplexCron.js.map +1 -1
  57. package/build/Dialogs/Error.js +3 -2
  58. package/build/Dialogs/Error.js.map +1 -1
  59. package/build/index.d.ts +4 -0
  60. package/build/index.js +4 -0
  61. package/build/index.js.map +1 -1
  62. package/package.json +5 -5
package/README.md CHANGED
@@ -36,7 +36,7 @@ npm create vite@latest src -- --template react-ts
36
36
  "dependencies": {
37
37
  "@emotion/react": "^11.14.0",
38
38
  "@emotion/styled": "^11.14.1",
39
- "@iobroker/gui-components": "^10.3.15",
39
+ "@iobroker/gui-components": "^10.4.0",
40
40
  "@mui/icons-material": "^9.0.1",
41
41
  "@mui/material": "^9.0.1",
42
42
  "react": "^19.2.5",
@@ -877,6 +877,26 @@ You can find the migration instructions:
877
877
  -->
878
878
 
879
879
  ## Changelog
880
+ ### 10.4.0 (2026-11-04)
881
+ - (@GermanBluefox) Changed: the animated trash can is used for all delete buttons: file browser, `TreeTable`, `ColorPicker`, `DialogComplexCron` and the context menu of the object browser. The lid stays open while the delete confirmation is shown
882
+ - (@GermanBluefox) Changed: file browser: the reload button turns while the files are read
883
+ - (@GermanBluefox) Changed: object browser: the online/offline symbol of the devices is animated, when the state changes
884
+ - (@GermanBluefox) Changed: `DialogError` draws its error symbol
885
+ - (@GermanBluefox) Added: animated icons `IconCopyToClipboard` (check mark after copying), `IconThemeMode` (sun and moon turn into each other), `IconUpload` and `IconFilter`; `IconFolder` got the prop `toggleOnHover`
886
+ - (@GermanBluefox) Changed: object browser: the copy buttons confirm the copy with a check mark, also in `FileViewer`
887
+ - (@GermanBluefox) Changed: object browser: a command (`ack=false`) that the adapter confirms (`ack=true`) gets a check mark next to the value for a moment
888
+ - (@GermanBluefox) Changed: `ToggleThemeMenu` animates the change of the theme
889
+ - (@GermanBluefox) Changed: file browser: the upload arrow moves up while the files are uploaded, the drop area pulses while files are dragged over it
890
+ - (@GermanBluefox) Changed: object browser: the buttons "expand/collapse all" show on hover, what they will do; the import and export buttons have the animated upload symbol
891
+ - (@GermanBluefox) Changed: `TreeTable`: the children slide out from under their row, like in the object browser
892
+ - (@GermanBluefox) Changed: object browser: placeholder rows with a shimmer while the objects are loading, instead of a progress bar
893
+ - (@GermanBluefox) Fixed: object browser: the button "clear filter" in the header of the value column was shown when no filter was set, and hidden when one was set. Now its cross jumps away when the filter is cleared
894
+ - (@GermanBluefox) Fixed: object browser under web: the objects of the types `config`, `user` and `group` were listed under their name instead of their ID, and `config` objects outside of `system.` were missing. With `root`, only the objects below it are read, and the way down to it is expanded
895
+
896
+ ### 10.3.16 (2026-11-04)
897
+ - (@GermanBluefox) Added: object browser shows an arrow up/down next to a changed number; a changed boolean flips
898
+ - (@GermanBluefox) Added: `SaveCloseButtons` shows a check mark after saving
899
+
880
900
  ### 10.3.15 (2026-11-03)
881
901
  - (@GermanBluefox) Added: animated icons `IconPlayPause`, `IconStatusLed`, `IconResult`, `IconRefresh`, `IconBell`, `IconMenuArrow`, `IconTrash`, `IconConnectionState`, `IconExpertMode` and `IconFolder` (CSS/SVG)
882
902
  - (@GermanBluefox) Changed: the object browser uses the animated reload and delete icons
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { type AnimatedIconProps } from './common';
8
+ export interface IconCopyToClipboardProps extends AnimatedIconProps {
9
+ /** Every change of this value shows the check mark, e.g. a counter of the copies */
10
+ copied?: string | number | boolean | null;
11
+ /** Click on the icon, e.g. to copy the text. The check mark is shown after it. */
12
+ onClick?: (e: React.MouseEvent<HTMLSpanElement>) => void;
13
+ }
14
+ /**
15
+ * Copy symbol, which confirms the copy: the copy slides out of the original, and a check mark is drawn in it.
16
+ *
17
+ * The check mark is shown after a click on the icon (with `onClick`), on every change of `copied`, or - if
18
+ * neither is given - after a click on the button around the icon, e.g. on a `Button` with this icon as `startIcon`.
19
+ *
20
+ * ```tsx
21
+ * <IconCopyToClipboard onClick={() => Utils.copyToClipboard(text)} />
22
+ * <Button startIcon={<IconCopyToClipboard />} onClick={() => Utils.copyToClipboard(text)}>Copy</Button>
23
+ * ```
24
+ */
25
+ export declare function IconCopyToClipboard(props: IconCopyToClipboardProps): React.JSX.Element;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React, { useEffect, useRef } from 'react';
7
+ import { prefersReducedMotion, rootProps, useAnimatedIconStyles, useChange } from './common';
8
+ const DURATION = 1600;
9
+ /**
10
+ * Copy symbol, which confirms the copy: the copy slides out of the original, and a check mark is drawn in it.
11
+ *
12
+ * The check mark is shown after a click on the icon (with `onClick`), on every change of `copied`, or - if
13
+ * neither is given - after a click on the button around the icon, e.g. on a `Button` with this icon as `startIcon`.
14
+ *
15
+ * ```tsx
16
+ * <IconCopyToClipboard onClick={() => Utils.copyToClipboard(text)} />
17
+ * <Button startIcon={<IconCopyToClipboard />} onClick={() => Utils.copyToClipboard(text)}>Copy</Button>
18
+ * ```
19
+ */
20
+ export function IconCopyToClipboard(props) {
21
+ useAnimatedIconStyles();
22
+ const root = useRef(null);
23
+ const back = useRef(null);
24
+ const front = useRef(null);
25
+ const check = useRef(null);
26
+ const animations = useRef([]);
27
+ const showCopied = () => {
28
+ animations.current.forEach(a => a?.cancel());
29
+ if (!check.current?.animate) {
30
+ return;
31
+ }
32
+ // with less motion, the check mark only fades in and out
33
+ const reduced = prefersReducedMotion();
34
+ const draw = reduced ? 0 : 1;
35
+ animations.current = [
36
+ reduced
37
+ ? undefined
38
+ : front.current?.animate([{ transform: 'translate(-5px, -5px)' }, { transform: 'translate(0, 0)' }], {
39
+ duration: 320,
40
+ easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)',
41
+ }),
42
+ back.current?.animate([{ opacity: 1 }, { opacity: 0, offset: 0.15 }, { opacity: 0, offset: 0.85 }, { opacity: 1 }], { duration: DURATION }),
43
+ check.current.animate([
44
+ { opacity: 0, strokeDashoffset: draw },
45
+ { opacity: 1, strokeDashoffset: draw, offset: 0.12 },
46
+ { opacity: 1, strokeDashoffset: 0, offset: 0.3 },
47
+ { opacity: 1, strokeDashoffset: 0, offset: 0.85 },
48
+ { opacity: 0, strokeDashoffset: 0 },
49
+ ], { duration: DURATION }),
50
+ ];
51
+ };
52
+ const showCopiedRef = useRef(showCopied);
53
+ useEffect(() => {
54
+ showCopiedRef.current = showCopied;
55
+ });
56
+ useChange(props.copied, showCopied);
57
+ // neither own click nor `copied`: the click on the button around the icon copies
58
+ const listenToButton = !props.onClick && props.copied === undefined;
59
+ useEffect(() => {
60
+ if (!listenToButton) {
61
+ return undefined;
62
+ }
63
+ const button = root.current?.parentElement?.closest('button, a, [role="button"], [role="menuitem"]');
64
+ if (!button) {
65
+ return undefined;
66
+ }
67
+ const onClick = () => showCopiedRef.current();
68
+ button.addEventListener('click', onClick);
69
+ return () => button.removeEventListener('click', onClick);
70
+ }, [listenToButton]);
71
+ useEffect(() => () => animations.current.forEach(a => a?.cancel()), []);
72
+ const { onClick } = props;
73
+ return (React.createElement("span", { ...rootProps(props, `iob-ani-copy${onClick ? ' iob-ani-click' : ''}`), ref: root, onClick: onClick
74
+ ? e => {
75
+ onClick(e);
76
+ showCopied();
77
+ }
78
+ : undefined },
79
+ React.createElement("svg", { viewBox: "0 0 24 24" },
80
+ React.createElement("path", { ref: back, d: "M6.5 16H5.5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" }),
81
+ React.createElement("path", { ref: front, d: "M10.5 8h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2z" }),
82
+ React.createElement("path", { ref: check, className: "iob-ani-copy-check", d: "M11.5 14.6l2.2 2.2 4-4.4", pathLength: 1 }))));
83
+ }
84
+ //# sourceMappingURL=IconCopyToClipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IconCopyToClipboard.js","sourceRoot":"./src/","sources":["Components/AnimatedIcons/IconCopyToClipboard.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>\n *\n * MIT License\n */\nimport React, { useEffect, useRef } from 'react';\nimport { type AnimatedIconProps, prefersReducedMotion, rootProps, useAnimatedIconStyles, useChange } from './common';\n\nconst DURATION = 1600;\n\nexport interface IconCopyToClipboardProps extends AnimatedIconProps {\n /** Every change of this value shows the check mark, e.g. a counter of the copies */\n copied?: string | number | boolean | null;\n /** Click on the icon, e.g. to copy the text. The check mark is shown after it. */\n onClick?: (e: React.MouseEvent<HTMLSpanElement>) => void;\n}\n\n/**\n * Copy symbol, which confirms the copy: the copy slides out of the original, and a check mark is drawn in it.\n *\n * The check mark is shown after a click on the icon (with `onClick`), on every change of `copied`, or - if\n * neither is given - after a click on the button around the icon, e.g. on a `Button` with this icon as `startIcon`.\n *\n * ```tsx\n * <IconCopyToClipboard onClick={() => Utils.copyToClipboard(text)} />\n * <Button startIcon={<IconCopyToClipboard />} onClick={() => Utils.copyToClipboard(text)}>Copy</Button>\n * ```\n */\nexport function IconCopyToClipboard(props: IconCopyToClipboardProps): React.JSX.Element {\n useAnimatedIconStyles();\n const root = useRef<HTMLSpanElement>(null);\n const back = useRef<SVGPathElement>(null);\n const front = useRef<SVGPathElement>(null);\n const check = useRef<SVGPathElement>(null);\n const animations = useRef<(Animation | undefined)[]>([]);\n\n const showCopied = (): void => {\n animations.current.forEach(a => a?.cancel());\n if (!check.current?.animate) {\n return;\n }\n // with less motion, the check mark only fades in and out\n const reduced = prefersReducedMotion();\n const draw = reduced ? 0 : 1;\n animations.current = [\n reduced\n ? undefined\n : front.current?.animate([{ transform: 'translate(-5px, -5px)' }, { transform: 'translate(0, 0)' }], {\n duration: 320,\n easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)',\n }),\n back.current?.animate(\n [{ opacity: 1 }, { opacity: 0, offset: 0.15 }, { opacity: 0, offset: 0.85 }, { opacity: 1 }],\n { duration: DURATION },\n ),\n check.current.animate(\n [\n { opacity: 0, strokeDashoffset: draw },\n { opacity: 1, strokeDashoffset: draw, offset: 0.12 },\n { opacity: 1, strokeDashoffset: 0, offset: 0.3 },\n { opacity: 1, strokeDashoffset: 0, offset: 0.85 },\n { opacity: 0, strokeDashoffset: 0 },\n ],\n { duration: DURATION },\n ),\n ];\n };\n const showCopiedRef = useRef(showCopied);\n useEffect(() => {\n showCopiedRef.current = showCopied;\n });\n\n useChange(props.copied, showCopied);\n\n // neither own click nor `copied`: the click on the button around the icon copies\n const listenToButton = !props.onClick && props.copied === undefined;\n useEffect(() => {\n if (!listenToButton) {\n return undefined;\n }\n const button = root.current?.parentElement?.closest('button, a, [role=\"button\"], [role=\"menuitem\"]');\n if (!button) {\n return undefined;\n }\n const onClick = (): void => showCopiedRef.current();\n button.addEventListener('click', onClick);\n return () => button.removeEventListener('click', onClick);\n }, [listenToButton]);\n\n useEffect(() => () => animations.current.forEach(a => a?.cancel()), []);\n\n const { onClick } = props;\n return (\n <span\n {...rootProps(props, `iob-ani-copy${onClick ? ' iob-ani-click' : ''}`)}\n ref={root}\n onClick={\n onClick\n ? e => {\n onClick(e);\n showCopied();\n }\n : undefined\n }\n >\n <svg viewBox=\"0 0 24 24\">\n <path\n ref={back}\n d=\"M6.5 16H5.5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1\"\n />\n {/* a path and not a rect: a CSS `width` (e.g. `* { width: initial }`) would change a rect */}\n <path\n ref={front}\n d=\"M10.5 8h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2z\"\n />\n <path\n ref={check}\n className=\"iob-ani-copy-check\"\n d=\"M11.5 14.6l2.2 2.2 4-4.4\"\n pathLength={1}\n />\n </svg>\n </span>\n );\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { type AnimatedIconProps } from './common';
8
+ export interface IconFilterProps extends AnimatedIconProps {
9
+ /** A filter is set: a red cross next to the funnel shows that a click removes it */
10
+ active: boolean;
11
+ }
12
+ /**
13
+ * Filter symbol for "clear filter" buttons. While a filter is set, a red cross pops up next to the funnel,
14
+ * it turns on hover of the button and jumps away when the filter is cleared. The color of the cross can be
15
+ * changed with the CSS class `iob-ani-filter-x`.
16
+ *
17
+ * ```tsx
18
+ * <IconButton onClick={() => this.clearFilter()}>
19
+ * <IconFilter active={!!this.state.filter} />
20
+ * </IconButton>
21
+ * ```
22
+ */
23
+ export declare function IconFilter(props: IconFilterProps): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { rootProps, useAnimatedIconStyles } from './common';
8
+ /**
9
+ * Filter symbol for "clear filter" buttons. While a filter is set, a red cross pops up next to the funnel,
10
+ * it turns on hover of the button and jumps away when the filter is cleared. The color of the cross can be
11
+ * changed with the CSS class `iob-ani-filter-x`.
12
+ *
13
+ * ```tsx
14
+ * <IconButton onClick={() => this.clearFilter()}>
15
+ * <IconFilter active={!!this.state.filter} />
16
+ * </IconButton>
17
+ * ```
18
+ */
19
+ export function IconFilter(props) {
20
+ useAnimatedIconStyles();
21
+ return (React.createElement("span", { ...rootProps(props, `iob-ani-filter${props.active ? ' iob-ani-filter-on' : ''}`) },
22
+ React.createElement("svg", { viewBox: "0 0 24 24" },
23
+ React.createElement("path", { className: "iob-ani-filter-funnel", d: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z" }),
24
+ React.createElement("path", { className: "iob-ani-filter-x", d: "M16.75 14.5l4.5 4.5M21.25 14.5l-4.5 4.5" }))));
25
+ }
26
+ //# sourceMappingURL=IconFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IconFilter.js","sourceRoot":"./src/","sources":["Components/AnimatedIcons/IconFilter.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA0B,SAAS,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAOpF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC7C,qBAAqB,EAAE,CAAC;IACxB,OAAO,CACH,iCAAU,SAAS,CAAC,KAAK,EAAE,iBAAiB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACnF,6BAAK,OAAO,EAAC,WAAW;YAEpB,8BACI,SAAS,EAAC,uBAAuB,EACjC,CAAC,EAAC,8IAA8I,GAClJ;YACF,8BACI,SAAS,EAAC,kBAAkB,EAC5B,CAAC,EAAC,yCAAyC,GAC7C,CACA,CACH,CACV,CAAC;AACN,CAAC","sourcesContent":["/**\n * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>\n *\n * MIT License\n */\nimport React from 'react';\nimport { type AnimatedIconProps, rootProps, useAnimatedIconStyles } from './common';\n\nexport interface IconFilterProps extends AnimatedIconProps {\n /** A filter is set: a red cross next to the funnel shows that a click removes it */\n active: boolean;\n}\n\n/**\n * Filter symbol for \"clear filter\" buttons. While a filter is set, a red cross pops up next to the funnel,\n * it turns on hover of the button and jumps away when the filter is cleared. The color of the cross can be\n * changed with the CSS class `iob-ani-filter-x`.\n *\n * ```tsx\n * <IconButton onClick={() => this.clearFilter()}>\n * <IconFilter active={!!this.state.filter} />\n * </IconButton>\n * ```\n */\nexport function IconFilter(props: IconFilterProps): React.JSX.Element {\n useAnimatedIconStyles();\n return (\n <span {...rootProps(props, `iob-ani-filter${props.active ? ' iob-ani-filter-on' : ''}`)}>\n <svg viewBox=\"0 0 24 24\">\n {/* filter_alt of the Material icons, Apache 2.0, see IconClearFilter */}\n <path\n className=\"iob-ani-filter-funnel\"\n d=\"M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z\"\n />\n <path\n className=\"iob-ani-filter-x\"\n d=\"M16.75 14.5l4.5 4.5M21.25 14.5l-4.5 4.5\"\n />\n </svg>\n </span>\n );\n}\n"]}
@@ -8,6 +8,11 @@ import { type AnimatedIconProps } from './common';
8
8
  export interface IconFolderProps extends AnimatedIconProps {
9
9
  /** The folder is open: its front flap folds down */
10
10
  open: boolean;
11
+ /**
12
+ * Shows the other state while the mouse is over the icon or over its button, e.g. a closed folder opens
13
+ * on the button "expand all" and an open folder closes on "collapse all"
14
+ */
15
+ toggleOnHover?: boolean;
11
16
  /** Click on the icon, e.g. to open or close the folder */
12
17
  onClick?: (e: React.MouseEvent<HTMLSpanElement>) => void;
13
18
  }
@@ -17,6 +22,7 @@ export interface IconFolderProps extends AnimatedIconProps {
17
22
  *
18
23
  * ```tsx
19
24
  * <IconFolder open={expanded} onClick={() => toggle(id)} />
25
+ * <IconButton onClick={() => expandAll()}><IconFolder open={false} toggleOnHover /></IconButton>
20
26
  * ```
21
27
  */
22
28
  export declare function IconFolder(props: IconFolderProps): React.JSX.Element;
@@ -16,11 +16,12 @@ const FRONT = 'M631.756,292.093 l-72.424,124.155 a63.997,63.997 0 0 1 -55.281,31
16
16
  *
17
17
  * ```tsx
18
18
  * <IconFolder open={expanded} onClick={() => toggle(id)} />
19
+ * <IconButton onClick={() => expandAll()}><IconFolder open={false} toggleOnHover /></IconButton>
19
20
  * ```
20
21
  */
21
22
  export function IconFolder(props) {
22
23
  useAnimatedIconStyles();
23
- return (React.createElement("span", { ...rootProps(props, `iob-ani-folder${props.open ? ' iob-ani-folder-open' : ''}`), onClick: props.onClick },
24
+ return (React.createElement("span", { ...rootProps(props, `iob-ani-folder${props.open ? ' iob-ani-folder-open' : ''}${props.toggleOnHover ? ' iob-ani-folder-hover' : ''}`), onClick: props.onClick },
24
25
  React.createElement("svg", { viewBox: "0 0 650 512" },
25
26
  React.createElement("path", { d: BACK }),
26
27
  React.createElement("path", { className: "iob-ani-folder-front", d: FRONT }),
@@ -1 +1 @@
1
- {"version":3,"file":"IconFolder.js","sourceRoot":"./src/","sources":["Components/AnimatedIcons/IconFolder.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA0B,SAAS,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEpF,yGAAyG;AACzG,sDAAsD;AACtD,MAAM,MAAM,GACR,qKAAqK,CAAC;AAC1K,MAAM,IAAI,GACN,yLAAyL,CAAC;AAC9L,MAAM,KAAK,GACP,6NAA6N,CAAC;AASlO;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC7C,qBAAqB,EAAE,CAAC;IACxB,OAAO,CACH,iCACQ,SAAS,CAAC,KAAK,EAAE,iBAAiB,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EACjF,OAAO,EAAE,KAAK,CAAC,OAAO;QAEtB,6BAAK,OAAO,EAAC,aAAa;YACtB,8BAAM,CAAC,EAAE,IAAI,GAAI;YACjB,8BACI,SAAS,EAAC,sBAAsB,EAChC,CAAC,EAAE,KAAK,GACV;YACF,8BACI,SAAS,EAAC,uBAAuB,EACjC,CAAC,EAAE,MAAM,GACX,CACA,CACH,CACV,CAAC;AACN,CAAC","sourcesContent":["/**\n * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>\n *\n * MIT License\n */\nimport React from 'react';\nimport { type AnimatedIconProps, rootProps, useAnimatedIconStyles } from './common';\n\n// The shapes of `IconClosed` and `IconOpen` (Font Awesome, regular): the closed folder, and the open one\n// split into its back with the tab and its front flap\nconst CLOSED =\n 'M524,128 l-192,0 l-64,-64 l-160,0 c-26.51,0 -48,21.49 -48,48 l0,288 c0,26.51 21.49,48 48,48 l416,0 c26.51,0 48,-21.49 48,-48 l0,-224 c0,-26.51 -21.49,-48 -48,-48 z';\nconst BACK =\n 'M211.062,224 l328,0 l0,-48 c0,-26.51 -21.49,-48 -48,-48 l-160,0 l-64,-64 l-160,0 c-26.51,0 -48,21.49 -48,48 l0,278.046 l69.077,-118.418 c17.137,-29.378 48.912,-47.628 82.923,-47.628 z';\nconst FRONT =\n 'M631.756,292.093 l-72.424,124.155 a63.997,63.997 0 0 1 -55.281,31.752 l-399.964,0 c-18.523,0 -30.064,-20.093 -20.731,-36.093 l72.424,-124.155 a64,64 0 0 1 55.282,-31.752 l399.964,0 c18.523,0 30.064,20.093 20.73,36.093 z';\n\nexport interface IconFolderProps extends AnimatedIconProps {\n /** The folder is open: its front flap folds down */\n open: boolean;\n /** Click on the icon, e.g. to open or close the folder */\n onClick?: (e: React.MouseEvent<HTMLSpanElement>) => void;\n}\n\n/**\n * Folder that opens and closes: the front flap folds down and back up. In both end states it looks\n * like `IconClosed` and `IconOpen`.\n *\n * ```tsx\n * <IconFolder open={expanded} onClick={() => toggle(id)} />\n * ```\n */\nexport function IconFolder(props: IconFolderProps): React.JSX.Element {\n useAnimatedIconStyles();\n return (\n <span\n {...rootProps(props, `iob-ani-folder${props.open ? ' iob-ani-folder-open' : ''}`)}\n onClick={props.onClick}\n >\n <svg viewBox=\"0 0 650 512\">\n <path d={BACK} />\n <path\n className=\"iob-ani-folder-front\"\n d={FRONT}\n />\n <path\n className=\"iob-ani-folder-closed\"\n d={CLOSED}\n />\n </svg>\n </span>\n );\n}\n"]}
1
+ {"version":3,"file":"IconFolder.js","sourceRoot":"./src/","sources":["Components/AnimatedIcons/IconFolder.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA0B,SAAS,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEpF,yGAAyG;AACzG,sDAAsD;AACtD,MAAM,MAAM,GACR,qKAAqK,CAAC;AAC1K,MAAM,IAAI,GACN,yLAAyL,CAAC;AAC9L,MAAM,KAAK,GACP,6NAA6N,CAAC;AAclO;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC7C,qBAAqB,EAAE,CAAC;IACxB,OAAO,CACH,iCACQ,SAAS,CACT,KAAK,EACL,iBAAiB,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,EAAE,EAAE,CACnH,EACD,OAAO,EAAE,KAAK,CAAC,OAAO;QAEtB,6BAAK,OAAO,EAAC,aAAa;YACtB,8BAAM,CAAC,EAAE,IAAI,GAAI;YACjB,8BACI,SAAS,EAAC,sBAAsB,EAChC,CAAC,EAAE,KAAK,GACV;YACF,8BACI,SAAS,EAAC,uBAAuB,EACjC,CAAC,EAAE,MAAM,GACX,CACA,CACH,CACV,CAAC;AACN,CAAC","sourcesContent":["/**\n * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>\n *\n * MIT License\n */\nimport React from 'react';\nimport { type AnimatedIconProps, rootProps, useAnimatedIconStyles } from './common';\n\n// The shapes of `IconClosed` and `IconOpen` (Font Awesome, regular): the closed folder, and the open one\n// split into its back with the tab and its front flap\nconst CLOSED =\n 'M524,128 l-192,0 l-64,-64 l-160,0 c-26.51,0 -48,21.49 -48,48 l0,288 c0,26.51 21.49,48 48,48 l416,0 c26.51,0 48,-21.49 48,-48 l0,-224 c0,-26.51 -21.49,-48 -48,-48 z';\nconst BACK =\n 'M211.062,224 l328,0 l0,-48 c0,-26.51 -21.49,-48 -48,-48 l-160,0 l-64,-64 l-160,0 c-26.51,0 -48,21.49 -48,48 l0,278.046 l69.077,-118.418 c17.137,-29.378 48.912,-47.628 82.923,-47.628 z';\nconst FRONT =\n 'M631.756,292.093 l-72.424,124.155 a63.997,63.997 0 0 1 -55.281,31.752 l-399.964,0 c-18.523,0 -30.064,-20.093 -20.731,-36.093 l72.424,-124.155 a64,64 0 0 1 55.282,-31.752 l399.964,0 c18.523,0 30.064,20.093 20.73,36.093 z';\n\nexport interface IconFolderProps extends AnimatedIconProps {\n /** The folder is open: its front flap folds down */\n open: boolean;\n /**\n * Shows the other state while the mouse is over the icon or over its button, e.g. a closed folder opens\n * on the button \"expand all\" and an open folder closes on \"collapse all\"\n */\n toggleOnHover?: boolean;\n /** Click on the icon, e.g. to open or close the folder */\n onClick?: (e: React.MouseEvent<HTMLSpanElement>) => void;\n}\n\n/**\n * Folder that opens and closes: the front flap folds down and back up. In both end states it looks\n * like `IconClosed` and `IconOpen`.\n *\n * ```tsx\n * <IconFolder open={expanded} onClick={() => toggle(id)} />\n * <IconButton onClick={() => expandAll()}><IconFolder open={false} toggleOnHover /></IconButton>\n * ```\n */\nexport function IconFolder(props: IconFolderProps): React.JSX.Element {\n useAnimatedIconStyles();\n return (\n <span\n {...rootProps(\n props,\n `iob-ani-folder${props.open ? ' iob-ani-folder-open' : ''}${props.toggleOnHover ? ' iob-ani-folder-hover' : ''}`,\n )}\n onClick={props.onClick}\n >\n <svg viewBox=\"0 0 650 512\">\n <path d={BACK} />\n <path\n className=\"iob-ani-folder-front\"\n d={FRONT}\n />\n <path\n className=\"iob-ani-folder-closed\"\n d={CLOSED}\n />\n </svg>\n </span>\n );\n}\n"]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { type AnimatedIconProps } from './common';
8
+ export interface IconThemeModeProps extends AnimatedIconProps {
9
+ /** light: sun, blue: sun as a ring, colored: half filled sun, dark: moon */
10
+ mode: 'light' | 'blue' | 'colored' | 'dark';
11
+ }
12
+ /**
13
+ * Symbol of the color theme. The sun and the moon turn into each other: the rays draw back and the shadow
14
+ * slides over the sun until only the crescent is left. Between the light themes the rays spin.
15
+ *
16
+ * ```tsx
17
+ * <IconButton onClick={() => this.toggleTheme()}>
18
+ * <IconThemeMode mode={this.state.themeType === 'dark' ? 'dark' : 'light'} />
19
+ * </IconButton>
20
+ * ```
21
+ */
22
+ export declare function IconThemeMode(props: IconThemeModeProps): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { rootProps, useAnimatedIconStyles, useSvgId } from './common';
8
+ /**
9
+ * Symbol of the color theme. The sun and the moon turn into each other: the rays draw back and the shadow
10
+ * slides over the sun until only the crescent is left. Between the light themes the rays spin.
11
+ *
12
+ * ```tsx
13
+ * <IconButton onClick={() => this.toggleTheme()}>
14
+ * <IconThemeMode mode={this.state.themeType === 'dark' ? 'dark' : 'light'} />
15
+ * </IconButton>
16
+ * ```
17
+ */
18
+ export function IconThemeMode(props) {
19
+ useAnimatedIconStyles();
20
+ const maskId = useSvgId('theme');
21
+ return (React.createElement("span", { ...rootProps(props, `iob-ani-theme iob-ani-theme-${props.mode}`) },
22
+ React.createElement("svg", { viewBox: "0 0 24 24" },
23
+ React.createElement("defs", null,
24
+ React.createElement("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: "-4", y: "-4", width: "32", height: "32" },
25
+ React.createElement("path", { d: "M-4 -4h32v32h-32z", fill: "#fff" }),
26
+ React.createElement("circle", { className: "iob-ani-theme-cut", cx: "18", cy: "6", r: "7", fill: "#000" }))),
27
+ React.createElement("g", { mask: `url(#${maskId})` },
28
+ React.createElement("circle", { className: "iob-ani-theme-disc", cx: "12", cy: "12", r: "5" }),
29
+ React.createElement("path", { className: "iob-ani-theme-half", d: "M12 7a5 5 0 0 0 0 10z" })),
30
+ React.createElement("path", { className: "iob-ani-theme-rays", d: "M20.5 12h2M18 18l1.4 1.4M12 20.5v2M6 18l-1.4 1.4M3.5 12h-2M6 6L4.6 4.6M12 3.5v-2M18 6l1.4-1.4" }))));
31
+ }
32
+ //# sourceMappingURL=IconThemeMode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IconThemeMode.js","sourceRoot":"./src/","sources":["Components/AnimatedIcons/IconThemeMode.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA0B,SAAS,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAO9F;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACnD,qBAAqB,EAAE,CAAC;IACxB,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,CACH,iCAAU,SAAS,CAAC,KAAK,EAAE,+BAA+B,KAAK,CAAC,IAAI,EAAE,CAAC;QACnE,6BAAK,OAAO,EAAC,WAAW;YACpB;gBACI,8BACI,EAAE,EAAE,MAAM,EACV,SAAS,EAAC,gBAAgB,EAC1B,CAAC,EAAC,IAAI,EACN,CAAC,EAAC,IAAI,EACN,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI;oBAGX,8BACI,CAAC,EAAC,mBAAmB,EACrB,IAAI,EAAC,MAAM,GACb;oBAEF,gCACI,SAAS,EAAC,mBAAmB,EAC7B,EAAE,EAAC,IAAI,EACP,EAAE,EAAC,GAAG,EACN,CAAC,EAAC,GAAG,EACL,IAAI,EAAC,MAAM,GACb,CACC,CACJ;YACP,2BAAG,IAAI,EAAE,QAAQ,MAAM,GAAG;gBACtB,gCACI,SAAS,EAAC,oBAAoB,EAC9B,EAAE,EAAC,IAAI,EACP,EAAE,EAAC,IAAI,EACP,CAAC,EAAC,GAAG,GACP;gBACF,8BACI,SAAS,EAAC,oBAAoB,EAC9B,CAAC,EAAC,uBAAuB,GAC3B,CACF;YACJ,8BACI,SAAS,EAAC,oBAAoB,EAC9B,CAAC,EAAC,+FAA+F,GACnG,CACA,CACH,CACV,CAAC;AACN,CAAC","sourcesContent":["/**\n * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>\n *\n * MIT License\n */\nimport React from 'react';\nimport { type AnimatedIconProps, rootProps, useAnimatedIconStyles, useSvgId } from './common';\n\nexport interface IconThemeModeProps extends AnimatedIconProps {\n /** light: sun, blue: sun as a ring, colored: half filled sun, dark: moon */\n mode: 'light' | 'blue' | 'colored' | 'dark';\n}\n\n/**\n * Symbol of the color theme. The sun and the moon turn into each other: the rays draw back and the shadow\n * slides over the sun until only the crescent is left. Between the light themes the rays spin.\n *\n * ```tsx\n * <IconButton onClick={() => this.toggleTheme()}>\n * <IconThemeMode mode={this.state.themeType === 'dark' ? 'dark' : 'light'} />\n * </IconButton>\n * ```\n */\nexport function IconThemeMode(props: IconThemeModeProps): React.JSX.Element {\n useAnimatedIconStyles();\n const maskId = useSvgId('theme');\n return (\n <span {...rootProps(props, `iob-ani-theme iob-ani-theme-${props.mode}`)}>\n <svg viewBox=\"0 0 24 24\">\n <defs>\n <mask\n id={maskId}\n maskUnits=\"userSpaceOnUse\"\n x=\"-4\"\n y=\"-4\"\n width=\"32\"\n height=\"32\"\n >\n {/* a path and not a rect: a CSS `width` (e.g. `* { width: initial }`) would change a rect */}\n <path\n d=\"M-4 -4h32v32h-32z\"\n fill=\"#fff\"\n />\n {/* the shadow that makes the moon; outside of the sun for the light themes */}\n <circle\n className=\"iob-ani-theme-cut\"\n cx=\"18\"\n cy=\"6\"\n r=\"7\"\n fill=\"#000\"\n />\n </mask>\n </defs>\n <g mask={`url(#${maskId})`}>\n <circle\n className=\"iob-ani-theme-disc\"\n cx=\"12\"\n cy=\"12\"\n r=\"5\"\n />\n <path\n className=\"iob-ani-theme-half\"\n d=\"M12 7a5 5 0 0 0 0 10z\"\n />\n </g>\n <path\n className=\"iob-ani-theme-rays\"\n d=\"M20.5 12h2M18 18l1.4 1.4M12 20.5v2M6 18l-1.4 1.4M3.5 12h-2M6 6L4.6 4.6M12 3.5v-2M18 6l1.4-1.4\"\n />\n </svg>\n </span>\n );\n}\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { type AnimatedIconProps } from './common';
8
+ export interface IconUploadProps extends AnimatedIconProps {
9
+ /** Files are being uploaded: the arrow moves up into the bar again and again */
10
+ uploading?: boolean;
11
+ /** Files are dragged over the drop area: the arrow bobs up and down */
12
+ ready?: boolean;
13
+ }
14
+ /**
15
+ * Upload symbol (arrow up to a bar, like the MUI icon `Publish`). The arrow lifts on hover of its button,
16
+ * bobs while files are dragged over the drop area and moves up into the bar while they are uploaded.
17
+ *
18
+ * ```tsx
19
+ * <IconButton onClick={() => this.setState({ showUpload: true })}>
20
+ * <IconUpload uploading={this.state.uploading} />
21
+ * </IconButton>
22
+ * ```
23
+ */
24
+ export declare function IconUpload(props: IconUploadProps): React.JSX.Element;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ */
6
+ import React from 'react';
7
+ import { rootProps, useAnimatedIconStyles, useSvgId } from './common';
8
+ /**
9
+ * Upload symbol (arrow up to a bar, like the MUI icon `Publish`). The arrow lifts on hover of its button,
10
+ * bobs while files are dragged over the drop area and moves up into the bar while they are uploaded.
11
+ *
12
+ * ```tsx
13
+ * <IconButton onClick={() => this.setState({ showUpload: true })}>
14
+ * <IconUpload uploading={this.state.uploading} />
15
+ * </IconButton>
16
+ * ```
17
+ */
18
+ export function IconUpload(props) {
19
+ useAnimatedIconStyles();
20
+ const clipId = useSvgId('upload');
21
+ let className = 'iob-ani-up';
22
+ if (props.uploading) {
23
+ className += ' iob-ani-up-uploading';
24
+ }
25
+ else if (props.ready) {
26
+ className += ' iob-ani-up-ready';
27
+ }
28
+ return (React.createElement("span", { ...rootProps(props, className) },
29
+ React.createElement("svg", { viewBox: "0 0 24 24" },
30
+ React.createElement("defs", null,
31
+ React.createElement("clipPath", { id: clipId },
32
+ React.createElement("path", { d: "M0 6.5h24v15h-24z" }))),
33
+ React.createElement("path", { className: "iob-ani-up-bar", d: "M5 4v2h14V4H5z" }),
34
+ React.createElement("g", { clipPath: `url(#${clipId})` },
35
+ React.createElement("path", { className: "iob-ani-up-arrow", d: "M5 14h4v6h6v-6h4l-7-7-7 7z" })))));
36
+ }
37
+ //# sourceMappingURL=IconUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IconUpload.js","sourceRoot":"./src/","sources":["Components/AnimatedIcons/IconUpload.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA0B,SAAS,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAS9F;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC7C,qBAAqB,EAAE,CAAC;IACxB,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,SAAS,GAAG,YAAY,CAAC;IAC7B,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAClB,SAAS,IAAI,uBAAuB,CAAC;IACzC,CAAC;SAAM,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QACrB,SAAS,IAAI,mBAAmB,CAAC;IACrC,CAAC;IACD,OAAO,CACH,iCAAU,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC;QACjC,6BAAK,OAAO,EAAC,WAAW;YACpB;gBAEI,kCAAU,EAAE,EAAE,MAAM;oBAEhB,8BAAM,CAAC,EAAC,mBAAmB,GAAG,CACvB,CACR;YACP,8BACI,SAAS,EAAC,gBAAgB,EAC1B,CAAC,EAAC,gBAAgB,GACpB;YACF,2BAAG,QAAQ,EAAE,QAAQ,MAAM,GAAG;gBAC1B,8BACI,SAAS,EAAC,kBAAkB,EAC5B,CAAC,EAAC,4BAA4B,GAChC,CACF,CACF,CACH,CACV,CAAC;AACN,CAAC","sourcesContent":["/**\n * Copyright 2026 Denis Haev (bluefox) <dogafox@gmail.com>\n *\n * MIT License\n */\nimport React from 'react';\nimport { type AnimatedIconProps, rootProps, useAnimatedIconStyles, useSvgId } from './common';\n\nexport interface IconUploadProps extends AnimatedIconProps {\n /** Files are being uploaded: the arrow moves up into the bar again and again */\n uploading?: boolean;\n /** Files are dragged over the drop area: the arrow bobs up and down */\n ready?: boolean;\n}\n\n/**\n * Upload symbol (arrow up to a bar, like the MUI icon `Publish`). The arrow lifts on hover of its button,\n * bobs while files are dragged over the drop area and moves up into the bar while they are uploaded.\n *\n * ```tsx\n * <IconButton onClick={() => this.setState({ showUpload: true })}>\n * <IconUpload uploading={this.state.uploading} />\n * </IconButton>\n * ```\n */\nexport function IconUpload(props: IconUploadProps): React.JSX.Element {\n useAnimatedIconStyles();\n const clipId = useSvgId('upload');\n let className = 'iob-ani-up';\n if (props.uploading) {\n className += ' iob-ani-up-uploading';\n } else if (props.ready) {\n className += ' iob-ani-up-ready';\n }\n return (\n <span {...rootProps(props, className)}>\n <svg viewBox=\"0 0 24 24\">\n <defs>\n {/* the arrow disappears in the bar and appears from below */}\n <clipPath id={clipId}>\n {/* a path and not a rect: a CSS `width` (e.g. `* { width: initial }`) would change a rect */}\n <path d=\"M0 6.5h24v15h-24z\" />\n </clipPath>\n </defs>\n <path\n className=\"iob-ani-up-bar\"\n d=\"M5 4v2h14V4H5z\"\n />\n <g clipPath={`url(#${clipId})`}>\n <path\n className=\"iob-ani-up-arrow\"\n d=\"M5 14h4v6h6v-6h4l-7-7-7 7z\"\n />\n </g>\n </svg>\n </span>\n );\n}\n"]}
@@ -23,6 +23,8 @@ export interface AnimatedIconProps {
23
23
  export declare function useAnimatedIconStyles(): void;
24
24
  /** Properties of the root element: class, size and accessibility */
25
25
  export declare function rootProps(props: AnimatedIconProps, className: string): React.HTMLAttributes<HTMLSpanElement>;
26
+ /** Unique ID for a mask or a clip path of one icon (the IDs of `useId` contain characters like `:` or `«`) */
27
+ export declare function useSvgId(name: string): string;
26
28
  /** True if the user asked the system for less motion */
27
29
  export declare function prefersReducedMotion(): boolean;
28
30
  /** Plays a one-time animation (Web Animations API), unless the user wants less motion */
@@ -1,4 +1,4 @@
1
- import { useEffect, useInsertionEffect, useRef } from 'react';
1
+ import { useEffect, useId, useInsertionEffect, useRef } from 'react';
2
2
  const SIZES = {
3
3
  small: '1.25rem',
4
4
  medium: '1.5rem',
@@ -60,12 +60,12 @@ const styles = `
60
60
  .iob-ani-menu-open .iob-ani-menu-b2 { transform: translateX(-0.0417em) scaleX(0.889); }
61
61
  .iob-ani-menu-open .iob-ani-menu-b3 { transform: translate(-0.0417em, -0.2083em) rotate(-45deg) scaleX(0.63); }
62
62
 
63
- /* Trash: the lid opens on hover of the icon or of its button */
63
+ /* Trash: the lid opens on hover of the icon or of its button or menu item */
64
64
  .iob-ani-trash-lid { transform-origin: 5px 6px; transition: transform 0.25s cubic-bezier(0.3, 1.5, 0.6, 1); }
65
65
  .iob-ani-trash-open .iob-ani-trash-lid,
66
66
  .iob-ani-trash:hover .iob-ani-trash-lid,
67
- :is(button, a, [role="button"]):hover .iob-ani-trash .iob-ani-trash-lid,
68
- :is(button, a, [role="button"]):focus-visible .iob-ani-trash .iob-ani-trash-lid { transform: translateY(-1.5px) rotate(-18deg); }
67
+ :is(button, a, [role="button"], [role="menuitem"]):hover .iob-ani-trash .iob-ani-trash-lid,
68
+ :is(button, a, [role="button"], [role="menuitem"]):focus-visible .iob-ani-trash .iob-ani-trash-lid { transform: translateY(-1.5px) rotate(-18deg); }
69
69
 
70
70
  /* Expert mode: mortarboard, outlined when off and filled when on */
71
71
  .iob-ani-expert-hat { transform-origin: 12px 12px; }
@@ -82,6 +82,58 @@ const styles = `
82
82
  .iob-ani-folder-open .iob-ani-folder-front { transform: none; transition: transform 0.32s cubic-bezier(0.3, 1.35, 0.5, 1); }
83
83
  .iob-ani-folder-closed { transition: opacity 0.06s linear 0.18s; }
84
84
  .iob-ani-folder-open .iob-ani-folder-closed { opacity: 0; transition-delay: 0s; }
85
+ /* toggleOnHover: shows the other state while the mouse is over the icon or its button, e.g. "expand all" */
86
+ .iob-ani-folder-hover:not(.iob-ani-folder-open):hover .iob-ani-folder-front,
87
+ :is(button, a, [role="button"]):is(:hover, :focus-visible) .iob-ani-folder-hover:not(.iob-ani-folder-open) .iob-ani-folder-front { transform: none; transition: transform 0.32s cubic-bezier(0.3, 1.35, 0.5, 1); }
88
+ .iob-ani-folder-hover:not(.iob-ani-folder-open):hover .iob-ani-folder-closed,
89
+ :is(button, a, [role="button"]):is(:hover, :focus-visible) .iob-ani-folder-hover:not(.iob-ani-folder-open) .iob-ani-folder-closed { opacity: 0; transition-delay: 0s; }
90
+ .iob-ani-folder-hover.iob-ani-folder-open:hover .iob-ani-folder-front,
91
+ :is(button, a, [role="button"]):is(:hover, :focus-visible) .iob-ani-folder-hover.iob-ani-folder-open .iob-ani-folder-front { transform: scaleY(1.1667) skewX(29.1deg); transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); }
92
+ .iob-ani-folder-hover.iob-ani-folder-open:hover .iob-ani-folder-closed,
93
+ :is(button, a, [role="button"]):is(:hover, :focus-visible) .iob-ani-folder-hover.iob-ani-folder-open .iob-ani-folder-closed { opacity: 1; transition-delay: 0.18s; }
94
+
95
+ /* Copy to clipboard: the copy slides out of the original, then a check mark is drawn in it */
96
+ .iob-ani-copy svg * { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
97
+ .iob-ani-copy .iob-ani-copy-check { opacity: 0; stroke-width: 2.2; stroke-dasharray: 1 1; stroke-dashoffset: 1; }
98
+ .iob-ani-click { cursor: pointer; }
99
+
100
+ /* Theme: sun (light), ring (blue), half filled (colored) or moon (dark). The shadow of the moon is a mask. */
101
+ .iob-ani-theme-disc { fill: currentColor; fill-opacity: 1; stroke: currentColor; stroke-width: 2; transform-origin: 12px 12px; transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), fill-opacity 0.3s; }
102
+ .iob-ani-theme-half { fill: currentColor; opacity: 0; transition: opacity 0.3s; }
103
+ .iob-ani-theme-rays { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; transform-origin: 12px 12px; transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s; }
104
+ .iob-ani-theme-cut { transform: translate(8px, -8px); transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
105
+ .iob-ani-theme-blue .iob-ani-theme-disc, .iob-ani-theme-colored .iob-ani-theme-disc { fill-opacity: 0; }
106
+ .iob-ani-theme-colored .iob-ani-theme-half { opacity: 1; }
107
+ /* the rays are 45 degrees apart: turned by 45 degrees they look the same, but they spin on the change */
108
+ .iob-ani-theme-blue .iob-ani-theme-rays { transform: rotate(45deg); }
109
+ .iob-ani-theme-colored .iob-ani-theme-rays { transform: rotate(90deg); }
110
+ .iob-ani-theme-dark .iob-ani-theme-disc { transform: scale(1.55); }
111
+ .iob-ani-theme-dark .iob-ani-theme-cut { transform: none; transition-delay: 0.08s; }
112
+ .iob-ani-theme-dark .iob-ani-theme-rays { transform: rotate(-90deg) scale(0.4); opacity: 0; }
113
+
114
+ /* Upload: the arrow moves up into the bar */
115
+ .iob-ani-up path { fill: currentColor; }
116
+ .iob-ani-up-arrow { transition: transform 0.2s ease-out; }
117
+ .iob-ani-up:hover .iob-ani-up-arrow,
118
+ :is(button, a, [role="button"]):is(:hover, :focus-visible) .iob-ani-up .iob-ani-up-arrow { transform: translateY(-2px); }
119
+ .iob-ani-up-ready .iob-ani-up-arrow { animation: iob-ani-up-bob 0.8s ease-in-out infinite; }
120
+ .iob-ani-up-uploading .iob-ani-up-arrow { animation: iob-ani-up-rise 1.1s linear infinite; }
121
+ .iob-ani-up-uploading .iob-ani-up-bar { animation: iob-ani-up-bar 1.1s ease-out infinite; }
122
+ @keyframes iob-ani-up-rise {
123
+ 0% { transform: translateY(15px); animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
124
+ 35%, 50% { transform: translateY(0); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
125
+ 100% { transform: translateY(-15px); }
126
+ }
127
+ @keyframes iob-ani-up-bar { 0%, 84% { transform: none; } 92% { transform: translateY(-1.2px); } }
128
+ @keyframes iob-ani-up-bob { 50% { transform: translateY(-2.5px); } }
129
+
130
+ /* Filter: a red cross shows that a click removes the filter; it jumps away when the filter is cleared */
131
+ .iob-ani-filter-funnel { fill: currentColor; stroke: currentColor; stroke-width: 1; }
132
+ :where(.iob-ani-filter-x) { color: #F44336; }
133
+ .iob-ani-filter-x { fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; transform-origin: 19px 16.75px; opacity: 0; transform: translate(3px, -5px) rotate(90deg) scale(0.2); transition: transform 0.3s cubic-bezier(0.5, -0.4, 0.8, 0.6), opacity 0.2s 0.1s; }
134
+ .iob-ani-filter-on .iob-ani-filter-x { opacity: 1; transform: none; transition: transform 0.35s cubic-bezier(0.3, 1.6, 0.5, 1), opacity 0.1s; }
135
+ .iob-ani-filter-on:hover .iob-ani-filter-x,
136
+ :is(button, a, [role="button"]):is(:hover, :focus-visible) .iob-ani-filter-on .iob-ani-filter-x { transform: rotate(90deg); }
85
137
 
86
138
  /* Connection */
87
139
  .iob-ani-wifi path { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; transition: opacity 0.3s; }
@@ -101,8 +153,11 @@ const styles = `
101
153
  @keyframes iob-ani-shake { 20% { transform: translateX(-1.5px); } 40% { transform: translateX(1.5px); } 60% { transform: translateX(-1px); } 80% { transform: translateX(1px); } }
102
154
 
103
155
  @media (prefers-reduced-motion: reduce) {
104
- .iob-ani-pp-a, .iob-ani-pp-b, .iob-ani-menu-rot, .iob-ani-menu-bar, .iob-ani-trash-lid, .iob-ani-expert-shape, .iob-ani-folder-front, .iob-ani-folder-closed { transition-duration: 0.01ms; transition-delay: 0s; }
156
+ .iob-ani-pp-a, .iob-ani-pp-b, .iob-ani-menu-rot, .iob-ani-menu-bar, .iob-ani-trash-lid, .iob-ani-expert-shape, .iob-ani-folder-front, .iob-ani-folder-closed,
157
+ .iob-ani-theme-disc, .iob-ani-theme-half, .iob-ani-theme-rays, .iob-ani-theme-cut, .iob-ani-up-arrow, .iob-ani-filter-x { transition-duration: 0.01ms !important; transition-delay: 0s !important; }
105
158
  .iob-ani-pp-pending .iob-ani-pp-ring { animation: iob-ani-breathe 1.2s ease-in-out infinite; }
159
+ .iob-ani-up-ready .iob-ani-up-arrow, .iob-ani-up-uploading .iob-ani-up-bar { animation: none; }
160
+ .iob-ani-up-uploading .iob-ani-up-arrow { animation: iob-ani-breathe 1.2s ease-in-out infinite; }
106
161
  .iob-ani-res svg, .iob-ani-res svg * { animation: none !important; stroke-dashoffset: 0; }
107
162
  }
108
163
  `;
@@ -135,6 +190,10 @@ export function rootProps(props, className) {
135
190
  'aria-hidden': props.ariaLabel ? undefined : true,
136
191
  };
137
192
  }
193
+ /** Unique ID for a mask or a clip path of one icon (the IDs of `useId` contain characters like `:` or `«`) */
194
+ export function useSvgId(name) {
195
+ return `iob-ani-${name}-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
196
+ }
138
197
  /** True if the user asked the system for less motion */
139
198
  export function prefersReducedMotion() {
140
199
  return !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;