@bzync/rui 0.0.8 → 0.0.9

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
@@ -1,6 +1,74 @@
1
- # rui
1
+ # @bzync/rui
2
+
3
+ Composable React UI components with scoped, fully customizable light and dark themes.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@bzync/rui.svg)](https://www.npmjs.com/package/@bzync/rui)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@bzync/rui.svg)](https://www.npmjs.com/package/@bzync/rui)
7
+ [![gzip size](https://img.shields.io/bundlephobia/minzip/@bzync/rui.svg)](https://bundlephobia.com/package/@bzync/rui)
8
+ [![types included](https://img.shields.io/npm/types/@bzync/rui.svg)](https://www.npmjs.com/package/@bzync/rui)
9
+ [![license](https://img.shields.io/npm/l/@bzync/rui.svg)](./LICENSE)
10
+
11
+ `@bzync/rui` (**r**eact **ui**) is the component library behind [Bzync](https://www.bzync.com).
12
+ It ships 79 components plus a small SVG chart set, a token-driven theming system, and
13
+ production-grade lifecycle primitives — with **zero runtime dependencies**.
14
+
15
+ - **Zero runtime dependencies.** `dependencies` is empty. `react`, `react-dom`, and
16
+ `framer-motion` are peer dependencies you already control.
17
+ - **Scoped theming.** `ThemeProvider` renders a `.rui-theme` scope, so you can run
18
+ multiple themes on one page or theme a subtree without touching `<html>`.
19
+ - **Token-driven.** Override accent and neutral scales, radii, fonts, spacing, shadows,
20
+ or any CSS variable through a single `palette` prop — per theme.
21
+ - **Light and dark, controlled or uncontrolled.** System-preference tracking and
22
+ persistent selection are built in.
23
+ - **Accessible by default.** Native semantics, keyboard behavior, focus management,
24
+ focus trapping, scroll-lock restoration, and `prefers-reduced-motion` support.
25
+ - **ESM and CJS.** Dual builds, per-component subpath exports, `sideEffects` metadata
26
+ for tree-shaking, and readable (unminified) published output.
27
+ - **Typed.** Written in TypeScript; declaration files ship with the package.
28
+ - **RSC-friendly.** Client components are marked `"use client"`; server components can
29
+ import and render them directly.
30
+ - **React 18.2 and React 19.**
31
+
32
+ ## Table of contents
33
+
34
+ - [Installation](#installation)
35
+ - [Quick start](#quick-start)
36
+ - [The stylesheet](#the-stylesheet)
37
+ - [Theming](#theming)
38
+ - [Importing components](#importing-components)
39
+ - [Server components and SSR](#server-components-and-ssr)
40
+ - [Accessibility and motion](#accessibility-and-motion)
41
+ - [Component catalog](#component-catalog)
42
+ - [Common recipes](#common-recipes)
43
+ - [Charts](#charts)
44
+ - [Hooks and utilities](#hooks-and-utilities)
45
+ - [TypeScript](#typescript)
46
+ - [Browser and React support](#browser-and-react-support)
47
+ - [Versioning](#versioning)
48
+ - [Documentation](#documentation)
49
+ - [Contributing](#contributing)
50
+ - [Security](#security)
51
+ - [License](#license)
2
52
 
3
- Composable React UI components with scoped, customizable light and dark themes.
53
+ ## Installation
54
+
55
+ ```sh
56
+ npm install @bzync/rui framer-motion
57
+ ```
58
+
59
+ ```sh
60
+ pnpm add @bzync/rui framer-motion
61
+ ```
62
+
63
+ ```sh
64
+ yarn add @bzync/rui framer-motion
65
+ ```
66
+
67
+ `react` and `react-dom` (`^18.2.0 || ^19.0.0`) and `framer-motion` (`^13.1.0`) are peer
68
+ dependencies. `framer-motion` powers the animated overlays (Modal, Drawer, Popover,
69
+ Snackbar); it is kept off the critical path and loaded only by the components that use it.
70
+
71
+ ## Quick start
4
72
 
5
73
  ```tsx
6
74
  import { Button, ThemeProvider, ThemeToggle } from "@bzync/rui"
@@ -27,99 +95,445 @@ export function App() {
27
95
  }
28
96
  ```
29
97
 
30
- Every component accepts `className` (and native element props where applicable), so one-off
31
- changes compose with the defaults. `ThemeProvider` supports controlled or uncontrolled themes,
32
- system preference, persistent selection, scoped themes, custom accent/neutral scales, and any
33
- CSS variable through `tokens`. `ThemeToggle` accepts custom icons, labels, and classes.
98
+ Every component accepts `className` (and native element props where applicable), so
99
+ one-off changes compose with the defaults instead of fighting them.
34
100
 
35
- ## Installation
101
+ ## The stylesheet
36
102
 
37
- ```sh
38
- npm install @bzync/rui framer-motion
103
+ Import the core stylesheet once, near the application root:
104
+
105
+ ```tsx
106
+ import "@bzync/rui/styles.css"
39
107
  ```
40
108
 
41
- Import the core stylesheet once near the application root:
109
+ It defines the semantic design tokens, the `.rui-theme` scope, the dark-mode variant,
110
+ and component styles. It does **not** download or bundle webfonts — `@bzync/rui` uses
111
+ system fallbacks by default to keep the stylesheet small and avoid unexpected network
112
+ requests. Load any font in your application and override `--font-sans`, `--font-display`,
113
+ and `--font-mono` on `.rui-theme` (or via `ThemeProvider`'s `fonts` option).
114
+
115
+ ## Theming
116
+
117
+ ### ThemeProvider
118
+
119
+ `ThemeProvider` establishes a theme scope. Place one near the root, or wrap any subtree
120
+ to give it its own theme.
42
121
 
43
122
  ```tsx
44
- import "@bzync/rui/styles.css"
123
+ import { ThemeProvider } from "@bzync/rui"
124
+
125
+ <ThemeProvider
126
+ defaultTheme="system" // "light" | "dark" | "system"
127
+ storageKey="app-theme" // localStorage key, or false to disable persistence
128
+ applyToRoot // also toggle the `dark` class on <html>
129
+ palette={{ accent, neutral, radius, fonts, spacing, shadows, colors, tokens }}
130
+ lightPalette={{ /* overrides applied only in light mode */ }}
131
+ darkPalette={{ /* overrides applied only in dark mode */ }}
132
+ >
133
+ {children}
134
+ </ThemeProvider>
45
135
  ```
46
136
 
47
- rui does not download or bundle webfonts. It uses system fallbacks by default, keeping the
48
- core stylesheet small and avoiding unexpected network requests. Applications can load any font
49
- and override `--font-sans`, `--font-display`, and `--font-mono` on `.rui-theme`.
137
+ Controlled usage is supported by passing `theme` and `onThemeChange` instead of
138
+ `defaultTheme`.
50
139
 
51
- ## Accessibility and motion
140
+ **Palette shape** (`ThemePalette`, all fields optional):
52
141
 
53
- Interactive components expose native semantics and keyboard behavior. Modal dialogs label their
54
- content, trap keyboard focus, close with Escape, restore the previously focused element, and
55
- preserve the page's prior scroll-lock state. rui also disables nonessential animation within its
56
- theme scope when the user requests reduced motion.
142
+ | Field | Type | Purpose |
143
+ |---|---|---|
144
+ | `accent` | `Partial<Record<50‥950, string>>` | Primary/action color scale |
145
+ | `neutral` | `Partial<Record<50‥950, string>>` | Grayscale / surfaces / borders |
146
+ | `colors` | `ThemeColors` | Named semantic colors (bg, surface, foreground, border, status colors, focus ring…) |
147
+ | `radius` | `Partial<Record<"sm"‥"2xl" \| "full", string>>` | Corner radii |
148
+ | `fonts` | `ThemeFonts` | `sans`, `display`, `mono` families |
149
+ | `spacing` | `Record<string, string>` | Spacing scale entries |
150
+ | `shadows` | `Partial<Record<"xs"‥"2xl", string>>` | Elevation shadows |
151
+ | `tokens` | `Partial<Record<\`--${string}\`, string \| number>>` | Any CSS custom property, escape hatch |
57
152
 
58
- ## Component imports
153
+ ### useTheme
59
154
 
60
- Use subpath imports when an application only needs a small part of the library:
155
+ ```tsx
156
+ import { useTheme } from "@bzync/rui"
157
+
158
+ function Example() {
159
+ const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme()
160
+ // theme: "light" | "dark" | "system" (the user's selection)
161
+ // resolvedTheme: "light" | "dark" (after resolving "system")
162
+ return <button onClick={toggleTheme}>Now: {resolvedTheme}</button>
163
+ }
164
+ ```
165
+
166
+ `useTheme` must be called inside a `ThemeProvider`.
167
+
168
+ ### ThemeToggle
169
+
170
+ ```tsx
171
+ import { ThemeToggle } from "@bzync/rui"
172
+
173
+ <ThemeToggle
174
+ showLabel
175
+ lightLabel="Light"
176
+ darkLabel="Dark"
177
+ lightIcon={<SunIcon />}
178
+ darkIcon={<MoonIcon />}
179
+ />
180
+ ```
181
+
182
+ Renders a button with `aria-pressed` and an `aria-label` that reflects the current state.
183
+
184
+ ### Semantic tokens
185
+
186
+ Author custom components against the semantic tokens rather than raw palette values:
187
+ `bg`, `surface`, `surface-raised`, `surface-muted`, `foreground`, `muted-foreground`,
188
+ `border`, `border-strong`, `primary`, `primary-foreground`, `destructive`, the status
189
+ colors, and `focus-ring`. They adapt to light/dark and to any palette override
190
+ automatically.
191
+
192
+ ## Importing components
193
+
194
+ The root entry re-exports everything for convenience:
195
+
196
+ ```tsx
197
+ import { Button, Modal, DataTable } from "@bzync/rui"
198
+ ```
199
+
200
+ Per-component subpath entries let a bundler pull in only what an application uses:
61
201
 
62
202
  ```tsx
63
203
  import { Button } from "@bzync/rui/button"
204
+ import { Modal } from "@bzync/rui/modal"
205
+ import { BarChart } from "@bzync/rui/charts"
64
206
  ```
65
207
 
66
- The root export remains available for convenience. React 18.2 and React 19 are supported.
208
+ Both forms are tree-shakeable the package sets `"sideEffects": ["**/*.css"]` and ships
209
+ ESM — but subpath imports keep dependency graphs smallest and are the recommended default
210
+ for libraries and performance-sensitive apps.
67
211
 
68
- ## Documentation
212
+ ## Server components and SSR
69
213
 
70
- The component documentation and live demos are published to
71
- [bzync.github.io/rui](https://bzync.github.io/rui/). A push to `main` deploys
72
- the latest documentation through GitHub Pages; maintainers can also run the
73
- **Deploy documentation** workflow manually from the Actions tab.
214
+ Client components are marked with the `"use client"` directive, so React Server
215
+ Components and frameworks like Next.js App Router can import and render them directly.
216
+ `ThemeProvider` reads `localStorage` only on the client and resolves `"system"` with
217
+ `matchMedia`; render it in a client boundary and pass a `defaultTheme` so the server and
218
+ first client paint agree.
74
219
 
75
- ## Maintainer
220
+ ## Accessibility and motion
76
221
 
77
- `@bzync/rui` is maintained by [Rayan Reynaldo](https://www.bzync.com), Founder of Bzync
78
- ([www.bzync.com](https://www.bzync.com)).
222
+ - Interactive components expose native element semantics and keyboard behavior.
223
+ - Modal dialogs label their content, trap keyboard focus, close on <kbd>Escape</kbd>,
224
+ restore focus to the previously focused element on close, and preserve the page's
225
+ prior scroll-lock state.
226
+ - Menus, listboxes, tabs, and toggles implement roving focus and arrow-key navigation.
227
+ - Non-essential animation inside the `.rui-theme` scope is disabled when the user has
228
+ `prefers-reduced-motion: reduce` set.
229
+ - Icon-only controls (`IconButton`, `InfoButton`, `CopyButton`) require an accessible
230
+ `label`.
231
+
232
+ ## Component catalog
233
+
234
+ Import any of these from the root or from `@bzync/rui/<kebab-name>`.
235
+
236
+ ### Providers and theming
237
+
238
+ | Component / export | Notes |
239
+ |---|---|
240
+ | `ThemeProvider` | Token-driven theme scope; light/dark, controlled or uncontrolled, persistence, `applyToRoot` |
241
+ | `useTheme()` | `{ theme, resolvedTheme, setTheme, toggleTheme }` |
242
+ | `ThemeToggle` | Accessible light/dark switch with custom icons and labels |
243
+ | `SnackbarProvider` / `useSnackbar()` | Programmatic toast notifications |
244
+ | `CommandProvider` / `CommandPalette` | App-wide searchable command palette |
245
+ | `cn()` | `clsx` + `tailwind-merge` class combiner |
246
+
247
+ ### Actions
248
+
249
+ | Component | Key props |
250
+ |---|---|
251
+ | `Button` | `variant: primary \| secondary \| ghost \| outline \| destructive \| link`, `size: sm \| md \| lg \| icon`, `loading`, `icon`, `iconPosition` |
252
+ | `ButtonGroup` | `orientation`, `aria-label`; wraps `Button` children with `role="group"` |
253
+ | `IconButton` / `InfoButton` | `label` (required for a11y), icon |
254
+ | `CopyButton` | `value` (text to copy), `label`, `timeout` |
255
+ | `Toggle` / `ToggleGroup` / `ToggleGroupItem` | `pressed` / `defaultPressed`, `onPressedChange`, `type: single \| multiple`, `variant`, `size`, `orientation`, `loop` |
256
+ | `BillingIntervalToggle` | `value: monthly \| yearly`, `onChange` |
257
+
258
+ ### Forms
259
+
260
+ | Component | Key props |
261
+ |---|---|
262
+ | `Input` | `label`, `hint`, `error`, `prefix` / `suffix`, `size` |
263
+ | `Textarea` | `label`, `hint`, `error`, `rows` |
264
+ | `NumberInput` | `value` / `defaultValue`, `min`, `max`, `step`, `onChange` |
265
+ | `OtpInput` | `length`, `value`, `onChange`, `label` |
266
+ | `Select` | `options`, `label`, `multiple`, groups, searchable, `onChange` |
267
+ | `Autocomplete` | `options`, `multiple`, custom filter, single/multi triggers |
268
+ | `Checkbox` | `label`, `checked` / `defaultChecked`, `onCheckedChange` |
269
+ | `Radio` / `RadioGroup` | `value`, `onChange`, `options` |
270
+ | `Switch` | `label`, `checked`, `onCheckedChange` |
271
+ | `Slider` | `value` / `defaultValue`, `min`, `max`, `step`, `label` |
272
+ | `Rating` | `value` / `defaultValue`, `max`, `size`, `readOnly`, `onValueChange` |
273
+ | `DatePicker` | `label`, `value`, `onChange` |
274
+ | `Calendar` | `value: Date`, `onChange`, month/week views |
275
+ | `TimePicker` | `value` / `defaultValue`, `format: 12 \| 24`, `minuteStep`, `showSeconds`, `min` / `max`, `clearable` |
276
+ | `FileUpload` | `label`, `accept`, `multiple`, `onFilesChange` |
277
+ | `Label` | `htmlFor`, `required`, `hint` |
278
+ | `FormField` | `label`, `htmlFor`, `required`, `hint`, `error` — wraps a control |
279
+ | `Stepper` | `steps`, `activeStep` |
280
+ | `Kbd` | keyboard-key children |
281
+
282
+ ### Display
283
+
284
+ | Component | Key props |
285
+ |---|---|
286
+ | `Badge` / `Tag` | `variant`, `dot`; `Tag` adds `onRemove` |
287
+ | `Avatar` / `AvatarGroup` / `AvatarGroupOverflow` | `name`, `src`, `size`, initials fallback; group `spacing`, `count` |
288
+ | `Card` (`CardHeader` / `CardTitle` / `CardDescription` / `CardBody` / `CardFooter`) | composition |
289
+ | `Callout` / `Alert` | `title`, `variant`; `Alert` adds `dismissable`, `onDismiss` |
290
+ | `Stat` / `StatusDot` | `Stat`: `label`, `value`, `trend`, `trendValue`; `StatusDot`: `status`, `label` |
291
+ | `Tooltip` | `content`, trigger child |
292
+ | `Link` | `href`, `variant` |
293
+ | `Typography`: `Heading` / `Text` / `Prose` / `Time` | `Heading`: `as h1‥h6`, `size`, `tone`, `weight`, `balance`; `Text`: `variant`, `size`, number/date/currency formatting; `Time`: renders `<time datetime>` |
294
+ | `Code` / `InlineCode` / `CodeBlock` / `CodeEditor` | `code`, `filename`, `showLineNumbers`, `value` / `onChange` |
295
+ | `Blockquote` | `variant`, `size`, `cite`, `source` / `sourceHref` |
296
+ | `Currency` | `value: number \| bigint`, `currency`, `locale`, `accounting`, `tone`, `size` |
297
+ | `DescriptionList` (`DescriptionItem` / `DescriptionTerm` / `DescriptionDetails`) | `columns: 1 \| 2 \| 3`, `density`, `orientation` |
298
+ | `List` / `ListItem`, `Timeline`, `Tree` | item collections; `Tree` is expandable |
299
+ | `Table` (`TableHeader` / `TableHead` / `TableBody` / `TableRow` / `TableCell`) | styled primitive table |
300
+ | `Divider` / `Separator` | `orientation`, `variant`, `spacing`, optional `label` |
301
+ | `ScrollArea` | `orientation: vertical \| horizontal \| both`, `hideScrollbar`, `keyboardNavigable` |
302
+ | `AspectRatio` | `ratio: number` |
303
+ | `Skeleton` (+ `SkeletonAvatar` / `SkeletonCard` / `SkeletonTable` / `SkeletonText` / `SkeletonTopbar`) | loading placeholders |
304
+ | `Spinner` / `Progressbar` | `size`; `value`, `max` |
305
+ | `EmptyState` / `ErrorState` | `title`, `description`, `error`, action slot |
306
+ | `RichText` / `RichTextEditor` | rendered content and editor |
307
+ | `AuthBackdrop` | decorative auth-screen background |
308
+
309
+ ### Overlays and feedback
310
+
311
+ | Component | Key props |
312
+ |---|---|
313
+ | `Modal` (`ModalHeader` / `ModalTitle` / `ModalDescription` / `ModalBody` / `ModalFooter`) | `open`, `onClose`, `title`, `size: sm‥7xl \| full`, `ModalBody` `scrollable` |
314
+ | `Drawer` | `open`, `onClose`, `title`, `size`, focus trap |
315
+ | `ConfirmDialog` | `open`, `onConfirm`, `onCancel`, `title` |
316
+ | `Popover` / `PopoverContent` | trigger, `open` / `onOpenChange` |
317
+ | `DropdownMenu` | `trigger`, `items: { label, onClick }[]` |
318
+ | `Command` / `CommandPalette` / `CommandProvider` | searchable command palette |
319
+ | `SnackbarProvider` / `useSnackbar()` | queue and dismiss toasts programmatically |
320
+ | `Tabs` (`TabsList` / `TabsTrigger` / `TabsContent`) | `value`, `onValueChange` |
321
+ | `Accordion` | `items: { id, trigger, content }[]` |
322
+ | `Pagination` | `page`, `totalPages`, `onPageChange` |
323
+ | `Terminal` / `TerminalBlock` / `TerminalEmulator` | virtual filesystem, runnable shell commands |
324
+
325
+ ### Navigation and layout
326
+
327
+ | Component | Key props |
328
+ |---|---|
329
+ | `AppShell` (`AppShellHeader` / `AppShellBody` / `AppShellMain` / `Footer`) | app frame; `sticky`, `scrollable`, `fixed` |
330
+ | `Container` | `size: sm‥xl \| full`, `gutter` |
331
+ | `Stack` / `Inline` / `PageHeader` | spacing and header primitives |
332
+ | `Navbar` / `Sidebar` / `Topbar` / `BottomBar` | `items: NavigationItem[]`, `activeId`, `onSelect` |
333
+ | `NavigationLink` | `id`, `label`, `href`, `icon`, `badge`, `active`, `compact` |
334
+ | `Breadcrumb` | `items` |
335
+ | `SEO` | document head tags |
336
+
337
+ ### Data
338
+
339
+ | Component | Key props |
340
+ |---|---|
341
+ | `DataTable<T>` | `columns: ColumnDef<T>[]` (`{ key, header, cell, sortable?, searchable?, align?, width? }`), `data: T[]`, `searchable`, `searchPlaceholder`, `pageSizeOptions`, `density`, `loading`, `emptyMessage`, `onRowClick`, `unstyled` — built-in sort, search, and pagination. `T` must have an `id`. |
342
+
343
+ ## Common recipes
344
+
345
+ ### Button
79
346
 
80
- ## Support
347
+ ```tsx
348
+ import { Button } from "@bzync/rui/button"
81
349
 
82
- If `@bzync/rui` is useful to you, you can support its continued development on
83
- [Buy Me a Coffee](https://buymeacoffee.com/adminjw).
350
+ <Button variant="primary" size="lg" loading={isSaving} onClick={save}>
351
+ Save changes
352
+ </Button>
84
353
 
85
- ## Releasing to npm
354
+ <Button variant="outline" icon={<PlusIcon />} iconPosition="left">
355
+ New item
356
+ </Button>
357
+ ```
86
358
 
87
- Add an npm publishing token to the GitHub repository as an Actions secret named
88
- `NPM_TOKEN`. The release workflow runs when a `v*` tag is pushed and requires the
89
- tag to match the version in `package.json` exactly.
359
+ ### A labelled, validated field
90
360
 
91
- To publish the version currently declared in `package.json`:
361
+ ```tsx
362
+ import { FormField } from "@bzync/rui/form-field"
363
+ import { Input } from "@bzync/rui/input"
92
364
 
93
- ```sh
94
- VERSION="$(node --print "require('./package.json').version")"
95
- git tag -a "v${VERSION}" -m "Release v${VERSION}"
96
- git push origin "v${VERSION}"
365
+ <FormField label="Email" htmlFor="email" required error={errors.email}>
366
+ <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
367
+ </FormField>
97
368
  ```
98
369
 
99
- For later patch releases, commit all pending changes and run:
370
+ ### Select
100
371
 
101
- ```sh
102
- npm version patch
103
- git push origin HEAD --follow-tags
372
+ ```tsx
373
+ import { Select } from "@bzync/rui/select"
374
+
375
+ <Select
376
+ label="Environment"
377
+ options={[
378
+ { label: "Production", value: "prod" },
379
+ { label: "Staging", value: "staging" },
380
+ { label: "Development", value: "dev" },
381
+ ]}
382
+ value={env}
383
+ onChange={setEnv}
384
+ />
104
385
  ```
105
386
 
106
- `npm version patch` updates `package.json` and `package-lock.json`, creates a
107
- release commit, and creates the matching version tag. Use `minor` or `major`
108
- instead of `patch` when appropriate. Before publishing, GitHub Actions runs the
109
- complete release check. It also generates npm provenance when the source
110
- repository is public; npm does not support provenance from private GitHub
111
- repositories.
387
+ ### Modal
112
388
 
113
- ### Retrying a failed release
389
+ ```tsx
390
+ import { Modal, ModalBody, ModalFooter } from "@bzync/rui/modal"
391
+ import { Button } from "@bzync/rui/button"
114
392
 
115
- Do not move or reuse an existing version tag after its workflow fails. Commit
116
- the fixes, create the next patch version, and push its new tag:
393
+ const [open, setOpen] = useState(false)
117
394
 
118
- ```sh
119
- git add .
120
- git commit -m "Fix npm release workflow"
121
- npm version patch
122
- git push origin HEAD --follow-tags
395
+ <Modal open={open} onClose={() => setOpen(false)} title="Delete project" size="sm">
396
+ <ModalBody>This action cannot be undone.</ModalBody>
397
+ <ModalFooter>
398
+ <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
399
+ <Button variant="destructive" onClick={confirmDelete}>Delete</Button>
400
+ </ModalFooter>
401
+ </Modal>
402
+ ```
403
+
404
+ ### Toasts
405
+
406
+ ```tsx
407
+ import { SnackbarProvider, useSnackbar } from "@bzync/rui/snackbar"
408
+
409
+ function Root() {
410
+ return (
411
+ <SnackbarProvider>
412
+ <App />
413
+ </SnackbarProvider>
414
+ )
415
+ }
416
+
417
+ function SaveButton() {
418
+ const { show } = useSnackbar()
419
+ return <Button onClick={() => show({ title: "Saved", variant: "success" })}>Save</Button>
420
+ }
123
421
  ```
124
422
 
125
- For example, because `v0.0.2` failed, this creates and publishes `v0.0.3`.
423
+ `useSnackbar()` returns `{ show, dismiss, dismissAll }`. `show(opts)` returns the toast id;
424
+ pass a stable `id` in `opts` to replace an existing toast in place instead of stacking.
425
+
426
+ ### DataTable
427
+
428
+ ```tsx
429
+ import { DataTable } from "@bzync/rui/datatable"
430
+
431
+ type Row = { id: string; name: string; role: string; seats: number }
432
+
433
+ <DataTable<Row>
434
+ data={members}
435
+ columns={[
436
+ { key: "name", header: "Name", cell: (r) => r.name, sortable: true, searchable: true },
437
+ { key: "role", header: "Role", cell: (r) => r.role, sortable: true },
438
+ { key: "seats", header: "Seats", cell: (r) => r.seats, align: "right", sortable: true },
439
+ ]}
440
+ searchable
441
+ pageSizeOptions={[10, 25, 50]}
442
+ onRowClick={(row) => open(row.id)}
443
+ />
444
+ ```
445
+
446
+ ## Charts
447
+
448
+ The chart set is a separate subpath entry so it stays out of the main graph unless used.
449
+ Every chart renders plain SVG and takes data arrays — no canvas, no chart engine.
450
+
451
+ ```tsx
452
+ import { BarChart, LineChart, DonutChart } from "@bzync/rui/charts"
453
+
454
+ <BarChart data={[{ label: "Jan", value: 42 }, { label: "Feb", value: 55 }]} />
455
+ ```
456
+
457
+ Available: `BarChart`, `LineChart`, `MultiLineChart`, `DonutChart`, `ScatterChart`,
458
+ `GanttChart`, `HeatmapChart`, `RadarChart`, `FunnelChart`, `WaterfallChart`. See
459
+ [`src/components/charts`](./src/components/charts) for exact per-chart props.
460
+
461
+ ## Hooks and utilities
462
+
463
+ Lifecycle-correct hooks (mount/update/unmount with cleanup), re-exported from the root:
464
+
465
+ | Hook | Purpose |
466
+ |---|---|
467
+ | `useIsMounted()` | Guard async setState after unmount |
468
+ | `useIsomorphicLayoutEffect()` | `useLayoutEffect` on the client, `useEffect` on the server |
469
+ | `usePrevious(value)` | Previous render's value |
470
+ | `useUpdateEffect(fn, deps)` | Effect that skips the first render |
471
+ | `useEventCallback(fn)` | Stable callback identity with fresh closure |
472
+ | `useControllableState(opts)` | Controlled/uncontrolled state pattern |
473
+ | `useMediaQuery(query)` | Subscribe to a media query |
474
+ | `useAbortSignal()` | Abort in-flight work on unmount |
475
+ | `useFocusTrap(ref, active)` | Trap focus within a container |
476
+ | `useOutsideClick(ref, handler)` | Detect clicks outside an element |
477
+
478
+ Utilities: `cn()`, focus helpers, `Portal`, and assertion helpers from `@bzync/rui/utils`;
479
+ `ErrorBoundary` from the root; `createSafeEffect` and mount helpers from `@bzync/rui`'s
480
+ lifecycle exports; `KEY`, `DURATIONS`, and `FOCUSABLE_SELECTOR` constants.
481
+
482
+ ## TypeScript
483
+
484
+ `@bzync/rui` is written in TypeScript and ships `.d.ts` files for the root and every
485
+ subpath entry. Prop types, variant unions (`ButtonVariant`, `ButtonSize`, …), and the
486
+ theming types (`Theme`, `ThemePalette`, `ThemeColors`, `ColorShade`, …) are all exported.
487
+ No `@types/*` package is required.
488
+
489
+ ## Browser and React support
490
+
491
+ - **React** `^18.2.0 || ^19.0.0`
492
+ - **Modern evergreen browsers.** The library relies on CSS custom properties, `matchMedia`,
493
+ and standard DOM APIs; no polyfills are bundled.
494
+ - **SSR / RSC** via the `"use client"` boundary described above.
495
+
496
+ ## Bundle and dependencies
497
+
498
+ - `dependencies`: **none**. `clsx`/`tailwind-merge` logic is inlined; icons are inlined SVG.
499
+ - Published output is **not minified** — readable ESM and CJS ship to the registry so the
500
+ code is auditable; your bundler minifies the final app build.
501
+ - `sideEffects` is limited to `**/*.css`, so unused components are dropped by any
502
+ tree-shaking bundler.
503
+
504
+ ## Versioning
505
+
506
+ `@bzync/rui` follows [semantic versioning](https://semver.org/). While the major version
507
+ is `0`, minor releases may contain breaking changes; pin a version or a tight range and
508
+ review the release notes before upgrading. Releases are published from CI with npm
509
+ [provenance](https://docs.npmjs.com/generating-provenance-statements) attestations.
510
+
511
+ ## Documentation
512
+
513
+ Full component documentation and live, themeable demos are published at
514
+ **[bzync.github.io/rui](https://bzync.github.io/rui/)**. A push to `main` deploys the
515
+ latest docs via GitHub Pages.
516
+
517
+ ## Contributing
518
+
519
+ Bug reports, accessibility fixes, documentation improvements, and focused component
520
+ contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development
521
+ setup, the verification gate, and the release process, and
522
+ [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
523
+
524
+ ## Security
525
+
526
+ Report vulnerabilities privately using the process in [SECURITY.md](./SECURITY.md).
527
+ Please do not open public issues for security reports.
528
+
529
+ ## License
530
+
531
+ [ISC](./LICENSE) © 2026 Bzync
532
+
533
+ ## Maintainer
534
+
535
+ `@bzync/rui` is maintained by [Rayan Reynaldo](https://www.bzync.com), Founder of Bzync
536
+ ([www.bzync.com](https://www.bzync.com)).
537
+
538
+ If `@bzync/rui` is useful to you, you can support its continued development on
539
+ [Buy Me a Coffee](https://buymeacoffee.com/adminjw).
@@ -1 +1 @@
1
- "use client";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../cn-B6R3HKzx.cjs");let t=require("react"),n=require("react/jsx-runtime");function r({onClick:t,title:r,active:i,children:a}){return(0,n.jsx)(`button`,{type:`button`,title:r,onMouseDown:e=>{e.preventDefault(),t()},className:e.t(`h-7 w-7 flex items-center justify-center rounded-md text-sm transition-colors`,i?`bg-black/10 dark:bg-white/15 text-foreground`:`text-slate-500 dark:text-slate-400 hover:text-foreground hover:bg-black/6 dark:hover:bg-white/8`),children:a})}function i(){return(0,n.jsx)(`div`,{className:`w-px h-5 bg-black/10 dark:bg-white/10 mx-0.5`})}function a(e){if(typeof document>`u`)return e;let t=document.createElement(`template`);t.innerHTML=e,t.content.querySelectorAll(`script, style, iframe, object, embed, link, meta`).forEach(e=>e.remove());let n=document.createTreeWalker(t.content,NodeFilter.SHOW_ELEMENT);for(;n.nextNode();){let e=n.currentNode;for(let t of Array.from(e.attributes)){let n=t.name.toLowerCase(),r=t.value.trim().toLowerCase();(n.startsWith(`on`)||r.startsWith(`javascript:`))&&e.removeAttribute(t.name)}}let r=t.innerHTML;return r=r.replace(/\son\w+\s*=\s*(['"])[^'"]*\1/gi,``),r}function o(e,t){return t?typeof t==`function`?t(e):a(e):e}function s({value:a=``,onChange:s,placeholder:c=`Start typing…`,className:l,minHeight:u=160,sanitize:d=!1}){let f=(0,t.useRef)(null),p=(0,t.useRef)(a);(0,t.useEffect)(()=>{let e=o(a,d);f.current&&f.current.innerHTML!==e&&(f.current.innerHTML=e,p.current=e)},[a,d]);let m=(0,t.useCallback)(()=>{let e=f.current?.innerHTML??``,t=d?o(e,d):e;t!==p.current&&(p.current=t,s?.(t))},[s,d]);function h(e,t){try{document.execCommand(e,!1,t)}catch{}f.current?.focus(),m()}function g(e){try{return document.queryCommandState(e)}catch{return!1}}return(0,n.jsxs)(`div`,{className:e.t(`rounded-xl overflow-hidden border border-black/10 dark:border-white/10 bg-white dark:bg-navy-900 focus-within:ring-1 focus-within:ring-blue-500/40 focus-within:border-accent-500/30 transition-all`,l),children:[(0,n.jsxs)(`div`,{className:`flex flex-wrap items-center gap-0.5 px-2 py-2 border-b border-black/[0.07] dark:border-white/[0.07] bg-black/2 dark:bg-white/2`,children:[(0,n.jsx)(r,{title:`Bold (⌘B)`,onClick:()=>h(`bold`),active:g(`bold`),children:(0,n.jsx)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,children:(0,n.jsx)(`path`,{d:`M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8`})})}),(0,n.jsx)(r,{title:`Italic (⌘I)`,onClick:()=>h(`italic`),active:g(`italic`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`line`,{x1:`19`,y1:`4`,x2:`10`,y2:`4`}),(0,n.jsx)(`line`,{x1:`14`,y1:`20`,x2:`5`,y2:`20`}),(0,n.jsx)(`line`,{x1:`15`,y1:`4`,x2:`9`,y2:`20`})]})}),(0,n.jsx)(r,{title:`Underline (⌘U)`,onClick:()=>h(`underline`),active:g(`underline`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M6 4v6a6 6 0 0 0 12 0V4`}),(0,n.jsx)(`line`,{x1:`4`,y1:`20`,x2:`20`,y2:`20`})]})}),(0,n.jsx)(r,{title:`Strikethrough`,onClick:()=>h(`strikeThrough`),active:g(`strikeThrough`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6`}),(0,n.jsx)(`line`,{x1:`4`,y1:`12`,x2:`20`,y2:`12`})]})}),(0,n.jsx)(i,{}),(0,n.jsx)(r,{title:`Heading 1`,onClick:()=>h(`formatBlock`,`<h1>`),children:(0,n.jsx)(`span`,{className:`text-[11px] font-bold`,children:`H1`})}),(0,n.jsx)(r,{title:`Heading 2`,onClick:()=>h(`formatBlock`,`<h2>`),children:(0,n.jsx)(`span`,{className:`text-[11px] font-bold`,children:`H2`})}),(0,n.jsx)(r,{title:`Paragraph`,onClick:()=>h(`formatBlock`,`<p>`),children:(0,n.jsx)(`span`,{className:`text-[11px] font-medium`,children:`P`})}),(0,n.jsx)(i,{}),(0,n.jsx)(r,{title:`Bullet list`,onClick:()=>h(`insertUnorderedList`),active:g(`insertUnorderedList`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`line`,{x1:`9`,y1:`6`,x2:`20`,y2:`6`}),(0,n.jsx)(`line`,{x1:`9`,y1:`12`,x2:`20`,y2:`12`}),(0,n.jsx)(`line`,{x1:`9`,y1:`18`,x2:`20`,y2:`18`}),(0,n.jsx)(`circle`,{cx:`4`,cy:`6`,r:`1.5`,fill:`currentColor`,stroke:`none`}),(0,n.jsx)(`circle`,{cx:`4`,cy:`12`,r:`1.5`,fill:`currentColor`,stroke:`none`}),(0,n.jsx)(`circle`,{cx:`4`,cy:`18`,r:`1.5`,fill:`currentColor`,stroke:`none`})]})}),(0,n.jsx)(r,{title:`Ordered list`,onClick:()=>h(`insertOrderedList`),active:g(`insertOrderedList`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`line`,{x1:`10`,y1:`6`,x2:`21`,y2:`6`}),(0,n.jsx)(`line`,{x1:`10`,y1:`12`,x2:`21`,y2:`12`}),(0,n.jsx)(`line`,{x1:`10`,y1:`18`,x2:`21`,y2:`18`}),(0,n.jsx)(`path`,{d:`M4 6h1v4`}),(0,n.jsx)(`path`,{d:`M4 10h2`}),(0,n.jsx)(`path`,{d:`M6 18H4c0-1 2-2 2-3s-1-1.5-2-1`})]})}),(0,n.jsx)(r,{title:`Blockquote`,onClick:()=>h(`formatBlock`,`<blockquote>`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z`}),(0,n.jsx)(`path`,{d:`M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z`})]})}),(0,n.jsx)(i,{}),(0,n.jsx)(r,{title:`Undo (⌘Z)`,onClick:()=>h(`undo`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M3 7v6h6`}),(0,n.jsx)(`path`,{d:`M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13`})]})}),(0,n.jsx)(r,{title:`Redo (⌘⇧Z)`,onClick:()=>h(`redo`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M21 7v6h-6`}),(0,n.jsx)(`path`,{d:`M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7`})]})}),(0,n.jsx)(r,{title:`Clear formatting`,onClick:()=>h(`removeFormat`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M4 7V4h16v3`}),(0,n.jsx)(`path`,{d:`M5 20h6`}),(0,n.jsx)(`path`,{d:`M13 4 8 20`}),(0,n.jsx)(`line`,{x1:`3`,y1:`3`,x2:`21`,y2:`21`})]})})]}),(0,n.jsxs)(`div`,{className:`relative`,children:[(0,n.jsx)(`div`,{ref:f,contentEditable:!0,suppressContentEditableWarning:!0,onInput:m,onBlur:m,style:{minHeight:u},className:e.t(`px-4 py-3 text-sm text-slate-800 dark:text-slate-200 focus:outline-none`,`prose prose-sm max-w-none`,`[&_h1]:text-xl [&_h1]:font-bold [&_h1]:mb-2 [&_h1]:text-gray-900 dark:[&_h1]:text-white`,`[&_h2]:text-lg [&_h2]:font-semibold [&_h2]:mb-1.5 [&_h2]:text-gray-900 dark:[&_h2]:text-white`,`[&_p]:mb-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:mb-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:mb-2`,`[&_blockquote]:border-l-2 [&_blockquote]:border-accent-400 [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:italic`)}),!a&&(0,n.jsx)(`div`,{className:`pointer-events-none absolute top-3 left-4 text-sm text-slate-400 dark:text-slate-600 select-none`,"aria-hidden":!0,children:c})]})]})}exports.RichTextEditor=s;
1
+ "use client";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../cn-B6R3HKzx.cjs");let t=require("react"),n=require("react/jsx-runtime");function r({onClick:t,title:r,active:i,children:a}){return(0,n.jsx)(`button`,{type:`button`,title:r,onMouseDown:e=>{e.preventDefault(),t()},className:e.t(`h-7 w-7 flex items-center justify-center rounded-md text-sm transition-colors`,i?`bg-black/10 dark:bg-white/15 text-foreground`:`text-slate-500 dark:text-slate-400 hover:text-foreground hover:bg-black/6 dark:hover:bg-white/8`),children:a})}function i(){return(0,n.jsx)(`div`,{className:`w-px h-5 bg-black/10 dark:bg-white/10 mx-0.5`})}var a=new Set([`href`,`src`,`xlink:href`,`action`,`formaction`,`poster`,`background`,`cite`,`data`,`ping`,`srcset`]),o=new Set([`http`,`https`,`mailto`,`tel`,`ftp`,`sms`]);function s(e){let t=e.replace(/[\u0000-\u0020\u007f-\u00a0]/g,``).toLowerCase(),n=/^([a-z][a-z0-9+.-]*):/.exec(t);return n?!o.has(n[1]):!1}function c(e){if(typeof document>`u`)return e;let t=document.createElement(`template`);t.innerHTML=e,t.content.querySelectorAll(`script, style, iframe, object, embed, link, meta, base, form`).forEach(e=>e.remove());let n=document.createTreeWalker(t.content,NodeFilter.SHOW_ELEMENT);for(;n.nextNode();){let e=n.currentNode;for(let t of Array.from(e.attributes)){let n=t.name.toLowerCase();if(n.startsWith(`on`)){e.removeAttribute(t.name);continue}a.has(n)&&s(t.value)&&e.removeAttribute(t.name)}}return t.innerHTML}function l(e,t){return t?typeof t==`function`?t(e):c(e):e}function u({value:a=``,onChange:o,placeholder:s=`Start typing…`,className:c,minHeight:u=160,sanitize:d=!1}){let f=(0,t.useRef)(null),p=(0,t.useRef)(a);(0,t.useEffect)(()=>{let e=l(a,d);f.current&&f.current.innerHTML!==e&&(f.current.innerHTML=e,p.current=e)},[a,d]);let m=(0,t.useCallback)(()=>{let e=f.current?.innerHTML??``,t=d?l(e,d):e;t!==p.current&&(p.current=t,o?.(t))},[o,d]);function h(e,t){try{document.execCommand(e,!1,t)}catch{}f.current?.focus(),m()}function g(e){try{return document.queryCommandState(e)}catch{return!1}}return(0,n.jsxs)(`div`,{className:e.t(`rounded-xl overflow-hidden border border-black/10 dark:border-white/10 bg-white dark:bg-navy-900 focus-within:ring-1 focus-within:ring-blue-500/40 focus-within:border-accent-500/30 transition-all`,c),children:[(0,n.jsxs)(`div`,{className:`flex flex-wrap items-center gap-0.5 px-2 py-2 border-b border-black/[0.07] dark:border-white/[0.07] bg-black/2 dark:bg-white/2`,children:[(0,n.jsx)(r,{title:`Bold (⌘B)`,onClick:()=>h(`bold`),active:g(`bold`),children:(0,n.jsx)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,children:(0,n.jsx)(`path`,{d:`M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8`})})}),(0,n.jsx)(r,{title:`Italic (⌘I)`,onClick:()=>h(`italic`),active:g(`italic`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`line`,{x1:`19`,y1:`4`,x2:`10`,y2:`4`}),(0,n.jsx)(`line`,{x1:`14`,y1:`20`,x2:`5`,y2:`20`}),(0,n.jsx)(`line`,{x1:`15`,y1:`4`,x2:`9`,y2:`20`})]})}),(0,n.jsx)(r,{title:`Underline (⌘U)`,onClick:()=>h(`underline`),active:g(`underline`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M6 4v6a6 6 0 0 0 12 0V4`}),(0,n.jsx)(`line`,{x1:`4`,y1:`20`,x2:`20`,y2:`20`})]})}),(0,n.jsx)(r,{title:`Strikethrough`,onClick:()=>h(`strikeThrough`),active:g(`strikeThrough`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6`}),(0,n.jsx)(`line`,{x1:`4`,y1:`12`,x2:`20`,y2:`12`})]})}),(0,n.jsx)(i,{}),(0,n.jsx)(r,{title:`Heading 1`,onClick:()=>h(`formatBlock`,`<h1>`),children:(0,n.jsx)(`span`,{className:`text-[11px] font-bold`,children:`H1`})}),(0,n.jsx)(r,{title:`Heading 2`,onClick:()=>h(`formatBlock`,`<h2>`),children:(0,n.jsx)(`span`,{className:`text-[11px] font-bold`,children:`H2`})}),(0,n.jsx)(r,{title:`Paragraph`,onClick:()=>h(`formatBlock`,`<p>`),children:(0,n.jsx)(`span`,{className:`text-[11px] font-medium`,children:`P`})}),(0,n.jsx)(i,{}),(0,n.jsx)(r,{title:`Bullet list`,onClick:()=>h(`insertUnorderedList`),active:g(`insertUnorderedList`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`line`,{x1:`9`,y1:`6`,x2:`20`,y2:`6`}),(0,n.jsx)(`line`,{x1:`9`,y1:`12`,x2:`20`,y2:`12`}),(0,n.jsx)(`line`,{x1:`9`,y1:`18`,x2:`20`,y2:`18`}),(0,n.jsx)(`circle`,{cx:`4`,cy:`6`,r:`1.5`,fill:`currentColor`,stroke:`none`}),(0,n.jsx)(`circle`,{cx:`4`,cy:`12`,r:`1.5`,fill:`currentColor`,stroke:`none`}),(0,n.jsx)(`circle`,{cx:`4`,cy:`18`,r:`1.5`,fill:`currentColor`,stroke:`none`})]})}),(0,n.jsx)(r,{title:`Ordered list`,onClick:()=>h(`insertOrderedList`),active:g(`insertOrderedList`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`line`,{x1:`10`,y1:`6`,x2:`21`,y2:`6`}),(0,n.jsx)(`line`,{x1:`10`,y1:`12`,x2:`21`,y2:`12`}),(0,n.jsx)(`line`,{x1:`10`,y1:`18`,x2:`21`,y2:`18`}),(0,n.jsx)(`path`,{d:`M4 6h1v4`}),(0,n.jsx)(`path`,{d:`M4 10h2`}),(0,n.jsx)(`path`,{d:`M6 18H4c0-1 2-2 2-3s-1-1.5-2-1`})]})}),(0,n.jsx)(r,{title:`Blockquote`,onClick:()=>h(`formatBlock`,`<blockquote>`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z`}),(0,n.jsx)(`path`,{d:`M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z`})]})}),(0,n.jsx)(i,{}),(0,n.jsx)(r,{title:`Undo (⌘Z)`,onClick:()=>h(`undo`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M3 7v6h6`}),(0,n.jsx)(`path`,{d:`M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13`})]})}),(0,n.jsx)(r,{title:`Redo (⌘⇧Z)`,onClick:()=>h(`redo`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M21 7v6h-6`}),(0,n.jsx)(`path`,{d:`M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7`})]})}),(0,n.jsx)(r,{title:`Clear formatting`,onClick:()=>h(`removeFormat`),children:(0,n.jsxs)(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,n.jsx)(`path`,{d:`M4 7V4h16v3`}),(0,n.jsx)(`path`,{d:`M5 20h6`}),(0,n.jsx)(`path`,{d:`M13 4 8 20`}),(0,n.jsx)(`line`,{x1:`3`,y1:`3`,x2:`21`,y2:`21`})]})})]}),(0,n.jsxs)(`div`,{className:`relative`,children:[(0,n.jsx)(`div`,{ref:f,contentEditable:!0,suppressContentEditableWarning:!0,onInput:m,onBlur:m,style:{minHeight:u},className:e.t(`px-4 py-3 text-sm text-slate-800 dark:text-slate-200 focus:outline-none`,`prose prose-sm max-w-none`,`[&_h1]:text-xl [&_h1]:font-bold [&_h1]:mb-2 [&_h1]:text-gray-900 dark:[&_h1]:text-white`,`[&_h2]:text-lg [&_h2]:font-semibold [&_h2]:mb-1.5 [&_h2]:text-gray-900 dark:[&_h2]:text-white`,`[&_p]:mb-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:mb-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:mb-2`,`[&_blockquote]:border-l-2 [&_blockquote]:border-accent-400 [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:italic`)}),!a&&(0,n.jsx)(`div`,{className:`pointer-events-none absolute top-3 left-4 text-sm text-slate-400 dark:text-slate-600 select-none`,"aria-hidden":!0,children:s})]})]})}exports.RichTextEditor=u;
@@ -17,34 +17,61 @@ function o({ onClick: t, title: n, active: r, children: a }) {
17
17
  function s() {
18
18
  return /* @__PURE__ */ i("div", { className: "w-px h-5 bg-black/10 dark:bg-white/10 mx-0.5" });
19
19
  }
20
- function c(e) {
20
+ var c = /* @__PURE__ */ new Set([
21
+ "href",
22
+ "src",
23
+ "xlink:href",
24
+ "action",
25
+ "formaction",
26
+ "poster",
27
+ "background",
28
+ "cite",
29
+ "data",
30
+ "ping",
31
+ "srcset"
32
+ ]), l = /* @__PURE__ */ new Set([
33
+ "http",
34
+ "https",
35
+ "mailto",
36
+ "tel",
37
+ "ftp",
38
+ "sms"
39
+ ]);
40
+ function u(e) {
41
+ let t = e.replace(/[\u0000-\u0020\u007f-\u00a0]/g, "").toLowerCase(), n = /^([a-z][a-z0-9+.-]*):/.exec(t);
42
+ return n ? !l.has(n[1]) : !1;
43
+ }
44
+ function d(e) {
21
45
  if (typeof document > "u") return e;
22
46
  let t = document.createElement("template");
23
- t.innerHTML = e, t.content.querySelectorAll("script, style, iframe, object, embed, link, meta").forEach((e) => e.remove());
47
+ t.innerHTML = e, t.content.querySelectorAll("script, style, iframe, object, embed, link, meta, base, form").forEach((e) => e.remove());
24
48
  let n = document.createTreeWalker(t.content, NodeFilter.SHOW_ELEMENT);
25
49
  for (; n.nextNode();) {
26
50
  let e = n.currentNode;
27
51
  for (let t of Array.from(e.attributes)) {
28
- let n = t.name.toLowerCase(), r = t.value.trim().toLowerCase();
29
- (n.startsWith("on") || r.startsWith("javascript:")) && e.removeAttribute(t.name);
52
+ let n = t.name.toLowerCase();
53
+ if (n.startsWith("on")) {
54
+ e.removeAttribute(t.name);
55
+ continue;
56
+ }
57
+ c.has(n) && u(t.value) && e.removeAttribute(t.name);
30
58
  }
31
59
  }
32
- let r = t.innerHTML;
33
- return r = r.replace(/\son\w+\s*=\s*(['"])[^'"]*\1/gi, ""), r;
60
+ return t.innerHTML;
34
61
  }
35
- function l(e, t) {
36
- return t ? typeof t == "function" ? t(e) : c(e) : e;
62
+ function f(e, t) {
63
+ return t ? typeof t == "function" ? t(e) : d(e) : e;
37
64
  }
38
- function u({ value: c = "", onChange: u, placeholder: d = "Start typing…", className: f, minHeight: p = 160, sanitize: m = !1 }) {
65
+ function p({ value: c = "", onChange: l, placeholder: u = "Start typing…", className: d, minHeight: p = 160, sanitize: m = !1 }) {
39
66
  let h = r(null), g = r(c);
40
67
  n(() => {
41
- let e = l(c, m);
68
+ let e = f(c, m);
42
69
  h.current && h.current.innerHTML !== e && (h.current.innerHTML = e, g.current = e);
43
70
  }, [c, m]);
44
71
  let _ = t(() => {
45
- let e = h.current?.innerHTML ?? "", t = m ? l(e, m) : e;
46
- t !== g.current && (g.current = t, u?.(t));
47
- }, [u, m]);
72
+ let e = h.current?.innerHTML ?? "", t = m ? f(e, m) : e;
73
+ t !== g.current && (g.current = t, l?.(t));
74
+ }, [l, m]);
48
75
  function v(e, t) {
49
76
  try {
50
77
  document.execCommand(e, !1, t);
@@ -59,7 +86,7 @@ function u({ value: c = "", onChange: u, placeholder: d = "Start typing…", cla
59
86
  }
60
87
  }
61
88
  return /* @__PURE__ */ a("div", {
62
- className: e("rounded-xl overflow-hidden border border-black/10 dark:border-white/10 bg-white dark:bg-navy-900 focus-within:ring-1 focus-within:ring-blue-500/40 focus-within:border-accent-500/30 transition-all", f),
89
+ className: e("rounded-xl overflow-hidden border border-black/10 dark:border-white/10 bg-white dark:bg-navy-900 focus-within:ring-1 focus-within:ring-blue-500/40 focus-within:border-accent-500/30 transition-all", d),
63
90
  children: [/* @__PURE__ */ a("div", {
64
91
  className: "flex flex-wrap items-center gap-0.5 px-2 py-2 border-b border-black/[0.07] dark:border-white/[0.07] bg-black/2 dark:bg-white/2",
65
92
  children: [
@@ -361,10 +388,10 @@ function u({ value: c = "", onChange: u, placeholder: d = "Start typing…", cla
361
388
  }), !c && /* @__PURE__ */ i("div", {
362
389
  className: "pointer-events-none absolute top-3 left-4 text-sm text-slate-400 dark:text-slate-600 select-none",
363
390
  "aria-hidden": !0,
364
- children: d
391
+ children: u
365
392
  })]
366
393
  })]
367
394
  });
368
395
  }
369
396
  //#endregion
370
- export { u as RichTextEditor };
397
+ export { p as RichTextEditor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bzync/rui",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Bzync React Tailwind UI component library",
5
5
  "type": "module",
6
6
  "files": [