tref-block 0.1.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/LICENSE +33 -0
- package/README.md +162 -0
- package/dist/tref-block.cjs +174 -0
- package/dist/tref-block.js +175 -0
- package/dist/tref-block.js.map +7 -0
- package/package.json +67 -0
- package/src/wrapper/wrapper.js +606 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
TREF - Traceable Reference Blocks
|
|
2
|
+
Copyright (C) 2026 lpm
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
-------------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
SCOPE CLARIFICATION:
|
|
20
|
+
|
|
21
|
+
This AGPL-3.0 license applies to the SOFTWARE CODE only:
|
|
22
|
+
- CLI tools (tref, tref-mcp)
|
|
23
|
+
- Publisher library
|
|
24
|
+
- MCP server
|
|
25
|
+
- All source code in this repository
|
|
26
|
+
|
|
27
|
+
This license does NOT apply to:
|
|
28
|
+
- The TREF format specification (doc/format.md) - public domain
|
|
29
|
+
- TREF blocks created by users - user's choice of license
|
|
30
|
+
- Content within blocks - governed by TREF-1.0 or user's choice
|
|
31
|
+
|
|
32
|
+
The TREF format is FREE and OPEN. Anyone can implement it.
|
|
33
|
+
The code in this repository must remain open source.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# TREF
|
|
2
|
+
|
|
3
|
+
**Traceable Reference Format**
|
|
4
|
+
|
|
5
|
+
A JSON-based format for knowledge exchange where references are structural, not optional.
|
|
6
|
+
|
|
7
|
+
## What is TREF?
|
|
8
|
+
|
|
9
|
+
TREF is a **knowledge exchange format** – not a CMS, not an AI model, not a platform. It's a JSON format for creating self-contained units of knowledge that preserve:
|
|
10
|
+
|
|
11
|
+
- **Content** – the actual information
|
|
12
|
+
- **Metadata** – author, timestamps, source
|
|
13
|
+
- **License** – attribution requirements
|
|
14
|
+
- **References** – URLs, archived snippets, search prompts, hashes
|
|
15
|
+
- **Lineage** – parent/child relationships, version history
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install tref-block
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or use CDN:
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import { TrefWrapper } from 'https://cdn.jsdelivr.net/npm/tref-block';
|
|
27
|
+
</script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Core Principles
|
|
31
|
+
|
|
32
|
+
1. **The Block is the Atom of Truth** – smallest meaningful unit of knowledge
|
|
33
|
+
2. **Reference Lineage** – modifications create new blocks, preserving history
|
|
34
|
+
3. **References Survive** – not just URLs, but archived snippets and search prompts
|
|
35
|
+
4. **AI-First, Human-Readable** – JSON format readable by both machines and humans
|
|
36
|
+
5. **Verifiability Over Authority** – trust through transparency, not central control
|
|
37
|
+
6. **Copy = Citation** – copying preserves origin, references, and license
|
|
38
|
+
7. **The Icon IS the Block** – no extra containers, just the draggable icon
|
|
39
|
+
|
|
40
|
+
## Browser Components
|
|
41
|
+
|
|
42
|
+
Two components enable TREF exchange between websites and AI chats.
|
|
43
|
+
See [src/wrapper/README.md](src/wrapper/README.md) for full design guide.
|
|
44
|
+
|
|
45
|
+
### TrefWrapper – Display and Share
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
const wrapper = new TrefWrapper(block);
|
|
49
|
+
container.innerHTML = wrapper.toHTML();
|
|
50
|
+
wrapper.attachEvents(container.querySelector('.tref-wrapper'));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- ICON is the drag handle (only the icon, not the entire block)
|
|
54
|
+
- Actions appear on hover: "drag me" hint, copy, download
|
|
55
|
+
- Status feedback in the shortId badge
|
|
56
|
+
|
|
57
|
+
### TrefReceiver – Accept TREF Blocks
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const receiver = new TrefReceiver(element, {
|
|
61
|
+
onReceive: wrapper => console.log('Got block:', wrapper.block),
|
|
62
|
+
onError: err => console.error(err),
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Drag-and-Drop Flow
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
70
|
+
│ Site A │ │ Site B / Chat │
|
|
71
|
+
│ │ drag TREF │ │
|
|
72
|
+
│ [TREF icon] ──────────────────────► │ [Drop Zone] │
|
|
73
|
+
│ TrefWrapper │ │ TrefReceiver │
|
|
74
|
+
└─────────────────┘ └─────────────────┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The entire block (content, refs, license, lineage) transfers with the drag.
|
|
78
|
+
|
|
79
|
+
## Quick Start
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install
|
|
83
|
+
npm run build # build browser bundle
|
|
84
|
+
npm run dev # start dev server (http://localhost:8080)
|
|
85
|
+
npm run check # typecheck + lint + format
|
|
86
|
+
npm test # run tests
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Architecture
|
|
90
|
+
|
|
91
|
+
**Single source, many outputs:**
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/wrapper/wrapper.js ← Write code here
|
|
95
|
+
│
|
|
96
|
+
npm run build
|
|
97
|
+
▼
|
|
98
|
+
dist/tref-block.js ← Built output (10kb)
|
|
99
|
+
│
|
|
100
|
+
├──> CDN (jsdelivr/unpkg)
|
|
101
|
+
├──> npm install
|
|
102
|
+
└──> demo/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
This project uses **TypeScript-level safety in pure JavaScript**:
|
|
108
|
+
|
|
109
|
+
- JSDoc type annotations
|
|
110
|
+
- TypeScript CLI for type checking (no transpilation)
|
|
111
|
+
- ESLint with type-aware rules
|
|
112
|
+
- Zod for runtime validation
|
|
113
|
+
- Prettier for formatting
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm run build:browser # build browser bundle
|
|
117
|
+
npm run typecheck # type check
|
|
118
|
+
npm run lint # lint
|
|
119
|
+
npm run format # format code
|
|
120
|
+
npm run check # all checks
|
|
121
|
+
npm test # run tests
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Project Structure
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
src/
|
|
128
|
+
wrapper/
|
|
129
|
+
wrapper.js # TrefWrapper + TrefReceiver (single source)
|
|
130
|
+
cli/
|
|
131
|
+
index.js # CLI tool
|
|
132
|
+
mcp/
|
|
133
|
+
server.js # MCP server for Claude Code
|
|
134
|
+
dist/
|
|
135
|
+
tref-block.js # Built browser bundle
|
|
136
|
+
demo/
|
|
137
|
+
index.html # Visual testing
|
|
138
|
+
doc/
|
|
139
|
+
project.md # Vision & architecture
|
|
140
|
+
TrefBlockUIUX.md # UI/UX specification
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Documentation
|
|
144
|
+
|
|
145
|
+
- [Project & Architecture](doc/project.md) – vision, principles, single-source architecture
|
|
146
|
+
- [UI/UX Specification](doc/TrefBlockUIUX.md) – complete UI/UX design guide
|
|
147
|
+
|
|
148
|
+
## Status
|
|
149
|
+
|
|
150
|
+
**MVP Complete** – Single-source architecture with browser bundle.
|
|
151
|
+
|
|
152
|
+
- TrefWrapper + TrefReceiver (drag-and-drop)
|
|
153
|
+
- Accessibility: ARIA, keyboard, focus-visible
|
|
154
|
+
- Mobile/touch support: tap-to-toggle, long-press
|
|
155
|
+
- Dropdown action menu with SVG icons
|
|
156
|
+
- CLI tool (`tref publish`, `tref validate`, `tref derive`)
|
|
157
|
+
- MCP server for Claude Code integration
|
|
158
|
+
- Live demo at [tref.lpmwfx.com](https://tref.lpmwfx.com)
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
AGPL-3.0-or-later
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";var p=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var k=(o,e)=>{for(var t in e)p(o,t,{get:e[t],enumerable:!0})},T=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of y(e))!m.call(o,r)&&r!==t&&p(o,r,{get:()=>e[r],enumerable:!(n=w(e,r))||n.enumerable});return o};var C=o=>T(p({},"__esModule",{value:!0}),o);var j={};k(j,{TREF_ICON_DATA_URL:()=>E,TREF_ICON_SVG:()=>g,TREF_MIME_TYPE:()=>c,TrefReceiver:()=>h,TrefWrapper:()=>l,unwrap:()=>b,wrap:()=>u});module.exports=C(j);var L=".tref",g=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="24" height="24">
|
|
2
|
+
<rect x="6" y="6" width="88" height="88" rx="12" ry="12" fill="#2D1B4E" stroke="#5CCCCC" stroke-width="5"/>
|
|
3
|
+
<g transform="translate(50 50) scale(0.022) translate(-1125 -1125)">
|
|
4
|
+
<g transform="translate(0,2250) scale(1,-1)" fill="#5CCCCC">
|
|
5
|
+
<path d="M1515 2244 c-66 -10 -144 -38 -220 -77 -67 -35 -106 -67 -237 -195 -155 -152 -188 -195 -188 -247 0 -41 30 -95 64 -116 39 -24 113 -25 146 -3 14 9 90 81 170 160 183 181 216 199 350 199 83 0 103 -4 155 -28 78 -36 146 -104 182 -181 24 -53 28 -73 28 -151 0 -137 -21 -175 -199 -355 -79 -80 -151 -156 -160 -170 -39 -59 -8 -162 58 -194 81 -38 113 -22 284 147 165 163 230 252 268 370 24 71 28 99 28 202 0 106 -3 130 -28 200 -91 261 -310 428 -579 439 -50 3 -105 2 -122 0z"/>
|
|
6
|
+
<path d="M1395 1585 c-17 -9 -189 -174 -382 -368 -377 -378 -383 -385 -362 -461 21 -76 87 -116 166 -101 33 6 80 49 386 353 191 191 358 362 369 381 26 42 28 109 4 146 -39 59 -118 81 -181 50z"/>
|
|
7
|
+
<path d="M463 1364 c-47 -24 -323 -310 -365 -379 -20 -33 -49 -96 -64 -140 -24 -69 -28 -96 -28 -195 0 -127 14 -190 66 -294 63 -126 157 -220 284 -284 104 -52 167 -66 294 -66 99 0 126 4 195 28 44 15 107 44 140 64 65 39 348 309 371 354 41 78 -10 184 -96 203 -61 13 -98 -11 -256 -166 -186 -183 -222 -204 -359 -204 -77 0 -98 4 -147 27 -79 37 -142 98 -181 177 -29 59 -32 74 -32 156 0 136 21 174 199 355 79 80 150 156 159 170 23 33 22 107 -2 146 -35 57 -115 79 -178 48z"/>
|
|
8
|
+
</g>
|
|
9
|
+
</g>
|
|
10
|
+
</svg>`,c="application/vnd.tref+json",E="data:image/svg+xml,"+encodeURIComponent(g);function D(o){if(!o||typeof o!="object")return!1;let e=o;return!(e.v!==1||typeof e.id!="string"||!e.id.startsWith("sha256:")||typeof e.content!="string"||!e.meta||typeof e.meta!="object")}var l=class{#e;constructor(e){if(!D(e))throw new Error("Invalid TREF block");this.#e=e}get block(){return this.#e}get id(){return this.#e.id}get shortId(){return this.#e.id.replace("sha256:","").slice(0,8)}get content(){return this.#e.content}toJSON(e={}){return e.pretty?JSON.stringify(this.#e,null,2):JSON.stringify(this.#e)}getFilename(){return this.#e.id.replace("sha256:","")+L}toBlob(){return new Blob([this.toJSON()],{type:c})}toDataURL(){let e=this.toJSON(),t=btoa(unescape(encodeURIComponent(e)));return`data:${c};base64,${t}`}toObjectURL(){return URL.createObjectURL(this.toBlob())}async copyToClipboard(){await navigator.clipboard.writeText(this.toJSON())}async copyContentToClipboard(){await navigator.clipboard.writeText(this.#e.content)}getDragData(){let e=this.toJSON();return[{type:c,data:e},{type:"application/json",data:e},{type:"text/plain",data:e}]}setDragData(e){for(let{type:t,data:n}of this.getDragData())e.setData(t,n)}toHTML(e={}){let{interactive:t=!0}=e,a=t?`<div class="tref-actions" role="group" aria-label="Block actions">
|
|
11
|
+
<button class="tref-action" data-action="copy-content" title="Copy content" aria-label="Copy content to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
|
12
|
+
<button class="tref-action" data-action="copy-json" title="Copy JSON" aria-label="Copy JSON to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1"></path><path d="M16 3h1a2 2 0 0 1 2 2v5a2 2 0 0 0 2 2 2 2 0 0 0-2 2v5a2 2 0 0 1-2 2h-1"></path><circle cx="12" cy="12" r="1" fill="currentColor"></circle><circle cx="8" cy="12" r="1" fill="currentColor"></circle><circle cx="16" cy="12" r="1" fill="currentColor"></circle></svg></button>
|
|
13
|
+
<button class="tref-action" data-action="download" title="Download .tref" aria-label="Download as .tref file"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
|
14
|
+
</div>`:"";return`<div class="tref-wrapper" data-tref-id="${this.#e.id}">
|
|
15
|
+
<span class="tref-icon"
|
|
16
|
+
role="button"
|
|
17
|
+
aria-label="TREF block - drag to share"
|
|
18
|
+
tabindex="0"
|
|
19
|
+
draggable="true"
|
|
20
|
+
title="Drag to share">${g}</span>
|
|
21
|
+
${a}
|
|
22
|
+
</div>`}#t(e){let t=e.querySelector(".tref-actions");if(t){let n=t,r=n.style.opacity==="1";if(n.style.opacity=r?"0":"1",!r){let i=n.querySelector("button");i&&i.focus()}}}attachEvents(e){let t=e.querySelector(".tref-icon");if(t){let r=t;r.addEventListener("dragstart",a=>{let s=a;s.dataTransfer&&(this.setDragData(s.dataTransfer),s.dataTransfer.effectAllowed="copy")}),r.addEventListener("keydown",a=>{(a.key==="Enter"||a.key===" ")&&(a.preventDefault(),this.#t(e))}),r.addEventListener("touchend",a=>{r.dataset.dragging||(a.preventDefault(),this.#t(e))});let i;r.addEventListener("touchstart",()=>{i=setTimeout(()=>{r.dataset.dragging="true",r.style.transform="scale(1.15)"},500)}),r.addEventListener("touchend",()=>{clearTimeout(i),delete r.dataset.dragging,r.style.transform=""}),r.addEventListener("touchcancel",()=>{clearTimeout(i),delete r.dataset.dragging,r.style.transform=""})}let n=async r=>{r.stopPropagation();let i=r.currentTarget,a=i.dataset.action,s=i.innerHTML,f='<svg class="tref-icon-success" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',x='<svg class="tref-icon-error" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';try{if(a==="copy-content")await this.copyContentToClipboard(),i.innerHTML=f;else if(a==="copy-json")await this.copyToClipboard(),i.innerHTML=f;else if(a==="download"){let v=this.toObjectURL(),d=document.createElement("a");d.href=v,d.download=this.getFilename(),d.click(),URL.revokeObjectURL(v),i.innerHTML=f}setTimeout(()=>{i.innerHTML=s},1e3)}catch{i.innerHTML=x,setTimeout(()=>{i.innerHTML=s},1e3)}};e.querySelectorAll(".tref-action").forEach(r=>{r.addEventListener("click",i=>{n(i)})})}static getStyles(){return`
|
|
23
|
+
:root {
|
|
24
|
+
--tref-accent: #5CCCCC;
|
|
25
|
+
--tref-accent-hover: #8B5CF6;
|
|
26
|
+
--tref-success: #10B981;
|
|
27
|
+
--tref-error: #ef4444;
|
|
28
|
+
--tref-menu-bg: #ffffff;
|
|
29
|
+
--tref-menu-text: #374151;
|
|
30
|
+
--tref-menu-hover: #f3f4f6;
|
|
31
|
+
--tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
32
|
+
--tref-receiver-bg: #f9fafb;
|
|
33
|
+
--tref-receiver-text: #6b7280;
|
|
34
|
+
--tref-receiver-active-bg: #f3e8ff;
|
|
35
|
+
--tref-receiver-success-bg: #ecfdf5;
|
|
36
|
+
--tref-receiver-error-bg: #fef2f2;
|
|
37
|
+
--tref-receiver-block-bg: #ffffff;
|
|
38
|
+
}
|
|
39
|
+
@media (prefers-color-scheme: dark) {
|
|
40
|
+
:root {
|
|
41
|
+
--tref-menu-bg: #1f2937;
|
|
42
|
+
--tref-menu-text: #e5e7eb;
|
|
43
|
+
--tref-menu-hover: #374151;
|
|
44
|
+
--tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
45
|
+
--tref-receiver-bg: #1f2937;
|
|
46
|
+
--tref-receiver-text: #9ca3af;
|
|
47
|
+
--tref-receiver-active-bg: #3b2d5e;
|
|
48
|
+
--tref-receiver-success-bg: #064e3b;
|
|
49
|
+
--tref-receiver-error-bg: #450a0a;
|
|
50
|
+
--tref-receiver-block-bg: #111827;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
.tref-wrapper {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
position: relative;
|
|
56
|
+
}
|
|
57
|
+
.tref-icon {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
width: 32px;
|
|
60
|
+
height: 32px;
|
|
61
|
+
cursor: grab;
|
|
62
|
+
transition: transform 0.15s;
|
|
63
|
+
}
|
|
64
|
+
.tref-icon:hover { transform: scale(1.1); }
|
|
65
|
+
.tref-icon:active { cursor: grabbing; }
|
|
66
|
+
.tref-icon svg { width: 100%; height: 100%; }
|
|
67
|
+
.tref-actions {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: 100%;
|
|
70
|
+
left: 50%;
|
|
71
|
+
transform: translateX(-50%);
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 2px;
|
|
75
|
+
padding: 4px;
|
|
76
|
+
background: var(--tref-menu-bg);
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
box-shadow: var(--tref-menu-shadow);
|
|
79
|
+
opacity: 0;
|
|
80
|
+
visibility: hidden;
|
|
81
|
+
transition: opacity 0.15s, visibility 0.15s;
|
|
82
|
+
z-index: 100;
|
|
83
|
+
margin-top: 4px;
|
|
84
|
+
}
|
|
85
|
+
.tref-wrapper:hover .tref-actions {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
visibility: visible;
|
|
88
|
+
}
|
|
89
|
+
.tref-action {
|
|
90
|
+
background: transparent;
|
|
91
|
+
border: none;
|
|
92
|
+
outline: none;
|
|
93
|
+
padding: 8px;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
color: var(--tref-menu-text);
|
|
97
|
+
transition: background 0.15s;
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
.tref-action svg {
|
|
103
|
+
width: 16px;
|
|
104
|
+
height: 16px;
|
|
105
|
+
}
|
|
106
|
+
.tref-action:hover { background: var(--tref-menu-hover); }
|
|
107
|
+
.tref-action:focus { outline: none; }
|
|
108
|
+
.tref-action:focus-visible {
|
|
109
|
+
outline: 2px solid var(--tref-accent);
|
|
110
|
+
outline-offset: 1px;
|
|
111
|
+
}
|
|
112
|
+
.tref-icon:focus { outline: none; }
|
|
113
|
+
.tref-icon:focus-visible {
|
|
114
|
+
outline: 2px solid var(--tref-accent);
|
|
115
|
+
outline-offset: 2px;
|
|
116
|
+
border-radius: 4px;
|
|
117
|
+
}
|
|
118
|
+
.tref-icon-success { color: var(--tref-success); }
|
|
119
|
+
.tref-icon-error { color: var(--tref-error); }
|
|
120
|
+
`}},h=class{#e;#t;#r;#o;constructor(e,t={}){this.#e=e,this.#t=t.onReceive||(()=>{}),this.#r=t.onError||(()=>{}),this.#o=t.compact||!1,this.#i()}#i(){let e=this.#e;e.classList.add("tref-receiver"),this.#o&&e.classList.add("tref-receiver-compact"),e.setAttribute("role","region"),e.setAttribute("aria-label","Drop zone for TREF blocks"),e.setAttribute("aria-dropeffect","copy"),e.addEventListener("dragover",t=>{t.preventDefault(),t.dataTransfer&&(t.dataTransfer.dropEffect="copy"),e.classList.add("tref-receiver-active")}),e.addEventListener("dragleave",()=>{e.classList.remove("tref-receiver-active")}),e.addEventListener("drop",t=>{if(t.preventDefault(),e.classList.remove("tref-receiver-active"),!t.dataTransfer){this.#r(new Error("No data"));return}let n=b(t.dataTransfer);n?(e.classList.add("tref-receiver-success"),setTimeout(()=>e.classList.remove("tref-receiver-success"),1e3),this.#t(n)):(e.classList.add("tref-receiver-error"),setTimeout(()=>e.classList.remove("tref-receiver-error"),1e3),this.#r(new Error("Invalid TREF data")))})}get element(){return this.#e}showBlock(e){this.#e.innerHTML=e.toHTML(),this.#e.classList.add("tref-receiver-has-block")}clear(){this.#e.innerHTML=this.#e.dataset.placeholder||"Drop TREF here",this.#e.classList.remove("tref-receiver-has-block")}static getStyles(){return`
|
|
121
|
+
.tref-receiver {
|
|
122
|
+
border: 2px dashed var(--tref-accent);
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
padding: 20px;
|
|
125
|
+
min-height: 80px;
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
color: var(--tref-receiver-text);
|
|
130
|
+
background: var(--tref-receiver-bg);
|
|
131
|
+
transition: all 0.2s;
|
|
132
|
+
}
|
|
133
|
+
.tref-receiver-active {
|
|
134
|
+
border-color: var(--tref-accent-hover);
|
|
135
|
+
background: var(--tref-receiver-active-bg);
|
|
136
|
+
color: var(--tref-accent-hover);
|
|
137
|
+
}
|
|
138
|
+
.tref-receiver-success {
|
|
139
|
+
border-color: var(--tref-success);
|
|
140
|
+
background: var(--tref-receiver-success-bg);
|
|
141
|
+
}
|
|
142
|
+
.tref-receiver-error {
|
|
143
|
+
border-color: var(--tref-error);
|
|
144
|
+
background: var(--tref-receiver-error-bg);
|
|
145
|
+
}
|
|
146
|
+
.tref-receiver-has-block {
|
|
147
|
+
border-style: solid;
|
|
148
|
+
background: var(--tref-receiver-block-bg);
|
|
149
|
+
}
|
|
150
|
+
.tref-receiver-compact {
|
|
151
|
+
width: 32px;
|
|
152
|
+
height: 32px;
|
|
153
|
+
min-height: 32px;
|
|
154
|
+
padding: 0;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
}
|
|
157
|
+
/* Touch devices - larger hit areas */
|
|
158
|
+
@media (pointer: coarse) {
|
|
159
|
+
.tref-icon {
|
|
160
|
+
min-width: 44px;
|
|
161
|
+
min-height: 44px;
|
|
162
|
+
}
|
|
163
|
+
.tref-action {
|
|
164
|
+
min-width: 44px;
|
|
165
|
+
min-height: 44px;
|
|
166
|
+
padding: 10px;
|
|
167
|
+
}
|
|
168
|
+
.tref-receiver-compact {
|
|
169
|
+
width: 48px;
|
|
170
|
+
height: 48px;
|
|
171
|
+
min-height: 48px;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
`}};function u(o){return new l(o)}function b(o){try{let e;if(typeof o=="string")e=o;else if(o&&typeof o.getData=="function")e=o.getData(c)||o.getData("application/json")||o.getData("text/plain");else return null;return e?u(JSON.parse(e)):null}catch{return null}}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
var u=".tref",g=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="24" height="24">
|
|
2
|
+
<rect x="6" y="6" width="88" height="88" rx="12" ry="12" fill="#2D1B4E" stroke="#5CCCCC" stroke-width="5"/>
|
|
3
|
+
<g transform="translate(50 50) scale(0.022) translate(-1125 -1125)">
|
|
4
|
+
<g transform="translate(0,2250) scale(1,-1)" fill="#5CCCCC">
|
|
5
|
+
<path d="M1515 2244 c-66 -10 -144 -38 -220 -77 -67 -35 -106 -67 -237 -195 -155 -152 -188 -195 -188 -247 0 -41 30 -95 64 -116 39 -24 113 -25 146 -3 14 9 90 81 170 160 183 181 216 199 350 199 83 0 103 -4 155 -28 78 -36 146 -104 182 -181 24 -53 28 -73 28 -151 0 -137 -21 -175 -199 -355 -79 -80 -151 -156 -160 -170 -39 -59 -8 -162 58 -194 81 -38 113 -22 284 147 165 163 230 252 268 370 24 71 28 99 28 202 0 106 -3 130 -28 200 -91 261 -310 428 -579 439 -50 3 -105 2 -122 0z"/>
|
|
6
|
+
<path d="M1395 1585 c-17 -9 -189 -174 -382 -368 -377 -378 -383 -385 -362 -461 21 -76 87 -116 166 -101 33 6 80 49 386 353 191 191 358 362 369 381 26 42 28 109 4 146 -39 59 -118 81 -181 50z"/>
|
|
7
|
+
<path d="M463 1364 c-47 -24 -323 -310 -365 -379 -20 -33 -49 -96 -64 -140 -24 -69 -28 -96 -28 -195 0 -127 14 -190 66 -294 63 -126 157 -220 284 -284 104 -52 167 -66 294 -66 99 0 126 4 195 28 44 15 107 44 140 64 65 39 348 309 371 354 41 78 -10 184 -96 203 -61 13 -98 -11 -256 -166 -186 -183 -222 -204 -359 -204 -77 0 -98 4 -147 27 -79 37 -142 98 -181 177 -29 59 -32 74 -32 156 0 136 21 174 199 355 79 80 150 156 159 170 23 33 22 107 -2 146 -35 57 -115 79 -178 48z"/>
|
|
8
|
+
</g>
|
|
9
|
+
</g>
|
|
10
|
+
</svg>`,c="application/vnd.tref+json",y="data:image/svg+xml,"+encodeURIComponent(g);function b(i){if(!i||typeof i!="object")return!1;let e=i;return!(e.v!==1||typeof e.id!="string"||!e.id.startsWith("sha256:")||typeof e.content!="string"||!e.meta||typeof e.meta!="object")}var d=class{#e;constructor(e){if(!b(e))throw new Error("Invalid TREF block");this.#e=e}get block(){return this.#e}get id(){return this.#e.id}get shortId(){return this.#e.id.replace("sha256:","").slice(0,8)}get content(){return this.#e.content}toJSON(e={}){return e.pretty?JSON.stringify(this.#e,null,2):JSON.stringify(this.#e)}getFilename(){return this.#e.id.replace("sha256:","")+u}toBlob(){return new Blob([this.toJSON()],{type:c})}toDataURL(){let e=this.toJSON(),t=btoa(unescape(encodeURIComponent(e)));return`data:${c};base64,${t}`}toObjectURL(){return URL.createObjectURL(this.toBlob())}async copyToClipboard(){await navigator.clipboard.writeText(this.toJSON())}async copyContentToClipboard(){await navigator.clipboard.writeText(this.#e.content)}getDragData(){let e=this.toJSON();return[{type:c,data:e},{type:"application/json",data:e},{type:"text/plain",data:e}]}setDragData(e){for(let{type:t,data:a}of this.getDragData())e.setData(t,a)}toHTML(e={}){let{interactive:t=!0}=e,n=t?`<div class="tref-actions" role="group" aria-label="Block actions">
|
|
11
|
+
<button class="tref-action" data-action="copy-content" title="Copy content" aria-label="Copy content to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>
|
|
12
|
+
<button class="tref-action" data-action="copy-json" title="Copy JSON" aria-label="Copy JSON to clipboard"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1"></path><path d="M16 3h1a2 2 0 0 1 2 2v5a2 2 0 0 0 2 2 2 2 0 0 0-2 2v5a2 2 0 0 1-2 2h-1"></path><circle cx="12" cy="12" r="1" fill="currentColor"></circle><circle cx="8" cy="12" r="1" fill="currentColor"></circle><circle cx="16" cy="12" r="1" fill="currentColor"></circle></svg></button>
|
|
13
|
+
<button class="tref-action" data-action="download" title="Download .tref" aria-label="Download as .tref file"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>
|
|
14
|
+
</div>`:"";return`<div class="tref-wrapper" data-tref-id="${this.#e.id}">
|
|
15
|
+
<span class="tref-icon"
|
|
16
|
+
role="button"
|
|
17
|
+
aria-label="TREF block - drag to share"
|
|
18
|
+
tabindex="0"
|
|
19
|
+
draggable="true"
|
|
20
|
+
title="Drag to share">${g}</span>
|
|
21
|
+
${n}
|
|
22
|
+
</div>`}#t(e){let t=e.querySelector(".tref-actions");if(t){let a=t,r=a.style.opacity==="1";if(a.style.opacity=r?"0":"1",!r){let o=a.querySelector("button");o&&o.focus()}}}attachEvents(e){let t=e.querySelector(".tref-icon");if(t){let r=t;r.addEventListener("dragstart",n=>{let s=n;s.dataTransfer&&(this.setDragData(s.dataTransfer),s.dataTransfer.effectAllowed="copy")}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),this.#t(e))}),r.addEventListener("touchend",n=>{r.dataset.dragging||(n.preventDefault(),this.#t(e))});let o;r.addEventListener("touchstart",()=>{o=setTimeout(()=>{r.dataset.dragging="true",r.style.transform="scale(1.15)"},500)}),r.addEventListener("touchend",()=>{clearTimeout(o),delete r.dataset.dragging,r.style.transform=""}),r.addEventListener("touchcancel",()=>{clearTimeout(o),delete r.dataset.dragging,r.style.transform=""})}let a=async r=>{r.stopPropagation();let o=r.currentTarget,n=o.dataset.action,s=o.innerHTML,l='<svg class="tref-icon-success" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',v='<svg class="tref-icon-error" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';try{if(n==="copy-content")await this.copyContentToClipboard(),o.innerHTML=l;else if(n==="copy-json")await this.copyToClipboard(),o.innerHTML=l;else if(n==="download"){let p=this.toObjectURL(),f=document.createElement("a");f.href=p,f.download=this.getFilename(),f.click(),URL.revokeObjectURL(p),o.innerHTML=l}setTimeout(()=>{o.innerHTML=s},1e3)}catch{o.innerHTML=v,setTimeout(()=>{o.innerHTML=s},1e3)}};e.querySelectorAll(".tref-action").forEach(r=>{r.addEventListener("click",o=>{a(o)})})}static getStyles(){return`
|
|
23
|
+
:root {
|
|
24
|
+
--tref-accent: #5CCCCC;
|
|
25
|
+
--tref-accent-hover: #8B5CF6;
|
|
26
|
+
--tref-success: #10B981;
|
|
27
|
+
--tref-error: #ef4444;
|
|
28
|
+
--tref-menu-bg: #ffffff;
|
|
29
|
+
--tref-menu-text: #374151;
|
|
30
|
+
--tref-menu-hover: #f3f4f6;
|
|
31
|
+
--tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
32
|
+
--tref-receiver-bg: #f9fafb;
|
|
33
|
+
--tref-receiver-text: #6b7280;
|
|
34
|
+
--tref-receiver-active-bg: #f3e8ff;
|
|
35
|
+
--tref-receiver-success-bg: #ecfdf5;
|
|
36
|
+
--tref-receiver-error-bg: #fef2f2;
|
|
37
|
+
--tref-receiver-block-bg: #ffffff;
|
|
38
|
+
}
|
|
39
|
+
@media (prefers-color-scheme: dark) {
|
|
40
|
+
:root {
|
|
41
|
+
--tref-menu-bg: #1f2937;
|
|
42
|
+
--tref-menu-text: #e5e7eb;
|
|
43
|
+
--tref-menu-hover: #374151;
|
|
44
|
+
--tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
45
|
+
--tref-receiver-bg: #1f2937;
|
|
46
|
+
--tref-receiver-text: #9ca3af;
|
|
47
|
+
--tref-receiver-active-bg: #3b2d5e;
|
|
48
|
+
--tref-receiver-success-bg: #064e3b;
|
|
49
|
+
--tref-receiver-error-bg: #450a0a;
|
|
50
|
+
--tref-receiver-block-bg: #111827;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
.tref-wrapper {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
position: relative;
|
|
56
|
+
}
|
|
57
|
+
.tref-icon {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
width: 32px;
|
|
60
|
+
height: 32px;
|
|
61
|
+
cursor: grab;
|
|
62
|
+
transition: transform 0.15s;
|
|
63
|
+
}
|
|
64
|
+
.tref-icon:hover { transform: scale(1.1); }
|
|
65
|
+
.tref-icon:active { cursor: grabbing; }
|
|
66
|
+
.tref-icon svg { width: 100%; height: 100%; }
|
|
67
|
+
.tref-actions {
|
|
68
|
+
position: absolute;
|
|
69
|
+
top: 100%;
|
|
70
|
+
left: 50%;
|
|
71
|
+
transform: translateX(-50%);
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 2px;
|
|
75
|
+
padding: 4px;
|
|
76
|
+
background: var(--tref-menu-bg);
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
box-shadow: var(--tref-menu-shadow);
|
|
79
|
+
opacity: 0;
|
|
80
|
+
visibility: hidden;
|
|
81
|
+
transition: opacity 0.15s, visibility 0.15s;
|
|
82
|
+
z-index: 100;
|
|
83
|
+
margin-top: 4px;
|
|
84
|
+
}
|
|
85
|
+
.tref-wrapper:hover .tref-actions {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
visibility: visible;
|
|
88
|
+
}
|
|
89
|
+
.tref-action {
|
|
90
|
+
background: transparent;
|
|
91
|
+
border: none;
|
|
92
|
+
outline: none;
|
|
93
|
+
padding: 8px;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
color: var(--tref-menu-text);
|
|
97
|
+
transition: background 0.15s;
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
.tref-action svg {
|
|
103
|
+
width: 16px;
|
|
104
|
+
height: 16px;
|
|
105
|
+
}
|
|
106
|
+
.tref-action:hover { background: var(--tref-menu-hover); }
|
|
107
|
+
.tref-action:focus { outline: none; }
|
|
108
|
+
.tref-action:focus-visible {
|
|
109
|
+
outline: 2px solid var(--tref-accent);
|
|
110
|
+
outline-offset: 1px;
|
|
111
|
+
}
|
|
112
|
+
.tref-icon:focus { outline: none; }
|
|
113
|
+
.tref-icon:focus-visible {
|
|
114
|
+
outline: 2px solid var(--tref-accent);
|
|
115
|
+
outline-offset: 2px;
|
|
116
|
+
border-radius: 4px;
|
|
117
|
+
}
|
|
118
|
+
.tref-icon-success { color: var(--tref-success); }
|
|
119
|
+
.tref-icon-error { color: var(--tref-error); }
|
|
120
|
+
`}},h=class{#e;#t;#r;#o;constructor(e,t={}){this.#e=e,this.#t=t.onReceive||(()=>{}),this.#r=t.onError||(()=>{}),this.#o=t.compact||!1,this.#i()}#i(){let e=this.#e;e.classList.add("tref-receiver"),this.#o&&e.classList.add("tref-receiver-compact"),e.setAttribute("role","region"),e.setAttribute("aria-label","Drop zone for TREF blocks"),e.setAttribute("aria-dropeffect","copy"),e.addEventListener("dragover",t=>{t.preventDefault(),t.dataTransfer&&(t.dataTransfer.dropEffect="copy"),e.classList.add("tref-receiver-active")}),e.addEventListener("dragleave",()=>{e.classList.remove("tref-receiver-active")}),e.addEventListener("drop",t=>{if(t.preventDefault(),e.classList.remove("tref-receiver-active"),!t.dataTransfer){this.#r(new Error("No data"));return}let a=w(t.dataTransfer);a?(e.classList.add("tref-receiver-success"),setTimeout(()=>e.classList.remove("tref-receiver-success"),1e3),this.#t(a)):(e.classList.add("tref-receiver-error"),setTimeout(()=>e.classList.remove("tref-receiver-error"),1e3),this.#r(new Error("Invalid TREF data")))})}get element(){return this.#e}showBlock(e){this.#e.innerHTML=e.toHTML(),this.#e.classList.add("tref-receiver-has-block")}clear(){this.#e.innerHTML=this.#e.dataset.placeholder||"Drop TREF here",this.#e.classList.remove("tref-receiver-has-block")}static getStyles(){return`
|
|
121
|
+
.tref-receiver {
|
|
122
|
+
border: 2px dashed var(--tref-accent);
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
padding: 20px;
|
|
125
|
+
min-height: 80px;
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
color: var(--tref-receiver-text);
|
|
130
|
+
background: var(--tref-receiver-bg);
|
|
131
|
+
transition: all 0.2s;
|
|
132
|
+
}
|
|
133
|
+
.tref-receiver-active {
|
|
134
|
+
border-color: var(--tref-accent-hover);
|
|
135
|
+
background: var(--tref-receiver-active-bg);
|
|
136
|
+
color: var(--tref-accent-hover);
|
|
137
|
+
}
|
|
138
|
+
.tref-receiver-success {
|
|
139
|
+
border-color: var(--tref-success);
|
|
140
|
+
background: var(--tref-receiver-success-bg);
|
|
141
|
+
}
|
|
142
|
+
.tref-receiver-error {
|
|
143
|
+
border-color: var(--tref-error);
|
|
144
|
+
background: var(--tref-receiver-error-bg);
|
|
145
|
+
}
|
|
146
|
+
.tref-receiver-has-block {
|
|
147
|
+
border-style: solid;
|
|
148
|
+
background: var(--tref-receiver-block-bg);
|
|
149
|
+
}
|
|
150
|
+
.tref-receiver-compact {
|
|
151
|
+
width: 32px;
|
|
152
|
+
height: 32px;
|
|
153
|
+
min-height: 32px;
|
|
154
|
+
padding: 0;
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
}
|
|
157
|
+
/* Touch devices - larger hit areas */
|
|
158
|
+
@media (pointer: coarse) {
|
|
159
|
+
.tref-icon {
|
|
160
|
+
min-width: 44px;
|
|
161
|
+
min-height: 44px;
|
|
162
|
+
}
|
|
163
|
+
.tref-action {
|
|
164
|
+
min-width: 44px;
|
|
165
|
+
min-height: 44px;
|
|
166
|
+
padding: 10px;
|
|
167
|
+
}
|
|
168
|
+
.tref-receiver-compact {
|
|
169
|
+
width: 48px;
|
|
170
|
+
height: 48px;
|
|
171
|
+
min-height: 48px;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
`}};function x(i){return new d(i)}function w(i){try{let e;if(typeof i=="string")e=i;else if(i&&typeof i.getData=="function")e=i.getData(c)||i.getData("application/json")||i.getData("text/plain");else return null;return e?x(JSON.parse(e)):null}catch{return null}}export{y as TREF_ICON_DATA_URL,g as TREF_ICON_SVG,c as TREF_MIME_TYPE,h as TrefReceiver,d as TrefWrapper,w as unwrap,x as wrap};
|
|
175
|
+
//# sourceMappingURL=tref-block.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/wrapper/wrapper.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * @fileoverview TREF Block wrapper for display and interaction\n * Self-contained - no external dependencies\n */\n\n/* global btoa, navigator, Blob, URL, document */\n\n/** File extension for TREF files */\nconst TREF_EXTENSION = '.tref';\n\n/**\n * @typedef {object} TrefBlock\n * @property {1} v\n * @property {string} id\n * @property {string} content\n * @property {{ author?: string, created: string, modified?: string, license: string, lang?: string }} meta\n * @property {Array<{ type: string, url?: string, title?: string, snippet?: string, query?: string }>} [refs]\n * @property {string} [parent]\n */\n\n/**\n * SVG icon for TREF (purple-mint theme with chain link)\n */\nexport const TREF_ICON_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" width=\"24\" height=\"24\">\n <rect x=\"6\" y=\"6\" width=\"88\" height=\"88\" rx=\"12\" ry=\"12\" fill=\"#2D1B4E\" stroke=\"#5CCCCC\" stroke-width=\"5\"/>\n <g transform=\"translate(50 50) scale(0.022) translate(-1125 -1125)\">\n <g transform=\"translate(0,2250) scale(1,-1)\" fill=\"#5CCCCC\">\n <path d=\"M1515 2244 c-66 -10 -144 -38 -220 -77 -67 -35 -106 -67 -237 -195 -155 -152 -188 -195 -188 -247 0 -41 30 -95 64 -116 39 -24 113 -25 146 -3 14 9 90 81 170 160 183 181 216 199 350 199 83 0 103 -4 155 -28 78 -36 146 -104 182 -181 24 -53 28 -73 28 -151 0 -137 -21 -175 -199 -355 -79 -80 -151 -156 -160 -170 -39 -59 -8 -162 58 -194 81 -38 113 -22 284 147 165 163 230 252 268 370 24 71 28 99 28 202 0 106 -3 130 -28 200 -91 261 -310 428 -579 439 -50 3 -105 2 -122 0z\"/>\n <path d=\"M1395 1585 c-17 -9 -189 -174 -382 -368 -377 -378 -383 -385 -362 -461 21 -76 87 -116 166 -101 33 6 80 49 386 353 191 191 358 362 369 381 26 42 28 109 4 146 -39 59 -118 81 -181 50z\"/>\n <path d=\"M463 1364 c-47 -24 -323 -310 -365 -379 -20 -33 -49 -96 -64 -140 -24 -69 -28 -96 -28 -195 0 -127 14 -190 66 -294 63 -126 157 -220 284 -284 104 -52 167 -66 294 -66 99 0 126 4 195 28 44 15 107 44 140 64 65 39 348 309 371 354 41 78 -10 184 -96 203 -61 13 -98 -11 -256 -166 -186 -183 -222 -204 -359 -204 -77 0 -98 4 -147 27 -79 37 -142 98 -181 177 -29 59 -32 74 -32 156 0 136 21 174 199 355 79 80 150 156 159 170 23 33 22 107 -2 146 -35 57 -115 79 -178 48z\"/>\n </g>\n </g>\n</svg>`;\n\n/** MIME type for TREF files */\nexport const TREF_MIME_TYPE = 'application/vnd.tref+json';\n\n/** Icon as data URL for embedding */\nexport const TREF_ICON_DATA_URL = 'data:image/svg+xml,' + encodeURIComponent(TREF_ICON_SVG);\n\n/**\n * Escape HTML special characters\n * @param {string} str\n * @returns {string}\n */\nfunction _escapeHtml(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n// Reserved for future use (content escaping)\nvoid _escapeHtml;\n\n/**\n * Validate block structure\n * @param {unknown} block\n * @returns {block is TrefBlock}\n */\nfunction isValidBlock(block) {\n if (!block || typeof block !== 'object') {\n return false;\n }\n const b = /** @type {Record<string, unknown>} */ (block);\n if (b.v !== 1) {\n return false;\n }\n if (typeof b.id !== 'string') {\n return false;\n }\n if (!b.id.startsWith('sha256:')) {\n return false;\n }\n if (typeof b.content !== 'string') {\n return false;\n }\n if (!b.meta || typeof b.meta !== 'object') {\n return false;\n }\n return true;\n}\n\n/**\n * TrefWrapper - displays a TREF block with drag/copy/download actions\n *\n * Design:\n * - Icon is the drag handle (only icon is draggable)\n * - Hover shows action buttons (copy, download)\n * - Status feedback in the ID badge\n */\nexport class TrefWrapper {\n /** @type {TrefBlock} */\n #block;\n\n /**\n * @param {TrefBlock} block\n */\n constructor(block) {\n if (!isValidBlock(block)) {\n throw new Error('Invalid TREF block');\n }\n this.#block = block;\n }\n\n get block() {\n return this.#block;\n }\n\n get id() {\n return this.#block.id;\n }\n\n get shortId() {\n return this.#block.id.replace('sha256:', '').slice(0, 8);\n }\n\n get content() {\n return this.#block.content;\n }\n\n /**\n * @param {{ pretty?: boolean }} [options]\n */\n toJSON(options = {}) {\n return options.pretty ? JSON.stringify(this.#block, null, 2) : JSON.stringify(this.#block);\n }\n\n getFilename() {\n return this.#block.id.replace('sha256:', '') + TREF_EXTENSION;\n }\n\n toBlob() {\n return new Blob([this.toJSON()], { type: TREF_MIME_TYPE });\n }\n\n toDataURL() {\n const json = this.toJSON();\n const base64 = btoa(unescape(encodeURIComponent(json)));\n return `data:${TREF_MIME_TYPE};base64,${base64}`;\n }\n\n toObjectURL() {\n return URL.createObjectURL(this.toBlob());\n }\n\n async copyToClipboard() {\n await navigator.clipboard.writeText(this.toJSON());\n }\n\n async copyContentToClipboard() {\n await navigator.clipboard.writeText(this.#block.content);\n }\n\n getDragData() {\n const json = this.toJSON();\n return [\n { type: TREF_MIME_TYPE, data: json },\n { type: 'application/json', data: json },\n { type: 'text/plain', data: json },\n ];\n }\n\n /** @param {DataTransfer} dataTransfer */\n setDragData(dataTransfer) {\n for (const { type, data } of this.getDragData()) {\n dataTransfer.setData(type, data);\n }\n }\n\n /**\n * Generate HTML - icon only with hover actions\n * @param {{ interactive?: boolean }} [options]\n */\n toHTML(options = {}) {\n const { interactive = true } = options;\n\n // SVG icons for actions\n const iconCopy = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>`;\n const iconJson = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1\"></path><path d=\"M16 3h1a2 2 0 0 1 2 2v5a2 2 0 0 0 2 2 2 2 0 0 0-2 2v5a2 2 0 0 1-2 2h-1\"></path><circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"currentColor\"></circle><circle cx=\"8\" cy=\"12\" r=\"1\" fill=\"currentColor\"></circle><circle cx=\"16\" cy=\"12\" r=\"1\" fill=\"currentColor\"></circle></svg>`;\n const iconDownload = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"></path><polyline points=\"7 10 12 15 17 10\"></polyline><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"></line></svg>`;\n\n // Hover actions appear on hover\n const actionsHtml = interactive\n ? `<div class=\"tref-actions\" role=\"group\" aria-label=\"Block actions\">\n <button class=\"tref-action\" data-action=\"copy-content\" title=\"Copy content\" aria-label=\"Copy content to clipboard\">${iconCopy}</button>\n <button class=\"tref-action\" data-action=\"copy-json\" title=\"Copy JSON\" aria-label=\"Copy JSON to clipboard\">${iconJson}</button>\n <button class=\"tref-action\" data-action=\"download\" title=\"Download .tref\" aria-label=\"Download as .tref file\">${iconDownload}</button>\n </div>`\n : '';\n\n return `<div class=\"tref-wrapper\" data-tref-id=\"${this.#block.id}\">\n <span class=\"tref-icon\"\n role=\"button\"\n aria-label=\"TREF block - drag to share\"\n tabindex=\"0\"\n draggable=\"true\"\n title=\"Drag to share\">${TREF_ICON_SVG}</span>\n ${actionsHtml}\n</div>`;\n }\n\n /**\n * Toggle actions visibility (for keyboard/touch)\n * @param {HTMLElement} element\n */\n #toggleActions(element) {\n const actions = element.querySelector('.tref-actions');\n if (actions) {\n const actionsEl = /** @type {HTMLElement} */ (actions);\n const isVisible = actionsEl.style.opacity === '1';\n actionsEl.style.opacity = isVisible ? '0' : '1';\n if (!isVisible) {\n // Focus first action button\n const firstBtn = actionsEl.querySelector('button');\n if (firstBtn) {\n /** @type {HTMLElement} */ (firstBtn).focus();\n }\n }\n }\n }\n\n /**\n * Attach event listeners to a rendered wrapper\n * @param {HTMLElement} element\n */\n attachEvents(element) {\n const iconEl = element.querySelector('.tref-icon');\n\n // Icon is drag handle\n if (iconEl) {\n const icon = /** @type {HTMLElement} */ (iconEl);\n\n icon.addEventListener('dragstart', e => {\n const de = /** @type {DragEvent} */ (e);\n if (de.dataTransfer) {\n this.setDragData(de.dataTransfer);\n de.dataTransfer.effectAllowed = 'copy';\n }\n });\n\n // Keyboard: Enter/Space shows actions or triggers default action\n icon.addEventListener('keydown', e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.#toggleActions(element);\n }\n });\n\n // Touch: tap toggles actions\n icon.addEventListener('touchend', e => {\n // Only handle if not dragging\n if (!icon.dataset.dragging) {\n e.preventDefault();\n this.#toggleActions(element);\n }\n });\n\n // Touch: long-press (500ms) to start drag indication\n /** @type {ReturnType<typeof setTimeout> | undefined} */\n let longPressTimer;\n icon.addEventListener('touchstart', () => {\n longPressTimer = setTimeout(() => {\n icon.dataset.dragging = 'true';\n icon.style.transform = 'scale(1.15)';\n }, 500);\n });\n icon.addEventListener('touchend', () => {\n clearTimeout(longPressTimer);\n delete icon.dataset.dragging;\n icon.style.transform = '';\n });\n icon.addEventListener('touchcancel', () => {\n clearTimeout(longPressTimer);\n delete icon.dataset.dragging;\n icon.style.transform = '';\n });\n }\n\n // Action buttons (visible on hover via CSS)\n const handleAction = async (/** @type {Event} */ e) => {\n e.stopPropagation();\n const btn = /** @type {HTMLElement} */ (e.currentTarget);\n const action = btn.dataset.action;\n const originalHtml = btn.innerHTML;\n\n const iconCheck = `<svg class=\"tref-icon-success\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg>`;\n const iconError = `<svg class=\"tref-icon-error\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line></svg>`;\n\n try {\n if (action === 'copy-content') {\n await this.copyContentToClipboard();\n btn.innerHTML = iconCheck;\n } else if (action === 'copy-json') {\n await this.copyToClipboard();\n btn.innerHTML = iconCheck;\n } else if (action === 'download') {\n const url = this.toObjectURL();\n const a = document.createElement('a');\n a.href = url;\n a.download = this.getFilename();\n a.click();\n URL.revokeObjectURL(url);\n btn.innerHTML = iconCheck;\n }\n setTimeout(() => {\n btn.innerHTML = originalHtml;\n }, 1000);\n } catch {\n btn.innerHTML = iconError;\n setTimeout(() => {\n btn.innerHTML = originalHtml;\n }, 1000);\n }\n };\n\n element.querySelectorAll('.tref-action').forEach(btn => {\n btn.addEventListener('click', e => void handleAction(e));\n });\n }\n\n static getStyles() {\n return `\n:root {\n --tref-accent: #5CCCCC;\n --tref-accent-hover: #8B5CF6;\n --tref-success: #10B981;\n --tref-error: #ef4444;\n --tref-menu-bg: #ffffff;\n --tref-menu-text: #374151;\n --tref-menu-hover: #f3f4f6;\n --tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.15);\n --tref-receiver-bg: #f9fafb;\n --tref-receiver-text: #6b7280;\n --tref-receiver-active-bg: #f3e8ff;\n --tref-receiver-success-bg: #ecfdf5;\n --tref-receiver-error-bg: #fef2f2;\n --tref-receiver-block-bg: #ffffff;\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --tref-menu-bg: #1f2937;\n --tref-menu-text: #e5e7eb;\n --tref-menu-hover: #374151;\n --tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.4);\n --tref-receiver-bg: #1f2937;\n --tref-receiver-text: #9ca3af;\n --tref-receiver-active-bg: #3b2d5e;\n --tref-receiver-success-bg: #064e3b;\n --tref-receiver-error-bg: #450a0a;\n --tref-receiver-block-bg: #111827;\n }\n}\n.tref-wrapper {\n display: inline-block;\n position: relative;\n}\n.tref-icon {\n display: inline-flex;\n width: 32px;\n height: 32px;\n cursor: grab;\n transition: transform 0.15s;\n}\n.tref-icon:hover { transform: scale(1.1); }\n.tref-icon:active { cursor: grabbing; }\n.tref-icon svg { width: 100%; height: 100%; }\n.tref-actions {\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 4px;\n background: var(--tref-menu-bg);\n border-radius: 6px;\n box-shadow: var(--tref-menu-shadow);\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.15s, visibility 0.15s;\n z-index: 100;\n margin-top: 4px;\n}\n.tref-wrapper:hover .tref-actions {\n opacity: 1;\n visibility: visible;\n}\n.tref-action {\n background: transparent;\n border: none;\n outline: none;\n padding: 8px;\n border-radius: 4px;\n cursor: pointer;\n color: var(--tref-menu-text);\n transition: background 0.15s;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n.tref-action svg {\n width: 16px;\n height: 16px;\n}\n.tref-action:hover { background: var(--tref-menu-hover); }\n.tref-action:focus { outline: none; }\n.tref-action:focus-visible {\n outline: 2px solid var(--tref-accent);\n outline-offset: 1px;\n}\n.tref-icon:focus { outline: none; }\n.tref-icon:focus-visible {\n outline: 2px solid var(--tref-accent);\n outline-offset: 2px;\n border-radius: 4px;\n}\n.tref-icon-success { color: var(--tref-success); }\n.tref-icon-error { color: var(--tref-error); }\n`;\n }\n}\n\n/**\n * TrefReceiver - drop zone for TREF blocks\n */\nexport class TrefReceiver {\n /** @type {HTMLElement} */\n #element;\n /** @type {(wrapper: TrefWrapper) => void} */\n #onReceive;\n /** @type {(error: Error) => void} */\n #onError;\n /** @type {boolean} */\n #compact;\n\n /**\n * @param {HTMLElement} element\n * @param {{ onReceive?: (wrapper: TrefWrapper) => void, onError?: (error: Error) => void, compact?: boolean }} [options]\n */\n constructor(element, options = {}) {\n this.#element = element;\n this.#onReceive = options.onReceive || (() => {});\n this.#onError = options.onError || (() => {});\n this.#compact = options.compact || false;\n this.#setup();\n }\n\n #setup() {\n const el = this.#element;\n el.classList.add('tref-receiver');\n if (this.#compact) {\n el.classList.add('tref-receiver-compact');\n }\n el.setAttribute('role', 'region');\n el.setAttribute('aria-label', 'Drop zone for TREF blocks');\n el.setAttribute('aria-dropeffect', 'copy');\n\n el.addEventListener('dragover', e => {\n e.preventDefault();\n if (e.dataTransfer) {\n e.dataTransfer.dropEffect = 'copy';\n }\n el.classList.add('tref-receiver-active');\n });\n\n el.addEventListener('dragleave', () => {\n el.classList.remove('tref-receiver-active');\n });\n\n el.addEventListener('drop', e => {\n e.preventDefault();\n el.classList.remove('tref-receiver-active');\n\n if (!e.dataTransfer) {\n this.#onError(new Error('No data'));\n return;\n }\n\n const wrapper = unwrap(e.dataTransfer);\n if (wrapper) {\n el.classList.add('tref-receiver-success');\n setTimeout(() => el.classList.remove('tref-receiver-success'), 1000);\n this.#onReceive(wrapper);\n } else {\n el.classList.add('tref-receiver-error');\n setTimeout(() => el.classList.remove('tref-receiver-error'), 1000);\n this.#onError(new Error('Invalid TREF data'));\n }\n });\n }\n\n get element() {\n return this.#element;\n }\n\n /**\n * Display a block in the receiver\n * @param {TrefWrapper} wrapper\n */\n showBlock(wrapper) {\n this.#element.innerHTML = wrapper.toHTML();\n this.#element.classList.add('tref-receiver-has-block');\n }\n\n clear() {\n this.#element.innerHTML = this.#element.dataset.placeholder || 'Drop TREF here';\n this.#element.classList.remove('tref-receiver-has-block');\n }\n\n static getStyles() {\n return `\n.tref-receiver {\n border: 2px dashed var(--tref-accent);\n border-radius: 8px;\n padding: 20px;\n min-height: 80px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--tref-receiver-text);\n background: var(--tref-receiver-bg);\n transition: all 0.2s;\n}\n.tref-receiver-active {\n border-color: var(--tref-accent-hover);\n background: var(--tref-receiver-active-bg);\n color: var(--tref-accent-hover);\n}\n.tref-receiver-success {\n border-color: var(--tref-success);\n background: var(--tref-receiver-success-bg);\n}\n.tref-receiver-error {\n border-color: var(--tref-error);\n background: var(--tref-receiver-error-bg);\n}\n.tref-receiver-has-block {\n border-style: solid;\n background: var(--tref-receiver-block-bg);\n}\n.tref-receiver-compact {\n width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0;\n border-radius: 4px;\n}\n/* Touch devices - larger hit areas */\n@media (pointer: coarse) {\n .tref-icon {\n min-width: 44px;\n min-height: 44px;\n }\n .tref-action {\n min-width: 44px;\n min-height: 44px;\n padding: 10px;\n }\n .tref-receiver-compact {\n width: 48px;\n height: 48px;\n min-height: 48px;\n }\n}\n`;\n }\n}\n\n/**\n * Create wrapper from block data\n * @param {unknown} data\n * @returns {TrefWrapper}\n */\nexport function wrap(data) {\n return new TrefWrapper(/** @type {TrefBlock} */ (data));\n}\n\n/**\n * Parse TREF from DataTransfer or string\n * @param {DataTransfer | string} source\n * @returns {TrefWrapper | null}\n */\nexport function unwrap(source) {\n try {\n let json;\n if (typeof source === 'string') {\n json = source;\n } else if (source && typeof source.getData === 'function') {\n json =\n source.getData(TREF_MIME_TYPE) ||\n source.getData('application/json') ||\n source.getData('text/plain');\n } else {\n return null;\n }\n\n if (!json) {\n return null;\n }\n return wrap(JSON.parse(json));\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["TREF_EXTENSION", "TREF_ICON_SVG", "TREF_MIME_TYPE", "TREF_ICON_DATA_URL", "isValidBlock", "block", "b", "TrefWrapper", "#block", "options", "TREF_EXTENSION", "TREF_MIME_TYPE", "json", "base64", "dataTransfer", "type", "data", "interactive", "actionsHtml", "TREF_ICON_SVG", "#toggleActions", "element", "actions", "actionsEl", "isVisible", "firstBtn", "iconEl", "icon", "e", "de", "longPressTimer", "handleAction", "btn", "action", "originalHtml", "iconCheck", "iconError", "url", "a", "TrefReceiver", "#element", "#onReceive", "#onError", "#compact", "#setup", "el", "wrapper", "unwrap", "wrap", "source"]
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tref-block",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TREF - Traceable Reference Format for knowledge with origin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/tref-block.js",
|
|
7
|
+
"module": "dist/tref-block.js",
|
|
8
|
+
"browser": "dist/tref-block.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/tref-block.js",
|
|
12
|
+
"require": "./dist/tref-block.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./src": "./src/wrapper/wrapper.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/tref-block.js",
|
|
18
|
+
"dist/tref-block.js.map",
|
|
19
|
+
"dist/tref-block.cjs",
|
|
20
|
+
"src/wrapper/wrapper.js"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"typecheck": "tsc $(find src -name '*.js' ! -name '*.test.js') --noEmit --checkJs --strict --target ES2024 --module NodeNext --moduleResolution NodeNext --skipLibCheck --types node",
|
|
24
|
+
"lint": "eslint src/",
|
|
25
|
+
"lint:fix": "eslint src/ --fix",
|
|
26
|
+
"lint:css": "stylelint 'docs/**/*.css' 'src/**/*.css'",
|
|
27
|
+
"lint:css:fix": "stylelint 'docs/**/*.css' 'src/**/*.css' --fix",
|
|
28
|
+
"format": "prettier --write src/",
|
|
29
|
+
"format:check": "prettier --check src/",
|
|
30
|
+
"check": "npm run typecheck && npm run lint && npm run format:check",
|
|
31
|
+
"test": "node --test src/**/*.test.js",
|
|
32
|
+
"start": "node src/index.js",
|
|
33
|
+
"build": "npm run build:browser && npm run build:cli",
|
|
34
|
+
"build:browser": "esbuild src/wrapper/wrapper.js --bundle --format=esm --minify --sourcemap --outfile=dist/tref-block.js",
|
|
35
|
+
"build:browser:cjs": "esbuild src/wrapper/wrapper.js --bundle --format=cjs --minify --outfile=dist/tref-block.cjs",
|
|
36
|
+
"build:cli": "npm run build:cli:bundle && npm run build:cli:pkg",
|
|
37
|
+
"build:cli:bundle": "esbuild src/cli/index.js --bundle --platform=node --format=cjs --outfile=build/tref-cli.cjs && esbuild src/mcp/server.js --bundle --platform=node --format=cjs --outfile=build/tref-mcp.cjs",
|
|
38
|
+
"build:cli:pkg": "pkg build/tref-cli.cjs --targets node18-linux-x64,node18-macos-x64,node18-macos-arm64,node18-win-x64 --output dist/tref && pkg build/tref-mcp.cjs --targets node18-linux-x64,node18-macos-x64,node18-macos-arm64,node18-win-x64 --output dist/tref-mcp",
|
|
39
|
+
"dev": "python3 -m http.server 8080 --directory ."
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"tref",
|
|
43
|
+
"traceable",
|
|
44
|
+
"reference",
|
|
45
|
+
"knowledge",
|
|
46
|
+
"provenance",
|
|
47
|
+
"citation"
|
|
48
|
+
],
|
|
49
|
+
"author": "lpm",
|
|
50
|
+
"license": "AGPL-3.0-or-later",
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^9.39.2",
|
|
53
|
+
"@types/node": "^25.0.3",
|
|
54
|
+
"esbuild": "^0.27.2",
|
|
55
|
+
"eslint": "^9.39.2",
|
|
56
|
+
"pkg": "^5.8.1",
|
|
57
|
+
"prettier": "^3.7.4",
|
|
58
|
+
"stylelint": "^16.26.1",
|
|
59
|
+
"stylelint-config-standard": "^39.0.1",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"typescript-eslint": "^8.52.0"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
65
|
+
"zod": "^3.25.76"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview TREF Block wrapper for display and interaction
|
|
3
|
+
* Self-contained - no external dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* global btoa, navigator, Blob, URL, document */
|
|
7
|
+
|
|
8
|
+
/** File extension for TREF files */
|
|
9
|
+
const TREF_EXTENSION = '.tref';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} TrefBlock
|
|
13
|
+
* @property {1} v
|
|
14
|
+
* @property {string} id
|
|
15
|
+
* @property {string} content
|
|
16
|
+
* @property {{ author?: string, created: string, modified?: string, license: string, lang?: string }} meta
|
|
17
|
+
* @property {Array<{ type: string, url?: string, title?: string, snippet?: string, query?: string }>} [refs]
|
|
18
|
+
* @property {string} [parent]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SVG icon for TREF (purple-mint theme with chain link)
|
|
23
|
+
*/
|
|
24
|
+
export const TREF_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="24" height="24">
|
|
25
|
+
<rect x="6" y="6" width="88" height="88" rx="12" ry="12" fill="#2D1B4E" stroke="#5CCCCC" stroke-width="5"/>
|
|
26
|
+
<g transform="translate(50 50) scale(0.022) translate(-1125 -1125)">
|
|
27
|
+
<g transform="translate(0,2250) scale(1,-1)" fill="#5CCCCC">
|
|
28
|
+
<path d="M1515 2244 c-66 -10 -144 -38 -220 -77 -67 -35 -106 -67 -237 -195 -155 -152 -188 -195 -188 -247 0 -41 30 -95 64 -116 39 -24 113 -25 146 -3 14 9 90 81 170 160 183 181 216 199 350 199 83 0 103 -4 155 -28 78 -36 146 -104 182 -181 24 -53 28 -73 28 -151 0 -137 -21 -175 -199 -355 -79 -80 -151 -156 -160 -170 -39 -59 -8 -162 58 -194 81 -38 113 -22 284 147 165 163 230 252 268 370 24 71 28 99 28 202 0 106 -3 130 -28 200 -91 261 -310 428 -579 439 -50 3 -105 2 -122 0z"/>
|
|
29
|
+
<path d="M1395 1585 c-17 -9 -189 -174 -382 -368 -377 -378 -383 -385 -362 -461 21 -76 87 -116 166 -101 33 6 80 49 386 353 191 191 358 362 369 381 26 42 28 109 4 146 -39 59 -118 81 -181 50z"/>
|
|
30
|
+
<path d="M463 1364 c-47 -24 -323 -310 -365 -379 -20 -33 -49 -96 -64 -140 -24 -69 -28 -96 -28 -195 0 -127 14 -190 66 -294 63 -126 157 -220 284 -284 104 -52 167 -66 294 -66 99 0 126 4 195 28 44 15 107 44 140 64 65 39 348 309 371 354 41 78 -10 184 -96 203 -61 13 -98 -11 -256 -166 -186 -183 -222 -204 -359 -204 -77 0 -98 4 -147 27 -79 37 -142 98 -181 177 -29 59 -32 74 -32 156 0 136 21 174 199 355 79 80 150 156 159 170 23 33 22 107 -2 146 -35 57 -115 79 -178 48z"/>
|
|
31
|
+
</g>
|
|
32
|
+
</g>
|
|
33
|
+
</svg>`;
|
|
34
|
+
|
|
35
|
+
/** MIME type for TREF files */
|
|
36
|
+
export const TREF_MIME_TYPE = 'application/vnd.tref+json';
|
|
37
|
+
|
|
38
|
+
/** Icon as data URL for embedding */
|
|
39
|
+
export const TREF_ICON_DATA_URL = 'data:image/svg+xml,' + encodeURIComponent(TREF_ICON_SVG);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escape HTML special characters
|
|
43
|
+
* @param {string} str
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function _escapeHtml(str) {
|
|
47
|
+
return str
|
|
48
|
+
.replace(/&/g, '&')
|
|
49
|
+
.replace(/</g, '<')
|
|
50
|
+
.replace(/>/g, '>')
|
|
51
|
+
.replace(/"/g, '"');
|
|
52
|
+
}
|
|
53
|
+
// Reserved for future use (content escaping)
|
|
54
|
+
void _escapeHtml;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate block structure
|
|
58
|
+
* @param {unknown} block
|
|
59
|
+
* @returns {block is TrefBlock}
|
|
60
|
+
*/
|
|
61
|
+
function isValidBlock(block) {
|
|
62
|
+
if (!block || typeof block !== 'object') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const b = /** @type {Record<string, unknown>} */ (block);
|
|
66
|
+
if (b.v !== 1) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (typeof b.id !== 'string') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (!b.id.startsWith('sha256:')) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (typeof b.content !== 'string') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (!b.meta || typeof b.meta !== 'object') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* TrefWrapper - displays a TREF block with drag/copy/download actions
|
|
86
|
+
*
|
|
87
|
+
* Design:
|
|
88
|
+
* - Icon is the drag handle (only icon is draggable)
|
|
89
|
+
* - Hover shows action buttons (copy, download)
|
|
90
|
+
* - Status feedback in the ID badge
|
|
91
|
+
*/
|
|
92
|
+
export class TrefWrapper {
|
|
93
|
+
/** @type {TrefBlock} */
|
|
94
|
+
#block;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {TrefBlock} block
|
|
98
|
+
*/
|
|
99
|
+
constructor(block) {
|
|
100
|
+
if (!isValidBlock(block)) {
|
|
101
|
+
throw new Error('Invalid TREF block');
|
|
102
|
+
}
|
|
103
|
+
this.#block = block;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get block() {
|
|
107
|
+
return this.#block;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get id() {
|
|
111
|
+
return this.#block.id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get shortId() {
|
|
115
|
+
return this.#block.id.replace('sha256:', '').slice(0, 8);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get content() {
|
|
119
|
+
return this.#block.content;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {{ pretty?: boolean }} [options]
|
|
124
|
+
*/
|
|
125
|
+
toJSON(options = {}) {
|
|
126
|
+
return options.pretty ? JSON.stringify(this.#block, null, 2) : JSON.stringify(this.#block);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getFilename() {
|
|
130
|
+
return this.#block.id.replace('sha256:', '') + TREF_EXTENSION;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
toBlob() {
|
|
134
|
+
return new Blob([this.toJSON()], { type: TREF_MIME_TYPE });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
toDataURL() {
|
|
138
|
+
const json = this.toJSON();
|
|
139
|
+
const base64 = btoa(unescape(encodeURIComponent(json)));
|
|
140
|
+
return `data:${TREF_MIME_TYPE};base64,${base64}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
toObjectURL() {
|
|
144
|
+
return URL.createObjectURL(this.toBlob());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async copyToClipboard() {
|
|
148
|
+
await navigator.clipboard.writeText(this.toJSON());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async copyContentToClipboard() {
|
|
152
|
+
await navigator.clipboard.writeText(this.#block.content);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getDragData() {
|
|
156
|
+
const json = this.toJSON();
|
|
157
|
+
return [
|
|
158
|
+
{ type: TREF_MIME_TYPE, data: json },
|
|
159
|
+
{ type: 'application/json', data: json },
|
|
160
|
+
{ type: 'text/plain', data: json },
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {DataTransfer} dataTransfer */
|
|
165
|
+
setDragData(dataTransfer) {
|
|
166
|
+
for (const { type, data } of this.getDragData()) {
|
|
167
|
+
dataTransfer.setData(type, data);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate HTML - icon only with hover actions
|
|
173
|
+
* @param {{ interactive?: boolean }} [options]
|
|
174
|
+
*/
|
|
175
|
+
toHTML(options = {}) {
|
|
176
|
+
const { interactive = true } = options;
|
|
177
|
+
|
|
178
|
+
// SVG icons for actions
|
|
179
|
+
const iconCopy = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
180
|
+
const iconJson = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1"></path><path d="M16 3h1a2 2 0 0 1 2 2v5a2 2 0 0 0 2 2 2 2 0 0 0-2 2v5a2 2 0 0 1-2 2h-1"></path><circle cx="12" cy="12" r="1" fill="currentColor"></circle><circle cx="8" cy="12" r="1" fill="currentColor"></circle><circle cx="16" cy="12" r="1" fill="currentColor"></circle></svg>`;
|
|
181
|
+
const iconDownload = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`;
|
|
182
|
+
|
|
183
|
+
// Hover actions appear on hover
|
|
184
|
+
const actionsHtml = interactive
|
|
185
|
+
? `<div class="tref-actions" role="group" aria-label="Block actions">
|
|
186
|
+
<button class="tref-action" data-action="copy-content" title="Copy content" aria-label="Copy content to clipboard">${iconCopy}</button>
|
|
187
|
+
<button class="tref-action" data-action="copy-json" title="Copy JSON" aria-label="Copy JSON to clipboard">${iconJson}</button>
|
|
188
|
+
<button class="tref-action" data-action="download" title="Download .tref" aria-label="Download as .tref file">${iconDownload}</button>
|
|
189
|
+
</div>`
|
|
190
|
+
: '';
|
|
191
|
+
|
|
192
|
+
return `<div class="tref-wrapper" data-tref-id="${this.#block.id}">
|
|
193
|
+
<span class="tref-icon"
|
|
194
|
+
role="button"
|
|
195
|
+
aria-label="TREF block - drag to share"
|
|
196
|
+
tabindex="0"
|
|
197
|
+
draggable="true"
|
|
198
|
+
title="Drag to share">${TREF_ICON_SVG}</span>
|
|
199
|
+
${actionsHtml}
|
|
200
|
+
</div>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Toggle actions visibility (for keyboard/touch)
|
|
205
|
+
* @param {HTMLElement} element
|
|
206
|
+
*/
|
|
207
|
+
#toggleActions(element) {
|
|
208
|
+
const actions = element.querySelector('.tref-actions');
|
|
209
|
+
if (actions) {
|
|
210
|
+
const actionsEl = /** @type {HTMLElement} */ (actions);
|
|
211
|
+
const isVisible = actionsEl.style.opacity === '1';
|
|
212
|
+
actionsEl.style.opacity = isVisible ? '0' : '1';
|
|
213
|
+
if (!isVisible) {
|
|
214
|
+
// Focus first action button
|
|
215
|
+
const firstBtn = actionsEl.querySelector('button');
|
|
216
|
+
if (firstBtn) {
|
|
217
|
+
/** @type {HTMLElement} */ (firstBtn).focus();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Attach event listeners to a rendered wrapper
|
|
225
|
+
* @param {HTMLElement} element
|
|
226
|
+
*/
|
|
227
|
+
attachEvents(element) {
|
|
228
|
+
const iconEl = element.querySelector('.tref-icon');
|
|
229
|
+
|
|
230
|
+
// Icon is drag handle
|
|
231
|
+
if (iconEl) {
|
|
232
|
+
const icon = /** @type {HTMLElement} */ (iconEl);
|
|
233
|
+
|
|
234
|
+
icon.addEventListener('dragstart', e => {
|
|
235
|
+
const de = /** @type {DragEvent} */ (e);
|
|
236
|
+
if (de.dataTransfer) {
|
|
237
|
+
this.setDragData(de.dataTransfer);
|
|
238
|
+
de.dataTransfer.effectAllowed = 'copy';
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Keyboard: Enter/Space shows actions or triggers default action
|
|
243
|
+
icon.addEventListener('keydown', e => {
|
|
244
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
this.#toggleActions(element);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Touch: tap toggles actions
|
|
251
|
+
icon.addEventListener('touchend', e => {
|
|
252
|
+
// Only handle if not dragging
|
|
253
|
+
if (!icon.dataset.dragging) {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
this.#toggleActions(element);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Touch: long-press (500ms) to start drag indication
|
|
260
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
261
|
+
let longPressTimer;
|
|
262
|
+
icon.addEventListener('touchstart', () => {
|
|
263
|
+
longPressTimer = setTimeout(() => {
|
|
264
|
+
icon.dataset.dragging = 'true';
|
|
265
|
+
icon.style.transform = 'scale(1.15)';
|
|
266
|
+
}, 500);
|
|
267
|
+
});
|
|
268
|
+
icon.addEventListener('touchend', () => {
|
|
269
|
+
clearTimeout(longPressTimer);
|
|
270
|
+
delete icon.dataset.dragging;
|
|
271
|
+
icon.style.transform = '';
|
|
272
|
+
});
|
|
273
|
+
icon.addEventListener('touchcancel', () => {
|
|
274
|
+
clearTimeout(longPressTimer);
|
|
275
|
+
delete icon.dataset.dragging;
|
|
276
|
+
icon.style.transform = '';
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Action buttons (visible on hover via CSS)
|
|
281
|
+
const handleAction = async (/** @type {Event} */ e) => {
|
|
282
|
+
e.stopPropagation();
|
|
283
|
+
const btn = /** @type {HTMLElement} */ (e.currentTarget);
|
|
284
|
+
const action = btn.dataset.action;
|
|
285
|
+
const originalHtml = btn.innerHTML;
|
|
286
|
+
|
|
287
|
+
const iconCheck = `<svg class="tref-icon-success" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
|
288
|
+
const iconError = `<svg class="tref-icon-error" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
if (action === 'copy-content') {
|
|
292
|
+
await this.copyContentToClipboard();
|
|
293
|
+
btn.innerHTML = iconCheck;
|
|
294
|
+
} else if (action === 'copy-json') {
|
|
295
|
+
await this.copyToClipboard();
|
|
296
|
+
btn.innerHTML = iconCheck;
|
|
297
|
+
} else if (action === 'download') {
|
|
298
|
+
const url = this.toObjectURL();
|
|
299
|
+
const a = document.createElement('a');
|
|
300
|
+
a.href = url;
|
|
301
|
+
a.download = this.getFilename();
|
|
302
|
+
a.click();
|
|
303
|
+
URL.revokeObjectURL(url);
|
|
304
|
+
btn.innerHTML = iconCheck;
|
|
305
|
+
}
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
btn.innerHTML = originalHtml;
|
|
308
|
+
}, 1000);
|
|
309
|
+
} catch {
|
|
310
|
+
btn.innerHTML = iconError;
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
btn.innerHTML = originalHtml;
|
|
313
|
+
}, 1000);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
element.querySelectorAll('.tref-action').forEach(btn => {
|
|
318
|
+
btn.addEventListener('click', e => void handleAction(e));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
static getStyles() {
|
|
323
|
+
return `
|
|
324
|
+
:root {
|
|
325
|
+
--tref-accent: #5CCCCC;
|
|
326
|
+
--tref-accent-hover: #8B5CF6;
|
|
327
|
+
--tref-success: #10B981;
|
|
328
|
+
--tref-error: #ef4444;
|
|
329
|
+
--tref-menu-bg: #ffffff;
|
|
330
|
+
--tref-menu-text: #374151;
|
|
331
|
+
--tref-menu-hover: #f3f4f6;
|
|
332
|
+
--tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
333
|
+
--tref-receiver-bg: #f9fafb;
|
|
334
|
+
--tref-receiver-text: #6b7280;
|
|
335
|
+
--tref-receiver-active-bg: #f3e8ff;
|
|
336
|
+
--tref-receiver-success-bg: #ecfdf5;
|
|
337
|
+
--tref-receiver-error-bg: #fef2f2;
|
|
338
|
+
--tref-receiver-block-bg: #ffffff;
|
|
339
|
+
}
|
|
340
|
+
@media (prefers-color-scheme: dark) {
|
|
341
|
+
:root {
|
|
342
|
+
--tref-menu-bg: #1f2937;
|
|
343
|
+
--tref-menu-text: #e5e7eb;
|
|
344
|
+
--tref-menu-hover: #374151;
|
|
345
|
+
--tref-menu-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
346
|
+
--tref-receiver-bg: #1f2937;
|
|
347
|
+
--tref-receiver-text: #9ca3af;
|
|
348
|
+
--tref-receiver-active-bg: #3b2d5e;
|
|
349
|
+
--tref-receiver-success-bg: #064e3b;
|
|
350
|
+
--tref-receiver-error-bg: #450a0a;
|
|
351
|
+
--tref-receiver-block-bg: #111827;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
.tref-wrapper {
|
|
355
|
+
display: inline-block;
|
|
356
|
+
position: relative;
|
|
357
|
+
}
|
|
358
|
+
.tref-icon {
|
|
359
|
+
display: inline-flex;
|
|
360
|
+
width: 32px;
|
|
361
|
+
height: 32px;
|
|
362
|
+
cursor: grab;
|
|
363
|
+
transition: transform 0.15s;
|
|
364
|
+
}
|
|
365
|
+
.tref-icon:hover { transform: scale(1.1); }
|
|
366
|
+
.tref-icon:active { cursor: grabbing; }
|
|
367
|
+
.tref-icon svg { width: 100%; height: 100%; }
|
|
368
|
+
.tref-actions {
|
|
369
|
+
position: absolute;
|
|
370
|
+
top: 100%;
|
|
371
|
+
left: 50%;
|
|
372
|
+
transform: translateX(-50%);
|
|
373
|
+
display: flex;
|
|
374
|
+
align-items: center;
|
|
375
|
+
gap: 2px;
|
|
376
|
+
padding: 4px;
|
|
377
|
+
background: var(--tref-menu-bg);
|
|
378
|
+
border-radius: 6px;
|
|
379
|
+
box-shadow: var(--tref-menu-shadow);
|
|
380
|
+
opacity: 0;
|
|
381
|
+
visibility: hidden;
|
|
382
|
+
transition: opacity 0.15s, visibility 0.15s;
|
|
383
|
+
z-index: 100;
|
|
384
|
+
margin-top: 4px;
|
|
385
|
+
}
|
|
386
|
+
.tref-wrapper:hover .tref-actions {
|
|
387
|
+
opacity: 1;
|
|
388
|
+
visibility: visible;
|
|
389
|
+
}
|
|
390
|
+
.tref-action {
|
|
391
|
+
background: transparent;
|
|
392
|
+
border: none;
|
|
393
|
+
outline: none;
|
|
394
|
+
padding: 8px;
|
|
395
|
+
border-radius: 4px;
|
|
396
|
+
cursor: pointer;
|
|
397
|
+
color: var(--tref-menu-text);
|
|
398
|
+
transition: background 0.15s;
|
|
399
|
+
display: inline-flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: center;
|
|
402
|
+
}
|
|
403
|
+
.tref-action svg {
|
|
404
|
+
width: 16px;
|
|
405
|
+
height: 16px;
|
|
406
|
+
}
|
|
407
|
+
.tref-action:hover { background: var(--tref-menu-hover); }
|
|
408
|
+
.tref-action:focus { outline: none; }
|
|
409
|
+
.tref-action:focus-visible {
|
|
410
|
+
outline: 2px solid var(--tref-accent);
|
|
411
|
+
outline-offset: 1px;
|
|
412
|
+
}
|
|
413
|
+
.tref-icon:focus { outline: none; }
|
|
414
|
+
.tref-icon:focus-visible {
|
|
415
|
+
outline: 2px solid var(--tref-accent);
|
|
416
|
+
outline-offset: 2px;
|
|
417
|
+
border-radius: 4px;
|
|
418
|
+
}
|
|
419
|
+
.tref-icon-success { color: var(--tref-success); }
|
|
420
|
+
.tref-icon-error { color: var(--tref-error); }
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* TrefReceiver - drop zone for TREF blocks
|
|
427
|
+
*/
|
|
428
|
+
export class TrefReceiver {
|
|
429
|
+
/** @type {HTMLElement} */
|
|
430
|
+
#element;
|
|
431
|
+
/** @type {(wrapper: TrefWrapper) => void} */
|
|
432
|
+
#onReceive;
|
|
433
|
+
/** @type {(error: Error) => void} */
|
|
434
|
+
#onError;
|
|
435
|
+
/** @type {boolean} */
|
|
436
|
+
#compact;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @param {HTMLElement} element
|
|
440
|
+
* @param {{ onReceive?: (wrapper: TrefWrapper) => void, onError?: (error: Error) => void, compact?: boolean }} [options]
|
|
441
|
+
*/
|
|
442
|
+
constructor(element, options = {}) {
|
|
443
|
+
this.#element = element;
|
|
444
|
+
this.#onReceive = options.onReceive || (() => {});
|
|
445
|
+
this.#onError = options.onError || (() => {});
|
|
446
|
+
this.#compact = options.compact || false;
|
|
447
|
+
this.#setup();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#setup() {
|
|
451
|
+
const el = this.#element;
|
|
452
|
+
el.classList.add('tref-receiver');
|
|
453
|
+
if (this.#compact) {
|
|
454
|
+
el.classList.add('tref-receiver-compact');
|
|
455
|
+
}
|
|
456
|
+
el.setAttribute('role', 'region');
|
|
457
|
+
el.setAttribute('aria-label', 'Drop zone for TREF blocks');
|
|
458
|
+
el.setAttribute('aria-dropeffect', 'copy');
|
|
459
|
+
|
|
460
|
+
el.addEventListener('dragover', e => {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
if (e.dataTransfer) {
|
|
463
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
464
|
+
}
|
|
465
|
+
el.classList.add('tref-receiver-active');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
el.addEventListener('dragleave', () => {
|
|
469
|
+
el.classList.remove('tref-receiver-active');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
el.addEventListener('drop', e => {
|
|
473
|
+
e.preventDefault();
|
|
474
|
+
el.classList.remove('tref-receiver-active');
|
|
475
|
+
|
|
476
|
+
if (!e.dataTransfer) {
|
|
477
|
+
this.#onError(new Error('No data'));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const wrapper = unwrap(e.dataTransfer);
|
|
482
|
+
if (wrapper) {
|
|
483
|
+
el.classList.add('tref-receiver-success');
|
|
484
|
+
setTimeout(() => el.classList.remove('tref-receiver-success'), 1000);
|
|
485
|
+
this.#onReceive(wrapper);
|
|
486
|
+
} else {
|
|
487
|
+
el.classList.add('tref-receiver-error');
|
|
488
|
+
setTimeout(() => el.classList.remove('tref-receiver-error'), 1000);
|
|
489
|
+
this.#onError(new Error('Invalid TREF data'));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
get element() {
|
|
495
|
+
return this.#element;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Display a block in the receiver
|
|
500
|
+
* @param {TrefWrapper} wrapper
|
|
501
|
+
*/
|
|
502
|
+
showBlock(wrapper) {
|
|
503
|
+
this.#element.innerHTML = wrapper.toHTML();
|
|
504
|
+
this.#element.classList.add('tref-receiver-has-block');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
clear() {
|
|
508
|
+
this.#element.innerHTML = this.#element.dataset.placeholder || 'Drop TREF here';
|
|
509
|
+
this.#element.classList.remove('tref-receiver-has-block');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
static getStyles() {
|
|
513
|
+
return `
|
|
514
|
+
.tref-receiver {
|
|
515
|
+
border: 2px dashed var(--tref-accent);
|
|
516
|
+
border-radius: 8px;
|
|
517
|
+
padding: 20px;
|
|
518
|
+
min-height: 80px;
|
|
519
|
+
display: flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
justify-content: center;
|
|
522
|
+
color: var(--tref-receiver-text);
|
|
523
|
+
background: var(--tref-receiver-bg);
|
|
524
|
+
transition: all 0.2s;
|
|
525
|
+
}
|
|
526
|
+
.tref-receiver-active {
|
|
527
|
+
border-color: var(--tref-accent-hover);
|
|
528
|
+
background: var(--tref-receiver-active-bg);
|
|
529
|
+
color: var(--tref-accent-hover);
|
|
530
|
+
}
|
|
531
|
+
.tref-receiver-success {
|
|
532
|
+
border-color: var(--tref-success);
|
|
533
|
+
background: var(--tref-receiver-success-bg);
|
|
534
|
+
}
|
|
535
|
+
.tref-receiver-error {
|
|
536
|
+
border-color: var(--tref-error);
|
|
537
|
+
background: var(--tref-receiver-error-bg);
|
|
538
|
+
}
|
|
539
|
+
.tref-receiver-has-block {
|
|
540
|
+
border-style: solid;
|
|
541
|
+
background: var(--tref-receiver-block-bg);
|
|
542
|
+
}
|
|
543
|
+
.tref-receiver-compact {
|
|
544
|
+
width: 32px;
|
|
545
|
+
height: 32px;
|
|
546
|
+
min-height: 32px;
|
|
547
|
+
padding: 0;
|
|
548
|
+
border-radius: 4px;
|
|
549
|
+
}
|
|
550
|
+
/* Touch devices - larger hit areas */
|
|
551
|
+
@media (pointer: coarse) {
|
|
552
|
+
.tref-icon {
|
|
553
|
+
min-width: 44px;
|
|
554
|
+
min-height: 44px;
|
|
555
|
+
}
|
|
556
|
+
.tref-action {
|
|
557
|
+
min-width: 44px;
|
|
558
|
+
min-height: 44px;
|
|
559
|
+
padding: 10px;
|
|
560
|
+
}
|
|
561
|
+
.tref-receiver-compact {
|
|
562
|
+
width: 48px;
|
|
563
|
+
height: 48px;
|
|
564
|
+
min-height: 48px;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Create wrapper from block data
|
|
573
|
+
* @param {unknown} data
|
|
574
|
+
* @returns {TrefWrapper}
|
|
575
|
+
*/
|
|
576
|
+
export function wrap(data) {
|
|
577
|
+
return new TrefWrapper(/** @type {TrefBlock} */ (data));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Parse TREF from DataTransfer or string
|
|
582
|
+
* @param {DataTransfer | string} source
|
|
583
|
+
* @returns {TrefWrapper | null}
|
|
584
|
+
*/
|
|
585
|
+
export function unwrap(source) {
|
|
586
|
+
try {
|
|
587
|
+
let json;
|
|
588
|
+
if (typeof source === 'string') {
|
|
589
|
+
json = source;
|
|
590
|
+
} else if (source && typeof source.getData === 'function') {
|
|
591
|
+
json =
|
|
592
|
+
source.getData(TREF_MIME_TYPE) ||
|
|
593
|
+
source.getData('application/json') ||
|
|
594
|
+
source.getData('text/plain');
|
|
595
|
+
} else {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!json) {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return wrap(JSON.parse(json));
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|