@sanity/block-insert-picker 0.0.1 → 1.0.1

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/index.js ADDED
@@ -0,0 +1,1195 @@
1
+ import { c } from "react/compiler-runtime";
2
+ import { useEditor } from "@portabletext/editor";
3
+ import { Box, Card, Flex, Hotkeys, Popover, Stack, Text, useToast } from "@sanity/ui";
4
+ import { useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
5
+ import { useFormCallbacks, useResolveInitialValueForType, useSchema } from "sanity";
6
+ import { defineBehavior, effect, forward, raise } from "@portabletext/editor/behaviors";
7
+ import { blockOffsetsToSelection, getTextBlockText, isTextBlock } from "@portabletext/editor/utils";
8
+ import { PortableTextMemberItemsContext, PortableTextMemberSchemaTypesContext } from "sanity/_singletons";
9
+ import { getBlockTextBefore, getFirstBlock, getFocusBlock, getFocusTextBlock, isSelectionCollapsed } from "@portabletext/editor/selectors";
10
+ import { defineInputRule, defineInputRuleBehavior } from "@portabletext/plugin-input-rule";
11
+ import { BlockElementIcon } from "@sanity/icons/BlockElement";
12
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
+ /**
14
+ * The member's own name plus every name up its compiled `type` chain, most
15
+ * specific first: `{type: 'image', name: 'photo'}` yields `['photo',
16
+ * 'image', ...]`. Metadata and presets match against this chain so curation
17
+ * can target either the member name or the underlying type name.
18
+ */
19
+ function typeNameChain(memberType) {
20
+ let names = [], current = memberType;
21
+ for (; current && typeof current.name == "string";) names.push(current.name), current = current.type && typeof current.type == "object" && "name" in current.type ? current.type : void 0;
22
+ return names;
23
+ }
24
+ /**
25
+ * Derives picker items from a portable-text array's insertable member types
26
+ * (see PickerItemsContext), so the picker automatically covers every object
27
+ * block the schema allows. The optional metadata list curates triggers,
28
+ * keywords, grouping, badges, visibility, and descriptions per member type;
29
+ * its array order is the sort rank (see PickerItemMetadata). Titles and
30
+ * icons intentionally stay unresolved here: BlockInsertPicker resolves
31
+ * presentation from the member schema type, which carries member-level
32
+ * config (a member-specific icon, a per-member title override).
33
+ */
34
+ function derivePickerItems(context, metadata = []) {
35
+ return context.memberTypes.map((memberType, schemaIndex) => {
36
+ let chain = typeNameChain(memberType), nameRank = metadata.findIndex((entry) => entry.type === memberType.name), rank = nameRank === -1 ? metadata.findIndex((entry) => chain.includes(entry.type)) : nameRank;
37
+ return {
38
+ entry: rank === -1 ? void 0 : metadata[rank],
39
+ name: memberType.name,
40
+ rank: rank === -1 ? 2 ** 53 - 1 : rank,
41
+ schemaIndex
42
+ };
43
+ }).filter(({ entry }) => !entry?.hidden).sort((a, b) => a.rank - b.rank || a.schemaIndex - b.schemaIndex).map(({ entry, name }) => ({
44
+ action: {
45
+ blockType: name,
46
+ type: "insertBlock"
47
+ },
48
+ badge: entry?.badge,
49
+ description: entry?.description,
50
+ group: entry?.group,
51
+ id: name,
52
+ keywords: entry?.keywords,
53
+ openOnInsert: entry?.openOnInsert,
54
+ title: "",
55
+ trigger: entry?.trigger
56
+ }));
57
+ }
58
+ /**
59
+ * Metadata entries whose `type` matches neither a member name nor any name
60
+ * in a member's resolved type chain — almost always a typo. Surfaced as a
61
+ * dev-mode warning by BlockInsertPicker.
62
+ */
63
+ function unknownMetadataTypes(context, metadata) {
64
+ let known = new Set(context.memberTypes.flatMap((memberType) => typeNameChain(memberType)));
65
+ return metadata.map((entry) => entry.type).filter((type) => !known.has(type));
66
+ }
67
+ function filterPickerItems(items, query) {
68
+ if (query === "" || query === "/") return items;
69
+ let queryLower = query.toLowerCase(), bareQuery = queryLower.startsWith("/") ? queryLower.slice(1) : queryLower;
70
+ return items.filter((item) => {
71
+ let bareTrigger = item.trigger?.toLowerCase().replace(/^\//, "");
72
+ return bareTrigger && bareQuery !== "" && bareTrigger.startsWith(bareQuery) ? !0 : bareQuery !== "" && !!(item.title.toLowerCase().includes(bareQuery) || item.description?.toLowerCase().includes(bareQuery) || item.keywords?.some((kw) => kw.toLowerCase().includes(bareQuery)));
73
+ });
74
+ }
75
+ /**
76
+ * Flattens sections back into the single index space that keyboard navigation,
77
+ * selection, and scroll-into-view all share. Rendering the sections and
78
+ * indexing this array must stay derived from the same `groupPickerItems` call
79
+ * so Enter inserts exactly the highlighted row.
80
+ */
81
+ function flattenSections(sections) {
82
+ return sections.flatMap((section) => section.items);
83
+ }
84
+ /**
85
+ * Partitions items into sections by their `group`, preserving the incoming
86
+ * order within each section and ordering the sections by first appearance.
87
+ *
88
+ * Because the caller feeds rank-ordered items (see derivePickerItems), the
89
+ * first-appearance rule places the section holding the most-used block first,
90
+ * without a separate ordering constant to drift out of sync. Items without a
91
+ * group collapse into a single `group: null` section (e.g. a curated `items`
92
+ * prop), so the flat/ungrouped case stays a no-op reordering.
93
+ */
94
+ function groupPickerItems(items) {
95
+ let order = [], byGroup = /* @__PURE__ */ new Map();
96
+ for (let item of items) {
97
+ let key = item.group ?? null, bucket = byGroup.get(key);
98
+ bucket || (bucket = [], byGroup.set(key, bucket), order.push(key)), bucket.push(item);
99
+ }
100
+ return order.map((key) => ({
101
+ group: key,
102
+ items: byGroup.get(key)
103
+ }));
104
+ }
105
+ /**
106
+ * Performs the picker insertion as a single behavior action set so the whole
107
+ * operation is one undo step, and cleans up the typed "/query" text with
108
+ * delete.text over exactly that range (never delete.block) so text that
109
+ * already existed in the anchor block can't be destroyed: the caret can sit
110
+ * at offset 0 of a populated block and still open the picker.
111
+ */
112
+ function createInsertBehavior() {
113
+ return defineBehavior({
114
+ actions: [(_, { anchor, block, cleanup, placement }) => [...cleanup ? [raise({
115
+ at: cleanup,
116
+ type: "delete.text"
117
+ })] : [], raise({
118
+ block,
119
+ placement,
120
+ select: "start",
121
+ type: "insert.block",
122
+ ...anchor ? { at: anchor } : {}
123
+ })]],
124
+ guard: ({ event, snapshot }) => {
125
+ let { anchorBlockKey, block, mode } = event;
126
+ return {
127
+ anchor: snapshot.context.value.some((candidate) => candidate._key === anchorBlockKey) ? blockOffsetsToSelection({
128
+ offsets: {
129
+ anchor: {
130
+ offset: 0,
131
+ path: [{ _key: anchorBlockKey }]
132
+ },
133
+ focus: {
134
+ offset: 0,
135
+ path: [{ _key: anchorBlockKey }]
136
+ }
137
+ },
138
+ snapshot
139
+ }) : null,
140
+ block,
141
+ cleanup: resolveQueryCleanup(event, snapshot),
142
+ placement: mode === "slash" ? "auto" : "after"
143
+ };
144
+ },
145
+ on: "custom.blockInsertPicker.insert"
146
+ });
147
+ }
148
+ function sendInsertPickerItem(editor, payload) {
149
+ editor.send({
150
+ type: "custom.blockInsertPicker.insert",
151
+ ...payload
152
+ });
153
+ }
154
+ /**
155
+ * The query-cleanup half of the insert behavior on its own, for `custom`
156
+ * picker actions that don't insert a block: the typed "/query" text still
157
+ * leaves the document before the host's `onSelect` runs, under the same
158
+ * only-if-unchanged guard as a real insert.
159
+ */
160
+ function createCleanupQueryBehavior() {
161
+ return defineBehavior({
162
+ actions: [(_, cleanup) => [raise({
163
+ at: cleanup,
164
+ type: "delete.text"
165
+ })]],
166
+ guard: ({ event, snapshot }) => resolveQueryCleanup(event, snapshot) ?? !1,
167
+ on: "custom.blockInsertPicker.cleanupQuery"
168
+ });
169
+ }
170
+ function sendCleanupQuery(editor, payload) {
171
+ editor.send({
172
+ type: "custom.blockInsertPicker.cleanupQuery",
173
+ ...payload
174
+ });
175
+ }
176
+ /**
177
+ * The selection spanning the typed "/query" text in the anchor block, when it
178
+ * is safe to delete. Only slash mode leaves query text in the document, and
179
+ * only when the anchor block still starts with the typed query; if it changed
180
+ * underneath us (collaborator edit, uncaptured caret movement) the action is
181
+ * still the user's intent, but nothing may be deleted.
182
+ */
183
+ function resolveQueryCleanup(event, snapshot) {
184
+ let { anchorBlockKey, mode, query } = event;
185
+ if (mode !== "slash" || query.length === 0) return null;
186
+ let anchorBlock = snapshot.context.value.find((candidate) => candidate._key === anchorBlockKey);
187
+ return !anchorBlock || !isTextBlock({ schema: snapshot.context.schema }, anchorBlock) || !getTextBlockText(anchorBlock).startsWith(query) ? null : blockOffsetsToSelection({
188
+ offsets: {
189
+ anchor: {
190
+ offset: 0,
191
+ path: [{ _key: anchorBlockKey }]
192
+ },
193
+ focus: {
194
+ offset: query.length,
195
+ path: [{ _key: anchorBlockKey }]
196
+ }
197
+ },
198
+ snapshot
199
+ });
200
+ }
201
+ function insertPickerItem(ctx) {
202
+ let blockKey = ctx.keyGenerator();
203
+ sendInsertPickerItem(ctx.editor, {
204
+ anchorBlockKey: ctx.anchorBlockKey,
205
+ block: {
206
+ ...ctx.initialValue,
207
+ _key: blockKey,
208
+ _type: ctx.blockType
209
+ },
210
+ mode: ctx.mode,
211
+ query: ctx.query
212
+ }), ctx.onInsertedKey?.(blockKey);
213
+ }
214
+ const DEFAULT_LABELS = {
215
+ footerAnywhere: "Anywhere",
216
+ footerDismiss: "Dismiss",
217
+ footerInsert: "Insert",
218
+ footerNavigate: "Navigate",
219
+ insertAnchorRemoved: "The block you were inserting into was removed",
220
+ insertError: "Could not insert block",
221
+ noMatches: "No matches for \"{query}\"",
222
+ positionAnnouncement: "{index} of {count}",
223
+ title: "Insert block",
224
+ ungroupedSection: "Other blocks"
225
+ };
226
+ function resolveLabels(overrides) {
227
+ return overrides ? {
228
+ ...DEFAULT_LABELS,
229
+ ...overrides
230
+ } : DEFAULT_LABELS;
231
+ }
232
+ /** Fills the `{query}` placeholder in the no-matches label. */
233
+ function formatNoMatches(labels, query) {
234
+ return labels.noMatches.replace("{query}", () => query);
235
+ }
236
+ /** Fills `{index}`/`{count}` in the screen-reader position announcement. */
237
+ function formatPosition(labels, index, count) {
238
+ return labels.positionAnnouncement.replace("{index}", () => String(index)).replace("{count}", () => String(count));
239
+ }
240
+ function usePickerItemsContext(arrayTypeName) {
241
+ let $ = c(7), memberSchemaTypes = useContext(PortableTextMemberSchemaTypesContext), schema = useSchema(), t0;
242
+ if ($[0] !== arrayTypeName || $[1] !== memberSchemaTypes || $[2] !== schema) {
243
+ bb0: {
244
+ if (memberSchemaTypes) {
245
+ let t1;
246
+ $[4] !== memberSchemaTypes.blockObjects || $[5] !== memberSchemaTypes.portableText ? (t1 = {
247
+ memberTypes: memberSchemaTypes.blockObjects,
248
+ schemaType: memberSchemaTypes.portableText
249
+ }, $[4] = memberSchemaTypes.blockObjects, $[5] = memberSchemaTypes.portableText, $[6] = t1) : t1 = $[6], t0 = t1;
250
+ break bb0;
251
+ }
252
+ let arrayType = arrayTypeName ? schema.get(arrayTypeName) : void 0;
253
+ if (!arrayType || arrayType.jsonType !== "array") {
254
+ t0 = null;
255
+ break bb0;
256
+ }
257
+ t0 = {
258
+ memberTypes: arrayType.of.filter(_temp$1),
259
+ schemaType: arrayType
260
+ };
261
+ }
262
+ $[0] = arrayTypeName, $[1] = memberSchemaTypes, $[2] = schema, $[3] = t0;
263
+ } else t0 = $[3];
264
+ return t0;
265
+ }
266
+ /**
267
+ * Whether a member resolves to the built-in text block: walk the compiled
268
+ * `type` chain to its root and compare the root's name — the same algorithm
269
+ * Studio's schema bridge uses to bucketize `of`. A literal `name === 'block'`
270
+ * check would miss aliased text blocks (`{name: 'myBlock', type: 'block'}`)
271
+ * and wrongly offer them as object inserts.
272
+ */
273
+ function _temp$1(member) {
274
+ return member.jsonType === "object" && !isTextBlockMember(member);
275
+ }
276
+ function isTextBlockMember(memberType) {
277
+ let current = memberType;
278
+ for (; current.type && typeof current.type == "object" && "name" in current.type;) current = current.type;
279
+ return current.name === "block";
280
+ }
281
+ /**
282
+ * Opens a just-inserted block's editing UI (dialog/popover per the member's
283
+ * config) once its member item mounts. An insert lands synchronously, but the
284
+ * member item only exists on a later render — so callers schedule the key and
285
+ * the effect opens it when it appears.
286
+ *
287
+ * Together with memberSchemaTypes.ts, this module is one of exactly two
288
+ * importers of `sanity/_singletons` in this plugin: the subpath is internal
289
+ * with no stability guarantee, so each dependency stays quarantined where it
290
+ * can be swapped for a public API in one place. Consumers that need to react
291
+ * to inserts without this internal should use the `onInsert` seam on the
292
+ * plugin components instead.
293
+ */
294
+ function useOpenBlockOnInsert() {
295
+ let $ = c(6), memberItems = useContext(PortableTextMemberItemsContext), { onPathOpen, onSetPathCollapsed } = useFormCallbacks(), pendingKeyRef = useRef(null), t0, t1;
296
+ $[0] !== memberItems || $[1] !== onPathOpen || $[2] !== onSetPathCollapsed ? (t0 = () => {
297
+ let key = pendingKeyRef.current;
298
+ if (!key) return;
299
+ let item = memberItems.find((m) => m.member.key === key);
300
+ item && (onPathOpen(item.member.item.path), onSetPathCollapsed(item.member.item.path, !1), pendingKeyRef.current = null);
301
+ }, t1 = [
302
+ memberItems,
303
+ onPathOpen,
304
+ onSetPathCollapsed
305
+ ], $[0] = memberItems, $[1] = onPathOpen, $[2] = onSetPathCollapsed, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1);
306
+ let t2;
307
+ return $[5] === Symbol.for("react.memo_cache_sentinel") ? (t2 = (blockKey) => {
308
+ pendingKeyRef.current = blockKey;
309
+ }, $[5] = t2) : t2 = $[5], t2;
310
+ }
311
+ function createPickerBehavior({ getState, isShortcutEnabled, onIntent }) {
312
+ return defineBehavior({
313
+ actions: [(payload, guardResponse) => {
314
+ let { blockEvent, intent } = guardResponse, sideEffect = effect(() => onIntent(intent));
315
+ return blockEvent ? [sideEffect] : [sideEffect, forward(payload.event)];
316
+ }],
317
+ guard: ({ event, snapshot }) => {
318
+ let state = getState();
319
+ if (state.mode === "closed") {
320
+ if (event.type === "insert.text" && event.text === "/") {
321
+ let focus = getFocusTextBlock(snapshot);
322
+ return !focus || !isSelectionCollapsed(snapshot) || getBlockTextBefore(snapshot) !== "" ? !1 : {
323
+ blockEvent: !1,
324
+ intent: {
325
+ anchorBlockKey: focus.node._key,
326
+ mode: "slash",
327
+ query: "/",
328
+ type: "open"
329
+ }
330
+ };
331
+ }
332
+ if (event.type === "keyboard.keydown" && event.originEvent.key === "/" && (event.originEvent.metaKey || event.originEvent.ctrlKey) && isShortcutEnabled()) {
333
+ let focus = getFocusBlock(snapshot), fallback = getFirstBlock(snapshot), anchorBlock = focus ?? fallback;
334
+ return anchorBlock ? {
335
+ blockEvent: !0,
336
+ intent: {
337
+ anchorBlockKey: anchorBlock.node._key,
338
+ mode: "shortcut",
339
+ query: "",
340
+ type: "open"
341
+ }
342
+ } : !1;
343
+ }
344
+ return !1;
345
+ }
346
+ if (event.type === "insert.text") return state.mode === "slash" ? event.text === " " ? {
347
+ blockEvent: !1,
348
+ intent: { type: "close" }
349
+ } : {
350
+ blockEvent: !1,
351
+ intent: {
352
+ query: state.query + event.text,
353
+ type: "updateQuery"
354
+ }
355
+ } : {
356
+ blockEvent: !0,
357
+ intent: {
358
+ query: state.query + event.text,
359
+ type: "updateQuery"
360
+ }
361
+ };
362
+ if (event.type === "delete.backward") return state.mode === "shortcut" ? state.query.length === 0 ? {
363
+ blockEvent: !0,
364
+ intent: { type: "close" }
365
+ } : {
366
+ blockEvent: !0,
367
+ intent: {
368
+ query: event.unit === "character" ? state.query.slice(0, -1) : "",
369
+ type: "updateQuery"
370
+ }
371
+ } : event.unit === "character" && state.query.length > 1 ? {
372
+ blockEvent: !1,
373
+ intent: {
374
+ query: state.query.slice(0, -1),
375
+ type: "updateQuery"
376
+ }
377
+ } : {
378
+ blockEvent: !1,
379
+ intent: { type: "close" }
380
+ };
381
+ if (event.type === "delete.forward") return {
382
+ blockEvent: !1,
383
+ intent: { type: "close" }
384
+ };
385
+ if (event.type === "keyboard.keydown") {
386
+ let key = event.originEvent.key;
387
+ if (key === "ArrowDown") return {
388
+ blockEvent: !0,
389
+ intent: {
390
+ delta: 1,
391
+ type: "navigate"
392
+ }
393
+ };
394
+ if (key === "ArrowUp") return {
395
+ blockEvent: !0,
396
+ intent: {
397
+ delta: -1,
398
+ type: "navigate"
399
+ }
400
+ };
401
+ if (key === "Enter") return {
402
+ blockEvent: !0,
403
+ intent: { type: "select" }
404
+ };
405
+ if (key === "Escape") return {
406
+ blockEvent: !0,
407
+ intent: { type: "close" }
408
+ };
409
+ if (key === "ArrowLeft" || key === "ArrowRight" || key === "Home" || key === "End" || key === "PageUp" || key === "PageDown") return {
410
+ blockEvent: !1,
411
+ intent: { type: "close" }
412
+ };
413
+ }
414
+ return !1;
415
+ },
416
+ on: "*"
417
+ });
418
+ }
419
+ function pickerReducer(state, intent) {
420
+ switch (intent.type) {
421
+ case "close":
422
+ case "select": return { mode: "closed" };
423
+ case "navigate": return state.mode === "closed" ? state : {
424
+ ...state,
425
+ highlightedIndex: state.highlightedIndex + intent.delta
426
+ };
427
+ case "open": return state.mode === "closed" ? {
428
+ anchorBlockKey: intent.anchorBlockKey,
429
+ highlightedIndex: 0,
430
+ mode: intent.mode,
431
+ query: intent.query
432
+ } : state;
433
+ case "setHighlightedIndex": return state.mode === "closed" ? state : {
434
+ ...state,
435
+ highlightedIndex: intent.index
436
+ };
437
+ case "updateQuery": return state.mode === "closed" ? state : {
438
+ ...state,
439
+ highlightedIndex: 0,
440
+ query: intent.query
441
+ };
442
+ default: return intent;
443
+ }
444
+ }
445
+ /**
446
+ * Builds InputRules for the configs whose block type the host array actually
447
+ * allows, so a rule can't insert a block the field would reject.
448
+ */
449
+ function createMarkdownInputRules({ allowedBlockTypes, keyGenerator, onInserted, rules }) {
450
+ return rules.filter((config) => allowedBlockTypes.has(config.blockType)).map((config) => createInsertRule(config, keyGenerator, onInserted));
451
+ }
452
+ const CODE_FENCE_PATTERN = /^`{3}\S*\s$/, BLOCKQUOTE_PATTERN = /^>\s$/;
453
+ /** Sugar: a blockquote rule ("> " at the start of a block) for `blockType`. */
454
+ function blockquoteRule(options) {
455
+ return {
456
+ blockType: options.blockType,
457
+ buildValue: ({ keyGenerator }) => options.createValue?.({ keyGenerator }) ?? {},
458
+ pattern: BLOCKQUOTE_PATTERN
459
+ };
460
+ }
461
+ /**
462
+ * Sugar: a code-fence rule ("```lang␣") for `blockType`. The language table
463
+ * resolves fence tokens (aliases included) to the values the block's language
464
+ * selector stores, plus an optional filename; tokens outside the table pass
465
+ * through as typed so nothing the writer means is lost. `createValue` turns
466
+ * the resolved language and filename into the block's fields.
467
+ */
468
+ function codeFenceRule(options) {
469
+ let languages = options.languages ?? [];
470
+ return {
471
+ blockType: options.blockType,
472
+ buildValue: ({ keyGenerator, matchText }) => {
473
+ let language = normalizeFenceLanguage(fenceLanguageFromMatch(matchText), languages, options.defaultLanguage), filename = language ? languages.find((entry) => entry.value === language)?.filename : void 0;
474
+ return options.createValue({
475
+ filename,
476
+ keyGenerator,
477
+ language
478
+ });
479
+ },
480
+ pattern: CODE_FENCE_PATTERN
481
+ };
482
+ }
483
+ /** Strips the leading "```" fence and surrounding whitespace from a match. */
484
+ function fenceLanguageFromMatch(matchText) {
485
+ return matchText.replace(/^`{3}/, "").trim();
486
+ }
487
+ /**
488
+ * Resolves a typed fence token against a language table: a canonical value,
489
+ * an alias (`py`→python, `yml`→yaml), or — for anything the table doesn't
490
+ * know — the token as typed. An empty token yields `defaultLanguage`.
491
+ */
492
+ function normalizeFenceLanguage(raw, languages, defaultLanguage) {
493
+ let token = raw.trim(), lower = token.toLowerCase();
494
+ if (!lower) return defaultLanguage;
495
+ for (let { aliases, value } of languages) if (value.toLowerCase() === lower || aliases?.some((alias) => alias.toLowerCase() === lower)) return value;
496
+ return token;
497
+ }
498
+ function createInsertRule(config, keyGenerator, onInserted) {
499
+ return defineInputRule({
500
+ actions: [({ event, snapshot }) => {
501
+ let match = event.matches[0];
502
+ if (!match) return [];
503
+ let block = {
504
+ ...config.buildValue({
505
+ keyGenerator,
506
+ matchText: match.text
507
+ }),
508
+ _key: keyGenerator(),
509
+ _type: config.blockType
510
+ };
511
+ return [
512
+ raise({
513
+ at: blockOffsetsToSelection({
514
+ offsets: {
515
+ anchor: match.targetOffsets.anchor,
516
+ focus: match.targetOffsets.focus
517
+ },
518
+ snapshot
519
+ }) ?? match.selection,
520
+ type: "delete.text"
521
+ }),
522
+ raise({
523
+ block,
524
+ placement: "auto",
525
+ select: "start",
526
+ type: "insert.block"
527
+ }),
528
+ effect(() => onInserted(block))
529
+ ];
530
+ }],
531
+ on: config.pattern
532
+ });
533
+ }
534
+ /**
535
+ * Default triggers and search keywords for block types with fixed, official
536
+ * names: Studio's intrinsic types (`image`, `file`), first-party plugin
537
+ * types (`code`, `table`, `color`, `latex`, `mux.video`), and the Media
538
+ * Library's `sanity.video`. Presets are deliberately presentation-only —
539
+ * they never add items (a type must actually be a member of the array),
540
+ * never set groups (partial grouping would strand unmatched items in an
541
+ * "Other" bucket), never affect rank, and always lose to a host's `items`
542
+ * entry for the same type. Opt out entirely with `presets: false`.
543
+ */
544
+ const standardBlockPresets = [
545
+ {
546
+ keywords: [
547
+ "photo",
548
+ "picture",
549
+ "figure",
550
+ "media"
551
+ ],
552
+ trigger: "/image",
553
+ type: "image"
554
+ },
555
+ {
556
+ keywords: [
557
+ "attachment",
558
+ "download",
559
+ "document"
560
+ ],
561
+ trigger: "/file",
562
+ type: "file"
563
+ },
564
+ {
565
+ keywords: [
566
+ "snippet",
567
+ "syntax",
568
+ "fence",
569
+ "pre"
570
+ ],
571
+ trigger: "/code",
572
+ type: "code"
573
+ },
574
+ {
575
+ keywords: [
576
+ "grid",
577
+ "rows",
578
+ "columns",
579
+ "spreadsheet"
580
+ ],
581
+ trigger: "/table",
582
+ type: "table"
583
+ },
584
+ {
585
+ keywords: ["swatch", "palette"],
586
+ trigger: "/color",
587
+ type: "color"
588
+ },
589
+ {
590
+ keywords: [
591
+ "formula",
592
+ "equation",
593
+ "math",
594
+ "tex",
595
+ "katex"
596
+ ],
597
+ trigger: "/latex",
598
+ type: "latex"
599
+ },
600
+ {
601
+ keywords: [
602
+ "video",
603
+ "player",
604
+ "mux"
605
+ ],
606
+ trigger: "/video",
607
+ type: "mux.video"
608
+ },
609
+ {
610
+ keywords: [
611
+ "video",
612
+ "player",
613
+ "media"
614
+ ],
615
+ trigger: "/video",
616
+ type: "sanity.video"
617
+ }
618
+ ], wellKnownInputRules = [codeFenceRule({
619
+ blockType: "code",
620
+ createValue: ({ language }) => language ? { language } : {}
621
+ })];
622
+ /**
623
+ * Fills preset triggers and keywords into derived items that don't already
624
+ * have them, matching presets against each member's resolved type chain so
625
+ * `{type: 'image', name: 'photo'}` still gets the image preset. Host `items`
626
+ * metadata has already been folded in by derivePickerItems, so anything it
627
+ * set wins by construction.
628
+ */
629
+ function applyPresetMetadata(items, context, presets = standardBlockPresets) {
630
+ let presetByType = new Map(presets.map((preset) => [preset.type, preset]));
631
+ return items.map((item) => {
632
+ if (item.action.type !== "insertBlock" || item.trigger !== void 0 && item.keywords !== void 0) return item;
633
+ let blockType = item.action.blockType, memberType = context.memberTypes.find((candidate) => candidate.name === blockType);
634
+ if (!memberType) return item;
635
+ let preset = typeNameChain(memberType).map((name) => presetByType.get(name)).find(Boolean);
636
+ return preset ? {
637
+ ...item,
638
+ keywords: item.keywords ?? preset.keywords,
639
+ trigger: item.trigger ?? preset.trigger
640
+ } : item;
641
+ });
642
+ }
643
+ /**
644
+ * Resolves what a picker row shows for an item, following the same fallback
645
+ * chains Studio's built-in insert menu uses: icon from the member type, its
646
+ * parent type, a reference target, then the generic block icon; title from
647
+ * the member type or its capitalized name. The curated description (from
648
+ * `items` metadata) wins over the schema type's own — curation is the more
649
+ * specific intent.
650
+ */
651
+ function resolveItemPresentation(item, schemaType) {
652
+ return schemaType ? {
653
+ description: item.description ?? schemaType.description,
654
+ icon: schemaType.icon ?? schemaType.type?.icon ?? schemaType.to?.[0]?.icon ?? BlockElementIcon,
655
+ title: schemaType.title ?? upperFirst(schemaType.name)
656
+ } : {
657
+ description: item.description,
658
+ icon: item.icon ?? BlockElementIcon,
659
+ title: item.title || (item.action.type === "insertBlock" ? item.action.blockType : item.id)
660
+ };
661
+ }
662
+ function upperFirst(value) {
663
+ return value.charAt(0).toUpperCase() + value.slice(1);
664
+ }
665
+ function BlockInsertPicker({ arrayTypeName, filter, items, labels: labelOverrides, onInsert, openOnInsert = !0, presets = !0, resolveItems, shortcut = !0 }) {
666
+ let editor = useEditor(), openBlockOnInsert = useOpenBlockOnInsert(), itemsContext = usePickerItemsContext(arrayTypeName), schema = useSchema(), resolveInitialValueForType = useResolveInitialValueForType(), toast = useToast(), labels = useMemo(() => resolveLabels(labelOverrides), [labelOverrides]), instanceId = useId(), listboxId = `${instanceId}-listbox`, optionId = (id) => `${instanceId}-option-${id}`, pickerItems = useMemo(() => {
667
+ if (!itemsContext) return [];
668
+ let presented = (presets ? applyPresetMetadata(derivePickerItems(itemsContext, items), itemsContext) : derivePickerItems(itemsContext, items)).map((item) => {
669
+ if (item.action.type !== "insertBlock") return item;
670
+ let presentation = resolveItemPresentation(item, memberTypeFor(itemsContext, schema, item.action.blockType));
671
+ return Object.assign(item, {
672
+ description: presentation.description,
673
+ icon: presentation.icon,
674
+ title: presentation.title
675
+ });
676
+ });
677
+ return resolveItems ? [...resolveItems(presented, itemsContext)] : presented;
678
+ }, [
679
+ items,
680
+ itemsContext,
681
+ presets,
682
+ resolveItems,
683
+ schema
684
+ ]), hasItems = pickerItems.length > 0, warnedRef = useRef({});
685
+ useEffect(() => {
686
+ if (process.env.NODE_ENV === "production") return;
687
+ let warned = warnedRef.current;
688
+ if (!itemsContext && !warned.context) {
689
+ warned.context = !0, console.warn("[@sanity/block-insert-picker] No Portable Text member schema context found and no `arrayTypeName` resolved to an array type — the picker is disabled for this field.");
690
+ return;
691
+ }
692
+ if (itemsContext && items && !warned.unknown) {
693
+ let unknown = unknownMetadataTypes(itemsContext, items);
694
+ unknown.length > 0 && (warned.unknown = !0, console.warn(`[@sanity/block-insert-picker] \`items\` metadata references types that are not members of this array: ${unknown.join(", ")}`));
695
+ }
696
+ itemsContext && !hasItems && !warned.empty && (warned.empty = !0, console.warn("[@sanity/block-insert-picker] No insertable items: the array has no non-text-block members (and `resolveItems` added none) — the picker is disabled for this field."));
697
+ }, [
698
+ hasItems,
699
+ items,
700
+ itemsContext
701
+ ]);
702
+ let [state, dispatch] = useReducer(pickerReducer, { mode: "closed" }), stateRef = useRef(state);
703
+ useLayoutEffect(() => {
704
+ stateRef.current = state;
705
+ });
706
+ let shortcutRef = useRef(shortcut);
707
+ useLayoutEffect(() => {
708
+ shortcutRef.current = shortcut;
709
+ });
710
+ let open = state.mode !== "closed", query = open ? state.query : "", mode = open ? state.mode : null, filterItems = filter ?? filterPickerItems, filterQuery = mode === "slash" ? bareQuery(query) : query, sections = useMemo(() => groupPickerItems(filterItems(pickerItems, filterQuery)), [
711
+ filterItems,
712
+ filterQuery,
713
+ pickerItems
714
+ ]), filtered = useMemo(() => flattenSections(sections), [sections]), sectionOffsets = useMemo(() => {
715
+ let offsets = [], total = 0;
716
+ for (let section of sections) offsets.push(total), total += section.items.length;
717
+ return offsets;
718
+ }, [sections]), showGroupHeaders = sections.length > 1, insertInFlightRef = useRef(!1), insertItemAtIndex = async (current, idx) => {
719
+ if (insertInFlightRef.current) {
720
+ dispatch({ type: "close" });
721
+ return;
722
+ }
723
+ insertInFlightRef.current = !0;
724
+ try {
725
+ let visible = flattenSections(groupPickerItems(filterItems(pickerItems, current.mode === "slash" ? bareQuery(current.query) : current.query))), chosen = visible[Math.max(0, Math.min(visible.length - 1, idx))];
726
+ if (dispatch({ type: "close" }), !chosen) return;
727
+ if (chosen.action.type === "custom") {
728
+ sendCleanupQuery(editor, {
729
+ anchorBlockKey: current.anchorBlockKey,
730
+ mode: current.mode,
731
+ query: current.query
732
+ }), chosen.action.onSelect({
733
+ closePicker: () => dispatch({ type: "close" }),
734
+ editor
735
+ });
736
+ return;
737
+ }
738
+ let blockType = chosen.action.blockType, initialValue, schemaType_0 = memberTypeFor(itemsContext, schema, blockType);
739
+ schemaType_0 && (initialValue = await resolveInitialValueForType(schemaType_0, {}) ?? void 0);
740
+ let snapshot = editor.getSnapshot();
741
+ if (!snapshot.context.value.some((b) => b._key === current.anchorBlockKey)) {
742
+ toast.push({
743
+ closable: !0,
744
+ description: labels.insertAnchorRemoved,
745
+ status: "warning",
746
+ title: labels.insertError
747
+ });
748
+ return;
749
+ }
750
+ insertPickerItem({
751
+ anchorBlockKey: current.anchorBlockKey,
752
+ blockType,
753
+ editor,
754
+ initialValue,
755
+ keyGenerator: () => snapshot.context.keyGenerator(),
756
+ mode: current.mode,
757
+ onInsertedKey: (key) => {
758
+ (chosen.openOnInsert ?? openOnInsert) && openBlockOnInsert(key), onInsert?.({
759
+ blockKey: key,
760
+ blockType,
761
+ mode: current.mode,
762
+ query: current.query,
763
+ via: "picker"
764
+ });
765
+ },
766
+ query: current.query
767
+ });
768
+ } catch (error) {
769
+ toast.push({
770
+ closable: !0,
771
+ description: error instanceof Error ? error.message : String(error),
772
+ status: "error",
773
+ title: labels.insertError
774
+ });
775
+ } finally {
776
+ insertInFlightRef.current = !1;
777
+ }
778
+ }, selectItemAtIndex = (idx_0) => {
779
+ let current_0 = stateRef.current;
780
+ current_0.mode !== "closed" && insertItemAtIndex(current_0, idx_0);
781
+ }, handleIntent = (intent) => {
782
+ if (intent.type === "navigate") {
783
+ let current_1 = stateRef.current;
784
+ if (current_1.mode === "closed") return;
785
+ let lastIndex = filtered.length - 1, base = Math.max(0, Math.min(lastIndex, current_1.highlightedIndex)), next = Math.max(0, Math.min(lastIndex, base + intent.delta));
786
+ dispatch({
787
+ index: next,
788
+ type: "setHighlightedIndex"
789
+ });
790
+ return;
791
+ }
792
+ if (intent.type !== "select") {
793
+ dispatch(intent);
794
+ return;
795
+ }
796
+ let current_2 = stateRef.current;
797
+ current_2.mode !== "closed" && insertItemAtIndex(current_2, current_2.highlightedIndex);
798
+ }, handleIntentRef = useRef(handleIntent);
799
+ useLayoutEffect(() => {
800
+ handleIntentRef.current = handleIntent;
801
+ }), useEffect(() => {
802
+ if (!hasItems) return;
803
+ let behavior = createPickerBehavior({
804
+ getState: () => stateRef.current,
805
+ isShortcutEnabled: () => shortcutRef.current,
806
+ onIntent: (intent_0) => handleIntentRef.current(intent_0)
807
+ }), unregisterPicker = editor.registerBehavior({ behavior }), unregisterInsert = editor.registerBehavior({ behavior: createInsertBehavior() }), unregisterCleanup = editor.registerBehavior({ behavior: createCleanupQueryBehavior() });
808
+ return () => {
809
+ unregisterPicker(), unregisterInsert(), unregisterCleanup();
810
+ };
811
+ }, [editor, hasItems]), useEffect(() => {
812
+ !hasItems && stateRef.current.mode !== "closed" && dispatch({ type: "close" });
813
+ }, [hasItems]);
814
+ let highlightedIndex = open ? Math.max(0, Math.min(filtered.length - 1, state.highlightedIndex)) : 0, highlightedItem = open ? filtered[highlightedIndex] : void 0, announcement = highlightedItem ? [
815
+ highlightedItem.title,
816
+ highlightedItem.description,
817
+ formatPosition(labels, highlightedIndex + 1, filtered.length)
818
+ ].filter(Boolean).join(", ") : "", [cursorRect, setCursorRect] = useState(null);
819
+ useLayoutEffect(() => {
820
+ if (!open) {
821
+ setCursorRect(null);
822
+ return;
823
+ }
824
+ let snapshot_0 = editor.getSnapshot(), rect = editor.dom.getSelectionRect(snapshot_0);
825
+ if (rect && (rect.x || rect.y || rect.width || rect.height)) {
826
+ setCursorRect(rect);
827
+ return;
828
+ }
829
+ let blockRect = editor.dom.getStartBlockElement(snapshot_0)?.getBoundingClientRect();
830
+ blockRect && setCursorRect(blockRect);
831
+ }, [editor, open]);
832
+ let popoverContentRef = useRef(null);
833
+ useEffect(() => {
834
+ if (!open) return;
835
+ let handlePointerDown = (event) => {
836
+ let target = event.target;
837
+ target instanceof Node && popoverContentRef.current?.contains(target) || dispatch({ type: "close" });
838
+ }, editorElement = editor.dom.getEditorElement(), handleFocusOut = (event_0) => {
839
+ let next_0 = event_0 instanceof FocusEvent ? event_0.relatedTarget : null;
840
+ next_0 instanceof Node && (editorElement?.contains(next_0) || popoverContentRef.current?.contains(next_0)) || dispatch({ type: "close" });
841
+ };
842
+ return document.addEventListener("pointerdown", handlePointerDown, !0), editorElement?.addEventListener("focusout", handleFocusOut), () => {
843
+ document.removeEventListener("pointerdown", handlePointerDown, !0), editorElement?.removeEventListener("focusout", handleFocusOut);
844
+ };
845
+ }, [editor, open]);
846
+ let highlightedRowRef = useRef(null), lastPointerRef = useRef(null), highlightOnPointerMove = (idx_1, x, y) => {
847
+ let last = lastPointerRef.current;
848
+ if (lastPointerRef.current = {
849
+ x,
850
+ y
851
+ }, !last || last.x === x && last.y === y) return;
852
+ let current_3 = stateRef.current;
853
+ current_3.mode !== "closed" && idx_1 !== current_3.highlightedIndex && dispatch({
854
+ index: idx_1,
855
+ type: "setHighlightedIndex"
856
+ });
857
+ };
858
+ useEffect(() => {
859
+ highlightedRowRef.current?.scrollIntoView({ block: "nearest" });
860
+ }, [highlightedIndex]);
861
+ let cursorElement = useMemo(() => cursorRect ? { getBoundingClientRect: () => cursorRect } : null, [cursorRect]);
862
+ return open ? filtered.length === 0 ? /* @__PURE__ */ jsx(Popover, {
863
+ constrainSize: !0,
864
+ content: /* @__PURE__ */ jsx(Card, {
865
+ "aria-live": "polite",
866
+ onMouseDown: preventFocusSteal,
867
+ padding: 3,
868
+ radius: 2,
869
+ ref: popoverContentRef,
870
+ role: "status",
871
+ children: /* @__PURE__ */ jsx(Text, {
872
+ muted: !0,
873
+ size: 1,
874
+ children: formatNoMatches(labels, query)
875
+ })
876
+ }),
877
+ fallbackPlacements: ["top-start"],
878
+ open: !0,
879
+ placement: "bottom-start",
880
+ portal: !0,
881
+ preventOverflow: !0,
882
+ referenceElement: cursorElement
883
+ }) : /* @__PURE__ */ jsx(Popover, {
884
+ constrainSize: !0,
885
+ content: /* @__PURE__ */ jsxs(Card, {
886
+ onMouseDown: preventFocusSteal,
887
+ padding: 1,
888
+ radius: 2,
889
+ ref: popoverContentRef,
890
+ style: {
891
+ display: "flex",
892
+ flexDirection: "column",
893
+ maxHeight: "100%",
894
+ minWidth: 300
895
+ },
896
+ children: [
897
+ /* @__PURE__ */ jsx("output", {
898
+ "aria-live": "polite",
899
+ style: SR_ONLY,
900
+ children: announcement
901
+ }),
902
+ /* @__PURE__ */ jsxs(Flex, {
903
+ align: "center",
904
+ flex: "none",
905
+ gap: 2,
906
+ padding: 2,
907
+ children: [/* @__PURE__ */ jsx(Box, {
908
+ flex: 1,
909
+ children: /* @__PURE__ */ jsx(Text, {
910
+ muted: !0,
911
+ size: 0,
912
+ style: {
913
+ letterSpacing: "0.05em",
914
+ textTransform: "uppercase"
915
+ },
916
+ weight: "medium",
917
+ children: labels.title
918
+ })
919
+ }), mode === "shortcut" && query !== "" ? /* @__PURE__ */ jsx(Text, {
920
+ muted: !0,
921
+ size: 1,
922
+ textOverflow: "ellipsis",
923
+ children: query
924
+ }) : null]
925
+ }),
926
+ /* @__PURE__ */ jsx(Box, {
927
+ "aria-label": labels.title,
928
+ id: listboxId,
929
+ role: "listbox",
930
+ style: {
931
+ flex: 1,
932
+ maxHeight: 320,
933
+ minHeight: 0,
934
+ overflowY: "auto"
935
+ },
936
+ children: sections.map((section_0, sectionIndex) => {
937
+ let sectionLabel = section_0.group ?? labels.ungroupedSection, sectionKey = section_0.group === null ? "ungrouped" : `group:${section_0.group}`, rows = section_0.items.map((item_0, rowIndex) => {
938
+ let idx_2 = (sectionOffsets[sectionIndex] ?? 0) + rowIndex, Icon = item_0.icon, isHighlighted = idx_2 === highlightedIndex;
939
+ return /* @__PURE__ */ jsx(Card, {
940
+ "aria-selected": isHighlighted,
941
+ id: optionId(String(idx_2)),
942
+ onClick: () => selectItemAtIndex(idx_2),
943
+ onMouseMove: (event_1) => highlightOnPointerMove(idx_2, event_1.clientX, event_1.clientY),
944
+ padding: 2,
945
+ radius: 1,
946
+ ref: isHighlighted ? highlightedRowRef : void 0,
947
+ role: "option",
948
+ style: { cursor: "pointer" },
949
+ tabIndex: -1,
950
+ tone: isHighlighted ? "primary" : "default",
951
+ children: /* @__PURE__ */ jsxs(Flex, {
952
+ align: "center",
953
+ gap: 3,
954
+ children: [
955
+ /* @__PURE__ */ jsx(Box, {
956
+ style: {
957
+ alignItems: "center",
958
+ display: "flex",
959
+ height: 22,
960
+ justifyContent: "center",
961
+ width: 22
962
+ },
963
+ children: Icon ? /* @__PURE__ */ jsx(Icon, {}) : null
964
+ }),
965
+ /* @__PURE__ */ jsxs(Stack, {
966
+ flex: 1,
967
+ gap: 2,
968
+ children: [/* @__PURE__ */ jsx(Text, {
969
+ size: 1,
970
+ textOverflow: "ellipsis",
971
+ children: item_0.title
972
+ }), item_0.description ? /* @__PURE__ */ jsx(Text, {
973
+ muted: !0,
974
+ size: 0,
975
+ textOverflow: "ellipsis",
976
+ children: item_0.description
977
+ }) : null]
978
+ }),
979
+ item_0.badge ? /* @__PURE__ */ jsx(Box, {
980
+ flex: "none",
981
+ children: /* @__PURE__ */ jsx(Text, {
982
+ muted: !0,
983
+ size: 0,
984
+ children: item_0.badge
985
+ })
986
+ }) : null,
987
+ item_0.trigger ? /* @__PURE__ */ jsx(Box, {
988
+ flex: "none",
989
+ children: /* @__PURE__ */ jsx(Hotkeys, {
990
+ fontSize: 0,
991
+ keys: [item_0.trigger]
992
+ })
993
+ }) : null
994
+ ]
995
+ })
996
+ }, `${item_0.id}-${idx_2}`);
997
+ });
998
+ return showGroupHeaders ? /* @__PURE__ */ jsxs(Box, {
999
+ "aria-label": sectionLabel,
1000
+ paddingTop: sectionIndex === 0 ? 0 : 2,
1001
+ role: "group",
1002
+ children: [/* @__PURE__ */ jsx(Box, {
1003
+ paddingBottom: 1,
1004
+ paddingTop: 2,
1005
+ paddingX: 2,
1006
+ children: /* @__PURE__ */ jsx(Text, {
1007
+ muted: !0,
1008
+ size: 0,
1009
+ style: {
1010
+ letterSpacing: "0.05em",
1011
+ textTransform: "uppercase"
1012
+ },
1013
+ weight: "medium",
1014
+ children: sectionLabel
1015
+ })
1016
+ }), /* @__PURE__ */ jsx(Stack, {
1017
+ gap: 1,
1018
+ children: rows
1019
+ })]
1020
+ }, sectionKey) : /* @__PURE__ */ jsx(Stack, {
1021
+ gap: 1,
1022
+ children: rows
1023
+ }, sectionKey);
1024
+ })
1025
+ }),
1026
+ /* @__PURE__ */ jsx(Card, {
1027
+ borderTop: !0,
1028
+ flex: "none",
1029
+ padding: 2,
1030
+ radius: 0,
1031
+ children: /* @__PURE__ */ jsxs(Flex, {
1032
+ align: "center",
1033
+ gap: 3,
1034
+ wrap: "wrap",
1035
+ children: [
1036
+ /* @__PURE__ */ jsx(FooterHint, {
1037
+ keys: ["↑", "↓"],
1038
+ label: labels.footerNavigate
1039
+ }),
1040
+ /* @__PURE__ */ jsx(FooterHint, {
1041
+ keys: ["↵"],
1042
+ label: labels.footerInsert
1043
+ }),
1044
+ /* @__PURE__ */ jsx(FooterHint, {
1045
+ keys: ["Esc"],
1046
+ label: labels.footerDismiss
1047
+ }),
1048
+ shortcut ? /* @__PURE__ */ jsx(FooterHint, {
1049
+ keys: [OPEN_MODIFIER, "/"],
1050
+ label: labels.footerAnywhere
1051
+ }) : null
1052
+ ]
1053
+ })
1054
+ })
1055
+ ]
1056
+ }),
1057
+ fallbackPlacements: ["top-start"],
1058
+ open: !0,
1059
+ placement: "bottom-start",
1060
+ portal: !0,
1061
+ preventOverflow: !0,
1062
+ referenceElement: cursorElement
1063
+ }) : null;
1064
+ }
1065
+ function FooterHint(t0) {
1066
+ let $ = c(7), { keys, label } = t0, t1;
1067
+ $[0] === keys ? t1 = $[1] : (t1 = /* @__PURE__ */ jsx(Hotkeys, {
1068
+ fontSize: 0,
1069
+ keys
1070
+ }), $[0] = keys, $[1] = t1);
1071
+ let t2;
1072
+ $[2] === label ? t2 = $[3] : (t2 = /* @__PURE__ */ jsx(Text, {
1073
+ muted: !0,
1074
+ size: 0,
1075
+ children: label
1076
+ }), $[2] = label, $[3] = t2);
1077
+ let t3;
1078
+ return $[4] !== t1 || $[5] !== t2 ? (t3 = /* @__PURE__ */ jsxs(Flex, {
1079
+ align: "center",
1080
+ gap: 2,
1081
+ children: [t1, t2]
1082
+ }), $[4] = t1, $[5] = t2, $[6] = t3) : t3 = $[6], t3;
1083
+ }
1084
+ function memberTypeFor(itemsContext, schema, blockType) {
1085
+ return itemsContext?.memberTypes.find((candidate) => candidate.name === blockType) ?? schema.get(blockType);
1086
+ }
1087
+ function bareQuery(query) {
1088
+ return query.startsWith("/") ? query.slice(1) : query;
1089
+ }
1090
+ function preventFocusSteal(event) {
1091
+ event.preventDefault();
1092
+ }
1093
+ const SR_ONLY = {
1094
+ border: 0,
1095
+ clipPath: "inset(50%)",
1096
+ height: 1,
1097
+ margin: -1,
1098
+ overflow: "hidden",
1099
+ padding: 0,
1100
+ position: "absolute",
1101
+ whiteSpace: "nowrap",
1102
+ width: 1
1103
+ }, OPEN_MODIFIER = typeof navigator < "u" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || "") ? "⌘" : "Ctrl";
1104
+ /**
1105
+ * Mounts markdown input rules on the PTE and opens each inserted block for
1106
+ * editing once its member item mounts — the same open-on-insert behavior the
1107
+ * picker uses, so a fenced code block lands ready to type into.
1108
+ */
1109
+ function MarkdownInputRules(t0) {
1110
+ let $ = c(15), { arrayTypeName, onInsert, openOnInsert: t1, rules } = t0, openOnInsert = t1 === void 0 || t1, editor = useEditor(), itemsContext = usePickerItemsContext(arrayTypeName), openBlockOnInsert = useOpenBlockOnInsert(), t2;
1111
+ $[0] === itemsContext?.memberTypes ? t2 = $[1] : (t2 = itemsContext?.memberTypes ?? [], $[0] = itemsContext?.memberTypes, $[1] = t2);
1112
+ let memberTypes = t2, t3;
1113
+ if ($[2] !== memberTypes || $[3] !== rules) {
1114
+ let t4;
1115
+ $[5] === memberTypes ? t4 = $[6] : (t4 = (rule) => {
1116
+ let member = memberTypes.find((candidate_0) => candidate_0.name === rule.blockType) ?? memberTypes.find((candidate) => typeNameChain(candidate).includes(rule.blockType));
1117
+ return member ? [rule.blockType === member.name ? rule : {
1118
+ ...rule,
1119
+ blockType: member.name
1120
+ }] : [];
1121
+ }, $[5] = memberTypes, $[6] = t4), t3 = rules.flatMap(t4), $[2] = memberTypes, $[3] = rules, $[4] = t3;
1122
+ } else t3 = $[4];
1123
+ let resolvedRules = t3, onInsertedRef = useRef(_temp), t4;
1124
+ $[7] !== onInsert || $[8] !== openBlockOnInsert || $[9] !== openOnInsert ? (t4 = () => {
1125
+ onInsertedRef.current = (block) => {
1126
+ openOnInsert && openBlockOnInsert(block._key), onInsert?.({
1127
+ blockKey: block._key,
1128
+ blockType: block._type,
1129
+ via: "inputRule"
1130
+ });
1131
+ };
1132
+ }, $[7] = onInsert, $[8] = openBlockOnInsert, $[9] = openOnInsert, $[10] = t4) : t4 = $[10], useLayoutEffect(t4);
1133
+ let t5, t6;
1134
+ return $[11] !== editor || $[12] !== resolvedRules ? (t5 = () => {
1135
+ let inputRules = createMarkdownInputRules({
1136
+ allowedBlockTypes: new Set(resolvedRules.map(_temp2)),
1137
+ keyGenerator: () => editor.getSnapshot().context.keyGenerator(),
1138
+ onInserted: (block_0) => onInsertedRef.current(block_0),
1139
+ rules: resolvedRules
1140
+ });
1141
+ if (inputRules.length !== 0) return editor.registerBehavior({ behavior: defineInputRuleBehavior({ rules: inputRules }) });
1142
+ }, t6 = [editor, resolvedRules], $[11] = editor, $[12] = resolvedRules, $[13] = t5, $[14] = t6) : (t5 = $[13], t6 = $[14]), useEffect(t5, t6), null;
1143
+ }
1144
+ function _temp2(rule_0) {
1145
+ return rule_0.blockType;
1146
+ }
1147
+ function _temp() {}
1148
+ /**
1149
+ * Creates the Portable Text editor plugins component for the block-insert
1150
+ * picker: a slash-command / Cmd+/ menu of the blocks the host array allows,
1151
+ * plus optional markdown input rules. Zero-config is the intended call —
1152
+ * items derive from the array the plugin is mounted on. Attach it to the
1153
+ * array type's `components.portableText.plugins`:
1154
+ *
1155
+ * ```ts
1156
+ * defineType({
1157
+ * name: 'content',
1158
+ * type: 'array',
1159
+ * of: [{type: 'block'}, {type: 'callout'}],
1160
+ * components: {
1161
+ * portableText: {
1162
+ * plugins: blockInsertPicker(),
1163
+ * },
1164
+ * },
1165
+ * })
1166
+ * ```
1167
+ */
1168
+ function blockInsertPicker(config = {}) {
1169
+ let { arrayTypeName, filter, inputRules, items, labels, onInsert, openOnInsert, presets, resolveItems, shortcut } = config;
1170
+ return function(props) {
1171
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1172
+ props.renderDefault(props),
1173
+ inputRules?.length ? /* @__PURE__ */ jsx(MarkdownInputRules, {
1174
+ arrayTypeName,
1175
+ onInsert,
1176
+ openOnInsert,
1177
+ rules: inputRules
1178
+ }) : null,
1179
+ /* @__PURE__ */ jsx(BlockInsertPicker, {
1180
+ arrayTypeName,
1181
+ filter,
1182
+ items,
1183
+ labels,
1184
+ onInsert,
1185
+ openOnInsert,
1186
+ presets,
1187
+ resolveItems,
1188
+ shortcut
1189
+ })
1190
+ ] });
1191
+ };
1192
+ }
1193
+ export { BLOCKQUOTE_PATTERN, BlockInsertPicker, CODE_FENCE_PATTERN, MarkdownInputRules, blockInsertPicker, blockquoteRule, codeFenceRule, derivePickerItems, fenceLanguageFromMatch, filterPickerItems, normalizeFenceLanguage, standardBlockPresets, wellKnownInputRules };
1194
+
1195
+ //# sourceMappingURL=index.js.map