@liberation-data/desk 0.2.0 → 0.3.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/README.md CHANGED
@@ -144,6 +144,40 @@ item it goes in front of that one, and the fixed items are never split. A `movab
144
144
  the dock to a new place. `contextMenu` gives any item a right-click menu; an item that is `disabled` but has
145
145
  a menu stays reachable, so something that can no longer open can still be removed.
146
146
 
147
+ ## Inside a window: a pane
148
+
149
+ ```tsx
150
+ import { Button, InfoTip, Pane, Toolbar } from '@liberation-data/desk/react'
151
+
152
+ <Pane
153
+ label="Log"
154
+ header={
155
+ <Toolbar
156
+ label="Log controls"
157
+ trailing={<><Button onClick={pause}>Pause</Button><Button onClick={load}>Refresh</Button></>}
158
+ >
159
+ <TextField className="desk-grow" label="Filter" value={filter} onChange={…} />
160
+ <InfoTip label="About these logs">This process only, and nothing that survives a restart.</InfoTip>
161
+ </Toolbar>
162
+ }
163
+ footer={<p>{lines.length} lines</p>}
164
+ >
165
+ {lines.map(line => <LogLine key={line.id} line={line} />)}
166
+ </Pane>
167
+ ```
168
+
169
+ A window is a fixed height, so something inside it has to give. A pane fills the window and scrolls only
170
+ its middle: the toolbar and the footer stay where the pointer left them, and the window itself never
171
+ scrolls. Name the pane and the scrolling part can be reached and scrolled from the keyboard. `bodyRef` and `onScroll`
172
+ hand over the scrolling element itself, for a view that follows its own tail.
173
+
174
+ `Toolbar` is one aligned row — every control on the same centre line whatever its own height, `trailing`
175
+ pushed to the end, and `className="desk-grow"` on the one item that should take the spare room.
176
+
177
+ `InfoTip` is an (i) holding what the view would explain if asked, so a window does not open with a
178
+ paragraph everyone reads once. Explanation only: a warning belongs in front of people, not behind a
179
+ disclosure.
180
+
147
181
  ## Controls
148
182
 
149
183
  ```tsx
package/dist/desk.css CHANGED
@@ -2693,3 +2693,109 @@
2693
2693
  .desk-dock-item[data-desk-drop][data-over] {
2694
2694
  box-shadow: -6px 0 0 -2px var(--desk-accent);
2695
2695
  }
2696
+
2697
+ /* ── The inside of a window: controls that stay, content that scrolls ── */
2698
+
2699
+ .desk-pane {
2700
+ display: flex;
2701
+ flex-direction: column;
2702
+ height: 100%;
2703
+ min-height: 0;
2704
+ }
2705
+
2706
+ .desk-pane-header,
2707
+ .desk-pane-footer {
2708
+ flex: none;
2709
+ }
2710
+
2711
+ /* The only part that scrolls, so the toolbar never leaves with the content. */
2712
+ .desk-pane-body {
2713
+ flex: 1;
2714
+ min-height: 0;
2715
+ overflow: auto;
2716
+ }
2717
+
2718
+ .desk-pane-body:focus-visible {
2719
+ outline: 2px solid var(--desk-accent);
2720
+ outline-offset: -2px;
2721
+ }
2722
+
2723
+ /* A window whose whole content is a pane does not scroll itself: the pane's middle does. */
2724
+ .desk-body:has(> .desk-pane) {
2725
+ overflow: hidden;
2726
+ }
2727
+
2728
+ .desk-toolbar {
2729
+ display: flex;
2730
+ align-items: center;
2731
+ flex-wrap: wrap;
2732
+ gap: var(--desk-space-2);
2733
+ /* One centre line for everything in the row, whatever each control's own height is. */
2734
+ min-height: calc(var(--desk-control-height) + var(--desk-space-2) * 2);
2735
+ padding: var(--desk-space-2) var(--desk-space-3);
2736
+ border-bottom: 1px solid var(--desk-rule);
2737
+ }
2738
+
2739
+ .desk-toolbar > *,
2740
+ .desk-toolbar-trailing > * {
2741
+ flex: none;
2742
+ }
2743
+
2744
+ .desk-toolbar .desk-grow {
2745
+ flex: 1 1 140px;
2746
+ min-width: 0;
2747
+ }
2748
+
2749
+ .desk-toolbar-trailing {
2750
+ margin-left: auto;
2751
+ display: flex;
2752
+ align-items: center;
2753
+ gap: var(--desk-space-2);
2754
+ }
2755
+
2756
+ /* ── An (i): what a view would explain if asked ── */
2757
+
2758
+ .desk-infotip {
2759
+ flex: none;
2760
+ display: inline-grid;
2761
+ place-items: center;
2762
+ width: 18px;
2763
+ height: 18px;
2764
+ padding: 0;
2765
+ border: 1px solid var(--desk-rule);
2766
+ border-radius: 50%;
2767
+ background: none;
2768
+ color: var(--desk-muted);
2769
+ font-family: var(--desk-font);
2770
+ font-size: 11px;
2771
+ font-style: italic;
2772
+ font-weight: 700;
2773
+ line-height: 1;
2774
+ cursor: pointer;
2775
+ }
2776
+
2777
+ .desk-infotip:hover,
2778
+ .desk-infotip[data-open] {
2779
+ border-color: var(--desk-accent);
2780
+ color: var(--desk-accent);
2781
+ }
2782
+
2783
+ .desk-infotip:focus-visible {
2784
+ outline: 2px solid var(--desk-accent);
2785
+ outline-offset: 2px;
2786
+ }
2787
+
2788
+ .desk-infotip-body {
2789
+ max-width: 34ch;
2790
+ font-size: 12.5px;
2791
+ line-height: 1.45;
2792
+ color: var(--desk-muted);
2793
+ }
2794
+
2795
+ .desk-infotip-body > :first-child {
2796
+ margin-top: 0;
2797
+ }
2798
+
2799
+ .desk-infotip-body > :last-child {
2800
+ margin-bottom: 0;
2801
+ }
@@ -14,6 +14,10 @@ export { lazyWindow } from './windowBoundary.js';
14
14
  export type { WindowFailed, WindowLoading } from './windowBoundary.js';
15
15
  export { Button, Checkbox, ChoiceGroup, RadioGroup, SegmentedControl, Slider, TextField, Toggle } from './controls.js';
16
16
  export type { ButtonIntent, ButtonProps, CheckboxProps, ChoiceGroupProps, ChoiceOption, RadioGroupProps, RadioOption, SegmentedControlProps, SegmentedOption, SliderProps, TextFieldProps, ToggleProps, } from './controls.js';
17
+ export { Pane, Toolbar } from './pane.js';
18
+ export type { PaneProps, ToolbarProps } from './pane.js';
19
+ export { InfoTip } from './infoTip.js';
20
+ export type { InfoTipProps } from './infoTip.js';
17
21
  export { Checklist, useTasks } from './tasks.js';
18
22
  export { localSetupStore, useSetupProgress } from './setupProgress.js';
19
23
  export type { SetupProgress, SetupProgressOptions, SetupRecord, SetupStore } from './setupProgress.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,8 @@ export { useCanPerform, useCommand, usePerform, useShortcuts } from './commands.
6
6
  export { Desktop } from './Desktop.js';
7
7
  export { lazyWindow } from './windowBoundary.js';
8
8
  export { Button, Checkbox, ChoiceGroup, RadioGroup, SegmentedControl, Slider, TextField, Toggle } from './controls.js';
9
+ export { Pane, Toolbar } from './pane.js';
10
+ export { InfoTip } from './infoTip.js';
9
11
  export { Checklist, useTasks } from './tasks.js';
10
12
  export { localSetupStore, useSetupProgress } from './setupProgress.js';
11
13
  export { PopUpButton } from './popup.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE1E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAWhG,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAetH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGtE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEzE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAErG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE9E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3G,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAGjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE1E,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAWhG,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAetH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGtE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEzE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAErG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE9E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC3G,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAGjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface InfoTipProps {
3
+ /** What it explains, e.g. "About these logs" — the button's name and the popover's. */
4
+ readonly label: string;
5
+ readonly children: ReactNode;
6
+ readonly placement?: 'below' | 'above';
7
+ readonly align?: 'start' | 'end';
8
+ readonly className?: string;
9
+ }
10
+ export declare function InfoTip({ label, children, placement, align, className }: InfoTipProps): import("react").JSX.Element;
11
+ //# sourceMappingURL=infoTip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infoTip.d.ts","sourceRoot":"","sources":["../../src/react/infoTip.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAYtC,MAAM,WAAW,YAAY;IAC3B,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAmB,EAAE,KAAe,EAAE,SAAS,EAAE,EAAE,YAAY,+BAyBzG"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Popover } from './overlays.js';
4
+ export function InfoTip({ label, children, placement = 'below', align = 'start', className }) {
5
+ const [open, setOpen] = useState(false);
6
+ return (_jsx(Popover, { open: open, onOpenChange: setOpen, label: label, placement: placement, align: align, trigger: props => (_jsx("button", { ...props, ref: props.ref, type: "button", className: ['desk-infotip', className].filter(Boolean).join(' '), "aria-label": label, "data-open": open || undefined, children: _jsx("span", { "aria-hidden": "true", children: "i" }) })), children: _jsx("div", { className: "desk-infotip-body", children: children }) }));
7
+ }
8
+ //# sourceMappingURL=infoTip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infoTip.js","sourceRoot":"","sources":["../../src/react/infoTip.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAoBvC,MAAM,UAAU,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,GAAG,OAAO,EAAE,KAAK,GAAG,OAAO,EAAE,SAAS,EAAgB;IACxG,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,OAAO,CACL,KAAC,OAAO,IACN,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,OAAO,EACrB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,CAChB,oBACM,KAAK,EACT,GAAG,EAAE,KAAK,CAAC,GAAG,EACd,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,gBACpD,KAAK,eACN,IAAI,IAAI,SAAS,YAE5B,8BAAkB,MAAM,kBAAS,GAC1B,CACV,YAED,cAAK,SAAS,EAAC,mBAAmB,YAAE,QAAQ,GAAO,GAC3C,CACX,CAAA;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Ref, UIEvent, ReactNode } from 'react';
2
+ export interface ToolbarProps {
3
+ /** Controls, in reading order. Anything given `className="desk-grow"` takes the spare room. */
4
+ readonly children: ReactNode;
5
+ /** Pushed to the trailing end: what acts on everything here, or the state of it. */
6
+ readonly trailing?: ReactNode;
7
+ /** Names the row for assistive technology when it holds more than one thing: "Log controls". */
8
+ readonly label?: string;
9
+ readonly className?: string;
10
+ }
11
+ /** One aligned row of controls. Every item sits on the same centre line, whatever its height. */
12
+ export declare function Toolbar({ children, trailing, label, className }: ToolbarProps): import("react").JSX.Element;
13
+ export interface PaneProps {
14
+ /** Stays put at the top: a Toolbar, usually. */
15
+ readonly header?: ReactNode;
16
+ /** Stays put at the bottom: a count, a status line, the buttons of a form. */
17
+ readonly footer?: ReactNode;
18
+ /** Fills what is left and scrolls, on its own. */
19
+ readonly children: ReactNode;
20
+ /** The scrolling part is the thing being read, so it can be named and reached by the keyboard. */
21
+ readonly label?: string;
22
+ /** The scrolling element itself: for reading where it sits, or sending it to the bottom. */
23
+ readonly bodyRef?: Ref<HTMLDivElement>;
24
+ readonly onScroll?: (event: UIEvent<HTMLDivElement>) => void;
25
+ readonly className?: string;
26
+ }
27
+ export declare function Pane({ header, footer, children, label, bodyRef, onScroll, className }: PaneProps): import("react").JSX.Element;
28
+ //# sourceMappingURL=pane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane.d.ts","sourceRoot":"","sources":["../../src/react/pane.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAWpD,MAAM,WAAW,YAAY;IAC3B,+FAA+F;IAC/F,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,iGAAiG;AACjG,wBAAgB,OAAO,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,YAAY,+BAU7E;AAED,MAAM,WAAW,SAAS;IACxB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;IAC3B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,kGAAkG;IAClG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAA;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,CAAA;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,SAAS,+BAehG"}
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** One aligned row of controls. Every item sits on the same centre line, whatever its height. */
3
+ export function Toolbar({ children, trailing, label, className }) {
4
+ return (_jsxs("div", { ...(label ? { role: 'group', 'aria-label': label } : {}), className: ['desk-toolbar', className].filter(Boolean).join(' '), children: [children, trailing != null && _jsx("div", { className: "desk-toolbar-trailing", children: trailing })] }));
5
+ }
6
+ export function Pane({ header, footer, children, label, bodyRef, onScroll, className }) {
7
+ return (_jsxs("div", { className: ['desk-pane', className].filter(Boolean).join(' '), children: [header != null && _jsx("div", { className: "desk-pane-header", children: header }), _jsx("div", { ref: bodyRef, className: "desk-pane-body", onScroll: onScroll, ...(label ? { role: 'region', 'aria-label': label, tabIndex: 0 } : {}), children: children }), footer != null && _jsx("div", { className: "desk-pane-footer", children: footer })] }));
8
+ }
9
+ //# sourceMappingURL=pane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane.js","sourceRoot":"","sources":["../../src/react/pane.tsx"],"names":[],"mappings":";AAqBA,iGAAiG;AACjG,MAAM,UAAU,OAAO,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAgB;IAC5E,OAAO,CACL,kBACM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EACzD,SAAS,EAAE,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAE/D,QAAQ,EACR,QAAQ,IAAI,IAAI,IAAI,cAAK,SAAS,EAAC,uBAAuB,YAAE,QAAQ,GAAO,IACxE,CACP,CAAA;AACH,CAAC;AAiBD,MAAM,UAAU,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAa;IAC/F,OAAO,CACL,eAAK,SAAS,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAC/D,MAAM,IAAI,IAAI,IAAI,cAAK,SAAS,EAAC,kBAAkB,YAAE,MAAM,GAAO,EACnE,cACE,GAAG,EAAE,OAAO,EACZ,SAAS,EAAC,gBAAgB,EAC1B,QAAQ,EAAE,QAAQ,KACd,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,YAEtE,QAAQ,GACL,EACL,MAAM,IAAI,IAAI,IAAI,cAAK,SAAS,EAAC,kBAAkB,YAAE,MAAM,GAAO,IAC/D,CACP,CAAA;AACH,CAAC"}
package/llms.txt CHANGED
@@ -202,6 +202,18 @@ Every window has its own Suspense and error boundary: one window loading or thro
202
202
  the menu bar or the dock. A failure is logged with `console.error` and shows Reload, which remounts the
203
203
  window; `lazyWindow` retries its import on reload, unlike `React.lazy`, which caches a failed import.
204
204
 
205
+ ### Inside a window
206
+
207
+ ```tsx
208
+ <Pane label?={string} header?={ReactNode} footer?={ReactNode} bodyRef?={Ref} onScroll?={fn}>{scrolling content}</Pane>
209
+ <Toolbar label?={string} trailing?={ReactNode}>{controls}</Toolbar> // className="desk-grow" takes spare room
210
+ <InfoTip label={string} placement?={'below'|'above'} align?={'start'|'end'}>{explanation}</InfoTip>
211
+ ```
212
+
213
+ A pane fills its window and scrolls only its middle, so the window never scrolls and the toolbar stays put.
214
+ A named pane is a focusable region, so it scrolls from the keyboard. InfoTip is an (i) popover for
215
+ explanation only — never a warning, which belongs in front of people.
216
+
205
217
  ### Dock
206
218
 
207
219
  ```tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liberation-data/desk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A desktop for the web, for React: windows, a dock, menus and commands, controls, tours and setup assistants.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Jasper Blues",
package/src/desk.css CHANGED
@@ -2693,3 +2693,109 @@
2693
2693
  .desk-dock-item[data-desk-drop][data-over] {
2694
2694
  box-shadow: -6px 0 0 -2px var(--desk-accent);
2695
2695
  }
2696
+
2697
+ /* ── The inside of a window: controls that stay, content that scrolls ── */
2698
+
2699
+ .desk-pane {
2700
+ display: flex;
2701
+ flex-direction: column;
2702
+ height: 100%;
2703
+ min-height: 0;
2704
+ }
2705
+
2706
+ .desk-pane-header,
2707
+ .desk-pane-footer {
2708
+ flex: none;
2709
+ }
2710
+
2711
+ /* The only part that scrolls, so the toolbar never leaves with the content. */
2712
+ .desk-pane-body {
2713
+ flex: 1;
2714
+ min-height: 0;
2715
+ overflow: auto;
2716
+ }
2717
+
2718
+ .desk-pane-body:focus-visible {
2719
+ outline: 2px solid var(--desk-accent);
2720
+ outline-offset: -2px;
2721
+ }
2722
+
2723
+ /* A window whose whole content is a pane does not scroll itself: the pane's middle does. */
2724
+ .desk-body:has(> .desk-pane) {
2725
+ overflow: hidden;
2726
+ }
2727
+
2728
+ .desk-toolbar {
2729
+ display: flex;
2730
+ align-items: center;
2731
+ flex-wrap: wrap;
2732
+ gap: var(--desk-space-2);
2733
+ /* One centre line for everything in the row, whatever each control's own height is. */
2734
+ min-height: calc(var(--desk-control-height) + var(--desk-space-2) * 2);
2735
+ padding: var(--desk-space-2) var(--desk-space-3);
2736
+ border-bottom: 1px solid var(--desk-rule);
2737
+ }
2738
+
2739
+ .desk-toolbar > *,
2740
+ .desk-toolbar-trailing > * {
2741
+ flex: none;
2742
+ }
2743
+
2744
+ .desk-toolbar .desk-grow {
2745
+ flex: 1 1 140px;
2746
+ min-width: 0;
2747
+ }
2748
+
2749
+ .desk-toolbar-trailing {
2750
+ margin-left: auto;
2751
+ display: flex;
2752
+ align-items: center;
2753
+ gap: var(--desk-space-2);
2754
+ }
2755
+
2756
+ /* ── An (i): what a view would explain if asked ── */
2757
+
2758
+ .desk-infotip {
2759
+ flex: none;
2760
+ display: inline-grid;
2761
+ place-items: center;
2762
+ width: 18px;
2763
+ height: 18px;
2764
+ padding: 0;
2765
+ border: 1px solid var(--desk-rule);
2766
+ border-radius: 50%;
2767
+ background: none;
2768
+ color: var(--desk-muted);
2769
+ font-family: var(--desk-font);
2770
+ font-size: 11px;
2771
+ font-style: italic;
2772
+ font-weight: 700;
2773
+ line-height: 1;
2774
+ cursor: pointer;
2775
+ }
2776
+
2777
+ .desk-infotip:hover,
2778
+ .desk-infotip[data-open] {
2779
+ border-color: var(--desk-accent);
2780
+ color: var(--desk-accent);
2781
+ }
2782
+
2783
+ .desk-infotip:focus-visible {
2784
+ outline: 2px solid var(--desk-accent);
2785
+ outline-offset: 2px;
2786
+ }
2787
+
2788
+ .desk-infotip-body {
2789
+ max-width: 34ch;
2790
+ font-size: 12.5px;
2791
+ line-height: 1.45;
2792
+ color: var(--desk-muted);
2793
+ }
2794
+
2795
+ .desk-infotip-body > :first-child {
2796
+ margin-top: 0;
2797
+ }
2798
+
2799
+ .desk-infotip-body > :last-child {
2800
+ margin-bottom: 0;
2801
+ }
@@ -36,6 +36,10 @@ export type {
36
36
  TextFieldProps,
37
37
  ToggleProps,
38
38
  } from './controls.js'
39
+ export { Pane, Toolbar } from './pane.js'
40
+ export type { PaneProps, ToolbarProps } from './pane.js'
41
+ export { InfoTip } from './infoTip.js'
42
+ export type { InfoTipProps } from './infoTip.js'
39
43
  export { Checklist, useTasks } from './tasks.js'
40
44
  export { localSetupStore, useSetupProgress } from './setupProgress.js'
41
45
  export type { SetupProgress, SetupProgressOptions, SetupRecord, SetupStore } from './setupProgress.js'
@@ -0,0 +1,48 @@
1
+ import { useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { Popover } from './overlays.js'
4
+
5
+ /*
6
+ * What a view would explain if asked. A window that opens with a paragraph makes everyone read it
7
+ * every time to reach the thing they came for; the same words behind an (i) are there for whoever
8
+ * has the question, once.
9
+ *
10
+ * Only for explanation. A warning, or anything that changes what someone is about to do, belongs
11
+ * in front of them — hiding that behind a disclosure is how it goes unread.
12
+ */
13
+
14
+ export interface InfoTipProps {
15
+ /** What it explains, e.g. "About these logs" — the button's name and the popover's. */
16
+ readonly label: string
17
+ readonly children: ReactNode
18
+ readonly placement?: 'below' | 'above'
19
+ readonly align?: 'start' | 'end'
20
+ readonly className?: string
21
+ }
22
+
23
+ export function InfoTip({ label, children, placement = 'below', align = 'start', className }: InfoTipProps) {
24
+ const [open, setOpen] = useState(false)
25
+ return (
26
+ <Popover
27
+ open={open}
28
+ onOpenChange={setOpen}
29
+ label={label}
30
+ placement={placement}
31
+ align={align}
32
+ trigger={props => (
33
+ <button
34
+ {...props}
35
+ ref={props.ref}
36
+ type="button"
37
+ className={['desk-infotip', className].filter(Boolean).join(' ')}
38
+ aria-label={label}
39
+ data-open={open || undefined}
40
+ >
41
+ <span aria-hidden="true">i</span>
42
+ </button>
43
+ )}
44
+ >
45
+ <div className="desk-infotip-body">{children}</div>
46
+ </Popover>
47
+ )
48
+ }
@@ -0,0 +1,65 @@
1
+ import type { Ref, UIEvent, ReactNode } from 'react'
2
+
3
+ /*
4
+ * The inside of a window: a row of controls that stays put, and content that scrolls under it.
5
+ *
6
+ * A window is a fixed height, so something inside it has to give. When the content simply grows,
7
+ * the window grows with it and the whole thing scrolls — the toolbar leaves with it, and a log or a
8
+ * table has to be scrolled past to reach anything below. A pane fills the window instead and scrolls
9
+ * only its middle, so the controls stay where the pointer left them.
10
+ */
11
+
12
+ export interface ToolbarProps {
13
+ /** Controls, in reading order. Anything given `className="desk-grow"` takes the spare room. */
14
+ readonly children: ReactNode
15
+ /** Pushed to the trailing end: what acts on everything here, or the state of it. */
16
+ readonly trailing?: ReactNode
17
+ /** Names the row for assistive technology when it holds more than one thing: "Log controls". */
18
+ readonly label?: string
19
+ readonly className?: string
20
+ }
21
+
22
+ /** One aligned row of controls. Every item sits on the same centre line, whatever its height. */
23
+ export function Toolbar({ children, trailing, label, className }: ToolbarProps) {
24
+ return (
25
+ <div
26
+ {...(label ? { role: 'group', 'aria-label': label } : {})}
27
+ className={['desk-toolbar', className].filter(Boolean).join(' ')}
28
+ >
29
+ {children}
30
+ {trailing != null && <div className="desk-toolbar-trailing">{trailing}</div>}
31
+ </div>
32
+ )
33
+ }
34
+
35
+ export interface PaneProps {
36
+ /** Stays put at the top: a Toolbar, usually. */
37
+ readonly header?: ReactNode
38
+ /** Stays put at the bottom: a count, a status line, the buttons of a form. */
39
+ readonly footer?: ReactNode
40
+ /** Fills what is left and scrolls, on its own. */
41
+ readonly children: ReactNode
42
+ /** The scrolling part is the thing being read, so it can be named and reached by the keyboard. */
43
+ readonly label?: string
44
+ /** The scrolling element itself: for reading where it sits, or sending it to the bottom. */
45
+ readonly bodyRef?: Ref<HTMLDivElement>
46
+ readonly onScroll?: (event: UIEvent<HTMLDivElement>) => void
47
+ readonly className?: string
48
+ }
49
+
50
+ export function Pane({ header, footer, children, label, bodyRef, onScroll, className }: PaneProps) {
51
+ return (
52
+ <div className={['desk-pane', className].filter(Boolean).join(' ')}>
53
+ {header != null && <div className="desk-pane-header">{header}</div>}
54
+ <div
55
+ ref={bodyRef}
56
+ className="desk-pane-body"
57
+ onScroll={onScroll}
58
+ {...(label ? { role: 'region', 'aria-label': label, tabIndex: 0 } : {})}
59
+ >
60
+ {children}
61
+ </div>
62
+ {footer != null && <div className="desk-pane-footer">{footer}</div>}
63
+ </div>
64
+ )
65
+ }