@portabletext/plugin-typeahead-picker 1.0.0 → 2.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/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  The `useTypeaheadPicker` hook provides the state and logic needed to build typeahead pickers (emoji pickers, mention pickers, slash commands, etc.) for the Portable Text Editor. It manages keyword matching, keyboard navigation, and triggering of actions, but is not concerned with the UI, how the picker is rendered, or how it's positioned in the document.
8
8
 
9
9
  ```tsx
10
+ import {EditorProvider, PortableTextEditable} from '@portabletext/editor'
10
11
  import {raise} from '@portabletext/editor/behaviors'
11
12
  import {
12
13
  defineTypeaheadPicker,
@@ -14,7 +15,7 @@ import {
14
15
  type AutoCompleteMatch,
15
16
  } from '@portabletext/plugin-typeahead-picker'
16
17
 
17
- // With `autoCompleteWith` configured, matches must include `type: 'exact' | 'partial'`
18
+ // With `delimiter` configured, matches must include `type: 'exact' | 'partial'`
18
19
  // for auto-completion to work. Use `AutoCompleteMatch` as the base type.
19
20
  type EmojiMatch = AutoCompleteMatch & {
20
21
  key: string
@@ -23,13 +24,15 @@ type EmojiMatch = AutoCompleteMatch & {
23
24
  }
24
25
 
25
26
  const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
26
- // Pattern to match trigger + keyword. The capture group (\S*) becomes the keyword.
27
- // This matches `:` followed by any non-whitespace characters.
28
- pattern: /:(\S*)/,
27
+ // Trigger pattern - activates the picker when typed
28
+ trigger: /:/,
29
29
 
30
- // Optional autoCompleteWith enables auto-completion.
30
+ // Keyword pattern - matches characters after the trigger
31
+ keyword: /\S*/,
32
+
33
+ // Optional delimiter enables auto-completion.
31
34
  // Typing `:joy:` will auto-insert if "joy" is an exact match.
32
- autoCompleteWith: ':',
35
+ delimiter: ':',
33
36
 
34
37
  // Return matches for the keyword. Can be sync or async (with mode: 'async').
35
38
  getMatches: ({keyword}) => searchEmojis(keyword),
@@ -44,7 +47,7 @@ const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
44
47
  ],
45
48
  })
46
49
 
47
- function EmojiPicker() {
50
+ function EmojiPickerPlugin() {
48
51
  // Activate the picker and get its current state
49
52
  const picker = useTypeaheadPicker(emojiPicker)
50
53
 
@@ -76,18 +79,30 @@ function EmojiPicker() {
76
79
  </ul>
77
80
  )
78
81
  }
82
+
83
+ // Render the picker inside EditorProvider, alongside PortableTextEditable
84
+ function MyEditor() {
85
+ return (
86
+ <EditorProvider /* ...config */>
87
+ <PortableTextEditable />
88
+ <EmojiPickerPlugin />
89
+ </EditorProvider>
90
+ )
91
+ }
79
92
  ```
80
93
 
94
+ The picker component must be rendered inside `EditorProvider` to access the editor context. Position it as a sibling to `PortableTextEditable` - you'll handle the visual positioning (popover, dropdown, etc.) separately with CSS or a positioning library.
95
+
81
96
  ## How It Works
82
97
 
83
- The picker activates when users type text matching the `pattern` (e.g., `:smile` or `@john`).
98
+ The picker activates when users type the `trigger` pattern (e.g., `:` or `@`). The `keyword` pattern then matches characters typed after the trigger.
84
99
 
85
100
  - **Keyboard shortcuts are built-in**:
86
101
  - `Enter` or `Tab` inserts the selected match
87
102
  - `↑` / `↓` navigate through matches
88
103
  - `Esc` dismisses the picker
89
104
  - **Mouse interactions are opt-in**: Use `send({type: 'navigate to', index})` and `send({type: 'select'})` to enable hover and click
90
- - **Auto-completion**: With `autoCompleteWith` configured, typing the delimiter after an exact match auto-inserts it (e.g., `:joy:` auto-inserts the emoji)
105
+ - **Auto-completion**: With `delimiter` configured, typing the delimiter after an exact match auto-inserts it (e.g., `:joy:` auto-inserts the emoji)
91
106
 
92
107
  ## Examples
93
108
 
@@ -95,8 +110,9 @@ The picker activates when users type text matching the `pattern` (e.g., `:smile`
95
110
 
96
111
  ```ts
97
112
  const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
98
- pattern: /:(\S*)/,
99
- autoCompleteWith: ':',
113
+ trigger: /:/,
114
+ keyword: /\S*/,
115
+ delimiter: ':',
100
116
  getMatches: ({keyword}) => searchEmojis(keyword),
101
117
  actions: [
102
118
  ({event}) => [
@@ -112,11 +128,12 @@ const emojiPicker = defineTypeaheadPicker<EmojiMatch>({
112
128
  ### Mention picker (async with debounce)
113
129
 
114
130
  ```ts
115
- // Without `autoCompleteWith`, the `type` field is not required on matches.
131
+ // Without `delimiter`, the `type` field is not required on matches.
116
132
  // MentionMatch can just be: { id: string; name: string }
117
133
  const mentionPicker = defineTypeaheadPicker<MentionMatch>({
118
134
  mode: 'async',
119
- pattern: /@(\w*)/,
135
+ trigger: /@/,
136
+ keyword: /\w*/,
120
137
  debounceMs: 200,
121
138
  getMatches: async ({keyword}) => api.searchUsers(keyword),
122
139
  actions: [
@@ -136,9 +153,10 @@ const mentionPicker = defineTypeaheadPicker<MentionMatch>({
136
153
  ### Slash command picker (start of block only)
137
154
 
138
155
  ```ts
139
- // Without `autoCompleteWith`, the `type` field is not required on matches.
156
+ // Without `delimiter`, the `type` field is not required on matches.
140
157
  const commandPicker = defineTypeaheadPicker<CommandMatch>({
141
- pattern: /^\/(\w*)/, // ^ anchors to start of block
158
+ trigger: /^\//, // ^ anchors to start of block
159
+ keyword: /\w*/,
142
160
  getMatches: ({keyword}) => searchCommands(keyword),
143
161
  actions: [
144
162
  ({event}) => {
@@ -173,73 +191,51 @@ Creates a picker definition to pass to `useTypeaheadPicker`.
173
191
 
174
192
  **Config:**
175
193
 
176
- | Property | Type | Description |
177
- | ------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
178
- | `pattern` | `RegExp` | Pattern for matching trigger + keyword. Use a capture group for the keyword (e.g., `/:(\S*)/`, `/@(\w*)/`). Can include position anchors like `^` for start-of-block triggers. |
179
- | `autoCompleteWith` | `string?` | Optional delimiter that triggers auto-completion (e.g., `:` for `:joy:`) |
180
- | `mode` | `'sync' \| 'async'` | Whether `getMatches` returns synchronously or a Promise (default: `'sync'`) |
181
- | `debounceMs` | `number?` | Delay in ms before calling `getMatches`. Useful for both async (API calls) and sync (expensive local search) modes. (default: `0`) |
182
- | `getMatches` | `(ctx: {keyword: string}) => TMatch[]` | Function that returns matches for the keyword |
183
- | `actions` | `Array<TypeaheadSelectActionSet>` | Actions to execute when a match is selected |
194
+ | Property | Type | Description |
195
+ | ------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
196
+ | `trigger` | `RegExp` | Pattern that activates the picker. Can include `^` for start-of-block triggers. Must be single-character (e.g., `/:/`, `/@/`, `/^\//`). |
197
+ | `keyword` | `RegExp` | Pattern matching characters after the trigger (e.g., `/\S*/`, `/\w*/`). |
198
+ | `delimiter` | `string?` | Optional delimiter that triggers auto-completion (e.g., `':'` for `:joy:`) |
199
+ | `mode` | `'sync' \| 'async'` | Whether `getMatches` returns synchronously or a Promise (default: `'sync'`) |
200
+ | `debounceMs` | `number?` | Delay in ms before calling `getMatches`. Useful for both async (API calls) and sync (expensive local search) modes. (default: `0`) |
201
+ | `getMatches` | `(ctx: {keyword: string}) => TMatch[]` | Function that returns matches for the keyword |
202
+ | `actions` | `Array<TypeaheadSelectActionSet>` | Actions to execute when a match is selected |
184
203
 
185
- **Pattern rules:**
204
+ **Trigger pattern rules:**
186
205
 
187
- - If pattern has capture groups: keyword = first capture group
188
- - If no capture group: keyword = entire match
206
+ - Must be a single-character trigger (e.g., `:`, `@`, `/`)
207
+ - Multi-character triggers (e.g., `##`) are not supported
189
208
  - Position anchors (`^`) allow start-of-block constraints
190
- - Regex flags are ignored (the picker normalizes patterns internally)
191
209
 
192
210
  **How triggering works:**
193
211
 
194
- The picker activates the moment a trigger character is typed - not later when more text matches. This is the key to understanding pattern requirements:
212
+ The picker activates the moment a trigger character is typed. After activation, the keyword is tracked via editor selection changes.
195
213
 
196
214
  ```
197
- User types `:` → Pattern /:(\S*)/ matches → Picker activates with keyword ""
215
+ User types `:` → Trigger matches → Picker activates with keyword ""
198
216
  User types `j` → Keyword updates to "j" (via selection tracking)
199
217
  User types `o` → Keyword updates to "jo"
200
218
  User types `y` → Keyword updates to "joy"
201
219
  ```
202
220
 
203
- The pattern must match **immediately when the trigger is typed**. After activation, the keyword is tracked via editor selection changes, not by re-running the pattern.
204
-
205
- **Why some patterns don't work:**
206
-
207
- Multi-character triggers like `##` fail because the picker can only activate on the character you just typed:
208
-
209
- ```
210
- User types `#` → Pattern /##(\w*)/ doesn't match yet → Nothing happens
211
- User types `#` → Pattern matches "##", but the first # was already there
212
- Picker only activates on newly typed triggers, not existing text
213
- ```
214
-
215
- Same issue with patterns requiring specific characters mid-keyword like `/:(\w*)-(\w*)/`:
216
-
217
- ```
218
- User types `:foo` → Pattern doesn't match yet (needs a `-`)
219
- User types `-` → Pattern matches `:foo-`, but `:foo` was already there
220
- Picker only activates on newly typed triggers
221
- ```
222
-
223
- **Pattern compatibility summary:**
221
+ **Trigger compatibility summary:**
224
222
 
225
- | Pattern | Example input | Works? | Why |
226
- | ---------------- | ------------- | ------ | --------------------------------- |
227
- | `/:(\S*)/` | `:joy` | ✅ | `:` alone triggers, keyword grows |
228
- | `/@(\w*)/` | `@john` | ✅ | `@` alone triggers, keyword grows |
229
- | `/^\/(\w*)/` | `/cmd` | ✅ | `/` at block start triggers |
230
- | `/##(\w*)/` | `##tag` | ❌ | `#` alone doesn't match pattern |
231
- | `/:(\w*)-(\w*)/` | `:a-b` | ❌ | `:` alone doesn't match pattern |
223
+ | Trigger | Example input | Works? | Why |
224
+ | ------- | ------------- | ------ | -------------------------------- |
225
+ | `/:/` | `:joy` | ✅ | Single-char trigger |
226
+ | `/@/` | `@john` | ✅ | Single-char trigger |
227
+ | `/^\//` | `/cmd` | ✅ | Single-char with position anchor |
228
+ | `/##/` | `##tag` | ❌ | Multi-char triggers unsupported |
232
229
 
233
- **autoCompleteWith requirements:**
230
+ **delimiter requirements:**
234
231
 
235
- When using `autoCompleteWith`, the delimiter character must be included in the keyword's character class, otherwise typing it dismisses the picker:
232
+ Single-character delimiters work regardless of whether the character is included in the keyword pattern. Multi-character delimiters are not supported.
236
233
 
237
- | Pattern | autoCompleteWith | Example | Works? | Why |
238
- | ---------- | ---------------- | -------- | ------ | --------------------------------------------------- |
239
- | `/:(\S*)/` | `:` | `:joy:` | ✅ | `\S` matches `:`, cursor stays in match |
240
- | `/:(\w*)/` | `:` | `:joy:` | ✅ | Cursor at match boundary, still valid |
241
- | `/#(\w*)/` | `#` | `#tag#` | | Cursor at match boundary, still valid |
242
- | `/#(\w*)/` | `##` | `#tag##` | ❌ | First `#` moves cursor past match, picker dismisses |
234
+ | keyword | delimiter | Example | Works? | Why |
235
+ | ------- | --------- | -------- | ------ | --------------------------------------- |
236
+ | `/\S*/` | `:` | `:joy:` | ✅ | `\S` matches `:`, keyword becomes `joy` |
237
+ | `/\w*/` | `:` | `:joy:` | ✅ | `\w` stops at `:`, keyword is `joy` |
238
+ | `/\w*/` | `##` | `#tag##` | | Multi-char delimiter not supported |
243
239
 
244
240
  ### `useTypeaheadPicker(definition)`
245
241
 
@@ -250,7 +246,7 @@ React hook that activates a picker and returns its state.
250
246
  | Property | Description |
251
247
  | -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
252
248
  | `snapshot.matches(state)` | Check picker state: `'idle'`, `{active: 'loading'}`, `{active: 'no matches'}`, `{active: 'showing matches'}` |
253
- | `snapshot.context.keyword` | The current keyword (extracted from capture group) |
249
+ | `snapshot.context.keyword` | The current keyword |
254
250
  | `snapshot.context.matches` | Array of matches from `getMatches` |
255
251
  | `snapshot.context.selectedIndex` | Index of the currently selected match |
256
252
  | `send(event)` | Dispatch events: `{type: 'select'}`, `{type: 'dismiss'}`, `{type: 'navigate to', index}` |
@@ -301,7 +297,7 @@ When users type quickly, earlier slow requests may complete after later fast req
301
297
  If `getMatches` throws or rejects, the error is captured in `snapshot.context.error`. The picker transitions to `'no matches'` state and continues to function.
302
298
 
303
299
  ```tsx
304
- function EmojiPicker() {
300
+ function EmojiPickerPlugin() {
305
301
  const picker = useTypeaheadPicker(emojiPicker)
306
302
  const {error} = picker.snapshot.context
307
303
 
@@ -326,7 +322,8 @@ Action functions receive more than just the event. The full payload includes acc
326
322
 
327
323
  ```tsx
328
324
  const commandPicker = defineTypeaheadPicker<CommandMatch>({
329
- pattern: /^\/(\w*)/,
325
+ trigger: /^\//,
326
+ keyword: /\w*/,
330
327
  getMatches: ({keyword}) => searchCommands(keyword),
331
328
  actions: [
332
329
  ({event, snapshot}) => {
@@ -388,7 +385,8 @@ Choose debounce values based on your data source:
388
385
  ```tsx
389
386
  // Local data - no debounce needed
390
387
  const emojiPicker = defineTypeaheadPicker({
391
- pattern: /:(\S*)/,
388
+ trigger: /:/,
389
+ keyword: /\S*/,
392
390
  getMatches: ({keyword}) => filterEmojis(keyword), // Fast local filter
393
391
  // ...
394
392
  })
@@ -397,7 +395,8 @@ const emojiPicker = defineTypeaheadPicker({
397
395
  const mentionPicker = defineTypeaheadPicker({
398
396
  mode: 'async',
399
397
  debounceMs: 200,
400
- pattern: /@(\w*)/,
398
+ trigger: /@/,
399
+ keyword: /\w*/,
401
400
  getMatches: async ({keyword}) => api.searchUsers(keyword),
402
401
  // ...
403
402
  })
@@ -460,17 +459,17 @@ The following keyboard shortcuts are handled automatically by the picker:
460
459
 
461
460
  ### Picker doesn't activate
462
461
 
463
- - **Check pattern**: Ensure your regex has a capture group for the keyword: `/:(\S*)/` not `/:\S*/`
464
- - **Check position anchors**: `^` means start of block, not start of line. `hello /command` won't match `/^\/(\w*)/`
462
+ - **Check trigger**: Must be a single-character trigger (e.g., `/:/`, `/@/`)
463
+ - **Check position anchors**: `^` means start of block, not start of line. `hello /command` won't match `/^\//`
465
464
  - **Check for conflicts**: Only one picker can be active at a time
466
- - **Avoid multi-character triggers**: Patterns like `/##(\w*)/` don't work because the picker only activates on newly typed triggers, not existing text
465
+ - **Avoid multi-character triggers**: Triggers like `/##/` don't work because the picker only activates on newly typed single characters
467
466
 
468
467
  ### Auto-completion doesn't work
469
468
 
470
- - **Check `autoCompleteWith`**: Must be set (e.g., `autoCompleteWith: ':'`)
469
+ - **Check `delimiter`**: Must be set (e.g., `delimiter: ':'`)
471
470
  - **Check match type**: Matches must include `type: 'exact' | 'partial'`
472
471
  - **Check for exact match**: Auto-completion only triggers when exactly one match has `type: 'exact'`
473
- - **Check character class**: The keyword character class must match the `autoCompleteWith` character. Use `\S*` (matches any non-whitespace including `:`) rather than `\w*` (only matches word characters) when `autoCompleteWith: ':'`
472
+ - **Check keyword pattern**: The keyword pattern must allow the delimiter character at the boundary. Use `\S*` (matches any non-whitespace) when `delimiter: ':'`
474
473
 
475
474
  ### Stale matches appear
476
475
 
package/dist/index.d.ts CHANGED
@@ -1,34 +1,58 @@
1
1
  import type {EditorSelection} from '@portabletext/editor'
2
2
  import type {BehaviorActionSet} from '@portabletext/editor/behaviors'
3
3
 
4
- declare type AsyncConfigWithAutoComplete<TMatch extends AutoCompleteMatch> =
5
- BaseConfigWithAutoComplete<TMatch> & {
4
+ declare type AsyncConfigWithDelimiter<TMatch extends AutoCompleteMatch> =
5
+ BaseConfigWithDelimiter<TMatch> & {
6
+ /**
7
+ * Set to `'async'` when `getMatches` returns a Promise.
8
+ */
6
9
  mode: 'async'
10
+ /**
11
+ * Debounce delay in milliseconds before calling `getMatches`.
12
+ * Recommended for API calls to reduce request frequency.
13
+ * @defaultValue `0` (no debounce)
14
+ */
7
15
  debounceMs?: number
8
- getMatches: (context: {keyword: string}) => Promise<Array<TMatch>>
16
+ /**
17
+ * Async function that returns matches for the current keyword.
18
+ * Called whenever the keyword changes (after debounce if configured).
19
+ */
20
+ getMatches: (context: {keyword: string}) => Promise<ReadonlyArray<TMatch>>
9
21
  }
10
22
 
11
- declare type AsyncConfigWithoutAutoComplete<TMatch extends object> =
12
- BaseConfigWithoutAutoComplete<TMatch> & {
23
+ declare type AsyncConfigWithoutDelimiter<TMatch extends object> =
24
+ BaseConfigWithoutDelimiter<TMatch> & {
25
+ /**
26
+ * Set to `'async'` when `getMatches` returns a Promise.
27
+ */
13
28
  mode: 'async'
29
+ /**
30
+ * Debounce delay in milliseconds before calling `getMatches`.
31
+ * Recommended for API calls to reduce request frequency.
32
+ * @defaultValue `0` (no debounce)
33
+ */
14
34
  debounceMs?: number
15
- getMatches: (context: {keyword: string}) => Promise<Array<TMatch>>
35
+ /**
36
+ * Async function that returns matches for the current keyword.
37
+ * Called whenever the keyword changes (after debounce if configured).
38
+ */
39
+ getMatches: (context: {keyword: string}) => Promise<ReadonlyArray<TMatch>>
16
40
  }
17
41
 
18
42
  /**
19
43
  * Match type for pickers with auto-completion support.
20
- * Use this when `autoCompleteWith` is configured.
44
+ * Use this when `delimiter` is configured.
21
45
  *
22
46
  * The `type` property indicates how well the match corresponds to the keyword:
23
47
  * - `'exact'` - The keyword matches this item exactly (e.g., keyword `joy` matches emoji `:joy:`)
24
48
  * - `'partial'` - The keyword partially matches this item (e.g., keyword `jo` matches `:joy:`)
25
49
  *
26
- * When `autoCompleteWith` is configured and there's exactly one `'exact'` match,
50
+ * When `delimiter` is configured and there's exactly one `'exact'` match,
27
51
  * the picker will auto-insert that match.
28
52
  *
29
53
  * @example
30
54
  * ```ts
31
- * // With autoCompleteWith - type field required for auto-completion
55
+ * // With delimiter - type field required for auto-completion
32
56
  * type EmojiMatch = AutoCompleteMatch & {
33
57
  * key: string
34
58
  * emoji: string
@@ -42,15 +66,57 @@ export declare type AutoCompleteMatch = {
42
66
  type: 'exact' | 'partial'
43
67
  }
44
68
 
45
- declare type BaseConfigWithAutoComplete<TMatch extends AutoCompleteMatch> = {
46
- pattern: RegExp
47
- autoCompleteWith: string
69
+ declare type BaseConfigWithDelimiter<TMatch extends AutoCompleteMatch> = {
70
+ /**
71
+ * Pattern that activates the picker. Must be a single character.
72
+ * Can include `^` for start-of-block triggers.
73
+ *
74
+ * @example `/:/` - activates on colon
75
+ * @example `/@/` - activates on at-sign
76
+ * @example `/^\//` - activates on slash at start of block
77
+ */
78
+ trigger: RegExp
79
+ /**
80
+ * Pattern matching the keyword portion after the trigger.
81
+ * The entire match becomes the keyword passed to `getMatches`.
82
+ * Common patterns: non-whitespace (`\S*`) or word characters (`\w*`).
83
+ */
84
+ keyword: RegExp
85
+ /**
86
+ * Character that triggers auto-completion.
87
+ * Typing this after a keyword with an exact match auto-inserts it.
88
+ *
89
+ * @example `':'` - typing `:joy:` auto-inserts the joy emoji
90
+ */
91
+ delimiter: string
92
+ /**
93
+ * Actions to execute when a match is selected.
94
+ * Typically deletes the trigger text and inserts the selected content.
95
+ */
48
96
  actions: Array<TypeaheadSelectActionSet<TMatch>>
49
97
  }
50
98
 
51
- declare type BaseConfigWithoutAutoComplete<TMatch extends object> = {
52
- pattern: RegExp
53
- autoCompleteWith?: undefined
99
+ declare type BaseConfigWithoutDelimiter<TMatch extends object> = {
100
+ /**
101
+ * Pattern that activates the picker. Must be a single character.
102
+ * Can include `^` for start-of-block triggers.
103
+ *
104
+ * @example `/:/` - activates on colon
105
+ * @example `/@/` - activates on at-sign
106
+ * @example `/^\//` - activates on slash at start of block
107
+ */
108
+ trigger: RegExp
109
+ /**
110
+ * Pattern matching the keyword portion after the trigger.
111
+ * The entire match becomes the keyword passed to `getMatches`.
112
+ * Common patterns: non-whitespace (`\S*`) or word characters (`\w*`).
113
+ */
114
+ keyword: RegExp
115
+ delimiter?: undefined
116
+ /**
117
+ * Actions to execute when a match is selected.
118
+ * Typically deletes the trigger text and inserts the selected content.
119
+ */
54
120
  actions: Array<TypeaheadSelectActionSet<TMatch>>
55
121
  }
56
122
 
@@ -60,13 +126,11 @@ declare type BaseConfigWithoutAutoComplete<TMatch extends object> = {
60
126
  * @example Emoji picker with auto-complete
61
127
  * ```ts
62
128
  * const emojiPicker = defineTypeaheadPicker({
63
- * pattern: /:(\S*)/,
64
- * autoCompleteWith: ':',
129
+ * trigger: /:/,
130
+ * keyword: /[\S]+/,
131
+ * delimiter: ':',
65
132
  * getMatches: ({keyword}) => searchEmojis(keyword),
66
- * actions: [({event}) => [
67
- * raise({type: 'delete', at: event.patternSelection}),
68
- * raise({type: 'insert.text', text: event.match.emoji}),
69
- * ]],
133
+ * actions: [insertEmojiAction],
70
134
  * })
71
135
  * ```
72
136
  *
@@ -74,47 +138,43 @@ declare type BaseConfigWithoutAutoComplete<TMatch extends object> = {
74
138
  * ```ts
75
139
  * const mentionPicker = defineTypeaheadPicker({
76
140
  * mode: 'async',
77
- * pattern: /@(\w*)/,
141
+ * trigger: /@/,
142
+ * keyword: /[\w]+/,
78
143
  * debounceMs: 200,
79
144
  * getMatches: async ({keyword}) => api.searchUsers(keyword),
80
- * actions: [({event}) => [
81
- * raise({type: 'delete', at: event.patternSelection}),
82
- * raise({type: 'insert.text', text: event.match.name}),
83
- * ]],
145
+ * actions: [insertMentionAction],
84
146
  * })
85
147
  * ```
86
148
  *
87
149
  * @example Slash commands at start of block
88
150
  * ```ts
89
151
  * const slashCommandPicker = defineTypeaheadPicker({
90
- * pattern: /^\/(\w*)/, // ^ anchors to start of block
152
+ * trigger: /^\//,
153
+ * keyword: /[\w]+/,
91
154
  * getMatches: ({keyword}) => filterCommands(keyword),
92
- * actions: [({event}) => [
93
- * raise({type: 'delete', at: event.patternSelection}),
94
- * raise(event.match.action),
95
- * ]],
155
+ * actions: [executeCommandAction],
96
156
  * })
97
157
  * ```
98
158
  *
99
159
  * @public
100
160
  */
101
161
  export declare function defineTypeaheadPicker<TMatch extends object>(
102
- config: SyncConfigWithoutAutoComplete<TMatch>,
162
+ config: SyncConfigWithoutDelimiter<TMatch>,
103
163
  ): TypeaheadPickerDefinition<TMatch>
104
164
 
105
165
  /** @public */
106
166
  export declare function defineTypeaheadPicker<TMatch extends AutoCompleteMatch>(
107
- config: SyncConfigWithAutoComplete<TMatch>,
167
+ config: SyncConfigWithDelimiter<TMatch>,
108
168
  ): TypeaheadPickerDefinition<TMatch>
109
169
 
110
170
  /** @public */
111
171
  export declare function defineTypeaheadPicker<TMatch extends object>(
112
- config: AsyncConfigWithoutAutoComplete<TMatch>,
172
+ config: AsyncConfigWithoutDelimiter<TMatch>,
113
173
  ): TypeaheadPickerDefinition<TMatch>
114
174
 
115
175
  /** @public */
116
176
  export declare function defineTypeaheadPicker<TMatch extends AutoCompleteMatch>(
117
- config: AsyncConfigWithAutoComplete<TMatch>,
177
+ config: AsyncConfigWithDelimiter<TMatch>,
118
178
  ): TypeaheadPickerDefinition<TMatch>
119
179
 
120
180
  /**
@@ -142,20 +202,46 @@ export declare function defineTypeaheadPicker<TMatch extends AutoCompleteMatch>(
142
202
  */
143
203
  export declare type GetMatches<TMatch extends object> = (context: {
144
204
  keyword: string
145
- }) => Array<TMatch> | Promise<Array<TMatch>>
205
+ }) => ReadonlyArray<TMatch> | Promise<ReadonlyArray<TMatch>>
146
206
 
147
- declare type SyncConfigWithAutoComplete<TMatch extends AutoCompleteMatch> =
148
- BaseConfigWithAutoComplete<TMatch> & {
207
+ declare type SyncConfigWithDelimiter<TMatch extends AutoCompleteMatch> =
208
+ BaseConfigWithDelimiter<TMatch> & {
209
+ /**
210
+ * Whether `getMatches` returns synchronously or asynchronously.
211
+ * @defaultValue `'sync'`
212
+ */
149
213
  mode?: 'sync'
214
+ /**
215
+ * Debounce delay in milliseconds before calling `getMatches`.
216
+ * Useful for expensive local searches.
217
+ * @defaultValue `0` (no debounce)
218
+ */
150
219
  debounceMs?: number
151
- getMatches: (context: {keyword: string}) => Array<TMatch>
220
+ /**
221
+ * Function that returns matches for the current keyword.
222
+ * Called whenever the keyword changes (after debounce if configured).
223
+ */
224
+ getMatches: (context: {keyword: string}) => ReadonlyArray<TMatch>
152
225
  }
153
226
 
154
- declare type SyncConfigWithoutAutoComplete<TMatch extends object> =
155
- BaseConfigWithoutAutoComplete<TMatch> & {
227
+ declare type SyncConfigWithoutDelimiter<TMatch extends object> =
228
+ BaseConfigWithoutDelimiter<TMatch> & {
229
+ /**
230
+ * Whether `getMatches` returns synchronously or asynchronously.
231
+ * @defaultValue `'sync'`
232
+ */
156
233
  mode?: 'sync'
234
+ /**
235
+ * Debounce delay in milliseconds before calling `getMatches`.
236
+ * Useful for expensive local searches.
237
+ * @defaultValue `0` (no debounce)
238
+ */
157
239
  debounceMs?: number
158
- getMatches: (context: {keyword: string}) => Array<TMatch>
240
+ /**
241
+ * Function that returns matches for the current keyword.
242
+ * Called whenever the keyword changes (after debounce if configured).
243
+ */
244
+ getMatches: (context: {keyword: string}) => ReadonlyArray<TMatch>
159
245
  }
160
246
 
161
247
  /**
@@ -224,7 +310,7 @@ export declare type TypeaheadPickerContext<TMatch> = {
224
310
  /** The extracted keyword from the trigger pattern (e.g., `joy` from `:joy`) */
225
311
  keyword: string
226
312
  /** The current list of matches returned by `getMatches` */
227
- matches: Array<TMatch>
313
+ matches: ReadonlyArray<TMatch>
228
314
  /** Index of the currently selected match (for keyboard navigation and highlighting) */
229
315
  selectedIndex: number
230
316
  /** Error from `getMatches` if it threw, otherwise `undefined` */
@@ -239,8 +325,9 @@ export declare type TypeaheadPickerContext<TMatch> = {
239
325
  * @example
240
326
  * ```ts
241
327
  * const emojiPicker = defineTypeaheadPicker({
242
- * pattern: /:(\S*)/,
243
- * autoCompleteWith: ':',
328
+ * trigger: /:/,
329
+ * keyword: /[\S]+/,
330
+ * delimiter: ':',
244
331
  * getMatches: ({keyword}) => searchEmojis(keyword),
245
332
  * actions: [insertEmojiAction],
246
333
  * })
@@ -273,32 +360,29 @@ export declare type TypeaheadPickerDefinition<TMatch extends object = object> =
273
360
 
274
361
  declare type TypeaheadPickerDefinitionBase<TMatch extends object> = {
275
362
  /**
276
- * RegExp pattern for matching trigger + keyword.
277
- *
278
- * If pattern has capture groups: keyword = first capture group (additional groups ignored).
279
- * If no capture group: keyword = entire match.
280
- *
281
- * Can include position anchors like `^` for start-of-block triggers.
363
+ * Pattern that activates the picker.
364
+ * Can include positional anchors like `^` for start-of-block triggers.
282
365
  *
283
- * @example
284
- * ```ts
285
- * // Emoji picker - `:` trigger anywhere
286
- * pattern: /:(\S*)/
287
- *
288
- * // Slash commands - `/` only at start of block
289
- * pattern: /^\/(\w*)/
366
+ * @example `/:/` for emoji
367
+ * @example `/@/` for mentions
368
+ * @example `/^\//` for slash commands (start of block only)
369
+ */
370
+ trigger: RegExp
371
+ /**
372
+ * Pattern matching the keyword portion (after trigger, before delimiter).
373
+ * The entire match is used as the keyword.
290
374
  *
291
- * // Mentions - `@` trigger anywhere
292
- * pattern: /@(\w*)/
293
- * ```
375
+ * @example `/[\S]+/` for emoji (any non-whitespace)
376
+ * @example `/[\w]+/` for mentions (word characters only)
294
377
  */
295
- pattern: RegExp
378
+ keyword: RegExp
296
379
  /**
297
- * Optional delimiter that triggers auto-completion.
298
- * When typed after a keyword with exactly one exact match, that match auto-inserts.
380
+ * Character that triggers auto-completion.
381
+ * Typing this after a keyword with exactly one exact match auto-inserts it.
382
+ *
299
383
  * @example `':'` - typing `:joy:` auto-inserts the joy emoji
300
384
  */
301
- autoCompleteWith?: string
385
+ delimiter?: string
302
386
  /**
303
387
  * Actions to execute when a match is selected.
304
388
  * Typically deletes the trigger text and inserts the selected content.