@womp/mcp-ui 0.1.2 → 0.1.3
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/README.md +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -2
- package/dist/print/index.html +223 -0
- package/package.json +4 -3
- package/src/print/index.html +149 -0
- package/src/print/main.ts +108 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@womp/mcp-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP Apps widgets for Womp's MCP server — interactive cards rendered inline in Claude and other MCP hosts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "yarn build:ui && yarn build:bake",
|
|
18
|
-
"build:ui": "yarn build:ui:image && yarn build:ui:mesh",
|
|
18
|
+
"build:ui": "yarn build:ui:image && yarn build:ui:mesh && yarn build:ui:print",
|
|
19
19
|
"build:bake": "node scripts/bake-html.mjs",
|
|
20
20
|
"dev": "vite",
|
|
21
21
|
"dev:host": "vite",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"clean": "rm -rf dist",
|
|
25
25
|
"prepublishOnly": "yarn clean && yarn build",
|
|
26
26
|
"build:ui:image": "CARD=image vite build",
|
|
27
|
-
"build:ui:mesh": "CARD=mesh vite build"
|
|
27
|
+
"build:ui:mesh": "CARD=mesh vite build",
|
|
28
|
+
"build:ui:print": "CARD=print vite build"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Womp 3D print</title>
|
|
6
|
+
<style>
|
|
7
|
+
/* Same var()-with-fallback discipline as the other cards: host style
|
|
8
|
+
variables arrive over the bridge after the handshake, not as CSS
|
|
9
|
+
custom properties, so the card must look right without them. */
|
|
10
|
+
:root {
|
|
11
|
+
color-scheme: light dark;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-family: var(--font-sans, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
16
|
+
color: var(--color-text-primary, #e8e8e8);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.card {
|
|
20
|
+
max-width: 360px;
|
|
21
|
+
border-radius: var(--border-radius-lg, 14px);
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: var(--color-background-secondary, #1b1b1d);
|
|
24
|
+
border: 1px solid var(--color-border-primary, rgba(255, 255, 255, 0.08));
|
|
25
|
+
padding: 14px;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.head {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: baseline;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
gap: 8px;
|
|
34
|
+
}
|
|
35
|
+
.title {
|
|
36
|
+
font-size: var(--font-text-sm-size, 13px);
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
}
|
|
39
|
+
.material {
|
|
40
|
+
font-size: var(--font-text-xs-size, 11px);
|
|
41
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.6));
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
text-overflow: ellipsis;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.size {
|
|
48
|
+
margin-top: 4px;
|
|
49
|
+
font-size: var(--font-text-xs-size, 11px);
|
|
50
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.6));
|
|
51
|
+
}
|
|
52
|
+
.size:empty { display: none; }
|
|
53
|
+
|
|
54
|
+
.price {
|
|
55
|
+
margin-top: 10px;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: baseline;
|
|
58
|
+
gap: 8px;
|
|
59
|
+
}
|
|
60
|
+
.price:empty { display: none; }
|
|
61
|
+
.amount {
|
|
62
|
+
font-size: 24px;
|
|
63
|
+
font-weight: 700;
|
|
64
|
+
letter-spacing: -0.02em;
|
|
65
|
+
}
|
|
66
|
+
.was {
|
|
67
|
+
font-size: var(--font-text-xs-size, 12px);
|
|
68
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.6));
|
|
69
|
+
text-decoration: line-through;
|
|
70
|
+
}
|
|
71
|
+
.per {
|
|
72
|
+
font-size: var(--font-text-xs-size, 11px);
|
|
73
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.6));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.rows {
|
|
77
|
+
margin-top: 6px;
|
|
78
|
+
font-size: var(--font-text-xs-size, 11px);
|
|
79
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.6));
|
|
80
|
+
display: grid;
|
|
81
|
+
gap: 2px;
|
|
82
|
+
}
|
|
83
|
+
.rows:empty { display: none; }
|
|
84
|
+
|
|
85
|
+
.cta {
|
|
86
|
+
margin-top: 12px;
|
|
87
|
+
display: flex;
|
|
88
|
+
width: 100%;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
gap: 6px;
|
|
92
|
+
border: 0;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
font: inherit;
|
|
95
|
+
font-size: var(--font-text-sm-size, 13px);
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
padding: 9px 12px;
|
|
98
|
+
border-radius: var(--border-radius-full, 999px);
|
|
99
|
+
color: #fff;
|
|
100
|
+
background: var(--color-background-accent, #5867dd);
|
|
101
|
+
}
|
|
102
|
+
.cta:hover { filter: brightness(1.1); }
|
|
103
|
+
.cta:focus-visible { outline: 2px solid var(--color-ring-primary, #7aa2ff); outline-offset: 1px; }
|
|
104
|
+
.cta[hidden] { display: none; }
|
|
105
|
+
.cta svg { width: 14px; height: 14px; display: block; }
|
|
106
|
+
|
|
107
|
+
/* The disclaimer only after there is a price to disclaim. */
|
|
108
|
+
.note {
|
|
109
|
+
margin-top: 8px;
|
|
110
|
+
font-size: 10px;
|
|
111
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.5));
|
|
112
|
+
}
|
|
113
|
+
.note[hidden] { display: none; }
|
|
114
|
+
|
|
115
|
+
.status {
|
|
116
|
+
margin-top: 8px;
|
|
117
|
+
font-size: var(--font-text-xs-size, 11px);
|
|
118
|
+
color: var(--color-text-secondary, rgba(232, 232, 232, 0.6));
|
|
119
|
+
}
|
|
120
|
+
.status:empty { display: none; }
|
|
121
|
+
|
|
122
|
+
/* Skeleton shimmer while the engine measures. */
|
|
123
|
+
.measuring .price,
|
|
124
|
+
.measuring .rows { opacity: 0.35; }
|
|
125
|
+
</style>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
<div class="card" id="card">
|
|
129
|
+
<div class="head">
|
|
130
|
+
<div class="title">3D print</div>
|
|
131
|
+
<div class="material" id="material"></div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="size" id="size"></div>
|
|
134
|
+
<div class="price" id="price"></div>
|
|
135
|
+
<div class="rows" id="rows"></div>
|
|
136
|
+
<button class="cta" id="order" hidden>
|
|
137
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
138
|
+
<path d="M7 17 17 7M9 7h8v8" />
|
|
139
|
+
</svg>
|
|
140
|
+
Complete order on womp.com
|
|
141
|
+
</button>
|
|
142
|
+
<div class="note" id="note" hidden>
|
|
143
|
+
Shipping is added at checkout; the final price is confirmed before printing.
|
|
144
|
+
</div>
|
|
145
|
+
<div class="status" id="status">Connecting…</div>
|
|
146
|
+
</div>
|
|
147
|
+
<script type="module" src="./main.ts"></script>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { $, createCard, openLink, type GenerationResult } from "../shared/bridge";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The print card — a quote, and (after prepare_print) the way out to checkout.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately has NO payment surface: the button opens womp.com's print flow
|
|
7
|
+
* via `ui/open-link`, where address, shipping and payment run with every gate
|
|
8
|
+
* the web app already has. The card renders what estimate_print /
|
|
9
|
+
* prepare_print returned; it never calls a tool itself, so unlike the mesh
|
|
10
|
+
* card it needs no app-callable visibility anywhere.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** One priced variant, as swan-backend's QUOTE_ROW_SCHEMA emits it. */
|
|
14
|
+
interface QuoteRow {
|
|
15
|
+
salePrice: number;
|
|
16
|
+
discount: number;
|
|
17
|
+
unitPrice: number;
|
|
18
|
+
productionDays: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** structuredContent of estimate_print / prepare_print (api/mcp.ts §13). */
|
|
22
|
+
interface PrintQuoteResult {
|
|
23
|
+
material?: string;
|
|
24
|
+
quantity?: number;
|
|
25
|
+
dimensionsCm?: { width: number; height: number; depth: number };
|
|
26
|
+
solid?: QuoteRow;
|
|
27
|
+
hollow?: QuoteRow;
|
|
28
|
+
callerIsPro?: boolean;
|
|
29
|
+
projectId?: number;
|
|
30
|
+
/** Present on prepare_print only — what the CTA opens. */
|
|
31
|
+
printUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const usd = (n: number) => `$${n.toFixed(2)}`;
|
|
35
|
+
|
|
36
|
+
const setStatus = (text: string) => {
|
|
37
|
+
$("status").textContent = text;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function render(data: PrintQuoteResult) {
|
|
41
|
+
$("card").classList.remove("measuring");
|
|
42
|
+
|
|
43
|
+
if (data.material) {
|
|
44
|
+
$("material").textContent = `${data.quantity && data.quantity > 1 ? `${data.quantity}x ` : ""}${data.material}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const d = data.dimensionsCm;
|
|
48
|
+
if (d) {
|
|
49
|
+
$("size").textContent = `${d.width.toFixed(1)} x ${d.height.toFixed(1)} x ${d.depth.toFixed(1)} cm`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const solid = data.solid;
|
|
53
|
+
if (solid) {
|
|
54
|
+
const price = $("price");
|
|
55
|
+
price.textContent = "";
|
|
56
|
+
const amount = document.createElement("span");
|
|
57
|
+
amount.className = "amount";
|
|
58
|
+
amount.textContent = usd(solid.unitPrice);
|
|
59
|
+
price.append(amount);
|
|
60
|
+
if (solid.discount > 0) {
|
|
61
|
+
const was = document.createElement("span");
|
|
62
|
+
was.className = "was";
|
|
63
|
+
was.textContent = usd(solid.salePrice);
|
|
64
|
+
price.append(was);
|
|
65
|
+
}
|
|
66
|
+
const per = document.createElement("span");
|
|
67
|
+
per.className = "per";
|
|
68
|
+
per.textContent = "per unit";
|
|
69
|
+
price.append(per);
|
|
70
|
+
|
|
71
|
+
const rows = $("rows");
|
|
72
|
+
rows.textContent = "";
|
|
73
|
+
if (data.hollow) {
|
|
74
|
+
const hollow = document.createElement("div");
|
|
75
|
+
hollow.textContent = `hollow (Pro): ${usd(data.hollow.unitPrice)} per unit${data.callerIsPro ? "" : " — requires a subscription"}`;
|
|
76
|
+
rows.append(hollow);
|
|
77
|
+
}
|
|
78
|
+
const days = document.createElement("div");
|
|
79
|
+
days.textContent = `~${solid.productionDays} production day${solid.productionDays === 1 ? "" : "s"}`;
|
|
80
|
+
rows.append(days);
|
|
81
|
+
|
|
82
|
+
$("note").hidden = false;
|
|
83
|
+
setStatus("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const order = $("order");
|
|
87
|
+
order.hidden = !data.printUrl;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const card = createCard("Womp print", {
|
|
91
|
+
onTimeout: () => setStatus("Still measuring — ask me to check on it."),
|
|
92
|
+
onResult: (raw: GenerationResult) => {
|
|
93
|
+
const data = raw as PrintQuoteResult & GenerationResult;
|
|
94
|
+
// ontoolinput sends {status: "running"} while the engine measures; a print
|
|
95
|
+
// result itself never carries `status`.
|
|
96
|
+
if (data.status === "running") {
|
|
97
|
+
$("card").classList.add("measuring");
|
|
98
|
+
setStatus("Measuring mesh…");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
render(data);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
$("order").addEventListener("click", () => {
|
|
106
|
+
const data = card.current() as PrintQuoteResult | undefined;
|
|
107
|
+
if (data?.printUrl) openLink(card.app, data.printUrl);
|
|
108
|
+
});
|