@bsky.app/tapper 0.1.1 → 0.2.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @bsky.app/tapper
2
+
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`255ae92`](https://github.com/bluesky-social/toolbox/commit/255ae923d552c12b6ad36dedde37a9bf44d02866) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Bump sift/tapper for publish
8
+
9
+ ## 0.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [`06c8141`](https://github.com/bluesky-social/toolbox/commit/06c81411f8420240c8c794d41398df034c514fda) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Bump packages
package/README.md CHANGED
@@ -1,3 +1,206 @@
1
1
  # @bsky.app/tapper
2
2
 
3
- A minimal rich text editor for React Native and web.
3
+ A facet-aware text input engine for React Native (including web). Parses
4
+ configurable patterns (mentions, emoji, tags, URLs) from text as the user types,
5
+ producing a node list you render as a colored preview overlay on top of a
6
+ transparent `TextInput`.
7
+
8
+ ## Installation
9
+
10
+ ```sh
11
+ pnpm add @bsky.app/tapper
12
+ ```
13
+
14
+ Peer dependencies: `react`, `react-native`.
15
+
16
+ ## Basic example
17
+
18
+ ```tsx
19
+ import {useEffect} from 'react'
20
+ import {View, Text, TextInput} from 'react-native'
21
+ import {useTapper} from '@bsky.app/tapper'
22
+
23
+ function Composer() {
24
+ const {text, nodes, inputProps, focus, on} = useTapper({
25
+ facets: {
26
+ mention: {match: /@([a-zA-Z0-9._]+)/g},
27
+ emoji: {match: /:([a-zA-Z0-9_]+):/g},
28
+ },
29
+ })
30
+
31
+ // Re-focus after an autocomplete insertion
32
+ useEffect(() => {
33
+ return on('afterInsert', () => focus())
34
+ }, [on, focus])
35
+
36
+ return (
37
+ <View style={{position: 'relative'}}>
38
+ {/* Preview overlay — renders colored facets */}
39
+ <View
40
+ style={{position: 'absolute', top: 0, left: 0, right: 0, bottom: 0}}>
41
+ <Text>
42
+ {nodes.map((node, i) => (
43
+ <Text
44
+ key={i}
45
+ style={{
46
+ color:
47
+ node.type === 'facet'
48
+ ? '#2e7de9'
49
+ : node.type === 'trigger'
50
+ ? '#999'
51
+ : '#000',
52
+ }}>
53
+ {node.raw}
54
+ </Text>
55
+ ))}
56
+ </Text>
57
+ </View>
58
+
59
+ {/* Actual input — transparent text, visible caret */}
60
+ <TextInput
61
+ {...inputProps}
62
+ multiline
63
+ style={{
64
+ color: 'transparent',
65
+ caretColor: '#000',
66
+ }}
67
+ placeholder="Type here..."
68
+ />
69
+ </View>
70
+ )
71
+ }
72
+ ```
73
+
74
+ The preview overlay and `TextInput` must share the same text styles (font
75
+ family, size, line height, padding) so they align exactly.
76
+
77
+ ## How it works
78
+
79
+ `useTapper` returns:
80
+
81
+ | Property | Description |
82
+ | ------------- | --------------------------------------------------- |
83
+ | `text` | Current text value |
84
+ | `nodes` | Parsed node list for rendering |
85
+ | `activeFacet` | The facet the cursor is currently inside, or `null` |
86
+ | `inputProps` | Spread onto your `TextInput` |
87
+ | `on` | Subscribe to events |
88
+ | `focus` | Re-focus the input (useful after `commit`) |
89
+
90
+ ### Nodes
91
+
92
+ Each node has a `type` discriminant:
93
+
94
+ - **`'text'`** — plain text, render as-is.
95
+ - **`'trigger'`** — the user just typed a trigger character (e.g. `@`) but
96
+ hasn't typed any content yet. Has `facetType` so you know which kind.
97
+ - **`'facet'`** — a matched pattern. Has `facetType` (e.g. `'mention'`,
98
+ `'emoji'`).
99
+
100
+ All nodes have `raw` (the full matched text including trigger) and `value`
101
+ (content only, trigger stripped). For display, use `node.raw`.
102
+
103
+ ```ts
104
+ // Typing "@eric" produces:
105
+ {type: 'facet', facetType: 'mention', raw: '@eric', value: 'eric', start: 0, end: 5}
106
+ ```
107
+
108
+ ### Active facet
109
+
110
+ When the cursor is inside a facet or right after a trigger character,
111
+ `activeFacet` is non-null:
112
+
113
+ ```ts
114
+ activeFacet: {
115
+ type: 'mention', // the facet type
116
+ value: 'er', // content after trigger (no @)
117
+ range: {start: 0, end: 3},
118
+ insert: (value, options?) => void,
119
+ }
120
+ ```
121
+
122
+ Call `activeFacet.insert('@eric')` to replace the in-progress facet with a
123
+ final value. A trailing space is appended by default; pass
124
+ `{noTrailingSpace: true}` to suppress it.
125
+
126
+ ## Emoji auto-commit example
127
+
128
+ A common pattern: when the user types a closing `:` on an emoji shortcode
129
+ (e.g. `:wave:`), immediately replace it with the emoji character.
130
+
131
+ ```tsx
132
+ const {on, activeFacet, focus} = useTapper({
133
+ facets: {
134
+ emoji: {match: /:([a-zA-Z0-9_]+):/g},
135
+ },
136
+ })
137
+
138
+ const EMOJI: Record<string, string> = {
139
+ wave: '👋',
140
+ tada: '🎉',
141
+ v: '✌️',
142
+ }
143
+
144
+ useEffect(() => {
145
+ return on('activeFacet', facet => {
146
+ if (facet?.type === 'emoji' && facet.value.endsWith(':')) {
147
+ const name = facet.value.slice(0, -1) // strip closing ":"
148
+ const emoji = EMOJI[name]
149
+ if (emoji) {
150
+ facet.insert(emoji, {noTrailingSpace: true})
151
+ }
152
+ }
153
+ })
154
+ }, [on])
155
+
156
+ // Re-focus after insertion
157
+ useEffect(() => {
158
+ return on('afterInsert', () => focus())
159
+ }, [on, focus])
160
+ ```
161
+
162
+ ## Events
163
+
164
+ Subscribe with `on(event, callback)`. Returns an unsubscribe function — use it
165
+ in a `useEffect` cleanup.
166
+
167
+ | Event | Data | When |
168
+ | ---------------- | --------------------------- | ----------------------------------------------- |
169
+ | `activeFacet` | `TapperActiveFacet \| null` | Cursor enters or leaves a facet |
170
+ | `facetCommitted` | `TapperFacet` | A facet is finalized (cursor left or committed) |
171
+ | `afterInsert` | `TapperFacet` | After `insert()` replaces text |
172
+
173
+ ### `focus()`
174
+
175
+ After `insert()` replaces text, the input may lose focus on some platforms.
176
+ Listen for `afterInsert` and call `focus()` to restore it:
177
+
178
+ ```tsx
179
+ useEffect(() => {
180
+ return on('afterInsert', () => focus())
181
+ }, [on, focus])
182
+ ```
183
+
184
+ ## Atomic deletion
185
+
186
+ Committed facets are deleted atomically — backspacing into a committed facet
187
+ from outside removes the entire facet in one keystroke, rather than deleting
188
+ character by character.
189
+
190
+ ## `initialText`
191
+
192
+ Pre-populate the input with text. Any matched facets are automatically marked
193
+ as committed.
194
+
195
+ ```tsx
196
+ const {nodes} = useTapper({
197
+ facets: {mention: {match: /@([a-zA-Z0-9._]+)/g}},
198
+ initialText: 'Hello @eric',
199
+ })
200
+ ```
201
+
202
+ ## `replaceText`
203
+
204
+ The `Tapper` class (used internally by `useTapper`) exposes `replaceText(text,
205
+ cursor?)` for programmatic text replacement. All matched facets in the new text
206
+ are marked as committed.
package/build/index.d.ts CHANGED
@@ -1,8 +1,40 @@
1
- import { type TapperConfig } from './types';
1
+ import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, TapperSnapshot } from './types';
2
2
  export * from './types';
3
- export * from './core';
3
+ export declare class Tapper {
4
+ private facetRegexes;
5
+ private triggers;
6
+ private listeners;
7
+ text: string;
8
+ cursor: number;
9
+ nodes: TapperNode[];
10
+ activeFacet: TapperActiveFacet | null;
11
+ private updating;
12
+ private pendingCursor;
13
+ private storeListeners;
14
+ private snapshot;
15
+ constructor(config: TapperConfig);
16
+ subscribe: (listener: () => void) => () => boolean;
17
+ getSnapshot: () => TapperSnapshot;
18
+ on: <K extends keyof TapperEvents>(event: K, cb: (data: TapperEvents[K]) => void) => () => void;
19
+ private emit;
20
+ private notify;
21
+ private update;
22
+ handleTextChange: (newText: string) => void;
23
+ handleSelectionChange: (e: {
24
+ nativeEvent: {
25
+ selection: {
26
+ start: number;
27
+ };
28
+ };
29
+ }) => void;
30
+ replaceText: (text: string, cursor?: number) => void;
31
+ private insert;
32
+ }
4
33
  export declare function useTapper(config: TapperConfig): {
34
+ on: <K extends keyof TapperEvents>(event: K, cb: (data: TapperEvents[K]) => void) => () => void;
35
+ focus: () => void | undefined;
5
36
  inputProps: {
37
+ ref: React.RefObject<any>;
6
38
  value: string;
7
39
  selection: {
8
40
  start: number;
@@ -17,10 +49,9 @@ export declare function useTapper(config: TapperConfig): {
17
49
  };
18
50
  }) => void;
19
51
  };
20
- store: import("./core").Tapper;
21
52
  text: string;
22
53
  cursor: number;
23
- nodes: import("./types").TapperNode[];
24
- activeFacet: import("./types").TapperActiveFacet | null;
54
+ nodes: TapperNode[];
55
+ activeFacet: TapperActiveFacet | null;
25
56
  };
26
57
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,KAAK,YAAY,EAAC,MAAM,SAAS,CAAA;AAEzC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;;;;;;;;+BASjB;YAAC,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SAAC;;;;;;;EAIvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,SAAS,CAAA;AAUhB,cAAc,SAAS,CAAA;AAEvB,qBAAa,MAAM;IACjB,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAqB;IAIrC,OAAO,CAAC,SAAS,CAA0D;IAG3E,IAAI,SAAK;IACT,MAAM,SAAI;IACV,KAAK,EAAE,UAAU,EAAE,CAAK;IACxB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAO;IAG5C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,aAAa,CAAsB;IAG3C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,MAAM,EAAE,YAAY;IAchC,SAAS,GAAI,UAAU,MAAM,IAAI,mBAGhC;IAED,WAAW,QAAO,cAAc,CAE/B;IAED,EAAE,GAAI,CAAC,SAAS,MAAM,YAAY,EAChC,OAAO,CAAC,EACR,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,gBAOpC;IAED,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,MAAM;IA8Dd,gBAAgB,GAAI,SAAS,MAAM,UAuBlC;IAED,qBAAqB,GAAI,GAAG;QAAC,WAAW,EAAE;YAAC,SAAS,EAAE;gBAAC,KAAK,EAAE,MAAM,CAAA;aAAC,CAAA;SAAC,CAAA;KAAC,UActE;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UA+B3C;IAED,OAAO,CAAC,MAAM,CAoBb;CACF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;SAxLtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;;;aAgMhB,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC;;;;;;gCA3GZ,MAAM;+BAyBP;YAAC,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SAAC;;;;;;EAyFxE"}
package/build/index.js CHANGED
@@ -1,15 +1,204 @@
1
- import { useTapperCore } from './core';
1
+ import { useRef, useState, useSyncExternalStore } from 'react';
2
+ import { parseNodesFromText, detectActiveFacet, deriveTriggers, nodeToFacet, compileFacetRegexes, } from './util';
2
3
  export * from './types';
3
- export * from './core';
4
+ export class Tapper {
5
+ facetRegexes;
6
+ triggers;
7
+ // Event emitters
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ listeners = new Map();
10
+ // Public state
11
+ text = '';
12
+ cursor = 0;
13
+ nodes = [];
14
+ activeFacet = null;
15
+ // Internal tracking
16
+ updating = false;
17
+ pendingCursor = null;
18
+ // useSyncExternalStore plumbing
19
+ storeListeners = new Set();
20
+ snapshot;
21
+ constructor(config) {
22
+ this.facetRegexes = compileFacetRegexes(config.facets);
23
+ this.triggers = deriveTriggers(config.facets);
24
+ this.snapshot = {
25
+ text: this.text,
26
+ cursor: this.cursor,
27
+ nodes: this.nodes,
28
+ activeFacet: this.activeFacet,
29
+ };
30
+ if (config.initialText) {
31
+ this.replaceText(config.initialText);
32
+ }
33
+ }
34
+ subscribe = (listener) => {
35
+ this.storeListeners.add(listener);
36
+ return () => this.storeListeners.delete(listener);
37
+ };
38
+ getSnapshot = () => {
39
+ return this.snapshot;
40
+ };
41
+ on = (event, cb) => {
42
+ if (!this.listeners.has(event))
43
+ this.listeners.set(event, new Set());
44
+ this.listeners.get(event).add(cb);
45
+ return () => {
46
+ this.listeners.get(event)?.delete(cb);
47
+ };
48
+ };
49
+ emit(event, data) {
50
+ this.listeners.get(event)?.forEach(cb => cb(data));
51
+ }
52
+ notify() {
53
+ this.snapshot = {
54
+ text: this.text,
55
+ cursor: this.cursor,
56
+ nodes: this.nodes,
57
+ activeFacet: this.activeFacet,
58
+ };
59
+ this.storeListeners.forEach(l => l());
60
+ }
61
+ update(newText, cursor) {
62
+ // guard against circular updates when commit() is called during an update cycle
63
+ if (this.updating)
64
+ return;
65
+ this.updating = true;
66
+ const nodes = newText === this.text
67
+ ? this.nodes
68
+ : parseNodesFromText(newText, this.facetRegexes, this.nodes, cursor, this.triggers);
69
+ const prev = this.activeFacet;
70
+ const detected = detectActiveFacet(nodes, newText, cursor, this.triggers);
71
+ // When cursor leaves a facet, commit it
72
+ let committedNode = null;
73
+ if (prev && (!detected || detected.type !== prev.type)) {
74
+ for (const node of nodes) {
75
+ if (node.type === 'facet' &&
76
+ node.facetType === prev.type &&
77
+ node.start === prev.range.start &&
78
+ node.end === prev.range.end) {
79
+ node.committed = true;
80
+ committedNode = node;
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ // Build activeFacet with commit() baked in
86
+ const active = detected
87
+ ? { ...detected, insert: this.insert }
88
+ : null;
89
+ this.text = newText;
90
+ this.cursor = cursor;
91
+ this.nodes = nodes;
92
+ this.activeFacet = active;
93
+ this.updating = false;
94
+ this.notify();
95
+ // Fire events after state is finalized
96
+ const facetChanged = active?.type !== prev?.type ||
97
+ active?.value !== prev?.value ||
98
+ active?.range.start !== prev?.range.start ||
99
+ active?.range.end !== prev?.range.end;
100
+ if (facetChanged) {
101
+ this.emit('activeFacet', active);
102
+ }
103
+ if (committedNode) {
104
+ this.emit('facetCommitted', nodeToFacet(committedNode));
105
+ }
106
+ }
107
+ handleTextChange = (newText) => {
108
+ const diff = newText.length - this.text.length;
109
+ let newCursor = Math.max(this.cursor + diff, 0);
110
+ // Atomic deletion: backspace into a facet from outside
111
+ if (diff === -1 && newCursor < this.cursor) {
112
+ for (const node of this.nodes) {
113
+ if (node.type === 'facet' &&
114
+ node.committed &&
115
+ this.cursor === node.end) {
116
+ const remnant = node.end - node.start - 1;
117
+ newText =
118
+ newText.slice(0, node.start) + newText.slice(node.start + remnant);
119
+ newCursor = node.start;
120
+ this.pendingCursor = newCursor;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ this.update(newText, newCursor);
126
+ };
127
+ handleSelectionChange = (e) => {
128
+ const newCursor = e.nativeEvent.selection.start;
129
+ // After atomic deletion, ignore stale cursor values from the DOM
130
+ if (this.pendingCursor !== null) {
131
+ if (newCursor !== this.pendingCursor) {
132
+ return;
133
+ }
134
+ this.pendingCursor = null;
135
+ }
136
+ if (newCursor === this.cursor)
137
+ return;
138
+ this.cursor = newCursor;
139
+ this.update(this.text, newCursor);
140
+ };
141
+ replaceText = (text, cursor) => {
142
+ const prevActive = this.activeFacet;
143
+ const nodes = parseNodesFromText(text, this.facetRegexes);
144
+ // Mark all non-text nodes as committed (pre-existing facets)
145
+ const newlyCommitted = [];
146
+ for (const node of nodes) {
147
+ if (node.type === 'facet') {
148
+ node.committed = true;
149
+ newlyCommitted.push(node);
150
+ }
151
+ }
152
+ const pos = cursor ?? text.length;
153
+ const detected = detectActiveFacet(nodes, text, pos, this.triggers);
154
+ const active = detected
155
+ ? { ...detected, insert: this.insert }
156
+ : null;
157
+ this.text = text;
158
+ this.cursor = pos;
159
+ this.nodes = nodes;
160
+ this.activeFacet = active;
161
+ this.notify();
162
+ if (active !== prevActive) {
163
+ this.emit('activeFacet', active);
164
+ }
165
+ for (const node of newlyCommitted) {
166
+ this.emit('facetCommitted', nodeToFacet(node));
167
+ }
168
+ };
169
+ insert = (value, options) => {
170
+ if (!this.activeFacet)
171
+ return;
172
+ const replacement = value + (options?.noTrailingSpace ? '' : ' ');
173
+ const newText = this.text.slice(0, this.activeFacet.range.start) +
174
+ replacement +
175
+ this.text.slice(this.activeFacet.range.end);
176
+ const newCursor = this.activeFacet.range.start + replacement.length;
177
+ if (this.activeFacet) {
178
+ this.activeFacet.value = value;
179
+ this.activeFacet.range = {
180
+ start: this.activeFacet.range.start,
181
+ end: this.activeFacet.range.start + value.length,
182
+ };
183
+ this.emit('afterInsert', this.activeFacet);
184
+ }
185
+ this.update(newText, newCursor);
186
+ };
187
+ }
4
188
  export function useTapper(config) {
5
- const tapper = useTapperCore(config);
189
+ const [store] = useState(() => new Tapper(config));
190
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
191
+ const inputRef = useRef(null);
6
192
  return {
7
- ...tapper,
193
+ ...state,
194
+ on: store.on,
195
+ focus: () => inputRef.current?.focus(),
8
196
  inputProps: {
9
- value: tapper.text,
10
- selection: { start: tapper.cursor, end: tapper.cursor },
11
- onChangeText: tapper.store.handleTextChange,
12
- onSelectionChange: (e) => tapper.store.handleSelectionChange(e.nativeEvent.selection.start),
197
+ ref: inputRef,
198
+ value: state.text,
199
+ selection: { start: state.cursor, end: state.cursor },
200
+ onChangeText: store.handleTextChange,
201
+ onSelectionChange: store.handleSelectionChange,
13
202
  },
14
203
  };
15
204
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,QAAQ,CAAA;AAGpC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,MAAM,UAAU,SAAS,CAAC,MAAoB;IAC5C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IAEpC,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE;YACV,KAAK,EAAE,MAAM,CAAC,IAAI;YAClB,SAAS,EAAE,EAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAC;YACrD,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,gBAAgB;YAC3C,iBAAiB,EAAE,CAAC,CAA8C,EAAE,EAAE,CACpE,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC;SACpE;KACF,CAAA;AACH,CAAC","sourcesContent":["import {useTapperCore} from './core'\nimport {type TapperConfig} from './types'\n\nexport * from './types'\nexport * from './core'\n\nexport function useTapper(config: TapperConfig) {\n const tapper = useTapperCore(config)\n\n return {\n ...tapper,\n inputProps: {\n value: tapper.text,\n selection: {start: tapper.cursor, end: tapper.cursor},\n onChangeText: tapper.store.handleTextChange,\n onSelectionChange: (e: {nativeEvent: {selection: {start: number}}}) =>\n tapper.store.handleSelectionChange(e.nativeEvent.selection.start),\n },\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {useRef, useState, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperEvents,\n TapperNode,\n TapperActiveFacet,\n TapperSnapshot,\n} from './types'\nimport {\n parseNodesFromText,\n detectActiveFacet,\n deriveTriggers,\n nodeToFacet,\n compileFacetRegexes,\n type CompiledFacetRegexes,\n} from './util'\n\nexport * from './types'\n\nexport class Tapper {\n private facetRegexes: CompiledFacetRegexes\n private triggers: Map<string, string>\n\n // Event emitters\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()\n\n // Public state\n text = ''\n cursor = 0\n nodes: TapperNode[] = []\n activeFacet: TapperActiveFacet | null = null\n\n // Internal tracking\n private updating = false\n private pendingCursor: number | null = null\n\n // useSyncExternalStore plumbing\n private storeListeners = new Set<() => void>()\n private snapshot: TapperSnapshot\n\n constructor(config: TapperConfig) {\n this.facetRegexes = compileFacetRegexes(config.facets)\n this.triggers = deriveTriggers(config.facets)\n this.snapshot = {\n text: this.text,\n cursor: this.cursor,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n if (config.initialText) {\n this.replaceText(config.initialText)\n }\n }\n\n subscribe = (listener: () => void) => {\n this.storeListeners.add(listener)\n return () => this.storeListeners.delete(listener)\n }\n\n getSnapshot = (): TapperSnapshot => {\n return this.snapshot\n }\n\n on = <K extends keyof TapperEvents>(\n event: K,\n cb: (data: TapperEvents[K]) => void,\n ) => {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set())\n this.listeners.get(event)!.add(cb)\n return () => {\n this.listeners.get(event)?.delete(cb)\n }\n }\n\n private emit<K extends keyof TapperEvents>(event: K, data: TapperEvents[K]) {\n this.listeners.get(event)?.forEach(cb => cb(data))\n }\n\n private notify() {\n this.snapshot = {\n text: this.text,\n cursor: this.cursor,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n this.storeListeners.forEach(l => l())\n }\n\n private update(newText: string, cursor: number) {\n // guard against circular updates when commit() is called during an update cycle\n if (this.updating) return\n this.updating = true\n\n const nodes =\n newText === this.text\n ? this.nodes\n : parseNodesFromText(\n newText,\n this.facetRegexes,\n this.nodes,\n cursor,\n this.triggers,\n )\n const prev = this.activeFacet\n const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n let committedNode: TapperNode | null = null\n if (prev && (!detected || detected.type !== prev.type)) {\n for (const node of nodes) {\n if (\n node.type === 'facet' &&\n node.facetType === prev.type &&\n node.start === prev.range.start &&\n node.end === prev.range.end\n ) {\n node.committed = true\n committedNode = node\n break\n }\n }\n }\n\n // Build activeFacet with commit() baked in\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = newText\n this.cursor = cursor\n this.nodes = nodes\n this.activeFacet = active\n\n this.updating = false\n this.notify()\n\n // Fire events after state is finalized\n const facetChanged =\n active?.type !== prev?.type ||\n active?.value !== prev?.value ||\n active?.range.start !== prev?.range.start ||\n active?.range.end !== prev?.range.end\n if (facetChanged) {\n this.emit('activeFacet', active)\n }\n if (committedNode) {\n this.emit('facetCommitted', nodeToFacet(committedNode))\n }\n }\n\n handleTextChange = (newText: string) => {\n const diff = newText.length - this.text.length\n let newCursor = Math.max(this.cursor + diff, 0)\n\n // Atomic deletion: backspace into a facet from outside\n if (diff === -1 && newCursor < this.cursor) {\n for (const node of this.nodes) {\n if (\n node.type === 'facet' &&\n node.committed &&\n this.cursor === node.end\n ) {\n const remnant = node.end - node.start - 1\n newText =\n newText.slice(0, node.start) + newText.slice(node.start + remnant)\n newCursor = node.start\n this.pendingCursor = newCursor\n break\n }\n }\n }\n\n this.update(newText, newCursor)\n }\n\n handleSelectionChange = (e: {nativeEvent: {selection: {start: number}}}) => {\n const newCursor = e.nativeEvent.selection.start\n // After atomic deletion, ignore stale cursor values from the DOM\n if (this.pendingCursor !== null) {\n if (newCursor !== this.pendingCursor) {\n return\n }\n this.pendingCursor = null\n }\n\n if (newCursor === this.cursor) return\n\n this.cursor = newCursor\n this.update(this.text, newCursor)\n }\n\n replaceText = (text: string, cursor?: number) => {\n const prevActive = this.activeFacet\n const nodes = parseNodesFromText(text, this.facetRegexes)\n // Mark all non-text nodes as committed (pre-existing facets)\n const newlyCommitted: TapperNode[] = []\n for (const node of nodes) {\n if (node.type === 'facet') {\n node.committed = true\n newlyCommitted.push(node)\n }\n }\n\n const pos = cursor ?? text.length\n const detected = detectActiveFacet(nodes, text, pos, this.triggers)\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = text\n this.cursor = pos\n this.nodes = nodes\n this.activeFacet = active\n\n this.notify()\n\n if (active !== prevActive) {\n this.emit('activeFacet', active)\n }\n for (const node of newlyCommitted) {\n this.emit('facetCommitted', nodeToFacet(node))\n }\n }\n\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n if (!this.activeFacet) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, this.activeFacet.range.start) +\n replacement +\n this.text.slice(this.activeFacet.range.end)\n const newCursor = this.activeFacet.range.start + replacement.length\n\n if (this.activeFacet) {\n this.activeFacet.value = value\n this.activeFacet.range = {\n start: this.activeFacet.range.start,\n end: this.activeFacet.range.start + value.length,\n }\n this.emit('afterInsert', this.activeFacet)\n }\n\n this.update(newText, newCursor)\n }\n}\n\nexport function useTapper(config: TapperConfig) {\n const [store] = useState(() => new Tapper(config))\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot)\n const inputRef = useRef<{focus(): void}>(null)\n\n return {\n ...state,\n on: store.on,\n focus: () => inputRef.current?.focus(),\n inputProps: {\n ref: inputRef as React.RefObject<any>,\n value: state.text,\n selection: {start: state.cursor, end: state.cursor},\n onChangeText: store.handleTextChange,\n onSelectionChange: store.handleSelectionChange,\n },\n }\n}\n"]}
package/build/types.d.ts CHANGED
@@ -3,15 +3,33 @@ export type TapperFacetConfig = {
3
3
  };
4
4
  export type TapperFacetConfigMap = Record<string, TapperFacetConfig>;
5
5
  export type TapperNode = {
6
- type: string;
6
+ type: 'text';
7
+ raw: string;
8
+ value: string;
9
+ start: number;
10
+ end: number;
11
+ committed?: boolean;
12
+ facetType?: undefined;
13
+ } | {
14
+ type: 'trigger';
15
+ raw: string;
16
+ value: string;
17
+ start: number;
18
+ end: number;
19
+ committed?: boolean;
20
+ facetType: string;
21
+ } | {
22
+ type: 'facet';
23
+ raw: string;
7
24
  value: string;
8
25
  start: number;
9
26
  end: number;
10
27
  committed?: boolean;
28
+ facetType: string;
11
29
  };
12
30
  export type TapperFacet = {
13
- facetType: string;
14
- query: string;
31
+ type: string;
32
+ value: string;
15
33
  range: {
16
34
  start: number;
17
35
  end: number;
@@ -32,4 +50,9 @@ export type TapperConfig = {
32
50
  facets: TapperFacetConfigMap;
33
51
  initialText?: string;
34
52
  };
53
+ export type TapperEvents = {
54
+ activeFacet: TapperActiveFacet | null;
55
+ facetCommitted: TapperFacet;
56
+ afterInsert: TapperFacet;
57
+ };
35
58
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAEpE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,oBAAoB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAEpE,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,GACD;IACE,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,GACD;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,oBAAoB,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,cAAc,EAAE,WAAW,CAAA;IAC3B,WAAW,EAAE,WAAW,CAAA;CACzB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = {\n match: RegExp\n}\n\nexport type TapperFacetConfigMap = Record<string, TapperFacetConfig>\n\nexport type TapperNode = {\n type: string\n value: string\n start: number\n end: number\n committed?: boolean\n}\n\nexport type TapperFacet = {\n facetType: string\n query: string\n range: {start: number; end: number}\n}\n\nexport type TapperActiveFacet = TapperFacet & {\n insert: (value: string, options?: {noTrailingSpace?: boolean}) => void\n}\n\nexport type TapperSnapshot = {\n text: string\n cursor: number\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets: TapperFacetConfigMap\n initialText?: string\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = {\n match: RegExp\n}\n\nexport type TapperFacetConfigMap = Record<string, TapperFacetConfig>\n\nexport type TapperNode =\n | {\n type: 'text'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType?: undefined\n }\n | {\n type: 'trigger'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n | {\n type: 'facet'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n\nexport type TapperFacet = {\n type: string\n value: string\n range: {start: number; end: number}\n}\n\nexport type TapperActiveFacet = TapperFacet & {\n insert: (value: string, options?: {noTrailingSpace?: boolean}) => void\n}\n\nexport type TapperSnapshot = {\n text: string\n cursor: number\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets: TapperFacetConfigMap\n initialText?: string\n}\n\nexport type TapperEvents = {\n activeFacet: TapperActiveFacet | null\n facetCommitted: TapperFacet\n afterInsert: TapperFacet\n}\n"]}
package/build/util.d.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import { TapperFacetConfigMap, TapperNode, TapperFacet } from './types';
2
- export declare function parseNodesFromText(text: string, config: TapperFacetConfigMap, prevNodes?: TapperNode[]): TapperNode[];
2
+ export type CompiledFacetRegexes = Map<string, RegExp>;
3
+ /**
4
+ * Pre-compile facet regexes once at init time. This avoids re-creating
5
+ * RegExp objects on every keystroke in parseNodesFromText. Each Tapper
6
+ * instance gets its own compiled copy so lastIndex state can't leak
7
+ * between instances.
8
+ */
9
+ export declare function compileFacetRegexes(config: TapperFacetConfigMap): CompiledFacetRegexes;
10
+ export declare function parseNodesFromText(text: string, regexes: CompiledFacetRegexes, prevNodes?: TapperNode[], cursor?: number, triggers?: Map<string, string>): TapperNode[];
3
11
  export declare function deriveTriggers(config: TapperFacetConfigMap): Map<string, string>;
12
+ export declare function nodeToFacet(node: TapperNode): TapperFacet;
4
13
  export declare function detectActiveFacet(nodes: TapperNode[], text: string, cursor: number, triggers: Map<string, string>): TapperFacet | null;
5
14
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,WAAW,EACZ,MAAM,SAAS,CAAA;AAEhB,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,oBAAoB,EAC5B,SAAS,CAAC,EAAE,UAAU,EAAE,GACvB,UAAU,EAAE,CAyFd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOhF;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,WAAW,GAAG,IAAI,CA2BpB"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAC,MAAM,SAAS,CAAA;AAIrE,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,oBAAoB,GAC3B,oBAAoB,CAMtB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,EAC7B,SAAS,CAAC,EAAE,UAAU,EAAE,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,UAAU,EAAE,CAqJd;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,oBAAoB,GAC3B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAOrB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAMzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,WAAW,GAAG,IAAI,CAkCpB"}