@rtzll/reflow 0.5.0 → 0.7.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.
- package/dist/avatar.css +119 -0
- package/dist/avatar.d.ts +42 -0
- package/dist/avatar.js +74 -0
- package/dist/basic.d.ts +1 -20
- package/dist/basic.js +1 -20
- package/dist/carousel.css +67 -0
- package/dist/carousel.d.ts +36 -0
- package/dist/carousel.js +277 -0
- package/dist/chat-layout.css +124 -0
- package/dist/chat-layout.d.ts +24 -0
- package/dist/chat-layout.js +108 -0
- package/dist/citation.css +121 -0
- package/dist/citation.d.ts +35 -0
- package/dist/citation.js +45 -0
- package/dist/code-block.css +292 -0
- package/dist/code-block.d.ts +59 -0
- package/dist/code-block.js +117 -0
- package/dist/colors.d.ts +21 -0
- package/dist/colors.js +21 -0
- package/dist/compositions.css +51 -0
- package/dist/compositions.d.ts +8 -1
- package/dist/compositions.js +8 -0
- package/dist/drawer.css +33 -37
- package/dist/highlight.d.ts +36 -0
- package/dist/highlight.js +134 -0
- package/dist/hover-card.d.ts +6 -1
- package/dist/hover-card.js +19 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/queued-stack.js +50 -1
- package/dist/styles.css +5 -0
- package/dist/toaster.js +3 -2
- package/package.json +19 -3
package/dist/drawer.css
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
/* Drawer: a modal panel attached to one edge of the viewport.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/* Drawer: a modal panel attached to one edge of the viewport. The panel
|
|
2
|
+
and the backdrop share one motion, in and out and back from a drag: a
|
|
3
|
+
long ease that starts fast and settles gently, with no overshoot, so the
|
|
4
|
+
panel never lifts off its edge and the backdrop darkens as it arrives.
|
|
5
|
+
While dragged the panel follows --rf-drawer-drag, the distance toward its
|
|
6
|
+
edge, and the backdrop lightens with --rf-drawer-progress. A strip of
|
|
7
|
+
panel color past the edge covers the gap when a drag pulls it the other
|
|
8
|
+
way. */
|
|
7
9
|
.rf-drawer {
|
|
10
|
+
--rf-drawer-motion: 500ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
8
11
|
--rf-drawer-bg: var(--rf-surface-5);
|
|
9
12
|
--rf-drawer-drag: 0px;
|
|
10
13
|
--rf-drawer-closed: 0 100%;
|
|
@@ -32,24 +35,12 @@
|
|
|
32
35
|
font: var(--rf-text-body) / var(--rf-leading-body) var(--rf-font);
|
|
33
36
|
translate: var(--rf-drawer-closed);
|
|
34
37
|
transition:
|
|
35
|
-
translate var(--rf-
|
|
36
|
-
overlay
|
|
37
|
-
display
|
|
38
|
+
translate var(--rf-drawer-motion),
|
|
39
|
+
overlay var(--rf-drawer-motion) allow-discrete,
|
|
40
|
+
display var(--rf-drawer-motion) allow-discrete;
|
|
38
41
|
}
|
|
39
42
|
.rf-drawer[open] {
|
|
40
43
|
translate: 0 var(--rf-drawer-drag);
|
|
41
|
-
transition:
|
|
42
|
-
translate var(--rf-spring-moderate),
|
|
43
|
-
overlay 160ms allow-discrete,
|
|
44
|
-
display 160ms allow-discrete;
|
|
45
|
-
}
|
|
46
|
-
@starting-style {
|
|
47
|
-
.rf-drawer[open] {
|
|
48
|
-
translate: var(--rf-drawer-closed);
|
|
49
|
-
}
|
|
50
|
-
.rf-drawer[open]::backdrop {
|
|
51
|
-
opacity: 0;
|
|
52
|
-
}
|
|
53
44
|
}
|
|
54
45
|
|
|
55
46
|
/* Top and bottom drawers keep their content in a readable column. */
|
|
@@ -100,23 +91,32 @@
|
|
|
100
91
|
background: rgb(0 0 0 / 40%);
|
|
101
92
|
opacity: 0;
|
|
102
93
|
transition:
|
|
103
|
-
opacity
|
|
104
|
-
overlay
|
|
105
|
-
display
|
|
94
|
+
opacity 500ms cubic-bezier(0.32, 0.72, 0, 1),
|
|
95
|
+
overlay 500ms cubic-bezier(0.32, 0.72, 0, 1) allow-discrete,
|
|
96
|
+
display 500ms cubic-bezier(0.32, 0.72, 0, 1) allow-discrete;
|
|
106
97
|
}
|
|
107
98
|
[data-theme='dark'] .rf-drawer::backdrop {
|
|
108
99
|
background: rgb(0 0 0 / 70%);
|
|
109
100
|
}
|
|
110
101
|
.rf-drawer[open]::backdrop {
|
|
111
|
-
opacity:
|
|
112
|
-
transition:
|
|
113
|
-
opacity var(--rf-spring-moderate),
|
|
114
|
-
overlay 160ms allow-discrete,
|
|
115
|
-
display 160ms allow-discrete;
|
|
102
|
+
opacity: 1;
|
|
116
103
|
}
|
|
117
104
|
.rf-drawer[data-dragging]::backdrop {
|
|
105
|
+
opacity: calc(1 - var(--rf-drawer-progress, 0));
|
|
118
106
|
transition: none;
|
|
119
107
|
}
|
|
108
|
+
/* Where the panel and backdrop start from as the drawer opens. Starting
|
|
109
|
+
styles cascade with the rest, so these follow the open rules above and
|
|
110
|
+
match the per-side ones in specificity, or those would win and the
|
|
111
|
+
drawer would appear without its slide. */
|
|
112
|
+
@starting-style {
|
|
113
|
+
.rf-drawer[data-side][open] {
|
|
114
|
+
translate: var(--rf-drawer-closed);
|
|
115
|
+
}
|
|
116
|
+
.rf-drawer[data-side][open]::backdrop {
|
|
117
|
+
opacity: 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
120
|
|
|
121
121
|
.rf-drawer-handle {
|
|
122
122
|
inline-size: 40px;
|
|
@@ -193,19 +193,15 @@
|
|
|
193
193
|
translate: none;
|
|
194
194
|
opacity: 0;
|
|
195
195
|
transition:
|
|
196
|
-
opacity var(--rf-
|
|
197
|
-
overlay
|
|
198
|
-
display
|
|
196
|
+
opacity var(--rf-drawer-motion),
|
|
197
|
+
overlay var(--rf-drawer-motion) allow-discrete,
|
|
198
|
+
display var(--rf-drawer-motion) allow-discrete;
|
|
199
199
|
}
|
|
200
200
|
.rf-drawer[open] {
|
|
201
201
|
opacity: 1;
|
|
202
|
-
transition:
|
|
203
|
-
opacity var(--rf-spring-moderate),
|
|
204
|
-
overlay 160ms allow-discrete,
|
|
205
|
-
display 160ms allow-discrete;
|
|
206
202
|
}
|
|
207
203
|
@starting-style {
|
|
208
|
-
.rf-drawer[open] {
|
|
204
|
+
.rf-drawer[data-side][open] {
|
|
209
205
|
translate: none;
|
|
210
206
|
opacity: 0;
|
|
211
207
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rtzll/reflow/highlight`: Shiki behind one function, for CodeBlock.
|
|
3
|
+
*
|
|
4
|
+
* This entry is optional. It needs the `shiki` and `@shikijs/transformers`
|
|
5
|
+
* peer dependencies, and nothing else in Reflow imports it, so an app that
|
|
6
|
+
* never imports it never installs or loads Shiki.
|
|
7
|
+
*/
|
|
8
|
+
import { type BundledTheme, type ShikiTransformer, type ThemeRegistrationAny } from 'shiki';
|
|
9
|
+
import type { HighlightedCode } from './code-block.tsx';
|
|
10
|
+
export type { CodeLine, CodeToken, HighlightedCode } from './code-block.tsx';
|
|
11
|
+
type Theme = BundledTheme | ThemeRegistrationAny;
|
|
12
|
+
export interface HighlightOptions {
|
|
13
|
+
/** Any language Shiki bundles, or one of its aliases. Unknown languages render as plain text. Default text. */
|
|
14
|
+
lang?: string;
|
|
15
|
+
/** A Shiki theme, by name or definition. Defaults to Reflow's, which colours tokens with `--rf-code-token-*` variables. */
|
|
16
|
+
theme?: Theme;
|
|
17
|
+
/** A light and a dark theme; the block follows the page's color scheme between them. */
|
|
18
|
+
themes?: {
|
|
19
|
+
light: Theme;
|
|
20
|
+
dark: Theme;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Reads `// [!code highlight]`, `[!code ++]`, `[!code --]`, `[!code focus]`,
|
|
24
|
+
* `[!code error]`, `[!code warning]` and `[!code word:name]` comments into
|
|
25
|
+
* line and word marks, and removes them from the code. Default true.
|
|
26
|
+
*/
|
|
27
|
+
notation?: boolean;
|
|
28
|
+
/** More Shiki transformers, run after the notation ones. */
|
|
29
|
+
transformers?: ShikiTransformer[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Highlights code for CodeBlock. Call it where the code is known: in a route
|
|
33
|
+
* handler, so the page arrives highlighted and the browser loads nothing, or
|
|
34
|
+
* in the browser for code that arrives later, such as a streamed reply.
|
|
35
|
+
*/
|
|
36
|
+
export declare function highlight(code: string, options?: HighlightOptions): Promise<HighlightedCode>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rtzll/reflow/highlight`: Shiki behind one function, for CodeBlock.
|
|
3
|
+
*
|
|
4
|
+
* This entry is optional. It needs the `shiki` and `@shikijs/transformers`
|
|
5
|
+
* peer dependencies, and nothing else in Reflow imports it, so an app that
|
|
6
|
+
* never imports it never installs or loads Shiki.
|
|
7
|
+
*/
|
|
8
|
+
import { bundledLanguages, bundledLanguagesAlias, createCssVariablesTheme, createHighlighter, createJavaScriptRegexEngine, } from 'shiki';
|
|
9
|
+
import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, } from '@shikijs/transformers';
|
|
10
|
+
// Reflow's theme: every colour is a CSS variable, set in code-block.css for
|
|
11
|
+
// the light and dark themes, so highlighted code follows the page.
|
|
12
|
+
const reflowTheme = createCssVariablesTheme({
|
|
13
|
+
name: 'reflow',
|
|
14
|
+
variablePrefix: '--rf-code-',
|
|
15
|
+
fontStyle: true,
|
|
16
|
+
});
|
|
17
|
+
let shared;
|
|
18
|
+
// One highlighter for the process or page, with the JavaScript regex engine,
|
|
19
|
+
// which needs no WebAssembly. Languages and themes load as they are first used.
|
|
20
|
+
function highlighter() {
|
|
21
|
+
shared ??= createHighlighter({
|
|
22
|
+
themes: [reflowTheme],
|
|
23
|
+
langs: [],
|
|
24
|
+
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
|
25
|
+
});
|
|
26
|
+
return shared;
|
|
27
|
+
}
|
|
28
|
+
function bundled(lang) {
|
|
29
|
+
return lang in bundledLanguages || lang in bundledLanguagesAlias;
|
|
30
|
+
}
|
|
31
|
+
function themeName(theme) {
|
|
32
|
+
return typeof theme === 'string' ? theme : (theme.name ?? 'custom');
|
|
33
|
+
}
|
|
34
|
+
function classes(node) {
|
|
35
|
+
const value = node.properties?.class ?? node.properties?.className;
|
|
36
|
+
return Array.isArray(value) ? value.join(' ') : typeof value === 'string' ? value : '';
|
|
37
|
+
}
|
|
38
|
+
// Flattens a line into runs of text, each with the innermost style and
|
|
39
|
+
// every class around it, such as a token inside a highlighted word.
|
|
40
|
+
function tokens(node, style = '', classList = []) {
|
|
41
|
+
if (node.type === 'text') {
|
|
42
|
+
if (!node.value)
|
|
43
|
+
return [];
|
|
44
|
+
const token = { content: node.value };
|
|
45
|
+
if (style)
|
|
46
|
+
token.style = style;
|
|
47
|
+
if (classList.length)
|
|
48
|
+
token.class = classList.join(' ');
|
|
49
|
+
return [token];
|
|
50
|
+
}
|
|
51
|
+
const own = typeof node.properties?.style === 'string' ? node.properties.style : '';
|
|
52
|
+
const name = classes(node);
|
|
53
|
+
const nested = name ? [...classList, name] : classList;
|
|
54
|
+
return (node.children ?? []).flatMap((child) => tokens(child, own || style, nested));
|
|
55
|
+
}
|
|
56
|
+
function trimEnd(list) {
|
|
57
|
+
while (list.length) {
|
|
58
|
+
const last = list[list.length - 1];
|
|
59
|
+
last.content = last.content.replace(/[ \t]+$/, '');
|
|
60
|
+
if (last.content)
|
|
61
|
+
return;
|
|
62
|
+
list.pop();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Highlights code for CodeBlock. Call it where the code is known: in a route
|
|
67
|
+
* handler, so the page arrives highlighted and the browser loads nothing, or
|
|
68
|
+
* in the browser for code that arrives later, such as a streamed reply.
|
|
69
|
+
*/
|
|
70
|
+
export async function highlight(code, options = {}) {
|
|
71
|
+
const instance = await highlighter();
|
|
72
|
+
const requested = options.lang ?? 'text';
|
|
73
|
+
const lang = bundled(requested) ? requested : 'text';
|
|
74
|
+
if (lang !== 'text' && !instance.getLoadedLanguages().includes(lang))
|
|
75
|
+
await instance.loadLanguage(lang);
|
|
76
|
+
const themes = options.themes ? [options.themes.light, options.themes.dark] : [options.theme];
|
|
77
|
+
for (const theme of themes)
|
|
78
|
+
if (theme && !instance.getLoadedThemes().includes(themeName(theme)))
|
|
79
|
+
await instance.loadTheme(theme);
|
|
80
|
+
const transformers = [
|
|
81
|
+
...(options.notation === false
|
|
82
|
+
? []
|
|
83
|
+
: [
|
|
84
|
+
transformerNotationHighlight(),
|
|
85
|
+
transformerNotationDiff(),
|
|
86
|
+
transformerNotationFocus(),
|
|
87
|
+
transformerNotationErrorLevel(),
|
|
88
|
+
transformerNotationWordHighlight(),
|
|
89
|
+
]),
|
|
90
|
+
...(options.transformers ?? []),
|
|
91
|
+
];
|
|
92
|
+
const root = instance.codeToHast(code, options.themes
|
|
93
|
+
? {
|
|
94
|
+
lang,
|
|
95
|
+
themes: {
|
|
96
|
+
light: themeName(options.themes.light),
|
|
97
|
+
dark: themeName(options.themes.dark),
|
|
98
|
+
},
|
|
99
|
+
defaultColor: false,
|
|
100
|
+
transformers,
|
|
101
|
+
}
|
|
102
|
+
: { lang, theme: options.theme ? themeName(options.theme) : 'reflow', transformers });
|
|
103
|
+
const pre = root.children?.find((node) => node.tagName === 'pre');
|
|
104
|
+
const body = pre?.children?.find((node) => node.tagName === 'code');
|
|
105
|
+
const lines = (body?.children ?? [])
|
|
106
|
+
.filter((node) => node.type === 'element')
|
|
107
|
+
.map((node) => {
|
|
108
|
+
const mark = classes(node)
|
|
109
|
+
.split(/\s+/)
|
|
110
|
+
.filter((name) => name && name !== 'line')
|
|
111
|
+
.join(' ');
|
|
112
|
+
const line = { tokens: tokens({ ...node, properties: {} }) };
|
|
113
|
+
// Removing a notation comment leaves the space before it.
|
|
114
|
+
if (options.notation !== false)
|
|
115
|
+
trimEnd(line.tokens);
|
|
116
|
+
if (mark)
|
|
117
|
+
line.class = mark;
|
|
118
|
+
return line;
|
|
119
|
+
});
|
|
120
|
+
const flags = classes(pre ?? { type: 'element' })
|
|
121
|
+
.split(/\s+/)
|
|
122
|
+
.filter((name) => name.startsWith('has-'));
|
|
123
|
+
const style = typeof pre?.properties?.style === 'string' ? pre.properties.style : undefined;
|
|
124
|
+
const result = {
|
|
125
|
+
lang: requested === 'text' || lang === 'text' ? 'text' : requested,
|
|
126
|
+
code: lines.map((line) => line.tokens.map((token) => token.content).join('')).join('\n'),
|
|
127
|
+
lines,
|
|
128
|
+
flags,
|
|
129
|
+
};
|
|
130
|
+
// Reflow's own theme takes its colours from CSS; others bring theirs inline.
|
|
131
|
+
if (style && (options.theme || options.themes))
|
|
132
|
+
result.style = style;
|
|
133
|
+
return result;
|
|
134
|
+
}
|
package/dist/hover-card.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface HoverCardProps {
|
|
|
27
27
|
* supplementary: the trigger still works on its own, a tap on a touch screen
|
|
28
28
|
* follows it, and the card never takes focus or locks scrolling. The pointer
|
|
29
29
|
* can cross into the card and use its links; it closes once the pointer has
|
|
30
|
-
* left both, or
|
|
30
|
+
* left both, on Escape, or when the page scrolls. It shares Popover's
|
|
31
|
+
* surface and motion.
|
|
32
|
+
*
|
|
33
|
+
* The card is a span, so a trigger can sit inside a paragraph. Keep its
|
|
34
|
+
* content to phrasing elements there, such as span and a, since a div or a
|
|
35
|
+
* list would end the paragraph when the browser parses server-rendered HTML.
|
|
31
36
|
*/
|
|
32
37
|
export declare function HoverCard(handle: Handle<HoverCardProps>): () => import("remix/component").RemixElement;
|
package/dist/hover-card.js
CHANGED
|
@@ -7,7 +7,12 @@ import { anchor } from '@remix-run/ui/anchor';
|
|
|
7
7
|
* supplementary: the trigger still works on its own, a tap on a touch screen
|
|
8
8
|
* follows it, and the card never takes focus or locks scrolling. The pointer
|
|
9
9
|
* can cross into the card and use its links; it closes once the pointer has
|
|
10
|
-
* left both, or
|
|
10
|
+
* left both, on Escape, or when the page scrolls. It shares Popover's
|
|
11
|
+
* surface and motion.
|
|
12
|
+
*
|
|
13
|
+
* The card is a span, so a trigger can sit inside a paragraph. Keep its
|
|
14
|
+
* content to phrasing elements there, such as span and a, since a div or a
|
|
15
|
+
* list would end the paragraph when the browser parses server-rendered HTML.
|
|
11
16
|
*/
|
|
12
17
|
export function HoverCard(handle) {
|
|
13
18
|
let trigger = null;
|
|
@@ -84,6 +89,18 @@ export function HoverCard(handle) {
|
|
|
84
89
|
return;
|
|
85
90
|
setOpen(false);
|
|
86
91
|
}, options);
|
|
92
|
+
// The card is placed by script, so while the trigger scrolls it trails a
|
|
93
|
+
// frame behind. A preview belongs to where the pointer rests, so when the
|
|
94
|
+
// page, or anything holding the trigger, scrolls, it closes at once. Other
|
|
95
|
+
// scrollers, such as a carousel elsewhere, leave it be.
|
|
96
|
+
element.ownerDocument.addEventListener('scroll', (event) => {
|
|
97
|
+
if (!isOpen())
|
|
98
|
+
return;
|
|
99
|
+
const source = event.target;
|
|
100
|
+
const movesTrigger = source === element.ownerDocument || (source instanceof Node && source.contains(element));
|
|
101
|
+
if (movesTrigger)
|
|
102
|
+
setOpen(false);
|
|
103
|
+
}, { capture: true, passive: true, signal });
|
|
87
104
|
signal.addEventListener('abort', () => {
|
|
88
105
|
clearTimeout(openTimer);
|
|
89
106
|
clearTimeout(closeTimer);
|
|
@@ -114,6 +131,6 @@ export function HoverCard(handle) {
|
|
|
114
131
|
});
|
|
115
132
|
return () => {
|
|
116
133
|
handle.queueTask(sync);
|
|
117
|
-
return (_jsxs(_Fragment, { children: [handle.props.renderTrigger({ mix: triggerMix }), _jsx("
|
|
134
|
+
return (_jsxs(_Fragment, { children: [handle.props.renderTrigger({ mix: triggerMix }), _jsx("span", { class: "rf-menu-surface rf-popover-surface rf-hover-card", popover: "manual", "data-density": handle.props.size, mix: cardMix, children: handle.props.children })] }));
|
|
118
135
|
};
|
|
119
136
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export * from './popover.tsx';
|
|
|
15
15
|
export * from './hover-card.tsx';
|
|
16
16
|
export * from './toaster.tsx';
|
|
17
17
|
export * from './drawer.tsx';
|
|
18
|
+
export * from './code-block.tsx';
|
|
19
|
+
export * from './citation.tsx';
|
|
20
|
+
export * from './avatar.tsx';
|
|
21
|
+
export * from './chat-layout.tsx';
|
|
22
|
+
export * from './carousel.tsx';
|
|
18
23
|
export * from './systems.ts';
|
|
19
24
|
export * from './queued-stack.tsx';
|
|
20
25
|
export * from './scroll-area.tsx';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,11 @@ export * from './popover.js';
|
|
|
15
15
|
export * from './hover-card.js';
|
|
16
16
|
export * from './toaster.js';
|
|
17
17
|
export * from './drawer.js';
|
|
18
|
+
export * from './code-block.js';
|
|
19
|
+
export * from './citation.js';
|
|
20
|
+
export * from './avatar.js';
|
|
21
|
+
export * from './chat-layout.js';
|
|
22
|
+
export * from './carousel.js';
|
|
18
23
|
export * from './systems.js';
|
|
19
24
|
export * from './queued-stack.js';
|
|
20
25
|
export * from './scroll-area.js';
|
package/dist/queued-stack.js
CHANGED
|
@@ -12,6 +12,41 @@ export function QueuedStack(handle) {
|
|
|
12
12
|
list = node;
|
|
13
13
|
});
|
|
14
14
|
const sending = new Set();
|
|
15
|
+
// The action that had focus when its message moved or left, to hand focus
|
|
16
|
+
// on to once the queue has re-rendered.
|
|
17
|
+
let refocus;
|
|
18
|
+
function remember(action, id, index) {
|
|
19
|
+
if (list?.contains(document.activeElement))
|
|
20
|
+
refocus = { action, id, index };
|
|
21
|
+
}
|
|
22
|
+
function restoreFocus() {
|
|
23
|
+
const target = refocus;
|
|
24
|
+
if (!target || !list)
|
|
25
|
+
return;
|
|
26
|
+
const rows = Array.from(list.children);
|
|
27
|
+
const row = rows.find((item) => item.dataset.queueId === target.id);
|
|
28
|
+
let button;
|
|
29
|
+
if (target.action === 'up' || target.action === 'down') {
|
|
30
|
+
// The moved message keeps focus, on the other arrow if this one ran out.
|
|
31
|
+
if (!row)
|
|
32
|
+
return;
|
|
33
|
+
if (rows.indexOf(row) === target.index)
|
|
34
|
+
return;
|
|
35
|
+
button = row.querySelector(`[data-queue-action="${target.action}"]`);
|
|
36
|
+
if (button?.disabled)
|
|
37
|
+
button = row.querySelector(`[data-queue-action="${target.action === 'up' ? 'down' : 'up'}"]`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// The message that takes the sent or removed one's place, or the one before it.
|
|
41
|
+
if (row)
|
|
42
|
+
return;
|
|
43
|
+
const next = rows[Math.min(target.index, rows.length - 1)];
|
|
44
|
+
button = next?.querySelector(`[data-queue-action="${target.action}"]`);
|
|
45
|
+
}
|
|
46
|
+
refocus = undefined;
|
|
47
|
+
// It takes the place of the button that had focus, so nothing needs scrolling.
|
|
48
|
+
button?.focus({ preventScroll: true });
|
|
49
|
+
}
|
|
15
50
|
async function send(id) {
|
|
16
51
|
if (sending.has(id))
|
|
17
52
|
return;
|
|
@@ -51,6 +86,8 @@ export function QueuedStack(handle) {
|
|
|
51
86
|
const { messages, onRemove, onEdit, label = 'Queued messages' } = handle.props;
|
|
52
87
|
if (!messages.length)
|
|
53
88
|
expanded = false;
|
|
89
|
+
if (refocus)
|
|
90
|
+
handle.queueTask(restoreFocus);
|
|
54
91
|
return (_jsxs("div", { class: "rf-queue-wrap", hidden: !messages.length, "data-expanded": expanded || undefined, children: [messages.length > 1 && (_jsx("button", { type: "button", class: "rf-queue-toggle", "aria-expanded": expanded, "aria-controls": handle.id, mix: on('click', () => {
|
|
55
92
|
expanded = !expanded;
|
|
56
93
|
handle.update();
|
|
@@ -133,6 +170,18 @@ export function QueuedStack(handle) {
|
|
|
133
170
|
expanded = true;
|
|
134
171
|
handle.update();
|
|
135
172
|
}),
|
|
136
|
-
], children: [_jsx("span", { class: "rf-queue-content", children: message.content }), _jsxs("div", { class: "rf-queue-actions", children: [_jsx(Button, { size: "icon-compact", variant: "ghost", disabled: index === 0, "aria-label": `Move message ${index + 1} up`,
|
|
173
|
+
], children: [_jsx("span", { class: "rf-queue-content", children: message.content }), _jsxs("div", { class: "rf-queue-actions", children: [_jsx(Button, { size: "icon-compact", variant: "ghost", disabled: index === 0, "aria-label": `Move message ${index + 1} up`, "data-queue-action": "up", mix: on('click', () => {
|
|
174
|
+
remember('up', message.id, index);
|
|
175
|
+
move(message.id, index - 1);
|
|
176
|
+
}), children: "\u2191" }), _jsx(Button, { size: "icon-compact", variant: "ghost", disabled: index === messages.length - 1, "aria-label": `Move message ${index + 1} down`, "data-queue-action": "down", mix: on('click', () => {
|
|
177
|
+
remember('down', message.id, index);
|
|
178
|
+
move(message.id, index + 1);
|
|
179
|
+
}), children: "\u2193" }), onRemove && (_jsx(Button, { size: "icon-compact", variant: "ghost", "aria-label": `Remove message ${index + 1}`, "data-queue-action": "remove", mix: on('click', () => {
|
|
180
|
+
remember('remove', message.id, index);
|
|
181
|
+
onRemove(message.id);
|
|
182
|
+
}), children: "\u00D7" })), _jsx(Button, { size: "compact", variant: "secondary", "data-queue-action": "send", mix: on('click', () => {
|
|
183
|
+
remember('send', message.id, index);
|
|
184
|
+
void send(message.id);
|
|
185
|
+
}), children: "Send" })] })] }, message.id))) })] }));
|
|
137
186
|
};
|
|
138
187
|
}
|
package/dist/styles.css
CHANGED
package/dist/toaster.js
CHANGED
|
@@ -70,12 +70,13 @@ export const toast = Object.assign((title, options) => upsert(title, 'default',
|
|
|
70
70
|
info: (title, options) => upsert(title, 'info', options),
|
|
71
71
|
warning: (title, options) => upsert(title, 'warning', options),
|
|
72
72
|
error: (title, options) => upsert(title, 'error', options),
|
|
73
|
-
loading
|
|
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),
|
|
74
76
|
promise(promise, messages, options = {}) {
|
|
75
77
|
const id = upsert(messages.loading, 'loading', {
|
|
76
78
|
description: messages.description,
|
|
77
79
|
...options,
|
|
78
|
-
duration: Infinity,
|
|
79
80
|
});
|
|
80
81
|
const settle = (status, title) => {
|
|
81
82
|
// A toast closed while it waited stays closed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtzll/reflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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
|
-
"
|
|
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
|
-
"
|
|
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"
|