@estiva-app/ui 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BlockNodes.d.ts +12 -1
- package/dist/BlockNodes.d.ts.map +1 -1
- package/dist/LinkChips.d.ts +82 -0
- package/dist/LinkChips.d.ts.map +1 -0
- package/dist/ReferenceCards.d.ts +77 -0
- package/dist/ReferenceCards.d.ts.map +1 -0
- package/dist/ReferenceMenu.d.ts +18 -0
- package/dist/ReferenceMenu.d.ts.map +1 -0
- package/dist/RichText.d.ts +9 -2
- package/dist/RichText.d.ts.map +1 -1
- package/dist/SelectionToolbar.d.ts +9 -1
- package/dist/SelectionToolbar.d.ts.map +1 -1
- package/dist/editor.d.ts +3 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +589 -20
- package/dist/editor.js.map +4 -4
- package/dist/index.js +39 -19
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/registry.json +290 -3
- package/src/BlockNodes.test.tsx +2 -1
- package/src/BlockNodes.tsx +51 -9
- package/src/LinkChips.mdx +57 -0
- package/src/LinkChips.stories.tsx +67 -0
- package/src/LinkChips.test.tsx +178 -0
- package/src/LinkChips.ts +243 -0
- package/src/ReferenceCards.mdx +79 -0
- package/src/ReferenceCards.stories.tsx +64 -0
- package/src/ReferenceCards.test.tsx +143 -0
- package/src/ReferenceCards.ts +225 -0
- package/src/ReferenceMenu.mdx +75 -0
- package/src/ReferenceMenu.stories.tsx +59 -0
- package/src/ReferenceMenu.test.tsx +289 -0
- package/src/ReferenceMenu.tsx +346 -0
- package/src/RichText.test.tsx +17 -0
- package/src/RichText.tsx +34 -3
- package/src/SelectionToolbar.tsx +10 -2
- package/src/editor.ts +13 -0
- package/src/pages.test.ts +3 -0
- package/src/registry/registry.test.ts +6 -6
- package/stories/Choosing.mdx +3 -0
package/package.json
CHANGED
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.
|
|
8
|
-
"exports":
|
|
9
|
-
"typeExports":
|
|
7
|
+
"packageVersion": "0.68.0",
|
|
8
|
+
"exports": 162,
|
|
9
|
+
"typeExports": 152,
|
|
10
10
|
"files": null
|
|
11
11
|
},
|
|
12
12
|
"storybook": {
|
|
@@ -2729,6 +2729,25 @@
|
|
|
2729
2729
|
"app": null,
|
|
2730
2730
|
"looks": []
|
|
2731
2731
|
},
|
|
2732
|
+
{
|
|
2733
|
+
"name": "findLinkChips",
|
|
2734
|
+
"repo": "estiva-ui",
|
|
2735
|
+
"kind": "helper",
|
|
2736
|
+
"importPath": "@estiva-app/ui/editor",
|
|
2737
|
+
"sourceFile": "src/LinkChips.ts",
|
|
2738
|
+
"purpose": "What to change in a document, and what to ask.",
|
|
2739
|
+
"purposeFrom": "comment",
|
|
2740
|
+
"props": [],
|
|
2741
|
+
"variants": [],
|
|
2742
|
+
"ownsBehaviours": [],
|
|
2743
|
+
"status": "stable",
|
|
2744
|
+
"migrationStage": null,
|
|
2745
|
+
"docsId": "primitives-linkchips--docs",
|
|
2746
|
+
"storyId": "primitives-linkchips--in-an-editor",
|
|
2747
|
+
"docPage": null,
|
|
2748
|
+
"app": null,
|
|
2749
|
+
"looks": []
|
|
2750
|
+
},
|
|
2732
2751
|
{
|
|
2733
2752
|
"name": "Form",
|
|
2734
2753
|
"repo": "estiva-ui",
|
|
@@ -3337,6 +3356,25 @@
|
|
|
3337
3356
|
"app": null,
|
|
3338
3357
|
"looks": []
|
|
3339
3358
|
},
|
|
3359
|
+
{
|
|
3360
|
+
"name": "isReferenceMenuOpen",
|
|
3361
|
+
"repo": "estiva-ui",
|
|
3362
|
+
"kind": "helper",
|
|
3363
|
+
"importPath": "@estiva-app/ui/editor",
|
|
3364
|
+
"sourceFile": "src/ReferenceMenu.tsx",
|
|
3365
|
+
"purpose": "Whether the menu is open, for an editor's own Escape: while it is, Escape closes it rather than leaving.",
|
|
3366
|
+
"purposeFrom": "comment",
|
|
3367
|
+
"props": [],
|
|
3368
|
+
"variants": [],
|
|
3369
|
+
"ownsBehaviours": [],
|
|
3370
|
+
"status": "stable",
|
|
3371
|
+
"migrationStage": null,
|
|
3372
|
+
"docsId": "primitives-referencemenu--docs",
|
|
3373
|
+
"storyId": "primitives-referencemenu--in-an-editor",
|
|
3374
|
+
"docPage": null,
|
|
3375
|
+
"app": null,
|
|
3376
|
+
"looks": []
|
|
3377
|
+
},
|
|
3340
3378
|
{
|
|
3341
3379
|
"name": "isSuggestionActive",
|
|
3342
3380
|
"repo": "estiva-ui",
|
|
@@ -3667,6 +3705,25 @@
|
|
|
3667
3705
|
"underline underline-offset-2 hover:text-text-primary truncate after:rounded-md focus-visible:outline-none"
|
|
3668
3706
|
]
|
|
3669
3707
|
},
|
|
3708
|
+
{
|
|
3709
|
+
"name": "LinkChips",
|
|
3710
|
+
"repo": "estiva-ui",
|
|
3711
|
+
"kind": "component",
|
|
3712
|
+
"importPath": "@estiva-app/ui/editor",
|
|
3713
|
+
"sourceFile": "src/LinkChips.ts",
|
|
3714
|
+
"purpose": "A URL in a document that the app recognises reads as a chip of what it points at, and stays that URL when the document is saved.",
|
|
3715
|
+
"purposeFrom": "page",
|
|
3716
|
+
"props": [],
|
|
3717
|
+
"variants": [],
|
|
3718
|
+
"ownsBehaviours": [],
|
|
3719
|
+
"status": "stable",
|
|
3720
|
+
"migrationStage": null,
|
|
3721
|
+
"docsId": "primitives-linkchips--docs",
|
|
3722
|
+
"storyId": "primitives-linkchips--in-an-editor",
|
|
3723
|
+
"docPage": "src/LinkChips.mdx",
|
|
3724
|
+
"app": null,
|
|
3725
|
+
"looks": []
|
|
3726
|
+
},
|
|
3670
3727
|
{
|
|
3671
3728
|
"name": "ListColumn",
|
|
3672
3729
|
"repo": "estiva-ui",
|
|
@@ -5485,6 +5542,63 @@
|
|
|
5485
5542
|
"app": null,
|
|
5486
5543
|
"looks": []
|
|
5487
5544
|
},
|
|
5545
|
+
{
|
|
5546
|
+
"name": "referenceAtSelection",
|
|
5547
|
+
"repo": "estiva-ui",
|
|
5548
|
+
"kind": "helper",
|
|
5549
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5550
|
+
"sourceFile": "src/ReferenceCards.ts",
|
|
5551
|
+
"purpose": "The chip the person has selected, with what it names and its card — or null.",
|
|
5552
|
+
"purposeFrom": "comment",
|
|
5553
|
+
"props": [],
|
|
5554
|
+
"variants": [],
|
|
5555
|
+
"ownsBehaviours": [],
|
|
5556
|
+
"status": "stable",
|
|
5557
|
+
"migrationStage": null,
|
|
5558
|
+
"docsId": "primitives-referencecards--docs",
|
|
5559
|
+
"storyId": "primitives-referencecards--in-an-editor",
|
|
5560
|
+
"docPage": null,
|
|
5561
|
+
"app": null,
|
|
5562
|
+
"looks": []
|
|
5563
|
+
},
|
|
5564
|
+
{
|
|
5565
|
+
"name": "referenceCardPlacements",
|
|
5566
|
+
"repo": "estiva-ui",
|
|
5567
|
+
"kind": "helper",
|
|
5568
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5569
|
+
"sourceFile": "src/ReferenceCards.ts",
|
|
5570
|
+
"purpose": "Every line that asks for a card, in document order.",
|
|
5571
|
+
"purposeFrom": "comment",
|
|
5572
|
+
"props": [],
|
|
5573
|
+
"variants": [],
|
|
5574
|
+
"ownsBehaviours": [],
|
|
5575
|
+
"status": "stable",
|
|
5576
|
+
"migrationStage": null,
|
|
5577
|
+
"docsId": "primitives-referencecards--docs",
|
|
5578
|
+
"storyId": "primitives-referencecards--in-an-editor",
|
|
5579
|
+
"docPage": null,
|
|
5580
|
+
"app": null,
|
|
5581
|
+
"looks": []
|
|
5582
|
+
},
|
|
5583
|
+
{
|
|
5584
|
+
"name": "ReferenceCards",
|
|
5585
|
+
"repo": "estiva-ui",
|
|
5586
|
+
"kind": "component",
|
|
5587
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5588
|
+
"sourceFile": "src/ReferenceCards.ts",
|
|
5589
|
+
"purpose": "A chip in a document can ask for a card after its line: compact, detailed or none.",
|
|
5590
|
+
"purposeFrom": "page",
|
|
5591
|
+
"props": [],
|
|
5592
|
+
"variants": [],
|
|
5593
|
+
"ownsBehaviours": [],
|
|
5594
|
+
"status": "stable",
|
|
5595
|
+
"migrationStage": null,
|
|
5596
|
+
"docsId": "primitives-referencecards--docs",
|
|
5597
|
+
"storyId": "primitives-referencecards--in-an-editor",
|
|
5598
|
+
"docPage": "src/ReferenceCards.mdx",
|
|
5599
|
+
"app": null,
|
|
5600
|
+
"looks": []
|
|
5601
|
+
},
|
|
5488
5602
|
{
|
|
5489
5603
|
"name": "referenceMenu",
|
|
5490
5604
|
"repo": "estiva-ui",
|
|
@@ -5529,6 +5643,91 @@
|
|
|
5529
5643
|
"app": null,
|
|
5530
5644
|
"looks": []
|
|
5531
5645
|
},
|
|
5646
|
+
{
|
|
5647
|
+
"name": "ReferenceMenu",
|
|
5648
|
+
"repo": "estiva-ui",
|
|
5649
|
+
"kind": "component",
|
|
5650
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5651
|
+
"sourceFile": "src/ReferenceMenu.tsx",
|
|
5652
|
+
"purpose": "What a chip in a document offers whoever is writing it: Open, and its Card — none, compact or detailed.",
|
|
5653
|
+
"purposeFrom": "page",
|
|
5654
|
+
"props": [],
|
|
5655
|
+
"variants": [],
|
|
5656
|
+
"ownsBehaviours": [],
|
|
5657
|
+
"status": "stable",
|
|
5658
|
+
"migrationStage": null,
|
|
5659
|
+
"docsId": "primitives-referencemenu--docs",
|
|
5660
|
+
"storyId": "primitives-referencemenu--in-an-editor",
|
|
5661
|
+
"docPage": "src/ReferenceMenu.mdx",
|
|
5662
|
+
"app": null,
|
|
5663
|
+
"looks": []
|
|
5664
|
+
},
|
|
5665
|
+
{
|
|
5666
|
+
"name": "referenceMenuClick",
|
|
5667
|
+
"repo": "estiva-ui",
|
|
5668
|
+
"kind": "helper",
|
|
5669
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5670
|
+
"sourceFile": "src/ReferenceMenu.tsx",
|
|
5671
|
+
"purpose": "A click on the chip at `pos`, from its node view: while editing, with the menu installed, it opens the menu, and the caller stops the click there.",
|
|
5672
|
+
"purposeFrom": "comment",
|
|
5673
|
+
"props": [],
|
|
5674
|
+
"variants": [],
|
|
5675
|
+
"ownsBehaviours": [],
|
|
5676
|
+
"status": "stable",
|
|
5677
|
+
"migrationStage": null,
|
|
5678
|
+
"docsId": "primitives-referencemenu--docs",
|
|
5679
|
+
"storyId": "primitives-referencemenu--in-an-editor",
|
|
5680
|
+
"docPage": null,
|
|
5681
|
+
"app": null,
|
|
5682
|
+
"looks": []
|
|
5683
|
+
},
|
|
5684
|
+
{
|
|
5685
|
+
"name": "ReferenceMenuView",
|
|
5686
|
+
"repo": "estiva-ui",
|
|
5687
|
+
"kind": "component",
|
|
5688
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5689
|
+
"sourceFile": "src/ReferenceMenu.tsx",
|
|
5690
|
+
"purpose": "The menu `ReferenceMenu` holds, hung under its chip.",
|
|
5691
|
+
"purposeFrom": "comment",
|
|
5692
|
+
"props": [
|
|
5693
|
+
{
|
|
5694
|
+
"name": "editor",
|
|
5695
|
+
"takes": "Editor | null",
|
|
5696
|
+
"required": true,
|
|
5697
|
+
"note": null
|
|
5698
|
+
}
|
|
5699
|
+
],
|
|
5700
|
+
"variants": [],
|
|
5701
|
+
"ownsBehaviours": [
|
|
5702
|
+
{
|
|
5703
|
+
"id": "portal",
|
|
5704
|
+
"behaviour": "Floats on top of the page"
|
|
5705
|
+
},
|
|
5706
|
+
{
|
|
5707
|
+
"id": "press-outside",
|
|
5708
|
+
"behaviour": "Closes on a press outside"
|
|
5709
|
+
},
|
|
5710
|
+
{
|
|
5711
|
+
"id": "page-keys",
|
|
5712
|
+
"behaviour": "Takes its keys by itself"
|
|
5713
|
+
},
|
|
5714
|
+
{
|
|
5715
|
+
"id": "focus",
|
|
5716
|
+
"behaviour": "Holds focus inside while open, and gives it back"
|
|
5717
|
+
},
|
|
5718
|
+
{
|
|
5719
|
+
"id": "follow",
|
|
5720
|
+
"behaviour": "Stays attached to its anchor on scroll and resize"
|
|
5721
|
+
}
|
|
5722
|
+
],
|
|
5723
|
+
"status": "stable",
|
|
5724
|
+
"migrationStage": null,
|
|
5725
|
+
"docsId": "primitives-referencemenu--docs",
|
|
5726
|
+
"storyId": "primitives-referencemenu--in-an-editor",
|
|
5727
|
+
"docPage": null,
|
|
5728
|
+
"app": null,
|
|
5729
|
+
"looks": []
|
|
5730
|
+
},
|
|
5532
5731
|
{
|
|
5533
5732
|
"name": "ReferenceNode",
|
|
5534
5733
|
"repo": "estiva-ui",
|
|
@@ -5567,6 +5766,25 @@
|
|
|
5567
5766
|
"app": null,
|
|
5568
5767
|
"looks": []
|
|
5569
5768
|
},
|
|
5769
|
+
{
|
|
5770
|
+
"name": "refreshLinkChips",
|
|
5771
|
+
"repo": "estiva-ui",
|
|
5772
|
+
"kind": "helper",
|
|
5773
|
+
"importPath": "@estiva-app/ui/editor",
|
|
5774
|
+
"sourceFile": "src/LinkChips.ts",
|
|
5775
|
+
"purpose": "Look at the document again — after the app learned what more URLs name some other way.",
|
|
5776
|
+
"purposeFrom": "comment",
|
|
5777
|
+
"props": [],
|
|
5778
|
+
"variants": [],
|
|
5779
|
+
"ownsBehaviours": [],
|
|
5780
|
+
"status": "stable",
|
|
5781
|
+
"migrationStage": null,
|
|
5782
|
+
"docsId": "primitives-linkchips--docs",
|
|
5783
|
+
"storyId": "primitives-linkchips--in-an-editor",
|
|
5784
|
+
"docPage": null,
|
|
5785
|
+
"app": null,
|
|
5786
|
+
"looks": []
|
|
5787
|
+
},
|
|
5570
5788
|
{
|
|
5571
5789
|
"name": "RichText",
|
|
5572
5790
|
"repo": "estiva-ui",
|
|
@@ -5606,6 +5824,12 @@
|
|
|
5606
5824
|
"required": false,
|
|
5607
5825
|
"note": "Draws a block this part has no design for (an attachment, a widget)."
|
|
5608
5826
|
},
|
|
5827
|
+
{
|
|
5828
|
+
"name": "renderAfter",
|
|
5829
|
+
"takes": "a handler",
|
|
5830
|
+
"required": false,
|
|
5831
|
+
"note": "Draws something after a line of text: after a paragraph, a heading or a quote, and inside a list item or a table cell after its words — where a document's chip asks for its card (SPEC §13.3)."
|
|
5832
|
+
},
|
|
5609
5833
|
{
|
|
5610
5834
|
"name": "onTick",
|
|
5611
5835
|
"takes": "a handler",
|
|
@@ -6154,6 +6378,12 @@
|
|
|
6154
6378
|
"takes": "true/false",
|
|
6155
6379
|
"required": false,
|
|
6156
6380
|
"note": "The Link button and its address field. Needs a link mark in the editor and `KeptSelection`."
|
|
6381
|
+
},
|
|
6382
|
+
{
|
|
6383
|
+
"name": "children",
|
|
6384
|
+
"takes": "anything",
|
|
6385
|
+
"required": false,
|
|
6386
|
+
"note": "More of the strip, after the marks: the app's own `ToolbarButton`s for what is selected — a chip's card, say."
|
|
6157
6387
|
}
|
|
6158
6388
|
],
|
|
6159
6389
|
"variants": [],
|
|
@@ -6187,6 +6417,44 @@
|
|
|
6187
6417
|
"app": null,
|
|
6188
6418
|
"looks": []
|
|
6189
6419
|
},
|
|
6420
|
+
{
|
|
6421
|
+
"name": "setReferenceCard",
|
|
6422
|
+
"repo": "estiva-ui",
|
|
6423
|
+
"kind": "helper",
|
|
6424
|
+
"importPath": "@estiva-app/ui/editor",
|
|
6425
|
+
"sourceFile": "src/ReferenceCards.ts",
|
|
6426
|
+
"purpose": "Sets the card of the selected chip. `none` removes it.",
|
|
6427
|
+
"purposeFrom": "comment",
|
|
6428
|
+
"props": [],
|
|
6429
|
+
"variants": [],
|
|
6430
|
+
"ownsBehaviours": [],
|
|
6431
|
+
"status": "stable",
|
|
6432
|
+
"migrationStage": null,
|
|
6433
|
+
"docsId": "primitives-referencecards--docs",
|
|
6434
|
+
"storyId": "primitives-referencecards--in-an-editor",
|
|
6435
|
+
"docPage": null,
|
|
6436
|
+
"app": null,
|
|
6437
|
+
"looks": []
|
|
6438
|
+
},
|
|
6439
|
+
{
|
|
6440
|
+
"name": "setReferenceCardAt",
|
|
6441
|
+
"repo": "estiva-ui",
|
|
6442
|
+
"kind": "helper",
|
|
6443
|
+
"importPath": "@estiva-app/ui/editor",
|
|
6444
|
+
"sourceFile": "src/ReferenceCards.ts",
|
|
6445
|
+
"purpose": "Sets the card of the chip at `pos` — one that names something.",
|
|
6446
|
+
"purposeFrom": "comment",
|
|
6447
|
+
"props": [],
|
|
6448
|
+
"variants": [],
|
|
6449
|
+
"ownsBehaviours": [],
|
|
6450
|
+
"status": "stable",
|
|
6451
|
+
"migrationStage": null,
|
|
6452
|
+
"docsId": "primitives-referencecards--docs",
|
|
6453
|
+
"storyId": "primitives-referencecards--in-an-editor",
|
|
6454
|
+
"docPage": null,
|
|
6455
|
+
"app": null,
|
|
6456
|
+
"looks": []
|
|
6457
|
+
},
|
|
6190
6458
|
{
|
|
6191
6459
|
"name": "shortcutLabel",
|
|
6192
6460
|
"repo": "estiva-ui",
|
|
@@ -7608,6 +7876,25 @@
|
|
|
7608
7876
|
"app": null,
|
|
7609
7877
|
"looks": []
|
|
7610
7878
|
},
|
|
7879
|
+
{
|
|
7880
|
+
"name": "useReferenceCards",
|
|
7881
|
+
"repo": "estiva-ui",
|
|
7882
|
+
"kind": "hook",
|
|
7883
|
+
"importPath": "@estiva-app/ui/editor",
|
|
7884
|
+
"sourceFile": "src/ReferenceCards.ts",
|
|
7885
|
+
"purpose": "The slots to draw cards into, kept current as the document changes.",
|
|
7886
|
+
"purposeFrom": "comment",
|
|
7887
|
+
"props": [],
|
|
7888
|
+
"variants": [],
|
|
7889
|
+
"ownsBehaviours": [],
|
|
7890
|
+
"status": "stable",
|
|
7891
|
+
"migrationStage": null,
|
|
7892
|
+
"docsId": "primitives-referencecards--docs",
|
|
7893
|
+
"storyId": "primitives-referencecards--in-an-editor",
|
|
7894
|
+
"docPage": null,
|
|
7895
|
+
"app": null,
|
|
7896
|
+
"looks": []
|
|
7897
|
+
},
|
|
7611
7898
|
{
|
|
7612
7899
|
"name": "useToast",
|
|
7613
7900
|
"repo": "estiva-ui",
|
package/src/BlockNodes.test.tsx
CHANGED
|
@@ -79,7 +79,8 @@ describe('what the editor hands back', () => {
|
|
|
79
79
|
it('keeps a reference an atom, with its uri and the marks it carries', () => {
|
|
80
80
|
editor = new Editor({ extensions: EXTENSIONS, content: DOCUMENT })
|
|
81
81
|
const reference = (editor.getJSON() as Json).content?.[1].content?.[1]
|
|
82
|
-
|
|
82
|
+
// No card chosen, no key a later version added (SPEC §13.3), and not a URL chip.
|
|
83
|
+
expect(reference).toEqual({ type: 'reference', attrs: { uri: 'nostr:naddr1example', marks: ['bold'], embed: null, extra: null, href: null } })
|
|
83
84
|
})
|
|
84
85
|
|
|
85
86
|
it('keeps every imeta field on a file, which ProseMirror drops when undeclared', () => {
|
package/src/BlockNodes.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentType } from 'react'
|
|
1
|
+
import type { ComponentType, MouseEvent } from 'react'
|
|
2
2
|
import { Extension, InputRule, Node, type Editor } from '@tiptap/core'
|
|
3
3
|
import { DOMSerializer, type Node as ProseMirrorNode, type Slice } from '@tiptap/pm/model'
|
|
4
4
|
import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/pm/state'
|
|
@@ -7,6 +7,7 @@ import type { EditorView, NodeView } from '@tiptap/pm/view'
|
|
|
7
7
|
import { NodeViewWrapper, ReactNodeViewRenderer, ReactRenderer, type ReactNodeViewProps } from '@tiptap/react'
|
|
8
8
|
import { Checkbox, type CheckboxProps } from './Checkbox'
|
|
9
9
|
import { calloutLook } from './RichText'
|
|
10
|
+
import { referenceMenuClick } from './ReferenceMenu'
|
|
10
11
|
|
|
11
12
|
/*
|
|
12
13
|
* The four nodes an editor of a block document needs that a stock schema does
|
|
@@ -364,9 +365,13 @@ export const BlockId = Extension.create({
|
|
|
364
365
|
},
|
|
365
366
|
})
|
|
366
367
|
|
|
367
|
-
/**
|
|
368
|
+
/** An http(s) URL, or null. */
|
|
369
|
+
const webUrl = (raw: string | null): string | null => (raw && /^https?:\/\/\S+$/i.test(raw) ? raw : null)
|
|
370
|
+
|
|
371
|
+
/** What a reference's view is handed: the `nostr:` URI it stands for, and the URL a URL chip was written as. */
|
|
368
372
|
export interface ReferenceViewProps {
|
|
369
373
|
uri: string
|
|
374
|
+
href?: string
|
|
370
375
|
}
|
|
371
376
|
|
|
372
377
|
export interface ReferenceNodeOptions {
|
|
@@ -384,6 +389,16 @@ export interface ReferenceNodeOptions {
|
|
|
384
389
|
* nobody edits half a `naddr`. Selecting it selects the whole reference, which
|
|
385
390
|
* is also how deleting one works. `marks` carries the run's other marks, so a
|
|
386
391
|
* bold reference comes back bold.
|
|
392
|
+
*
|
|
393
|
+
* `embed` is the card its writer chose to draw after its block — `compact`,
|
|
394
|
+
* `detailed`, or null for none (SPEC §13.3, "A reference's card") — and `extra`
|
|
395
|
+
* every key of the mark this version does not know. Both are declared so that
|
|
396
|
+
* ProseMirror keeps them: an undeclared attribute is dropped when the node is
|
|
397
|
+
* made, and the card with it. `ReferenceCards` draws the cards.
|
|
398
|
+
*
|
|
399
|
+
* `href` makes it a **URL chip**: an app URL that reads as the chip of what it
|
|
400
|
+
* names, and is that URL on the wire (§13.3). `uri` is what it names, filled in
|
|
401
|
+
* by `LinkChips` once the app has resolved it; until then it draws as the URL.
|
|
387
402
|
*/
|
|
388
403
|
export const ReferenceNode = Node.create<ReferenceNodeOptions>({
|
|
389
404
|
name: 'reference',
|
|
@@ -397,8 +412,14 @@ export const ReferenceNode = Node.create<ReferenceNodeOptions>({
|
|
|
397
412
|
},
|
|
398
413
|
addAttributes() {
|
|
399
414
|
return {
|
|
400
|
-
|
|
415
|
+
// A pasted URL chip keeps only its URL: what it names is the editor's to resolve (`LinkChips`), never
|
|
416
|
+
// the clipboard's, which could show one object while saving another URL (security review).
|
|
417
|
+
uri: { default: '', parseHTML: (element: HTMLElement) => (webUrl(element.getAttribute('data-href')) ? '' : (element.getAttribute('data-reference') ?? '')) },
|
|
401
418
|
marks: { default: [] as string[] },
|
|
419
|
+
embed: { default: null as string | null, parseHTML: (element: HTMLElement) => element.getAttribute('data-embed') },
|
|
420
|
+
extra: { default: null as Record<string, unknown> | null, parseHTML: () => null },
|
|
421
|
+
// An http(s) URL only: `LinkChips` makes no other, and a pasted `javascript:` must not reach a saved link.
|
|
422
|
+
href: { default: null as string | null, parseHTML: (element: HTMLElement) => webUrl(element.getAttribute('data-href')) },
|
|
402
423
|
}
|
|
403
424
|
},
|
|
404
425
|
parseHTML() {
|
|
@@ -406,16 +427,37 @@ export const ReferenceNode = Node.create<ReferenceNodeOptions>({
|
|
|
406
427
|
},
|
|
407
428
|
renderHTML({ node }) {
|
|
408
429
|
// A copy to the clipboard as HTML, and the whole drawing when no view is given.
|
|
409
|
-
|
|
430
|
+
// The card goes along, so a paste keeps it; a URL chip copies as its URL.
|
|
431
|
+
const embed = typeof node.attrs.embed === 'string' ? { 'data-embed': node.attrs.embed } : {}
|
|
432
|
+
const href = typeof node.attrs.href === 'string' && node.attrs.href ? { 'data-href': node.attrs.href } : {}
|
|
433
|
+
return ['span', { 'data-reference': String(node.attrs.uri), ...embed, ...href }, String(node.attrs.href || node.attrs.uri)]
|
|
434
|
+
},
|
|
435
|
+
renderText({ node }) {
|
|
436
|
+
return String(node.attrs.href || node.attrs.uri)
|
|
410
437
|
},
|
|
411
438
|
addNodeView() {
|
|
412
439
|
const View = this.options.view
|
|
413
440
|
if (!View) return null
|
|
414
|
-
return ReactNodeViewRenderer(({ node }: ReactNodeViewProps) =>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
441
|
+
return ReactNodeViewRenderer(({ node, editor, getPos }: ReactNodeViewProps) => {
|
|
442
|
+
const uri = String(node.attrs.uri ?? '')
|
|
443
|
+
const href = typeof node.attrs.href === 'string' && node.attrs.href ? node.attrs.href : undefined
|
|
444
|
+
return (
|
|
445
|
+
<NodeViewWrapper
|
|
446
|
+
as="span"
|
|
447
|
+
className="inline"
|
|
448
|
+
// While editing, a click opens the chip's menu (`ReferenceMenu`) rather than following the chip.
|
|
449
|
+
onClickCapture={(event: MouseEvent) => {
|
|
450
|
+
const pos = getPos()
|
|
451
|
+
if (typeof pos !== 'number' || !referenceMenuClick(editor, pos)) return
|
|
452
|
+
event.preventDefault()
|
|
453
|
+
event.stopPropagation()
|
|
454
|
+
}}
|
|
455
|
+
>
|
|
456
|
+
{/* A URL chip not resolved yet reads as the URL it was written as. */}
|
|
457
|
+
{uri ? <View uri={uri} {...(href ? { href } : {})} /> : <span>{href}</span>}
|
|
458
|
+
</NodeViewWrapper>
|
|
459
|
+
)
|
|
460
|
+
})
|
|
419
461
|
},
|
|
420
462
|
})
|
|
421
463
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Meta, Canvas } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as LinkChipsStories from './LinkChips.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={LinkChipsStories} />
|
|
5
|
+
|
|
6
|
+
# LinkChips
|
|
7
|
+
|
|
8
|
+
A URL in a document that the app recognises reads as a chip of what it points
|
|
9
|
+
at, and stays that URL when the document is saved. A URL the app does not
|
|
10
|
+
recognise, or has not answered for yet, stays text.
|
|
11
|
+
|
|
12
|
+
<Canvas of={LinkChipsStories.InAnEditor} />
|
|
13
|
+
|
|
14
|
+
## When
|
|
15
|
+
|
|
16
|
+
- **An editor of a document whose URLs can name things**: pair it with
|
|
17
|
+
**ReferenceNode**, which draws the chip, and **ReferenceCards**, which places
|
|
18
|
+
the card a chip can ask for.
|
|
19
|
+
|
|
20
|
+
## When not
|
|
21
|
+
|
|
22
|
+
- A document that is only read → **RichText**, whose text already says which
|
|
23
|
+
runs are chips.
|
|
24
|
+
- An editor with a link mark (a composer's Link button) → its links are the
|
|
25
|
+
author's words, not chips. Turn the link mark off in a document editor
|
|
26
|
+
(`StarterKit.configure({ link: false })`), or URLs are linked before this
|
|
27
|
+
sees them.
|
|
28
|
+
|
|
29
|
+
## How
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { LinkChips, ReferenceCards, ReferenceNode } from '@estiva-app/ui/editor'
|
|
33
|
+
|
|
34
|
+
LinkChips.configure({
|
|
35
|
+
// What the app has resolved so far: the reference a URL names, or undefined.
|
|
36
|
+
referenceOf: (url) => known(url),
|
|
37
|
+
// Look up the rest. When it settles, the document is looked at again.
|
|
38
|
+
ask: (urls) => lookUp(urls),
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- A chip made from a URL is a `reference` node with `href` set to the URL;
|
|
43
|
+
`uri` is what it names. A document saves it as the URL.
|
|
44
|
+
- A chip read from a document before its URL is answered draws as the URL, and
|
|
45
|
+
offers no card choice until it is answered.
|
|
46
|
+
- The URL the caret is in, or at the end of, stays text: a half-typed address
|
|
47
|
+
never turns into a chip while it is being typed. A pasted URL is whole: it
|
|
48
|
+
is asked about and becomes its chip at once, with the caret at its end, so
|
|
49
|
+
**ReferenceMenu** can open its menu.
|
|
50
|
+
- A URL in code, in a code block, or under a link mark stays text.
|
|
51
|
+
- Each URL is asked about once, and one editor asks about at most `limit`
|
|
52
|
+
(24 by default), the first ones: an answer can cost the app a query.
|
|
53
|
+
- Making a chip is not an undo step: undo takes back what was typed.
|
|
54
|
+
|
|
55
|
+
## What it owns
|
|
56
|
+
|
|
57
|
+
Nothing. It draws nothing; it turns the URLs the app recognises into chips.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { EditorContent, useEditor } from '@tiptap/react'
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
4
|
+
import { BlockId, ReferenceNode, type ReferenceViewProps } from './BlockNodes'
|
|
5
|
+
import { Card } from './Card'
|
|
6
|
+
import { InlineChip } from './InlineChip'
|
|
7
|
+
import { LinkChips } from './LinkChips'
|
|
8
|
+
import { ReferenceCards } from './ReferenceCards'
|
|
9
|
+
import { richTextClassName } from './RichText'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A URL the app recognises reads as a chip; one it does not stays text. Here
|
|
13
|
+
* the app is a stand-in that recognises one address, after a short wait, and
|
|
14
|
+
* the chip is a placeholder naming what it points at: the real one is the
|
|
15
|
+
* app's, which knows how to resolve it.
|
|
16
|
+
*/
|
|
17
|
+
const meta = {
|
|
18
|
+
title: 'Primitives/LinkChips',
|
|
19
|
+
parameters: { layout: 'padded' },
|
|
20
|
+
} satisfies Meta
|
|
21
|
+
|
|
22
|
+
export default meta
|
|
23
|
+
type Story = StoryObj
|
|
24
|
+
|
|
25
|
+
const KNOWN = 'https://example.com/item/one'
|
|
26
|
+
const answered = new Map<string, string>()
|
|
27
|
+
|
|
28
|
+
function Placeholder({ uri }: ReferenceViewProps) {
|
|
29
|
+
return <InlineChip>Item for {uri}</InlineChip>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Box() {
|
|
33
|
+
const editor = useEditor({
|
|
34
|
+
extensions: [
|
|
35
|
+
StarterKit.configure({ link: false }),
|
|
36
|
+
BlockId,
|
|
37
|
+
ReferenceNode.configure({ view: Placeholder }),
|
|
38
|
+
ReferenceCards,
|
|
39
|
+
LinkChips.configure({
|
|
40
|
+
referenceOf: (url) => answered.get(url),
|
|
41
|
+
ask: (urls) =>
|
|
42
|
+
new Promise<void>((resolve) =>
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
if (urls.includes(KNOWN)) answered.set(KNOWN, 'nostr:naddr1one')
|
|
45
|
+
resolve()
|
|
46
|
+
}, 600),
|
|
47
|
+
),
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
content: {
|
|
51
|
+
type: 'doc',
|
|
52
|
+
content: [
|
|
53
|
+
{ type: 'paragraph', attrs: { blockId: 'p1' }, content: [{ type: 'text', text: `Recognised: ${KNOWN}` }] },
|
|
54
|
+
{ type: 'paragraph', attrs: { blockId: 'p2' }, content: [{ type: 'text', text: 'Not recognised: https://example.com/elsewhere' }] },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
editorProps: { attributes: { class: richTextClassName('default', 'outline-none'), 'aria-label': 'Text' } },
|
|
58
|
+
})
|
|
59
|
+
return (
|
|
60
|
+
<Card fill="surface" className="flex w-[520px] flex-col gap-2 p-3">
|
|
61
|
+
<EditorContent editor={editor} />
|
|
62
|
+
</Card>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Two URLs: the first turns into its chip once the stand-in answers, the second stays text. */
|
|
67
|
+
export const InAnEditor: Story = { render: () => <Box /> }
|