@olenbetong/synergi-react 0.6.7

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 (92) hide show
  1. package/dist/esm/ob.react.css +708 -0
  2. package/dist/esm/ob.react.js +3077 -0
  3. package/dist/esm/ob.react.min.css +2 -0
  4. package/dist/esm/ob.react.min.css.map +7 -0
  5. package/dist/esm/ob.react.min.js +8 -0
  6. package/dist/esm/ob.react.min.js.map +7 -0
  7. package/dist/iife/ob.react.css +708 -0
  8. package/dist/iife/ob.react.js +3089 -0
  9. package/dist/iife/ob.react.min.css +2 -0
  10. package/dist/iife/ob.react.min.css.map +7 -0
  11. package/dist/iife/ob.react.min.js +8 -0
  12. package/dist/iife/ob.react.min.js.map +7 -0
  13. package/dist/styles/styles.css +1 -0
  14. package/dist/styles/styles.css.map +1 -0
  15. package/es/Checkbox/index.css +1 -0
  16. package/es/ColorCard/index.css +1 -0
  17. package/es/DateNavigator/index.css +1 -0
  18. package/es/LinkedCardList/index.css +1 -0
  19. package/es/PageBanner/index.css +1 -0
  20. package/es/ProgressBar/index.css +1 -0
  21. package/es/Sidebar/index.css +1 -0
  22. package/es/Spinner/index.css +1 -0
  23. package/es/SplitContainer/index.css +1 -0
  24. package/es/ValueToggle/index.css +1 -0
  25. package/es/appframe-react/src/useDebounce.d.ts +8 -0
  26. package/es/appframe-react/src/useDebounce.js +16 -0
  27. package/es/index.css +1 -0
  28. package/es/styles/_mixins.css +0 -0
  29. package/es/styles/_variables.css +0 -0
  30. package/es/synergi-react/src/Checkbox/index.d.ts +9 -0
  31. package/es/synergi-react/src/Checkbox/index.js +41 -0
  32. package/es/synergi-react/src/ColorCard/index.d.ts +14 -0
  33. package/es/synergi-react/src/ColorCard/index.js +16 -0
  34. package/es/synergi-react/src/DateNavigator/index.d.ts +6 -0
  35. package/es/synergi-react/src/DateNavigator/index.js +51 -0
  36. package/es/synergi-react/src/LinkedCardList/index.d.ts +9 -0
  37. package/es/synergi-react/src/LinkedCardList/index.js +13 -0
  38. package/es/synergi-react/src/PageBanner/index.d.ts +27 -0
  39. package/es/synergi-react/src/PageBanner/index.js +24 -0
  40. package/es/synergi-react/src/Portal/index.d.ts +6 -0
  41. package/es/synergi-react/src/Portal/index.js +32 -0
  42. package/es/synergi-react/src/ProgressBar/index.d.ts +4 -0
  43. package/es/synergi-react/src/ProgressBar/index.js +7 -0
  44. package/es/synergi-react/src/Sidebar/index.d.ts +10 -0
  45. package/es/synergi-react/src/Sidebar/index.js +57 -0
  46. package/es/synergi-react/src/Spinner/index.d.ts +10 -0
  47. package/es/synergi-react/src/Spinner/index.js +6 -0
  48. package/es/synergi-react/src/SplitContainer/index.d.ts +20 -0
  49. package/es/synergi-react/src/SplitContainer/index.js +151 -0
  50. package/es/synergi-react/src/ValueToggle/index.d.ts +19 -0
  51. package/es/synergi-react/src/ValueToggle/index.js +38 -0
  52. package/es/synergi-react/src/index.d.ts +15 -0
  53. package/es/synergi-react/src/index.js +15 -0
  54. package/es/synergi-react/src/useDebounce.d.ts +9 -0
  55. package/es/synergi-react/src/useDebounce.js +17 -0
  56. package/es/synergi-react/src/usePersistedSetting.d.ts +7 -0
  57. package/es/synergi-react/src/usePersistedSetting.js +53 -0
  58. package/es/synergi-react/src/useSearchParamState.d.ts +13 -0
  59. package/es/synergi-react/src/useSearchParamState.js +42 -0
  60. package/es/synergi-react/src/useTranslation/index.d.ts +7 -0
  61. package/es/synergi-react/src/useTranslation/index.js +64 -0
  62. package/package.json +90 -0
  63. package/src/Checkbox/index.scss +119 -0
  64. package/src/Checkbox/index.tsx +81 -0
  65. package/src/ColorCard/index.scss +154 -0
  66. package/src/ColorCard/index.tsx +58 -0
  67. package/src/DateNavigator/index.scss +60 -0
  68. package/src/DateNavigator/index.tsx +92 -0
  69. package/src/LinkedCardList/index.scss +86 -0
  70. package/src/LinkedCardList/index.tsx +38 -0
  71. package/src/PageBanner/index.scss +99 -0
  72. package/src/PageBanner/index.tsx +79 -0
  73. package/src/Portal/index.tsx +36 -0
  74. package/src/ProgressBar/index.scss +86 -0
  75. package/src/ProgressBar/index.tsx +13 -0
  76. package/src/Sidebar/index.scss +53 -0
  77. package/src/Sidebar/index.tsx +117 -0
  78. package/src/Spinner/index.scss +71 -0
  79. package/src/Spinner/index.tsx +21 -0
  80. package/src/SplitContainer/index.scss +43 -0
  81. package/src/SplitContainer/index.tsx +271 -0
  82. package/src/ValueToggle/index.scss +50 -0
  83. package/src/ValueToggle/index.tsx +75 -0
  84. package/src/global.d.ts +2 -0
  85. package/src/index.scss +9 -0
  86. package/src/index.ts +27 -0
  87. package/src/styles/_mixins.scss +19 -0
  88. package/src/styles/_variables.scss +70 -0
  89. package/src/useDebounce.ts +21 -0
  90. package/src/usePersistedSetting.ts +78 -0
  91. package/src/useSearchParamState.ts +78 -0
  92. package/src/useTranslation/index.ts +74 -0
@@ -0,0 +1,271 @@
1
+ import "./index.scss";
2
+
3
+ import clsx from "clsx";
4
+ import localforage from "localforage";
5
+ import React, { Children, forwardRef, useEffect, useRef, useState } from "react";
6
+
7
+ const settingsStore = localforage.createInstance({
8
+ name: `AfSplitContainers.${af.userSession.login}`,
9
+ });
10
+
11
+ type SplitContainerSettings = {
12
+ layoutVersion: string | number;
13
+ fixedSize: number;
14
+ };
15
+
16
+ const settingsCache: Record<string, SplitContainerSettings> = {};
17
+
18
+ async function persistState(key: string, layoutVersion: string | number, fixedSize: number | null | undefined) {
19
+ if (fixedSize != null) {
20
+ let settings = {
21
+ layoutVersion,
22
+ fixedSize,
23
+ };
24
+
25
+ settingsCache[key] = settings;
26
+ await settingsStore.setItem<SplitContainerSettings>(key, settings);
27
+ }
28
+ }
29
+
30
+ async function getSettings(key: string, layoutVersion: string | number): Promise<number | null> {
31
+ let settings = settingsCache[key] ?? (await settingsStore.getItem<SplitContainerSettings>(key));
32
+
33
+ if (settings && settings.layoutVersion === layoutVersion) {
34
+ return settings.fixedSize;
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ // Load all article grid layouts even if the grid is not mounted yet.
41
+ // This ways the persisted state can be restored faster after the initial
42
+ // page load.
43
+ settingsStore.keys().then((keys) =>
44
+ Promise.allSettled(
45
+ keys
46
+ .filter((key) => key.startsWith(af.article.id))
47
+ .map((key) =>
48
+ settingsStore.getItem<SplitContainerSettings>(key).then((settings) => {
49
+ if (settings && !settingsCache[key]) {
50
+ settingsCache[key] = settings;
51
+ }
52
+ })
53
+ )
54
+ )
55
+ );
56
+
57
+ type DividerProps = {
58
+ className?: string;
59
+ direction: "horizontal" | "vertical";
60
+ isDragging: boolean;
61
+ };
62
+
63
+ const Divider = forwardRef<HTMLDivElement, DividerProps>(function Divider(props: DividerProps, ref) {
64
+ return <div tabIndex={0} className={clsx("ObSplitContainer-divider", props.className)} ref={ref} />;
65
+ });
66
+
67
+ type SplitRootProps = React.HTMLProps<HTMLDivElement> & {
68
+ className?: string;
69
+ children: React.ReactNode;
70
+ direction: "horizontal" | "vertical";
71
+ };
72
+
73
+ function SplitRoot({ className, direction, ...props }: SplitRootProps) {
74
+ return <div className={clsx(className, "ObSplitContainer-root", direction)} {...props} />;
75
+ }
76
+
77
+ type SplitPanelProps = {
78
+ children: React.ReactNode;
79
+ className?: string;
80
+ style?: React.CSSProperties;
81
+ } & (
82
+ | {
83
+ fixed: true;
84
+ size: number;
85
+ }
86
+ | {
87
+ fixed: false;
88
+ size?: number;
89
+ }
90
+ );
91
+
92
+ function SplitPanel(props: SplitPanelProps) {
93
+ let style = {
94
+ flex: props.fixed ? `0 0 ${Math.floor(props.size)}px` : "1 1 100%",
95
+ ...props.style,
96
+ };
97
+
98
+ return (
99
+ <div className={clsx(props.className, "ObSplitContainer-panel")} style={style}>
100
+ {props.children}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ export type SplitContainerProps = {
106
+ className?: string;
107
+ panelClassName?: string;
108
+ children: React.ReactNode[];
109
+ direction?: "horizontal" | "vertical";
110
+ fixedPanel?: 1 | 2;
111
+ defaultFixedSize?: number;
112
+ /**
113
+ * When the fixed size is persisted, changing the layoutVersion will force clients
114
+ * to reset to the default value.
115
+ */
116
+ layoutVersion?: string | number;
117
+ /**
118
+ * If you set an id, the fixed size will be persisted, and restored after page load
119
+ */
120
+ id?: string;
121
+ };
122
+
123
+ export function SplitContainer({
124
+ className,
125
+ children,
126
+ direction = "vertical",
127
+ fixedPanel = 1,
128
+ panelClassName,
129
+ defaultFixedSize = 400,
130
+ layoutVersion = 1,
131
+ id,
132
+ }: SplitContainerProps) {
133
+ let [fixedSize, setFixedSize] = useState<number>(defaultFixedSize);
134
+ let [isDragging, setIsDragging] = useState(false);
135
+ let dividerRef = useRef<HTMLDivElement>(null);
136
+
137
+ if (Children.count(children) !== 2) {
138
+ throw Error("SplitContainer requires exactly 2 children");
139
+ }
140
+
141
+ function handleFixedSizeChanged(size: React.SetStateAction<number>) {
142
+ setFixedSize((current) => {
143
+ let newValue: number = typeof size === "function" ? size(current) : size;
144
+
145
+ if (id) {
146
+ let key = `${af.article.id}.${id}`;
147
+ persistState(key, layoutVersion, newValue);
148
+ }
149
+
150
+ return newValue;
151
+ });
152
+ }
153
+
154
+ useEffect(() => {
155
+ if (id) {
156
+ let key = `${af.article.id}.${id}`;
157
+
158
+ getSettings(key, layoutVersion).then((data: number | null) => {
159
+ if (data) {
160
+ // Persisted state exists, and matches the current layout version,
161
+ // so restore the state.
162
+ setFixedSize(data);
163
+
164
+ return Promise.resolve();
165
+ }
166
+ });
167
+ }
168
+ }, [id, layoutVersion]);
169
+
170
+ let childrenArray = Children.toArray(children);
171
+
172
+ useEffect(() => {
173
+ let divider = dividerRef.current;
174
+ if (divider) {
175
+ let initialPosition: { clientX: number; clientY: number; size: number } = {
176
+ clientX: 0,
177
+ clientY: 0,
178
+ size: 0,
179
+ };
180
+ let movingPanel: HTMLElement = (
181
+ fixedPanel === 1 ? divider.previousElementSibling : divider.nextElementSibling
182
+ ) as HTMLElement;
183
+
184
+ function getNewSize(event: PointerEvent) {
185
+ let deltaSize =
186
+ direction === "horizontal"
187
+ ? event.clientX - initialPosition.clientX
188
+ : event.clientY - initialPosition.clientY;
189
+
190
+ deltaSize *= fixedPanel === 1 ? -1 : 1;
191
+
192
+ return initialPosition.size - deltaSize;
193
+ }
194
+
195
+ function handlePointerUp(event: PointerEvent) {
196
+ let newSize = getNewSize(event);
197
+ handleFixedSizeChanged(newSize);
198
+ setIsDragging(false);
199
+
200
+ movingPanel.style.flex = `0 0 ${Math.floor(newSize)}px`;
201
+ window.removeEventListener("pointermove", handlePointerMove);
202
+ }
203
+
204
+ function handlePointerMove(event: PointerEvent) {
205
+ event.preventDefault();
206
+
207
+ let newSize = getNewSize(event);
208
+ movingPanel.style.flex = `0 0 ${newSize}px`;
209
+ }
210
+
211
+ function handlePointerDown(event: PointerEvent) {
212
+ setIsDragging(true);
213
+
214
+ let rect = movingPanel.getBoundingClientRect();
215
+ let size = direction === "horizontal" ? rect.width : rect.height;
216
+ initialPosition = {
217
+ clientX: event.clientX,
218
+ clientY: event.clientY,
219
+ size,
220
+ };
221
+
222
+ window.addEventListener("pointermove", handlePointerMove);
223
+ window.addEventListener("pointerup", handlePointerUp, { once: true });
224
+ }
225
+
226
+ function handleKeyDown(event: KeyboardEvent) {
227
+ let reduce: boolean = true;
228
+ let shouldHandle: boolean = false;
229
+ if (!event.altKey && !event.ctrlKey && !event.metaKey) {
230
+ if (direction === "horizontal" && ["ArrowLeft", "ArrowRight"].includes(event.key)) {
231
+ shouldHandle = true;
232
+ reduce =
233
+ (fixedPanel === 1 && event.key === "ArrowLeft") || (fixedPanel === 2 && event.key === "ArrowRight");
234
+ } else if (direction === "vertical" && ["ArrowUp", "ArrowDown"].includes(event.key)) {
235
+ shouldHandle = true;
236
+ reduce = (fixedPanel === 1 && event.key === "ArrowUp") || (fixedPanel === 2 && event.key === "ArrowDown");
237
+ }
238
+ }
239
+
240
+ if (shouldHandle) {
241
+ event.preventDefault();
242
+ event.stopPropagation();
243
+
244
+ let delta = event.shiftKey ? 25 : 5;
245
+
246
+ handleFixedSizeChanged((size) => size + (reduce ? -delta : delta));
247
+ }
248
+ }
249
+
250
+ divider.addEventListener("pointerdown", handlePointerDown);
251
+ divider.addEventListener("keydown", handleKeyDown);
252
+
253
+ return () => {
254
+ divider?.removeEventListener("pointerdown", handlePointerDown);
255
+ divider?.removeEventListener("keydown", handleKeyDown);
256
+ };
257
+ }
258
+ }, [direction, fixedPanel]);
259
+
260
+ return (
261
+ <SplitRoot direction={direction} className={className}>
262
+ <SplitPanel className={panelClassName} fixed={fixedPanel === 1} size={fixedSize}>
263
+ {childrenArray[0]}
264
+ </SplitPanel>
265
+ <Divider ref={dividerRef} direction={direction} isDragging={isDragging} />
266
+ <SplitPanel className={panelClassName} fixed={fixedPanel === 2} size={fixedSize}>
267
+ {childrenArray[1]}
268
+ </SplitPanel>
269
+ </SplitRoot>
270
+ );
271
+ }
@@ -0,0 +1,50 @@
1
+ @import "../styles/variables";
2
+
3
+ .ObValueToggle-root {
4
+ display: flex;
5
+ align-items: center;
6
+ }
7
+
8
+ .ObValueToggle-option {
9
+ margin-bottom: 0;
10
+ }
11
+
12
+ .ObValueToggle-label {
13
+ cursor: pointer;
14
+ font-size: 0.8em;
15
+ margin: 0 0.2em;
16
+ padding: 0.3em 0.5em;
17
+ text-transform: uppercase;
18
+
19
+ &:hover {
20
+ background: #ccc;
21
+ }
22
+
23
+ .ObValueToggle-input:checked + & {
24
+ --color-bg: var(--brand-dark);
25
+ background-color: #666;
26
+ background-color: var(--color-bg);
27
+ cursor: default;
28
+ color: white;
29
+ }
30
+
31
+ .ObValueToggle-input:checked:focus-within + & {
32
+ --color-bg: var(--brand-light);
33
+ }
34
+
35
+ .ObValueToggle-input:checked:hover + & {
36
+ background-color: #666;
37
+ background-color: var(--color-bg);
38
+ }
39
+ }
40
+
41
+ .ObValueToggle-input {
42
+ position: absolute;
43
+ width: 1px;
44
+ height: 1px;
45
+ padding: 0;
46
+ margin: -1px;
47
+ overflow: hidden;
48
+ clip: rect(0, 0, 0, 0);
49
+ border: 0;
50
+ }
@@ -0,0 +1,75 @@
1
+ import "./index.scss";
2
+
3
+ import { forwardRef, useImperativeHandle, useRef, useState } from "react";
4
+
5
+ let valueToggleId = 0;
6
+
7
+ export type ValueToggleProps = {
8
+ className?: string;
9
+ onChange?: (value: string | number) => void;
10
+ value?: string | number;
11
+ defaultValue?: string | number;
12
+ id?: string;
13
+ options: Array<string | { value: string | number; label?: string }>;
14
+ style?: React.CSSProperties;
15
+ };
16
+
17
+ function ValueToggleInner(
18
+ { className, onChange, defaultValue, options, value, ...other }: ValueToggleProps,
19
+ ref: React.ForwardedRef<{
20
+ focus: () => void;
21
+ blur: () => void;
22
+ value: string | number;
23
+ }>
24
+ ) {
25
+ const wrapperRef = useRef<HTMLDivElement>(null);
26
+ const [toggleName] = useState("obet-value-toggle-" + String(++valueToggleId));
27
+ const isControlled = value !== undefined;
28
+
29
+ useImperativeHandle(ref, () => {
30
+ const e = wrapperRef.current?.querySelector("input");
31
+
32
+ return {
33
+ focus: () => e?.focus(),
34
+ blur: () => e?.blur(),
35
+ value: (e?.querySelector("input:checked") as HTMLInputElement).value,
36
+ };
37
+ });
38
+
39
+ return (
40
+ <fieldset className={className} {...other}>
41
+ <div className="ObValueToggle-root" ref={wrapperRef}>
42
+ {options.map((option) => {
43
+ const isObj = typeof option === "object";
44
+ const optValue = isObj ? option.value : option;
45
+ const props: React.HTMLProps<HTMLInputElement> = {
46
+ value: optValue,
47
+ };
48
+
49
+ if (isControlled) {
50
+ props.checked = value === optValue;
51
+ } else {
52
+ props.defaultChecked = defaultValue === optValue;
53
+ }
54
+
55
+ if (typeof onChange === "function") {
56
+ props.onChange = () => onChange(optValue);
57
+ }
58
+
59
+ if (isObj) {
60
+ Object.assign(props, option);
61
+ }
62
+
63
+ return (
64
+ <label key={optValue} className="ObValueToggle-option">
65
+ <input type="radio" className="ObValueToggle-input" name={toggleName} {...props} />
66
+ <span className="ObValueToggle-label">{isObj ? option.label || optValue : optValue}</span>
67
+ </label>
68
+ );
69
+ })}
70
+ </div>
71
+ </fieldset>
72
+ );
73
+ }
74
+
75
+ export const ValueToggle = forwardRef(ValueToggleInner);
@@ -0,0 +1,2 @@
1
+ /// <reference path="@olenbetong/appframe-core" />
2
+ /// <reference path="@olenbetong/appframe-data" />
package/src/index.scss ADDED
@@ -0,0 +1,9 @@
1
+ @import "./Checkbox/index.scss";
2
+ @import "./ColorCard/index.scss";
3
+ @import "./DateNavigator/index.scss";
4
+ @import "./LinkedCardList/index.scss";
5
+ @import "./PageBanner/index.scss";
6
+ @import "./ProgressBar/index.scss";
7
+ @import "./Sidebar/index.scss";
8
+ @import "./SplitContainer/index.scss";
9
+ @import "./ValueToggle/index.scss";
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ export { Checkbox, CheckboxProps } from "./Checkbox/index.js";
2
+ export { ColorCard, CardColumn, CardDetail, CardLabel } from "./ColorCard/index.js";
3
+ export { DateNavigator } from "./DateNavigator/index.js";
4
+ export { Card, List as LinkedList, ListItem as LinkedListItem } from "./LinkedCardList/index.js";
5
+ export {
6
+ BannerImage as PageBannerImage,
7
+ BannerImageProps,
8
+ PageBanner,
9
+ PageBannerProps,
10
+ Tab as PageBannerTab,
11
+ TabProps as PageBannerTabProps,
12
+ TabList as PageBannerTabList,
13
+ TabListProps as PageBannerTabListProps,
14
+ Title as PageBannerTitle,
15
+ TitleProps as PageBannerTitleProps,
16
+ } from "./PageBanner/index.js";
17
+ export { Portal } from "./Portal/index.js";
18
+ export { ProgressBar } from "./ProgressBar/index.js";
19
+ export { Sidebar, SidebarProps } from "./Sidebar/index.js";
20
+ export { Spinner, SpinnerProps } from "./Spinner/index.js";
21
+ export { SplitContainer, SplitContainerProps } from "./SplitContainer/index.js";
22
+
23
+ export { useTranslation } from "./useTranslation/index.js";
24
+ export { ValueToggle, ValueToggleProps } from "./ValueToggle/index.js";
25
+ export { useDebounce } from "../../appframe-react/src/useDebounce.js";
26
+ export { usePersistedSetting } from "./usePersistedSetting.js";
27
+ export { useSearchParamState } from "./useSearchParamState.js";
@@ -0,0 +1,19 @@
1
+ @mixin btn-reset {
2
+ background: none;
3
+ border: 0;
4
+ color: inherit;
5
+ /* cursor: default; */
6
+ font: inherit;
7
+ line-height: normal;
8
+ overflow: visible;
9
+ padding: 0;
10
+ -webkit-appearance: button; /* for input */
11
+ -webkit-user-select: none; /* for button */
12
+ -moz-user-select: none;
13
+ -ms-user-select: none;
14
+
15
+ &::-moz-focus-inner {
16
+ border: 0;
17
+ padding: 0;
18
+ }
19
+ }
@@ -0,0 +1,70 @@
1
+ $brand-primary: hsl(186, 72%, 24%);
2
+ $brand-light: hsl(186, 72%, 37%);
3
+ $brand-dark: hsl(186, 74%, 20%);
4
+ $brand-tertiary: rgb(68, 69, 84);
5
+
6
+ $link-color: $brand-primary;
7
+ $link-color-hover: $brand-dark;
8
+
9
+ $line-height: 1.4;
10
+
11
+ $color-bg-hover: hsla(186, 72%, 37%, 0.6);
12
+ $color-bg-active: hsla(95, 49%, 71%, 0.2);
13
+ $color-fg-active: black;
14
+
15
+ $color-border-top: #e5e6e9;
16
+ $color-border-side: #dfe0e4;
17
+ $color-border-bottom: #d0d1d5;
18
+
19
+ $navbar-height: 40px;
20
+
21
+ $screen-xs: 0;
22
+ $screen-sm: 576px;
23
+ $screen-md: 768px;
24
+ $screen-lg: 992px;
25
+ $screen-xl: 1200px;
26
+
27
+ $block-header-color-bg: hsl(220, 20%, 96%);
28
+ $block-header-color-bg-hover: hsl(220, 20%, 92%);
29
+ $block-header-color-fg: hsl(0, 0%, 20%);
30
+ $block-color-bg-alt: hsl(220, 20%, 98%);
31
+ $block-color-bg-hover: hsl(220, 20%, 94%);
32
+ $block-color-border: #e5e5e5;
33
+
34
+ $block-header-button-hover: hsl(220, 20%, 92%);
35
+ $block-header-button-border: hsl(220, 7%, 82%);
36
+ $block-header-button-active: hsl(220, 20%, 86%);
37
+
38
+ $sidebar-padding: 0.9375rem;
39
+ $sidebar-width: 18rem;
40
+ $sidebar-fg-color: hsl(0, 0%, 90%);
41
+ $sidebar-bg-color: hsl(186, 74%, 13%);
42
+ $sidebar-header-color: hsl(220, 6%, 70%);
43
+ $sidebar-link-fg-color: white;
44
+ $sidebar-link-fg-active: white;
45
+ $sidebar-link-bg-color: transparent;
46
+ $sidebar-link-bg-hover: hsla(0, 0%, 100%, 0.1);
47
+ $sidebar-link-bg-active: hsl(142, 13%, 13%);
48
+
49
+ $padding-xs: 0.75rem;
50
+ $padding-sm: 0.9375rem;
51
+ $padding-lg: 1.5rem;
52
+
53
+ $grid-gutter-width: 30px;
54
+
55
+ $container-tablet: (720px + $grid-gutter-width);
56
+ $container-sm: $container-tablet;
57
+ $container-desktop: (940px + $grid-gutter-width);
58
+ $container-md: $container-desktop;
59
+ $container-large-desktop: (1140px + $grid-gutter-width);
60
+ $container-lg: $container-large-desktop;
61
+
62
+ $toolbar-background: linear-gradient(#fefefe, #fafafa); //white;
63
+ $toolbar-height: 3.125rem;
64
+ $toolbar-border-color: #dedede;
65
+ $toolbar-border-width: 1px;
66
+
67
+ $toolbar-btn-color: hsla(0, 0%, 60%, 0.8);
68
+ $toolbar-btn-hover-color: hsla(0, 0%, 40%, 0.8);
69
+ $toolbar-btn-bg: transparent;
70
+ $toolbar-btn-hover-bg: linear-gradient(hsl(0, 0%, 89%), hsl(0, 0%, 87%));
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Returns a debounced version of the given value.
5
+ *
6
+ * @param value
7
+ * @param delay
8
+ * @returns
9
+ * @deprecated Use equivalent hook in @olenbetong/appframe-react instead
10
+ */
11
+ export function useDebounce<T>(value: T, delay = 300) {
12
+ let [debouncedValue, setValue] = useState(value);
13
+
14
+ useEffect(() => {
15
+ let timeout = setTimeout(() => setValue(value), delay);
16
+
17
+ return () => clearTimeout(timeout);
18
+ }, [delay, value]);
19
+
20
+ return debouncedValue;
21
+ }
@@ -0,0 +1,78 @@
1
+ import mitt, { Emitter, Handler } from "mitt";
2
+ import localforage from "localforage";
3
+ import { useSyncExternalStore } from "react";
4
+
5
+ const settingsStore = localforage.createInstance({
6
+ name: `AfUserSettings.${af.userSession.login}`,
7
+ });
8
+
9
+ class SettingsCache {
10
+ #mitt: Emitter<{ change: { key: string; value: unknown } }>;
11
+ #cache: Record<string, unknown> = {};
12
+
13
+ constructor() {
14
+ this.#mitt = mitt();
15
+ // Load all article user settings even if the setting is not needed yet.
16
+ // This ways the persisted state can be restored faster after the initial
17
+ // page load.'
18
+ settingsStore.keys().then((keys) =>
19
+ Promise.allSettled(
20
+ keys
21
+ .filter((key) => key.startsWith(af.article.id))
22
+ .map((storeKey) =>
23
+ settingsStore.getItem(storeKey).then((storedValue) => {
24
+ let key = storeKey.replace(`${af.article.id}.`, "");
25
+
26
+ if (storedValue !== undefined && this.#cache[key] === undefined) {
27
+ this.setItem(key, storedValue);
28
+ }
29
+ })
30
+ )
31
+ )
32
+ );
33
+ }
34
+
35
+ setItem(key: string, value: unknown) {
36
+ this.#cache[key] = value;
37
+ this.#mitt.emit("change", { key, value });
38
+
39
+ let storeKey = `${af.article.id}.${key}`;
40
+ settingsStore.setItem(storeKey, value);
41
+ }
42
+
43
+ getItem(key: string): unknown {
44
+ return this.#cache[key];
45
+ }
46
+
47
+ listen(setting: string, callback: Handler<unknown>) {
48
+ function handler({ key, value }: { key: string; value: unknown }) {
49
+ if (setting === key) {
50
+ callback(value);
51
+ }
52
+ }
53
+
54
+ this.#mitt.on("change", handler);
55
+
56
+ return () => this.#mitt.off("change", handler);
57
+ }
58
+ }
59
+
60
+ const settingsCache = new SettingsCache();
61
+
62
+ /**
63
+ * On mount, load the persisted state from Indexed DB, and if the persisted
64
+ * layout version matches, restore the layout. Then listen to changes to the
65
+ * state, and store the latest version in Indexed DB.
66
+ * @deprecated Use equivalent hook in @olenbetong/appframe-react instead
67
+ */
68
+ export function usePersistedSetting<T = unknown>(
69
+ key: string,
70
+ defaultValue: T
71
+ ): [T, (newValue: T) => void] {
72
+ let value = useSyncExternalStore(
73
+ (onChange) => settingsCache.listen(key, () => onChange()),
74
+ () => (settingsCache.getItem(key) as T) ?? defaultValue
75
+ );
76
+
77
+ return [value, (newValue: T) => settingsCache.setItem(key, newValue)];
78
+ }