@estiva-app/ui 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,15 @@ export interface SuggestionPopupOptions<T> {
22
22
  sections?: (items: T[], query: string) => readonly SuggestionMenuSection<T>[];
23
23
  itemKey: (item: T) => string;
24
24
  row: (item: T) => SuggestionMenuRow;
25
+ /**
26
+ * With `refresh`: called with a listener while the list is open; the app
27
+ * calls it when its data has changed (a search landing), and the open list is
28
+ * drawn again for the same query. Returns the unsubscribe, run when the list
29
+ * closes however it closes.
30
+ */
31
+ subscribe?: (listener: () => void) => () => void;
32
+ /** The items for a query, as the plugin's `items` answers — asked again when `subscribe` fires. */
33
+ refresh?: (query: string) => T[];
25
34
  }
26
35
  /**
27
36
  * The `render` of a Tiptap suggestion plugin, drawn as a `SuggestionMenu` —
@@ -1 +1 @@
1
- {"version":3,"file":"suggestionPopup.d.ts","sourceRoot":"","sources":["../src/suggestionPopup.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAA0B,iBAAiB,EAAmB,MAAM,oBAAoB,CAAA;AACpG,OAAO,EAA6C,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAchI;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,mFAAmF;AACnF,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7E,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAA;IAC5B,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,iBAAiB,CAAA;CACpC;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAgClH"}
1
+ {"version":3,"file":"suggestionPopup.d.ts","sourceRoot":"","sources":["../src/suggestionPopup.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAA0B,iBAAiB,EAAmB,MAAM,oBAAoB,CAAA;AACpG,OAAO,EAA6C,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAchI;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,mFAAmF;AACnF,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7E,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAA;IAC5B,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,iBAAiB,CAAA;IACnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;IAChD,mGAAmG;IACnG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,EAAE,CAAA;CACjC;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAmDlH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -88,6 +88,7 @@
88
88
  "eslint-plugin-better-tailwindcss": "^4.7.0",
89
89
  "typescript-eslint": "^8.0.0",
90
90
  "@tiptap/core": "^3.22.1",
91
+ "@tiptap/extension-mention": "^3.22.1",
91
92
  "@tiptap/pm": "^3.22.1",
92
93
  "@tiptap/react": "^3.22.1",
93
94
  "@tiptap/suggestion": "^3.22.1"
@@ -105,6 +106,9 @@
105
106
  "@tiptap/core": {
106
107
  "optional": true
107
108
  },
109
+ "@tiptap/extension-mention": {
110
+ "optional": true
111
+ },
108
112
  "@tiptap/pm": {
109
113
  "optional": true
110
114
  },
@@ -126,6 +130,7 @@
126
130
  "@testing-library/react": "^16.3.3",
127
131
  "@testing-library/user-event": "^14.6.7",
128
132
  "@tiptap/core": "^3.31.3",
133
+ "@tiptap/extension-mention": "^3.31.3",
129
134
  "@tiptap/pm": "^3.31.3",
130
135
  "@tiptap/react": "^3.31.3",
131
136
  "@tiptap/starter-kit": "^3.31.3",
package/registry.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.51.0",
8
- "exports": 105,
9
- "typeExports": 116,
7
+ "packageVersion": "0.53.0",
8
+ "exports": 120,
9
+ "typeExports": 128,
10
10
  "files": null
11
11
  },
12
12
  "storybook": {
@@ -15,6 +15,25 @@
15
15
  "devUrl": "http://localhost:6008"
16
16
  },
17
17
  "entries": [
18
+ {
19
+ "name": "applyFormat",
20
+ "repo": "estiva-ui",
21
+ "kind": "helper",
22
+ "importPath": "@estiva-app/ui/editor",
23
+ "sourceFile": "src/ComposerTriggers.tsx",
24
+ "purpose": "Apply one format to the current block. `set*` wherever Tiptap has one: choosing Heading should make a heading, not turn one off.",
25
+ "purposeFrom": "comment",
26
+ "props": [],
27
+ "variants": [],
28
+ "ownsBehaviours": [],
29
+ "status": "stable",
30
+ "migrationStage": null,
31
+ "docsId": "primitives-composertriggers--docs",
32
+ "storyId": "primitives-composertriggers--default",
33
+ "docPage": null,
34
+ "app": null,
35
+ "looks": []
36
+ },
18
37
  {
19
38
  "name": "AppShell",
20
39
  "repo": "estiva-ui",
@@ -583,6 +602,25 @@
583
602
  "signal:bg-transparent signal:border signal:border-border-default signal:text-text-primary signal:shadow-none signal:hover:bg-success-muted signal:hover:border-success-outline signal:hover:text-success-default signal:transition-colors"
584
603
  ]
585
604
  },
605
+ {
606
+ "name": "CaptionedReference",
607
+ "repo": "estiva-ui",
608
+ "kind": "component",
609
+ "importPath": "@estiva-app/ui/editor",
610
+ "sourceFile": "src/ComposerTriggers.tsx",
611
+ "purpose": "A reference mid-sentence, drawn as `label: snippet` and written as its `uri` verbatim (SPEC §13.1).",
612
+ "purposeFrom": "comment",
613
+ "props": [],
614
+ "variants": [],
615
+ "ownsBehaviours": [],
616
+ "status": "stable",
617
+ "migrationStage": null,
618
+ "docsId": "primitives-composertriggers--docs",
619
+ "storyId": "primitives-composertriggers--default",
620
+ "docPage": null,
621
+ "app": null,
622
+ "looks": []
623
+ },
586
624
  {
587
625
  "name": "Card",
588
626
  "repo": "estiva-ui",
@@ -2176,6 +2214,63 @@
2176
2214
  "app": null,
2177
2215
  "looks": []
2178
2216
  },
2217
+ {
2218
+ "name": "filterPeople",
2219
+ "repo": "estiva-ui",
2220
+ "kind": "helper",
2221
+ "importPath": "@estiva-app/ui/editor",
2222
+ "sourceFile": "src/ComposerTriggers.tsx",
2223
+ "purpose": "The people whose name holds the query, the first {@link PEOPLE_LIMIT} of them.",
2224
+ "purposeFrom": "comment",
2225
+ "props": [],
2226
+ "variants": [],
2227
+ "ownsBehaviours": [],
2228
+ "status": "stable",
2229
+ "migrationStage": null,
2230
+ "docsId": "primitives-composertriggers--docs",
2231
+ "storyId": "primitives-composertriggers--default",
2232
+ "docPage": null,
2233
+ "app": null,
2234
+ "looks": []
2235
+ },
2236
+ {
2237
+ "name": "filterReferences",
2238
+ "repo": "estiva-ui",
2239
+ "kind": "helper",
2240
+ "importPath": "@estiva-app/ui/editor",
2241
+ "sourceFile": "src/ComposerTriggers.tsx",
2242
+ "purpose": "The references whose label or text holds the query — the last {@link REFERENCE_LIMIT}, in the order given: a thread is unbounded, and a list that grew with it would be a hundred rows of prose.",
2243
+ "purposeFrom": "comment",
2244
+ "props": [],
2245
+ "variants": [],
2246
+ "ownsBehaviours": [],
2247
+ "status": "stable",
2248
+ "migrationStage": null,
2249
+ "docsId": "primitives-composertriggers--docs",
2250
+ "storyId": "primitives-composertriggers--default",
2251
+ "docPage": null,
2252
+ "app": null,
2253
+ "looks": []
2254
+ },
2255
+ {
2256
+ "name": "filterSlashSections",
2257
+ "repo": "estiva-ui",
2258
+ "kind": "helper",
2259
+ "importPath": "@estiva-app/ui/editor",
2260
+ "sourceFile": "src/ComposerTriggers.tsx",
2261
+ "purpose": "The sections narrowed to a query — the text typed after the `/`.",
2262
+ "purposeFrom": "comment",
2263
+ "props": [],
2264
+ "variants": [],
2265
+ "ownsBehaviours": [],
2266
+ "status": "stable",
2267
+ "migrationStage": null,
2268
+ "docsId": "primitives-composertriggers--docs",
2269
+ "storyId": "primitives-composertriggers--default",
2270
+ "docPage": null,
2271
+ "app": null,
2272
+ "looks": []
2273
+ },
2179
2274
  {
2180
2275
  "name": "Form",
2181
2276
  "repo": "estiva-ui",
@@ -2239,6 +2334,44 @@
2239
2334
  "app": null,
2240
2335
  "looks": []
2241
2336
  },
2337
+ {
2338
+ "name": "FORMATS",
2339
+ "repo": "estiva-ui",
2340
+ "kind": "helper",
2341
+ "importPath": "@estiva-app/ui/editor",
2342
+ "sourceFile": "src/ComposerTriggers.tsx",
2343
+ "purpose": "The formats `/` offers, in order. The hint is what you would type, not a key chord.",
2344
+ "purposeFrom": "comment",
2345
+ "props": [],
2346
+ "variants": [],
2347
+ "ownsBehaviours": [],
2348
+ "status": "stable",
2349
+ "migrationStage": null,
2350
+ "docsId": "primitives-composertriggers--docs",
2351
+ "storyId": "primitives-composertriggers--default",
2352
+ "docPage": null,
2353
+ "app": null,
2354
+ "looks": []
2355
+ },
2356
+ {
2357
+ "name": "formatSection",
2358
+ "repo": "estiva-ui",
2359
+ "kind": "helper",
2360
+ "importPath": "@estiva-app/ui/editor",
2361
+ "sourceFile": "src/ComposerTriggers.tsx",
2362
+ "purpose": "The Format section every app's `/` opens with.",
2363
+ "purposeFrom": "comment",
2364
+ "props": [],
2365
+ "variants": [],
2366
+ "ownsBehaviours": [],
2367
+ "status": "stable",
2368
+ "migrationStage": null,
2369
+ "docsId": "primitives-composertriggers--docs",
2370
+ "storyId": "primitives-composertriggers--default",
2371
+ "docPage": null,
2372
+ "app": null,
2373
+ "looks": []
2374
+ },
2242
2375
  {
2243
2376
  "name": "hueFor",
2244
2377
  "repo": "estiva-ui",
@@ -3725,6 +3858,25 @@
3725
3858
  "app": null,
3726
3859
  "looks": []
3727
3860
  },
3861
+ {
3862
+ "name": "peopleMenu",
3863
+ "repo": "estiva-ui",
3864
+ "kind": "helper",
3865
+ "importPath": "@estiva-app/ui/editor",
3866
+ "sourceFile": "src/ComposerTriggers.tsx",
3867
+ "purpose": "The people list `@` and `!@` open: the face, the name, the second line, and the Enter hint while highlighted — `MenuItem` at its tall size.",
3868
+ "purposeFrom": "comment",
3869
+ "props": [],
3870
+ "variants": [],
3871
+ "ownsBehaviours": [],
3872
+ "status": "stable",
3873
+ "migrationStage": null,
3874
+ "docsId": "primitives-composertriggers--docs",
3875
+ "storyId": "primitives-composertriggers--default",
3876
+ "docPage": null,
3877
+ "app": null,
3878
+ "looks": []
3879
+ },
3728
3880
  {
3729
3881
  "name": "Person",
3730
3882
  "repo": "estiva-ui",
@@ -3775,6 +3927,25 @@
3775
3927
  "app": null,
3776
3928
  "looks": []
3777
3929
  },
3930
+ {
3931
+ "name": "PersonMention",
3932
+ "repo": "estiva-ui",
3933
+ "kind": "component",
3934
+ "importPath": "@estiva-app/ui/editor",
3935
+ "sourceFile": "src/ComposerTriggers.tsx",
3936
+ "purpose": "`@` — a person, as a chip that writes their key (SPEC §13.1).",
3937
+ "purposeFrom": "comment",
3938
+ "props": [],
3939
+ "variants": [],
3940
+ "ownsBehaviours": [],
3941
+ "status": "stable",
3942
+ "migrationStage": null,
3943
+ "docsId": "primitives-composertriggers--docs",
3944
+ "storyId": "primitives-composertriggers--default",
3945
+ "docPage": null,
3946
+ "app": null,
3947
+ "looks": []
3948
+ },
3778
3949
  {
3779
3950
  "name": "PersonTrigger",
3780
3951
  "repo": "estiva-ui",
@@ -4410,6 +4581,44 @@
4410
4581
  "app": null,
4411
4582
  "looks": []
4412
4583
  },
4584
+ {
4585
+ "name": "referenceMenu",
4586
+ "repo": "estiva-ui",
4587
+ "kind": "helper",
4588
+ "importPath": "@estiva-app/ui/editor",
4589
+ "sourceFile": "src/ComposerTriggers.tsx",
4590
+ "purpose": "The list `[` opens: `label: snippet` rows, the same box as the people list.",
4591
+ "purposeFrom": "comment",
4592
+ "props": [
4593
+ {
4594
+ "name": "ariaLabel",
4595
+ "takes": "text",
4596
+ "required": true,
4597
+ "note": "Names the list, e.g. what can be referenced."
4598
+ },
4599
+ {
4600
+ "name": "sectionLabel",
4601
+ "takes": "text",
4602
+ "required": true,
4603
+ "note": "The section's heading."
4604
+ },
4605
+ {
4606
+ "name": "icon",
4607
+ "takes": "anything",
4608
+ "required": true,
4609
+ "note": "At each row's start: a 16px icon."
4610
+ }
4611
+ ],
4612
+ "variants": [],
4613
+ "ownsBehaviours": [],
4614
+ "status": "stable",
4615
+ "migrationStage": null,
4616
+ "docsId": "primitives-composertriggers--docs",
4617
+ "storyId": "primitives-composertriggers--default",
4618
+ "docPage": null,
4619
+ "app": null,
4620
+ "looks": []
4621
+ },
4413
4622
  {
4414
4623
  "name": "ReferenceNode",
4415
4624
  "repo": "estiva-ui",
@@ -4429,6 +4638,25 @@
4429
4638
  "app": null,
4430
4639
  "looks": []
4431
4640
  },
4641
+ {
4642
+ "name": "ReferenceTrigger",
4643
+ "repo": "estiva-ui",
4644
+ "kind": "component",
4645
+ "importPath": "@estiva-app/ui/editor",
4646
+ "sourceFile": "src/ComposerTriggers.tsx",
4647
+ "purpose": "`[` — reference something, inserted as a {@link CaptionedReference} chip.",
4648
+ "purposeFrom": "comment",
4649
+ "props": [],
4650
+ "variants": [],
4651
+ "ownsBehaviours": [],
4652
+ "status": "stable",
4653
+ "migrationStage": null,
4654
+ "docsId": "primitives-composertriggers--docs",
4655
+ "storyId": "primitives-composertriggers--default",
4656
+ "docPage": null,
4657
+ "app": null,
4658
+ "looks": []
4659
+ },
4432
4660
  {
4433
4661
  "name": "RichText",
4434
4662
  "repo": "estiva-ui",
@@ -5168,6 +5396,25 @@
5168
5396
  "app": null,
5169
5397
  "looks": []
5170
5398
  },
5399
+ {
5400
+ "name": "SlashCommands",
5401
+ "repo": "estiva-ui",
5402
+ "kind": "component",
5403
+ "importPath": "@estiva-app/ui/editor",
5404
+ "sourceFile": "src/ComposerTriggers.tsx",
5405
+ "purpose": "`/` — format the line or insert something, at the caret, mid-sentence too.",
5406
+ "purposeFrom": "comment",
5407
+ "props": [],
5408
+ "variants": [],
5409
+ "ownsBehaviours": [],
5410
+ "status": "stable",
5411
+ "migrationStage": null,
5412
+ "docsId": "primitives-composertriggers--docs",
5413
+ "storyId": "primitives-composertriggers--default",
5414
+ "docPage": null,
5415
+ "app": null,
5416
+ "looks": []
5417
+ },
5171
5418
  {
5172
5419
  "name": "SuggestionMenu",
5173
5420
  "repo": "estiva-ui",
@@ -5321,6 +5568,18 @@
5321
5568
  "takes": "a handler",
5322
5569
  "required": true,
5323
5570
  "note": null
5571
+ },
5572
+ {
5573
+ "name": "subscribe",
5574
+ "takes": "a handler",
5575
+ "required": false,
5576
+ "note": "With `refresh`: called with a listener while the list is open; the app calls it when its data has changed (a search landing), and the open list is drawn again for the same query."
5577
+ },
5578
+ {
5579
+ "name": "refresh",
5580
+ "takes": "a handler",
5581
+ "required": false,
5582
+ "note": "The items for a query, as the plugin's `items` answers — asked again when `subscribe` fires."
5324
5583
  }
5325
5584
  ],
5326
5585
  "variants": [],
@@ -6175,6 +6434,25 @@
6175
6434
  "app": null,
6176
6435
  "looks": []
6177
6436
  },
6437
+ {
6438
+ "name": "typeTrigger",
6439
+ "repo": "estiva-ui",
6440
+ "kind": "helper",
6441
+ "importPath": "@estiva-app/ui/editor",
6442
+ "sourceFile": "src/ComposerTriggers.tsx",
6443
+ "purpose": "A row that types another trigger, so its own list opens — `@` from `/`.",
6444
+ "purposeFrom": "comment",
6445
+ "props": [],
6446
+ "variants": [],
6447
+ "ownsBehaviours": [],
6448
+ "status": "stable",
6449
+ "migrationStage": null,
6450
+ "docsId": "primitives-composertriggers--docs",
6451
+ "storyId": "primitives-composertriggers--default",
6452
+ "docPage": null,
6453
+ "app": null,
6454
+ "looks": []
6455
+ },
6178
6456
  {
6179
6457
  "name": "UnknownBlock",
6180
6458
  "repo": "estiva-ui",
@@ -6194,6 +6472,51 @@
6194
6472
  "app": null,
6195
6473
  "looks": []
6196
6474
  },
6475
+ {
6476
+ "name": "UrgentPersonMention",
6477
+ "repo": "estiva-ui",
6478
+ "kind": "component",
6479
+ "importPath": "@estiva-app/ui/editor",
6480
+ "sourceFile": "src/ComposerTriggers.tsx",
6481
+ "purpose": "`!@` — a person, urgently: the same chip in the urgent tone.",
6482
+ "purposeFrom": "comment",
6483
+ "props": [],
6484
+ "variants": [],
6485
+ "ownsBehaviours": [],
6486
+ "status": "stable",
6487
+ "migrationStage": null,
6488
+ "docsId": "primitives-composertriggers--docs",
6489
+ "storyId": "primitives-composertriggers--default",
6490
+ "docPage": null,
6491
+ "app": null,
6492
+ "looks": []
6493
+ },
6494
+ {
6495
+ "name": "urgentPubkeys",
6496
+ "repo": "estiva-ui",
6497
+ "kind": "helper",
6498
+ "importPath": "@estiva-app/ui/editor",
6499
+ "sourceFile": "src/ComposerTriggers.tsx",
6500
+ "purpose": "The keys the document's urgent mentions name — what the write turns into `[\"urgent\", <pubkey>]` tags.",
6501
+ "purposeFrom": "comment",
6502
+ "props": [
6503
+ {
6504
+ "name": "state",
6505
+ "takes": "{ doc: Doc }",
6506
+ "required": true,
6507
+ "note": null
6508
+ }
6509
+ ],
6510
+ "variants": [],
6511
+ "ownsBehaviours": [],
6512
+ "status": "stable",
6513
+ "migrationStage": null,
6514
+ "docsId": "primitives-composertriggers--docs",
6515
+ "storyId": "primitives-composertriggers--default",
6516
+ "docPage": null,
6517
+ "app": null,
6518
+ "looks": []
6519
+ },
6197
6520
  {
6198
6521
  "name": "useToast",
6199
6522
  "repo": "estiva-ui",
@@ -0,0 +1,115 @@
1
+ import { Meta, Canvas } from '@storybook/addon-docs/blocks'
2
+ import * as ComposerTriggersStories from './ComposerTriggers.stories'
3
+
4
+ <Meta of={ComposerTriggersStories} />
5
+
6
+ # ComposerTriggers
7
+
8
+ The four keys a composer shares across apps: `@` and `!@` to mention a person,
9
+ `[` to reference something, `/` to format the line or insert something. Each
10
+ opens a `SuggestionMenu` at the caret; a pick inserts a chip.
11
+
12
+ <Canvas of={ComposerTriggersStories.Default} />
13
+
14
+ ## When
15
+
16
+ - **`PersonMention` and `UrgentPersonMention`** — any editor whose text names
17
+ people. The urgent one is the same chip in the urgent tone.
18
+ - **`CaptionedReference` and `ReferenceTrigger`** — referencing something the
19
+ app can list, drawn as who wrote it and its opening words.
20
+ - **`SlashCommands`** — `formatSection()` first, then only the rows the app can
21
+ perform. `formatSection('block')` makes Code a block, for an editor of a
22
+ block document.
23
+
24
+ ## When not
25
+
26
+ - Formatting text already written → **SelectionToolbar**. Bold, italic and
27
+ underline are not in `/` for that reason.
28
+ - A list that is not opened by a key typed in an editor → **CommandPalette** or
29
+ **Menu**.
30
+ - Drawing a sent body → **RichText**. These are for editing only.
31
+
32
+ ## How
33
+
34
+ ```tsx
35
+ import { EditorContent, useEditor } from '@tiptap/react'
36
+ import StarterKit from '@tiptap/starter-kit'
37
+ import { IconSquareRounded } from '@tabler/icons-react'
38
+ import {
39
+ CaptionedReference,
40
+ PersonMention,
41
+ ReferenceTrigger,
42
+ SlashCommands,
43
+ UrgentPersonMention,
44
+ formatSection,
45
+ typeTrigger,
46
+ urgentPubkeys,
47
+ type CaptionedItem,
48
+ type MentionPerson,
49
+ } from '@estiva-app/ui/editor'
50
+
51
+ const people = (): MentionPerson[] => [{ id: 'one', name: 'Person one', pubkey: null }]
52
+ const items = (): CaptionedItem[] => [{ id: 'one', label: 'Person one', snippet: 'Item one', uri: 'nostr:item' }]
53
+
54
+ const editor = useEditor({
55
+ extensions: [
56
+ StarterKit,
57
+ PersonMention.configure({ people }),
58
+ UrgentPersonMention.configure({ people }),
59
+ CaptionedReference.configure({ icon: <IconSquareRounded size={14} stroke={1.5} /> }),
60
+ ReferenceTrigger.configure({ items, ariaLabel: 'Items', sectionLabel: 'Items', icon: <IconSquareRounded size={16} stroke={1.5} /> }),
61
+ SlashCommands.configure({
62
+ sections: [formatSection(), { label: 'Insert', commands: [{ key: 'insert-@', row: { label: 'Mention', shortcut: '@' }, run: typeTrigger('@') }] }],
63
+ }),
64
+ ],
65
+ })
66
+
67
+ const urgent = editor ? urgentPubkeys(editor) : undefined
68
+
69
+ <EditorContent editor={editor} />
70
+ ```
71
+
72
+ - **The body is written by `@estiva-app/conversation`, not here.** A mention
73
+ chip carries `pubkey` and `label` for its `mentionText`; a reference chip
74
+ carries the `uri` the app built with that package, written back verbatim. The
75
+ urgency is not in the text: hand `urgentPubkeys(editor)` to `urgentTagsFor`.
76
+ - **The lists ask each time the query changes.** `people` and `items` are
77
+ functions, so they read the app's current data. `@` shows the first six
78
+ names holding the query; `[` the last six whose label or `search` (default:
79
+ `snippet`) holds it.
80
+ - **`[` in sections, filled in as answers land:** pass `sections(query)` in
81
+ place of `items` — labelled groups the app has already narrowed and ordered;
82
+ an empty group is not drawn — and `subscribe(listener)`, which the app calls
83
+ when a slower answer (a search) has landed. The open list is drawn again for
84
+ the same query; nothing waits for it. A row's `description` is its second
85
+ line, which the chip does not wear. A row the person has arrowed to stays
86
+ highlighted when the list is drawn again.
87
+ - **An app's own node name:** `CaptionedReference.extend({ name: 'yourName' })`
88
+ and `ReferenceTrigger.configure({ nodeName: 'yourName' })`. The chip's HTML
89
+ attribute follows the name (`data-your-name`).
90
+ - **A row that opens another list** runs `typeTrigger('@')`: it types the key
91
+ once the `/query` is gone.
92
+ - An open list is reported by `isSuggestionActive()`, so an editor that sends
93
+ on Enter does not send on the Enter that picked a row.
94
+
95
+ <Canvas of={ComposerTriggersStories.CodeAsABlock} />
96
+
97
+ ## What it owns
98
+
99
+ Nothing. It only draws. Clicks and keys are yours.
100
+
101
+ ## Keys
102
+
103
+ The lists are `SuggestionMenu`s, and their keys are its own. What the body
104
+ says, the tags it earns and sending stay the app's.
105
+
106
+
107
+ | Key | Does |
108
+ | --- | --- |
109
+ | `@` after a space or at the line's start | Opens the people list |
110
+ | `!@` | Opens the urgent list |
111
+ | `[` | Opens the reference list |
112
+ | `/` after a space or at the line's start | Opens Format and the app's sections |
113
+ | ↑ ↓ | Move the highlight, wrapping at the ends |
114
+ | Enter | Inserts the highlighted row, or runs it |
115
+ | Escape | Closes the list |
@@ -0,0 +1,99 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { IconSquareRounded } from '@tabler/icons-react'
3
+ import { EditorContent, useEditor } from '@tiptap/react'
4
+ import StarterKit from '@tiptap/starter-kit'
5
+ import { richTextClassName } from './RichText'
6
+ import {
7
+ CaptionedReference,
8
+ PersonMention,
9
+ ReferenceTrigger,
10
+ SlashCommands,
11
+ UrgentPersonMention,
12
+ formatSection,
13
+ typeTrigger,
14
+ type CaptionedItem,
15
+ type MentionPerson,
16
+ type ReferenceSection,
17
+ type SlashSection,
18
+ } from './ComposerTriggers'
19
+
20
+ /**
21
+ * The four composer triggers: `@` and `!@` offer a person, `[` offers
22
+ * something to reference, and `/` formats the line or inserts something.
23
+ * Type one of them in the box.
24
+ */
25
+ const meta = {
26
+ title: 'Primitives/ComposerTriggers',
27
+ parameters: { layout: 'padded' },
28
+ } satisfies Meta
29
+
30
+ export default meta
31
+ type Story = StoryObj
32
+
33
+ const PEOPLE: MentionPerson[] = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'].map((n, i) => ({
34
+ id: `person-${n}`,
35
+ name: `Person ${n}`,
36
+ description: i % 2 === 0 ? 'Label' : undefined,
37
+ pubkey: null,
38
+ }))
39
+
40
+ const ITEMS: CaptionedItem[] = ['one', 'two', 'three'].map((n) => ({
41
+ id: `item-${n}`,
42
+ label: 'Person one',
43
+ snippet: `Item ${n}`,
44
+ uri: `nostr:item-${n}`,
45
+ }))
46
+
47
+ const DESCRIBED: CaptionedItem[] = ['four', 'five'].map((n) => ({
48
+ id: `item-${n}`,
49
+ label: 'Label',
50
+ snippet: `Item ${n}`,
51
+ uri: `nostr:item-${n}`,
52
+ description: 'Label',
53
+ }))
54
+
55
+ const matching = (items: CaptionedItem[], query: string) => items.filter((item) => item.snippet.toLowerCase().includes(query.toLowerCase()))
56
+
57
+ const SECTIONS = (query: string): ReferenceSection[] => [
58
+ { label: 'Group one', items: matching(ITEMS, query) },
59
+ { label: 'Group two', items: matching(DESCRIBED, query) },
60
+ ]
61
+
62
+ const INSERT: SlashSection = {
63
+ label: 'Insert',
64
+ commands: [
65
+ { key: 'insert-@', row: { label: 'Mention', shortcut: '@' }, keywords: ['@'], run: typeTrigger('@') },
66
+ { key: 'insert-[', row: { label: 'Reference', shortcut: '[' }, keywords: ['['], run: typeTrigger('[') },
67
+ ],
68
+ }
69
+
70
+ function Composer({ code, sections }: { code: 'inline' | 'block'; sections?: typeof SECTIONS }) {
71
+ const editor = useEditor({
72
+ extensions: [
73
+ StarterKit,
74
+ PersonMention.configure({ people: () => PEOPLE }),
75
+ UrgentPersonMention.configure({ people: () => PEOPLE }),
76
+ CaptionedReference.configure({ icon: <IconSquareRounded size={14} stroke={1.5} /> }),
77
+ ReferenceTrigger.configure({
78
+ items: () => ITEMS,
79
+ sections,
80
+ ariaLabel: 'Items',
81
+ sectionLabel: 'Items',
82
+ icon: <IconSquareRounded size={16} stroke={1.5} className="text-text-secondary" />,
83
+ }),
84
+ SlashCommands.configure({ sections: [formatSection(code), INSERT] }),
85
+ ],
86
+ content: '<p>Type @, !@, [ or / after a space: </p>',
87
+ editorProps: { attributes: { class: richTextClassName('default', 'min-h-[80px] w-[480px] rounded-md border border-border-default p-3 outline-none') } },
88
+ })
89
+ return <EditorContent editor={editor} />
90
+ }
91
+
92
+ /** Code as a mark inside the line. */
93
+ export const Default: Story = { render: () => <Composer code="inline" /> }
94
+
95
+ /** Code as a block of its own, for an editor of a block document. */
96
+ export const CodeAsABlock: Story = { render: () => <Composer code="block" /> }
97
+
98
+ /** `[` with the app's own groups, each headed, a row with a second line. */
99
+ export const ReferencesInSections: Story = { render: () => <Composer code="inline" sections={SECTIONS} /> }