@rathkit/rte-ui 0.0.0 → 0.1.0-alpha.3
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 +32 -4
- package/css/base.css +264 -0
- package/dist/findbar.d.ts +24 -0
- package/dist/findbar.js +187 -0
- package/dist/icon.d.ts +2 -0
- package/dist/icon.js +25 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/messages.d.ts +3 -0
- package/dist/messages.js +34 -0
- package/dist/shortcuts.d.ts +14 -0
- package/dist/shortcuts.js +68 -0
- package/dist/source.d.ts +15 -0
- package/dist/source.js +67 -0
- package/dist/standard.d.ts +5 -0
- package/dist/standard.js +6 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +1 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +482 -0
- package/package.json +84 -3
package/README.md
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
# @rathkit/rte-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Alpha** (0.1.0-alpha.0). Not for production: the APIs will change between releases. Published to the `next` tag only.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The styled interface: a toolbar, menus, dialogs, find bar, status line and tooltips on the headless layer, themeable by preset, tokens, icons, classes and messages.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
|
-
npm install @rathkit/rte-ui@next
|
|
10
|
+
npm install @rathkit/rte-ui@next @rathkit/rte-core@next @rathkit/rte-headless@next @rathkit/rte-themes@next @rathkit/rte-icons@next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This package has no dependencies of its own. It works with `@rathkit/rte-core`, `@rathkit/rte-headless`, `@rathkit/rte-themes`, `@rathkit/rte-icons` (peer dependencies, all optional so that npm installs nothing you did not ask for), so install the ones you use alongside it.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { createEditor } from "@rathkit/rte-core/standard";
|
|
19
|
+
import { createStandardUi } from "@rathkit/rte-ui/standard";
|
|
20
|
+
import "@rathkit/rte-ui/base.css";
|
|
21
|
+
import "@rathkit/rte-themes/light.css";
|
|
22
|
+
|
|
23
|
+
const root = document.createElement("div");
|
|
24
|
+
document.body.append(root);
|
|
25
|
+
const surface = document.createElement("div");
|
|
26
|
+
surface.setAttribute("aria-label", "Body");
|
|
27
|
+
|
|
28
|
+
const editor = createEditor({ element: surface, html: "<p>Hello</p>" });
|
|
29
|
+
editor.subscribe(() => console.log(editor.getHtml()));
|
|
30
|
+
createStandardUi(editor, { root, style: { theme: "light" } });
|
|
9
31
|
```
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
## More
|
|
34
|
+
|
|
35
|
+
Documentation: <https://rathkit.com>. Source and issues: <https://github.com/rathkit/rte>. What is checked: [SUPPORT.md](https://github.com/rathkit/rte/blob/main/docs/SUPPORT.md). Changes: [CHANGELOG.md](https://github.com/rathkit/rte/blob/main/CHANGELOG.md). How a release is verified: [VERIFY.md](https://github.com/rathkit/rte/blob/main/docs/VERIFY.md).
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
[Apache-2.0](LICENSE)
|
package/css/base.css
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/* @rathkit/rte-ui base styles. Everything is in cascade layers, so any CSS you write outside a layer wins without a specificity fight.
|
|
2
|
+
Parts are addressed by data-rk-part, never by class; the only class is .rk-root, which scopes everything to one editor.
|
|
3
|
+
Colors, sizes and fonts come from the --rk-* tokens (see @rathkit/rte-themes). Nothing here reaches outside .rk-root. */
|
|
4
|
+
@layer rathkit.defaults, rathkit.base;
|
|
5
|
+
|
|
6
|
+
/* Light values, used only when no theme is chosen, so the editor is never unstyled. A theme's tokens replace them. */
|
|
7
|
+
@layer rathkit.defaults {
|
|
8
|
+
.rk-root:where(:not([data-rk-theme])) {
|
|
9
|
+
--rk-bg: #ffffff; --rk-surface: #f4f6f9; --rk-surface-raised: #ffffff; --rk-fg: #14181f; --rk-fg-muted: #505b6b;
|
|
10
|
+
--rk-border: #78849a; --rk-border-subtle: #d4dae3; --rk-accent: #17408b; --rk-accent-fg: #ffffff; --rk-accent-subtle: #e3ecfa;
|
|
11
|
+
--rk-danger: #b3261e; --rk-selection-bg: #cddffa; --rk-control-hover: #e6ebf3; --rk-control-active: #d3dcec;
|
|
12
|
+
--rk-brand: #b9812a; --rk-brand-text: #86590f; --rk-focus-color: #17408b;
|
|
13
|
+
--rk-space-1: 4px; --rk-space-2: 8px; --rk-space-3: 12px; --rk-space-4: 16px; --rk-control-size: 32px;
|
|
14
|
+
--rk-radius-s: 4px; --rk-radius-m: 8px; --rk-border-width: 1px; --rk-focus-width: 2px; --rk-focus-offset: 2px;
|
|
15
|
+
--rk-font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --rk-font-content: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
16
|
+
--rk-font-heading: system-ui, sans-serif; --rk-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
17
|
+
--rk-font-size: 16px; --rk-line-height: 1.55; --rk-motion-duration: 120ms;
|
|
18
|
+
--rk-shadow-popover: 0 6px 24px rgb(20 24 31 / 0.18), 0 1px 3px rgb(20 24 31 / 0.12);
|
|
19
|
+
color-scheme: light;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@layer rathkit.base {
|
|
24
|
+
.rk-root {
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
position: relative;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
font: var(--rk-font-size) / var(--rk-line-height) var(--rk-font-ui);
|
|
30
|
+
color: var(--rk-fg);
|
|
31
|
+
background: var(--rk-bg);
|
|
32
|
+
border: var(--rk-border-width) solid var(--rk-border);
|
|
33
|
+
border-radius: var(--rk-radius-m);
|
|
34
|
+
}
|
|
35
|
+
.rk-root *, .rk-root *::before, .rk-root *::after { box-sizing: inherit; }
|
|
36
|
+
|
|
37
|
+
/* ---- toolbar ---- */
|
|
38
|
+
[data-rk-part="toolbar"] {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: var(--rk-space-1);
|
|
43
|
+
padding: var(--rk-space-2);
|
|
44
|
+
background: var(--rk-surface);
|
|
45
|
+
border-bottom: var(--rk-border-width) solid var(--rk-border);
|
|
46
|
+
border-radius: calc(var(--rk-radius-m) - var(--rk-border-width)) calc(var(--rk-radius-m) - var(--rk-border-width)) 0 0;
|
|
47
|
+
}
|
|
48
|
+
[data-rk-part="separator"] {
|
|
49
|
+
align-self: stretch;
|
|
50
|
+
width: var(--rk-border-width);
|
|
51
|
+
margin-inline: var(--rk-space-1);
|
|
52
|
+
background: var(--rk-border-subtle);
|
|
53
|
+
}
|
|
54
|
+
[data-rk-part="button"],
|
|
55
|
+
[data-rk-part="select"] {
|
|
56
|
+
min-width: var(--rk-control-size);
|
|
57
|
+
height: var(--rk-control-size);
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 0 var(--rk-space-2);
|
|
60
|
+
font: inherit;
|
|
61
|
+
color: var(--rk-fg);
|
|
62
|
+
background: transparent;
|
|
63
|
+
border: var(--rk-border-width) solid transparent;
|
|
64
|
+
border-radius: var(--rk-radius-s);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
transition: background-color calc(var(--rk-motion-duration) * var(--rk-motion-scale, 1)) ease;
|
|
67
|
+
}
|
|
68
|
+
[data-rk-part="button"] { display: inline-flex; align-items: center; justify-content: center; gap: var(--rk-space-1); }
|
|
69
|
+
[data-rk-part="select"] { background: var(--rk-surface-raised); border-color: var(--rk-border); }
|
|
70
|
+
[data-rk-part="button"]:hover:not([aria-disabled="true"]),
|
|
71
|
+
[data-rk-part="select"]:hover:not([aria-disabled="true"]) { background: var(--rk-control-hover); }
|
|
72
|
+
[data-rk-part="button"]:active:not([aria-disabled="true"]) { background: var(--rk-control-active); }
|
|
73
|
+
/* On: a tint, an accent border all round, and a thicker bar underneath, so it never depends on color alone. */
|
|
74
|
+
[data-rk-part="button"][aria-pressed="true"] {
|
|
75
|
+
color: var(--rk-accent);
|
|
76
|
+
background: var(--rk-accent-subtle);
|
|
77
|
+
border-color: var(--rk-accent);
|
|
78
|
+
border-bottom-width: 3px;
|
|
79
|
+
}
|
|
80
|
+
[data-rk-part="button"][aria-disabled="true"],
|
|
81
|
+
[data-rk-part="select"][aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
82
|
+
[data-rk-part="icon"] { display: inline-flex; width: 20px; height: 20px; }
|
|
83
|
+
[data-rk-part="icon"] svg { width: 100%; height: 100%; }
|
|
84
|
+
|
|
85
|
+
/* ---- menus: a button that opens a list of commands ---- */
|
|
86
|
+
[data-rk-part="menu-host"] { position: relative; display: inline-flex; }
|
|
87
|
+
[data-rk-part="menu"] {
|
|
88
|
+
position: absolute; z-index: 10; inset-block-start: calc(100% + var(--rk-space-1)); inset-inline-start: 0;
|
|
89
|
+
min-width: 14rem; padding: var(--rk-space-1);
|
|
90
|
+
background: var(--rk-surface-raised); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-m);
|
|
91
|
+
box-shadow: var(--rk-shadow-popover); max-height: min(22rem, 60vh); overflow-y: auto;
|
|
92
|
+
}
|
|
93
|
+
[data-rk-part="menu"][hidden] { display: none; }
|
|
94
|
+
[data-rk-part="menu-item"] {
|
|
95
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--rk-space-4); width: 100%; min-height: var(--rk-control-size);
|
|
96
|
+
margin: 0; padding: 0 var(--rk-space-2) 0 calc(var(--rk-space-2) + 1.4em); font: inherit; text-align: start; color: var(--rk-fg);
|
|
97
|
+
background: transparent; border: 0; border-radius: var(--rk-radius-s); cursor: pointer; position: relative;
|
|
98
|
+
}
|
|
99
|
+
[data-rk-part="menu-item"]:hover:not([aria-disabled="true"]), [data-rk-part="menu-item"]:focus-visible { background: var(--rk-control-hover); }
|
|
100
|
+
[data-rk-part="menu-item"][aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
101
|
+
/* A switched-on entry has a check mark in front of it, so it never depends on color alone. */
|
|
102
|
+
[data-rk-part="menu-item"][aria-checked="true"]::before { content: "\2713"; position: absolute; inset-inline-start: var(--rk-space-2); font-weight: 700; color: var(--rk-accent); }
|
|
103
|
+
/* A sample of a color, with a border so it shows on any background (a white swatch on white, say). */
|
|
104
|
+
[data-rk-part="swatch"] { flex: none; width: 1em; height: 1em; margin-inline-end: var(--rk-space-2); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-s); }
|
|
105
|
+
[data-rk-part="menu-item"]:has([data-rk-part="swatch"]) { justify-content: flex-start; }
|
|
106
|
+
[data-rk-part="menu-item"]:has([data-rk-part="swatch"]) [data-rk-part="menu-keys"] { margin-inline-start: auto; }
|
|
107
|
+
[data-rk-part="menu-keys"] { color: var(--rk-fg-muted); font-size: 0.85em; }
|
|
108
|
+
[data-rk-part="menu-separator"] { height: var(--rk-border-width); margin: var(--rk-space-1) var(--rk-space-2); background: var(--rk-border-subtle); }
|
|
109
|
+
|
|
110
|
+
/* ---- tooltip: the control's name and key, shown near it on hover and keyboard focus ---- */
|
|
111
|
+
[data-rk-part="tooltip"] {
|
|
112
|
+
position: absolute; z-index: 20; max-width: 20rem; padding: var(--rk-space-1) var(--rk-space-2); font-size: 0.85em; line-height: 1.35;
|
|
113
|
+
color: var(--rk-bg); background: var(--rk-fg); border-radius: var(--rk-radius-s); box-shadow: var(--rk-shadow-popover);
|
|
114
|
+
white-space: nowrap; pointer-events: auto;
|
|
115
|
+
}
|
|
116
|
+
[data-rk-part="tooltip"][hidden] { display: none; }
|
|
117
|
+
|
|
118
|
+
.rk-root :focus-visible {
|
|
119
|
+
outline: var(--rk-focus-width) solid var(--rk-focus-color);
|
|
120
|
+
outline-offset: var(--rk-focus-offset);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ---- the writing area ---- */
|
|
124
|
+
[data-rk-part="content"] {
|
|
125
|
+
padding: var(--rk-space-3) var(--rk-space-4);
|
|
126
|
+
background: var(--rk-bg);
|
|
127
|
+
border-radius: 0 0 var(--rk-radius-m) var(--rk-radius-m);
|
|
128
|
+
font-family: var(--rk-font-content);
|
|
129
|
+
}
|
|
130
|
+
[data-rk-part="content"] > :first-child { outline: none; min-height: 8em; }
|
|
131
|
+
[data-rk-part="content"] ::selection { background: var(--rk-selection-bg); color: var(--rk-fg); }
|
|
132
|
+
/* The hint shown while the document is empty (the editor sets data-rk-empty and --rk-placeholder). */
|
|
133
|
+
[data-rk-part="content"] > [data-rk-empty] > :first-child::before { content: var(--rk-placeholder, none); color: var(--rk-fg-muted); pointer-events: none; }
|
|
134
|
+
[data-rk-part="content"] > :first-child > :first-child { margin-top: 0; }
|
|
135
|
+
[data-rk-part="content"] > :first-child > :last-child { margin-bottom: 0; }
|
|
136
|
+
[data-rk-part="content"] p { margin: 0 0 0.75em; }
|
|
137
|
+
[data-rk-part="content"] :is(h1, h2, h3, h4, h5, h6) { margin: 1.1em 0 0.4em; font-family: var(--rk-font-heading); line-height: 1.25; font-weight: 700; }
|
|
138
|
+
[data-rk-part="content"] h1 { font-size: 1.9em; }
|
|
139
|
+
[data-rk-part="content"] h2 { font-size: 1.5em; }
|
|
140
|
+
[data-rk-part="content"] h3 { font-size: 1.25em; }
|
|
141
|
+
[data-rk-part="content"] h4 { font-size: 1.1em; }
|
|
142
|
+
[data-rk-part="content"] :is(h5, h6) { font-size: 1em; }
|
|
143
|
+
[data-rk-part="content"] :is(ul, ol) { margin: 0 0 0.75em; padding-inline-start: 1.6em; }
|
|
144
|
+
[data-rk-part="content"] :is(ul, ol) :is(ul, ol) { margin-bottom: 0; }
|
|
145
|
+
/* A task's checkbox is a glyph in front of its text; clicking it ticks the task. */
|
|
146
|
+
[data-rk-part="content"] li[data-rk-task] { list-style: none; }
|
|
147
|
+
[data-rk-part="content"] [data-rte-check] { display: inline-block; margin-inline-end: 0.4em; cursor: pointer; user-select: none; font-size: 1.1em; line-height: 1; color: var(--rk-accent); }
|
|
148
|
+
[data-rk-part="content"] li[data-rk-task="open"] > [data-rte-check] { color: var(--rk-fg-muted); }
|
|
149
|
+
[data-rk-part="content"] dt { font-weight: 700; }
|
|
150
|
+
[data-rk-part="content"] dd { margin-inline-start: 1.6em; }
|
|
151
|
+
[data-rk-part="content"] blockquote { margin: 0 0 0.75em; padding-inline-start: var(--rk-space-3); border-inline-start: 3px solid var(--rk-brand); color: var(--rk-fg-muted); }
|
|
152
|
+
[data-rk-part="content"] pre { margin: 0 0 0.75em; padding: var(--rk-space-2) var(--rk-space-3); font-family: var(--rk-font-mono); font-size: 0.9em; background: var(--rk-surface); border: var(--rk-border-width) solid var(--rk-border-subtle); border-radius: var(--rk-radius-s); overflow-x: auto; }
|
|
153
|
+
[data-rk-part="content"] code { font-family: var(--rk-font-mono); font-size: 0.9em; padding: 0.1em 0.3em; background: var(--rk-surface); border-radius: var(--rk-radius-s); }
|
|
154
|
+
[data-rk-part="content"] pre code { padding: 0; background: none; }
|
|
155
|
+
[data-rk-part="content"] a { color: var(--rk-accent); text-decoration: underline; text-underline-offset: 0.15em; }
|
|
156
|
+
[data-rk-part="content"] hr { border: 0; border-top: var(--rk-border-width) solid var(--rk-border); margin: 1em 0; }
|
|
157
|
+
[data-rk-part="content"] img { max-width: 100%; height: auto; }
|
|
158
|
+
/* An image from another site that the host has not allowed to load: a dashed box in its place, so it is plain that something is there. */
|
|
159
|
+
[data-rk-part="content"] img[data-rte-remote-src] { display: inline-block; min-width: 6em; min-height: 3em; vertical-align: middle; border: 2px dashed var(--rk-border); border-radius: var(--rk-radius-s); background: var(--rk-surface); }
|
|
160
|
+
[data-rk-part="content"] table { border-collapse: collapse; margin: 0 0 0.75em; }
|
|
161
|
+
[data-rk-part="content"] :is(td, th) { padding: var(--rk-space-1) var(--rk-space-2); border: var(--rk-border-width) solid var(--rk-border); vertical-align: top; min-width: 2em; }
|
|
162
|
+
[data-rk-part="content"] :is(td, th) > p { margin: 0; }
|
|
163
|
+
[data-rk-part="content"] :is(td, th) > p + p { margin-top: 0.5em; }
|
|
164
|
+
[data-rk-part="content"] th { background: var(--rk-surface); text-align: start; }
|
|
165
|
+
/* Cells a selection runs across (the view marks them): a tint and an outline, so it is not color alone. */
|
|
166
|
+
[data-rk-part="content"] [data-rk-cell-selected] { background: var(--rk-selection-bg); outline: 2px solid var(--rk-accent); outline-offset: -2px; }
|
|
167
|
+
[data-rk-part="content"] img[data-rk-image-selected] { outline: 2px solid var(--rk-accent); outline-offset: 2px; }
|
|
168
|
+
/* Focus inside the writing area shows on the whole editor, not as a box around the text. */
|
|
169
|
+
.rk-root:focus-within { outline: var(--rk-focus-width) solid var(--rk-focus-color); outline-offset: var(--rk-focus-offset); }
|
|
170
|
+
.rk-root:focus-within [data-rk-part="content"] :focus-visible,
|
|
171
|
+
.rk-root [data-rk-part="content"] :focus-visible { outline: none; }
|
|
172
|
+
|
|
173
|
+
/* ---- HTML source view: the text box takes the place of the writing area ---- */
|
|
174
|
+
[data-rk-part="content"][data-rk-source] > :not([data-rk-part="source"]) { display: none; }
|
|
175
|
+
[data-rk-part="source"] {
|
|
176
|
+
display: block; width: 100%; min-height: 14em; margin: 0; padding: 0; font: 0.9em / 1.5 var(--rk-font-mono); color: var(--rk-fg);
|
|
177
|
+
background: var(--rk-bg); border: 0; outline: none; resize: vertical; tab-size: 2;
|
|
178
|
+
}
|
|
179
|
+
[data-rk-part="source"][hidden] { display: none; }
|
|
180
|
+
|
|
181
|
+
/* ---- keyboard shortcuts list ---- */
|
|
182
|
+
[data-rk-part="shortcuts"] { width: 100%; border-collapse: collapse; margin-bottom: var(--rk-space-2); }
|
|
183
|
+
[data-rk-part="shortcuts"] :is(th, td) { padding: var(--rk-space-1) var(--rk-space-2); text-align: start; border-bottom: var(--rk-border-width) solid var(--rk-border-subtle); }
|
|
184
|
+
[data-rk-part="shortcuts"] kbd { font: 0.9em var(--rk-font-mono); padding: 0.1em 0.4em; background: var(--rk-surface); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-s); }
|
|
185
|
+
|
|
186
|
+
/* ---- find and replace ---- */
|
|
187
|
+
[data-rk-part="findbar"] { padding: var(--rk-space-2); background: var(--rk-surface); border-bottom: var(--rk-border-width) solid var(--rk-border); }
|
|
188
|
+
[data-rk-part="findbar"][hidden] { display: none; }
|
|
189
|
+
[data-rk-part="find-line"] { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rk-space-1); }
|
|
190
|
+
[data-rk-part="find-line"] + [data-rk-part="find-line"] { margin-top: var(--rk-space-1); }
|
|
191
|
+
[data-rk-part="find-row"] { display: flex; align-items: center; gap: var(--rk-space-2); flex: 1 1 14rem; }
|
|
192
|
+
[data-rk-part="find-label"] { min-width: 6.5em; font-weight: 600; }
|
|
193
|
+
[data-rk-part="find-field"] {
|
|
194
|
+
flex: 1; min-width: 6em; height: var(--rk-control-size); padding: 0 var(--rk-space-2); font: inherit; color: var(--rk-fg);
|
|
195
|
+
background: var(--rk-bg); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-s);
|
|
196
|
+
}
|
|
197
|
+
[data-rk-part="find-count"] { min-width: 5.5em; color: var(--rk-fg-muted); font-size: 0.9em; }
|
|
198
|
+
[data-rk-part="findbar"] [aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
|
|
199
|
+
|
|
200
|
+
/* ---- status line ---- */
|
|
201
|
+
[data-rk-part="status"] {
|
|
202
|
+
padding: var(--rk-space-1) var(--rk-space-3); font-size: 0.85em; color: var(--rk-fg-muted); background: var(--rk-surface);
|
|
203
|
+
border-top: var(--rk-border-width) solid var(--rk-border-subtle); border-radius: 0 0 calc(var(--rk-radius-m) - var(--rk-border-width)) calc(var(--rk-radius-m) - var(--rk-border-width));
|
|
204
|
+
}
|
|
205
|
+
[data-rk-part="content"]:has(+ [data-rk-part="status"]) { border-radius: 0; }
|
|
206
|
+
|
|
207
|
+
/* ---- dialog ---- */
|
|
208
|
+
[data-rk-part="dialog"] {
|
|
209
|
+
width: min(26rem, calc(100vw - 2 * var(--rk-space-4)));
|
|
210
|
+
padding: var(--rk-space-4);
|
|
211
|
+
color: var(--rk-fg);
|
|
212
|
+
background: var(--rk-surface-raised);
|
|
213
|
+
border: var(--rk-border-width) solid var(--rk-border);
|
|
214
|
+
border-radius: var(--rk-radius-m);
|
|
215
|
+
box-shadow: var(--rk-shadow-popover);
|
|
216
|
+
font: var(--rk-font-size) / var(--rk-line-height) var(--rk-font-ui);
|
|
217
|
+
}
|
|
218
|
+
[data-rk-part="dialog"]::backdrop { background: rgb(0 0 0 / 0.45); }
|
|
219
|
+
[data-rk-part="dialog-title"] { margin: 0 0 var(--rk-space-3); font: 700 1.15em / 1.3 var(--rk-font-heading); }
|
|
220
|
+
[data-rk-part="field-label"] { display: block; margin-bottom: var(--rk-space-1); font-weight: 600; }
|
|
221
|
+
[data-rk-part="field"] {
|
|
222
|
+
display: block; width: 100%; height: var(--rk-control-size); padding: 0 var(--rk-space-2); font: inherit; color: var(--rk-fg);
|
|
223
|
+
background: var(--rk-bg); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-s);
|
|
224
|
+
}
|
|
225
|
+
[data-rk-part="field"][aria-invalid="true"] { border-color: var(--rk-danger); border-width: 2px; }
|
|
226
|
+
[data-rk-part="field-check"] { display: flex; align-items: center; gap: var(--rk-space-2); margin-top: var(--rk-space-2); }
|
|
227
|
+
[data-rk-part="field-check"] [data-rk-part="field"] { width: 1.1em; height: 1.1em; padding: 0; accent-color: var(--rk-accent); }
|
|
228
|
+
[data-rk-part="field-check"] [data-rk-part="field-label"] { display: inline; margin: 0; font-weight: 400; }
|
|
229
|
+
[data-rk-part="field-error"] { display: block; min-height: 1.4em; margin-top: var(--rk-space-1); color: var(--rk-danger); font-size: 0.9em; }
|
|
230
|
+
[data-rk-part="actions"] { display: flex; justify-content: flex-end; gap: var(--rk-space-2); margin-top: var(--rk-space-3); }
|
|
231
|
+
[data-rk-part="action"] {
|
|
232
|
+
min-width: 5rem; height: var(--rk-control-size); padding: 0 var(--rk-space-3); font: inherit; font-weight: 600; color: var(--rk-fg);
|
|
233
|
+
background: var(--rk-surface-raised); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-s); cursor: pointer;
|
|
234
|
+
}
|
|
235
|
+
[data-rk-part="action"][data-primary] { color: var(--rk-accent-fg); background: var(--rk-accent); border-color: var(--rk-accent); }
|
|
236
|
+
|
|
237
|
+
/* Matches painted by the page's highlight registry (nothing in the document changes). Dark text on a strong tint reads on any theme. */
|
|
238
|
+
::highlight(rk-find) { background-color: #ffe08a; color: #14181f; }
|
|
239
|
+
::highlight(rk-find-current) { background-color: #ff9632; color: #14181f; }
|
|
240
|
+
|
|
241
|
+
@media (prefers-reduced-motion: reduce) { .rk-root { --rk-motion-scale: 0; } }
|
|
242
|
+
|
|
243
|
+
/* Forced colors (Windows high contrast): use the user's colors, and real borders and outlines, which survive where shadows and tints do not. */
|
|
244
|
+
@media (forced-colors: active) {
|
|
245
|
+
.rk-root { border-color: CanvasText; }
|
|
246
|
+
[data-rk-part="toolbar"] { border-bottom-color: CanvasText; }
|
|
247
|
+
[data-rk-part="button"], [data-rk-part="select"], [data-rk-part="field"], [data-rk-part="action"] { border-color: ButtonText; color: ButtonText; background: ButtonFace; }
|
|
248
|
+
[data-rk-part="button"][aria-pressed="true"] { border-color: Highlight; color: HighlightText; background: Highlight; border-bottom-width: 4px; }
|
|
249
|
+
[data-rk-part="button"][aria-disabled="true"], [data-rk-part="select"][aria-disabled="true"] { color: GrayText; border-color: GrayText; opacity: 1; }
|
|
250
|
+
[data-rk-part="content"] a { color: LinkText; }
|
|
251
|
+
[data-rk-part="content"] [data-rk-cell-selected] { outline-color: Highlight; background: Canvas; }
|
|
252
|
+
[data-rk-part="content"] img[data-rk-image-selected] { outline-color: Highlight; }
|
|
253
|
+
.rk-root :focus-visible, .rk-root:focus-within { outline-color: Highlight; }
|
|
254
|
+
[data-rk-part="separator"], [data-rk-part="menu-separator"] { background: CanvasText; }
|
|
255
|
+
[data-rk-part="menu"] { border-color: CanvasText; background: Canvas; }
|
|
256
|
+
::highlight(rk-find) { background-color: Mark; color: MarkText; }
|
|
257
|
+
::highlight(rk-find-current) { background-color: Highlight; color: HighlightText; }
|
|
258
|
+
[data-rk-part="find-field"] { border-color: ButtonText; }
|
|
259
|
+
[data-rk-part="tooltip"] { color: CanvasText; background: Canvas; border: 1px solid CanvasText; }
|
|
260
|
+
[data-rk-part="menu-item"] { color: ButtonText; }
|
|
261
|
+
[data-rk-part="menu-item"]:hover:not([aria-disabled="true"]), [data-rk-part="menu-item"]:focus-visible { background: Highlight; color: HighlightText; }
|
|
262
|
+
[data-rk-part="menu-item"][aria-disabled="true"] { color: GrayText; opacity: 1; }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UiEditor } from "./types.ts";
|
|
2
|
+
export interface FindBarOptions {
|
|
3
|
+
readonly editor: UiEditor & {
|
|
4
|
+
rangeFor?(from: {
|
|
5
|
+
path: number[];
|
|
6
|
+
offset: number;
|
|
7
|
+
}, to: {
|
|
8
|
+
path: number[];
|
|
9
|
+
offset: number;
|
|
10
|
+
}): Range | null;
|
|
11
|
+
};
|
|
12
|
+
/** Makes an element a named part of the interface. */
|
|
13
|
+
readonly part: <E extends HTMLElement>(el: E, name: string) => E;
|
|
14
|
+
readonly message: (key: string, fallback?: string, values?: Record<string, string>) => string;
|
|
15
|
+
readonly icon: (name: string) => string | (() => Node) | undefined;
|
|
16
|
+
readonly announce: (message: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface FindBar {
|
|
19
|
+
readonly element: HTMLElement;
|
|
20
|
+
/** Words may have changed (a different language, say): write them again. */
|
|
21
|
+
refresh(): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function createFindBar(options: FindBarOptions): FindBar;
|
package/dist/findbar.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { setIcon } from "./icon.js";
|
|
2
|
+
const registry = () => globalThis.CSS?.highlights;
|
|
3
|
+
const HighlightCtor = () => globalThis.Highlight;
|
|
4
|
+
const MOST_PAINTED = 2000;
|
|
5
|
+
export function createFindBar(options) {
|
|
6
|
+
const { editor, part, message } = options;
|
|
7
|
+
let counter = 0;
|
|
8
|
+
const bar = part(document.createElement("div"), "findbar");
|
|
9
|
+
bar.setAttribute("role", "search");
|
|
10
|
+
bar.hidden = true;
|
|
11
|
+
const field = (label, name) => {
|
|
12
|
+
const id = `rk-find-${++counter}`;
|
|
13
|
+
const wrap = part(document.createElement("div"), "find-row");
|
|
14
|
+
const text = part(document.createElement("label"), "find-label");
|
|
15
|
+
text.htmlFor = id;
|
|
16
|
+
text.textContent = label;
|
|
17
|
+
const input = part(document.createElement("input"), "find-field");
|
|
18
|
+
input.id = id;
|
|
19
|
+
input.type = "text";
|
|
20
|
+
input.name = name;
|
|
21
|
+
input.autocomplete = "off";
|
|
22
|
+
input.spellcheck = false;
|
|
23
|
+
wrap.append(text, input);
|
|
24
|
+
return { wrap, input, text };
|
|
25
|
+
};
|
|
26
|
+
const iconButton = (name, label) => {
|
|
27
|
+
const b = part(document.createElement("button"), "button");
|
|
28
|
+
b.type = "button";
|
|
29
|
+
const i = part(document.createElement("span"), "icon");
|
|
30
|
+
i.setAttribute("aria-hidden", "true");
|
|
31
|
+
if (setIcon(i, options.icon(name)))
|
|
32
|
+
b.appendChild(i);
|
|
33
|
+
else
|
|
34
|
+
b.textContent = label;
|
|
35
|
+
b.setAttribute("aria-label", label);
|
|
36
|
+
b.setAttribute("data-rk-tip", label);
|
|
37
|
+
return b;
|
|
38
|
+
};
|
|
39
|
+
const textButton = (label) => {
|
|
40
|
+
const b = part(document.createElement("button"), "action");
|
|
41
|
+
b.type = "button";
|
|
42
|
+
b.textContent = label;
|
|
43
|
+
return b;
|
|
44
|
+
};
|
|
45
|
+
const find = field("", "find");
|
|
46
|
+
const replace = field("", "replace");
|
|
47
|
+
const count = part(document.createElement("span"), "find-count");
|
|
48
|
+
const previous = iconButton("chevron-up", "");
|
|
49
|
+
const next = iconButton("chevron-down", "");
|
|
50
|
+
const matchCase = part(document.createElement("button"), "button");
|
|
51
|
+
matchCase.type = "button";
|
|
52
|
+
matchCase.textContent = "Aa";
|
|
53
|
+
const wholeWord = part(document.createElement("button"), "button");
|
|
54
|
+
wholeWord.type = "button";
|
|
55
|
+
wholeWord.textContent = "ab";
|
|
56
|
+
const replaceOne = textButton("");
|
|
57
|
+
const replaceAll = textButton("");
|
|
58
|
+
const close = iconButton("close", "");
|
|
59
|
+
const first = part(document.createElement("div"), "find-line");
|
|
60
|
+
first.append(find.wrap, previous, next, matchCase, wholeWord, count, close);
|
|
61
|
+
const second = part(document.createElement("div"), "find-line");
|
|
62
|
+
second.append(replace.wrap, replaceOne, replaceAll);
|
|
63
|
+
bar.append(first, second);
|
|
64
|
+
const words = () => {
|
|
65
|
+
find.text.textContent = message("find.find");
|
|
66
|
+
replace.text.textContent = message("find.replaceWith");
|
|
67
|
+
for (const [el, key] of [[previous, "find.previous"], [next, "find.next"], [close, "find.close"], [matchCase, "find.matchCase"], [wholeWord, "find.wholeWord"]]) {
|
|
68
|
+
el.setAttribute("aria-label", message(key));
|
|
69
|
+
el.setAttribute("data-rk-tip", message(key));
|
|
70
|
+
}
|
|
71
|
+
replaceOne.textContent = message("find.replace");
|
|
72
|
+
replaceAll.textContent = message("find.replaceAll");
|
|
73
|
+
};
|
|
74
|
+
words();
|
|
75
|
+
const info = () => editor.commands.state("find.set").value;
|
|
76
|
+
let lastOpens = -1;
|
|
77
|
+
let lastCount = "";
|
|
78
|
+
let announceTimer;
|
|
79
|
+
const paint = (state) => {
|
|
80
|
+
const highlights = registry();
|
|
81
|
+
const Highlight = HighlightCtor();
|
|
82
|
+
if (!highlights || !Highlight || !editor.rangeFor)
|
|
83
|
+
return;
|
|
84
|
+
if (!state.open || state.matches.length === 0) {
|
|
85
|
+
highlights.delete("rk-find");
|
|
86
|
+
highlights.delete("rk-find-current");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const ranges = [];
|
|
90
|
+
let current;
|
|
91
|
+
state.matches.slice(0, MOST_PAINTED).forEach((m, i) => {
|
|
92
|
+
const range = editor.rangeFor?.({ path: [...m.path], offset: m.from }, { path: [...m.path], offset: m.to });
|
|
93
|
+
if (!range)
|
|
94
|
+
return;
|
|
95
|
+
if (i === state.index)
|
|
96
|
+
current = range;
|
|
97
|
+
else
|
|
98
|
+
ranges.push(range);
|
|
99
|
+
});
|
|
100
|
+
highlights.set("rk-find", new Highlight(...ranges));
|
|
101
|
+
if (current)
|
|
102
|
+
highlights.set("rk-find-current", new Highlight(current));
|
|
103
|
+
else
|
|
104
|
+
highlights.delete("rk-find-current");
|
|
105
|
+
};
|
|
106
|
+
const render = () => {
|
|
107
|
+
const state = info();
|
|
108
|
+
bar.hidden = !state.open;
|
|
109
|
+
if (!state.open) {
|
|
110
|
+
paint(state);
|
|
111
|
+
lastOpens = state.opens;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (state.query !== find.input.value)
|
|
115
|
+
find.input.value = state.query;
|
|
116
|
+
matchCase.setAttribute("aria-pressed", String(state.matchCase));
|
|
117
|
+
wholeWord.setAttribute("aria-pressed", String(state.wholeWord));
|
|
118
|
+
const text = state.query === "" ? "" : state.matches.length === 0 ? message("find.none")
|
|
119
|
+
: state.index >= 0 ? message("find.count", "", { current: String(state.index + 1), total: String(state.matches.length) })
|
|
120
|
+
: message("find.total", "", { total: String(state.matches.length) });
|
|
121
|
+
count.textContent = text;
|
|
122
|
+
if (text !== lastCount) {
|
|
123
|
+
lastCount = text;
|
|
124
|
+
if (announceTimer)
|
|
125
|
+
clearTimeout(announceTimer);
|
|
126
|
+
if (text !== "")
|
|
127
|
+
announceTimer = setTimeout(() => options.announce(text), 500);
|
|
128
|
+
}
|
|
129
|
+
for (const b of [previous, next, replaceOne, replaceAll])
|
|
130
|
+
b.setAttribute("aria-disabled", String(state.matches.length === 0));
|
|
131
|
+
paint(state);
|
|
132
|
+
if (state.opens !== lastOpens) {
|
|
133
|
+
lastOpens = state.opens;
|
|
134
|
+
find.input.focus();
|
|
135
|
+
find.input.select();
|
|
136
|
+
}
|
|
137
|
+
if (state.index >= 0 && editor.rangeFor) {
|
|
138
|
+
const m = state.matches[state.index];
|
|
139
|
+
const at = m && editor.rangeFor({ path: [...m.path], offset: m.from }, { path: [...m.path], offset: m.to });
|
|
140
|
+
(at?.startContainer instanceof Element ? at.startContainer : at?.startContainer.parentElement)?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const run = (name, arg) => { editor.commands.run(name, arg); };
|
|
144
|
+
const onFind = () => run("find.set", { query: find.input.value });
|
|
145
|
+
find.input.addEventListener("input", onFind);
|
|
146
|
+
find.input.addEventListener("keydown", (e) => {
|
|
147
|
+
if (e.key === "Enter") {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
run(e.shiftKey ? "find.previous" : "find.next");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
replace.input.addEventListener("keydown", (e) => {
|
|
153
|
+
if (e.key === "Enter") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
run("find.replace", { with: replace.input.value });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
bar.addEventListener("keydown", (e) => {
|
|
159
|
+
if (e.key === "Escape") {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
run("find.close");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
previous.addEventListener("click", () => run("find.previous"));
|
|
166
|
+
next.addEventListener("click", () => run("find.next"));
|
|
167
|
+
matchCase.addEventListener("click", () => run("find.set", { matchCase: matchCase.getAttribute("aria-pressed") !== "true" }));
|
|
168
|
+
wholeWord.addEventListener("click", () => run("find.set", { wholeWord: wholeWord.getAttribute("aria-pressed") !== "true" }));
|
|
169
|
+
replaceOne.addEventListener("click", () => run("find.replace", { with: replace.input.value }));
|
|
170
|
+
replaceAll.addEventListener("click", () => run("find.replaceAll", { with: replace.input.value }));
|
|
171
|
+
close.addEventListener("click", () => run("find.close"));
|
|
172
|
+
const stop = editor.commands.subscribe("find.set", render);
|
|
173
|
+
render();
|
|
174
|
+
return {
|
|
175
|
+
element: bar,
|
|
176
|
+
refresh() { words(); render(); },
|
|
177
|
+
destroy() {
|
|
178
|
+
stop();
|
|
179
|
+
if (announceTimer)
|
|
180
|
+
clearTimeout(announceTimer);
|
|
181
|
+
const highlights = registry();
|
|
182
|
+
highlights?.delete("rk-find");
|
|
183
|
+
highlights?.delete("rk-find-current");
|
|
184
|
+
bar.remove();
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
package/dist/icon.d.ts
ADDED
package/dist/icon.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const cache = new Map();
|
|
2
|
+
function parse(svg) {
|
|
3
|
+
const hit = cache.get(svg);
|
|
4
|
+
if (hit !== undefined)
|
|
5
|
+
return hit;
|
|
6
|
+
const template = document.createElement("template");
|
|
7
|
+
template.innerHTML = svg;
|
|
8
|
+
const ok = template.content.childElementCount === 1 && template.content.firstElementChild?.localName === "svg" && !template.content.querySelector("script, foreignObject, [onload], [onclick], [onerror]");
|
|
9
|
+
const fragment = ok ? template.content : null;
|
|
10
|
+
cache.set(svg, fragment);
|
|
11
|
+
return fragment;
|
|
12
|
+
}
|
|
13
|
+
export function setIcon(target, source) {
|
|
14
|
+
if (source === undefined) {
|
|
15
|
+
target.replaceChildren();
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const node = typeof source === "function" ? source() : parse(source)?.cloneNode(true);
|
|
19
|
+
if (!node) {
|
|
20
|
+
target.replaceChildren();
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
target.replaceChildren(node);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MessageKey } from "./types.ts";
|
|
2
|
+
export declare const DEFAULT_MESSAGES: Readonly<Record<string, string>>;
|
|
3
|
+
export declare function text(table: Partial<Record<MessageKey, string>> | undefined, key: MessageKey, fallback?: string, values?: Record<string, string>): string;
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const DEFAULT_MESSAGES = {
|
|
2
|
+
"toolbar.label": "Formatting",
|
|
3
|
+
"dialog.ok": "OK",
|
|
4
|
+
"dialog.cancel": "Cancel",
|
|
5
|
+
"field.required": "{label} is required.",
|
|
6
|
+
"field.url": "{label} must be a web, mail or phone address.",
|
|
7
|
+
"toggle.on": "{label} on",
|
|
8
|
+
"toggle.off": "{label} off",
|
|
9
|
+
"status.word": "{count} word",
|
|
10
|
+
"status.words": "{count} words",
|
|
11
|
+
"status.characters": "{count} characters",
|
|
12
|
+
"status.selection": "{selected} of {total} words",
|
|
13
|
+
"source.label": "HTML source",
|
|
14
|
+
"shortcuts.title": "Keyboard shortcuts",
|
|
15
|
+
"shortcuts.action": "Action",
|
|
16
|
+
"shortcuts.keys": "Keys",
|
|
17
|
+
"shortcuts.close": "Close",
|
|
18
|
+
"find.find": "Find",
|
|
19
|
+
"find.replaceWith": "Replace with",
|
|
20
|
+
"find.previous": "Previous match",
|
|
21
|
+
"find.next": "Next match",
|
|
22
|
+
"find.close": "Close find and replace",
|
|
23
|
+
"find.matchCase": "Match case",
|
|
24
|
+
"find.wholeWord": "Whole word",
|
|
25
|
+
"find.replace": "Replace",
|
|
26
|
+
"find.replaceAll": "Replace all",
|
|
27
|
+
"find.none": "No matches",
|
|
28
|
+
"find.count": "{current} of {total}",
|
|
29
|
+
"find.total": "{total} matches",
|
|
30
|
+
};
|
|
31
|
+
export function text(table, key, fallback = "", values = {}) {
|
|
32
|
+
const raw = table?.[key] ?? DEFAULT_MESSAGES[key] ?? fallback;
|
|
33
|
+
return raw.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? "");
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UiEditor } from "./types.ts";
|
|
2
|
+
export interface ShortcutsOptions {
|
|
3
|
+
readonly editor: UiEditor;
|
|
4
|
+
readonly root: HTMLElement;
|
|
5
|
+
readonly part: <E extends HTMLElement>(el: E, name: string) => E;
|
|
6
|
+
readonly message: (key: string) => string;
|
|
7
|
+
}
|
|
8
|
+
export interface Shortcuts {
|
|
9
|
+
open(opener: HTMLElement): void;
|
|
10
|
+
/** Words may have changed: write them again. */
|
|
11
|
+
refresh(): void;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createShortcuts(options: ShortcutsOptions): Shortcuts;
|