@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 CHANGED
@@ -2,16 +2,51 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@graphty/compact-mantine.svg)](https://www.npmjs.com/package/@graphty/compact-mantine)
4
4
  [![Storybook](https://img.shields.io/badge/storybook-examples-ff4785)](https://graphty.app/storybook/compact-mantine/)
5
-
6
- A Mantine theme and component library optimized for dense, compact UIs. All components default to compact sizing automatically using Mantine's `defaultProps` system.
7
-
8
- ## Features
9
-
10
- - **Compact by Default**: All components render at compact size without explicit props
11
- - **Mantine-Idiomatic**: Uses `defaultProps` with `size="sm"` as default
12
- - **Global Token Overrides**: Smaller font sizes, tighter spacing, compact radii
13
- - **Visual Styling**: Borderless inputs, semantic color backgrounds
14
- - **Fully Customizable**: Spread the theme to add your own customizations
5
+ [![license](https://img.shields.io/npm/l/@graphty/compact-mantine.svg)](./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
- ## Quick Start
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 { MantineProvider, TextInput, Button } from '@mantine/core';
26
- import { compactTheme } from '@graphty/compact-mantine';
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
- {/* Components use compact sizing automatically */}
32
- <TextInput label="Name" />
33
- <Button>Submit</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
- ## Usage
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
- ### Default Compact Sizing
116
+ ### Components the theme restyles
42
117
 
43
- No size prop needed - components render compact by default:
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
- // All of these use compact sizing automatically
47
- <TextInput label="Name" />
48
- <NumberInput label="Amount" />
49
- <Select label="Country" data={countries} />
50
- <Button>Submit</Button>
51
- <Checkbox label="I agree" />
52
- <Switch label="Enable notifications" />
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
- ### Override to Larger Sizes
145
+ <MantineProvider theme={theme}>{children}</MantineProvider>;
146
+ ```
56
147
 
57
- When you need larger components, use the standard Mantine size prop:
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
- // Override to larger sizes when needed
61
- <TextInput size="md" label="Medium Input" />
62
- <TextInput size="lg" label="Large Input" />
63
- <Button size="lg">Large Button</Button>
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
- ### Theme Customization
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
- Spread the compact theme to add your own customizations:
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 { MantineProvider } from '@mantine/core';
72
- import { compactTheme } from '@graphty/compact-mantine';
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
- const customTheme = {
75
- ...compactTheme,
76
- primaryColor: 'teal',
77
- other: {
78
- myCustomProperty: true,
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
- function App() {
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
- <MantineProvider theme={customTheme}>
85
- {/* Still compact by default with your customizations */}
86
- <Button>Teal Button</Button>
87
- </MantineProvider>
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
- ### Compact Region in Larger UI
353
+ ### Showing a word beside every drawing
93
354
 
94
- Use nested `MantineProvider` to create compact regions within a regular-sized UI:
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
- import { MantineProvider, TextInput, Button } from '@mantine/core';
98
- import { compactTheme } from '@graphty/compact-mantine';
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
- function App() {
101
- return (
102
- <MantineProvider>
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
- ## Global Token Overrides
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
- The compact theme overrides Mantine's global tokens:
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
- | Token | Values |
123
- |-------|--------|
124
- | **fontSizes** | xs=10px, sm=11px, md=13px, lg=14px, xl=16px |
125
- | **spacing** | xs=4px, sm=6px, md=8px, lg=12px, xl=16px |
126
- | **radius** | xs=2px, sm=4px, md=6px, lg=8px, xl=12px |
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
- ## Exports
404
+ Two shapes, used consistently across the library.
129
405
 
130
- ### Theme
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
- import { compactTheme, compactColors, compactDarkColors } from '@graphty/compact-mantine';
410
+ <ToggleRow label="Labels" onChange={(checked, event) => setLabels(checked)} />
411
+ <PanelField label="Radius" kind="number" onChange={(value) => setRadius(value)} />
134
412
  ```
135
413
 
136
- - `compactTheme` - Complete Mantine theme with compact defaults
137
- - `compactColors` - Color palette including dark colors
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
- ### Components
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
- import {
144
- CompactColorInput,
145
- ControlGroup,
146
- ControlSection,
147
- ControlSubGroup,
148
- EffectToggle,
149
- GradientEditor,
150
- Popout,
151
- PopoutManager,
152
- StatRow,
153
- StyleNumberInput,
154
- StyleSelect,
155
- } from '@graphty/compact-mantine';
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
- ### Hooks
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
- import { useActualColorScheme } from '@graphty/compact-mantine';
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
- ## Affected Components (43 total)
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
- ### Input Components (14)
167
- TextInput, NumberInput, Select, Textarea, PasswordInput, Autocomplete, MultiSelect, TagsInput, PillsInput, FileInput, JsonInput, ColorInput, NativeSelect, InputWrapper
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
- ### Button Components (3)
170
- Button, ActionIcon, CloseButton
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
- ### Control Components (6)
173
- Switch, Checkbox, Radio, Slider, RangeSlider, SegmentedControl
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
- ### Display Components (7)
176
- Badge, Text, Avatar, ThemeIcon, Indicator, Kbd, Pill
494
+ ## Internationalization
177
495
 
178
- ### Navigation Components (6)
179
- Tabs, NavLink, Pagination, Stepper, Anchor, Burger
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
- ### Feedback Components (3)
182
- Loader, Progress, RingProgress
500
+ `LabelsProvider` replaces the strings and sets the locale used for formatting:
183
501
 
184
- ### Overlay Components (4)
185
- Menu, Tooltip, Popover, HoverCard
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
- ## Migration from size="compact"
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
- If you're migrating from the previous `size="compact"` pattern:
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
- 1. Remove all `size="compact"` props - components are now compact by default
192
- 2. Add `size="md"` or `size="lg"` where you previously relied on Mantine's default sizing
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
- See the [Migration Guide](./stories/docs/Migration.mdx) for detailed instructions.
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