@matthiaskrijgsman/mat-ui 0.0.40 → 0.0.42

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
@@ -33,7 +33,7 @@ pnpm install @matthiaskrijgsman/mat-ui
33
33
 
34
34
  ### Styles
35
35
 
36
- Import the mat-ui stylesheet in your CSS entry file:
36
+ Import the mat-ui stylesheet in your CSS entry file. Be sure to do this **after** the Tailwind import.
37
37
 
38
38
  ```css
39
39
  @import "@matthiaskrijgsman/mat-ui/style";
@@ -49,141 +49,6 @@ function App() {
49
49
  }
50
50
  ```
51
51
 
52
- ## Rich text editor (`InputLexical`)
53
-
54
- `InputLexical` is a [Lexical](https://lexical.dev)-powered rich text editor styled like the rest of the kit. It ships with two toolbar variants that share the exact same controls:
55
-
56
- - **`static`** (default) — a light toolbar fixed at the top of the editor.
57
- - **`floating`** — a dark bar that appears above the editor while it is focused, matching the editor width.
58
-
59
- Lexical and its plugins are **peer dependencies** — install them alongside the library:
60
-
61
- ```bash
62
- pnpm add lexical @lexical/react @lexical/rich-text @lexical/list @lexical/link @lexical/selection @lexical/utils
63
- ```
64
-
65
- ### Basic usage
66
-
67
- The value is a **serialized Lexical editor state** (a JSON string). Pass the last `onChange` value back as `value` to restore content.
68
-
69
- ```tsx
70
- import { useState } from "react";
71
- import { InputLexical } from "@matthiaskrijgsman/mat-ui";
72
-
73
- function Editor() {
74
- const [value, setValue] = useState<string>();
75
-
76
- return (
77
- <InputLexical
78
- label={"Description"}
79
- placeholder={"Write something…"}
80
- toolbar={"floating"} // or "static" (default)
81
- value={value}
82
- onChange={setValue}
83
- autogrow // grow with content…
84
- minRows={4} // …from a 4-row floor…
85
- maxRows={12} // …up to 12 rows, then scroll
86
- />
87
- );
88
- }
89
- ```
90
-
91
- Sizing mirrors `InputTextArea`: `minRows` sets a height floor, `maxRows` caps the height (content beyond it scrolls), and `autogrow` lets the editor grow with its content between the two. Without `autogrow` the editor is fixed at `minRows`.
92
-
93
- ### Extending the toolbar
94
-
95
- The toolbar is assembled from exported **building blocks**, so you can reorder, drop, or add controls via the `renderToolbar` slot. It receives `{ editor, state, tone }` and renders into whichever variant is active — the same render function drives both the static and floating bars.
96
-
97
- ```tsx
98
- import {
99
- InputLexical,
100
- LexicalBlockTypeSelect,
101
- LexicalFormatButtons,
102
- LexicalListButtons,
103
- LexicalLinkButton,
104
- LexicalHistoryButtons,
105
- LexicalToolbarDivider,
106
- } from "@matthiaskrijgsman/mat-ui";
107
-
108
- <InputLexical
109
- renderToolbar={() => (
110
- <>
111
- <LexicalFormatButtons/>
112
- <LexicalToolbarDivider/>
113
- <LexicalListButtons/>
114
- <LexicalLinkButton/>
115
- <LexicalToolbarDivider/>
116
- <LexicalHistoryButtons/>
117
- </>
118
- )}
119
- />;
120
- ```
121
-
122
- Building blocks read the active editor and formatting `state`/`tone` from context, so they work in either variant with no extra wiring. Dividers automatically flip orientation (and the whole toolbar collapses overflowing controls into a vertical `⋮` dropdown) when space runs out — return each control as a top-level child so it stays individually measurable.
123
-
124
- #### Custom controls
125
-
126
- Build your own control with `LexicalToolbarButton` plus Lexical's editor context. `useLexicalToolbar()` exposes the current `{ state, tone }`:
127
-
128
- ```tsx
129
- import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
130
- import { FORMAT_TEXT_COMMAND } from "lexical";
131
- import { IconStrikethrough } from "@tabler/icons-react";
132
- import { LexicalToolbarButton, useLexicalToolbar } from "@matthiaskrijgsman/mat-ui";
133
-
134
- const StrikethroughButton = () => {
135
- const [editor] = useLexicalComposerContext();
136
- const { tone } = useLexicalToolbar();
137
- return (
138
- <LexicalToolbarButton
139
- Icon={IconStrikethrough}
140
- tone={tone}
141
- aria-label={"Strikethrough"}
142
- onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")}
143
- />
144
- );
145
- };
146
- ```
147
-
148
- #### Registering extra Lexical nodes
149
-
150
- The built-in set covers headings, lists, links, and quotes. For anything else (tables, mentions, code blocks, …) install the node's package yourself and pass the node via the `nodes` prop — it is registered alongside the built-in set:
151
-
152
- ```bash
153
- pnpm add @lexical/code
154
- ```
155
-
156
- ```tsx
157
- import { CodeNode } from "@lexical/code";
158
-
159
- <InputLexical nodes={[CodeNode]} renderToolbar={/* … */} />;
160
- ```
161
-
162
- > mat-ui does not bundle `lexical` or any `@lexical/*` package — they are peer dependencies, so a single shared copy is used. Only register a given node type once: don't pass a node that is already in the built-in set, or Lexical throws a duplicate-type error.
163
-
164
- #### Adding plugins (the `children` slot)
165
-
166
- A Lexical feature is usually a **node *plus* a plugin**. Register the node with `nodes`, then mount the plugin(s) as **children** — they run inside the editor alongside the built-ins (history, lists, links). Any `@lexical/react` plugin or your own works:
167
-
168
- ```tsx
169
- import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
170
- import { CodeHighlightPlugin } from "@lexical/react/LexicalCodeHighlightPlugin";
171
-
172
- <InputLexical nodes={[CodeNode]}>
173
- <CodeHighlightPlugin />
174
- <TabIndentationPlugin />
175
- {/* …or your own plugin using useLexicalComposerContext() */}
176
- </InputLexical>;
177
- ```
178
-
179
- Style custom nodes by merging theme classes over the defaults with the `theme` prop:
180
-
181
- ```tsx
182
- <InputLexical nodes={[CodeNode]} theme={{ code: "my-code-block" }}>
183
- <CodeHighlightPlugin />
184
- </InputLexical>;
185
- ```
186
-
187
52
  ## Dark Theme
188
53
 
189
54
  mat-ui ships with built-in dark theme support. Add the `dark` class to the `<html>` element to activate it:
@@ -205,44 +70,140 @@ document.documentElement.classList.toggle('dark', prefersDark);
205
70
 
206
71
  All components adapt automatically — no additional props or configuration needed.
207
72
 
208
- ## CSS Design Tokens
209
-
210
- mat-ui uses CSS custom properties (design tokens) for all component colors. These are defined on `:root` for light mode and overridden under `.dark` for dark mode. You can customize the look of any component by overriding these tokens in your own CSS.
73
+ ## Theming with design tokens
211
74
 
212
- ### Overriding tokens
213
-
214
- Define your overrides after importing the mat-ui stylesheet:
75
+ Every visual property in mat-ui — color, shape, type, elevation and size — is driven by CSS custom properties (design tokens) defined on `:root`. There is no Tailwind config to fork and no component props to thread: you retheme the whole kit by overriding tokens in your own CSS, after importing the stylesheet.
215
76
 
216
77
  ```css
217
78
  @import "@matthiaskrijgsman/mat-ui/style";
218
79
 
219
80
  :root {
220
- /* Change the primary button to your brand color */
221
- --color-button-primary-bg: #0ea5e9;
222
- --color-button-primary-border: #0ea5e9;
223
- --color-button-primary-bg-active: #0284c7;
224
- --color-button-primary-border-active: #0284c7;
81
+ --color-button-primary-bg: #0ea5e9; /* brand color */
82
+ --border-radius-input: 0px; /* square corners everywhere */
83
+ --font-weight-button: 700; /* bolder buttons */
225
84
  }
85
+ ```
86
+
87
+ ### How the tokens are organised
88
+
89
+ Tokens fall into three families:
226
90
 
227
- /* Override dark theme values too */
228
- .dark {
229
- --color-button-primary-bg: #38bdf8;
230
- --color-button-primary-border: #38bdf8;
231
- --color-button-primary-bg-active: #0ea5e9;
232
- --color-button-primary-border-active: #0ea5e9;
91
+ - **Structure** typography (family, weight, size), border radius, border width, shadow, ring (focus/hover) width and transition timing. Theme-independent.
92
+ - **Sizing** — the shared `sm | md | lg` control scale (height, padding, gap, icon size).
93
+ - **Color** — every surface, border, text and state color. Defined on `:root` (light) and overridden under `.dark`.
94
+
95
+ **Structure** and **color** tokens use a two-tier model:
96
+
97
+ 1. **Base scales** — a small set of primitives (e.g. `--font-weight-strong`, `--radius-xl`). Change one to shift the whole kit at once.
98
+ 2. **Semantic aliases** — per-component tokens that point at the base scale by default (e.g. `--font-weight-button: var(--font-weight-strong)`, `--border-radius-dropdown: var(--radius-xl)`). Override one to retheme a single component without touching anything else.
99
+
100
+ So `--font-weight-strong: 700` makes every emphasised element heavier, while `--font-weight-button: 700` changes only buttons. Pick the tier that matches how broad your change is.
101
+
102
+ > Dark mode: add the `dark` class to `<html>` (see [Dark Theme](#dark-theme)). Only **color** tokens differ between themes — structure and sizing are shared, so you never duplicate them under `.dark`.
103
+
104
+ ### Worked examples
105
+
106
+ Square, flat, heavier — in four tokens:
107
+
108
+ ```css
109
+ :root {
110
+ --border-radius-input: 0px; /* inputs, selects, buttons */
111
+ --border-radius-panel: 0.25rem; /* panels, modals */
112
+ --border-width-input: 2px; /* all control & surface borders */
113
+ --font-weight-strong: 700; /* buttons, badges, tabs, dropdown items… */
233
114
  }
234
115
  ```
235
116
 
236
- ### Token reference
117
+ Set the kit's typeface in one place:
237
118
 
238
- #### General
239
-
240
- | Token | Description | Light default |
241
- |-------|-------------|---------------|
242
- | `--color-input-focus-ring` | Focus ring color for buttons and inputs | `rgb(17 24 39 / 0.15)` |
243
- | `--border-radius-input` | Border radius for inputs and selects | `var(--radius-xl)` |
119
+ ```css
120
+ :root {
121
+ --font-family-base: "Inter", system-ui, sans-serif;
122
+ }
123
+ ```
244
124
 
245
- #### Control sizing
125
+ ---
126
+
127
+ ## Token reference
128
+
129
+ ### Structure — typography
130
+
131
+ Font weights resolve through a three-step base scale; the semantic tokens below point at it by default.
132
+
133
+ | Base token | Default |
134
+ |------------|---------|
135
+ | `--font-weight-normal` | `400` |
136
+ | `--font-weight-medium` | `500` |
137
+ | `--font-weight-strong` | `600` |
138
+
139
+ | Semantic token | Applies to | Default |
140
+ |----------------|-----------|---------|
141
+ | `--font-weight-input-text` | Text typed into inputs, selects, textareas | `--font-weight-normal` |
142
+ | `--font-weight-button` | `Button`, `ButtonIconSquare`, `ButtonIconRound`, file-input action text | `--font-weight-strong` |
143
+ | `--font-weight-badge` | `Badge` | `--font-weight-strong` |
144
+ | `--font-weight-tab` | `TabButtons` | `--font-weight-strong` |
145
+ | `--font-weight-input-label` | `InputLabel` (label above inputs) | `--font-weight-medium` |
146
+ | `--font-weight-input-description` | `InputDescription` | `--font-weight-medium` |
147
+ | `--font-weight-input-error` | `InputError` | `--font-weight-medium` |
148
+ | `--font-weight-input-option-label` | Inline labels on `InputCheck` / `InputRadio` / `InputToggle`, file tile names | `--font-weight-medium` |
149
+ | `--font-weight-dropdown-item` | `DropdownButton`, `PanelLink` | `--font-weight-strong` |
150
+ | `--font-weight-group-header` | Dropdown group labels, select group headers | `--font-weight-strong` |
151
+ | `--font-weight-panel-field` | `PanelField` label | `--font-weight-medium` |
152
+ | `--font-weight-panel-link` | `PanelLink` | `--font-weight-strong` |
153
+ | `--font-weight-table-header` | `Table` header cells, `TableEmpty` title | `--font-weight-medium` |
154
+ | `--font-weight-table-cell` | `Table` body cells | `--font-weight-normal` |
155
+
156
+ | Token | Description | Default |
157
+ |-------|-------------|---------|
158
+ | `--font-family-base` | Typeface for all kit text (defaults to the host font) | `inherit` |
159
+ | `--font-size-label` | Dropdown / select group label size | `var(--text-sm)` |
160
+ | `--font-size-description` | `InputDescription` and `PanelField` label size | `var(--text-sm)` |
161
+ | `--font-size-error` | `InputError` size | `var(--text-sm)` |
162
+
163
+ > The text size of the input/button itself comes from the **control sizing** scale below (`--control-size-{size}-font-size`), not from these tokens.
164
+
165
+ ### Structure — border radius
166
+
167
+ Semantic radius tokens map onto Tailwind's radius scale. Override a token to change one group; override the underlying `--radius-*` to change several at once.
168
+
169
+ | Token | Applies to | Default |
170
+ |-------|-----------|---------|
171
+ | `--border-radius-input` | Text inputs, selects, textareas, file inputs, the Lexical editor box | `var(--radius-xl)` |
172
+ | `--border-radius-button` | `Button`, `ButtonIconSquare` (and the file-input "Choose" button) | `var(--border-radius-input)` |
173
+ | `--border-radius-panel` | `Panel`, `PanelStack`, `Modal`, `TableEmpty` icon frame | `var(--radius-2xl)` |
174
+ | `--border-radius-dropdown` | `DropdownPanel`, Lexical floating toolbar | `var(--radius-xl)` |
175
+ | `--border-radius-option` | Select option rows | `var(--radius-xl)` |
176
+ | `--border-radius-menu-item` | `DropdownButton`, `PanelLink`, Lexical toolbar buttons | `var(--radius-lg)` |
177
+ | `--border-radius-badge` | `Badge` | `var(--radius-lg)` |
178
+ | `--border-radius-tab` | `TabButtons` container and pills | `var(--radius-xl)` |
179
+ | `--border-radius-checkbox` | `InputCheck` box | `var(--radius-lg)` |
180
+ | `--border-radius-control-inner` | Color swatch and picker bars in `InputColor` | `var(--radius-md)` |
181
+
182
+ > `ButtonIconRound`, the toggle track/thumb, and radio dots are intentionally fully round (`rounded-full`) and are not tokenized.
183
+
184
+ ### Structure — border width & shadow
185
+
186
+ | Token | Applies to | Default |
187
+ |-------|-----------|---------|
188
+ | `--border-width-input` | Border width of inputs, selects, buttons, panels, dropdowns, modals, check/radio | `1px` |
189
+ | `--shadow-control` | Resting elevation of buttons, inputs, panels, tabs | `var(--shadow-sm)` |
190
+ | `--shadow-dropdown` | `DropdownPanel` and the Lexical floating toolbar | `var(--shadow-lg)` |
191
+ | `--shadow-overlay` | `Modal` and `SidebarModal` | `var(--shadow-xl)` |
192
+
193
+ ### Structure — ring & transition
194
+
195
+ Controls share a consistent interaction model: a focus/hover "glow" ring, a thinner inset ring on press, and a single transition duration. The ring **color** comes from the per-component `--color-*-ring` tokens (see the color sections); these set its **width** and the animation timing.
196
+
197
+ | Token | Applies to | Default |
198
+ |-------|-----------|---------|
199
+ | `--control-ring-width` | Ring width on hover, focus, focus-within, and the select/dropzone open state | `4px` |
200
+ | `--control-ring-width-active` | Ring width on press (`:active`) | `1px` |
201
+ | `--control-transition-duration` | Duration of hover/focus/press transitions on buttons, inputs, selects, the icon-button press scale, etc. | `150ms` |
202
+ | `--control-transition-duration-fast` | Quicker color-only transitions (table row/header hover, clickable `Badge`) | `100ms` |
203
+
204
+ > The resting state is always ringless (`ring-0`) and a few elements opt out of a focus ring entirely (dropdown items, tabs) — these are intentional and not tokenized.
205
+
206
+ ### Control sizing
246
207
 
247
208
  `Button`, `ButtonIconSquare`, `ButtonIconRound`, `Input`, `InputColor`, `InputTextArea`, `InputSelectNative`, `InputSelect`, `InputSelectSearchable`, and `InputSelectSearchableAsync` accept a `size?: 'sm' | 'md' | 'lg'` prop (default `'md'`) and read their dimensions from a single shared scale. Override these to adjust heights, padding, font size, and icon sizing consistently across all controls. Replace `{size}` with `sm`, `md`, or `lg`.
248
209
 
@@ -255,7 +216,13 @@ Define your overrides after importing the mat-ui stylesheet:
255
216
  | `--control-size-{size}-icon` | Icon glyph size inside controls | `1rem` · `1.25rem` · `1.5rem` |
256
217
  | `--control-size-{size}-icon-offset` | Distance from the input edge to a leading icon (used when an `Icon` prop is set on `Input`) | `1rem` · `1rem` · `1.25rem` |
257
218
 
258
- #### Buttons
219
+ ### Color — focus ring
220
+
221
+ | Token | Description | Light default |
222
+ |-------|-------------|---------------|
223
+ | `--color-input-focus-ring` | Focus ring color for buttons and inputs | `rgb(17 24 39 / 0.15)` |
224
+
225
+ ### Color — buttons
259
226
 
260
227
  Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `tertiary`) uses the same set of tokens. Replace `{variant}` with the variant name.
261
228
 
@@ -273,7 +240,7 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
273
240
  | `--color-button-{variant}-border-disabled` | Border when disabled |
274
241
  | `--color-button-{variant}-text-disabled` | Text color when disabled |
275
242
 
276
- #### Inputs
243
+ ### Color — inputs
277
244
 
278
245
  | Token | Description | Light default |
279
246
  |-------|-------------|---------------|
@@ -286,7 +253,7 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
286
253
  | `--color-input-ring-error` | Ring color in error state | `rgb(220 38 38 / 0.2)` |
287
254
  | `--color-input-icon` | Leading icon color | `rgb(17 24 39 / 0.6)` |
288
255
 
289
- #### Input labels, descriptions & errors
256
+ ### Color — input labels, descriptions & errors
290
257
 
291
258
  | Token | Description | Light default |
292
259
  |-------|-------------|---------------|
@@ -294,16 +261,16 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
294
261
  | `--color-input-description-text` | Description text color | `#6b7280` |
295
262
  | `--color-input-error-text` | Error message text color | `#dc2626` |
296
263
 
297
- #### Input icon buttons
264
+ ### Color — input icon buttons
298
265
 
299
266
  | Token | Description | Light default |
300
267
  |-------|-------------|---------------|
301
268
  | `--color-input-icon-button-ring` | Ring color for icon buttons inside inputs | `#e5e7eb` |
302
269
  | `--color-input-icon-button-icon` | Icon color for icon buttons inside inputs | `#6b7280` |
303
270
 
304
- #### File inputs
271
+ ### Color — file inputs
305
272
 
306
- `InputFileSingle` and `UploadFileTile` are composed from existing primitives (input tokens, `Button` for the inset "Choose" button, `ButtonIconSquare` for the remove (X) button) — no dedicated tokens of their own. `InputFileMultiple` adds:
273
+ `InputFileSingle` and `UploadFileTile` are composed from existing primitives (input tokens, `Button` for the inset "Choose" button, `ButtonIconSquare` for the remove (X) button) — no dedicated color tokens of their own. `InputFileMultiple` adds:
307
274
 
308
275
  | Token | Description | Light default |
309
276
  |-------|-------------|---------------|
@@ -311,11 +278,11 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
311
278
 
312
279
  All three components also rely on the shared `--color-status-success` / `--color-status-error` tokens for the green check / red error icons in their upload-state slots.
313
280
 
314
- #### Color input
281
+ ### Color — color input
315
282
 
316
283
  `InputColor` reuses the standard input tokens — the color swatch in the field and the outline of the picker's saturation/value plane both derive from `--color-input-border`, and the field itself uses the same `--color-input-*` tokens as `Input`. The picker's hue/brightness gradients and indicator rings are intrinsic to the color-picking UI (not theme-based) and are intentionally not tokenized.
317
284
 
318
- #### Select options
285
+ ### Color — select options
319
286
 
320
287
  | Token | Description | Light default |
321
288
  |-------|-------------|---------------|
@@ -327,7 +294,7 @@ All three components also rely on the shared `--color-status-success` / `--color
327
294
  | `--color-option-text-disabled` | Disabled option text color | `#9ca3af` |
328
295
  | `--color-input-select-placeholder` | Select placeholder text color | `#6b7280` |
329
296
 
330
- #### Select search bar
297
+ ### Color — select search bar
331
298
 
332
299
  | Token | Description | Light default |
333
300
  |-------|-------------|---------------|
@@ -335,7 +302,7 @@ All three components also rely on the shared `--color-status-success` / `--color
335
302
  | `--color-select-search-bg` | Search input background | `rgb(255 255 255 / 0.5)` |
336
303
  | `--color-select-search-icon` | Search icon color | `#6b7280` |
337
304
 
338
- #### Toggle
305
+ ### Color — toggle
339
306
 
340
307
  | Token | Description | Light default |
341
308
  |-------|-------------|---------------|
@@ -345,14 +312,15 @@ All three components also rely on the shared `--color-status-success` / `--color
345
312
  | `--color-toggle-track-off-border` | Track border when off | `#d1d5db` |
346
313
  | `--color-toggle-thumb-bg` | Thumb background | `#ffffff` |
347
314
 
348
- #### Checkbox & Radio
315
+ ### Color — checkbox & radio
349
316
 
350
317
  | Token | Description | Light default |
351
318
  |-------|-------------|---------------|
352
319
  | `--color-check-border` | Checkbox/radio border | `#d1d5db` |
353
320
  | `--color-check-ring` | Checkbox/radio focus ring | `rgb(17 24 39 / 0.1)` |
321
+ | `--color-check-checked-bg` | Checkbox/radio fill when checked | `#2563eb` |
354
322
 
355
- #### Dropdown menu
323
+ ### Color — dropdown menu
356
324
 
357
325
  | Token | Description | Light default |
358
326
  |-------|-------------|---------------|
@@ -364,7 +332,7 @@ All three components also rely on the shared `--color-status-success` / `--color
364
332
  | `--color-dropdown-item-ring` | Item focus ring | `rgb(17 24 39 / 0.1)` |
365
333
  | `--color-dropdown-group-label` | Group label text color | `#6b7280` |
366
334
 
367
- #### Tab buttons
335
+ ### Color — tab buttons
368
336
 
369
337
  | Token | Description | Light default |
370
338
  |-------|-------------|---------------|
@@ -375,7 +343,7 @@ All three components also rely on the shared `--color-status-success` / `--color
375
343
  | `--color-tab-active-bg` | Active tab background | `#ffffff` |
376
344
  | `--color-tab-active-border` | Active tab border | `#e5e7eb` |
377
345
 
378
- #### Panel
346
+ ### Color — panel
379
347
 
380
348
  | Token | Description | Light default |
381
349
  |-------|-------------|---------------|
@@ -383,14 +351,14 @@ All three components also rely on the shared `--color-status-success` / `--color
383
351
  | `--color-panel-border` | Panel border | `#e5e7eb` |
384
352
  | `--color-panel-text` | Panel default text color (inherited by `PanelField`) | `#111827` |
385
353
 
386
- #### Modal & Sidebar modal
354
+ ### Color — modal & sidebar modal
387
355
 
388
356
  | Token | Description | Light default |
389
357
  |-------|-------------|---------------|
390
358
  | `--color-modal-overlay` | Backdrop overlay color | `rgb(156 163 175 / 0.3)` |
391
359
  | `--color-modal-bg` | Modal content background | `#ffffff` |
392
360
 
393
- #### Table
361
+ ### Color — table
394
362
 
395
363
  | Token | Description | Light default |
396
364
  |-------|-------------|---------------|
@@ -406,13 +374,13 @@ All three components also rely on the shared `--color-status-success` / `--color
406
374
  | `--color-table-resize-handle-hover` | Resize handle on hover | `#d1d5db` |
407
375
  | `--color-table-resize-handle-active` | Resize handle while dragging | `#2563eb` |
408
376
 
409
- #### Divider
377
+ ### Color — divider
410
378
 
411
379
  | Token | Description | Light default |
412
380
  |-------|-------------|---------------|
413
381
  | `--color-divider` | Divider line color | `#e5e7eb` |
414
382
 
415
- #### Badge
383
+ ### Color — badge
416
384
 
417
385
  | Token | Description | Light default |
418
386
  |-------|-------------|---------------|
@@ -425,7 +393,7 @@ All three components also rely on the shared `--color-status-success` / `--color
425
393
 
426
394
  > Colored badges (red, blue, green, etc.) use Tailwind color utility classes and are not token-based. They adapt to dark mode automatically via Tailwind's `dark:` variants.
427
395
 
428
- #### Status
396
+ ### Color — status
429
397
 
430
398
  Shared color tokens for status/notification indicators. Currently used by `PanelLink`'s `status` prop, but available for any component.
431
399
 
@@ -435,3 +403,139 @@ Shared color tokens for status/notification indicators. Currently used by `Panel
435
403
  | `--color-status-warning` | Warning state | `#f59e0b` |
436
404
  | `--color-status-success` | Success state | `#16a34a` |
437
405
  | `--color-status-info` | Informational state | `#2563eb` |
406
+
407
+ ## Rich text editor (`InputLexical`)
408
+
409
+ `InputLexical` is a [Lexical](https://lexical.dev)-powered rich text editor styled like the rest of the kit. It ships with two toolbar variants that share the exact same controls:
410
+
411
+ - **`static`** (default) — a light toolbar fixed at the top of the editor.
412
+ - **`floating`** — a dark bar that appears above the editor while it is focused, matching the editor width.
413
+
414
+ Lexical and its plugins are **peer dependencies** — install them alongside the library:
415
+
416
+ ```bash
417
+ pnpm add lexical @lexical/react @lexical/rich-text @lexical/list @lexical/link @lexical/selection @lexical/utils
418
+ ```
419
+
420
+ ### Basic usage
421
+
422
+ The value is a **serialized Lexical editor state** (a JSON string). Pass the last `onChange` value back as `value` to restore content.
423
+
424
+ ```tsx
425
+ import { useState } from "react";
426
+ import { InputLexical } from "@matthiaskrijgsman/mat-ui";
427
+
428
+ function Editor() {
429
+ const [value, setValue] = useState<string>();
430
+
431
+ return (
432
+ <InputLexical
433
+ label={"Description"}
434
+ placeholder={"Write something…"}
435
+ toolbar={"floating"} // or "static" (default)
436
+ value={value}
437
+ onChange={setValue}
438
+ autogrow // grow with content…
439
+ minRows={4} // …from a 4-row floor…
440
+ maxRows={12} // …up to 12 rows, then scroll
441
+ />
442
+ );
443
+ }
444
+ ```
445
+
446
+ Sizing mirrors `InputTextArea`: `minRows` sets a height floor, `maxRows` caps the height (content beyond it scrolls), and `autogrow` lets the editor grow with its content between the two. Without `autogrow` the editor is fixed at `minRows`.
447
+
448
+ ### Extending the toolbar
449
+
450
+ The toolbar is assembled from exported **building blocks**, so you can reorder, drop, or add controls via the `renderToolbar` slot. It receives `{ editor, state, tone }` and renders into whichever variant is active — the same render function drives both the static and floating bars.
451
+
452
+ ```tsx
453
+ import {
454
+ InputLexical,
455
+ LexicalBlockTypeSelect,
456
+ LexicalFormatButtons,
457
+ LexicalListButtons,
458
+ LexicalLinkButton,
459
+ LexicalHistoryButtons,
460
+ LexicalToolbarDivider,
461
+ } from "@matthiaskrijgsman/mat-ui";
462
+
463
+ <InputLexical
464
+ renderToolbar={() => (
465
+ <>
466
+ <LexicalFormatButtons/>
467
+ <LexicalToolbarDivider/>
468
+ <LexicalListButtons/>
469
+ <LexicalLinkButton/>
470
+ <LexicalToolbarDivider/>
471
+ <LexicalHistoryButtons/>
472
+ </>
473
+ )}
474
+ />;
475
+ ```
476
+
477
+ Building blocks read the active editor and formatting `state`/`tone` from context, so they work in either variant with no extra wiring. Dividers automatically flip orientation (and the whole toolbar collapses overflowing controls into a vertical `⋮` dropdown) when space runs out — return each control as a top-level child so it stays individually measurable.
478
+
479
+ #### Custom controls
480
+
481
+ Build your own control with `LexicalToolbarButton` plus Lexical's editor context. `useLexicalToolbar()` exposes the current `{ state, tone }`:
482
+
483
+ ```tsx
484
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
485
+ import { FORMAT_TEXT_COMMAND } from "lexical";
486
+ import { IconStrikethrough } from "@tabler/icons-react";
487
+ import { LexicalToolbarButton, useLexicalToolbar } from "@matthiaskrijgsman/mat-ui";
488
+
489
+ const StrikethroughButton = () => {
490
+ const [editor] = useLexicalComposerContext();
491
+ const { tone } = useLexicalToolbar();
492
+ return (
493
+ <LexicalToolbarButton
494
+ Icon={IconStrikethrough}
495
+ tone={tone}
496
+ aria-label={"Strikethrough"}
497
+ onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")}
498
+ />
499
+ );
500
+ };
501
+ ```
502
+
503
+ #### Registering extra Lexical nodes
504
+
505
+ The built-in set covers headings, lists, links, and quotes. For anything else (tables, mentions, code blocks, …) install the node's package yourself and pass the node via the `nodes` prop — it is registered alongside the built-in set:
506
+
507
+ ```bash
508
+ pnpm add @lexical/code
509
+ ```
510
+
511
+ ```tsx
512
+ import { CodeNode } from "@lexical/code";
513
+
514
+ <InputLexical nodes={[CodeNode]} renderToolbar={/* … */} />;
515
+ ```
516
+
517
+ > mat-ui does not bundle `lexical` or any `@lexical/*` package — they are peer dependencies, so a single shared copy is used. Only register a given node type once: don't pass a node that is already in the built-in set, or Lexical throws a duplicate-type error.
518
+
519
+ #### Adding plugins (the `children` slot)
520
+
521
+ A Lexical feature is usually a **node *plus* a plugin**. Register the node with `nodes`, then mount the plugin(s) as **children** — they run inside the editor alongside the built-ins (history, lists, links). Any `@lexical/react` plugin or your own works:
522
+
523
+ ```tsx
524
+ import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
525
+ import { CodeHighlightPlugin } from "@lexical/react/LexicalCodeHighlightPlugin";
526
+
527
+ <InputLexical nodes={[CodeNode]}>
528
+ <CodeHighlightPlugin />
529
+ <TabIndentationPlugin />
530
+ {/* …or your own plugin using useLexicalComposerContext() */}
531
+ </InputLexical>;
532
+ ```
533
+
534
+ Style custom nodes by merging theme classes over the defaults with the `theme` prop:
535
+
536
+ ```tsx
537
+ <InputLexical nodes={[CodeNode]} theme={{ code: "my-code-block" }}>
538
+ <CodeHighlightPlugin />
539
+ </InputLexical>;
540
+ ```
541
+
package/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ export { DropdownButton } from "./components/dropdown-menu/DropdownButton.tsx";
60
60
  export { DropdownButtonGroup } from "./components/dropdown-menu/DropdownButtonGroup.tsx";
61
61
  export { DropdownPanel } from "./components/dropdown-menu/DropdownPanel.tsx";
62
62
  export { DropdownMenu } from "./components/dropdown-menu/DropdownMenu.tsx";
63
+ export { useDropdownDismiss, DropdownDismissContext } from "./components/dropdown-menu/use-dropdown-dismiss.ts";
63
64
  export { usePopover } from "./popover/use-popover.tsx";
64
65
  export { useSelectPopover } from "./popover/use-select-popover.tsx";
65
66
  export { PopoverBase } from "./popover/PopoverBase.tsx";