torch-glare 2.5.5 → 2.5.6

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.
Files changed (40) hide show
  1. package/apps/lib/components/BadgeField.tsx +68 -3
  2. package/apps/lib/components/Button.tsx +10 -2
  3. package/apps/lib/components/DataViews/data-views.tsx +17 -5
  4. package/apps/lib/components/DataViews/index.ts +8 -4
  5. package/apps/lib/components/DataViews/slots.ts +9 -0
  6. package/apps/lib/components/DataViews/states.tsx +43 -8
  7. package/apps/lib/components/Drawer.tsx +70 -39
  8. package/apps/lib/components/DropdownMenu.tsx +14 -0
  9. package/apps/lib/components/FormBuilder/context.ts +12 -0
  10. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +19 -14
  11. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +31 -8
  12. package/apps/lib/components/FormBuilder/submit.tsx +21 -1
  13. package/apps/lib/components/FormBuilder/types.ts +5 -0
  14. package/apps/lib/components/FormRenderer/FormDrawer.tsx +139 -17
  15. package/apps/lib/components/FormRenderer/detail.tsx +57 -8
  16. package/apps/lib/components/FormRenderer/form-renderer.tsx +66 -5
  17. package/apps/lib/components/FormRenderer/index.ts +2 -0
  18. package/apps/lib/components/FormRenderer/notch-action.tsx +64 -0
  19. package/apps/lib/components/FormRenderer/stepper.tsx +56 -2
  20. package/apps/lib/components/FormRenderer/types.ts +37 -0
  21. package/apps/lib/components/SectionBlock.tsx +24 -3
  22. package/apps/lib/components/Select.tsx +9 -9
  23. package/apps/lib/components/SlideDatePicker.tsx +2 -0
  24. package/apps/lib/components/Table.tsx +15 -28
  25. package/apps/lib/hooks/useActiveTreeItem.ts +4 -1
  26. package/apps/lib/hooks/useHtmlDir.ts +31 -0
  27. package/apps/lib/hooks/useTagSelection.ts +95 -9
  28. package/apps/lib/registry.json +20 -4
  29. package/apps/lib/utils/scroller.ts +26 -0
  30. package/docs/components/badge-field.md +26 -0
  31. package/docs/components/data-views/index.md +31 -5
  32. package/docs/components/data-views/migration.md +7 -5
  33. package/docs/components/drawer.md +5 -5
  34. package/docs/components/form-builder.md +9 -1
  35. package/docs/components/form-renderer.md +71 -1
  36. package/docs/components/section-block.md +6 -0
  37. package/docs/migration/changelog.md +6 -0
  38. package/docs/reference/hooks.md +23 -0
  39. package/docs/reference/utilities.md +22 -0
  40. package/package.json +1 -1
@@ -39,7 +39,7 @@ import {
39
39
  useDataViewsFilters, useDataViewsPanel, useDataViewsPanelTabs,
40
40
  useActiveRow, // the row behind `activeId`
41
41
  Cell, // paint one field the way the views paint it
42
- markView, markHeader, markPanel, // register a part of your own
42
+ markView, markHeader, markPanel, markEmpty, // register a part of your own
43
43
  SkeletonBar, skeletonKeys, // the loading pieces every view is built from
44
44
  getByPath, formatPathLabel, defaultGetRowId, // read a value by dotted path
45
45
  buildCardRows, resolveBadgeVariant,
@@ -135,10 +135,35 @@ see the change, "new filter" and "new page" have already become one object.
135
135
 
136
136
  ## Empty and loading
137
137
 
138
- Neither is a part you render. When there is nothing to show, the view shows **nothing** — the table
139
- keeps its header band and has no rows; the board keeps its columns and has no cards. While
140
- `loading` is set, each view paints a **skeleton in its own shape**: the table shimmers rows at the
141
- real row height and column widths, the board shimmers cards inside its columns.
138
+ **Loading is not a part you render.** While `loading` is set, each view paints a **skeleton in its
139
+ own shape**: the table shimmers rows at the real row height and column widths, the board shimmers
140
+ cards inside its columns.
141
+
142
+ **Empty is opt-in.** By default the view simply shows nothing — the table keeps its header band and
143
+ has no rows, the board keeps its columns and has no cards. That default is deliberate: a centred
144
+ message in place of the view throws away the chrome, and it cannot tell "no results" from "not
145
+ fetched yet".
146
+
147
+ When you do want something there, render `DataViews.Empty` anywhere among the children. It is a
148
+ passthrough with a marker — it holds no opinion about what an empty state looks like, it only tells
149
+ the root to put its content where the view goes. The root swaps it in when the query has settled and
150
+ returned nothing (`!loading && rows.length === 0`):
151
+
152
+ ```tsx
153
+ <DataViews rows={rows} fields={fields} loading={loading}>
154
+ <DataViews.Table />
155
+ <DataViews.Empty>
156
+ <div className="flex flex-1 flex-col items-center justify-center gap-2">
157
+ <p>No invoices match these filters.</p>
158
+ <Button onClick={clearFilters}>Clear filters</Button>
159
+ </div>
160
+ </DataViews.Empty>
161
+ </DataViews>
162
+ ```
163
+
164
+ Your content is what receives the body slot's height, so give it `flex-1` if it should centre.
165
+ Wrapping `DataViews.Empty` in a component of your own? Mark that wrapper with `markEmpty` so the
166
+ root still recognises it, the same way `markView` / `markHeader` / `markPanel` work.
142
167
 
143
168
  ## Large datasets
144
169
 
@@ -183,6 +208,7 @@ and that is not built.
183
208
  | `DataViews.Board` | a kanban board | `groups` — it never groups rows itself | [`views`](./examples/views.md) |
184
209
  | `DataViews.Inbox` | a master list beside a detail pane | the pane, as `children` | [`inbox-routing`](./examples/inbox-routing.md) |
185
210
  | `DataViews.Tree` | a hierarchy, optionally beside a pane | `nodes` — it never builds one | [`tree-custom`](./examples/tree-custom.md) |
211
+ | `DataViews.Empty` | your content, in place of the view, once a settled query returns no rows | the content — it holds no opinion about what empty looks like | — |
186
212
 
187
213
  Each takes `id`, `label` and `icon` to control how it appears in the switcher, so the same view can
188
214
  be registered twice with different data. Full props are under
@@ -56,11 +56,13 @@ Three parts of `DataViews` itself went in the same release.
56
56
  `rows`; whether there is more is derived from `rows.length < total`, so there is no `hasMore` prop.
57
57
  See the *Large datasets* section of the [DataViews doc](./index.md).
58
58
 
59
- **`DataViews.Empty`** — when there is nothing to show, the view shows nothing: the table keeps its
60
- header band and has no rows, the board keeps its columns and has no cards. A centred message in
61
- place of the view threw away the chrome, and it could not tell "no results" apart from "not fetched
62
- yet" — so the first load of every page announced that nothing matched before anything had been
63
- asked for.
59
+ **`DataViews.Empty`** — *removed, then re-added as opt-in.* It was dropped because rendering a
60
+ centred message in place of the view threw away the chrome, and it could not tell "no results" apart
61
+ from "not fetched yet" — so the first load of every page announced that nothing matched before
62
+ anything had been asked for. It is back on different terms: the default is still to show nothing,
63
+ and `DataViews.Empty` now renders **only** once the query has settled with no rows
64
+ (`!loading && rows.length === 0`). Nothing to migrate — omit it and behaviour is unchanged. See the
65
+ *Empty and loading* section of the [DataViews doc](./index.md).
64
66
 
65
67
  **`DataViews.Loading`** — each view now paints its own skeleton, in its own shape, driven by the
66
68
  `loading` prop. A custom view registered with `markView` gets the same thing: read `loading` from
@@ -433,7 +433,7 @@ There is **no automatic per-direction styling**. The anchor is set on the root w
433
433
  | Drag handle | on — `<DrawerPanel showHandle>` | off (the default) | off (the default) |
434
434
  | Frame / tray | usually off (`framed={false}`) for clean sheet | on (default) for the dark tray | on (default) |
435
435
  | Rounded corners | top corners only | all/left corners | top-left + bottom corners |
436
- | Notch side | top-left (`notchSide="left"`) | top-left | mirror to `notchSide="right"` |
436
+ | Notch side | inline-start (`notchSide="start"`) | inline-start | mirror to `notchSide="end"` |
437
437
  | Best for | mobile sheets, action sheets, comments | create/edit forms, filters, detail panels | RTL panels, side navigation |
438
438
 
439
439
  ### Bottom (default)
@@ -509,7 +509,7 @@ A floating panel anchored to the right edge — the canonical home for create/ed
509
509
 
510
510
  ### Left (RTL / navigation)
511
511
 
512
- Mirror of the right recipe: `direction="left"`, anchor to the left edge, and if you use a notch, set `notchSide="right"` so the tab mirrors correctly.
512
+ Mirror of the right recipe: `direction="left"`, anchor to the left edge, and if you use a notch, set `notchSide="end"` so the tab sits on the panel's trailing edge.
513
513
 
514
514
  ```tsx
515
515
  <Drawer direction="left">
@@ -517,7 +517,7 @@ Mirror of the right recipe: `direction="left"`, anchor to the left edge, and if
517
517
  <Button variant="PrimeStyle">Open left drawer</Button>
518
518
  </DrawerTrigger>
519
519
  <DrawerContent
520
- notchSide="right"
520
+ notchSide="end"
521
521
  wrapperClassName="top-2 left-2 bottom-2 right-auto mt-0 h-auto w-[420px] max-w-[calc(100vw-16px)]"
522
522
  notch={
523
523
  <DrawerNotch>
@@ -589,7 +589,7 @@ brings its own background. This is where direction-specific styling is applied.
589
589
  |---|---|---|---|
590
590
  | `framed` | `boolean` | `true` | Show the dark "tray" frame (border + inset shadow) around the panel. Set `false` for clean bottom sheets. |
591
591
  | `notch` | `ReactNode` | — | A `DrawerNotch` tab rendered on the top edge. |
592
- | `notchSide` | `"left" \| "right"` | `"left"` | Which side the notch attaches to (and which corner stays square). Use `"right"` for left-anchored drawers. |
592
+ | `notchSide` | `"start" \| "end"` | `"start"` | Which **inline** edge the notch attaches to (and which corner stays square). Logical, so it mirrors under `dir="rtl"` without you computing a direction. Use `"end"` for left-anchored drawers. |
593
593
  | `wrapperClassName` | `string` | — | Classes on the outer positioned element — this is how you anchor/size the panel per direction. |
594
594
  | `className` | `string` | — | Classes on the **dark tray**. Add a `gap-*` here when the tray holds more than one child. |
595
595
  | `trayClassName` | `string` | — | **Deprecated** — an alias for `className` (merged last, so it still wins). |
@@ -629,7 +629,7 @@ with each bringing its own background.
629
629
  | `DrawerDescription` | Muted supporting text (maps to Vaul `Drawer.Description`). |
630
630
  | `DrawerBadge` | Small uppercase status pill. `color`: `Blue \| Green \| Red \| Yellow \| Purple \| Gray` (default `Blue`). |
631
631
  | `DrawerFooter` | Bottom action area (`mt-auto`, stacked). |
632
- | `DrawerNotch` | The top-edge tab container. `side`: `"left" \| "right"`. |
632
+ | `DrawerNotch` | The top-edge tab container. `side`: `"start" \| "end"` (default `"start"`); normally cloned in by `DrawerContent` from `notchSide`. |
633
633
  | `DrawerNotchClose` | Round close button for inside a notch. |
634
634
  | `DrawerNotchPill` | Pill button for inside a notch. `color`: `Yellow \| Blue \| Gray` (default `Yellow`). Styled `<button>` only — wire navigation yourself via `onClick` (see ["What 'Open in new tab' does"](#what-open-in-new-tab-does)). |
635
635
  | `DrawerNotchDivider` | Thin vertical divider between notch items. |
@@ -136,6 +136,7 @@ that.
136
136
  | `FormBuilder.Select` (`options`) | `Select` | `string` |
137
137
  | `FormBuilder.SearchableSelect` (`options`, async: `onSearchChange`/`onLoadMore`/`hasMore`) | `SearchableSelect` | `string` |
138
138
  | `FormBuilder.MultiSelect` / `.Tags` (`options`) | `BadgeField` | `string[]` |
139
+ | `FormBuilder.MultiSelect` / `.Tags` (`creatable`) | `BadgeField` with free text | `string[]` — values not in `options` survive |
139
140
  | `FormBuilder.RadioList` (`options`, each with optional `description`) | boxed radio list | `string` |
140
141
  | `FormBuilder.CheckboxGroup` (`options`, each with optional `description`) | boxed checkbox list | `string[]` |
141
142
  | `FormBuilder.RadioCards` (`options` with `description`) | `RadioCard` | `string` |
@@ -214,7 +215,10 @@ country code.
214
215
  `FormBuilder.RadioList` (single-select, `string`) and `FormBuilder.CheckboxGroup` (multi-select,
215
216
  `string[]`) render their `options` as a boxed, divided list — control on the left, primary
216
217
  label, and an optional per-option `description` shown as a secondary label. The whole row is
217
- clickable. Multi-select is also available as `.MultiSelect` / `.Tags` (a tag-chip picker).
218
+ clickable. Multi-select is also available as `.MultiSelect` / `.Tags` (a tag-chip picker). Add
219
+ `creatable` to either and the user can type a value that is not in `options` and commit it with
220
+ Enter or comma; pass `options={[]}` for a pure free-text list. (`createLabel` is a `BadgeField`
221
+ prop — it is not forwarded, so the create row keeps its default label inside a form.)
218
222
 
219
223
  `FormBuilder.SwitchBox` (value `boolean`) is a switch wrapped in a `#f9f9f9` field box. It
220
224
  renders like any other field — the `label` sits in the normal label column — and the box holds
@@ -227,6 +231,10 @@ presentation, so it lives there.
227
231
  form** (via context), so it submits even when placed in a header / action bar that renders
228
232
  _outside_ the `<form>` — no manual `form={id}` wiring.
229
233
 
234
+ It also takes `disabled`, for permission gating: someone with read access should still *see* a
235
+ record, so a Save they may not use is disabled rather than removed — a missing button looks broken,
236
+ a disabled one says "not yours to change". The server refuses the write either way.
237
+
230
238
  ## Moved to FormRenderer
231
239
 
232
240
  These all used to live here. They are chrome, so they now live on
@@ -58,6 +58,7 @@ It renders in the form's header action pill (page) or the drawer header (drawer)
58
58
  | `FormRenderer.Back` / `.Next` | Chevron step controls. The header's action bar prepends them for you. |
59
59
  | `FormRenderer.Sidebar` / `.Tab` | The display-only [detail-tabs](#detail-tabs-sidebar) view. |
60
60
  | `FormRenderer.Grid` / `.Row` | Read-only display cells inside a detail tab. |
61
+ | `FormRenderer.NotchAction` | A pill in the drawer's notch. You own the label, so it translates. |
61
62
 
62
63
  ### `FormRenderer.Section`
63
64
 
@@ -109,6 +110,10 @@ import { FormBuilder } from "@/components/FormBuilder";
109
110
  | `actions` | `ReactNode` | The form's action bar — rendered in the header action pill (page) or drawer header (drawer). Put the Save here: `actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}`. A bare `FormBuilder.Submit` auto-targets this form. |
110
111
  | `id` | `string` | `id` on the underlying `<form>`. Optional — FormRenderer generates and wires one otherwise. |
111
112
  | `open` / `onOpenChange` / `title` / `badge` / `onOpenInNewTab` | — | Drawer control (when `display="drawer"`). `title` / `badge` are strings that override `header.title` / `header.label`. |
113
+ | `embedded` | `boolean` | Render without the rounded body card, for a host that already draws one. Defaults to `true` in a drawer. |
114
+ | `activeTab` / `onTabChange` | `string` / `(tab) => void` | Make the [detail-tabs](#detail-tabs-sidebar) rail controlled, so the tab can live in the URL (`?tab=audit`) and survive a reload. Omit both for the uncontrolled default. Inert in form mode. |
115
+ | `activeStep` / `onStepChange` | `number` / `(index) => void` | External control of a `FormRenderer.Stepper`'s step, for a wizard owned by something other than form validity. In controlled mode internal advancement is suppressed and a click is *reported*, not applied. Omit both and the stepper behaves exactly as before. |
116
+ | `drawer` | `{ side?; nested?; framed?; hideHeader?; bareBody?; description?; wrapperClassName?; className? }` | Drawer layout, forwarded to `FormDrawer` — see [Drawer layout](#drawer-layout). One object rather than eight flat props, since none of it means anything on a page. |
112
117
 
113
118
  ## Drawer
114
119
 
@@ -129,6 +134,60 @@ import { FormBuilder } from "@/components/FormBuilder";
129
134
  </FormRenderer>
130
135
  ```
131
136
 
137
+ ### Drawer layout
138
+
139
+ Everything about how the drawer is *shaped* goes in one `drawer` object, because none of it means
140
+ anything on a page:
141
+
142
+ | Key | Type | Default | What it does |
143
+ | ----------------- | ----------------------------- | --------------- | -------------------------------------------------------------------------------------------------- |
144
+ | `side` | `'inline-end' \| 'bottom'` | `'inline-end'` | Which edge it slides from. `'bottom'` is a sheet and drops the notch — there is no inline edge to hang it from. The default follows document direction. |
145
+ | `nested` | `boolean` | `false` | **Required** when this drawer opens inside another one, or the two roots fight over the overlay and the scroll lock. Throws without a parent Drawer. |
146
+ | `framed` | `boolean` | `true` | The dark tray frame and the panel's border / inset shadow. |
147
+ | `hideHeader` | `boolean` | `false` | Skip the header bar, for a child that draws its own. Also drops the body's top padding. |
148
+ | `bareBody` | `boolean` | `false` | Skip the padded scroll wrapper, for a child that already scrolls and offsets for its own header. |
149
+ | `description` | `string` | — | Screen-reader-only description. vaul warns when a drawer has none. |
150
+ | `wrapperClassName`| `string` | per `side` | Lands on the positioner — width, height, insets. Replaces the default sizing. |
151
+ | `className` | `string` | — | Lands on the tray. |
152
+
153
+ ```tsx
154
+ // A bottom sheet, opened from inside another drawer.
155
+ <FormRenderer
156
+ display="drawer"
157
+ open={open}
158
+ onOpenChange={setOpen}
159
+ title="Quick add"
160
+ drawer={{ side: "bottom", nested: true, description: "Add a line item" }}
161
+ onSubmit={save}
162
+ actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}
163
+ >
164
+ {fields}
165
+ </FormRenderer>
166
+ ```
167
+
168
+ ### `FormRenderer.NotchAction`
169
+
170
+ Buttons in the drawer's notch, authored by you. `onOpenInNewTab` still works, but it hardcodes an
171
+ English label and allows only one action — write `NotchAction` children instead and you own both.
172
+ They render nothing where you write them; the renderer lifts them into the notch.
173
+
174
+ ```tsx
175
+ <FormRenderer display="drawer" open={open} onOpenChange={setOpen} title={t("invoice")}>
176
+ <FormRenderer.NotchAction onClick={openFullPage}>
177
+ {t("openInNewTab")}
178
+ <i className="ri-arrow-right-up-line text-[12px]" />
179
+ </FormRenderer.NotchAction>
180
+ <FormRenderer.NotchAction color="Blue" onClick={print}>
181
+ {t("print")}
182
+ </FormRenderer.NotchAction>
183
+
184
+ {fields}
185
+ </FormRenderer>
186
+ ```
187
+
188
+ `color` matches `DrawerNotchPill` and defaults to `"Yellow"`. Ignored on a page-display form, and a
189
+ `side: "bottom"` sheet has no notch to put them in.
190
+
132
191
  ## Stepper
133
192
 
134
193
  Drop a `FormRenderer.Stepper` in as the child. The Save lives in the header `actions` and
@@ -140,6 +199,10 @@ validates the current step, then advances (disabled on the last step). A step th
140
199
  validation **stays checked** in the rail — even after you navigate back — while a live validation
141
200
  error overrides it to red. You still pass just the Submit; the nav is wired for you:
142
201
 
202
+ The rail **stays put while the fields scroll**. It is navigation, so it pins below the header rather
203
+ than scrolling out of view with the form; a rail taller than the form body scrolls to its end first,
204
+ as any sticky element does.
205
+
143
206
  ```tsx
144
207
  <FormRenderer
145
208
  onSubmit={save}
@@ -266,7 +329,14 @@ outside the `<form>`, wire the Save button to the form via `id` / `form={id}`:
266
329
  ```
267
330
 
268
331
  `FormDrawer` props: `open`, `onOpenChange`, `title`, `badge`, `variant`, `actions`,
269
- `onOpenInNewTab`, `children`, `summary`. It owns no form state.
332
+ `onOpenInNewTab`, `notchActions`, `children`, `summary`, plus the layout flags listed under
333
+ [Drawer layout](#drawer-layout) — `side`, `nested`, `framed`, `hideHeader`, `bareBody`,
334
+ `description`, `wrapperClassName`, `className`. It owns no form state.
335
+
336
+ `notchActions` is the notch's button slot. Going through `FormRenderer` you rarely set it directly —
337
+ write `FormRenderer.NotchAction` children and they are lifted into it for you. Using `FormDrawer` on
338
+ its own, pass the buttons here. When both `notchActions` and `onOpenInNewTab` are given, the former
339
+ wins; `onOpenInNewTab` remains only for callers happy with its built-in English label.
270
340
 
271
341
  ### The title
272
342
 
@@ -380,6 +380,12 @@ The layout has three stacked parts, and the order matters. Only the **scroller**
380
380
  horizontally; the header actions above it and the end-action below it stay put, which is
381
381
  what keeps `Add New` reachable on a wide table.
382
382
 
383
+ The section body is itself the horizontal scrollport, so a table wider than the card scrolls
384
+ **inside** it rather than being clipped or widening the page. One consequence worth knowing: a
385
+ scrollport is the containing block for `position: sticky`, so a `Table`'s sticky header inside a
386
+ section now resolves against a box that never scrolls vertically — i.e. it stops sticking. If you
387
+ need a sticky header, give the table its own vertical scroller.
388
+
383
389
  ```tsx
384
390
  import { SectionBlock } from "@/components/SectionBlock";
385
391
  import { Button } from "@/components/Button";
@@ -28,6 +28,12 @@ npx torch-glare@latest add Button
28
28
 
29
29
  - **v2.5.5** — **breaking**: `DataViews.Filters.Summary` is removed with no shim; delete any
30
30
  `<DataViews.Filters.Summary />` (render your own from `useDataViewsFilters()` if you want one).
31
+ `DrawerContent.notchSide` / `DrawerNotch.side` become logical `"start" | "end"` (was
32
+ `"left" | "right"`), and `TreeFolder`'s drag wiring is internal — `dragHandlers`,
33
+ `TreeFolderRowDragHandlers`, `UseTreeFolderDnDResult`, `scrollContainerRef` and
34
+ `getRowDragHandlers` are gone. New, non-breaking: `BadgeField` `creatable` / `createLabel`,
35
+ `FormRenderer` `embedded` / controlled tabs and steps / `drawer` layout options /
36
+ `FormRenderer.NotchAction`, `FormBuilder.Submit` `disabled`, and `DataViews.Empty`.
31
37
  Also, every `FormBuilder.*` field takes a `hints` array, so one field can
32
38
  carry several alerts. The validation error renders first, your hints follow. See
33
39
  [FormBuilder](../components/form-builder.md#hints). Dropdown panels (`Select`,
@@ -20,6 +20,7 @@ Custom React hooks that provide reusable functionality for common UI patterns. A
20
20
  - **useClickOutside** - Detect clicks outside a referenced element
21
21
  - **useResize** - Handle element resizing with RTL support
22
22
  - **useTagSelection** - Manage tag selection state with keyboard navigation
23
+ - **useHtmlDir** - Track the document's text direction from `<html dir>`
23
24
 
24
25
  ---
25
26
 
@@ -1498,3 +1499,25 @@ All hooks support:
1498
1499
  - Edge 90+
1499
1500
 
1500
1501
  **IntersectionObserver** (useActiveTreeItem): Requires polyfill for older browsers.
1502
+
1503
+ ---
1504
+
1505
+ ## useHtmlDir
1506
+
1507
+ Tracks the document's text direction from `<html dir>`, re-reading it when it changes — a language
1508
+ switch, say. Returns `"ltr" | "rtl"`.
1509
+
1510
+ ```tsx
1511
+ import { useHtmlDir } from "@/hooks/useHtmlDir";
1512
+
1513
+ const dir = useHtmlDir();
1514
+ ```
1515
+
1516
+ Most mirroring should be done in CSS with logical properties, which need no JS at all. Reach for
1517
+ this only where a library wants the direction as a **value**: several Radix primitives default to
1518
+ `"ltr"` when given no `dir` prop and no `DirectionProvider`, and vaul computes an inline transform
1519
+ from its `direction` prop, which a stylesheet cannot override mid-drag.
1520
+
1521
+ | Returns | Notes |
1522
+ | --- | --- |
1523
+ | `"ltr" \| "rtl"` | SSR-safe — returns `"ltr"` when there is no `document`. Watches the attribute with a `MutationObserver`, so a runtime language switch updates every consumer. |
@@ -702,3 +702,25 @@ These utilities depend on the following external libraries:
702
702
  - [Tailwind CSS Documentation](https://tailwindcss.com)
703
703
  - [date-fns Format Reference](https://date-fns.org/docs/format)
704
704
  - [TypeScript Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)
705
+
706
+ ---
707
+
708
+ ## Scroller Utilities
709
+
710
+ ### horizontalScrollerStyles
711
+
712
+ The design's 14px horizontal scroller as a class string: a thin track that thickens and turns blue
713
+ on hover. Shared by `TableScroller` and `SectionBlock`'s body so the two cannot drift.
714
+
715
+ ```tsx
716
+ import { horizontalScrollerStyles } from "@/utils/scroller";
717
+ import { cn } from "@/utils/cn";
718
+
719
+ <div className={cn(horizontalScrollerStyles, "rounded-lg")}>
720
+ <table className="w-max">…</table>
721
+ </div>
722
+ ```
723
+
724
+ It includes `overflow-x-auto overflow-y-hidden`, so the element it lands on becomes the scrollport —
725
+ you do not add an overflow class yourself.
726
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torch-glare",
3
- "version": "2.5.5",
3
+ "version": "2.5.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A copy-in React component library (TypeScript + Radix UI + Tailwind CSS). Its CLI copies component source directly into your project — you own the code.",