@graphty/compact-mantine 0.5.0 → 0.6.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 +618 -119
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4893 -557
- package/dist/index.js +5269 -1056
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -2,16 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@graphty/compact-mantine)
|
|
4
4
|
[](https://graphty.app/storybook/compact-mantine/)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Compact components and a compact theme for [Mantine 8](https://mantine.dev), for
|
|
8
|
+
interfaces where screen space is the scarce resource: property panels,
|
|
9
|
+
inspectors, sidebars, editors and dashboards.
|
|
10
|
+
|
|
11
|
+
It gives you two things, and you can take either one on its own:
|
|
12
|
+
|
|
13
|
+
1. **A theme.** Drop `compactTheme` into your `MantineProvider` and 41 Mantine
|
|
14
|
+
components render at a dense size -- 24px controls, 11px text, tighter
|
|
15
|
+
spacing -- with no `size` prop anywhere in your code.
|
|
16
|
+
2. **A component library.** About thirty components built for a narrow column:
|
|
17
|
+
rows that put a value and its label on one 32px line, charts that fit in the
|
|
18
|
+
height of a line of text, a virtualized data table, and a floating-panel
|
|
19
|
+
system for the settings that do not fit.
|
|
20
|
+
|
|
21
|
+
Everything is typed, translatable, keyboard-operable, screen-reader-tested,
|
|
22
|
+
works in light and dark schemes, and works in right-to-left languages.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Contents
|
|
27
|
+
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [The compact theme](#the-compact-theme)
|
|
31
|
+
- [The components](#the-components)
|
|
32
|
+
- [Building a panel](#building-a-panel)
|
|
33
|
+
- [Editing a value](#editing-a-value)
|
|
34
|
+
- [Showing data](#showing-data)
|
|
35
|
+
- [Floating panels](#floating-panels)
|
|
36
|
+
- [Glyphs](#glyphs)
|
|
37
|
+
- [A worked example](#a-worked-example)
|
|
38
|
+
- [The panel grid](#the-panel-grid)
|
|
39
|
+
- [Handling events](#handling-events)
|
|
40
|
+
- [The shared prop names](#the-shared-prop-names)
|
|
41
|
+
- [Internationalization](#internationalization)
|
|
42
|
+
- [Right-to-left languages](#right-to-left-languages)
|
|
43
|
+
- [Accessibility](#accessibility)
|
|
44
|
+
- [TypeScript](#typescript)
|
|
45
|
+
- [Which one should I use?](#which-one-should-i-use)
|
|
46
|
+
- [Contributing](#contributing)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
|
|
49
|
+
---
|
|
15
50
|
|
|
16
51
|
## Installation
|
|
17
52
|
|
|
@@ -19,179 +54,643 @@ A Mantine theme and component library optimized for dense, compact UIs. All comp
|
|
|
19
54
|
npm install @graphty/compact-mantine @mantine/core @mantine/hooks
|
|
20
55
|
```
|
|
21
56
|
|
|
22
|
-
|
|
57
|
+
`@mantine/core`, `@mantine/hooks`, `react` and `react-dom` are peer
|
|
58
|
+
dependencies, so you control their versions. React 18 or later, Mantine 8 or
|
|
59
|
+
later.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
Two steps: import Mantine's stylesheet, and give `MantineProvider` the compact
|
|
64
|
+
theme. This is a complete, runnable file.
|
|
23
65
|
|
|
24
66
|
```tsx
|
|
25
|
-
import
|
|
26
|
-
|
|
67
|
+
import "@mantine/core/styles.css";
|
|
68
|
+
|
|
69
|
+
import { Button, MantineProvider, TextInput } from "@mantine/core";
|
|
70
|
+
import { compactTheme, ControlSection, FieldRow, PanelField, ToggleRow } from "@graphty/compact-mantine";
|
|
27
71
|
|
|
28
|
-
function App() {
|
|
72
|
+
export function App() {
|
|
29
73
|
return (
|
|
30
74
|
<MantineProvider theme={compactTheme}>
|
|
31
|
-
{/*
|
|
32
|
-
<TextInput label="
|
|
33
|
-
<Button>
|
|
75
|
+
{/* Ordinary Mantine components, now rendered compact. */}
|
|
76
|
+
<TextInput label="Project name" />
|
|
77
|
+
<Button>Save</Button>
|
|
78
|
+
|
|
79
|
+
{/* This package's own components, for a 280px panel. */}
|
|
80
|
+
<div style={{width: 280}}>
|
|
81
|
+
<ControlSection label="Node size">
|
|
82
|
+
<FieldRow>
|
|
83
|
+
<PanelField label="Smallest" glyph="sizeSmallest" defaultValue="1.0" />
|
|
84
|
+
<PanelField label="Largest" glyph="sizeLargest" defaultValue="4.0" />
|
|
85
|
+
</FieldRow>
|
|
86
|
+
<ToggleRow label="Scale with zoom" defaultChecked />
|
|
87
|
+
</ControlSection>
|
|
88
|
+
</div>
|
|
34
89
|
</MantineProvider>
|
|
35
90
|
);
|
|
36
91
|
}
|
|
37
92
|
```
|
|
38
93
|
|
|
39
|
-
|
|
94
|
+
That is the whole setup. Two further providers are optional and are introduced
|
|
95
|
+
where they matter: [`PopoutManager`](#floating-panels) if you use pop-outs, and
|
|
96
|
+
[`LabelsProvider`](#internationalization) if you translate the library's
|
|
97
|
+
strings.
|
|
98
|
+
|
|
99
|
+
## The compact theme
|
|
100
|
+
|
|
101
|
+
`compactTheme` is a complete Mantine theme. It changes three global tokens and
|
|
102
|
+
sets default props and styles on 41 Mantine components, so that a component you
|
|
103
|
+
already know renders small without being told to.
|
|
104
|
+
|
|
105
|
+
| Token | Values |
|
|
106
|
+
|-------|--------|
|
|
107
|
+
| `fontSizes` | xs 10px, sm 11px, md 13px, lg 14px, xl 16px |
|
|
108
|
+
| `spacing` | xs 4px, sm 6px, md 8px, lg 12px, xl 16px |
|
|
109
|
+
| `radius` | xs 2px, sm 4px, md 6px, lg 8px, xl 12px |
|
|
110
|
+
|
|
111
|
+
Inputs default to a 24px height and an 11px face, and are drawn without a
|
|
112
|
+
border at rest so that a column of them reads as a list of values rather than a
|
|
113
|
+
grid of boxes. Keyboard focus still paints a visible ring; a mouse click does
|
|
114
|
+
not, so the surface stays quiet under the pointer.
|
|
40
115
|
|
|
41
|
-
###
|
|
116
|
+
### Components the theme restyles
|
|
42
117
|
|
|
43
|
-
|
|
118
|
+
Pass no `size` prop and these render compact. Pass `size="md"` or `size="lg"`
|
|
119
|
+
and you get Mantine's usual sizes back.
|
|
120
|
+
|
|
121
|
+
| Group | Components |
|
|
122
|
+
|-------|------------|
|
|
123
|
+
| Inputs (12) | TextInput, NumberInput, Select, Textarea, PasswordInput, Autocomplete, MultiSelect, TagsInput, PillsInput, FileInput, JsonInput, InputClearButton |
|
|
124
|
+
| Buttons (3) | Button, ActionIcon, CloseButton |
|
|
125
|
+
| Controls (6) | Switch, Checkbox, Radio, Slider, RangeSlider, SegmentedControl |
|
|
126
|
+
| Display (7) | Badge, Text, Avatar, ThemeIcon, Indicator, Kbd, Pill |
|
|
127
|
+
| Navigation (6) | Tabs, NavLink, Pagination, Stepper, Anchor, Burger |
|
|
128
|
+
| Feedback (3) | Loader, Progress, RingProgress |
|
|
129
|
+
| Overlays (4) | Menu, Tooltip, Popover, HoverCard |
|
|
130
|
+
|
|
131
|
+
### Making it your own
|
|
132
|
+
|
|
133
|
+
`compactTheme` is a full theme, already merged with Mantine's default. Merge
|
|
134
|
+
your own on top of it:
|
|
44
135
|
|
|
45
136
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```
|
|
137
|
+
import { createTheme, MantineProvider, mergeMantineTheme } from "@mantine/core";
|
|
138
|
+
import { compactTheme } from "@graphty/compact-mantine";
|
|
139
|
+
|
|
140
|
+
const theme = mergeMantineTheme(compactTheme, createTheme({
|
|
141
|
+
primaryColor: "teal",
|
|
142
|
+
fontFamily: "Inter, sans-serif",
|
|
143
|
+
}));
|
|
54
144
|
|
|
55
|
-
|
|
145
|
+
<MantineProvider theme={theme}>{children}</MantineProvider>;
|
|
146
|
+
```
|
|
56
147
|
|
|
57
|
-
|
|
148
|
+
If you already build your theme from several overrides, take
|
|
149
|
+
`compactThemeOverride` instead. It is the raw `createTheme()` result, suitable
|
|
150
|
+
for `mergeThemeOverrides()`:
|
|
58
151
|
|
|
59
152
|
```tsx
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
153
|
+
import { createTheme, mergeThemeOverrides } from "@mantine/core";
|
|
154
|
+
import { compactThemeOverride } from "@graphty/compact-mantine";
|
|
155
|
+
|
|
156
|
+
const override = mergeThemeOverrides(compactThemeOverride, createTheme({primaryColor: "teal"}));
|
|
64
157
|
```
|
|
65
158
|
|
|
66
|
-
###
|
|
159
|
+
### A compact region inside a normal-sized app
|
|
160
|
+
|
|
161
|
+
`MantineProvider` nests, so you can keep your application at its usual size and
|
|
162
|
+
make one region dense -- which is the common case for a sidebar or an inspector.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<MantineProvider>
|
|
166
|
+
<TextInput label="Normal size" />
|
|
167
|
+
|
|
168
|
+
<MantineProvider theme={compactTheme}>
|
|
169
|
+
<aside style={{width: 280}}>
|
|
170
|
+
<TextInput label="Compact" />
|
|
171
|
+
</aside>
|
|
172
|
+
</MantineProvider>
|
|
173
|
+
</MantineProvider>
|
|
174
|
+
```
|
|
67
175
|
|
|
68
|
-
|
|
176
|
+
Also exported: `compactColors` (the palette this theme adds) and
|
|
177
|
+
`compactDarkColors` (its dark scale), if you want to reuse the colours
|
|
178
|
+
elsewhere.
|
|
179
|
+
|
|
180
|
+
## The components
|
|
181
|
+
|
|
182
|
+
Grouped by what you are trying to do. Every component has a page in
|
|
183
|
+
[Storybook](https://graphty.app/storybook/compact-mantine/) with runnable
|
|
184
|
+
examples, and every prop is documented in your editor.
|
|
185
|
+
|
|
186
|
+
### Building a panel
|
|
187
|
+
|
|
188
|
+
| Component | Reach for it when |
|
|
189
|
+
|-----------|-------------------|
|
|
190
|
+
| `ControlSection` | a run of controls needs a name, a rule above it and a chevron that folds it away. The workhorse container for a property panel. It can show a dot when something inside it is non-default, an empty state with a single "+", an explanation bubble, and buttons of its own in the header. |
|
|
191
|
+
| `ControlGroup` | the same, but it must never fold, or its rule has to bleed out to the edges of a padded container such as a pop-out. |
|
|
192
|
+
| `ControlSubGroup` | a handful of rarely-opened settings belong under a section you already have. Quieter than a section: no rule, a smaller chevron. |
|
|
193
|
+
| `FieldRow` | one or two fields share a line. It owns the widths and the gaps, so a column of rows lines up. |
|
|
194
|
+
| `TrailingSlot` | you are laying out a row by hand and need the fixed 24px slot every row ends with, so that rows with a trailing control end level with rows without one. |
|
|
195
|
+
| `AdvancedButton` | a row or a section has settings most people never change. The gear opens them in a pop-out, and marks itself when something behind it is no longer default. |
|
|
196
|
+
|
|
197
|
+
### Editing a value
|
|
198
|
+
|
|
199
|
+
| Component | Reach for it when |
|
|
200
|
+
|-----------|-------------------|
|
|
201
|
+
| `PanelField` | a value is typed, picked from a list or dragged. A real text box, number box or select, 24px tall, whose caption is a 16px drawing inside the box instead of a word above it. |
|
|
202
|
+
| `CompoundRow` | two or three values are one thing seen several ways -- a colour and its opacity, a width and its unit -- and must read as one control. |
|
|
203
|
+
| `IconGroupRow` | two to six mutually exclusive options whose difference can be drawn: node shapes, edge routing, scale curves. A `SegmentedControl` underneath, so arrow keys work. |
|
|
204
|
+
| `ToggleRow` | a setting is a plain yes or no that no picture could stand for. |
|
|
205
|
+
| `ToggleRowGroup` | you have two or more of those. It packs them at a 24px pitch and warns you in development if you give it only one. |
|
|
206
|
+
| `RampRow` | a range is better drawn than described: a size wedge or a colour ramp with its two endpoints. |
|
|
207
|
+
| `CompactColorInput` | a colour and its opacity, on one 24px line, with a picker in a pop-out. Needs a [`PopoutManager`](#floating-panels). |
|
|
208
|
+
| `GradientEditor` | a multi-stop linear gradient: colours, positions, angle. Needs a `PopoutManager`. |
|
|
209
|
+
| `StyleNumberInput` | a number that has a sensible default, and you want the panel to show at a glance whether the reader has overridden it. `undefined` means "not set" and shows the default in italics with no reset button. |
|
|
210
|
+
| `StyleSelect` | the same idea for a dropdown. |
|
|
211
|
+
| `ToggleWithContent` | a feature is a yes or no that brings its own settings with it. Turning it off takes its settings off the screen. |
|
|
212
|
+
|
|
213
|
+
### Showing data
|
|
214
|
+
|
|
215
|
+
| Component | Reach for it when |
|
|
216
|
+
|-----------|-------------------|
|
|
217
|
+
| `DataRow` | the string is the reader's own -- an id, a node label, a filename -- with a number beside it. The one row here that keeps a text label, because data cannot be drawn. Selectable, double-clickable, and it can carry a trailing control. |
|
|
218
|
+
| `DataRowHeader` | a run of data rows needs a caption, so the rows below can drop the unit word they would otherwise repeat. Give it `onSortChange` and it becomes a sort control. |
|
|
219
|
+
| `RankChip` | a rank belongs beside a row: `#6`, rather than a sentence saying "rank 6 of 318". |
|
|
220
|
+
| `MetricRow` | one reading has a percentile and a rank. Draws the name, a bar filled to the percentile, the number, and the chip. |
|
|
221
|
+
| `HistogramRow` | a distribution would otherwise be spelled as the four numbers that summarise it. 64px tall. |
|
|
222
|
+
| `SparklineRow` | a series is going somewhere and you want to see which way. 32px tall. |
|
|
223
|
+
| `ProseBlock` | the panel has to say something in words: a plain-language reading, a caveat about how a result falls short, or a record of the last run. |
|
|
224
|
+
| `ActionRow` | a row reports a state and offers verbs. The state is always visible; the verbs appear on hover, on focus, and always on a touch screen. |
|
|
225
|
+
| `DataTable` | you have columns rather than rows: thousands of them, sortable, searchable, selectable, with only the visible rows in the document. |
|
|
226
|
+
|
|
227
|
+
### Floating panels
|
|
228
|
+
|
|
229
|
+
A pop-out is a panel that opens beside a control, can be dragged, and can
|
|
230
|
+
contain pop-outs of its own.
|
|
231
|
+
|
|
232
|
+
**Put one `PopoutManager` high in your tree, above anything that opens a
|
|
233
|
+
pop-out.** It owns the shared floating layer: the stacking order, the portal the
|
|
234
|
+
panels render into, and the dismissal rules they all obey. Without it, anything
|
|
235
|
+
that opens a pop-out throws `usePopoutManagerContext must be used within a
|
|
236
|
+
PopoutManager` on first render. `CompactColorInput` and `GradientEditor` use a
|
|
237
|
+
pop-out internally, so they need one too. (`InfoCircle` is the exception: it
|
|
238
|
+
supplies its own if there is none.)
|
|
69
239
|
|
|
70
240
|
```tsx
|
|
71
|
-
import {
|
|
72
|
-
|
|
241
|
+
import { Popout, PopoutButton, PopoutManager, UiGlyph } from "@graphty/compact-mantine";
|
|
242
|
+
|
|
243
|
+
<PopoutManager>
|
|
244
|
+
<aside>
|
|
245
|
+
<Popout>
|
|
246
|
+
<Popout.Trigger>
|
|
247
|
+
<PopoutButton icon={<UiGlyph name="gear" />} aria-label="Display settings" />
|
|
248
|
+
</Popout.Trigger>
|
|
249
|
+
<Popout.Panel width={280} header={{variant: "title", title: "Display settings"}}>
|
|
250
|
+
<Popout.Content>{/* anything */}</Popout.Content>
|
|
251
|
+
</Popout.Panel>
|
|
252
|
+
</Popout>
|
|
253
|
+
</aside>
|
|
254
|
+
</PopoutManager>;
|
|
255
|
+
```
|
|
73
256
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
257
|
+
| Component | What it is |
|
|
258
|
+
|-----------|------------|
|
|
259
|
+
| `PopoutManager` | The shared floating layer. Required, once, near the root. |
|
|
260
|
+
| `Popout` | One pop-out: its trigger and its panel. Also namespaces `Popout.Trigger`, `Popout.Panel`, `Popout.Content` and `Popout.Anchor`. |
|
|
261
|
+
| `Popout.Trigger` | Wraps the single element that opens the panel. Give it a real button. |
|
|
262
|
+
| `Popout.Panel` | The panel itself: a width, an optional header or tab strip, and its content. |
|
|
263
|
+
| `Popout.Anchor` | Wraps a sidebar so every panel opened inside it lines up with that sidebar's edge instead of with its own button. |
|
|
264
|
+
| `PopoutButton` | An icon button that stays lit while its panel is open. |
|
|
265
|
+
| `InfoCircle` | A circled "i" that reveals an explanation on hover, focus or tap. |
|
|
81
266
|
|
|
82
|
-
|
|
267
|
+
The rules a `PopoutManager` enforces, so you do not have to: Escape closes the
|
|
268
|
+
innermost panel; a click outside closes everything; opening a panel closes its
|
|
269
|
+
siblings; closing a panel closes everything opened from it; focus returns to the
|
|
270
|
+
trigger.
|
|
271
|
+
|
|
272
|
+
Where a panel opens is set by two independent props on `Popout.Panel`:
|
|
273
|
+
`anchorX` decides which edge it lines up with (`"panel"`, `"trigger"`,
|
|
274
|
+
`"parent"`, or a ref of your own) and `anchorY` how far down it opens. The
|
|
275
|
+
defaults are the arrangement most sidebars want: flush with the panel's edge,
|
|
276
|
+
level with the row that opened it.
|
|
277
|
+
|
|
278
|
+
### Glyphs
|
|
279
|
+
|
|
280
|
+
The premise of the row components is that a small drawing can replace a word,
|
|
281
|
+
which only works if the drawings are a fixed, learnable set. Two components draw
|
|
282
|
+
them:
|
|
283
|
+
|
|
284
|
+
- `FieldGlyph` -- the eight glyphs allowed inside a field's 16px slot, each with
|
|
285
|
+
a hollow and a filled form. `FIELD_GLYPH_NAMES` lists them.
|
|
286
|
+
- `UiGlyph` -- the fifteen shared marks the components draw elsewhere: chevrons,
|
|
287
|
+
a gear, a close, a plus, a check, a warning. `UI_GLYPH_NAMES` lists them.
|
|
288
|
+
|
|
289
|
+
Both take a `name` and an optional `size`, draw in `currentColor`, and are
|
|
290
|
+
hidden from screen readers, because the control around them carries the name.
|
|
291
|
+
`FIELD_LETTERS` is the small set of capital letters a field may show in place of
|
|
292
|
+
a drawing when a concept has no picture.
|
|
293
|
+
|
|
294
|
+
See every drawing at once in the **Glyphs** section of
|
|
295
|
+
[Storybook](https://graphty.app/storybook/compact-mantine/).
|
|
296
|
+
|
|
297
|
+
## A worked example
|
|
298
|
+
|
|
299
|
+
A complete section of a property panel, using one component from each group.
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import {
|
|
303
|
+
AdvancedButton,
|
|
304
|
+
ControlSection,
|
|
305
|
+
FieldGlyph,
|
|
306
|
+
FieldRow,
|
|
307
|
+
IconGroupRow,
|
|
308
|
+
PanelField,
|
|
309
|
+
RampRow,
|
|
310
|
+
ToggleRow,
|
|
311
|
+
ToggleRowGroup,
|
|
312
|
+
} from "@graphty/compact-mantine";
|
|
313
|
+
|
|
314
|
+
function NodeSizeSection({openAdvanced}: {openAdvanced: () => void}) {
|
|
83
315
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
316
|
+
<ControlSection
|
|
317
|
+
label="Node size"
|
|
318
|
+
hasConfiguredValues
|
|
319
|
+
info="Node size maps a numeric attribute onto a radius."
|
|
320
|
+
actions={<AdvancedButton label="Advanced node size" changed onClick={openAdvanced} />}
|
|
321
|
+
>
|
|
322
|
+
{/* A pair of fields, each captioned by a drawing instead of a word. */}
|
|
323
|
+
<FieldRow>
|
|
324
|
+
<PanelField label="Smallest node size" glyph="sizeSmallest" kind="number" defaultValue={1} />
|
|
325
|
+
<PanelField label="Largest node size" glyph="sizeLargest" kind="number" defaultValue={4} />
|
|
326
|
+
</FieldRow>
|
|
327
|
+
|
|
328
|
+
{/* The mapping between them, drawn at row height. */}
|
|
329
|
+
<RampRow label="Size range" min="1.0" max="4.0" variant="size" scale="sqrt" />
|
|
330
|
+
|
|
331
|
+
{/* Three options whose difference can be drawn. */}
|
|
332
|
+
<IconGroupRow
|
|
333
|
+
label="Scale"
|
|
334
|
+
hybrid
|
|
335
|
+
defaultValue="sqrt"
|
|
336
|
+
options={[
|
|
337
|
+
{value: "sqrt", label: "Square root", icon: <FieldGlyph name="scaleSqrt" />},
|
|
338
|
+
{value: "linear", label: "Linear", icon: <FieldGlyph name="scaleLinear" />},
|
|
339
|
+
{value: "log", label: "Logarithmic", icon: <FieldGlyph name="scaleLog" />},
|
|
340
|
+
]}
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
{/* Booleans with no picture, packed tighter than the other rows. */}
|
|
344
|
+
<ToggleRowGroup label="Node decorations">
|
|
345
|
+
<ToggleRow label="Labels" defaultChecked />
|
|
346
|
+
<ToggleRow label="Halos" />
|
|
347
|
+
</ToggleRowGroup>
|
|
348
|
+
</ControlSection>
|
|
88
349
|
);
|
|
89
350
|
}
|
|
90
351
|
```
|
|
91
352
|
|
|
92
|
-
###
|
|
353
|
+
### Showing a word beside every drawing
|
|
93
354
|
|
|
94
|
-
|
|
355
|
+
An icon-first panel needs a way out for readers who do not yet know the
|
|
356
|
+
drawings, and `PanelLabelsProvider` is it. It carries one preference -- off by
|
|
357
|
+
default -- to every component below it, and each one degrades in its own way: a
|
|
358
|
+
field grows its label word beside its glyph, and a row holding a pair of fields
|
|
359
|
+
becomes two single rows, each with its word in a 76px column. A pair never
|
|
360
|
+
becomes a two-line stack.
|
|
95
361
|
|
|
96
362
|
```tsx
|
|
97
|
-
|
|
98
|
-
|
|
363
|
+
<PanelLabelsProvider showLabels={settings.showLabelsOnControls}>
|
|
364
|
+
<NodeSizeSection />
|
|
365
|
+
</PanelLabelsProvider>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Read the preference anywhere below with `usePanelLabels()`. Offering it as a
|
|
369
|
+
user setting is recommended; nothing in the library requires it.
|
|
370
|
+
|
|
371
|
+
## The panel grid
|
|
372
|
+
|
|
373
|
+
The row components are laid out for a 280px column, and they all measure
|
|
374
|
+
themselves from one exported object so that a column of them lines up:
|
|
99
375
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{/* Regular Mantine UI */}
|
|
104
|
-
<TextInput label="Regular Size" />
|
|
105
|
-
|
|
106
|
-
{/* Compact region */}
|
|
107
|
-
<MantineProvider theme={compactTheme}>
|
|
108
|
-
<div className="settings-panel">
|
|
109
|
-
<TextInput label="Compact Input" />
|
|
110
|
-
<Button>Compact Button</Button>
|
|
111
|
-
</div>
|
|
112
|
-
</MantineProvider>
|
|
113
|
-
</MantineProvider>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
376
|
+
```
|
|
377
|
+
16 + 108 + 8 + 108 + 8 + 24 + 8 = 280
|
|
378
|
+
pad field gut field gap trail pad
|
|
116
379
|
```
|
|
117
380
|
|
|
118
|
-
|
|
381
|
+
`PANEL_GRID` names every number in it -- `WIDTH`, `FIELD`, `BODY`,
|
|
382
|
+
`CONTROL_HEIGHT`, `ROW_PITCH`, `TRAIL` and the rest -- so your own rows can
|
|
383
|
+
match without retyping them. It is also published on the theme as
|
|
384
|
+
`theme.other.panelGrid`.
|
|
119
385
|
|
|
120
|
-
|
|
386
|
+
`PANEL_INK` is the matching colour map: one entry per role a row paints
|
|
387
|
+
(`VALUE`, `CHROME`, `SURFACE`, `ACCENT`, `BORDER`, `SELECTED`, `DISABLED` and
|
|
388
|
+
so on), each one a Mantine CSS variable rather than a fixed colour. Paint your
|
|
389
|
+
own rows from it and they follow the light scheme, the dark scheme and your
|
|
390
|
+
primary colour for free.
|
|
121
391
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
392
|
+
```tsx
|
|
393
|
+
import { PANEL_GRID, PANEL_INK } from "@graphty/compact-mantine";
|
|
394
|
+
|
|
395
|
+
<div style={{height: PANEL_GRID.ROW_PITCH, color: PANEL_INK.CHROME}}>Custom row</div>;
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
You do not have to use a 280px column. Nothing enforces the width; the numbers
|
|
399
|
+
are there so that the components agree with each other and with anything you
|
|
400
|
+
write beside them.
|
|
401
|
+
|
|
402
|
+
## Handling events
|
|
127
403
|
|
|
128
|
-
|
|
404
|
+
Two shapes, used consistently across the library.
|
|
129
405
|
|
|
130
|
-
|
|
406
|
+
**A value change gives you the value first and the event second, and the event
|
|
407
|
+
is optional:**
|
|
131
408
|
|
|
132
409
|
```tsx
|
|
133
|
-
|
|
410
|
+
<ToggleRow label="Labels" onChange={(checked, event) => setLabels(checked)} />
|
|
411
|
+
<PanelField label="Radius" kind="number" onChange={(value) => setRadius(value)} />
|
|
134
412
|
```
|
|
135
413
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- `compactDarkColors` - Dark color palette array
|
|
414
|
+
That matches Mantine, and it means a change made in code is expressible as
|
|
415
|
+
`onChange(next)` with no event to fabricate.
|
|
139
416
|
|
|
140
|
-
|
|
417
|
+
**An activation gives you the event, never a bare callback,** so you can read
|
|
418
|
+
modifier keys, call `preventDefault()`, or find the element that was activated:
|
|
141
419
|
|
|
142
420
|
```tsx
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
421
|
+
<AdvancedButton label="Advanced" onClick={(event) => {
|
|
422
|
+
if (event.shiftKey) { openInNewPanel(); } else { open(); }
|
|
423
|
+
}} />
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Rows whose selection behaviour depends on how they were activated -- `DataRow`,
|
|
427
|
+
`MetricRow`, `ActionRow` and `DataTable`'s rows -- get a second argument saying
|
|
428
|
+
so, rather than making you sniff the event:
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
<DataRow
|
|
432
|
+
label="Mr_Whiskers"
|
|
433
|
+
value={12}
|
|
434
|
+
onClick={(event, meta) => {
|
|
435
|
+
select(id, {add: meta.source === "pointer" && event.shiftKey});
|
|
436
|
+
}}
|
|
437
|
+
/>
|
|
156
438
|
```
|
|
157
439
|
|
|
158
|
-
|
|
440
|
+
**A drag reports its start, its changes and its end,** so you can open and close
|
|
441
|
+
one undo transaction around the whole gesture:
|
|
159
442
|
|
|
160
443
|
```tsx
|
|
161
|
-
|
|
444
|
+
<PanelField
|
|
445
|
+
label="Radius"
|
|
446
|
+
glyph="width"
|
|
447
|
+
kind="number"
|
|
448
|
+
onScrubStart={() => beginUndo()}
|
|
449
|
+
onScrub={(delta) => setRadius((r) => r + delta)}
|
|
450
|
+
onScrubEnd={() => commitUndo()}
|
|
451
|
+
/>
|
|
162
452
|
```
|
|
163
453
|
|
|
164
|
-
|
|
454
|
+
**Anything that opens and closes** takes `opened`, `defaultOpened` and
|
|
455
|
+
`onOpenChange(opened, event?)`: `ControlSection`, `ControlSubGroup`,
|
|
456
|
+
`InfoCircle` and the whole `Popout` family. Supply `opened` to drive it from
|
|
457
|
+
your own state, or leave it out and let the component remember.
|
|
165
458
|
|
|
166
|
-
|
|
167
|
-
|
|
459
|
+
**Anything whose content arrives later** takes `busy` and `live`:
|
|
460
|
+
`HistogramRow`, `SparklineRow`, `MetricRow`, `CompoundRow`, `RampRow`,
|
|
461
|
+
`ProseBlock` and `ActionRow`. Supplying `busy` at all -- true or false -- is how
|
|
462
|
+
you say the content is fed by something that finishes later, and that is what
|
|
463
|
+
makes a screen reader announce it; `live` sets the politeness and defaults to
|
|
464
|
+
`"polite"` once `busy` is given.
|
|
168
465
|
|
|
169
|
-
|
|
170
|
-
|
|
466
|
+
```tsx
|
|
467
|
+
<MetricRow name="Betweenness" busy={isRunning} percentile={98} value="0.31" />
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Pass `busy` for the whole life of the component rather than only while the run
|
|
471
|
+
is in flight: a live region has to be in the document before the change it
|
|
472
|
+
announces.
|
|
473
|
+
|
|
474
|
+
Every component that holds a value works controlled (`value` plus `onChange`)
|
|
475
|
+
or uncontrolled (`defaultValue`), and `onFocus` and `onBlur` are always
|
|
476
|
+
forwarded.
|
|
477
|
+
|
|
478
|
+
## The shared prop names
|
|
479
|
+
|
|
480
|
+
A handful of names appear on most components, and each one means exactly one
|
|
481
|
+
thing everywhere.
|
|
171
482
|
|
|
172
|
-
|
|
173
|
-
|
|
483
|
+
| Prop | What it always means |
|
|
484
|
+
|------|----------------------|
|
|
485
|
+
| `label` | What the thing is called. It is always the accessible name; whether it is also drawn depends on the component and on the [`showLabels` preference](#showing-a-word-beside-every-drawing). `ChartRow`'s label is never drawn, `PanelField`'s is drawn only with the preference on, `ControlSection`'s always is. |
|
|
486
|
+
| `value` / `defaultValue` / `onChange` | The state a control holds. Supply `value` with `onChange` to drive it yourself, or `defaultValue` to let it remember. On the display-only rows -- `DataRow` and `MetricRow` -- `value` is the reading drawn on the row and there is no `onChange`. |
|
|
487
|
+
| `trailing` | The row's occasional control, in the fixed 24px slot every row ends with. Always a node, always the last thing in the row. |
|
|
488
|
+
| `actions` | Buttons that belong to a container rather than to a row: a section header's, a pop-out panel's, an action row's cluster. |
|
|
489
|
+
| `disabled` | The control is present but cannot be used: dimmed, skipped by Tab, announced as unavailable. |
|
|
490
|
+
| `selected` / `selectedIds` | Which row of a list the reader has picked. Not the same as a control's own value. |
|
|
491
|
+
| `busy` / `live` | The content arrives from something that finishes later. See [Handling events](#handling-events). |
|
|
492
|
+
| `opened` / `defaultOpened` / `onOpenChange` | Anything that opens and closes. |
|
|
174
493
|
|
|
175
|
-
|
|
176
|
-
Badge, Text, Avatar, ThemeIcon, Indicator, Kbd, Pill
|
|
494
|
+
## Internationalization
|
|
177
495
|
|
|
178
|
-
|
|
179
|
-
|
|
496
|
+
Every string this library can put on the screen or into a screen reader is
|
|
497
|
+
overridable, and none of it requires an i18n framework. With no setup you get
|
|
498
|
+
English.
|
|
180
499
|
|
|
181
|
-
|
|
182
|
-
Loader, Progress, RingProgress
|
|
500
|
+
`LabelsProvider` replaces the strings and sets the locale used for formatting:
|
|
183
501
|
|
|
184
|
-
|
|
185
|
-
|
|
502
|
+
```tsx
|
|
503
|
+
import { LabelsProvider } from "@graphty/compact-mantine";
|
|
504
|
+
|
|
505
|
+
<LabelsProvider
|
|
506
|
+
locale="de-DE"
|
|
507
|
+
labels={{
|
|
508
|
+
mixed: "Verschieden",
|
|
509
|
+
about: (label) => `Info zu ${label}`,
|
|
510
|
+
}}
|
|
511
|
+
>
|
|
512
|
+
<Inspector />
|
|
513
|
+
</LabelsProvider>;
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
- **Anything you leave out keeps its English default**, so you can translate one
|
|
517
|
+
string or all of them.
|
|
518
|
+
- **Providers nest**, and an inner one merges over the outer one, so a dialog
|
|
519
|
+
can restate a single string without repeating the rest.
|
|
520
|
+
- **Strings that interpolate are functions**, taking already-formatted pieces,
|
|
521
|
+
so a translation can put them in the order its language needs.
|
|
522
|
+
- `defaultLabels` is the complete English set, and `CompactMantineLabels` is its
|
|
523
|
+
type -- start from either when writing a translation.
|
|
524
|
+
- `useLabels()` reads the strings from your own components.
|
|
525
|
+
|
|
526
|
+
The locale is resolved in this order: the `locale` you pass, then the `lang`
|
|
527
|
+
attribute on the document, then the browser's preference, then English. It
|
|
528
|
+
drives `Intl` throughout: numbers through `Intl.NumberFormat`, ordinals through
|
|
529
|
+
`Intl.PluralRules` (which is what makes `98th percentile` come out right outside
|
|
530
|
+
English), and sorting through `Intl.Collator`. The same formatters are exported
|
|
531
|
+
for your own use:
|
|
186
532
|
|
|
187
|
-
|
|
533
|
+
```tsx
|
|
534
|
+
import { useCollator, useNumberFormatter, useOrdinalFormatter } from "@graphty/compact-mantine";
|
|
535
|
+
|
|
536
|
+
const format = useNumberFormatter({maximumFractionDigits: 2});
|
|
537
|
+
format.format(1234.5678); // "1,234.57" in en, "1.234,57" in de
|
|
538
|
+
```
|
|
188
539
|
|
|
189
|
-
|
|
540
|
+
Every component draws from the same set, `DataTable` included. A table also
|
|
541
|
+
takes a `labels` prop of its own, which merges over whatever the provider says
|
|
542
|
+
-- reach for it when one table counts nodes and another counts files, and for
|
|
543
|
+
`LabelsProvider` when you are translating.
|
|
190
544
|
|
|
191
|
-
|
|
192
|
-
|
|
545
|
+
## Right-to-left languages
|
|
546
|
+
|
|
547
|
+
Wrap your application in Mantine's `DirectionProvider` and every component here
|
|
548
|
+
follows. Layouts are written in logical properties, so padding, gaps and the
|
|
549
|
+
order of a row all reverse; arrow keys in a segmented control follow the text
|
|
550
|
+
direction; charts and ramps are drawn from the start of the axis rather than
|
|
551
|
+
from the left edge, so the picture agrees with the labels beside it.
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
import { DirectionProvider, MantineProvider } from "@mantine/core";
|
|
555
|
+
|
|
556
|
+
<DirectionProvider initialDirection="rtl">
|
|
557
|
+
<MantineProvider theme={compactTheme}>
|
|
558
|
+
<App />
|
|
559
|
+
</MantineProvider>
|
|
560
|
+
</DirectionProvider>;
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Storybook has a **Direction** toolbar that flips every story, and most component
|
|
564
|
+
pages also ship a dedicated right-to-left story, so you can see what your own
|
|
565
|
+
layout will do.
|
|
566
|
+
|
|
567
|
+
## Accessibility
|
|
568
|
+
|
|
569
|
+
The library targets WCAG 2.2 AA, and the work is done for you rather than left
|
|
570
|
+
as a set of props you must remember:
|
|
571
|
+
|
|
572
|
+
- **Every control is a real control.** Fields are inputs, toggles are checkboxes
|
|
573
|
+
or switches, segmented options are radios, the advanced button is a button, so
|
|
574
|
+
keyboard behaviour, focus order and disabled semantics come from the platform.
|
|
575
|
+
- **Keyboard focus is always visible**, and pointer focus is not, so a dense
|
|
576
|
+
surface stays quiet under the mouse without giving up the focus indicator.
|
|
577
|
+
- **State is announced, not only drawn.** Expanded, checked, selected, disabled,
|
|
578
|
+
busy and current are all exposed as ARIA in addition to colour.
|
|
579
|
+
- **Charts are one named image with a hidden table behind them**, so a screen
|
|
580
|
+
reader announces the chart and can then read every value, rather than
|
|
581
|
+
announcing a run of anonymous bars. Give every chart a `label`.
|
|
582
|
+
- **Results that arrive late are announced.** Give a row `busy` and it becomes a
|
|
583
|
+
polite live region; while `busy` is true the announcement is held back, so the
|
|
584
|
+
reader hears the finished result once rather than every frame of it. A row
|
|
585
|
+
that is never given `busy` stays silent, because most rows hold a value the
|
|
586
|
+
reader set themselves.
|
|
587
|
+
- **Hover-revealed controls also appear on focus**, stay in the document, and
|
|
588
|
+
stay in the tab order.
|
|
589
|
+
- **Colour is never the only signal.** An advanced button whose settings have
|
|
590
|
+
changed says so in its accessible name; a section with non-default values does
|
|
591
|
+
the same.
|
|
592
|
+
|
|
593
|
+
Storybook runs axe-core against every story, so a regression shows up in the
|
|
594
|
+
Accessibility panel before it is published.
|
|
595
|
+
|
|
596
|
+
## TypeScript
|
|
597
|
+
|
|
598
|
+
The package is written in TypeScript and ships its own declarations; there is no
|
|
599
|
+
`@types` package to install. Every component's props are exported as a named
|
|
600
|
+
type, so you can extend them:
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
import type { DataRowProps, PanelFieldProps } from "@graphty/compact-mantine";
|
|
604
|
+
|
|
605
|
+
interface MyRowProps extends DataRowProps {
|
|
606
|
+
nodeId: string;
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
The prop documentation you see in your editor is the same text that appears in
|
|
611
|
+
Storybook's prop tables, so a tooltip is the primary reference and this README
|
|
612
|
+
does not repeat it.
|
|
613
|
+
|
|
614
|
+
A few types are worth knowing by name: `FieldGlyphName` and `UiGlyphName` are
|
|
615
|
+
the closed glyph registers (`isFieldGlyphName` and `isFieldLetter` narrow an
|
|
616
|
+
unknown string to them), `DataTableColumn<TRow>` describes a table column, and
|
|
617
|
+
`CompactMantineLabels` is the string set.
|
|
618
|
+
|
|
619
|
+
## Which one should I use?
|
|
620
|
+
|
|
621
|
+
Some components overlap, and a few sit beside a Mantine component that looks
|
|
622
|
+
as though it would do. This table names the one to reach for, and what it is
|
|
623
|
+
being chosen over where that choice is not obvious.
|
|
624
|
+
|
|
625
|
+
| If you have | Use | Instead of |
|
|
626
|
+
|-------------|-----|------------|
|
|
627
|
+
| A label and a reading on one line | `DataRow`, with the reading already formatted | -- it draws `value` verbatim and gives the pair no accessible name, so run a number through `useNumberFormatter().format(n)` yourself, and name the pair yourself where a reader has to hear the two together |
|
|
628
|
+
| A gear that opens advanced settings | `AdvancedButton`, with `changed` | -- |
|
|
629
|
+
| A group of controls that folds away | `ControlSection` | -- |
|
|
630
|
+
| A group of controls that must not fold, or whose rule has to bleed to the edges of a padded container | `ControlGroup` | -- |
|
|
631
|
+
| A checkbox on its own line | `ToggleRow` inside a `ToggleRowGroup` | -- |
|
|
632
|
+
| A checkbox that reveals the settings it turns on | `ToggleWithContent` | -- |
|
|
633
|
+
| A dropdown in a panel row | `PanelField` with `kind="select"` | -- |
|
|
634
|
+
| A dropdown with a default the reader can override | `StyleSelect` | -- |
|
|
635
|
+
| Two to six drawable options in a panel row | `IconGroupRow` | a bare Mantine `SegmentedControl`, which `IconGroupRow` is built on and adds the panel grid, the glyphs and the `showLabels` preference to |
|
|
636
|
+
| An icon button that opens a pop-out | `PopoutButton`, inside `Popout.Trigger` | `AdvancedButton`, which is for a row's or a section's advanced settings and does not light up while a panel is open |
|
|
637
|
+
| An explanation bubble | `InfoCircle` | a Mantine `Popover`, which does not share this library's dismissal rules |
|
|
638
|
+
|
|
639
|
+
A lone boolean is not a row: put it in the trailing slot of the row it modifies,
|
|
640
|
+
or make it one tile of an `IconGroupRow`. `ToggleRowGroup` warns in development
|
|
641
|
+
when given only one child, for exactly this reason.
|
|
642
|
+
|
|
643
|
+
Every component puts its own name, in kebab case, on a root `data-testid` --
|
|
644
|
+
`advanced-button`, `metric-row`, `popout-panel` -- so a test can find any of
|
|
645
|
+
them the same way.
|
|
646
|
+
|
|
647
|
+
## Contributing
|
|
648
|
+
|
|
649
|
+
The package lives in the [graphty monorepo](https://github.com/graphty-org/graphty-monorepo)
|
|
650
|
+
under `compact-mantine/`.
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
pnpm install # from the monorepo root
|
|
654
|
+
cd compact-mantine
|
|
655
|
+
npm run storybook # http://localhost:9060
|
|
656
|
+
npm run test:run # unit and browser tests, once
|
|
657
|
+
npm run lint # ESLint
|
|
658
|
+
npm run build # library build into dist/
|
|
659
|
+
npm run build-storybook # what CI builds
|
|
660
|
+
```
|
|
193
661
|
|
|
194
|
-
|
|
662
|
+
Storybook is the development environment: every component has a page, and a
|
|
663
|
+
change to a component shows up there without a rebuild. New components need a
|
|
664
|
+
story, a test and complete prop documentation -- the prop comments are compiled
|
|
665
|
+
into both the published type declarations and Storybook's tables, so they are
|
|
666
|
+
read by people who will never see this repository.
|
|
667
|
+
|
|
668
|
+
A new component follows the conventions the rest of the library already keeps,
|
|
669
|
+
each of which is written down in exactly one place and asserted in
|
|
670
|
+
`tests/consistency.test.tsx`:
|
|
671
|
+
|
|
672
|
+
- **Handlers** come from `src/types/events.ts`. A value change is
|
|
673
|
+
`ChangeHandler<T>`, an activation is `ActivationHandler`, a gesture is a
|
|
674
|
+
start/change/end triple, and anything that opens and closes extends
|
|
675
|
+
`DisclosureProps`. Never write a bare `() => void`.
|
|
676
|
+
- **Strings** go in `src/i18n/labels.ts`, never inline. Formatting goes through
|
|
677
|
+
the `Intl` hooks in `src/i18n/formatters.ts`, never through `parseFloat` or a
|
|
678
|
+
hand-written suffix.
|
|
679
|
+
- **Layout** is written in logical CSS properties. Where a drawing cannot be
|
|
680
|
+
expressed logically -- a gradient, a clip path, an SVG -- use the helpers in
|
|
681
|
+
`src/utils/rtl.ts`.
|
|
682
|
+
- **Announcements** go through `liveRegionProps` in `src/utils/live-region.ts`,
|
|
683
|
+
so every component answers `busy` and `live` the same way.
|
|
684
|
+
- **Development warnings** go through `useDevWarning` in
|
|
685
|
+
`src/utils/dev-warning.ts`, so they all fire once, from an effect, in plain
|
|
686
|
+
English, naming the component first.
|
|
687
|
+
- **Test hooks** are a root `data-testid` holding the component's own name in
|
|
688
|
+
kebab case, with the parts named after it.
|
|
689
|
+
- **Internal shorthand belongs in `//` comments only.** A `/** */` block is
|
|
690
|
+
compiled into `dist/index.d.ts` and into Storybook's prop tables, where a
|
|
691
|
+
stranger reads it.
|
|
692
|
+
|
|
693
|
+
Bugs and questions: <https://github.com/graphty-org/graphty-monorepo/issues>.
|
|
195
694
|
|
|
196
695
|
## License
|
|
197
696
|
|