@matthiaskrijgsman/mat-ui 0.0.41 → 0.0.43

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,142 @@ 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-tab-count` | `TabButtons` count chip | `--font-weight-strong` |
146
+ | `--font-weight-input-label` | `InputLabel` (label above inputs) | `--font-weight-medium` |
147
+ | `--font-weight-input-description` | `InputDescription` | `--font-weight-medium` |
148
+ | `--font-weight-input-error` | `InputError` | `--font-weight-medium` |
149
+ | `--font-weight-input-option-label` | Inline labels on `InputCheck` / `InputRadio` / `InputToggle`, file tile names | `--font-weight-medium` |
150
+ | `--font-weight-dropdown-item` | `DropdownButton`, `PanelLink` | `--font-weight-strong` |
151
+ | `--font-weight-group-header` | Dropdown group labels, select group headers | `--font-weight-strong` |
152
+ | `--font-weight-panel-field` | `PanelField` label | `--font-weight-medium` |
153
+ | `--font-weight-panel-link` | `PanelLink` | `--font-weight-strong` |
154
+ | `--font-weight-table-header` | `Table` header cells, `TableEmpty` title | `--font-weight-medium` |
155
+ | `--font-weight-table-cell` | `Table` body cells | `--font-weight-normal` |
156
+
157
+ | Token | Description | Default |
158
+ |-------|-------------|---------|
159
+ | `--font-family-base` | Typeface for all kit text (defaults to the host font) | `inherit` |
160
+ | `--font-size-label` | Dropdown / select group label size | `var(--text-sm)` |
161
+ | `--font-size-description` | `InputDescription` and `PanelField` label size | `var(--text-sm)` |
162
+ | `--font-size-error` | `InputError` size | `var(--text-sm)` |
163
+ | `--font-size-tab-count` | `TabButtons` count chip size | `var(--text-xs)` |
164
+
165
+ > The text size of the input/button itself comes from the **control sizing** scale below (`--control-size-{size}-font-size`), not from these tokens.
166
+
167
+ ### Structure — border radius
168
+
169
+ 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.
170
+
171
+ | Token | Applies to | Default |
172
+ |-------|-----------|---------|
173
+ | `--border-radius-input` | Text inputs, selects, textareas, file inputs, the Lexical editor box | `var(--radius-xl)` |
174
+ | `--border-radius-button` | `Button`, `ButtonIconSquare` (and the file-input "Choose" button) | `var(--border-radius-input)` |
175
+ | `--border-radius-panel` | `Panel`, `PanelStack`, `Modal`, `TableEmpty` icon frame | `var(--radius-2xl)` |
176
+ | `--border-radius-dropdown` | `DropdownPanel`, Lexical floating toolbar | `var(--radius-xl)` |
177
+ | `--border-radius-option` | Select option rows | `var(--radius-xl)` |
178
+ | `--border-radius-menu-item` | `DropdownButton`, `PanelLink`, Lexical toolbar buttons | `var(--radius-lg)` |
179
+ | `--border-radius-badge` | `Badge` | `var(--radius-lg)` |
180
+ | `--border-radius-tab` | `TabButtons` container and pills | `var(--radius-xl)` |
181
+ | `--border-radius-checkbox` | `InputCheck` box | `var(--radius-lg)` |
182
+ | `--border-radius-control-inner` | Color swatch and picker bars in `InputColor` | `var(--radius-md)` |
183
+
184
+ > `ButtonIconRound`, the toggle track/thumb, and radio dots are intentionally fully round (`rounded-full`) and are not tokenized.
185
+
186
+ ### Structure — border width & shadow
187
+
188
+ | Token | Applies to | Default |
189
+ |-------|-----------|---------|
190
+ | `--border-width-input` | Border width of inputs, selects, buttons, panels, dropdowns, modals, check/radio | `1px` |
191
+ | `--shadow-control` | Resting elevation of buttons, inputs, panels, tabs | `var(--shadow-sm)` |
192
+ | `--shadow-dropdown` | `DropdownPanel` and the Lexical floating toolbar | `var(--shadow-lg)` |
193
+ | `--shadow-overlay` | `Modal` and `SidebarModal` | `var(--shadow-xl)` |
194
+
195
+ ### Structure — ring & transition
196
+
197
+ 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.
198
+
199
+ | Token | Applies to | Default |
200
+ |-------|-----------|---------|
201
+ | `--control-ring-width` | Ring width on hover, focus, focus-within, and the select/dropzone open state | `4px` |
202
+ | `--control-ring-width-active` | Ring width on press (`:active`) | `1px` |
203
+ | `--control-transition-duration` | Duration of hover/focus/press transitions on buttons, inputs, selects, the icon-button press scale, etc. | `150ms` |
204
+ | `--control-transition-duration-fast` | Quicker color-only transitions (table row/header hover, clickable `Badge`) | `100ms` |
205
+
206
+ > 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.
207
+
208
+ ### Control sizing
246
209
 
247
210
  `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
211
 
@@ -255,7 +218,13 @@ Define your overrides after importing the mat-ui stylesheet:
255
218
  | `--control-size-{size}-icon` | Icon glyph size inside controls | `1rem` · `1.25rem` · `1.5rem` |
256
219
  | `--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
220
 
258
- #### Buttons
221
+ ### Color — focus ring
222
+
223
+ | Token | Description | Light default |
224
+ |-------|-------------|---------------|
225
+ | `--color-input-focus-ring` | Focus ring color for buttons and inputs | `rgb(17 24 39 / 0.15)` |
226
+
227
+ ### Color — buttons
259
228
 
260
229
  Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `tertiary`) uses the same set of tokens. Replace `{variant}` with the variant name.
261
230
 
@@ -273,7 +242,7 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
273
242
  | `--color-button-{variant}-border-disabled` | Border when disabled |
274
243
  | `--color-button-{variant}-text-disabled` | Text color when disabled |
275
244
 
276
- #### Inputs
245
+ ### Color — inputs
277
246
 
278
247
  | Token | Description | Light default |
279
248
  |-------|-------------|---------------|
@@ -286,7 +255,7 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
286
255
  | `--color-input-ring-error` | Ring color in error state | `rgb(220 38 38 / 0.2)` |
287
256
  | `--color-input-icon` | Leading icon color | `rgb(17 24 39 / 0.6)` |
288
257
 
289
- #### Input labels, descriptions & errors
258
+ ### Color — input labels, descriptions & errors
290
259
 
291
260
  | Token | Description | Light default |
292
261
  |-------|-------------|---------------|
@@ -294,16 +263,16 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
294
263
  | `--color-input-description-text` | Description text color | `#6b7280` |
295
264
  | `--color-input-error-text` | Error message text color | `#dc2626` |
296
265
 
297
- #### Input icon buttons
266
+ ### Color — input icon buttons
298
267
 
299
268
  | Token | Description | Light default |
300
269
  |-------|-------------|---------------|
301
270
  | `--color-input-icon-button-ring` | Ring color for icon buttons inside inputs | `#e5e7eb` |
302
271
  | `--color-input-icon-button-icon` | Icon color for icon buttons inside inputs | `#6b7280` |
303
272
 
304
- #### File inputs
273
+ ### Color — file inputs
305
274
 
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:
275
+ `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
276
 
308
277
  | Token | Description | Light default |
309
278
  |-------|-------------|---------------|
@@ -311,11 +280,11 @@ Each button variant (`primary`, `white`, `black`, `transparent`, `secondary`, `t
311
280
 
312
281
  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
282
 
314
- #### Color input
283
+ ### Color — color input
315
284
 
316
285
  `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
286
 
318
- #### Select options
287
+ ### Color — select options
319
288
 
320
289
  | Token | Description | Light default |
321
290
  |-------|-------------|---------------|
@@ -327,7 +296,7 @@ All three components also rely on the shared `--color-status-success` / `--color
327
296
  | `--color-option-text-disabled` | Disabled option text color | `#9ca3af` |
328
297
  | `--color-input-select-placeholder` | Select placeholder text color | `#6b7280` |
329
298
 
330
- #### Select search bar
299
+ ### Color — select search bar
331
300
 
332
301
  | Token | Description | Light default |
333
302
  |-------|-------------|---------------|
@@ -335,7 +304,7 @@ All three components also rely on the shared `--color-status-success` / `--color
335
304
  | `--color-select-search-bg` | Search input background | `rgb(255 255 255 / 0.5)` |
336
305
  | `--color-select-search-icon` | Search icon color | `#6b7280` |
337
306
 
338
- #### Toggle
307
+ ### Color — toggle
339
308
 
340
309
  | Token | Description | Light default |
341
310
  |-------|-------------|---------------|
@@ -345,14 +314,15 @@ All three components also rely on the shared `--color-status-success` / `--color
345
314
  | `--color-toggle-track-off-border` | Track border when off | `#d1d5db` |
346
315
  | `--color-toggle-thumb-bg` | Thumb background | `#ffffff` |
347
316
 
348
- #### Checkbox & Radio
317
+ ### Color — checkbox & radio
349
318
 
350
319
  | Token | Description | Light default |
351
320
  |-------|-------------|---------------|
352
321
  | `--color-check-border` | Checkbox/radio border | `#d1d5db` |
353
322
  | `--color-check-ring` | Checkbox/radio focus ring | `rgb(17 24 39 / 0.1)` |
323
+ | `--color-check-checked-bg` | Checkbox/radio fill when checked | `#2563eb` |
354
324
 
355
- #### Dropdown menu
325
+ ### Color — dropdown menu
356
326
 
357
327
  | Token | Description | Light default |
358
328
  |-------|-------------|---------------|
@@ -364,7 +334,7 @@ All three components also rely on the shared `--color-status-success` / `--color
364
334
  | `--color-dropdown-item-ring` | Item focus ring | `rgb(17 24 39 / 0.1)` |
365
335
  | `--color-dropdown-group-label` | Group label text color | `#6b7280` |
366
336
 
367
- #### Tab buttons
337
+ ### Color — tab buttons
368
338
 
369
339
  | Token | Description | Light default |
370
340
  |-------|-------------|---------------|
@@ -374,8 +344,12 @@ All three components also rely on the shared `--color-status-success` / `--color
374
344
  | `--color-tab-bg-active` | Tab background on press | `rgb(209 213 219 / 0.8)` |
375
345
  | `--color-tab-active-bg` | Active tab background | `#ffffff` |
376
346
  | `--color-tab-active-border` | Active tab border | `#e5e7eb` |
347
+ | `--color-tab-count-bg` | Count chip background (inactive tab) | `#e5e7eb` |
348
+ | `--color-tab-count-text` | Count chip text (inactive tab) | `#374151` |
349
+ | `--color-tab-active-count-bg` | Count chip background (active tab) | `#111827` |
350
+ | `--color-tab-active-count-text` | Count chip text (active tab) | `#ffffff` |
377
351
 
378
- #### Panel
352
+ ### Color — panel
379
353
 
380
354
  | Token | Description | Light default |
381
355
  |-------|-------------|---------------|
@@ -383,14 +357,14 @@ All three components also rely on the shared `--color-status-success` / `--color
383
357
  | `--color-panel-border` | Panel border | `#e5e7eb` |
384
358
  | `--color-panel-text` | Panel default text color (inherited by `PanelField`) | `#111827` |
385
359
 
386
- #### Modal & Sidebar modal
360
+ ### Color — modal & sidebar modal
387
361
 
388
362
  | Token | Description | Light default |
389
363
  |-------|-------------|---------------|
390
364
  | `--color-modal-overlay` | Backdrop overlay color | `rgb(156 163 175 / 0.3)` |
391
365
  | `--color-modal-bg` | Modal content background | `#ffffff` |
392
366
 
393
- #### Table
367
+ ### Color — table
394
368
 
395
369
  | Token | Description | Light default |
396
370
  |-------|-------------|---------------|
@@ -406,13 +380,13 @@ All three components also rely on the shared `--color-status-success` / `--color
406
380
  | `--color-table-resize-handle-hover` | Resize handle on hover | `#d1d5db` |
407
381
  | `--color-table-resize-handle-active` | Resize handle while dragging | `#2563eb` |
408
382
 
409
- #### Divider
383
+ ### Color — divider
410
384
 
411
385
  | Token | Description | Light default |
412
386
  |-------|-------------|---------------|
413
387
  | `--color-divider` | Divider line color | `#e5e7eb` |
414
388
 
415
- #### Badge
389
+ ### Color — badge
416
390
 
417
391
  | Token | Description | Light default |
418
392
  |-------|-------------|---------------|
@@ -425,7 +399,7 @@ All three components also rely on the shared `--color-status-success` / `--color
425
399
 
426
400
  > 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
401
 
428
- #### Status
402
+ ### Color — status
429
403
 
430
404
  Shared color tokens for status/notification indicators. Currently used by `PanelLink`'s `status` prop, but available for any component.
431
405
 
@@ -435,3 +409,139 @@ Shared color tokens for status/notification indicators. Currently used by `Panel
435
409
  | `--color-status-warning` | Warning state | `#f59e0b` |
436
410
  | `--color-status-success` | Success state | `#16a34a` |
437
411
  | `--color-status-info` | Informational state | `#2563eb` |
412
+
413
+ ## Rich text editor (`InputLexical`)
414
+
415
+ `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:
416
+
417
+ - **`static`** (default) — a light toolbar fixed at the top of the editor.
418
+ - **`floating`** — a dark bar that appears above the editor while it is focused, matching the editor width.
419
+
420
+ Lexical and its plugins are **peer dependencies** — install them alongside the library:
421
+
422
+ ```bash
423
+ pnpm add lexical @lexical/react @lexical/rich-text @lexical/list @lexical/link @lexical/selection @lexical/utils
424
+ ```
425
+
426
+ ### Basic usage
427
+
428
+ The value is a **serialized Lexical editor state** (a JSON string). Pass the last `onChange` value back as `value` to restore content.
429
+
430
+ ```tsx
431
+ import { useState } from "react";
432
+ import { InputLexical } from "@matthiaskrijgsman/mat-ui";
433
+
434
+ function Editor() {
435
+ const [value, setValue] = useState<string>();
436
+
437
+ return (
438
+ <InputLexical
439
+ label={"Description"}
440
+ placeholder={"Write something…"}
441
+ toolbar={"floating"} // or "static" (default)
442
+ value={value}
443
+ onChange={setValue}
444
+ autogrow // grow with content…
445
+ minRows={4} // …from a 4-row floor…
446
+ maxRows={12} // …up to 12 rows, then scroll
447
+ />
448
+ );
449
+ }
450
+ ```
451
+
452
+ 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`.
453
+
454
+ ### Extending the toolbar
455
+
456
+ 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.
457
+
458
+ ```tsx
459
+ import {
460
+ InputLexical,
461
+ LexicalBlockTypeSelect,
462
+ LexicalFormatButtons,
463
+ LexicalListButtons,
464
+ LexicalLinkButton,
465
+ LexicalHistoryButtons,
466
+ LexicalToolbarDivider,
467
+ } from "@matthiaskrijgsman/mat-ui";
468
+
469
+ <InputLexical
470
+ renderToolbar={() => (
471
+ <>
472
+ <LexicalFormatButtons/>
473
+ <LexicalToolbarDivider/>
474
+ <LexicalListButtons/>
475
+ <LexicalLinkButton/>
476
+ <LexicalToolbarDivider/>
477
+ <LexicalHistoryButtons/>
478
+ </>
479
+ )}
480
+ />;
481
+ ```
482
+
483
+ 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.
484
+
485
+ #### Custom controls
486
+
487
+ Build your own control with `LexicalToolbarButton` plus Lexical's editor context. `useLexicalToolbar()` exposes the current `{ state, tone }`:
488
+
489
+ ```tsx
490
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
491
+ import { FORMAT_TEXT_COMMAND } from "lexical";
492
+ import { IconStrikethrough } from "@tabler/icons-react";
493
+ import { LexicalToolbarButton, useLexicalToolbar } from "@matthiaskrijgsman/mat-ui";
494
+
495
+ const StrikethroughButton = () => {
496
+ const [editor] = useLexicalComposerContext();
497
+ const { tone } = useLexicalToolbar();
498
+ return (
499
+ <LexicalToolbarButton
500
+ Icon={IconStrikethrough}
501
+ tone={tone}
502
+ aria-label={"Strikethrough"}
503
+ onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")}
504
+ />
505
+ );
506
+ };
507
+ ```
508
+
509
+ #### Registering extra Lexical nodes
510
+
511
+ 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:
512
+
513
+ ```bash
514
+ pnpm add @lexical/code
515
+ ```
516
+
517
+ ```tsx
518
+ import { CodeNode } from "@lexical/code";
519
+
520
+ <InputLexical nodes={[CodeNode]} renderToolbar={/* … */} />;
521
+ ```
522
+
523
+ > 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.
524
+
525
+ #### Adding plugins (the `children` slot)
526
+
527
+ 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:
528
+
529
+ ```tsx
530
+ import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
531
+ import { CodeHighlightPlugin } from "@lexical/react/LexicalCodeHighlightPlugin";
532
+
533
+ <InputLexical nodes={[CodeNode]}>
534
+ <CodeHighlightPlugin />
535
+ <TabIndentationPlugin />
536
+ {/* …or your own plugin using useLexicalComposerContext() */}
537
+ </InputLexical>;
538
+ ```
539
+
540
+ Style custom nodes by merging theme classes over the defaults with the `theme` prop:
541
+
542
+ ```tsx
543
+ <InputLexical nodes={[CodeNode]} theme={{ code: "my-code-block" }}>
544
+ <CodeHighlightPlugin />
545
+ </InputLexical>;
546
+ ```
547
+
@@ -1,14 +1,17 @@
1
1
  import * as React from "react";
2
2
  import type { TablerIcon } from "@tabler/icons-react";
3
+ export type Size = 'sm' | 'md' | 'lg';
3
4
  export type TabButton = {
4
5
  label: string | React.ReactNode;
5
6
  active?: boolean;
6
7
  onClick?: () => void;
7
8
  href?: string;
8
9
  Icon?: TablerIcon;
10
+ count?: React.ReactNode;
9
11
  };
10
12
  export type TabButtonsProps = {
11
13
  className?: string;
12
14
  tabs: TabButton[];
15
+ size?: Size;
13
16
  };
14
17
  export declare const TabButtons: (props: TabButtonsProps) => import("react/jsx-runtime").JSX.Element;