@k2b/editor 0.3.1 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k2b/editor",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Configurable Markdown editor for SolidJS: editor, toolbar, bubble menu, and slash menu on one shared state.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -69,7 +69,7 @@
69
69
  "@codemirror/state": "6.7.1",
70
70
  "@codemirror/view": "6.43.7",
71
71
  "@k2b/stdlib": "0.30.0",
72
- "@k2b/ui": "0.40.0",
72
+ "@k2b/ui": "0.41.0",
73
73
  "@lezer/common": "1.5.3",
74
74
  "@lezer/highlight": "1.2.4",
75
75
  "@lezer/markdown": "1.7.2",
package/src/Toolbar.tsx CHANGED
@@ -1,7 +1,21 @@
1
1
  import { EditorView } from "@codemirror/view";
2
- import { Dropdown, type DropdownPosition, IconButton, Tooltip, Toolbar as UiToolbar, useLocale } from "@k2b/ui";
3
- import { createSignal, For, type JSX, onCleanup, onMount, Show } from "solid-js";
2
+ import {
3
+ createDictation,
4
+ type DictationAlternative,
5
+ DictationBar,
6
+ DictationButton,
7
+ type DictationSource,
8
+ Dropdown,
9
+ type DropdownPosition,
10
+ dictationBarShown,
11
+ IconButton,
12
+ Tooltip,
13
+ Toolbar as UiToolbar,
14
+ useLocale,
15
+ } from "@k2b/ui";
16
+ import { createEffect, createMemo, createSignal, For, type JSX, on, onCleanup, onMount, Show } from "solid-js";
4
17
  import { ariaShortcut, shortcutLabel } from "./actions";
18
+ import { dictationTarget } from "./dictation";
5
19
  import type { EditorAction, EditorHandle } from "./editor-state";
6
20
  import { editorMessages } from "./messages";
7
21
 
@@ -42,6 +56,12 @@ export type ToolbarProps = {
42
56
  * and give up the same height in the page above it, so the caret line stays visible.
43
57
  */
44
58
  liftAboveKeyboard?: boolean;
59
+ /**
60
+ * Live dictation as the first control after the action row, so it never scrolls away. While it runs, the dictation
61
+ * bar stands in for the action row in the same line; on a phone it fills the line. Without a source there is no
62
+ * microphone.
63
+ */
64
+ dictation?: { source: DictationSource; alternative?: DictationAlternative };
45
65
  };
46
66
 
47
67
  const isMenu = (item: ToolbarItem): item is ToolbarMenu => "kind" in item && item.kind === "menu";
@@ -114,6 +134,22 @@ export function Toolbar(props: ToolbarProps) {
114
134
  const [coarse, setCoarse] = createSignal(false);
115
135
  const [lift, setLift] = createSignal(0);
116
136
  let root!: HTMLDivElement;
137
+ const dictation = createDictation({
138
+ source: {
139
+ start: (options) => props.dictation!.source.start(options),
140
+ devices: () => props.dictation?.source.devices() ?? Promise.resolve([]),
141
+ },
142
+ get alternative() {
143
+ return props.dictation?.alternative;
144
+ },
145
+ target: dictationTarget(props.editor),
146
+ disabled: () => !props.dictation || !props.editor.state() || Boolean(props.editor.state()?.readOnly),
147
+ });
148
+ // A note that turns read-only, or a host that drops the source, ends dictation; the words so far stay.
149
+ // A memo compares the value, so ordinary edits never end it.
150
+ const dictationScope = createMemo(() => `${Boolean(props.dictation)}:${Boolean(props.editor.state()?.readOnly)}`);
151
+ createEffect(on(dictationScope, () => dictation.end(), { defer: true }));
152
+ const dictating = () => Boolean(props.dictation) && dictationBarShown(dictation);
117
153
 
118
154
  onMount(() => {
119
155
  const pointer = window.matchMedia("(pointer: coarse)");
@@ -173,34 +209,40 @@ export function Toolbar(props: ToolbarProps) {
173
209
  class={props.class ?? "k2b-editor-toolbar flex min-w-0 items-center gap-2 text-dimmed"}
174
210
  // The stylesheet sizes every control's icon here, the host's start and end controls included.
175
211
  data-k2b-editor-toolbar=""
212
+ data-dictating={dictating() ? "" : undefined}
176
213
  style={{ "margin-bottom": lift() ? `${lift()}px` : undefined }}
177
214
  on:mousedown={(event) => event.preventDefault()}
178
215
  >
179
216
  {props.start}
180
- <UiToolbar label={props.label ?? t().formatting} class={props.toolsClass ?? "no-scrollbar min-w-0 flex-1 overflow-x-auto"}>
181
- <For each={props.groups}>
182
- {(group, index) => (
183
- <>
184
- <UiToolbar.Group label={group.label} class={group.class ?? "shrink-0"}>
185
- <For each={group.items}>
186
- {(item) =>
187
- isCustom(item) ? (
188
- item.render()
189
- ) : isMenu(item) ? (
190
- <ToolbarMenuButton editor={props.editor} menu={item} coarse={coarse()} />
191
- ) : (
192
- <ToolbarButton editor={props.editor} action={item} />
193
- )
194
- }
195
- </For>
196
- </UiToolbar.Group>
197
- <Show when={index() < props.groups.length - 1}>
198
- <UiToolbar.Separator class={group.separatorClass} />
199
- </Show>
200
- </>
201
- )}
202
- </For>
203
- </UiToolbar>
217
+ <Show when={!dictating()} fallback={<DictationBar dictation={dictation} />}>
218
+ <UiToolbar label={props.label ?? t().formatting} class={props.toolsClass ?? "no-scrollbar min-w-0 flex-1 overflow-x-auto"}>
219
+ <For each={props.groups}>
220
+ {(group, index) => (
221
+ <>
222
+ <UiToolbar.Group label={group.label} class={group.class ?? "shrink-0"}>
223
+ <For each={group.items}>
224
+ {(item) =>
225
+ isCustom(item) ? (
226
+ item.render()
227
+ ) : isMenu(item) ? (
228
+ <ToolbarMenuButton editor={props.editor} menu={item} coarse={coarse()} />
229
+ ) : (
230
+ <ToolbarButton editor={props.editor} action={item} />
231
+ )
232
+ }
233
+ </For>
234
+ </UiToolbar.Group>
235
+ <Show when={index() < props.groups.length - 1}>
236
+ <UiToolbar.Separator class={group.separatorClass} />
237
+ </Show>
238
+ </>
239
+ )}
240
+ </For>
241
+ </UiToolbar>
242
+ </Show>
243
+ <Show when={props.dictation && !dictating()}>
244
+ <DictationButton dictation={dictation} size="xs" class="text-dimmed" />
245
+ </Show>
204
246
  {props.end}
205
247
  </div>
206
248
  );
@@ -0,0 +1,13 @@
1
+ import type { DictationTarget } from "@k2b/ui";
2
+ import { codeMirrorDictationTarget, dictationExtension } from "@k2b/ui/text-editor";
3
+ import type { EditorHandle } from "./editor-state";
4
+
5
+ /**
6
+ * A dictation target for the shared editor. Words go into the document at once, as typed text does, so a shared
7
+ * document shows them to everyone and they last as long as the host saves typed text. The mark follows local and
8
+ * remote edits; Discard removes exactly the dictated ranges in one step, including what the person typed inside them.
9
+ */
10
+ export function dictationTarget(editor: EditorHandle): DictationTarget {
11
+ editor.use(dictationExtension());
12
+ return codeMirrorDictationTarget(editor.view);
13
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ariaShortcut, isApple, markdownActions, shortcutLabel } from "./actions";
2
2
  export { BubbleMenu, type BubbleMenuProps } from "./BubbleMenu";
3
+ export { dictationTarget } from "./dictation";
3
4
  export { Editor, type EditorProps } from "./Editor";
4
5
  export { asUndoStep, createEditor, type EditorAction, type EditorHandle } from "./editor-state";
5
6
  export { editorMessages } from "./messages";
package/src/styles.css CHANGED
@@ -9,6 +9,19 @@
9
9
  }
10
10
  }
11
11
 
12
+ /* The dictation bar's controls match the toolbar's own, so the row keeps its height when the bar stands in. */
13
+ [data-k2b-editor-toolbar] .k2b-dictation-bar {
14
+ --k2b-dictation-control: 1.75rem;
15
+ }
16
+
17
+ /* While dictating, the bar stands in for the action row. On a phone it fills the whole toolbar line; the host's start
18
+ and end controls come back when dictation ended. */
19
+ @media (max-width: 47.999rem) {
20
+ [data-k2b-editor-toolbar][data-dictating] > :not(.k2b-dictation-bar) {
21
+ display: none;
22
+ }
23
+ }
24
+
12
25
  /* ── Slash-command autocomplete palette ─────────────────────────────────
13
26
  The CodeMirror autocomplete tooltip is portaled outside the editor
14
27
  surface AND its theme is injected as a runtime `<style>` after this file