@olenbetong/synergi-react 1.0.6 → 2.1.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 (59) hide show
  1. package/dist/esm/ob.react.css +221 -174
  2. package/dist/esm/ob.react.js +223 -104
  3. package/dist/esm/ob.react.min.css +1 -1
  4. package/dist/esm/ob.react.min.css.map +3 -3
  5. package/dist/esm/ob.react.min.js +1 -1
  6. package/dist/esm/ob.react.min.js.map +4 -4
  7. package/dist/iife/ob.react.css +221 -174
  8. package/dist/iife/ob.react.js +223 -104
  9. package/dist/iife/ob.react.min.css +1 -1
  10. package/dist/iife/ob.react.min.css.map +3 -3
  11. package/dist/iife/ob.react.min.js +1 -1
  12. package/dist/iife/ob.react.min.js.map +4 -4
  13. package/dist/styles/styles.css +1 -1
  14. package/dist/styles/styles.css.map +1 -1
  15. package/es/Checkbox/index.css +1 -1
  16. package/es/Checkbox/index.d.ts +11 -0
  17. package/es/Checkbox/index.js +7 -5
  18. package/es/ColorCard/index.d.ts +5 -2
  19. package/es/ColorCard/index.js +6 -0
  20. package/es/DateNavigator/index.d.ts +8 -3
  21. package/es/DateNavigator/index.js +99 -27
  22. package/es/LinkedCardList/index.css +1 -1
  23. package/es/LinkedCardList/index.d.ts +2 -2
  24. package/es/LinkedCardList/index.js +2 -2
  25. package/es/ProgressBar/index.css +1 -1
  26. package/es/SplitContainer/index.d.ts +3 -1
  27. package/es/SplitContainer/index.js +45 -14
  28. package/es/ValueToggleGroup/index.d.ts +18 -0
  29. package/es/ValueToggleGroup/index.js +12 -0
  30. package/es/index.css +1 -1
  31. package/es/index.d.ts +8 -8
  32. package/es/index.js +3 -3
  33. package/package.json +88 -90
  34. package/src/Checkbox/index.scss +12 -5
  35. package/src/Checkbox/index.tsx +23 -6
  36. package/src/ColorCard/{index.scss → index.css} +50 -59
  37. package/src/ColorCard/index.tsx +21 -9
  38. package/src/DateNavigator/index.css +96 -0
  39. package/src/DateNavigator/index.tsx +128 -36
  40. package/src/LinkedCardList/index.scss +8 -1
  41. package/src/LinkedCardList/index.tsx +2 -2
  42. package/src/ProgressBar/index.scss +2 -2
  43. package/src/Spinner/{index.scss → index.css} +13 -10
  44. package/src/Spinner/index.tsx +1 -1
  45. package/src/SplitContainer/index.tsx +71 -15
  46. package/src/ValueToggleGroup/index.css +79 -0
  47. package/src/ValueToggleGroup/index.tsx +52 -0
  48. package/src/index.scss +4 -3
  49. package/src/index.ts +12 -12
  50. package/LICENSE +0 -21
  51. package/es/ColorCard/index.css +0 -1
  52. package/es/DateNavigator/index.css +0 -1
  53. package/es/Spinner/index.css +0 -1
  54. package/es/ValueToggle/index.css +0 -1
  55. package/es/ValueToggle/index.d.ts +0 -18
  56. package/es/ValueToggle/index.js +0 -38
  57. package/src/DateNavigator/index.scss +0 -60
  58. package/src/ValueToggle/index.scss +0 -50
  59. package/src/ValueToggle/index.tsx +0 -75
@@ -0,0 +1,96 @@
1
+ :root {
2
+ --ob-datenavigator-height: 3rem;
3
+ }
4
+
5
+ .ObDateNavigator-icon {
6
+ display: inline-block;
7
+
8
+ svg {
9
+ width: 0.625em;
10
+ position: relative;
11
+ top: -1px;
12
+ }
13
+ }
14
+
15
+ .ObDateNavigator-root {
16
+ background-color: hsl(220, 20%, 96%);
17
+ background-color: var(--block-header-color-bg);
18
+ display: flex;
19
+ font-size: 1rem;
20
+ outline: none;
21
+
22
+ &:focus-visible {
23
+ outline: 2px solid var(--brand-dark, #336699);
24
+ outline-offset: 2px;
25
+ }
26
+ }
27
+
28
+ .ObDateNavigator-arrow {
29
+ display: flex;
30
+ align-items: center;
31
+ flex: 0 0 var(--ob-datenavigator-height);
32
+ justify-content: center;
33
+
34
+ height: var(--ob-datenavigator-height);
35
+
36
+ cursor: pointer;
37
+ text-align: center;
38
+
39
+ &:hover:not(:disabled) {
40
+ background-color: #e3e3e3;
41
+ background-color: var(--block-header-button-hover);
42
+ }
43
+
44
+ &:disabled {
45
+ cursor: not-allowed;
46
+ color: rgba(0 0 0 / 0.2);
47
+ }
48
+ }
49
+
50
+ .ObDateNavigator-arrowLeft {
51
+ border-right: 1px solid #dddddd;
52
+ order: 1;
53
+ }
54
+
55
+ .ObDateNavigator-arrowRight {
56
+ border-left: 1px solid #dddddd;
57
+ order: 3;
58
+ }
59
+
60
+ .ObDateNavigator-date {
61
+ display: flex;
62
+ flex: 1 1 100%;
63
+ flex-direction: column;
64
+ justify-content: center;
65
+ order: 2;
66
+
67
+ height: var(--ob-datenavigator-height);
68
+
69
+ text-align: center;
70
+ }
71
+
72
+ .ObDateNavigator-today {
73
+ order: 4;
74
+ padding: 0 1rem;
75
+ height: var(--ob-datenavigator-height);
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ font-size: 0.875rem;
80
+ color: var(--brand-dark, #336699);
81
+ text-transform: uppercase;
82
+ cursor: pointer;
83
+
84
+ text-wrap: nowrap;
85
+
86
+ border-left: 1px solid #dddddd;
87
+
88
+ &:hover:not(:disabled) {
89
+ background-color: var(--block-header-button-hover);
90
+ }
91
+
92
+ &:focus-visible {
93
+ outline: 2px solid var(--brand-dark, #336699);
94
+ outline-offset: 2px;
95
+ }
96
+ }
@@ -1,35 +1,36 @@
1
- import "./index.scss";
1
+ import "./index.css";
2
2
 
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
-
5
4
  import clsx from "clsx";
6
- import { useState } from "react";
5
+ import { useEffect, useRef, useState } from "react";
7
6
 
8
7
  function getUiCulture() {
9
8
  return af?.userSession?.culture ?? "default";
10
9
  }
11
10
 
12
- function Arrow(props) {
13
- const { direction, ...rest } = props;
14
-
11
+ function Arrow({
12
+ direction,
13
+ ...rest
14
+ }: { direction: "left" | "right" } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
15
15
  return (
16
16
  <button
17
17
  className={clsx(
18
- "ObDateNavigator-arrow",
18
+ "ObDateNavigator-arrow btn-reset",
19
19
  direction === "left" ? "ObDateNavigator-arrowLeft" : "ObDateNavigator-arrowRight",
20
20
  )}
21
+ aria-label={direction === "left" ? getLocalizedString("Previous day") : getLocalizedString("Next day")}
21
22
  {...rest}
22
23
  >
23
24
  <span className="ObDateNavigator-icon">
24
25
  {direction === "left" ? (
25
- <svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 320 512">
26
+ <svg aria-hidden="true" viewBox="0 0 320 512">
26
27
  <path
27
28
  fill="currentColor"
28
29
  d="M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z"
29
30
  />
30
31
  </svg>
31
32
  ) : (
32
- <svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 320 512">
33
+ <svg aria-hidden="true" viewBox="0 0 320 512">
33
34
  <path
34
35
  fill="currentColor"
35
36
  d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"
@@ -48,45 +49,136 @@ const formatter = new Intl.DateTimeFormat(getUiCulture(), {
48
49
  year: "numeric",
49
50
  timeZone: "utc",
50
51
  });
51
- const today = new Date();
52
- const todayUtc = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate());
53
52
 
54
53
  const oneDay = 24 * 60 * 60 * 1000;
55
54
 
56
- export function DateNavigator({ date: dateProp, onChange }: { date?: Date; onChange?: (date: Date) => void }) {
57
- let [internalDate, setInternalDate] = useState(new Date());
58
- const date = dateProp ?? internalDate;
59
- const dateUtc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
60
- let dateValue = new Date(dateUtc);
61
-
62
- let text = formatter.format(dateValue);
63
- let altText = "";
64
-
65
- if (todayUtc === dateUtc) {
66
- altText = getLocalizedString("Today");
67
- } else if (todayUtc === dateUtc + oneDay) {
68
- altText = getLocalizedString("Yesterday");
69
- } else if (todayUtc === dateUtc - oneDay) {
70
- altText = getLocalizedString("Tomorrow");
55
+ export type DateNavigatorProps = {
56
+ date?: Date | string | number;
57
+ defaultDate?: Date | string | number; // For default uncontrolled value
58
+ disableTodayButton?: boolean;
59
+ onChange?: (date: Date) => void;
60
+ minDate?: Date | string | number;
61
+ maxDate?: Date | string | number;
62
+ };
63
+
64
+ function sanitizeDate(date: Date | string | number): Date {
65
+ if (typeof date === "string" || typeof date === "number") {
66
+ let parsedDate = new Date(date);
67
+ if (Number.isNaN(parsedDate.getTime())) {
68
+ throw new Error(`Invalid date value: ${date}`);
69
+ }
70
+ return parsedDate;
71
+ } else if (date instanceof Date) {
72
+ if (Number.isNaN(date.getTime())) {
73
+ throw new Error(`Invalid date object: ${date}`);
74
+ }
75
+ return new Date(date.getTime()); // Return a copy of the date
76
+ } else {
77
+ throw new Error(`Unsupported date type: ${typeof date}`);
78
+ }
79
+ }
80
+
81
+ export function DateNavigator({
82
+ date: dateProp,
83
+ defaultDate,
84
+ disableTodayButton,
85
+ onChange,
86
+ minDate: minDateProp,
87
+ maxDate: maxDateProp,
88
+ }: DateNavigatorProps) {
89
+ let [internalDate, setInternalDate] = useState(() => (defaultDate ? sanitizeDate(defaultDate) : new Date()));
90
+ let date = dateProp ? sanitizeDate(dateProp) : internalDate;
91
+ let dateUtc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
92
+ let today = new Date();
93
+ let todayUtc = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate());
94
+ let liveRef = useRef<HTMLDivElement>(null);
95
+
96
+ let text = (() => {
97
+ let base = formatter.format(new Date(dateUtc));
98
+ if (todayUtc === dateUtc) return `${getLocalizedString("Today")}, ${base}`;
99
+ if (todayUtc === dateUtc + oneDay) return `${getLocalizedString("Yesterday")}, ${base}`;
100
+ if (todayUtc === dateUtc - oneDay) return `${getLocalizedString("Tomorrow")}, ${base}`;
101
+ return base;
102
+ })();
103
+
104
+ let minDate = minDateProp && sanitizeDate(minDateProp);
105
+ let maxDate = maxDateProp && sanitizeDate(maxDateProp);
106
+ let disablePrev = !!minDate && dateUtc <= Date.UTC(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());
107
+ let disableNext = !!maxDate && dateUtc >= Date.UTC(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate());
108
+
109
+ function updateDate(newDate: Date) {
110
+ if (minDate && newDate < minDate) {
111
+ newDate = new Date(minDate); // Ensure we don't go below minDate
112
+ } else if (maxDate && newDate > maxDate) {
113
+ newDate = new Date(maxDate); // Ensure we don't go above maxDate
114
+ }
115
+
116
+ onChange?.(newDate);
117
+ setInternalDate(newDate);
71
118
  }
72
119
 
73
- if (altText) {
74
- text = `${altText}, ${text}`;
120
+ function jumpToToday() {
121
+ updateDate(new Date());
75
122
  }
76
123
 
77
- function handleDateChanged(date: Date) {
78
- if (typeof onChange === "function") {
79
- onChange(date);
80
- } else {
81
- setInternalDate(date);
124
+ useEffect(() => {
125
+ if (liveRef.current) {
126
+ liveRef.current.textContent = text;
127
+ }
128
+ }, [text]);
129
+
130
+ function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
131
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
132
+ if (event.defaultPrevented) return;
133
+ if (event.altKey || event.metaKey) return; // Ignore if alt or meta key is pressed
134
+
135
+ if (disablePrev && event.key === "ArrowLeft") return;
136
+ if (disableNext && event.key === "ArrowRight") return;
137
+
138
+ if (event.ctrlKey && event.shiftKey) {
139
+ // If both ctrl and shift are pressed, navigate by 1 year
140
+ let newDate = new Date(dateUtc);
141
+ if (event.key === "ArrowLeft") {
142
+ newDate.setFullYear(newDate.getFullYear() - 1);
143
+ } else if (event.key === "ArrowRight") {
144
+ newDate.setFullYear(newDate.getFullYear() + 1);
145
+ }
146
+ updateDate(newDate);
147
+ return;
148
+ }
149
+
150
+ if (event.ctrlKey) {
151
+ // Navigate by month
152
+ let newDate = new Date(dateUtc);
153
+ if (event.key === "ArrowLeft") {
154
+ newDate.setMonth(newDate.getMonth() - 1);
155
+ } else if (event.key === "ArrowRight") {
156
+ newDate.setMonth(newDate.getMonth() + 1);
157
+ }
158
+ updateDate(newDate);
159
+ return;
160
+ }
161
+
162
+ let modifier = event.shiftKey ? 7 : 1;
163
+ if (event.key === "ArrowLeft") {
164
+ updateDate(new Date(dateUtc - oneDay * modifier));
165
+ } else if (event.key === "ArrowRight") {
166
+ updateDate(new Date(dateUtc + oneDay * modifier));
82
167
  }
83
168
  }
84
169
 
85
170
  return (
86
- <div className="ObDateNavigator-root">
87
- <Arrow direction="left" onClick={() => handleDateChanged(new Date(dateUtc - oneDay))} />
171
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: I hate this component
172
+ <div className="ObDateNavigator-root" onKeyDown={handleKeyDown} tabIndex={0} aria-label="Date navigator">
173
+ <Arrow direction="left" onClick={() => updateDate(new Date(dateUtc - oneDay))} disabled={disablePrev} />
88
174
  <div className="ObDateNavigator-date">{text}</div>
89
- <Arrow direction="right" onClick={() => handleDateChanged(new Date(dateUtc + oneDay))} />
175
+ <Arrow direction="right" onClick={() => updateDate(new Date(dateUtc + oneDay))} disabled={disableNext} />
176
+ {!disableTodayButton && (
177
+ <button type="button" className="ObDateNavigator-today btn-reset" onClick={jumpToToday}>
178
+ {getLocalizedString("Today")}
179
+ </button>
180
+ )}
181
+ <div aria-live="polite" ref={liveRef} style={{ position: "absolute", left: "-9999px" }} />
90
182
  </div>
91
183
  );
92
184
  }
@@ -3,7 +3,13 @@
3
3
  $bullet-size: 0.7em;
4
4
  $line-width: 0.2em;
5
5
 
6
+ :root {
7
+ --ob-linked-list-bullet-size: 0.7em;
8
+ --ob-linked-list-line-width: 0.2em;
9
+ }
10
+
6
11
  .ObLinkedList-root {
12
+
7
13
  list-style-type: none;
8
14
  margin-left: 0;
9
15
  padding-left: 0;
@@ -47,10 +53,11 @@ $line-width: 0.2em;
47
53
  left: 0;
48
54
  top: 50%;
49
55
 
50
- transform: translate(calc(-1 * ($bullet-size / 2) + ($line-width / 2)), -50%);
56
+ transform: translate(calc(-1 * (var(--ob-linked-list-bullet-size) / 2) + (var(--ob-linked-list-line-width) / 2)), -50%);
51
57
  }
52
58
 
53
59
  .ObLinkedList-card {
60
+ display: block;
54
61
  background: white;
55
62
  border: 1px solid;
56
63
  border-color: variables.$color-border-top variables.$color-border-side variables.$color-border-bottom;
@@ -2,7 +2,7 @@ import "./index.scss";
2
2
 
3
3
  import clsx from "clsx";
4
4
 
5
- export function List({ children, className, ...other }: React.HTMLProps<HTMLUListElement>) {
5
+ export function LinkedList({ children, className, ...other }: React.HTMLProps<HTMLUListElement>) {
6
6
  return (
7
7
  <ul className={clsx("ObLinkedList-root", className)} {...other}>
8
8
  {children}
@@ -10,7 +10,7 @@ export function List({ children, className, ...other }: React.HTMLProps<HTMLULis
10
10
  );
11
11
  }
12
12
 
13
- export function ListItem({
13
+ export function LinkedListItem({
14
14
  children,
15
15
  className,
16
16
  label,
@@ -28,7 +28,7 @@
28
28
  transparent 66%
29
29
  ),
30
30
  linear-gradient(top, rgba(255, 255, 255, 0.25), rgba(0, 0, 0, 0.2)),
31
- linear-gradient(left, var(--brand-primary, variables.$brand-primary), var(--brand-light, variables.$brand-light));
31
+ linear-gradient(left, var(--brand-primary), var(--brand-light));
32
32
  background-image:
33
33
  -webkit-linear-gradient(
34
34
  135deg,
@@ -39,7 +39,7 @@
39
39
  transparent 66%
40
40
  ),
41
41
  -webkit-linear-gradient(top, rgba(255, 255, 255, 0.25), rgba(0, 0, 0, 0.2)),
42
- -webkit-linear-gradient(left, var(--brand-primary), variables.$brand-primary, var(--brand-light, variables.$brand-light));
42
+ -webkit-linear-gradient(left, var(--brand-primary), var(--brand-light));
43
43
  background-size:
44
44
  35px 20px,
45
45
  100% 100%,
@@ -1,4 +1,7 @@
1
- $bullet-size: 0.5em;
1
+
2
+ .ObSpinner-wrapper, .ObSpinner-fullPage {
3
+ --bullet-size: 0.5em;
4
+ }
2
5
 
3
6
  .ObSpinner-root {
4
7
  display: inline-block;
@@ -15,38 +18,38 @@ $bullet-size: 0.5em;
15
18
 
16
19
  .ObSpinner-spinner {
17
20
  display: inline-block;
18
- height: $bullet-size * 1.5;
21
+ height: calc(var(--bullet-size) * 1.5);
19
22
  position: relative;
20
- width: $bullet-size * 5;
23
+ width: calc(var(--bullet-size) * 5);
21
24
  }
22
25
 
23
26
  .ObSpinner-bullet {
24
27
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
25
28
  background: #666;
26
29
  border-radius: 50%;
27
- height: $bullet-size;
30
+ height: var(--bullet-size);
28
31
  position: absolute;
29
32
  top: 0.2rem;
30
- width: $bullet-size;
33
+ width: var(--bullet-size);
31
34
 
32
35
  &:nth-child(1) {
33
36
  animation: scaleAnimation 0.6s infinite ease-in-out;
34
- left: $bullet-size * 0.5;
37
+ left: calc(var(--bullet-size) * 0.5);
35
38
  }
36
39
 
37
40
  &:nth-child(2) {
38
41
  animation: translateAnimation 0.6s infinite ease-in-out;
39
- left: $bullet-size * 0.5;
42
+ left: calc(var(--bullet-size) * 0.5);
40
43
  }
41
44
 
42
45
  &:nth-child(3) {
43
46
  animation: translateAnimation 0.6s infinite ease-in-out;
44
- left: $bullet-size * 2.4;
47
+ left: calc(var(--bullet-size) * 2.4);
45
48
  }
46
49
 
47
50
  &:nth-child(4) {
48
51
  animation: scaleAnimation 0.6s infinite ease-in-out reverse;
49
- left: $bullet-size * 4;
52
+ left: calc(var(--bullet-size) * 4);
50
53
  }
51
54
  }
52
55
 
@@ -66,6 +69,6 @@ $bullet-size: 0.5em;
66
69
  }
67
70
 
68
71
  to {
69
- transform: translate($bullet-size * 1.65, 0);
72
+ transform: translate(calc(var(--bullet-size) * 1.65), 0);
70
73
  }
71
74
  }
@@ -1,4 +1,4 @@
1
- import "./index.scss";
1
+ import "./index.css";
2
2
 
3
3
  import clsx from "clsx";
4
4
  import type React from "react";
@@ -60,11 +60,24 @@ type DividerProps = {
60
60
  className?: string;
61
61
  direction: "horizontal" | "vertical";
62
62
  isDragging: boolean;
63
+ min: number;
64
+ max: number;
65
+ size: number;
63
66
  };
64
67
 
65
68
  const Divider = forwardRef<HTMLDivElement, DividerProps>(function Divider(props: DividerProps, ref) {
66
69
  return (
67
- <div className={clsx("ObSplitContainer-divider", props.className)} ref={ref}>
70
+ <div
71
+ className={clsx("ObSplitContainer-divider", props.className)}
72
+ tabIndex={0}
73
+ ref={ref}
74
+ // biome-ignore lint/a11y/useSemanticElements: this is allowed according to WAI-ARIA
75
+ role="separator"
76
+ aria-orientation={props.direction}
77
+ aria-valuemin={props.min}
78
+ aria-valuemax={props.max}
79
+ aria-valuenow={props.size}
80
+ >
68
81
  <span>•••</span>
69
82
  </div>
70
83
  );
@@ -115,6 +128,8 @@ export type SplitContainerProps = {
115
128
  direction?: "horizontal" | "vertical";
116
129
  fixedPanel?: 1 | 2;
117
130
  defaultFixedSize?: number;
131
+ min?: number;
132
+ max?: number;
118
133
  /**
119
134
  * When the fixed size is persisted, changing the layoutVersion will force clients
120
135
  * to reset to the default value.
@@ -134,6 +149,8 @@ export function SplitContainer({
134
149
  panelClassName,
135
150
  defaultFixedSize = 400,
136
151
  layoutVersion = 1,
152
+ min = 0,
153
+ max,
137
154
  id,
138
155
  }: SplitContainerProps) {
139
156
  let [fixedSize, setFixedSize] = useState<number>(defaultFixedSize);
@@ -144,18 +161,23 @@ export function SplitContainer({
144
161
  throw Error("SplitContainer requires exactly 2 children");
145
162
  }
146
163
 
147
- function handleFixedSizeChanged(size: React.SetStateAction<number>) {
148
- setFixedSize((current) => {
149
- let newValue: number = typeof size === "function" ? size(current) : size;
164
+ let containerRef = useRef<HTMLDivElement>(null);
165
+ let [containerSize, setContainerSize] = useState<number>(0);
150
166
 
151
- if (id) {
152
- let key = `${article}.${id}`;
153
- persistState(key, layoutVersion, newValue);
154
- }
167
+ useEffect(() => {
168
+ if (!containerRef.current) return;
155
169
 
156
- return newValue;
170
+ let resizeObserver = new ResizeObserver((entries) => {
171
+ for (let entry of entries) {
172
+ if (entry.contentRect) {
173
+ setContainerSize(direction === "horizontal" ? entry.contentRect.width : entry.contentRect.height);
174
+ }
175
+ }
157
176
  });
158
- }
177
+
178
+ resizeObserver.observe(containerRef.current);
179
+ return () => resizeObserver.disconnect();
180
+ }, [direction]);
159
181
 
160
182
  useEffect(() => {
161
183
  if (id) {
@@ -175,7 +197,6 @@ export function SplitContainer({
175
197
 
176
198
  let childrenArray = Children.toArray(children);
177
199
 
178
- // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
179
200
  useEffect(() => {
180
201
  let divider = dividerRef.current;
181
202
  if (divider) {
@@ -188,6 +209,17 @@ export function SplitContainer({
188
209
  fixedPanel === 1 ? divider.previousElementSibling : divider.nextElementSibling
189
210
  ) as HTMLElement;
190
211
 
212
+ function clampSize(size: number) {
213
+ let adjustedContainerSize = containerSize - 10; // make sure divider will not be stuck outside the container
214
+ let maxSize = Math.min(max ?? adjustedContainerSize, adjustedContainerSize);
215
+ if (size < min) {
216
+ size = min;
217
+ } else if (size > maxSize) {
218
+ size = maxSize;
219
+ }
220
+ return size;
221
+ }
222
+
191
223
  function getNewSize(event: PointerEvent) {
192
224
  let deltaSize =
193
225
  direction === "horizontal"
@@ -196,7 +228,20 @@ export function SplitContainer({
196
228
 
197
229
  deltaSize *= fixedPanel === 1 ? -1 : 1;
198
230
 
199
- return initialPosition.size - deltaSize;
231
+ return clampSize(initialPosition.size - deltaSize);
232
+ }
233
+
234
+ function handleFixedSizeChanged(size: React.SetStateAction<number>) {
235
+ setFixedSize((current) => {
236
+ let newValue: number = clampSize(typeof size === "function" ? size(current) : size);
237
+
238
+ if (id) {
239
+ let key = `${article}.${id}`;
240
+ persistState(key, layoutVersion, newValue);
241
+ }
242
+
243
+ return newValue;
244
+ });
200
245
  }
201
246
 
202
247
  function handlePointerUp(event: PointerEvent) {
@@ -212,6 +257,10 @@ export function SplitContainer({
212
257
  event.preventDefault();
213
258
 
214
259
  let newSize = getNewSize(event);
260
+ let divider = dividerRef.current;
261
+ if (divider) {
262
+ divider.setAttribute("aria-valuenow", String(newSize));
263
+ }
215
264
  movingPanel.style.flex = `0 0 ${newSize}px`;
216
265
  }
217
266
 
@@ -262,14 +311,21 @@ export function SplitContainer({
262
311
  divider?.removeEventListener("keydown", handleKeyDown);
263
312
  };
264
313
  }
265
- }, [direction, fixedPanel]);
314
+ }, [direction, fixedPanel, layoutVersion, id, min, max, containerSize]);
266
315
 
267
316
  return (
268
- <SplitRoot direction={direction} className={className}>
317
+ <SplitRoot direction={direction} className={className} ref={containerRef}>
269
318
  <SplitPanel className={clsx(isDragging && "dragging", panelClassName)} fixed={fixedPanel === 1} size={fixedSize}>
270
319
  {childrenArray[0]}
271
320
  </SplitPanel>
272
- <Divider ref={dividerRef} direction={direction} isDragging={isDragging} />
321
+ <Divider
322
+ ref={dividerRef}
323
+ direction={direction}
324
+ isDragging={isDragging}
325
+ size={fixedSize}
326
+ min={min}
327
+ max={max ?? containerSize}
328
+ />
273
329
  <SplitPanel className={clsx(isDragging && "dragging", panelClassName)} fixed={fixedPanel === 2} size={fixedSize}>
274
330
  {childrenArray[1]}
275
331
  </SplitPanel>
@@ -0,0 +1,79 @@
1
+ .ObValueToggleGroup {
2
+ padding: 0.25rem;
3
+ border: 2px solid rgb(0 0 0 / 0.2);
4
+ border-radius: 4px;
5
+
6
+ display: flex;
7
+ gap: 0.25rem;
8
+ flex-wrap: wrap;
9
+
10
+ &.vertical {
11
+ flex-direction: column;
12
+ align-items: center;
13
+
14
+ & .ObValueToggleOption {
15
+ width: 100%;
16
+
17
+ & .ObValueToggleLabel {
18
+ width: 100%;
19
+ text-align: center;
20
+ }
21
+ }
22
+ }
23
+
24
+ &:focus-within {
25
+ border-color: rgb(0 0 0 / 0.8);
26
+ }
27
+ }
28
+
29
+ .ObValueToggleOption {
30
+ display: flex;
31
+ align-items: center;
32
+ margin: 0;
33
+ position: relative;
34
+ }
35
+
36
+ .ObValueToggleOption input {
37
+ position: absolute;
38
+ width: 1px;
39
+ height: 1px;
40
+ padding: 0;
41
+ margin: -1px;
42
+ overflow: hidden;
43
+ clip: rect(0, 0, 0, 0);
44
+ border: 0;
45
+ }
46
+
47
+ .ObValueToggleLabel {
48
+ font-size: 0.8em;
49
+ margin: 0 0.2em;
50
+ padding: 0.3em 0.5em;
51
+ text-transform: uppercase;
52
+ transition: background 0.2s ease, color 0.2s ease;
53
+ border-radius: 4px;
54
+ }
55
+
56
+ .ObValueToggleOption:hover input:not(:disabled) + .ObValueToggleLabel {
57
+ cursor: pointer;
58
+ background: #ccc;
59
+ }
60
+
61
+ .ObValueToggleOption input:checked + .ObValueToggleLabel {
62
+ --color-bg: var(--brand-dark);
63
+ background-color: var(--color-bg);
64
+ color: white;
65
+ cursor: default;
66
+ }
67
+
68
+ .ObValueToggleOption input:disabled + .ObValueToggleLabel {
69
+ color: rgb(0 0 0 / 0.5);
70
+ }
71
+
72
+ .ObValueToggleOption input:checked:focus-visible + .ObValueToggleLabel {
73
+ --color-bg: var(--brand-light);
74
+ outline: 2px solid var(--brand-light);
75
+ }
76
+
77
+ .ObValueToggleOption input:checked:hover + .ObValueToggleLabel {
78
+ background-color: var(--color-bg);
79
+ }