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 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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\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, '&amp;')
49
+ .replace(/</g, '&lt;')
50
+ .replace(/>/g, '&gt;')
51
+ .replace(/"/g, '&quot;');
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
+ }