react-cheminfo 0.44.0 → 0.45.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.
package/lib/core.d.ts CHANGED
@@ -28,6 +28,7 @@ export * from './scatter3d/core/index.ts';
28
28
  export * from './seo/core/index.ts';
29
29
  export * from './share/core/index.ts';
30
30
  export * from './slides/core/index.ts';
31
+ export * from './split/core/index.ts';
31
32
  export * from './state/core/index.ts';
32
33
  export * from './structure/core/index.ts';
33
34
  export * from './tokens/core/index.ts';
package/lib/core.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
package/lib/core.js CHANGED
@@ -30,6 +30,7 @@ export * from "./share/core/index.js";
30
30
  // The deck format and its navigation are framework-free, so a backend can read
31
31
  // a manifest without loading React: `react-cheminfo/slides` is the player.
32
32
  export * from "./slides/core/index.js";
33
+ export * from "./split/core/index.js";
33
34
  export * from "./state/core/index.js";
34
35
  // Framework-free molfile and editor-value helpers, so a worker can read a
35
36
  // molfile without loading the editor that `react-cheminfo/structure` exports.
package/lib/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,+EAA+E;AAC/E,2EAA2E;AAC3E,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,0EAA0E;AAC1E,8EAA8E;AAC9E,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,+EAA+E;AAC/E,2EAA2E;AAC3E,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,0EAA0E;AAC1E,8EAA8E;AAC9E,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export type { SplitRange } from './split.ts';
2
+ export { MAX_SPLIT, MIN_SPLIT, SPLIT_PARAM, clampSplit, splitParam, } from './split.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/split/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { MAX_SPLIT, MIN_SPLIT, SPLIT_PARAM, clampSplit, splitParam, } from "./split.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/split/core/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * How a row holding two figures is divided, and how that travels in a link.
3
+ *
4
+ * The share is the *first* pane's, as a whole percentage, because it is written
5
+ * into the address: `?split=35` is a number somebody can read off a link,
6
+ * retype into a slide and hand out. A link that names none opens the page at
7
+ * its own default, and nothing is written until a splitter has been dragged, so
8
+ * an ordinary link stays an ordinary link.
9
+ *
10
+ * One name across the family: a reader who has learned to widen the chart on
11
+ * one of our tools has learned it on all of them.
12
+ */
13
+ import type { ShareParamCodec } from '../../share/core/params.ts';
14
+ /** Query parameter a row's division is carried in, on every site. */
15
+ export declare const SPLIT_PARAM = "split";
16
+ /**
17
+ * The narrowest either pane is left at. Under a fifth of the row neither side
18
+ * is read, and a drag that lands there is one nobody can undo without the
19
+ * splitter they have just squeezed out of sight.
20
+ */
21
+ export declare const MIN_SPLIT = 20;
22
+ /** The widest the first pane goes, which is {@link MIN_SPLIT} from the other end. */
23
+ export declare const MAX_SPLIT: number;
24
+ /** The range a row may be divided in, where a page wants its own. */
25
+ export interface SplitRange {
26
+ /**
27
+ * The narrowest the first pane is left at.
28
+ * @default 20
29
+ */
30
+ min?: number;
31
+ /**
32
+ * The widest the first pane is left at.
33
+ * @default 80
34
+ */
35
+ max?: number;
36
+ }
37
+ /**
38
+ * The share to keep, given one asked for by a drag or named by a link.
39
+ * @param value - The share asked for.
40
+ * @param range - The range this row is divided in.
41
+ * @returns A whole percentage inside it.
42
+ */
43
+ export declare function clampSplit(value: number, range?: SplitRange): number;
44
+ /**
45
+ * The codec a site reads `split=` with, for its share vocabulary or its route
46
+ * table. `null` is "the page's own share", which is what an absent parameter
47
+ * and an unreadable one both mean, and what writes no parameter at all.
48
+ * @param range - The range this row is divided in.
49
+ * @returns The codec.
50
+ */
51
+ export declare function splitParam(range?: SplitRange): ShareParamCodec<number | null>;
52
+ //# sourceMappingURL=split.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split.d.ts","sourceRoot":"","sources":["../../../src/split/core/split.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGlE,qEAAqE;AACrE,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC;;;;GAIG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,qFAAqF;AACrF,eAAO,MAAM,SAAS,QAAkB,CAAC;AAEzC,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,UAAe,GAAG,MAAM,CAGxE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,KAAK,GAAE,UAAe,GACrB,eAAe,CAAC,MAAM,GAAG,IAAI,CAAC,CAGhC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * How a row holding two figures is divided, and how that travels in a link.
3
+ *
4
+ * The share is the *first* pane's, as a whole percentage, because it is written
5
+ * into the address: `?split=35` is a number somebody can read off a link,
6
+ * retype into a slide and hand out. A link that names none opens the page at
7
+ * its own default, and nothing is written until a splitter has been dragged, so
8
+ * an ordinary link stays an ordinary link.
9
+ *
10
+ * One name across the family: a reader who has learned to widen the chart on
11
+ * one of our tools has learned it on all of them.
12
+ */
13
+ import { integerParam } from "../../share/core/params.js";
14
+ /** Query parameter a row's division is carried in, on every site. */
15
+ export const SPLIT_PARAM = 'split';
16
+ /**
17
+ * The narrowest either pane is left at. Under a fifth of the row neither side
18
+ * is read, and a drag that lands there is one nobody can undo without the
19
+ * splitter they have just squeezed out of sight.
20
+ */
21
+ export const MIN_SPLIT = 20;
22
+ /** The widest the first pane goes, which is {@link MIN_SPLIT} from the other end. */
23
+ export const MAX_SPLIT = 100 - MIN_SPLIT;
24
+ /**
25
+ * The share to keep, given one asked for by a drag or named by a link.
26
+ * @param value - The share asked for.
27
+ * @param range - The range this row is divided in.
28
+ * @returns A whole percentage inside it.
29
+ */
30
+ export function clampSplit(value, range = {}) {
31
+ const { min = MIN_SPLIT, max = MAX_SPLIT } = range;
32
+ return Math.min(max, Math.max(min, Math.round(value)));
33
+ }
34
+ /**
35
+ * The codec a site reads `split=` with, for its share vocabulary or its route
36
+ * table. `null` is "the page's own share", which is what an absent parameter
37
+ * and an unreadable one both mean, and what writes no parameter at all.
38
+ * @param range - The range this row is divided in.
39
+ * @returns The codec.
40
+ */
41
+ export function splitParam(range = {}) {
42
+ const { min = MIN_SPLIT, max = MAX_SPLIT } = range;
43
+ return integerParam({ min, max, default: null });
44
+ }
45
+ //# sourceMappingURL=split.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split.js","sourceRoot":"","sources":["../../../src/split/core/split.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,qEAAqE;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAEnC;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAE5B,qFAAqF;AACrF,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,GAAG,SAAS,CAAC;AAgBzC;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,QAAoB,EAAE;IAC9D,MAAM,EAAE,GAAG,GAAG,SAAS,EAAE,GAAG,GAAG,SAAS,EAAE,GAAG,KAAK,CAAC;IACnD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,QAAoB,EAAE;IAEtB,MAAM,EAAE,GAAG,GAAG,SAAS,EAAE,GAAG,GAAG,SAAS,EAAE,GAAG,KAAK,CAAC;IACnD,OAAO,YAAY,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Two figures side by side, with a splitter between them and the share each
3
+ * takes written into the address.
4
+ *
5
+ * How much room the tool deserves beside the figure it feeds is not ours to
6
+ * decide: somebody embedding the chart wants it nearly whole, somebody
7
+ * teaching off the picker wants the picker nearly whole, and both want the
8
+ * link they hand out to open the way they left it. So the splitter is dragged,
9
+ * the share it comes to rest at goes into `?split=`, and the address is the
10
+ * figure.
11
+ *
12
+ * Two things it cannot do, and must not pretend to: on a narrow screen there
13
+ * is no room for two columns at all, so the panes stack and no splitter is
14
+ * drawn; and a link that hid everything one pane holds leaves the other the
15
+ * whole width rather than a share of it.
16
+ *
17
+ * The bar itself is react-science's `SplitPane`, and it keeps react-science's
18
+ * look — barely visible on purpose, found by the pointer that changes over it
19
+ * rather than by the eye. All `chrome.css` says about it is that eleven pixels
20
+ * is more gutter than two figures need.
21
+ */
22
+ import type { ReactElement, ReactNode } from 'react';
23
+ import type { SplitRange } from '../core/split.ts';
24
+ /** Props of {@link SplitRow}. */
25
+ export interface SplitRowProps extends SplitRange {
26
+ /**
27
+ * Share of the row the first pane takes, as a whole percentage, or `null`
28
+ * while the page sits at {@link SplitRowProps.defaultRatio}.
29
+ */
30
+ ratio: number | null;
31
+ /** The share the row opens at when the address names none. */
32
+ defaultRatio: number;
33
+ /**
34
+ * Take the share a drag came to rest at, or `null` when the splitter was
35
+ * double-clicked back to the default.
36
+ */
37
+ onRatio: (ratio: number | null) => void;
38
+ /** The left-hand pane, or `null` when a link left nothing in it. */
39
+ start: ReactNode;
40
+ /** The right-hand pane, or `null` when a link left nothing in it. */
41
+ end: ReactNode;
42
+ /**
43
+ * The narrowest row that is still split; under it the two panes stack.
44
+ * @default 860
45
+ */
46
+ stackBelow?: number;
47
+ /**
48
+ * The gutter each pane keeps beside the splitter, in pixels, so neither
49
+ * figure is read against its edge.
50
+ * @default 6
51
+ */
52
+ gutter?: number;
53
+ /**
54
+ * The gap between the two panes once they have stacked, in pixels.
55
+ * @default 16
56
+ */
57
+ stackedGap?: number;
58
+ }
59
+ /**
60
+ * Two panes sharing a row, divided where the address says.
61
+ * @param props - See {@link SplitRowProps}.
62
+ * @returns The two panes with the splitter between them, the two stacked on a
63
+ * narrow screen, or whichever one a link left on the page, alone.
64
+ */
65
+ export declare function SplitRow(props: SplitRowProps): ReactElement | null;
66
+ //# sourceMappingURL=SplitRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SplitRow.d.ts","sourceRoot":"","sources":["../../../src/split/ui/SplitRow.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAiB,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,iCAAiC;AACjC,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACxC,oEAAoE;IACpE,KAAK,EAAE,SAAS,CAAC;IACjB,qEAAqE;IACrE,GAAG,EAAE,SAAS,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,GAAG,IAAI,CAwFlE"}
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { SplitPane } from 'react-science/ui';
4
+ import { useResizeObserver } from "../../hooks/ui/useResizeObserver.js";
5
+ import { clampSplit } from "../core/split.js";
6
+ /**
7
+ * Two panes sharing a row, divided where the address says.
8
+ * @param props - See {@link SplitRowProps}.
9
+ * @returns The two panes with the splitter between them, the two stacked on a
10
+ * narrow screen, or whichever one a link left on the page, alone.
11
+ */
12
+ export function SplitRow(props) {
13
+ const { ratio, defaultRatio, onRatio, start, end, min, max, stackBelow = STACK_BELOW, gutter = GUTTER, stackedGap = STACKED_GAP, } = props;
14
+ const row = useRef(null);
15
+ const [width, setWidth] = useState(windowWidth);
16
+ // The share under the pointer, which is not the one in the address: a drag
17
+ // reports every pixel it crosses, and the address is rewritten once, where
18
+ // the splitter is let go.
19
+ const [dragged, setDragged] = useState(null);
20
+ useResizeObserver(row, (size) => {
21
+ setWidth(size.width);
22
+ });
23
+ if (start === null && end === null)
24
+ return null;
25
+ const stacked = start === null || end === null || width < stackBelow;
26
+ const startPane = start === null ? null : (_jsx("div", { "data-testid": "split-start", style: stacked ? STACKED_PANE : paneStyle('start', gutter), children: start }));
27
+ const endPane = end === null ? null : (_jsx("div", { "data-testid": "split-end", style: stacked ? STACKED_PANE : paneStyle('end', gutter), children: end }));
28
+ if (stacked) {
29
+ return (_jsxs("div", { ref: row, style: { ...STACK, gap: stackedGap }, children: [startPane, endPane] }));
30
+ }
31
+ const share = clampSplit(dragged ?? ratio ?? defaultRatio, { min, max });
32
+ return (_jsx("div", { ref: row, className: "split-row", children: _jsxs(SplitPane, { direction: "horizontal", controlledSide: "start", size: `${share}%`,
33
+ // Held open, so the only layout a visitor can reach is one the address
34
+ // describes: left to itself a double click collapses a pane, and a link
35
+ // shared from there would open with the pane back. The double click is
36
+ // answered here instead, by returning to the page's own share.
37
+ open: true, onOpenChange: () => {
38
+ setDragged(null);
39
+ onRatio(null);
40
+ }, onSizeChange: (asked) => {
41
+ setDragged(shareOf(asked, share, { min, max }));
42
+ }, onResize: (asked) => {
43
+ const rested = shareOf(asked, share, { min, max });
44
+ setDragged(null);
45
+ onRatio(rested);
46
+ }, children: [startPane, endPane] }) }));
47
+ }
48
+ /**
49
+ * The share a splitter asked for.
50
+ * @param asked - What it reported, a percentage because that is what it was
51
+ * given.
52
+ * @param current - The share in force, kept when the report is not a number.
53
+ * @param range - The range this row is divided in.
54
+ * @returns A whole percentage inside it.
55
+ */
56
+ function shareOf(asked, current, range) {
57
+ const value = Number.parseFloat(asked);
58
+ return Number.isFinite(value) ? clampSplit(value, range) : current;
59
+ }
60
+ /**
61
+ * How wide the window is, which is what the row is taken to be until it has
62
+ * been measured. Reading it rather than starting at zero is what keeps a wide
63
+ * page from laying itself out stacked and splitting a frame later, which would
64
+ * build both figures twice on every load.
65
+ * @returns The width in pixels, or the threshold itself where there is no
66
+ * window to read — a test, or a page rendered to a string — which is the
67
+ * narrowest a row is still split at.
68
+ */
69
+ function windowWidth() {
70
+ const { innerWidth } = globalThis;
71
+ return typeof innerWidth === 'number' && innerWidth > 0
72
+ ? innerWidth
73
+ : STACK_BELOW;
74
+ }
75
+ /** The narrowest row that is still split, in pixels. */
76
+ const STACK_BELOW = 860;
77
+ /** The gutter a pane keeps beside the splitter, in pixels. */
78
+ const GUTTER = 6;
79
+ /** The gap between the panes once they have stacked, in pixels. */
80
+ const STACKED_GAP = 16;
81
+ const STACK = {
82
+ display: 'flex',
83
+ flexDirection: 'column',
84
+ };
85
+ const STACKED_PANE = {
86
+ display: 'flex',
87
+ flexDirection: 'column',
88
+ minWidth: 0,
89
+ };
90
+ function paneStyle(side, gutter) {
91
+ return {
92
+ display: 'flex',
93
+ flex: '1 1 0%',
94
+ flexDirection: 'column',
95
+ minWidth: 0,
96
+ [side === 'start' ? 'paddingRight' : 'paddingLeft']: gutter,
97
+ };
98
+ }
99
+ //# sourceMappingURL=SplitRow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SplitRow.js","sourceRoot":"","sources":["../../../src/split/ui/SplitRow.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export type { SplitRowProps } from './SplitRow.tsx';
2
+ export { SplitRow } from './SplitRow.tsx';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/split/ui/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { SplitRow } from "./SplitRow.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/split/ui/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAgB,CAAC"}
package/lib/ui.d.ts CHANGED
@@ -30,6 +30,7 @@ export * from './scatter/ui/index.ts';
30
30
  export * from './scatter3d/ui/index.ts';
31
31
  export * from './share/ui/index.ts';
32
32
  export * from './shared/ui/index.ts';
33
+ export * from './split/ui/index.ts';
33
34
  export type { ConformerTableProps } from './structure/ui/ConformerTable.tsx';
34
35
  export { ConformerTable } from './structure/ui/ConformerTable.tsx';
35
36
  export type { ConformerColumn } from './structure/ui/conformerColumns.ts';
package/lib/ui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AAKrC,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AAKpC,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
package/lib/ui.js CHANGED
@@ -30,6 +30,7 @@ export * from "./scatter/ui/index.js";
30
30
  export * from "./scatter3d/ui/index.js";
31
31
  export * from "./share/ui/index.js";
32
32
  export * from "./shared/ui/index.js";
33
+ export * from "./split/ui/index.js";
33
34
  export { ConformerTable } from "./structure/ui/ConformerTable.js";
34
35
  export { CONFORMER_COLUMN_LABELS, PICKER_COLUMNS, RANKING_COLUMNS, } from "./structure/ui/conformerColumns.js";
35
36
  //# sourceMappingURL=ui.js.map
package/lib/ui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AAMrC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAmC,CAAC;AAEnE,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AAMpC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAmC,CAAC;AAEnE,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cheminfo",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "license": "MIT",
5
5
  "description": "React components, hooks and framework-free helpers shared by the cheminfo sites: site chrome, citations, sharing, routing, pedagogy, charts, and the structure, orbital and 3D molecule viewers",
6
6
  "author": "Luc Patiny",
package/src/core.ts CHANGED
@@ -30,6 +30,7 @@ export * from './share/core/index.ts';
30
30
  // The deck format and its navigation are framework-free, so a backend can read
31
31
  // a manifest without loading React: `react-cheminfo/slides` is the player.
32
32
  export * from './slides/core/index.ts';
33
+ export * from './split/core/index.ts';
33
34
  export * from './state/core/index.ts';
34
35
  // Framework-free molfile and editor-value helpers, so a worker can read a
35
36
  // molfile without loading the editor that `react-cheminfo/structure` exports.
@@ -0,0 +1,8 @@
1
+ export type { SplitRange } from './split.ts';
2
+ export {
3
+ MAX_SPLIT,
4
+ MIN_SPLIT,
5
+ SPLIT_PARAM,
6
+ clampSplit,
7
+ splitParam,
8
+ } from './split.ts';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * How a row holding two figures is divided, and how that travels in a link.
3
+ *
4
+ * The share is the *first* pane's, as a whole percentage, because it is written
5
+ * into the address: `?split=35` is a number somebody can read off a link,
6
+ * retype into a slide and hand out. A link that names none opens the page at
7
+ * its own default, and nothing is written until a splitter has been dragged, so
8
+ * an ordinary link stays an ordinary link.
9
+ *
10
+ * One name across the family: a reader who has learned to widen the chart on
11
+ * one of our tools has learned it on all of them.
12
+ */
13
+
14
+ import type { ShareParamCodec } from '../../share/core/params.ts';
15
+ import { integerParam } from '../../share/core/params.ts';
16
+
17
+ /** Query parameter a row's division is carried in, on every site. */
18
+ export const SPLIT_PARAM = 'split';
19
+
20
+ /**
21
+ * The narrowest either pane is left at. Under a fifth of the row neither side
22
+ * is read, and a drag that lands there is one nobody can undo without the
23
+ * splitter they have just squeezed out of sight.
24
+ */
25
+ export const MIN_SPLIT = 20;
26
+
27
+ /** The widest the first pane goes, which is {@link MIN_SPLIT} from the other end. */
28
+ export const MAX_SPLIT = 100 - MIN_SPLIT;
29
+
30
+ /** The range a row may be divided in, where a page wants its own. */
31
+ export interface SplitRange {
32
+ /**
33
+ * The narrowest the first pane is left at.
34
+ * @default 20
35
+ */
36
+ min?: number;
37
+ /**
38
+ * The widest the first pane is left at.
39
+ * @default 80
40
+ */
41
+ max?: number;
42
+ }
43
+
44
+ /**
45
+ * The share to keep, given one asked for by a drag or named by a link.
46
+ * @param value - The share asked for.
47
+ * @param range - The range this row is divided in.
48
+ * @returns A whole percentage inside it.
49
+ */
50
+ export function clampSplit(value: number, range: SplitRange = {}): number {
51
+ const { min = MIN_SPLIT, max = MAX_SPLIT } = range;
52
+ return Math.min(max, Math.max(min, Math.round(value)));
53
+ }
54
+
55
+ /**
56
+ * The codec a site reads `split=` with, for its share vocabulary or its route
57
+ * table. `null` is "the page's own share", which is what an absent parameter
58
+ * and an unreadable one both mean, and what writes no parameter at all.
59
+ * @param range - The range this row is divided in.
60
+ * @returns The codec.
61
+ */
62
+ export function splitParam(
63
+ range: SplitRange = {},
64
+ ): ShareParamCodec<number | null> {
65
+ const { min = MIN_SPLIT, max = MAX_SPLIT } = range;
66
+ return integerParam({ min, max, default: null });
67
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Two figures side by side, with a splitter between them and the share each
3
+ * takes written into the address.
4
+ *
5
+ * How much room the tool deserves beside the figure it feeds is not ours to
6
+ * decide: somebody embedding the chart wants it nearly whole, somebody
7
+ * teaching off the picker wants the picker nearly whole, and both want the
8
+ * link they hand out to open the way they left it. So the splitter is dragged,
9
+ * the share it comes to rest at goes into `?split=`, and the address is the
10
+ * figure.
11
+ *
12
+ * Two things it cannot do, and must not pretend to: on a narrow screen there
13
+ * is no room for two columns at all, so the panes stack and no splitter is
14
+ * drawn; and a link that hid everything one pane holds leaves the other the
15
+ * whole width rather than a share of it.
16
+ *
17
+ * The bar itself is react-science's `SplitPane`, and it keeps react-science's
18
+ * look — barely visible on purpose, found by the pointer that changes over it
19
+ * rather than by the eye. All `chrome.css` says about it is that eleven pixels
20
+ * is more gutter than two figures need.
21
+ */
22
+
23
+ import type { CSSProperties, ReactElement, ReactNode } from 'react';
24
+ import { useRef, useState } from 'react';
25
+ import type { SplitPaneSize } from 'react-science/ui';
26
+ import { SplitPane } from 'react-science/ui';
27
+
28
+ import { useResizeObserver } from '../../hooks/ui/useResizeObserver.ts';
29
+ import type { SplitRange } from '../core/split.ts';
30
+ import { clampSplit } from '../core/split.ts';
31
+
32
+ /** Props of {@link SplitRow}. */
33
+ export interface SplitRowProps extends SplitRange {
34
+ /**
35
+ * Share of the row the first pane takes, as a whole percentage, or `null`
36
+ * while the page sits at {@link SplitRowProps.defaultRatio}.
37
+ */
38
+ ratio: number | null;
39
+ /** The share the row opens at when the address names none. */
40
+ defaultRatio: number;
41
+ /**
42
+ * Take the share a drag came to rest at, or `null` when the splitter was
43
+ * double-clicked back to the default.
44
+ */
45
+ onRatio: (ratio: number | null) => void;
46
+ /** The left-hand pane, or `null` when a link left nothing in it. */
47
+ start: ReactNode;
48
+ /** The right-hand pane, or `null` when a link left nothing in it. */
49
+ end: ReactNode;
50
+ /**
51
+ * The narrowest row that is still split; under it the two panes stack.
52
+ * @default 860
53
+ */
54
+ stackBelow?: number;
55
+ /**
56
+ * The gutter each pane keeps beside the splitter, in pixels, so neither
57
+ * figure is read against its edge.
58
+ * @default 6
59
+ */
60
+ gutter?: number;
61
+ /**
62
+ * The gap between the two panes once they have stacked, in pixels.
63
+ * @default 16
64
+ */
65
+ stackedGap?: number;
66
+ }
67
+
68
+ /**
69
+ * Two panes sharing a row, divided where the address says.
70
+ * @param props - See {@link SplitRowProps}.
71
+ * @returns The two panes with the splitter between them, the two stacked on a
72
+ * narrow screen, or whichever one a link left on the page, alone.
73
+ */
74
+ export function SplitRow(props: SplitRowProps): ReactElement | null {
75
+ const {
76
+ ratio,
77
+ defaultRatio,
78
+ onRatio,
79
+ start,
80
+ end,
81
+ min,
82
+ max,
83
+ stackBelow = STACK_BELOW,
84
+ gutter = GUTTER,
85
+ stackedGap = STACKED_GAP,
86
+ } = props;
87
+ const row = useRef<HTMLDivElement>(null);
88
+ const [width, setWidth] = useState(windowWidth);
89
+ // The share under the pointer, which is not the one in the address: a drag
90
+ // reports every pixel it crosses, and the address is rewritten once, where
91
+ // the splitter is let go.
92
+ const [dragged, setDragged] = useState<number | null>(null);
93
+
94
+ useResizeObserver(row, (size) => {
95
+ setWidth(size.width);
96
+ });
97
+
98
+ if (start === null && end === null) return null;
99
+
100
+ const stacked = start === null || end === null || width < stackBelow;
101
+ const startPane =
102
+ start === null ? null : (
103
+ <div
104
+ data-testid="split-start"
105
+ style={stacked ? STACKED_PANE : paneStyle('start', gutter)}
106
+ >
107
+ {start}
108
+ </div>
109
+ );
110
+ const endPane =
111
+ end === null ? null : (
112
+ <div
113
+ data-testid="split-end"
114
+ style={stacked ? STACKED_PANE : paneStyle('end', gutter)}
115
+ >
116
+ {end}
117
+ </div>
118
+ );
119
+
120
+ if (stacked) {
121
+ return (
122
+ <div ref={row} style={{ ...STACK, gap: stackedGap }}>
123
+ {startPane}
124
+ {endPane}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ const share = clampSplit(dragged ?? ratio ?? defaultRatio, { min, max });
130
+
131
+ return (
132
+ // `split-row` is what `chrome.css` reaches the bar through: it is
133
+ // react-science's own element and carries no class of ours.
134
+ <div ref={row} className="split-row">
135
+ <SplitPane
136
+ direction="horizontal"
137
+ controlledSide="start"
138
+ size={`${share}%`}
139
+ // Held open, so the only layout a visitor can reach is one the address
140
+ // describes: left to itself a double click collapses a pane, and a link
141
+ // shared from there would open with the pane back. The double click is
142
+ // answered here instead, by returning to the page's own share.
143
+ open
144
+ onOpenChange={() => {
145
+ setDragged(null);
146
+ onRatio(null);
147
+ }}
148
+ onSizeChange={(asked) => {
149
+ setDragged(shareOf(asked, share, { min, max }));
150
+ }}
151
+ onResize={(asked) => {
152
+ const rested = shareOf(asked, share, { min, max });
153
+ setDragged(null);
154
+ onRatio(rested);
155
+ }}
156
+ >
157
+ {startPane}
158
+ {endPane}
159
+ </SplitPane>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ /**
165
+ * The share a splitter asked for.
166
+ * @param asked - What it reported, a percentage because that is what it was
167
+ * given.
168
+ * @param current - The share in force, kept when the report is not a number.
169
+ * @param range - The range this row is divided in.
170
+ * @returns A whole percentage inside it.
171
+ */
172
+ function shareOf(
173
+ asked: SplitPaneSize,
174
+ current: number,
175
+ range: SplitRange,
176
+ ): number {
177
+ const value = Number.parseFloat(asked);
178
+ return Number.isFinite(value) ? clampSplit(value, range) : current;
179
+ }
180
+
181
+ /**
182
+ * How wide the window is, which is what the row is taken to be until it has
183
+ * been measured. Reading it rather than starting at zero is what keeps a wide
184
+ * page from laying itself out stacked and splitting a frame later, which would
185
+ * build both figures twice on every load.
186
+ * @returns The width in pixels, or the threshold itself where there is no
187
+ * window to read — a test, or a page rendered to a string — which is the
188
+ * narrowest a row is still split at.
189
+ */
190
+ function windowWidth(): number {
191
+ const { innerWidth } = globalThis;
192
+ return typeof innerWidth === 'number' && innerWidth > 0
193
+ ? innerWidth
194
+ : STACK_BELOW;
195
+ }
196
+
197
+ /** The narrowest row that is still split, in pixels. */
198
+ const STACK_BELOW = 860;
199
+
200
+ /** The gutter a pane keeps beside the splitter, in pixels. */
201
+ const GUTTER = 6;
202
+
203
+ /** The gap between the panes once they have stacked, in pixels. */
204
+ const STACKED_GAP = 16;
205
+
206
+ const STACK = {
207
+ display: 'flex',
208
+ flexDirection: 'column',
209
+ } as const satisfies CSSProperties;
210
+
211
+ const STACKED_PANE = {
212
+ display: 'flex',
213
+ flexDirection: 'column',
214
+ minWidth: 0,
215
+ } as const satisfies CSSProperties;
216
+
217
+ function paneStyle(side: 'start' | 'end', gutter: number): CSSProperties {
218
+ return {
219
+ display: 'flex',
220
+ flex: '1 1 0%',
221
+ flexDirection: 'column',
222
+ minWidth: 0,
223
+ [side === 'start' ? 'paddingRight' : 'paddingLeft']: gutter,
224
+ };
225
+ }
@@ -0,0 +1,2 @@
1
+ export type { SplitRowProps } from './SplitRow.tsx';
2
+ export { SplitRow } from './SplitRow.tsx';
package/src/ui.ts CHANGED
@@ -30,6 +30,7 @@ export * from './scatter/ui/index.ts';
30
30
  export * from './scatter3d/ui/index.ts';
31
31
  export * from './share/ui/index.ts';
32
32
  export * from './shared/ui/index.ts';
33
+ export * from './split/ui/index.ts';
33
34
  // The conformer table, from the structure module's file rather than its
34
35
  // barrel: `structure/ui/index.ts` is the door that must never reach react-ocl,
35
36
  // and this component does not, but `react-cheminfo/conformers` is imported by
package/styles/chrome.css CHANGED
@@ -1173,6 +1173,24 @@ select,
1173
1173
  color: var(--accent);
1174
1174
  }
1175
1175
 
1176
+ /* ── The splitter ───────────────────────────────────────────────────────── */
1177
+
1178
+ /*
1179
+ The bar between the two figures of a `<SplitRow>` is react-science's, and it
1180
+ keeps react-science's look — the pale track with the grip written on it that
1181
+ NMRium and every other tool of ours is divided by. It is barely visible on
1182
+ purpose: what is read is the two figures, and the bar is found by the pointer
1183
+ that changes over it rather than by the eye.
1184
+
1185
+ All this says is that eleven pixels is more gutter than two figures need.
1186
+ `:nth-child(2)` is the bar: a split is only mounted with a pane on each side
1187
+ of it, so the three children are always side, bar, side.
1188
+ */
1189
+ .split-row > div > div:nth-child(2) {
1190
+ width: 7px;
1191
+ min-width: 7px;
1192
+ }
1193
+
1176
1194
  /* ── Printing ────────────────────────────────────────────────────────────── */
1177
1195
 
1178
1196
  /*