@rtzll/reflow 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ import { type Handle, type RemixNode } from 'remix/component';
2
+ export type ToastStatus = 'default' | 'info' | 'success' | 'warning' | 'error' | 'loading';
3
+ export type ToasterPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
4
+ export interface ToastAction {
5
+ label: string;
6
+ /** Runs on click. The toast then closes, unless the handler calls `event.preventDefault()`. */
7
+ onClick: (event: MouseEvent) => void;
8
+ }
9
+ export interface ToastOptions {
10
+ /** Reuse an id to update a toast in place instead of adding another. */
11
+ id?: string;
12
+ description?: RemixNode;
13
+ /** Milliseconds before it closes on its own. `Infinity` keeps it until dismissed. Defaults to the Toaster's duration. */
14
+ duration?: number;
15
+ action?: ToastAction;
16
+ /** Whether a swipe, Escape or the close button can close it. Default true. */
17
+ dismissible?: boolean;
18
+ /** Replaces the status icon. */
19
+ icon?: RemixNode;
20
+ /** Called when the toast is closed by a swipe, Escape, the close button, an action or `toast.dismiss`. */
21
+ onDismiss?: (toast: Toast) => void;
22
+ /** Called when the toast closes on its own after its duration. */
23
+ onAutoClose?: (toast: Toast) => void;
24
+ }
25
+ export interface Toast extends ToastOptions {
26
+ id: string;
27
+ title: RemixNode;
28
+ status: ToastStatus;
29
+ }
30
+ export interface ToastPromiseMessages<T> {
31
+ loading: RemixNode;
32
+ success: RemixNode | ((value: T) => RemixNode);
33
+ error: RemixNode | ((error: unknown) => RemixNode);
34
+ description?: RemixNode;
35
+ }
36
+ /**
37
+ * Raises a toast in the app's `<Toaster>` and returns its id. `toast.success`,
38
+ * `toast.info`, `toast.warning`, `toast.error` and `toast.loading` set its
39
+ * status; `toast.promise` follows a promise from loading to its outcome;
40
+ * `toast.dismiss` closes one toast, or all of them.
41
+ */
42
+ export declare const toast: ((title: RemixNode, options?: ToastOptions) => string) & {
43
+ success: (title: RemixNode, options?: ToastOptions) => string;
44
+ info: (title: RemixNode, options?: ToastOptions) => string;
45
+ warning: (title: RemixNode, options?: ToastOptions) => string;
46
+ error: (title: RemixNode, options?: ToastOptions) => string;
47
+ loading: (title: RemixNode, options?: ToastOptions) => string;
48
+ promise<T>(promise: Promise<T> | (() => Promise<T>), messages: ToastPromiseMessages<T>, options?: ToastOptions): string;
49
+ dismiss: (id?: string) => void;
50
+ };
51
+ export interface ToasterProps {
52
+ /** Where the stack sits. Default bottom-right. */
53
+ position?: ToasterPosition;
54
+ /** Show the stack spread out instead of piled. Default false. */
55
+ expand?: boolean;
56
+ /** How many toasts stay in view; older ones wait behind them. Default 3. */
57
+ visibleToasts?: number;
58
+ /** Milliseconds each toast stays before closing on its own. Default 4000. */
59
+ duration?: number;
60
+ /** Give every dismissible toast a close button. Default false. */
61
+ closeButton?: boolean;
62
+ /** Moves focus to the newest toast. Default alt+t. */
63
+ hotkey?: string;
64
+ /** Names the notifications region. Default "Notifications". */
65
+ label?: string;
66
+ /** Distance from the viewport's edges in pixels. Default 24, or 16 on narrow screens. */
67
+ offset?: number;
68
+ size?: 'default' | 'compact';
69
+ class?: string;
70
+ }
71
+ /**
72
+ * Shows the toasts raised with `toast()`. Place one in the app, outside
73
+ * anything that scrolls. The newest toast sits in front with up to two
74
+ * peeking behind it, on QueuedStack's stacking values; pointing at or
75
+ * focusing the stack spreads it out and pauses the timers. A swipe toward
76
+ * the nearest edge, Escape, or the close button dismisses a toast, and the
77
+ * hotkey moves focus to the newest one.
78
+ */
79
+ export declare function Toaster(handle: Handle<ToasterProps>): () => import("remix/component").RemixElement;
@@ -0,0 +1,419 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "remix/component/jsx-runtime";
2
+ import { on, ref } from 'remix/component';
3
+ import { Button } from './basic.js';
4
+ import { Icon } from './icon.js';
5
+ import { matchesShortcut, parseShortcut } from './shortcut.js';
6
+ import { statusIcon } from './status-icon.js';
7
+ // The module-level store behind toast(). Toasters subscribe; nothing renders
8
+ // on the server, where no toast is ever raised.
9
+ let entries = [];
10
+ let counter = 0;
11
+ const listeners = new Set();
12
+ function emit() {
13
+ for (const listener of listeners)
14
+ listener();
15
+ }
16
+ function upsert(title, status, options = {}) {
17
+ const id = options.id ?? `rf-toast-${++counter}`;
18
+ const index = entries.findIndex((entry) => entry.id === id && !entry.dismissed);
19
+ const previous = index >= 0 ? entries[index] : undefined;
20
+ const entry = {
21
+ ...previous,
22
+ ...options,
23
+ id,
24
+ title,
25
+ status,
26
+ dismissed: false,
27
+ version: (previous?.version ?? 0) + 1,
28
+ };
29
+ if (previous)
30
+ entries = entries.with(index, entry);
31
+ else
32
+ entries = [entry, ...entries.filter((item) => item.id !== id)];
33
+ emit();
34
+ return id;
35
+ }
36
+ function close(id, reason) {
37
+ const closing = entries.filter((entry) => !entry.dismissed && (id === undefined || entry.id === id));
38
+ if (!closing.length)
39
+ return;
40
+ entries = entries.map((entry) => closing.includes(entry) ? { ...entry, dismissed: true } : entry);
41
+ // Without a Toaster to play the exit, a closed toast leaves at once.
42
+ if (!listeners.size)
43
+ entries = entries.filter((entry) => !entry.dismissed);
44
+ emit();
45
+ for (const entry of closing) {
46
+ if (reason === 'auto')
47
+ entry.onAutoClose?.(entry);
48
+ else
49
+ entry.onDismiss?.(entry);
50
+ }
51
+ }
52
+ function remove(id) {
53
+ const next = entries.filter((entry) => !(entry.id === id && entry.dismissed));
54
+ if (next.length === entries.length)
55
+ return;
56
+ entries = next;
57
+ emit();
58
+ }
59
+ function resolveMessage(message, value) {
60
+ return typeof message === 'function' ? message(value) : message;
61
+ }
62
+ /**
63
+ * Raises a toast in the app's `<Toaster>` and returns its id. `toast.success`,
64
+ * `toast.info`, `toast.warning`, `toast.error` and `toast.loading` set its
65
+ * status; `toast.promise` follows a promise from loading to its outcome;
66
+ * `toast.dismiss` closes one toast, or all of them.
67
+ */
68
+ export const toast = Object.assign((title, options) => upsert(title, 'default', options), {
69
+ success: (title, options) => upsert(title, 'success', options),
70
+ info: (title, options) => upsert(title, 'info', options),
71
+ warning: (title, options) => upsert(title, 'warning', options),
72
+ error: (title, options) => upsert(title, 'error', options),
73
+ // A loading toast stays without a duration of its own: the close timer
74
+ // skips loading toasts, so it starts once the toast settles.
75
+ loading: (title, options) => upsert(title, 'loading', options),
76
+ promise(promise, messages, options = {}) {
77
+ const id = upsert(messages.loading, 'loading', {
78
+ description: messages.description,
79
+ ...options,
80
+ });
81
+ const settle = (status, title) => {
82
+ // A toast closed while it waited stays closed.
83
+ if (!entries.some((entry) => entry.id === id && !entry.dismissed))
84
+ return;
85
+ upsert(title, status, { ...options, id, description: options.description });
86
+ };
87
+ Promise.resolve()
88
+ .then(() => (typeof promise === 'function' ? promise() : promise))
89
+ .then((value) => settle('success', resolveMessage(messages.success, value)), (error) => settle('error', resolveMessage(messages.error, error)));
90
+ return id;
91
+ },
92
+ dismiss: (id) => close(id, 'dismiss'),
93
+ });
94
+ // Below these, a released swipe springs back.
95
+ const swipeDistance = 45;
96
+ const swipeVelocity = 0.11;
97
+ // How long a closed toast plays its exit before it leaves the DOM.
98
+ const exitDuration = 200;
99
+ const interactive = 'button, a, input, select, textarea, [contenteditable]';
100
+ /**
101
+ * Shows the toasts raised with `toast()`. Place one in the app, outside
102
+ * anything that scrolls. The newest toast sits in front with up to two
103
+ * peeking behind it, on QueuedStack's stacking values; pointing at or
104
+ * focusing the stack spreads it out and pauses the timers. A swipe toward
105
+ * the nearest edge, Escape, or the close button dismisses a toast, and the
106
+ * hotkey moves focus to the newest one.
107
+ */
108
+ export function Toaster(handle) {
109
+ let list = null;
110
+ let hovered = false;
111
+ let focused = false;
112
+ let swipe;
113
+ let returnFocus = null;
114
+ const timers = new Map();
115
+ const leaving = new Map();
116
+ let resizeObserver;
117
+ const position = () => handle.props.position ?? 'bottom-right';
118
+ const expanded = () => Boolean(handle.props.expand) || hovered || focused;
119
+ const paused = () => hovered || focused || Boolean(swipe) || document.visibilityState === 'hidden';
120
+ const sync = () => void handle.update();
121
+ listeners.add(sync);
122
+ // Keeps each toast's close timer running, paused or cleared to match.
123
+ function schedule() {
124
+ const now = performance.now();
125
+ const live = new Set();
126
+ for (const entry of entries) {
127
+ if (entry.dismissed)
128
+ continue;
129
+ const duration = entry.duration ?? handle.props.duration ?? 4000;
130
+ if (entry.status === 'loading' || !Number.isFinite(duration))
131
+ continue;
132
+ live.add(entry.id);
133
+ let timer = timers.get(entry.id);
134
+ if (!timer || timer.version !== entry.version) {
135
+ if (timer)
136
+ clearTimeout(timer.timeout);
137
+ timer = { version: entry.version, remaining: Math.max(0, duration), started: now };
138
+ timers.set(entry.id, timer);
139
+ }
140
+ if (paused()) {
141
+ if (timer.timeout !== undefined) {
142
+ clearTimeout(timer.timeout);
143
+ timer.timeout = undefined;
144
+ timer.remaining -= now - timer.started;
145
+ }
146
+ }
147
+ else if (timer.timeout === undefined) {
148
+ timer.started = now;
149
+ timer.timeout = setTimeout(() => {
150
+ timers.delete(entry.id);
151
+ close(entry.id, 'auto');
152
+ }, timer.remaining);
153
+ }
154
+ }
155
+ for (const [id, timer] of timers) {
156
+ if (live.has(id))
157
+ continue;
158
+ clearTimeout(timer.timeout);
159
+ timers.delete(id);
160
+ }
161
+ }
162
+ // Hands focus on before a focused toast leaves.
163
+ function handOff(item) {
164
+ if (!item.contains(document.activeElement))
165
+ return;
166
+ const next = list?.querySelector(':scope > .rf-toast:not([data-removed], [data-hidden])');
167
+ if (next && next !== item)
168
+ next.focus({ preventScroll: true });
169
+ else if (returnFocus?.isConnected)
170
+ returnFocus.focus({ preventScroll: true });
171
+ else
172
+ item.blur();
173
+ }
174
+ // Writes the stack's measurements: each toast's depth, its own height and
175
+ // its offset once spread out, and the height of the toast in front.
176
+ function layout() {
177
+ if (!list)
178
+ return;
179
+ const items = Array.from(list.querySelectorAll(':scope > .rf-toast:not([data-removed])'));
180
+ const style = getComputedStyle(list);
181
+ const gap = parseFloat(style.getPropertyValue('--rf-stack-gap')) || 5;
182
+ const peek = parseFloat(style.getPropertyValue('--rf-stack-peek')) || 6;
183
+ const visible = Math.max(1, handle.props.visibleToasts ?? 3);
184
+ let offset = 0;
185
+ let spread = 0;
186
+ for (const [depth, item] of items.entries()) {
187
+ const height = item.querySelector('.rf-toast-inner')?.offsetHeight ?? 0;
188
+ item.style.setProperty('--rf-toast-depth', String(depth));
189
+ item.style.setProperty('--rf-toast-height', `${height}px`);
190
+ item.style.setProperty('--rf-toast-offset', `${offset}px`);
191
+ item.toggleAttribute('data-front', depth === 0);
192
+ item.toggleAttribute('data-hidden', depth >= visible);
193
+ if (depth < visible)
194
+ spread = offset + height;
195
+ offset += height + gap;
196
+ }
197
+ const front = items[0]?.querySelector('.rf-toast-inner')?.offsetHeight ?? 0;
198
+ const shown = Math.min(items.length, visible);
199
+ const edge = handle.props.offset;
200
+ if (edge === undefined)
201
+ list.style.removeProperty('--rf-toaster-offset');
202
+ else
203
+ list.style.setProperty('--rf-toaster-offset', `${edge}px`);
204
+ list.style.setProperty('--rf-toast-front-height', `${front}px`);
205
+ list.style.setProperty('--rf-toaster-piled-height', `${front + Math.max(0, shown - 1) * peek}px`);
206
+ list.style.setProperty('--rf-toaster-spread-height', `${spread}px`);
207
+ }
208
+ function afterRender() {
209
+ if (!list)
210
+ return;
211
+ for (const item of list.querySelectorAll(':scope > .rf-toast')) {
212
+ const inner = item.querySelector('.rf-toast-inner');
213
+ if (inner)
214
+ resizeObserver?.observe(inner);
215
+ const id = item.dataset.toastId;
216
+ if (item.hasAttribute('data-removed') && !leaving.has(id)) {
217
+ handOff(item);
218
+ leaving.set(id, setTimeout(() => {
219
+ leaving.delete(id);
220
+ remove(id);
221
+ }, exitDuration));
222
+ }
223
+ }
224
+ layout();
225
+ schedule();
226
+ }
227
+ function endSwipe(event, item) {
228
+ if (!swipe || swipe.pointer !== event.pointerId)
229
+ return;
230
+ const current = swipe;
231
+ swipe = undefined;
232
+ item.removeAttribute('data-swiping');
233
+ const [y, x] = position().split('-');
234
+ const signY = y === 'bottom' ? 1 : -1;
235
+ const signX = x === 'right' ? 1 : x === 'left' ? -1 : 0;
236
+ const travel = current.axis === 'x' ? current.dx * signX : current.axis === 'y' ? current.dy * signY : 0;
237
+ const elapsed = Math.max(1, performance.now() - current.time);
238
+ const entry = entries.find((item) => item.id === current.id);
239
+ if (entry && travel > 0 && (travel >= swipeDistance || travel / elapsed > swipeVelocity)) {
240
+ // Carry on in the swipe's direction as it leaves.
241
+ const width = item.offsetWidth;
242
+ const height = item.offsetHeight;
243
+ item.dataset.swipeOut = current.axis;
244
+ if (current.axis === 'x')
245
+ item.style.setProperty('--rf-swipe-x', `${signX * width}px`);
246
+ else
247
+ item.style.setProperty('--rf-swipe-y', `${signY * height}px`);
248
+ close(entry.id, 'dismiss');
249
+ }
250
+ else {
251
+ item.style.setProperty('--rf-swipe-x', '0px');
252
+ item.style.setProperty('--rf-swipe-y', '0px');
253
+ void handle.update();
254
+ }
255
+ }
256
+ const listMix = ref((element, signal) => {
257
+ if (!(element instanceof HTMLOListElement))
258
+ return;
259
+ list = element;
260
+ const options = { signal };
261
+ resizeObserver = new ResizeObserver(() => layout());
262
+ element.addEventListener('pointerenter', (event) => {
263
+ if (event.pointerType === 'touch')
264
+ return;
265
+ hovered = true;
266
+ void handle.update();
267
+ }, options);
268
+ element.addEventListener('pointerleave', () => {
269
+ if (!hovered)
270
+ return;
271
+ hovered = false;
272
+ void handle.update();
273
+ }, options);
274
+ element.addEventListener('focusin', (event) => {
275
+ const from = event.relatedTarget;
276
+ if (from instanceof HTMLElement && !element.contains(from))
277
+ returnFocus = from;
278
+ if (focused)
279
+ return;
280
+ focused = true;
281
+ void handle.update();
282
+ }, options);
283
+ element.addEventListener('focusout', (event) => {
284
+ const next = event.relatedTarget;
285
+ if (next instanceof Node && element.contains(next))
286
+ return;
287
+ // A toast that leaves with focus inside hands it on in the same frame.
288
+ queueMicrotask(() => {
289
+ if (element.contains(document.activeElement))
290
+ return;
291
+ focused = false;
292
+ void handle.update();
293
+ });
294
+ }, options);
295
+ element.ownerDocument.addEventListener('keydown', (event) => {
296
+ if (!matchesShortcut(event, parseShortcut(handle.props.hotkey ?? 'alt+t')))
297
+ return;
298
+ const front = element.querySelector(':scope > .rf-toast:not([data-removed])');
299
+ if (!front)
300
+ return;
301
+ event.preventDefault();
302
+ const active = document.activeElement;
303
+ if (active instanceof HTMLElement && !element.contains(active))
304
+ returnFocus = active;
305
+ front.focus();
306
+ }, options);
307
+ element.ownerDocument.addEventListener('visibilitychange', () => schedule(), options);
308
+ signal.addEventListener('abort', () => {
309
+ resizeObserver?.disconnect();
310
+ if (list === element)
311
+ list = null;
312
+ }, { once: true });
313
+ });
314
+ handle.signal.addEventListener('abort', () => {
315
+ listeners.delete(sync);
316
+ // With no Toaster left to play their exit, closed toasts leave now.
317
+ if (!listeners.size)
318
+ entries = entries.filter((entry) => !entry.dismissed);
319
+ for (const timer of timers.values())
320
+ clearTimeout(timer.timeout);
321
+ for (const timeout of leaving.values())
322
+ clearTimeout(timeout);
323
+ }, { once: true });
324
+ function renderToast(entry) {
325
+ const dismissible = entry.dismissible ?? true;
326
+ const icon = entry.icon ??
327
+ (entry.status === 'loading' ? (_jsx("svg", { class: "rf-toast-spinner", viewBox: "0 0 16 16", fill: "none", children: _jsx("g", { children: _jsx("circle", { cx: "8", cy: "8", r: "6.25", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", pathLength: "100", transform: "rotate(-90 8 8)" }) }) })) : entry.status === 'default' ? undefined : (statusIcon(entry.status)));
328
+ return (_jsx("li", { class: "rf-toast", tabIndex: 0, "data-toast-id": entry.id, "data-status": entry.status, "data-removed": entry.dismissed || undefined, "data-dismissible": dismissible ? undefined : 'false', "aria-atomic": "true", mix: [
329
+ on('keydown', (event) => {
330
+ if (event.key !== 'Escape' || !dismissible || entry.dismissed)
331
+ return;
332
+ event.preventDefault();
333
+ event.stopPropagation();
334
+ close(entry.id, 'dismiss');
335
+ }),
336
+ on('pointerdown', (event) => {
337
+ if (!dismissible || entry.dismissed || event.button !== 0)
338
+ return;
339
+ if (event.target instanceof Element && event.target.closest(interactive))
340
+ return;
341
+ swipe = {
342
+ id: entry.id,
343
+ pointer: event.pointerId,
344
+ x: event.clientX,
345
+ y: event.clientY,
346
+ time: performance.now(),
347
+ dx: 0,
348
+ dy: 0,
349
+ };
350
+ try {
351
+ event.currentTarget.setPointerCapture(event.pointerId);
352
+ }
353
+ catch {
354
+ // Synthetic pointers have nothing to capture.
355
+ }
356
+ void handle.update();
357
+ }),
358
+ on('pointermove', (event) => {
359
+ if (!swipe || swipe.pointer !== event.pointerId || swipe.id !== entry.id)
360
+ return;
361
+ const item = event.currentTarget;
362
+ const dx = event.clientX - swipe.x;
363
+ const dy = event.clientY - swipe.y;
364
+ if (!swipe.axis) {
365
+ if (Math.max(Math.abs(dx), Math.abs(dy)) < 2)
366
+ return;
367
+ swipe.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
368
+ item.setAttribute('data-swiping', '');
369
+ }
370
+ const [y, x] = position().split('-');
371
+ const toward = swipe.axis === 'x'
372
+ ? x === 'right'
373
+ ? 1
374
+ : x === 'left'
375
+ ? -1
376
+ : 0
377
+ : y === 'bottom'
378
+ ? 1
379
+ : -1;
380
+ const delta = swipe.axis === 'x' ? dx : dy;
381
+ // Toward the edge it follows the pointer; away from it, it resists.
382
+ const moved = Math.sign(delta) === toward ? delta : delta / (1 + Math.abs(delta) / 24);
383
+ swipe.dx = swipe.axis === 'x' ? moved : 0;
384
+ swipe.dy = swipe.axis === 'y' ? moved : 0;
385
+ item.style.setProperty('--rf-swipe-x', `${Math.round(swipe.dx)}px`);
386
+ item.style.setProperty('--rf-swipe-y', `${Math.round(swipe.dy)}px`);
387
+ }),
388
+ on('pointerup', (event) => endSwipe(event, event.currentTarget)),
389
+ on('pointercancel', (event) => endSwipe(event, event.currentTarget)),
390
+ ], children: _jsxs("div", { class: "rf-toast-inner", children: [icon && (_jsx("span", { class: "rf-toast-icon", "aria-hidden": "true", children: icon })), _jsxs("div", { class: "rf-toast-content", children: [_jsx("div", { class: "rf-toast-title", children: entry.title }), entry.description && _jsx("div", { class: "rf-toast-description", children: entry.description })] }), entry.action && (_jsx(Button, { variant: "secondary", size: "compact", class: "rf-toast-action", mix: on('click', (event) => {
391
+ entry.action?.onClick(event);
392
+ if (!event.defaultPrevented)
393
+ close(entry.id, 'dismiss');
394
+ }), children: entry.action.label })), handle.props.closeButton && dismissible && (_jsx("button", { type: "button", class: "rf-toast-close", "aria-label": "Close", mix: on('click', () => close(entry.id, 'dismiss')), children: _jsx(Icon, { name: "x" }) }))] }) }, entry.id));
395
+ }
396
+ return () => {
397
+ handle.queueTask(afterRender);
398
+ const { label = 'Notifications', size, hotkey = 'alt+t' } = handle.props;
399
+ return (_jsx("section", { class: handle.props.class ? `rf-toaster-region ${handle.props.class}` : 'rf-toaster-region', "aria-label": label, "aria-keyshortcuts": ariaShortcut(hotkey), "aria-live": "polite", "aria-relevant": "additions text", "aria-atomic": "false", tabIndex: -1, "data-density": size, children: _jsx("ol", { class: "rf-toaster", "data-position": position(), "data-expanded": expanded() || undefined, mix: listMix, children: entries.map(renderToast) }) }));
400
+ };
401
+ }
402
+ // alt+t reads as Alt+T, the form aria-keyshortcuts expects.
403
+ function ariaShortcut(hotkey) {
404
+ return hotkey
405
+ .split('+')
406
+ .map((part) => {
407
+ const lower = part.trim().toLowerCase();
408
+ if (lower === 'mod' || lower === 'ctrl' || lower === 'control')
409
+ return 'Control';
410
+ if (lower === 'cmd' || lower === 'meta')
411
+ return 'Meta';
412
+ if (lower === 'option' || lower === 'opt')
413
+ return 'Alt';
414
+ return lower.length === 1
415
+ ? lower.toUpperCase()
416
+ : lower.charAt(0).toUpperCase() + lower.slice(1);
417
+ })
418
+ .join('+');
419
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtzll/reflow",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -13,13 +13,29 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "default": "./dist/index.js"
15
15
  },
16
+ "./highlight": {
17
+ "types": "./dist/highlight.d.ts",
18
+ "default": "./dist/highlight.js"
19
+ },
16
20
  "./styles.css": "./dist/styles.css"
17
21
  },
18
22
  "peerDependencies": {
19
- "remix": "3.2.0"
23
+ "@shikijs/transformers": "^4.5.0",
24
+ "remix": "3.2.0",
25
+ "shiki": "^4.5.0"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "@shikijs/transformers": {
29
+ "optional": true
30
+ },
31
+ "shiki": {
32
+ "optional": true
33
+ }
20
34
  },
21
35
  "devDependencies": {
22
- "remix": "3.2.0"
36
+ "@shikijs/transformers": "4.5.0",
37
+ "remix": "3.2.0",
38
+ "shiki": "4.5.0"
23
39
  },
24
40
  "dependencies": {
25
41
  "@remix-run/ui": "0.13.1"