@synfin/widget 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 +21 -0
- package/README.md +145 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +66 -0
- package/dist/assets.d.ts +25 -0
- package/dist/assets.js +64 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +80 -0
- package/dist/element.d.ts +65 -0
- package/dist/element.js +542 -0
- package/dist/events.d.ts +16 -0
- package/dist/events.js +17 -0
- package/dist/format.d.ts +36 -0
- package/dist/format.js +57 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +21 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +60 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +163 -0
- package/dist/state.d.ts +60 -0
- package/dist/state.js +55 -0
- package/dist/styles.d.ts +9 -0
- package/dist/styles.js +226 -0
- package/dist/synfin-widget.global.js +290 -0
- package/package.json +81 -0
package/dist/styles.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The widget's styles, scoped inside the Shadow DOM. Theming is a small set of
|
|
3
|
+
* --synfin-* custom properties (which pierce the shadow boundary), defaulted to
|
|
4
|
+
* the Synfin cream/ember dark system; [appearance="light"] flips to a warm
|
|
5
|
+
* off-white. A partner overrides any of --synfin-accent / -bg / -surface / -ink
|
|
6
|
+
* / -ink-dim / -radius / -font to match their site.
|
|
7
|
+
*/
|
|
8
|
+
export const STYLES = `
|
|
9
|
+
:host {
|
|
10
|
+
--synfin-accent: #ff6a3d;
|
|
11
|
+
--synfin-accent-hi: #ff8a5c;
|
|
12
|
+
--synfin-bg: #0b0b0d;
|
|
13
|
+
--synfin-surface: #191a1e;
|
|
14
|
+
--synfin-surface-2: #202126;
|
|
15
|
+
--synfin-ink: #f1efea;
|
|
16
|
+
--synfin-ink-dim: #a8a59d;
|
|
17
|
+
--synfin-ink-faint: #8b887f;
|
|
18
|
+
--synfin-line: rgba(241, 239, 234, 0.12);
|
|
19
|
+
--synfin-radius: 16px;
|
|
20
|
+
--synfin-font: 'Geist', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
21
|
+
--synfin-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
22
|
+
--synfin-ok: #57c78a;
|
|
23
|
+
--synfin-warn: #e0b24a;
|
|
24
|
+
|
|
25
|
+
display: block;
|
|
26
|
+
max-width: 420px;
|
|
27
|
+
width: 100%;
|
|
28
|
+
color: var(--synfin-ink);
|
|
29
|
+
font-family: var(--synfin-font);
|
|
30
|
+
font-size: 14px;
|
|
31
|
+
line-height: 1.45;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
-webkit-font-smoothing: antialiased;
|
|
34
|
+
}
|
|
35
|
+
:host([appearance="light"]) {
|
|
36
|
+
--synfin-bg: #faf7f1;
|
|
37
|
+
--synfin-surface: #ffffff;
|
|
38
|
+
--synfin-surface-2: #f3efe7;
|
|
39
|
+
--synfin-ink: #1a1814;
|
|
40
|
+
--synfin-ink-dim: #5c584f;
|
|
41
|
+
--synfin-ink-faint: #868075;
|
|
42
|
+
--synfin-line: rgba(20, 18, 14, 0.12);
|
|
43
|
+
}
|
|
44
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
45
|
+
|
|
46
|
+
.card {
|
|
47
|
+
background: var(--synfin-bg);
|
|
48
|
+
border: 1px solid var(--synfin-line);
|
|
49
|
+
border-radius: var(--synfin-radius);
|
|
50
|
+
padding: 16px;
|
|
51
|
+
position: relative;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
/* Reflow on the widget's OWN width, so a narrow column on a wide viewport
|
|
54
|
+
still restacks (a viewport media query would not fire). */
|
|
55
|
+
container-type: inline-size;
|
|
56
|
+
container-name: synfinw;
|
|
57
|
+
}
|
|
58
|
+
.head { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 12px; }
|
|
59
|
+
.refresh { display: inline-flex; align-items: center; gap: 6px; font-family: var(--synfin-mono); font-size: 11px; color: var(--synfin-ink-faint); }
|
|
60
|
+
.refresh__ring { width: 13px; height: 13px; transform: rotate(-90deg); }
|
|
61
|
+
.refresh__ring circle { fill: none; stroke-width: 2; }
|
|
62
|
+
.refresh__ring .bg { stroke: var(--synfin-line); }
|
|
63
|
+
.refresh__ring .fg { stroke: var(--synfin-accent); transition: stroke-dashoffset 1s linear; }
|
|
64
|
+
.refresh button {
|
|
65
|
+
all: unset; cursor: pointer; color: var(--synfin-ink-faint);
|
|
66
|
+
display: inline-flex; padding: 3px; border-radius: 6px;
|
|
67
|
+
}
|
|
68
|
+
.refresh button:hover { color: var(--synfin-ink); }
|
|
69
|
+
.refresh button:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 1px; }
|
|
70
|
+
|
|
71
|
+
/* pair + amount */
|
|
72
|
+
.field { background: var(--synfin-surface); border: 1px solid var(--synfin-line); border-radius: 12px; padding: 12px 14px; }
|
|
73
|
+
.field + .swapwrap { position: relative; height: 0; }
|
|
74
|
+
.swap {
|
|
75
|
+
all: unset; cursor: pointer; position: absolute; left: 50%; top: -14px; transform: translateX(-50%);
|
|
76
|
+
width: 30px; height: 30px; display: grid; place-items: center;
|
|
77
|
+
background: var(--synfin-surface-2); border: 3px solid var(--synfin-bg); border-radius: 9px; color: var(--synfin-ink-dim);
|
|
78
|
+
transition: transform .15s ease, color .15s ease; z-index: 2;
|
|
79
|
+
}
|
|
80
|
+
.swap:hover { color: var(--synfin-accent); transform: translateX(-50%) rotate(180deg); }
|
|
81
|
+
.swap:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
82
|
+
.field__label { font-size: 11px; color: var(--synfin-ink-faint); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
83
|
+
.field__row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
84
|
+
.amount { all: unset; flex: 1; font-family: var(--synfin-mono); font-size: 22px; font-weight: 500; color: var(--synfin-ink); min-width: 0; border-radius: 5px; }
|
|
85
|
+
.amount::placeholder { color: var(--synfin-ink-faint); }
|
|
86
|
+
.amount:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 3px; }
|
|
87
|
+
.receive { font-family: var(--synfin-mono); font-size: 22px; font-weight: 500; color: var(--synfin-ink); }
|
|
88
|
+
.receive--dim { color: var(--synfin-ink-faint); }
|
|
89
|
+
|
|
90
|
+
.asset {
|
|
91
|
+
all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 8px;
|
|
92
|
+
background: var(--synfin-surface-2); border: 1px solid var(--synfin-line); border-radius: 999px; padding: 6px 12px 6px 6px;
|
|
93
|
+
font-weight: 600; color: var(--synfin-ink); white-space: nowrap; transition: border-color .15s ease;
|
|
94
|
+
/* never shrink below the label: the amount input (min-width:0) absorbs the
|
|
95
|
+
squeeze instead, so asset symbols never clip at narrow widths. */
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
}
|
|
98
|
+
.asset:hover { border-color: var(--synfin-accent); }
|
|
99
|
+
.asset:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
100
|
+
.asset__icon { width: 22px; height: 22px; border-radius: 50%; display: block; flex-shrink: 0; }
|
|
101
|
+
.asset__sym { white-space: nowrap; }
|
|
102
|
+
.asset__caret { color: var(--synfin-ink-faint); font-size: 10px; }
|
|
103
|
+
|
|
104
|
+
/* selector popover */
|
|
105
|
+
.pop { position: absolute; inset: 0; background: var(--synfin-bg); z-index: 5; display: flex; flex-direction: column;
|
|
106
|
+
animation: pop-in .14s ease; }
|
|
107
|
+
@keyframes pop-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
|
108
|
+
.pop__head { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--synfin-line); }
|
|
109
|
+
.pop__search { all: unset; flex: 1; color: var(--synfin-ink); font-size: 15px; }
|
|
110
|
+
.pop__search::placeholder { color: var(--synfin-ink-faint); }
|
|
111
|
+
.pop__close { all: unset; cursor: pointer; color: var(--synfin-ink-dim); padding: 4px; border-radius: 6px; }
|
|
112
|
+
.pop__close:hover { color: var(--synfin-ink); }
|
|
113
|
+
.pop__close:focus-visible { outline: 2px solid var(--synfin-accent); }
|
|
114
|
+
.pop__list { overflow-y: auto; padding: 6px; }
|
|
115
|
+
.opt { all: unset; cursor: pointer; display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; }
|
|
116
|
+
.opt:hover, .opt[aria-selected="true"] { background: var(--synfin-surface); }
|
|
117
|
+
.opt:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: -2px; }
|
|
118
|
+
.opt__name { display: flex; flex-direction: column; }
|
|
119
|
+
.opt__sym { font-weight: 600; }
|
|
120
|
+
.opt__full { font-size: 12px; color: var(--synfin-ink-faint); }
|
|
121
|
+
.opt--empty { padding: 20px; text-align: center; color: var(--synfin-ink-faint); }
|
|
122
|
+
|
|
123
|
+
/* quote panel */
|
|
124
|
+
.quote { margin-top: 14px; }
|
|
125
|
+
.you-get { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
|
|
126
|
+
.you-get__k { font-size: 12px; color: var(--synfin-ink-dim); }
|
|
127
|
+
.you-get__v { font-family: var(--synfin-mono); font-size: 20px; font-weight: 600; }
|
|
128
|
+
.venues { display: flex; flex-direction: column; gap: 6px; }
|
|
129
|
+
.venue { display: flex; align-items: center; gap: 9px; padding: 10px 12px;
|
|
130
|
+
border: 1px solid var(--synfin-line); border-radius: 11px; background: var(--synfin-surface); transition: border-color .15s ease; }
|
|
131
|
+
.venue--pick { cursor: pointer; }
|
|
132
|
+
.venue--pick:hover { border-color: var(--synfin-accent); }
|
|
133
|
+
.venue--pick:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
134
|
+
.venue--best { background: color-mix(in srgb, var(--synfin-surface) 84%, var(--synfin-accent) 16%); }
|
|
135
|
+
.venue--sel { border-color: var(--synfin-accent); }
|
|
136
|
+
.venue--out { opacity: 0.55; }
|
|
137
|
+
/* the radio affordance: outline circle that fills accent when selected */
|
|
138
|
+
.venue__radio { width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--synfin-line); flex-shrink: 0; position: relative; transition: border-color .15s ease; }
|
|
139
|
+
.venue--sel .venue__radio { border-color: var(--synfin-accent); }
|
|
140
|
+
.venue--sel .venue__radio::after { content: ""; position: absolute; inset: 2px; border-radius: 50%; background: var(--synfin-accent); }
|
|
141
|
+
.venue__id { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; text-transform: capitalize; }
|
|
142
|
+
.badge { font-family: var(--synfin-mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--synfin-accent);
|
|
143
|
+
border: 1px solid var(--synfin-accent); border-radius: 999px; padding: 1px 6px; }
|
|
144
|
+
.venue__net { font-family: var(--synfin-mono); font-weight: 600; margin-left: auto; }
|
|
145
|
+
.venue__out { font-size: 11px; color: var(--synfin-ink-faint); font-family: var(--synfin-mono); margin-left: auto; }
|
|
146
|
+
|
|
147
|
+
.fees { margin-top: 12px; border-top: 1px dashed var(--synfin-line); padding-top: 12px; display: flex; flex-direction: column; gap: 7px; }
|
|
148
|
+
.fee { display: flex; align-items: center; justify-content: space-between; font-size: 12.5px; }
|
|
149
|
+
.fee__k { color: var(--synfin-ink-dim); }
|
|
150
|
+
.fee__k .bps { font-family: var(--synfin-mono); font-size: 11px; color: var(--synfin-ink-faint); margin-left: 5px; }
|
|
151
|
+
.fee__v { font-family: var(--synfin-mono); }
|
|
152
|
+
.fee--split { margin-top: -3px; }
|
|
153
|
+
.fee--split .fee__k { font-size: 11px; color: var(--synfin-ink-faint); }
|
|
154
|
+
.fee--net { border-top: 1px solid var(--synfin-line); padding-top: 8px; margin-top: 2px; }
|
|
155
|
+
.fee--net .fee__k { color: var(--synfin-ink); font-weight: 600; font-size: 13px; }
|
|
156
|
+
.fee--net .fee__v { color: var(--synfin-ink); font-weight: 700; }
|
|
157
|
+
|
|
158
|
+
.note { margin-top: 12px; padding: 10px 12px; border-radius: 10px; font-size: 12px; line-height: 1.5;
|
|
159
|
+
background: var(--synfin-surface); border: 1px solid var(--synfin-line); color: var(--synfin-ink-dim); }
|
|
160
|
+
.note--key { border-left: 2px solid var(--synfin-accent); }
|
|
161
|
+
.note a { color: var(--synfin-accent); }
|
|
162
|
+
|
|
163
|
+
.cta { all: unset; box-sizing: border-box; cursor: pointer; display: block; text-align: center; width: 100%; margin-top: 14px;
|
|
164
|
+
padding: 13px; border-radius: 12px; font-weight: 600; background: var(--synfin-accent); color: #0b0b0d; transition: background .15s ease; }
|
|
165
|
+
.cta:hover { background: var(--synfin-accent-hi); }
|
|
166
|
+
.cta:focus-visible { outline: 2px solid var(--synfin-ink); outline-offset: 2px; }
|
|
167
|
+
/* a genuinely disabled look: muted surface, not the accent at reduced opacity */
|
|
168
|
+
.cta--disabled { background: var(--synfin-surface-2); color: var(--synfin-ink-faint); border: 1px solid var(--synfin-line); cursor: not-allowed; }
|
|
169
|
+
.cta--disabled:hover { background: var(--synfin-surface-2); }
|
|
170
|
+
.cta__help { margin: 8px 0 0; font-size: 11px; line-height: 1.5; text-align: center; color: var(--synfin-ink-faint); }
|
|
171
|
+
.cta__help code { font-family: var(--synfin-mono); font-size: 10.5px; color: var(--synfin-ink-dim); }
|
|
172
|
+
/* visible confirmation that a plan was created and handed off */
|
|
173
|
+
.plan-ok { margin-top: 14px; padding: 11px 12px; border-radius: 10px; display: flex; align-items: flex-start; gap: 8px;
|
|
174
|
+
font-size: 12.5px; line-height: 1.5; color: var(--synfin-ink);
|
|
175
|
+
background: color-mix(in srgb, var(--synfin-surface) 82%, var(--synfin-ok) 18%); border: 1px solid var(--synfin-ok); }
|
|
176
|
+
.plan-ok__icon { color: var(--synfin-ok); font-weight: 700; }
|
|
177
|
+
.plan-ok code { font-family: var(--synfin-mono); font-size: 11px; color: var(--synfin-ink-dim); }
|
|
178
|
+
|
|
179
|
+
.foot { margin-top: 12px; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 10.5px; color: var(--synfin-ink-faint); }
|
|
180
|
+
.foot__sep { opacity: 0.5; }
|
|
181
|
+
.foot__brand { color: var(--synfin-ink-faint); text-decoration: none; border-radius: 3px; }
|
|
182
|
+
.foot__brand:hover { color: var(--synfin-accent); }
|
|
183
|
+
.foot__brand:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
184
|
+
.foot a { color: var(--synfin-ink-faint); }
|
|
185
|
+
.foot a:hover { color: var(--synfin-ink-dim); }
|
|
186
|
+
|
|
187
|
+
/* states */
|
|
188
|
+
.msg { margin-top: 14px; text-align: center; padding: 26px 16px; }
|
|
189
|
+
.msg__icon { font-size: 22px; margin-bottom: 8px; }
|
|
190
|
+
.msg__title { font-weight: 600; margin-bottom: 4px; }
|
|
191
|
+
.msg__body { font-size: 12.5px; color: var(--synfin-ink-dim); }
|
|
192
|
+
.msg--error .msg__title { color: var(--synfin-warn); }
|
|
193
|
+
|
|
194
|
+
.skel { border-radius: 10px; background:
|
|
195
|
+
linear-gradient(100deg, var(--synfin-surface) 30%, var(--synfin-surface-2) 50%, var(--synfin-surface) 70%);
|
|
196
|
+
background-size: 200% 100%; animation: shimmer 1.3s ease-in-out infinite; }
|
|
197
|
+
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
198
|
+
.skel-row { height: 44px; margin-top: 6px; }
|
|
199
|
+
.skel-big { height: 26px; width: 55%; margin-bottom: 12px; }
|
|
200
|
+
|
|
201
|
+
@media (prefers-reduced-motion: reduce) {
|
|
202
|
+
.skel { animation: none; } .swap { transition: none; } .pop { animation: none; }
|
|
203
|
+
}
|
|
204
|
+
/* Narrow embeds: reflow on the widget's own width (container query), with a
|
|
205
|
+
viewport media-query fallback for engines without container support. Both
|
|
206
|
+
only shrink type and tighten spacing; the asset flex-shrink:0 above is what
|
|
207
|
+
actually stops labels clipping, so legibility holds even without either. */
|
|
208
|
+
@container synfinw (max-width: 360px) {
|
|
209
|
+
.amount, .receive, .you-get__v { font-size: 18px; }
|
|
210
|
+
.you-get__v { font-size: 17px; }
|
|
211
|
+
.card { padding: 13px; }
|
|
212
|
+
.field { padding: 11px 12px; }
|
|
213
|
+
.asset { padding: 5px 10px 5px 5px; gap: 6px; }
|
|
214
|
+
.asset__icon { width: 20px; height: 20px; }
|
|
215
|
+
.venue, .fee { font-size: 12px; }
|
|
216
|
+
}
|
|
217
|
+
@container synfinw (max-width: 320px) {
|
|
218
|
+
.amount, .receive { font-size: 16px; }
|
|
219
|
+
.field__row { gap: 6px; }
|
|
220
|
+
}
|
|
221
|
+
@media (max-width: 360px) {
|
|
222
|
+
.amount, .receive, .you-get__v { font-size: 18px; }
|
|
223
|
+
.card { padding: 13px; }
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/*! @synfin/widget 0.1.0 | MIT | https://synfin.xyz/docs/sdk */
|
|
2
|
+
"use strict";(()=>{var W=Object.defineProperty;var D=(t,n,e)=>n in t?W(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e;var d=(t,n,e)=>D(t,typeof n!="symbol"?n+"":n,e);var p=class extends Error{constructor(e,i,s){super(s);d(this,"status");d(this,"code");this.name="SynfinApiError",this.status=e,this.code=i}};function V(t){if(t!==void 0)return t;let n=globalThis.fetch;if(n===void 0)throw new Error("no global fetch available; pass { fetch } to createClient (Node 18+ and browsers have it built in)");return n}function F(t){let n=[];for(let[e,i]of Object.entries(t))i!==void 0&&n.push(`${encodeURIComponent(e)}=${encodeURIComponent(String(i))}`);return n.length>0?`?${n.join("&")}`:""}async function S(t){let n;try{n=await t.json()}catch{let e=await t.text().catch(()=>"");throw new p(t.status,"bad_response",e.length>0?e:"non-JSON response from the Synfin API")}if(!t.ok){let e=n;throw new p(t.status,e.code??"error",e.error??`HTTP ${t.status}`)}return n}var O="https://synfin.xyz";function E(t){if(t.apiKey===void 0||t.apiKey==="")throw new Error("createClient requires an apiKey");let n={baseUrl:(t.baseUrl??O).replace(/\/+$/,""),apiKey:t.apiKey,fetch:V(t.fetch)};return{getQuote:e=>j(n,e),createPlan:e=>G(n,e)}}async function j(t,n){let e=F({from:n.from,to:n.to,amount:String(n.amount),slippageBps:n.slippageBps,feeBps:n.feeBps,feeRecipient:n.feeRecipient}),i=await t.fetch(`${t.baseUrl}/api/quote${e}`,{method:"GET",headers:{"x-api-key":t.apiKey}});return S(i)}async function G(t,n){let e={from:n.from,to:n.to,amount:String(n.amount),venueId:n.venueId,takerParty:n.takerParty,idempotencyKey:n.idempotencyKey};n.slippageBps!==void 0&&(e.slippageBps=n.slippageBps),n.feeBps!==void 0&&(e.feeBps=n.feeBps),n.feeRecipient!==void 0&&(e.feeRecipient=n.feeRecipient);let i=await t.fetch(`${t.baseUrl}/api/execute/plan`,{method:"POST",headers:{"x-api-key":t.apiKey,"content-type":"application/json"},body:JSON.stringify(e)});return S(i)}function Y(t){let n=[];for(let[e,i]of Object.entries(t))i!==void 0&&n.push(`${encodeURIComponent(e)}=${encodeURIComponent(String(i))}`);return n.length>0?`?${n.join("&")}`:""}function J(t){if(t!==void 0)return t;let n=globalThis.fetch;if(n===void 0)throw new p(0,"network","no fetch available in this runtime");return n}async function P(t,n){let e=Y({from:n.from,to:n.to,amount:String(n.amount),slippageBps:n.slippageBps,feeBps:n.feeBps,feeRecipient:n.feeRecipient}),i={};t.apiKey!==null&&t.apiKey!==""&&(i["x-api-key"]=t.apiKey);let s=await J(t.fetch)(`${t.baseUrl}/api/quote${e}`,{method:"GET",headers:i}),o;try{o=await s.json()}catch{let r=await s.text().catch(()=>"");throw new p(s.status,"bad_response",r.length>0?r:"non-JSON response from the Synfin API")}if(!s.ok){let r=o;throw new p(s.status,r.code??"error",r.error??`HTTP ${s.status}`)}return o}var y=[{symbol:"CC",name:"Canton Coin",instrumentId:"Amulet",decimals:10},{symbol:"USDCx",name:"USDC (interchain)",instrumentId:"USDCx",decimals:6},{symbol:"CBTC",name:"Canton BTC",instrumentId:"CBTC",decimals:8}];function x(t){let n=t.trim().toLowerCase();return y.find(e=>e.symbol.toLowerCase()===n)}function w(t){let n=t.trim(),e=n.toLowerCase(),i=y.find(o=>o.instrumentId.toLowerCase()===e);if(i)return i.symbol;let s=y.find(o=>o.symbol.toLowerCase()===e);return s?s.symbol:n}var X={CC:{label:"CC",hue:20},AMULET:{label:"CC",hue:20},USDCX:{label:"USDC",hue:212},CBTC:{label:"BTC",hue:33},CETH:{label:"ETH",hue:250},CUSD:{label:"USD",hue:150}};function Z(t){let n=0;for(let e=0;e<t.length;e++)n=n*31+t.charCodeAt(e)>>>0;return n%360}function R(t){let n=w(t).toUpperCase(),e=X[n]??{label:n.slice(0,3),hue:Z(n)},i=(e.hue+24)%360,s=`sfic${e.hue}`,o=e.label.length>=4?6.5:e.label.length===3?8:9.5;return`<svg class="asset__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><defs><linearGradient id="${s}" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${e.hue} 78% 58%)"></stop><stop offset="1" stop-color="hsl(${i} 72% 44%)"></stop></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#${s})"></circle><text x="12" y="12" dy="0.35em" text-anchor="middle" font-size="${o}" font-weight="700" fill="#fff">${e.label}</text></svg>`}function A(t){let n=t.trim().toLowerCase();return n===""?y:y.filter(e=>e.symbol.toLowerCase().includes(n)||e.name.toLowerCase().includes(n))}function g(t,n=6){if(t===null)return"-";if(!/^-?\d+(\.\d+)?$/.test(t))return t;let[e="0",i=""]=t.split(".");if(i==="")return e;let s=i.slice(0,n).replace(/0+$/,"");return s===""?e:`${e}.${s}`}function ee(t,n){let e=/^\d+(\.\d+)?$/;if(!e.test(t)||!e.test(n))return t;let i=_=>{let[K="0",Q=""]=_.split(".");return[K,Q]},[s,o]=i(t),[r,a]=i(n),l=Math.max(o.length,a.length),u=(BigInt(s+o.padEnd(l,"0"))+BigInt(r+a.padEnd(l,"0"))).toString().padStart(l+1,"0"),h=u.slice(0,u.length-l),m=l>0?u.slice(u.length-l):"";return m===""?h:`${h}.${m}`}function B(t){return{service:{label:"Synfin service",bps:t.service.bps,amount:t.service.amount},integrator:t.integrator===null?null:{label:"Integrator fee",bps:t.integrator.bps,amount:t.integrator.amount,partnerShare:t.integrator.partnerShare,synfinShare:t.integrator.synfinShare},userReceives:t.userReceives,total:t.integrator===null?t.service.amount:ee(t.service.amount,t.integrator.amount)}}var I=100,te=15e3;function C(t){let n=[],e=(t.from??"CC").trim(),i=(t.to??"USDCx").trim();x(e)===void 0&&n.push({field:"from",message:`unknown asset "${e}"`}),x(i)===void 0&&n.push({field:"to",message:`unknown asset "${i}"`}),e.toLowerCase()===i.toLowerCase()&&n.push({field:"to",message:"from and to must differ"});let s=(t.amount??"100").trim();(!/^\d+(\.\d+)?$/.test(s)||Number(s)<=0)&&n.push({field:"amount",message:"amount must be a positive number"});let o=null,r=null;if(t.feeBps!==void 0||t.feeRecipient!==void 0)if(t.feeBps===void 0||t.feeRecipient===void 0)n.push({field:"fee",message:"feeBps and feeRecipient must be set together"});else{let m=Number(t.feeBps);!Number.isInteger(m)||m<0||m>I?n.push({field:"feeBps",message:`feeBps must be an integer 0 to ${I}`}):o=m;let _=String(t.feeRecipient).trim();_===""?n.push({field:"feeRecipient",message:"feeRecipient required"}):r=_}let l=t.appearance==="light"?"light":"dark",f=(t.baseUrl??"https://synfin.xyz").replace(/\/+$/,""),u=typeof t.refreshMs=="number"&&t.refreshMs>=3e3?t.refreshMs:te,h=(t.apiKey??"").trim();return{config:{apiKey:h===""?null:h,feeBps:o,feeRecipient:r,from:e,to:i,amount:s,appearance:l,baseUrl:f,refreshMs:u,showFeeBreakdown:t.showFeeBreakdown===!0,showAttribution:t.showAttribution!==!1},issues:n}}var v={quote:"synfin:quote",asset:"synfin:asset",venue:"synfin:venue",plan:"synfin:plan",status:"synfin:status",error:"synfin:error"};function b(t,n,e){t.dispatchEvent(new CustomEvent(n,{detail:e,bubbles:!0,composed:!0}))}function $(t,n){let e=t.venues.filter(i=>i.available);return e.find(i=>i.venueId===n)??t.best??e[0]??null}function c(t){return t.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n]??n)}function z(t){return t.charAt(0).toUpperCase()+t.slice(1)}function N(t,n,e=null,i=null){switch(t.kind){case"idle":return"";case"loading":return ne();case"invalid":return L("!",t.title,c(t.message),"");case"error":return L("!","Could not load a quote",c(t.message),"error");case"no-route":return L("\u2205","No route right now",`No venue can fill ${c(t.pair.give)} to ${c(t.pair.want)} at this size. Try a different amount or pair.`,"");case"quoted":return ie(t,n,e,i)}}function ne(){return`<div class="quote" aria-busy="true" aria-label="Loading a quote">
|
|
3
|
+
<div class="skel skel-big"></div>
|
|
4
|
+
<div class="skel skel-row"></div>
|
|
5
|
+
<div class="skel skel-row"></div>
|
|
6
|
+
<div class="skel skel-row"></div>
|
|
7
|
+
</div>`}function L(t,n,e,i){return`<div class="msg ${i==="error"?"msg--error":""}" role="status">
|
|
8
|
+
<div class="msg__icon" aria-hidden="true">${c(t)}</div>
|
|
9
|
+
<div class="msg__title">${n}</div>
|
|
10
|
+
<div class="msg__body">${e}</div>
|
|
11
|
+
</div>`}function ie(t,n,e,i){let s=$(t,e),o=s?.venueId??null,r=s?.fees?s.fees.userReceives:s?.net??null,a=t.venues.map(u=>re(u,t.pair.want,u.venueId===o)).join(""),l=s?.fees?n.showFeeBreakdown?le(s.fees,t.pair.want):ae(s.fees,t.pair.want):"",f=n.showFeeBreakdown&&!t.keyed?'<div class="note note--key">Add your API key to see the fee breakdown (the service fee and your integrator fee). Keyless quotes show the venue net only.</div>':"";return`<div class="quote">
|
|
12
|
+
<div class="you-get">
|
|
13
|
+
<span class="you-get__k">You receive</span>
|
|
14
|
+
<span class="you-get__v">${g(r)} ${c(t.pair.want)}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="venues" role="radiogroup" aria-label="Choose a venue, best net receive first">${a}</div>
|
|
17
|
+
${l}${f}${ce(s,n,i)}
|
|
18
|
+
<div class="foot">Non-custodial${n.showAttribution?' <span class="foot__sep" aria-hidden="true">|</span> <a class="foot__brand" href="https://synfin.xyz" target="_blank" rel="noopener">Powered by Synfin</a>':", fees included"}</div>
|
|
19
|
+
</div>`}var se={maintenance:"under maintenance",no_liquidity:"no liquidity at this size",insufficient_liquidity:"no liquidity at this size",invalid_request:"unavailable",pair_unsupported:"pair not supported",venue_error:"temporarily unavailable",timeout:"did not respond in time",rate_limited:"temporarily unavailable"};function oe(t){return(t!==null?se[t]:void 0)??"unavailable"}function re(t,n,e){return t.available?`<div class="venue venue--pick ${t.isBest?"venue--best":""} ${e?"venue--sel":""}" role="radio" aria-checked="${e?"true":"false"}" tabindex="${e?"0":"-1"}" data-venue="${c(t.venueId)}">
|
|
20
|
+
<span class="venue__radio" aria-hidden="true"></span>
|
|
21
|
+
<span class="venue__id">${c(z(t.venueId))}${t.isBest?'<span class="badge">best</span>':""}</span>
|
|
22
|
+
<span class="venue__net">${g(t.net)} ${c(n)}</span>
|
|
23
|
+
</div>`:`<div class="venue venue--out" role="presentation">
|
|
24
|
+
<span class="venue__id">${c(z(t.venueId))}</span>
|
|
25
|
+
<span class="venue__out">${c(oe(t.rejectionCode))}</span>
|
|
26
|
+
</div>`}function M(t,n,e,i,s=""){return`<div class="fee ${s}">
|
|
27
|
+
<span class="fee__k">${c(t)}<span class="bps">${n} bps</span></span>
|
|
28
|
+
<span class="fee__v">${g(e)} ${c(i)}</span>
|
|
29
|
+
</div>`}function ae(t,n){return`<div class="fees fees--simple">
|
|
30
|
+
<div class="fee">
|
|
31
|
+
<span class="fee__k">Fee</span>
|
|
32
|
+
<span class="fee__v">${g(t.total)} ${c(n)}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>`}function le(t,n){let e=t.integrator===null?"":M(t.integrator.label,t.integrator.bps,t.integrator.amount,n)+`<div class="fee fee--split"><span class="fee__k">partner ${g(t.integrator.partnerShare)} ${c(n)}, Synfin ${g(t.integrator.synfinShare)} ${c(n)}</span></div>`;return`<div class="fees">
|
|
35
|
+
${M(t.service.label,t.service.bps,t.service.amount,n)}
|
|
36
|
+
${e}
|
|
37
|
+
<div class="fee fee--net">
|
|
38
|
+
<span class="fee__k">You receive</span>
|
|
39
|
+
<span class="fee__v">${g(t.userReceives)} ${c(n)}</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>`}var T='<p class="cta__help">On create, the widget emits <code>synfin:plan</code> for your app to execute via its WalletAdapter. In-widget wallet execution is coming.</p>';function ce(t,n,e){if(t===null||!t.available)return"";if(n.apiKey===null)return`<button class="cta cta--disabled" disabled aria-disabled="true" aria-describedby="synfin-cta-help">Create a plan</button>
|
|
42
|
+
<p class="cta__help" id="synfin-cta-help">Add an API key to create a plan.</p>`;if(e?.status==="creating")return`<button class="cta cta--disabled" disabled aria-disabled="true">Creating plan...</button>${T}`;if(e?.status==="created"){let i=e.planId?e.planId.slice(0,14):"";return`<div class="plan-ok" role="status"><span class="plan-ok__icon" aria-hidden="true">✓</span> Plan created${i?` <code>${c(i)}</code>`:""}, handed off to your app on <code>synfin:plan</code> (venue ${c(z(t.venueId))}).</div>${T}<button class="cta cta--ghost" data-action="plan">Create a new plan</button>`}return e?.status==="error"?`<div class="msg msg--error" role="status"><div class="msg__title">Could not create the plan</div><div class="msg__body">${c(e.message??"Please try again.")}</div></div><button class="cta" data-action="plan">Try again</button>`:`<button class="cta" data-action="plan">Create a plan</button>${T}`}var U=`
|
|
43
|
+
:host {
|
|
44
|
+
--synfin-accent: #ff6a3d;
|
|
45
|
+
--synfin-accent-hi: #ff8a5c;
|
|
46
|
+
--synfin-bg: #0b0b0d;
|
|
47
|
+
--synfin-surface: #191a1e;
|
|
48
|
+
--synfin-surface-2: #202126;
|
|
49
|
+
--synfin-ink: #f1efea;
|
|
50
|
+
--synfin-ink-dim: #a8a59d;
|
|
51
|
+
--synfin-ink-faint: #8b887f;
|
|
52
|
+
--synfin-line: rgba(241, 239, 234, 0.12);
|
|
53
|
+
--synfin-radius: 16px;
|
|
54
|
+
--synfin-font: 'Geist', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
55
|
+
--synfin-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
56
|
+
--synfin-ok: #57c78a;
|
|
57
|
+
--synfin-warn: #e0b24a;
|
|
58
|
+
|
|
59
|
+
display: block;
|
|
60
|
+
max-width: 420px;
|
|
61
|
+
width: 100%;
|
|
62
|
+
color: var(--synfin-ink);
|
|
63
|
+
font-family: var(--synfin-font);
|
|
64
|
+
font-size: 14px;
|
|
65
|
+
line-height: 1.45;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
-webkit-font-smoothing: antialiased;
|
|
68
|
+
}
|
|
69
|
+
:host([appearance="light"]) {
|
|
70
|
+
--synfin-bg: #faf7f1;
|
|
71
|
+
--synfin-surface: #ffffff;
|
|
72
|
+
--synfin-surface-2: #f3efe7;
|
|
73
|
+
--synfin-ink: #1a1814;
|
|
74
|
+
--synfin-ink-dim: #5c584f;
|
|
75
|
+
--synfin-ink-faint: #868075;
|
|
76
|
+
--synfin-line: rgba(20, 18, 14, 0.12);
|
|
77
|
+
}
|
|
78
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
79
|
+
|
|
80
|
+
.card {
|
|
81
|
+
background: var(--synfin-bg);
|
|
82
|
+
border: 1px solid var(--synfin-line);
|
|
83
|
+
border-radius: var(--synfin-radius);
|
|
84
|
+
padding: 16px;
|
|
85
|
+
position: relative;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
/* Reflow on the widget's OWN width, so a narrow column on a wide viewport
|
|
88
|
+
still restacks (a viewport media query would not fire). */
|
|
89
|
+
container-type: inline-size;
|
|
90
|
+
container-name: synfinw;
|
|
91
|
+
}
|
|
92
|
+
.head { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 12px; }
|
|
93
|
+
.refresh { display: inline-flex; align-items: center; gap: 6px; font-family: var(--synfin-mono); font-size: 11px; color: var(--synfin-ink-faint); }
|
|
94
|
+
.refresh__ring { width: 13px; height: 13px; transform: rotate(-90deg); }
|
|
95
|
+
.refresh__ring circle { fill: none; stroke-width: 2; }
|
|
96
|
+
.refresh__ring .bg { stroke: var(--synfin-line); }
|
|
97
|
+
.refresh__ring .fg { stroke: var(--synfin-accent); transition: stroke-dashoffset 1s linear; }
|
|
98
|
+
.refresh button {
|
|
99
|
+
all: unset; cursor: pointer; color: var(--synfin-ink-faint);
|
|
100
|
+
display: inline-flex; padding: 3px; border-radius: 6px;
|
|
101
|
+
}
|
|
102
|
+
.refresh button:hover { color: var(--synfin-ink); }
|
|
103
|
+
.refresh button:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 1px; }
|
|
104
|
+
|
|
105
|
+
/* pair + amount */
|
|
106
|
+
.field { background: var(--synfin-surface); border: 1px solid var(--synfin-line); border-radius: 12px; padding: 12px 14px; }
|
|
107
|
+
.field + .swapwrap { position: relative; height: 0; }
|
|
108
|
+
.swap {
|
|
109
|
+
all: unset; cursor: pointer; position: absolute; left: 50%; top: -14px; transform: translateX(-50%);
|
|
110
|
+
width: 30px; height: 30px; display: grid; place-items: center;
|
|
111
|
+
background: var(--synfin-surface-2); border: 3px solid var(--synfin-bg); border-radius: 9px; color: var(--synfin-ink-dim);
|
|
112
|
+
transition: transform .15s ease, color .15s ease; z-index: 2;
|
|
113
|
+
}
|
|
114
|
+
.swap:hover { color: var(--synfin-accent); transform: translateX(-50%) rotate(180deg); }
|
|
115
|
+
.swap:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
116
|
+
.field__label { font-size: 11px; color: var(--synfin-ink-faint); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
117
|
+
.field__row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
118
|
+
.amount { all: unset; flex: 1; font-family: var(--synfin-mono); font-size: 22px; font-weight: 500; color: var(--synfin-ink); min-width: 0; border-radius: 5px; }
|
|
119
|
+
.amount::placeholder { color: var(--synfin-ink-faint); }
|
|
120
|
+
.amount:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 3px; }
|
|
121
|
+
.receive { font-family: var(--synfin-mono); font-size: 22px; font-weight: 500; color: var(--synfin-ink); }
|
|
122
|
+
.receive--dim { color: var(--synfin-ink-faint); }
|
|
123
|
+
|
|
124
|
+
.asset {
|
|
125
|
+
all: unset; cursor: pointer; display: inline-flex; align-items: center; gap: 8px;
|
|
126
|
+
background: var(--synfin-surface-2); border: 1px solid var(--synfin-line); border-radius: 999px; padding: 6px 12px 6px 6px;
|
|
127
|
+
font-weight: 600; color: var(--synfin-ink); white-space: nowrap; transition: border-color .15s ease;
|
|
128
|
+
/* never shrink below the label: the amount input (min-width:0) absorbs the
|
|
129
|
+
squeeze instead, so asset symbols never clip at narrow widths. */
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
.asset:hover { border-color: var(--synfin-accent); }
|
|
133
|
+
.asset:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
134
|
+
.asset__icon { width: 22px; height: 22px; border-radius: 50%; display: block; flex-shrink: 0; }
|
|
135
|
+
.asset__sym { white-space: nowrap; }
|
|
136
|
+
.asset__caret { color: var(--synfin-ink-faint); font-size: 10px; }
|
|
137
|
+
|
|
138
|
+
/* selector popover */
|
|
139
|
+
.pop { position: absolute; inset: 0; background: var(--synfin-bg); z-index: 5; display: flex; flex-direction: column;
|
|
140
|
+
animation: pop-in .14s ease; }
|
|
141
|
+
@keyframes pop-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
|
142
|
+
.pop__head { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--synfin-line); }
|
|
143
|
+
.pop__search { all: unset; flex: 1; color: var(--synfin-ink); font-size: 15px; }
|
|
144
|
+
.pop__search::placeholder { color: var(--synfin-ink-faint); }
|
|
145
|
+
.pop__close { all: unset; cursor: pointer; color: var(--synfin-ink-dim); padding: 4px; border-radius: 6px; }
|
|
146
|
+
.pop__close:hover { color: var(--synfin-ink); }
|
|
147
|
+
.pop__close:focus-visible { outline: 2px solid var(--synfin-accent); }
|
|
148
|
+
.pop__list { overflow-y: auto; padding: 6px; }
|
|
149
|
+
.opt { all: unset; cursor: pointer; display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; }
|
|
150
|
+
.opt:hover, .opt[aria-selected="true"] { background: var(--synfin-surface); }
|
|
151
|
+
.opt:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: -2px; }
|
|
152
|
+
.opt__name { display: flex; flex-direction: column; }
|
|
153
|
+
.opt__sym { font-weight: 600; }
|
|
154
|
+
.opt__full { font-size: 12px; color: var(--synfin-ink-faint); }
|
|
155
|
+
.opt--empty { padding: 20px; text-align: center; color: var(--synfin-ink-faint); }
|
|
156
|
+
|
|
157
|
+
/* quote panel */
|
|
158
|
+
.quote { margin-top: 14px; }
|
|
159
|
+
.you-get { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
|
|
160
|
+
.you-get__k { font-size: 12px; color: var(--synfin-ink-dim); }
|
|
161
|
+
.you-get__v { font-family: var(--synfin-mono); font-size: 20px; font-weight: 600; }
|
|
162
|
+
.venues { display: flex; flex-direction: column; gap: 6px; }
|
|
163
|
+
.venue { display: flex; align-items: center; gap: 9px; padding: 10px 12px;
|
|
164
|
+
border: 1px solid var(--synfin-line); border-radius: 11px; background: var(--synfin-surface); transition: border-color .15s ease; }
|
|
165
|
+
.venue--pick { cursor: pointer; }
|
|
166
|
+
.venue--pick:hover { border-color: var(--synfin-accent); }
|
|
167
|
+
.venue--pick:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
168
|
+
.venue--best { background: color-mix(in srgb, var(--synfin-surface) 84%, var(--synfin-accent) 16%); }
|
|
169
|
+
.venue--sel { border-color: var(--synfin-accent); }
|
|
170
|
+
.venue--out { opacity: 0.55; }
|
|
171
|
+
/* the radio affordance: outline circle that fills accent when selected */
|
|
172
|
+
.venue__radio { width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--synfin-line); flex-shrink: 0; position: relative; transition: border-color .15s ease; }
|
|
173
|
+
.venue--sel .venue__radio { border-color: var(--synfin-accent); }
|
|
174
|
+
.venue--sel .venue__radio::after { content: ""; position: absolute; inset: 2px; border-radius: 50%; background: var(--synfin-accent); }
|
|
175
|
+
.venue__id { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; text-transform: capitalize; }
|
|
176
|
+
.badge { font-family: var(--synfin-mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--synfin-accent);
|
|
177
|
+
border: 1px solid var(--synfin-accent); border-radius: 999px; padding: 1px 6px; }
|
|
178
|
+
.venue__net { font-family: var(--synfin-mono); font-weight: 600; margin-left: auto; }
|
|
179
|
+
.venue__out { font-size: 11px; color: var(--synfin-ink-faint); font-family: var(--synfin-mono); margin-left: auto; }
|
|
180
|
+
|
|
181
|
+
.fees { margin-top: 12px; border-top: 1px dashed var(--synfin-line); padding-top: 12px; display: flex; flex-direction: column; gap: 7px; }
|
|
182
|
+
.fee { display: flex; align-items: center; justify-content: space-between; font-size: 12.5px; }
|
|
183
|
+
.fee__k { color: var(--synfin-ink-dim); }
|
|
184
|
+
.fee__k .bps { font-family: var(--synfin-mono); font-size: 11px; color: var(--synfin-ink-faint); margin-left: 5px; }
|
|
185
|
+
.fee__v { font-family: var(--synfin-mono); }
|
|
186
|
+
.fee--split { margin-top: -3px; }
|
|
187
|
+
.fee--split .fee__k { font-size: 11px; color: var(--synfin-ink-faint); }
|
|
188
|
+
.fee--net { border-top: 1px solid var(--synfin-line); padding-top: 8px; margin-top: 2px; }
|
|
189
|
+
.fee--net .fee__k { color: var(--synfin-ink); font-weight: 600; font-size: 13px; }
|
|
190
|
+
.fee--net .fee__v { color: var(--synfin-ink); font-weight: 700; }
|
|
191
|
+
|
|
192
|
+
.note { margin-top: 12px; padding: 10px 12px; border-radius: 10px; font-size: 12px; line-height: 1.5;
|
|
193
|
+
background: var(--synfin-surface); border: 1px solid var(--synfin-line); color: var(--synfin-ink-dim); }
|
|
194
|
+
.note--key { border-left: 2px solid var(--synfin-accent); }
|
|
195
|
+
.note a { color: var(--synfin-accent); }
|
|
196
|
+
|
|
197
|
+
.cta { all: unset; box-sizing: border-box; cursor: pointer; display: block; text-align: center; width: 100%; margin-top: 14px;
|
|
198
|
+
padding: 13px; border-radius: 12px; font-weight: 600; background: var(--synfin-accent); color: #0b0b0d; transition: background .15s ease; }
|
|
199
|
+
.cta:hover { background: var(--synfin-accent-hi); }
|
|
200
|
+
.cta:focus-visible { outline: 2px solid var(--synfin-ink); outline-offset: 2px; }
|
|
201
|
+
/* a genuinely disabled look: muted surface, not the accent at reduced opacity */
|
|
202
|
+
.cta--disabled { background: var(--synfin-surface-2); color: var(--synfin-ink-faint); border: 1px solid var(--synfin-line); cursor: not-allowed; }
|
|
203
|
+
.cta--disabled:hover { background: var(--synfin-surface-2); }
|
|
204
|
+
.cta__help { margin: 8px 0 0; font-size: 11px; line-height: 1.5; text-align: center; color: var(--synfin-ink-faint); }
|
|
205
|
+
.cta__help code { font-family: var(--synfin-mono); font-size: 10.5px; color: var(--synfin-ink-dim); }
|
|
206
|
+
/* visible confirmation that a plan was created and handed off */
|
|
207
|
+
.plan-ok { margin-top: 14px; padding: 11px 12px; border-radius: 10px; display: flex; align-items: flex-start; gap: 8px;
|
|
208
|
+
font-size: 12.5px; line-height: 1.5; color: var(--synfin-ink);
|
|
209
|
+
background: color-mix(in srgb, var(--synfin-surface) 82%, var(--synfin-ok) 18%); border: 1px solid var(--synfin-ok); }
|
|
210
|
+
.plan-ok__icon { color: var(--synfin-ok); font-weight: 700; }
|
|
211
|
+
.plan-ok code { font-family: var(--synfin-mono); font-size: 11px; color: var(--synfin-ink-dim); }
|
|
212
|
+
|
|
213
|
+
.foot { margin-top: 12px; display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 10.5px; color: var(--synfin-ink-faint); }
|
|
214
|
+
.foot__sep { opacity: 0.5; }
|
|
215
|
+
.foot__brand { color: var(--synfin-ink-faint); text-decoration: none; border-radius: 3px; }
|
|
216
|
+
.foot__brand:hover { color: var(--synfin-accent); }
|
|
217
|
+
.foot__brand:focus-visible { outline: 2px solid var(--synfin-accent); outline-offset: 2px; }
|
|
218
|
+
.foot a { color: var(--synfin-ink-faint); }
|
|
219
|
+
.foot a:hover { color: var(--synfin-ink-dim); }
|
|
220
|
+
|
|
221
|
+
/* states */
|
|
222
|
+
.msg { margin-top: 14px; text-align: center; padding: 26px 16px; }
|
|
223
|
+
.msg__icon { font-size: 22px; margin-bottom: 8px; }
|
|
224
|
+
.msg__title { font-weight: 600; margin-bottom: 4px; }
|
|
225
|
+
.msg__body { font-size: 12.5px; color: var(--synfin-ink-dim); }
|
|
226
|
+
.msg--error .msg__title { color: var(--synfin-warn); }
|
|
227
|
+
|
|
228
|
+
.skel { border-radius: 10px; background:
|
|
229
|
+
linear-gradient(100deg, var(--synfin-surface) 30%, var(--synfin-surface-2) 50%, var(--synfin-surface) 70%);
|
|
230
|
+
background-size: 200% 100%; animation: shimmer 1.3s ease-in-out infinite; }
|
|
231
|
+
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
232
|
+
.skel-row { height: 44px; margin-top: 6px; }
|
|
233
|
+
.skel-big { height: 26px; width: 55%; margin-bottom: 12px; }
|
|
234
|
+
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
.skel { animation: none; } .swap { transition: none; } .pop { animation: none; }
|
|
237
|
+
}
|
|
238
|
+
/* Narrow embeds: reflow on the widget's own width (container query), with a
|
|
239
|
+
viewport media-query fallback for engines without container support. Both
|
|
240
|
+
only shrink type and tighten spacing; the asset flex-shrink:0 above is what
|
|
241
|
+
actually stops labels clipping, so legibility holds even without either. */
|
|
242
|
+
@container synfinw (max-width: 360px) {
|
|
243
|
+
.amount, .receive, .you-get__v { font-size: 18px; }
|
|
244
|
+
.you-get__v { font-size: 17px; }
|
|
245
|
+
.card { padding: 13px; }
|
|
246
|
+
.field { padding: 11px 12px; }
|
|
247
|
+
.asset { padding: 5px 10px 5px 5px; gap: 6px; }
|
|
248
|
+
.asset__icon { width: 20px; height: 20px; }
|
|
249
|
+
.venue, .fee { font-size: 12px; }
|
|
250
|
+
}
|
|
251
|
+
@container synfinw (max-width: 320px) {
|
|
252
|
+
.amount, .receive { font-size: 16px; }
|
|
253
|
+
.field__row { gap: 6px; }
|
|
254
|
+
}
|
|
255
|
+
@media (max-width: 360px) {
|
|
256
|
+
.amount, .receive, .you-get__v { font-size: 18px; }
|
|
257
|
+
.card { padding: 13px; }
|
|
258
|
+
}
|
|
259
|
+
`;function H(t){return{...t,pair:{give:w(t.pair.give),want:w(t.pair.want)},venues:t.venues.map(n=>n.fee?{...n,fee:{...n.fee,asset:w(n.fee.asset)}}:n)}}function q(t){let n={give:w(t.pair.give),want:w(t.pair.want)},e=t.venues.filter(a=>a.available&&a.net!==null);if(e.length===0)return{kind:"no-route",pair:n};let i=t.best?.venueId??null,s=e.some(a=>a.clientFees!==void 0),o=t.venues.map(a=>({venueId:a.venueId,available:a.available&&a.net!==null,net:a.net,rejectionCode:a.rejectionCode,isBest:a.venueId===i,fees:a.clientFees!==void 0?B(a.clientFees):null})),r=o.find(a=>a.isBest)??null;return{kind:"quoted",pair:n,amount:t.amount,asOf:t.asOf,best:r,venues:o,keyed:s,edgeBps:t.edgeBps}}var de=["api-key","fee-bps","fee-recipient","from","to","amount","appearance","base-url","refresh-ms","show-fee-breakdown","show-attribution"],fe=400,k=class extends HTMLElement{constructor(){super(...arguments);d(this,"fetchImpl");d(this,"cfg",C({}).config);d(this,"client",null);d(this,"view",{kind:"idle"});d(this,"debounce",0);d(this,"ticker",0);d(this,"countdown",0);d(this,"reqSeq",0);d(this,"selectorOpen",null);d(this,"built",!1);d(this,"syncing",!1);d(this,"selectedVenue",null);d(this,"plan",null)}static get observedAttributes(){return de}connectedCallback(){this.build(),this.reconfigure()}disconnectedCallback(){clearTimeout(this.debounce),clearInterval(this.ticker)}attributeChangedCallback(){this.built&&!this.syncing&&this.reconfigure()}configure(e){for(let[i,s]of Object.entries(e)){let o=i.replace(/[A-Z]/g,r=>`-${r.toLowerCase()}`);s==null?this.removeAttribute(o):this.setAttribute(o,String(s))}}reconfigure(){let e=o=>this.getAttribute(o),i={...e("api-key")!==null?{apiKey:e("api-key")}:{},...e("fee-bps")!==null?{feeBps:Number(e("fee-bps"))}:{},...e("fee-recipient")!==null?{feeRecipient:e("fee-recipient")}:{},...e("from")!==null?{from:e("from")}:{},...e("to")!==null?{to:e("to")}:{},...e("amount")!==null?{amount:e("amount")}:{},...e("appearance")==="light"?{appearance:"light"}:{},...e("base-url")!==null?{baseUrl:e("base-url")}:{},...e("refresh-ms")!==null?{refreshMs:Number(e("refresh-ms"))}:{},...e("show-fee-breakdown")!==null?{showFeeBreakdown:e("show-fee-breakdown")!=="false"}:{},...e("show-attribution")!==null?{showAttribution:e("show-attribution")!=="false"}:{}},{config:s}=C(i);this.cfg=s,this.getAttribute("appearance")!==s.appearance&&(this.syncing=!0,this.setAttribute("appearance",s.appearance),this.syncing=!1),this.client=s.apiKey!==null?E({apiKey:s.apiKey,baseUrl:s.baseUrl,...this.fetchImpl?{fetch:this.fetchImpl}:{}}):null,this.paintChrome(),this.quote(!0)}root(){return this.shadowRoot}$(e){return this.root().querySelector(e)}build(){if(this.built)return;let e=this.attachShadow({mode:"open"}),i=document.createElement("style");i.textContent=U;let s=document.createElement("div");s.className="card",s.innerHTML=`
|
|
260
|
+
<div class="head">
|
|
261
|
+
<span class="refresh">
|
|
262
|
+
<svg class="refresh__ring" viewBox="0 0 16 16" aria-hidden="true"><circle class="bg" cx="8" cy="8" r="6"></circle><circle class="fg" cx="8" cy="8" r="6" pathLength="100" stroke-dasharray="100" stroke-dashoffset="0"></circle></svg>
|
|
263
|
+
<button type="button" data-refresh aria-label="Refresh quote">refresh</button>
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="field" data-field="from">
|
|
267
|
+
<span class="field__label">You pay</span>
|
|
268
|
+
<div class="field__row">
|
|
269
|
+
<input class="amount" data-amount type="text" inputmode="decimal" autocomplete="off" placeholder="0.00" aria-label="Amount to pay" />
|
|
270
|
+
<button class="asset" data-asset="from" aria-haspopup="listbox" aria-label="Select the asset you pay"></button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="swapwrap"><button class="swap" data-swap type="button" aria-label="Swap the pair">⇅</button></div>
|
|
274
|
+
<div class="field" data-field="to">
|
|
275
|
+
<span class="field__label">You receive</span>
|
|
276
|
+
<div class="field__row">
|
|
277
|
+
<span class="receive receive--dim" data-receive>-</span>
|
|
278
|
+
<button class="asset" data-asset="to" aria-haspopup="listbox" aria-label="Select the asset you receive"></button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="result" data-result aria-live="polite"></div>
|
|
282
|
+
`,e.append(i,s),this.built=!0,this.$("[data-amount]")?.addEventListener("input",()=>this.onAmount()),this.$("[data-swap]")?.addEventListener("click",()=>this.swap()),this.$("[data-refresh]")?.addEventListener("click",()=>this.quote(!0)),this.$('[data-asset="from"]')?.addEventListener("click",()=>this.openSelector("from")),this.$('[data-asset="to"]')?.addEventListener("click",()=>this.openSelector("to")),this.root().addEventListener("click",o=>{let r=o.target;if(r.closest('[data-action="plan"]')){this.createPlan();return}let a=r.closest("[data-venue]");a&&this.selectVenue(a.getAttribute("data-venue")??"")})}assetBtn(e,i){let s=this.$(`[data-asset="${e}"]`);s&&(s.innerHTML=`${R(i.symbol)}<span class="asset__sym">${i.symbol}</span><span class="asset__caret" aria-hidden="true">▾</span>`)}paintChrome(){let e=x(this.cfg.from)??y[0],i=x(this.cfg.to)??y[1];this.assetBtn("from",e),this.assetBtn("to",i);let s=this.$("[data-amount]");s&&s.value!==this.cfg.amount&&document.activeElement!==s&&(s.value=this.cfg.amount)}onAmount(){let e=this.$("[data-amount]"),i=e?e.value.trim():"";if(this.cfg={...this.cfg,amount:i},clearTimeout(this.debounce),i===""){this.setView({kind:"idle"}),this.stopCountdown();return}this.debounce=window.setTimeout(()=>void this.quote(!0),fe)}setPair(e,i){this.cfg={...this.cfg,from:e,to:i},this.paintChrome(),b(this,v.asset,{from:e,to:i}),this.quote(!0)}swap(){this.setPair(this.cfg.to,this.cfg.from)}setView(e){this.view=e,this.paintResult()}paintResult(){let e=this.$("[data-result]");e&&(e.innerHTML=N(this.view,this.cfg,this.selectedVenue,this.plan)),this.paintReceive(),this.$('.venues[role="radiogroup"]')?.addEventListener("keydown",i=>this.venueKeys(i))}paintReceive(){let e=this.$("[data-receive]");if(!e)return;let i=this.view.kind==="quoted"?$(this.view,this.selectedVenue):null;if(i){let s=i.fees?i.fees.userReceives:i.net;e.textContent=s!==null?g(s):"-",e.classList.remove("receive--dim")}else e.textContent=this.view.kind==="loading"?"...":"-",e.classList.add("receive--dim")}selectVenue(e){if(this.view.kind!=="quoted"||e==="")return;let i=this.view.venues.find(s=>s.venueId===e&&s.available);!i||this.selectedVenue===e||(this.selectedVenue=e,this.plan=null,this.paintResult(),this.$(`[data-venue="${e}"]`)?.focus(),b(this,v.venue,{venueId:e,net:i.net,isBest:i.isBest,pair:this.view.pair}))}venueKeys(e){if(this.view.kind!=="quoted")return;let i=this.view.venues.filter(a=>a.available).map(a=>a.venueId);if(i.length===0)return;if(e.key===" "||e.key==="Enter"){let l=e.target?.closest("[data-venue]")?.getAttribute("data-venue");l!=null&&i.includes(l)&&(e.preventDefault(),this.selectVenue(l));return}let s=this.selectedVenue!==null&&i.includes(this.selectedVenue)?i.indexOf(this.selectedVenue):0,o=s;if(e.key==="ArrowDown"||e.key==="ArrowRight")o=Math.min(i.length-1,s+1);else if(e.key==="ArrowUp"||e.key==="ArrowLeft")o=Math.max(0,s-1);else return;e.preventDefault();let r=i[o];r&&this.selectVenue(r)}async quote(e){let i=this.cfg.amount.trim();if(i===""){this.setView({kind:"idle"}),this.stopCountdown();return}if(this.cfg.from===this.cfg.to){this.setView({kind:"invalid",title:"Choose two different assets",message:"Pick a different asset to receive."}),this.stopCountdown();return}if(!/^\d+(\.\d+)?$/.test(i)||Number(i)<=0){this.setView({kind:"invalid",title:"Enter a valid amount",message:"Type a positive number, for example 100 or 12.5."}),this.stopCountdown();return}let s=++this.reqSeq;e&&this.view.kind!=="quoted"&&this.setView({kind:"loading"});try{let o=await P({baseUrl:this.cfg.baseUrl,apiKey:this.cfg.apiKey,...this.fetchImpl?{fetch:this.fetchImpl}:{}},{from:this.cfg.from,to:this.cfg.to,amount:this.cfg.amount,...this.cfg.feeBps!==null?{feeBps:this.cfg.feeBps}:{},...this.cfg.feeRecipient!==null?{feeRecipient:this.cfg.feeRecipient}:{}});if(s!==this.reqSeq)return;let r=q(o);if(r.kind==="quoted"){let a=r.venues.filter(l=>l.available).map(l=>l.venueId);(this.selectedVenue===null||!a.includes(this.selectedVenue))&&(this.selectedVenue=r.best?.venueId??a[0]??null),this.plan=null}this.setView(r),r.kind==="quoted"?(b(this,v.quote,H(o)),this.startCountdown()):(b(this,v.error,{code:"no_route",message:"No venue can fill this pair at this size."}),this.stopCountdown())}catch(o){if(s!==this.reqSeq)return;let r=o instanceof p?o.code:"network",a=o instanceof Error?o.message:"could not reach the Synfin API";this.setView({kind:"error",code:r,message:a}),b(this,v.error,{code:r,message:a}),this.stopCountdown()}}startCountdown(){this.stopCountdown();let e=Math.round(this.cfg.refreshMs/1e3);this.countdown=e;let i=()=>{let s=this.root().querySelector(".refresh__ring .fg");s&&(s.style.strokeDashoffset=String(100-this.countdown/e*100))};i(),this.ticker=window.setInterval(()=>{this.countdown-=1,i(),this.countdown<=0&&this.quote(!1)},1e3)}stopCountdown(){clearInterval(this.ticker)}async createPlan(){if(this.client===null||this.view.kind!=="quoted"||this.cfg.feeRecipient===null)return;let e=$(this.view,this.selectedVenue);if(!(e===null||!e.available)){this.stopCountdown(),this.plan={status:"creating"},this.paintResult();try{let i=await this.client.createPlan({from:this.cfg.from,to:this.cfg.to,amount:this.cfg.amount,venueId:e.venueId,takerParty:this.cfg.feeRecipient,idempotencyKey:`w_${Date.now()}_${Math.round(Number(this.cfg.amount)*1e3)}`,...this.cfg.feeBps!==null?{feeBps:this.cfg.feeBps}:{},feeRecipient:this.cfg.feeRecipient}),s=i.planId;this.plan={status:"created",...s?{planId:s}:{}},this.paintResult(),b(this,v.plan,i)}catch(i){let s=i instanceof p?i.code:"network",o=i instanceof Error?i.message:"plan failed";this.plan={status:"error",message:o},this.paintResult(),b(this,v.error,{code:s,message:o})}}}openSelector(e){this.selectorOpen&&this.closeSelector(),this.selectorOpen=e;let i=document.createElement("div");i.className="pop",i.setAttribute("role","dialog"),i.setAttribute("aria-modal","true"),i.setAttribute("aria-label",`Select the ${e} asset`),i.innerHTML=`
|
|
283
|
+
<div class="pop__head">
|
|
284
|
+
<input class="pop__search" data-search placeholder="Search assets" aria-label="Search assets" role="combobox" aria-expanded="true" aria-controls="synfin-opts" />
|
|
285
|
+
<button class="pop__close" data-close aria-label="Close">✕</button>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="pop__list" id="synfin-opts" role="listbox" data-list></div>`,this.$(".card").append(i);let s=i.querySelector("[data-search]"),o=i.querySelector("[data-list]"),r=l=>{let f=e==="from"?this.cfg.to:this.cfg.from,u=A(l);o.innerHTML=u.length===0?`<div class="opt--empty">No assets match "${l.replace(/[<>&]/g,"")}"</div>`:u.map((h,m)=>`<button class="opt" role="option" tabindex="-1" data-sym="${h.symbol}" ${h.symbol===f?'aria-disabled="true" disabled':""} ${m===0?'aria-selected="true"':""}>
|
|
288
|
+
${R(h.symbol)}
|
|
289
|
+
<span class="opt__name"><span class="opt__sym">${h.symbol}</span><span class="opt__full">${h.name}</span></span>
|
|
290
|
+
</button>`).join("")};r("");let a=l=>{e==="from"&&l===this.cfg.to||e==="to"&&l===this.cfg.from||(this.closeSelector(),e==="from"?this.setPair(l,this.cfg.to):this.setPair(this.cfg.from,l))};s.addEventListener("input",()=>r(s.value)),o.addEventListener("click",l=>{let f=l.target.closest("[data-sym]");f&&!f.hasAttribute("disabled")&&a(f.getAttribute("data-sym")??"")}),i.querySelector("[data-close]")?.addEventListener("click",()=>this.closeSelector()),s.addEventListener("keydown",l=>this.selectorKeys(l,o,a,s)),setTimeout(()=>s.focus(),0)}selectorKeys(e,i,s,o){let r=[...i.querySelectorAll("[data-sym]:not([disabled])")],a=i.querySelector('[aria-selected="true"]'),l=a?r.indexOf(a):-1;if(e.key==="Escape")e.preventDefault(),this.closeSelector();else if(e.key==="ArrowDown"||e.key==="ArrowUp")e.preventDefault(),l=e.key==="ArrowDown"?Math.min(r.length-1,l+1):Math.max(0,l-1),r.forEach(f=>f.setAttribute("aria-selected",f===r[l]?"true":"false")),r[l]?.scrollIntoView({block:"nearest"});else if(e.key==="Enter"){e.preventDefault();let f=(r[l]??r[0])?.getAttribute("data-sym");f&&s(f)}}closeSelector(){this.selectorOpen=null,this.root().querySelector(".pop")?.remove(),this.$('[data-asset="from"]')?.focus()}};function ue(t="synfin-widget"){typeof customElements<"u"&&customElements.get(t)===void 0&&customElements.define(t,k)}ue();})();
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synfin/widget",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Synfin widget: an embeddable, framework-agnostic quote-and-fee widget (a <synfin-widget> custom element, plus a React wrapper) built on @synfin/client. Live best-execution quotes across Canton venues with the partner's disclosed fees. Non-custodial: it never holds keys.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./react": {
|
|
22
|
+
"types": "./dist/react.d.ts",
|
|
23
|
+
"import": "./dist/react.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"unpkg": "./dist/synfin-widget.global.js",
|
|
29
|
+
"jsdelivr": "./dist/synfin-widget.global.js",
|
|
30
|
+
"typesVersions": {
|
|
31
|
+
"*": {
|
|
32
|
+
"react": [
|
|
33
|
+
"./dist/react.d.ts"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": [
|
|
38
|
+
"./dist/index.js"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://synfin.xyz/docs/sdk",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/cayvox/synfin.git",
|
|
47
|
+
"directory": "packages/widget"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"synfin",
|
|
51
|
+
"canton",
|
|
52
|
+
"widget",
|
|
53
|
+
"swap",
|
|
54
|
+
"web-component"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.build.json && node scripts/bundle-cdn.mjs",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@synfin/client": "^0.1.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"react": ">=18"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "20.19.43",
|
|
74
|
+
"@types/react": "18.3.12",
|
|
75
|
+
"esbuild": "0.24.2",
|
|
76
|
+
"jsdom": "25.0.1",
|
|
77
|
+
"react": "18.3.1",
|
|
78
|
+
"typescript": "5.7.2",
|
|
79
|
+
"vitest": "2.1.8"
|
|
80
|
+
}
|
|
81
|
+
}
|