@stellar-expert/ui-framework 1.16.8 → 1.19.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 (85) hide show
  1. package/README.md +1168 -3
  2. package/account/account-address.js +127 -127
  3. package/account/available-balance.js +22 -22
  4. package/account/identicon.js +90 -90
  5. package/account/signer-key.js +65 -64
  6. package/api/explorer-api-hooks.js +209 -202
  7. package/api/explorer-api-paginated-list-hooks.js +441 -440
  8. package/api/explorer-batch-info-loader.js +111 -85
  9. package/api/explorer-tx-api.js +28 -28
  10. package/api/ledger-stream.js +15 -0
  11. package/asset/amount.js +56 -56
  12. package/asset/asset-icon.js +41 -41
  13. package/asset/asset-issuer.js +21 -21
  14. package/asset/asset-link.js +107 -107
  15. package/asset/asset-list-hooks.js +59 -59
  16. package/asset/asset-meta-hooks.js +88 -88
  17. package/asset/asset-selector.js +72 -71
  18. package/claimable-balance/claimable-balance-claimants.js +23 -18
  19. package/contract/contract-api.js +15 -0
  20. package/contract/invocation-info-view.js +10 -2
  21. package/contract/sc-val.js +131 -107
  22. package/controls/button-group.js +25 -19
  23. package/controls/button.js +93 -78
  24. package/controls/code-block.js +42 -34
  25. package/controls/dropdown.js +318 -287
  26. package/controls/external-link.js +10 -4
  27. package/controls/info-tooltip.js +23 -16
  28. package/controls/slider.js +29 -19
  29. package/controls/tabs.js +94 -94
  30. package/controls/tooltip.js +244 -240
  31. package/controls/update-highlighter.js +32 -27
  32. package/date/date-selector.js +56 -54
  33. package/date/elapsed-time.js +28 -21
  34. package/dex/price-dynamic.js +53 -44
  35. package/directory/directory-hooks.js +109 -97
  36. package/effect/effect-description.js +346 -344
  37. package/errors/error-boundary.js +110 -97
  38. package/filter/editors/account-filter-view.js +20 -0
  39. package/filter/editors/asset-filter-view.js +9 -0
  40. package/filter/editors/timestamp-filter-view.js +34 -0
  41. package/filter/editors/type-filter-view.js +148 -0
  42. package/filter/filter-editors.js +102 -0
  43. package/filter/filter-view.js +218 -0
  44. package/filter/filter.scss +57 -0
  45. package/horizon/horizon-account-helpers.js +104 -104
  46. package/horizon/horizon-ledger-helpers.js +35 -35
  47. package/horizon/horizon-trades-helper.js +88 -88
  48. package/horizon/horizon-transaction-helpers.js +36 -36
  49. package/index.d.ts +1271 -0
  50. package/index.js +95 -93
  51. package/interaction/accordion.js +43 -35
  52. package/interaction/autofocus.js +13 -9
  53. package/interaction/block-select.js +64 -53
  54. package/interaction/copy-to-clipboard.js +25 -18
  55. package/interaction/inline-progress.js +20 -15
  56. package/interaction/qr-code.js +34 -34
  57. package/interaction/responsive.js +20 -20
  58. package/interaction/spoiler.js +52 -39
  59. package/interaction/theme-selector.js +13 -10
  60. package/ledger/ledger-entry-href-formatter.js +27 -26
  61. package/ledger/ledger-entry-link.js +93 -58
  62. package/ledger/ledger-info-parser.js +46 -18
  63. package/meta/page-meta-tags.js +243 -238
  64. package/module/dynamic-module.js +47 -47
  65. package/package.json +71 -40
  66. package/state/on-screen-hooks.js +22 -22
  67. package/state/page-visibility-helpers.js +29 -16
  68. package/state/page-visibility-hooks.js +13 -11
  69. package/state/screen-orientation-hooks.js +19 -15
  70. package/state/state-hooks.js +76 -76
  71. package/state/stellar-network-hooks.js +56 -44
  72. package/state/theme.js +29 -28
  73. package/stellar/key-type.js +92 -91
  74. package/stellar/ledger-generic-id.js +39 -39
  75. package/stellar/signature-hint-utils.js +65 -65
  76. package/toast/toast-notifications-block.js +59 -59
  77. package/tx/op-description-view.js +945 -942
  78. package/tx/op-icon.js +98 -98
  79. package/tx/parser/op-balance-changes.js +66 -66
  80. package/tx/parser/op-descriptor.js +51 -48
  81. package/tx/parser/tx-details-parser.js +81 -81
  82. package/tx/parser/tx-matcher.js +371 -371
  83. package/tx/parser/type-filter-matcher.js +126 -126
  84. package/tx/tx-list-hooks.js +32 -18
  85. package/tx/tx-operations-list.js +117 -116
package/package.json CHANGED
@@ -1,40 +1,71 @@
1
- {
2
- "name": "@stellar-expert/ui-framework",
3
- "version": "1.16.8",
4
- "description": "StellarExpert shared UI components library",
5
- "main": "index.js",
6
- "module": "./index.js",
7
- "sideEffects": [
8
- "*.scss"
9
- ],
10
- "keywords": [
11
- "StellarExpert",
12
- "UI"
13
- ],
14
- "author": "orbitlens <orbit@stellar.expert>",
15
- "license": "MIT",
16
- "peerDependencies": {
17
- "@stellar/stellar-base": "^14.0.0",
18
- "@stellar/stellar-sdk": "^14.0.0",
19
- "@stellar-expert/asset-descriptor": "^1.5.0",
20
- "@stellar-expert/claimable-balance-utils": "^1.4.1",
21
- "@stellar-expert/client-cache": "^1.1.0",
22
- "@stellar-expert/contract-wasm-interface-parser": "^3.2.0",
23
- "@stellar-expert/formatter": "^3.0.0",
24
- "@stellar-expert/navigation": "^1.1.0",
25
- "@stellar-expert/tx-meta-effects-parser": "^8.3.3",
26
- "classnames": ">=2",
27
- "prop-types": ">=15",
28
- "qrcode.react": "^4.2.0",
29
- "react": ">=17",
30
- "react-dom": ">=17",
31
- "react-fast-compare": ">=3",
32
- "throttle-debounce": ">=5.0.0"
33
- },
34
- "dependencies": {
35
- "highlight.js": "^11.8.0",
36
- "history": "^4.10.1",
37
- "react-copy-to-clipboard": "^5.1.0",
38
- "react-timeago": "^7.2.0"
39
- }
40
- }
1
+ {
2
+ "name": "@stellar-expert/ui-framework",
3
+ "version": "1.19.0",
4
+ "description": "StellarExpert shared UI components library",
5
+ "main": "index.js",
6
+ "module": "./index.js",
7
+ "types": "index.d.ts",
8
+ "sideEffects": [
9
+ "*.scss"
10
+ ],
11
+ "keywords": [
12
+ "StellarExpert",
13
+ "UI"
14
+ ],
15
+ "author": "orbitlens <orbit@stellar.expert>",
16
+ "license": "MIT",
17
+ "peerDependencies": {
18
+ "@stellar/stellar-base": "^15.0.0",
19
+ "@stellar/stellar-sdk": "^15.0.1",
20
+ "@stellar-expert/asset-descriptor": "^1.5.0",
21
+ "@stellar-expert/claimable-balance-utils": "^1.4.1",
22
+ "@stellar-expert/client-cache": "^1.1.0",
23
+ "@stellar-expert/contract-wasm-interface-parser": "^4.0.0",
24
+ "@stellar-expert/formatter": "^3.0.0",
25
+ "@stellar-expert/navigation": "^1.1.0",
26
+ "@stellar-expert/tx-meta-effects-parser": "^8.3.3",
27
+ "classnames": ">=2",
28
+ "deepmerge": "^4.3.1",
29
+ "prop-types": ">=15",
30
+ "qrcode.react": "^4.2.0",
31
+ "react": ">=17 <18.0.0",
32
+ "react-dom": ">=17 <18.0.0",
33
+ "react-fast-compare": ">=3",
34
+ "throttle-debounce": ">=5.0.0"
35
+ },
36
+ "dependencies": {
37
+ "highlight.js": "^11.8.0",
38
+ "history": "^4.10.1",
39
+ "react-copy-to-clipboard": "^5.1.0",
40
+ "react-timeago": "^7.2.0"
41
+ },
42
+ "files": [
43
+ "index.js",
44
+ "index.d.ts",
45
+ "index.scss",
46
+ ".npmrc",
47
+ "account/*",
48
+ "api/*",
49
+ "asset/*",
50
+ "basic-styles/*",
51
+ "claimable-balance/*",
52
+ "contract/*",
53
+ "controls/*",
54
+ "date/*",
55
+ "dex/*",
56
+ "directory/*",
57
+ "effect/*",
58
+ "errors/*",
59
+ "filter/*",
60
+ "fonts/*",
61
+ "horizon/*",
62
+ "interaction/*",
63
+ "ledger/*",
64
+ "meta/*",
65
+ "module/*",
66
+ "state/*",
67
+ "stellar/*",
68
+ "toast/*",
69
+ "tx/*"
70
+ ]
71
+ }
@@ -1,23 +1,23 @@
1
- import {useEffect, useState} from 'react'
2
-
3
- //TODO consider using a single instance of an IntersectionObserver to track all changes instead of creating many instances
4
-
5
- /**
6
- * Hook for determining the visibility of the element inside the scroll context using the Intersection Observer API
7
- * @param {MutableRefObject<Element>} root - Scroll parent ref
8
- * @param {String} [rootMargin] - Visibility margin, in pixels or percents (default 0)
9
- * @returns {Boolean} - Whether an element is visible or not
10
- */
11
- export function useOnScreen(root, rootMargin) {
12
- const [isVisible, setVisibility] = useState(false)
13
-
14
- useEffect(() => {
15
- if (!root.current)
16
- return
17
- const observer = new IntersectionObserver(([entry]) => setVisibility(entry.isIntersecting), {rootMargin})
18
- observer.observe(root.current)
19
- return () => observer.disconnect()
20
- }, [root.current])
21
-
22
- return isVisible
1
+ import {useEffect, useState} from 'react'
2
+
3
+ //TODO consider using a single instance of an IntersectionObserver to track all changes instead of creating many instances
4
+
5
+ /**
6
+ * Hook for determining the visibility of the element inside the scroll context using the Intersection Observer API
7
+ * @param {MutableRefObject<Element>} root - Scroll parent ref
8
+ * @param {string} [rootMargin] - Visibility margin, in pixels or percents (default 0)
9
+ * @returns {boolean} - Whether an element is visible or not
10
+ */
11
+ export function useOnScreen(root, rootMargin) {
12
+ const [isVisible, setVisibility] = useState(false)
13
+
14
+ useEffect(() => {
15
+ if (!root.current)
16
+ return
17
+ const observer = new IntersectionObserver(([entry]) => setVisibility(entry.isIntersecting), {rootMargin})
18
+ observer.observe(root.current)
19
+ return () => observer.disconnect()
20
+ }, [root.current])
21
+
22
+ return isVisible
23
23
  }
@@ -1,17 +1,30 @@
1
- export const isDocumentVisibilitySupported = 'hidden' in document
2
-
3
- export function isDocumentVisible() {
4
- if (!isDocumentVisibilitySupported) return true
5
- return !document.hidden
6
- }
7
-
8
- export function addVisibilityChangeListener(listener) {
9
- function wrappedListener() {
10
- listener(isDocumentVisible())
11
- }
12
-
13
- document.addEventListener('visibilitychange', wrappedListener)
14
- return function () {
15
- document.removeEventListener('visibilitychange', wrappedListener)
16
- }
1
+ /**
2
+ * Whether the Page Visibility API is supported by the browser
3
+ * @type {boolean}
4
+ */
5
+ export const isDocumentVisibilitySupported = 'hidden' in document
6
+
7
+ /**
8
+ * Check whether the document is currently visible (tab is active)
9
+ * @return {boolean}
10
+ */
11
+ export function isDocumentVisible() {
12
+ if (!isDocumentVisibilitySupported) return true
13
+ return !document.hidden
14
+ }
15
+
16
+ /**
17
+ * Register a listener for document visibility changes
18
+ * @param {function(boolean): void} listener - Callback invoked with `true` when visible, `false` when hidden
19
+ * @return {function(): void} Unsubscribe function
20
+ */
21
+ export function addVisibilityChangeListener(listener) {
22
+ function wrappedListener() {
23
+ listener(isDocumentVisible())
24
+ }
25
+
26
+ document.addEventListener('visibilitychange', wrappedListener)
27
+ return function () {
28
+ document.removeEventListener('visibilitychange', wrappedListener)
29
+ }
17
30
  }
@@ -1,12 +1,14 @@
1
- import {useState, useEffect} from 'react'
2
- import {isDocumentVisible, addVisibilityChangeListener, isDocumentVisibilitySupported} from './page-visibility-helpers'
3
-
4
- /**
5
- * @returns {Boolean} - True if the page is currently visible or false otherwise
6
- */
7
- export function usePageVisibility() {
8
- const [visible, setVisible] = useState(isDocumentVisible)
9
- if (!isDocumentVisibilitySupported || !document.addEventListener) return true
10
- useEffect(()=>addVisibilityChangeListener(() => setVisible(isDocumentVisible())))
11
- return visible
1
+ import {useState, useEffect} from 'react'
2
+ import {isDocumentVisible, addVisibilityChangeListener, isDocumentVisibilitySupported} from './page-visibility-helpers'
3
+
4
+ /**
5
+ * Hook that tracks whether the current browser tab/page is visible
6
+ * @returns {boolean} - True if the page is currently visible or false otherwise
7
+ */
8
+ export function usePageVisibility() {
9
+ const [visible, setVisible] = useState(isDocumentVisible)
10
+ if (!isDocumentVisibilitySupported || !document.addEventListener)
11
+ return true
12
+ useEffect(() => addVisibilityChangeListener(() => setVisible(isDocumentVisible())))
13
+ return visible
12
14
  }
@@ -1,16 +1,20 @@
1
- import {useState, useEffect} from 'react'
2
-
3
- export function useScreenOrientation() {
4
- const [orientation, setOrientation] = useState(window.screen.orientation.type)
5
-
6
- function onChange() {
7
- setOrientation(window.screen.orientation.type)
8
- }
9
-
10
- useEffect(() => {
11
- window.addEventListener('orientationchange', onChange)
12
- return () => window.removeEventListener('orientationchange', onChange)
13
- }, [])
14
-
15
- return orientation
1
+ import {useState, useEffect} from 'react'
2
+
3
+ /**
4
+ * React hook that tracks and returns the current screen orientation type
5
+ * @return {OrientationType} Screen orientation (e.g., "portrait-primary", "landscape-primary")
6
+ */
7
+ export function useScreenOrientation() {
8
+ const [orientation, setOrientation] = useState(window.screen.orientation.type)
9
+
10
+ function onChange() {
11
+ setOrientation(window.screen.orientation.type)
12
+ }
13
+
14
+ useEffect(() => {
15
+ window.addEventListener('orientationchange', onChange)
16
+ return () => window.removeEventListener('orientationchange', onChange)
17
+ }, [])
18
+
19
+ return orientation
16
20
  }
@@ -1,77 +1,77 @@
1
- import {useState, useEffect, useRef} from 'react'
2
- import isEqual from 'react-fast-compare'
3
-
4
- function ensureDependencies(dependencies) {
5
- if (!(dependencies instanceof Array))
6
- throw new Error('Parameter dependencies is required for useDependantState hook')
7
- }
8
-
9
- /**
10
- * React hook that automatically re-inits state when one of the dependencies changed
11
- * @param {Function|any} stateInitializer
12
- * @param {Array} dependencies
13
- * @param {Function} [finalizer]
14
- * @returns {[Object, Function]}
15
- */
16
- export function useDependantState(stateInitializer, dependencies, finalizer) {
17
- //ensureDependencies(dependencies)
18
- const [state, updateState] = useState(function () {
19
- return typeof stateInitializer === 'function' ? stateInitializer(dependencies) : stateInitializer
20
- })
21
- //pin dependencies object to invoke effect update only if dependencies changed
22
- const pinnedDeps = useRef(dependencies)
23
- let dependenciesChanged = !isEqual(dependencies, pinnedDeps.current)
24
- //check that dependencies really changed
25
- if (dependenciesChanged) {
26
- pinnedDeps.current = dependencies
27
- }
28
-
29
- //effect invokes the initializer each time dependencies changed
30
- useEffect(function () {
31
- //check that dependencies really changed
32
- if (dependenciesChanged) {
33
- //re-initialize state when any of the dependencies changed
34
- updateState(typeof stateInitializer === 'function' ? stateInitializer(dependencies, state) : stateInitializer)
35
- }
36
- return finalizer || undefined
37
- }, pinnedDeps.current)
38
-
39
- return [state, function (newState) {
40
- //use deep compare - as React used to in good old times
41
- updateState(current => {
42
- if (typeof newState === 'function') {
43
- newState = newState(current)
44
- }
45
- if (isEqual(current, newState)) return current
46
- return newState
47
- })
48
- }]
49
- }
50
-
51
- /**
52
- * Simple force update hook.
53
- * @return {Function}
54
- */
55
- export function useForceUpdate() {
56
- const [, updateNonce] = useState(0)
57
- return function () {
58
- updateNonce(nonce => nonce + 1)
59
- }
60
- }
61
-
62
- /**
63
- * React hook that automatically reacts on dependency changes using deep comparison
64
- * @param {Function} effect
65
- * @param {Array} dependencies
66
- */
67
- export function useDeepEffect(effect, dependencies) {
68
- ensureDependencies(dependencies)
69
- //pin dependencies object to invoke effect update only if dependencies changed
70
- const pinnedDeps = useRef([])
71
- //check that dependencies really changed
72
- if (!isEqual(dependencies, pinnedDeps.current)) {
73
- pinnedDeps.current = dependencies
74
- }
75
- //effect invokes the initializer each time dependencies changed
76
- useEffect(effect, pinnedDeps.current)
1
+ import {useState, useEffect, useRef} from 'react'
2
+ import isEqual from 'react-fast-compare'
3
+
4
+ function ensureDependencies(dependencies) {
5
+ if (!(dependencies instanceof Array))
6
+ throw new Error('Parameter dependencies is required for useDependantState hook')
7
+ }
8
+
9
+ /**
10
+ * React hook that automatically re-inits state when one of the dependencies changed
11
+ * @param {function|any} stateInitializer
12
+ * @param {Array} dependencies
13
+ * @param {function} [finalizer]
14
+ * @returns {[Object, function]}
15
+ */
16
+ export function useDependantState(stateInitializer, dependencies, finalizer) {
17
+ //ensureDependencies(dependencies)
18
+ const [state, updateState] = useState(function () {
19
+ return typeof stateInitializer === 'function' ? stateInitializer(dependencies) : stateInitializer
20
+ })
21
+ //pin dependencies object to invoke effect update only if dependencies changed
22
+ const pinnedDeps = useRef(dependencies)
23
+ let dependenciesChanged = !isEqual(dependencies, pinnedDeps.current)
24
+ //check that dependencies really changed
25
+ if (dependenciesChanged) {
26
+ pinnedDeps.current = dependencies
27
+ }
28
+
29
+ //effect invokes the initializer each time dependencies changed
30
+ useEffect(function () {
31
+ //check that dependencies really changed
32
+ if (dependenciesChanged) {
33
+ //re-initialize state when any of the dependencies changed
34
+ updateState(typeof stateInitializer === 'function' ? stateInitializer(dependencies, state) : stateInitializer)
35
+ }
36
+ return finalizer || undefined
37
+ }, pinnedDeps.current)
38
+
39
+ return [state, function (newState) {
40
+ //use deep compare - as React used to in good old times
41
+ updateState(current => {
42
+ if (typeof newState === 'function') {
43
+ newState = newState(current)
44
+ }
45
+ if (isEqual(current, newState)) return current
46
+ return newState
47
+ })
48
+ }]
49
+ }
50
+
51
+ /**
52
+ * Simple force update hook.
53
+ * @return {function}
54
+ */
55
+ export function useForceUpdate() {
56
+ const [, updateNonce] = useState(0)
57
+ return function () {
58
+ updateNonce(nonce => nonce + 1)
59
+ }
60
+ }
61
+
62
+ /**
63
+ * React hook that automatically reacts on dependency changes using deep comparison
64
+ * @param {function} effect
65
+ * @param {Array} dependencies
66
+ */
67
+ export function useDeepEffect(effect, dependencies) {
68
+ ensureDependencies(dependencies)
69
+ //pin dependencies object to invoke effect update only if dependencies changed
70
+ const pinnedDeps = useRef([])
71
+ //check that dependencies really changed
72
+ if (!isEqual(dependencies, pinnedDeps.current)) {
73
+ pinnedDeps.current = dependencies
74
+ }
75
+ //effect invokes the initializer each time dependencies changed
76
+ useEffect(effect, pinnedDeps.current)
77
77
  }
@@ -1,45 +1,57 @@
1
- import {useState, useEffect} from 'react'
2
-
3
- const listeners = []
4
- let currentNetwork = 'public'
5
-
6
- setStellarNetwork(currentNetwork)
7
-
8
- function removeListener(callback) {
9
- const idx = listeners.indexOf(callback)
10
- if (~idx) listeners.splice(idx, 1)
11
- }
12
-
13
- export function getCurrentStellarNetwork() {
14
- return currentNetwork
15
- }
16
-
17
- /**
18
- * Set current Stellar network.
19
- * @param {'testnet'|'public'} network
20
- */
21
- export function setStellarNetwork(network) {
22
- if (currentNetwork === network) return
23
- currentNetwork = network
24
- for (const listener of listeners) {
25
- listener(network)
26
- }
27
- }
28
-
29
- export function subscribeToStellarNetworkChange(onChange) {
30
- removeListener(onChange)
31
- listeners.push(onChange)
32
- }
33
-
34
- /**
35
- * React hook for reacting on Stellar network changes.
36
- * @return {'testnet'|'public'}
37
- */
38
- export function useStellarNetwork() {
39
- const [state, updateState] = useState(currentNetwork)
40
- useEffect(() => {
41
- subscribeToStellarNetworkChange(updateState)
42
- return () => removeListener(updateState)
43
- }, [])
44
- return state
1
+ import {useState, useEffect} from 'react'
2
+
3
+ /**
4
+ * @typedef {'public'|'testnet'} StellarNetwork
5
+ */
6
+
7
+ const listeners = []
8
+ let currentNetwork = 'public'
9
+
10
+ setStellarNetwork(currentNetwork)
11
+
12
+ function removeListener(callback) {
13
+ const idx = listeners.indexOf(callback)
14
+ if (~idx) listeners.splice(idx, 1)
15
+ }
16
+
17
+ /**
18
+ * Get current Stellar network identifier
19
+ * @return {StellarNetwork}
20
+ */
21
+ export function getCurrentStellarNetwork() {
22
+ return currentNetwork
23
+ }
24
+
25
+ /**
26
+ * Set current Stellar network
27
+ * @param {StellarNetwork} network
28
+ */
29
+ export function setStellarNetwork(network) {
30
+ if (currentNetwork === network) return
31
+ currentNetwork = network
32
+ for (const listener of listeners) {
33
+ listener(network)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Subscribe to Stellar network change events
39
+ * @param {function(StellarNetwork): void} onChange - Callback invoked when the network changes
40
+ */
41
+ export function subscribeToStellarNetworkChange(onChange) {
42
+ removeListener(onChange)
43
+ listeners.push(onChange)
44
+ }
45
+
46
+ /**
47
+ * React hook that returns the current Stellar network and re-renders on network changes
48
+ * @return {StellarNetwork}
49
+ */
50
+ export function useStellarNetwork() {
51
+ const [state, updateState] = useState(currentNetwork)
52
+ useEffect(() => {
53
+ subscribeToStellarNetworkChange(updateState)
54
+ return () => removeListener(updateState)
55
+ }, [])
56
+ return state
45
57
  }
package/state/theme.js CHANGED
@@ -1,29 +1,30 @@
1
- import {useState} from 'react'
2
-
3
- function setGlobalTheme(theme) {
4
- localStorage.setItem('preferred-color-theme', theme)
5
- document.documentElement.attributes['data-theme'].value = theme
6
- }
7
-
8
- const preferredTheme = localStorage.getItem('preferred-color-theme')
9
- if (preferredTheme) {
10
- setGlobalTheme(preferredTheme)
11
- } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
12
- setGlobalTheme('night')
13
- }
14
-
15
- /**
16
- * @returns {[colorTheme: String, setTheme: Function]}
17
- */
18
- export function useTheme() {
19
- const [theme, setTheme] = useState(localStorage.getItem('preferred-color-theme') || 'day')
20
- return [theme, function (newTheme) {
21
- setTheme(current => {
22
- if (typeof newTheme === 'function') {
23
- newTheme = newTheme(current)
24
- }
25
- setGlobalTheme(newTheme)
26
- return newTheme
27
- })
28
- }]
1
+ import {useState} from 'react'
2
+
3
+ function setGlobalTheme(theme) {
4
+ localStorage.setItem('preferred-color-theme', theme)
5
+ document.documentElement.attributes['data-theme'].value = theme
6
+ }
7
+
8
+ const preferredTheme = localStorage.getItem('preferred-color-theme')
9
+ if (preferredTheme) {
10
+ setGlobalTheme(preferredTheme)
11
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
12
+ setGlobalTheme('night')
13
+ }
14
+
15
+ /**
16
+ * Hook that manages the current color theme (day/night) with localStorage persistence
17
+ * @returns {[colorTheme: string, setTheme: function]}
18
+ */
19
+ export function useTheme() {
20
+ const [theme, setTheme] = useState(localStorage.getItem('preferred-color-theme') || 'day')
21
+ return [theme, function (newTheme) {
22
+ setTheme(current => {
23
+ if (typeof newTheme === 'function') {
24
+ newTheme = newTheme(current)
25
+ }
26
+ setGlobalTheme(newTheme)
27
+ return newTheme
28
+ })
29
+ }]
29
30
  }