@ottotheagent/mcp-card-assets 0.5.0 → 0.6.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/car-card.js +20 -3
- package/flight-card.js +16 -3
- package/hotel-card.js +20 -3
- package/package.json +1 -1
- package/room-card.js +20 -3
package/car-card.js
CHANGED
|
@@ -43,6 +43,15 @@ function icon(names, fallback, className = "icon") {
|
|
|
43
43
|
return node;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The pick message quotes the exact price, so the model can tell apart options that round alike.
|
|
47
|
+
function formatExactMoney(value, currency) {
|
|
48
|
+
if (value === null || value === undefined) return "";
|
|
49
|
+
const symbol = SYMBOLS[currency];
|
|
50
|
+
const fraction = Number.isInteger(value) ? 0 : 2;
|
|
51
|
+
const amount = value.toLocaleString("en-US", { minimumFractionDigits: fraction, maximumFractionDigits: 2 });
|
|
52
|
+
return symbol ? `${symbol}${amount}` : `${currency} ${amount}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
function formatMoney(value, currency) {
|
|
47
56
|
if (value === null || value === undefined) return "";
|
|
48
57
|
const symbol = SYMBOLS[currency];
|
|
@@ -87,6 +96,9 @@ function renderInsights(reasons, notes) {
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function mount(root, card, spec) {
|
|
99
|
+
// A second mount on the same root replaces the first card; its lock timer must not
|
|
100
|
+
// fire later and redraw over the new card.
|
|
101
|
+
if (root.__ottoCard) root.__ottoCard.cancel();
|
|
90
102
|
let chosen = null;
|
|
91
103
|
let busy = false;
|
|
92
104
|
let timer = null;
|
|
@@ -128,7 +140,8 @@ function mount(root, card, spec) {
|
|
|
128
140
|
|
|
129
141
|
// One pick at a time: the new pick replaces the old mark, then the card locks so a
|
|
130
142
|
// second click cannot send a second message.
|
|
131
|
-
|
|
143
|
+
// Assigned, not added: a re-mount replaces the handler instead of stacking another.
|
|
144
|
+
root.onclick = (event) => {
|
|
132
145
|
if (busy) return;
|
|
133
146
|
const button = event.target.closest("[data-select]");
|
|
134
147
|
const item = button && card.items.find((i) => i.id === button.dataset.select);
|
|
@@ -143,9 +156,12 @@ function mount(root, card, spec) {
|
|
|
143
156
|
} catch (error) {
|
|
144
157
|
unlock();
|
|
145
158
|
}
|
|
146
|
-
}
|
|
159
|
+
};
|
|
147
160
|
|
|
148
161
|
draw();
|
|
162
|
+
const handle = { cancel: () => clearTimeout(timer) };
|
|
163
|
+
root.__ottoCard = handle;
|
|
164
|
+
return handle;
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
/* Otto car card: OttoCarCard.render(root, card), card from render_cars.
|
|
@@ -228,6 +244,7 @@ function lineIcon(name) {
|
|
|
228
244
|
function render(root, card) {
|
|
229
245
|
injectCss("otto-car-card-css");
|
|
230
246
|
const money = (value, item) => formatMoney(value, item?.currency || card.currency);
|
|
247
|
+
const exactMoney = (value, item) => formatExactMoney(value, item?.currency || card.currency);
|
|
231
248
|
const chips = assignChips(card.items || [], carCallouts(card.items || []));
|
|
232
249
|
|
|
233
250
|
// Vendor and class as a traveler would say them; the card number only when another
|
|
@@ -236,7 +253,7 @@ function render(root, card) {
|
|
|
236
253
|
const name = oneLine(`${item.vendor} ${item.class_label}`);
|
|
237
254
|
const key = (other) => oneLine(`${other.vendor} ${other.class_label}`).toLowerCase();
|
|
238
255
|
const twin = card.items.some((other) => other !== item && key(other) === key(item));
|
|
239
|
-
const total =
|
|
256
|
+
const total = exactMoney(item.total_rate, item);
|
|
240
257
|
return [twin ? `${name} (No. ${card.items.indexOf(item) + 1} on the card)` : name, total ? `${total} total` : ""]
|
|
241
258
|
.filter(Boolean)
|
|
242
259
|
.join(", ");
|
package/flight-card.js
CHANGED
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
|
|
33
33
|
function render(root, card) {
|
|
34
34
|
injectCss();
|
|
35
|
+
// A second render into the same root replaces the first card, so the first one's
|
|
36
|
+
// lock timer must not fire later and redraw over the new card.
|
|
37
|
+
if (root.__ottoCard) root.__ottoCard.cancel();
|
|
35
38
|
let open = null;
|
|
36
39
|
let chosen = null;
|
|
37
40
|
let busy = false;
|
|
@@ -41,6 +44,12 @@
|
|
|
41
44
|
value === null || value === undefined
|
|
42
45
|
? ""
|
|
43
46
|
: (SYMBOLS[card.currency] || card.currency + " ") + Math.round(value).toLocaleString();
|
|
47
|
+
// The message quotes the exact price, so the model can tell apart fares that round alike.
|
|
48
|
+
const exactMoney = (value) =>
|
|
49
|
+
value === null || value === undefined
|
|
50
|
+
? ""
|
|
51
|
+
: (SYMBOLS[card.currency] || card.currency + " ") +
|
|
52
|
+
value.toLocaleString("en-US", { minimumFractionDigits: Number.isInteger(value) ? 0 : 2, maximumFractionDigits: 2 });
|
|
44
53
|
const duration = (m) => (m === null || m === undefined ? "" : `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`);
|
|
45
54
|
const stops = (s) => (!s ? "" : s === 1 ? "1 stop" : `${s} stops`);
|
|
46
55
|
const recommended = (option) => option.fares.find((f) => f.recommended) || option.fares[0];
|
|
@@ -65,7 +74,7 @@
|
|
|
65
74
|
option.depart ? `departing ${oneLine(option.depart, 12)}` : "",
|
|
66
75
|
oneLine(fare.name),
|
|
67
76
|
oneLine(fare.refundable),
|
|
68
|
-
|
|
77
|
+
exactMoney(fare.price),
|
|
69
78
|
].filter(Boolean);
|
|
70
79
|
return `Book flight ${parts.join(", ")}`;
|
|
71
80
|
}
|
|
@@ -222,7 +231,8 @@
|
|
|
222
231
|
}
|
|
223
232
|
}
|
|
224
233
|
|
|
225
|
-
|
|
234
|
+
// Assigned, not added: a re-render replaces the handler instead of stacking another.
|
|
235
|
+
root.onclick = (event) => {
|
|
226
236
|
if (busy) return;
|
|
227
237
|
const button = event.target.closest("[data-select]");
|
|
228
238
|
if (button) {
|
|
@@ -236,9 +246,12 @@
|
|
|
236
246
|
const id = opener.dataset.fares || opener.dataset.card;
|
|
237
247
|
open = open === id ? null : id;
|
|
238
248
|
draw();
|
|
239
|
-
}
|
|
249
|
+
};
|
|
240
250
|
|
|
241
251
|
draw();
|
|
252
|
+
const handle = { cancel: () => clearTimeout(timer) };
|
|
253
|
+
root.__ottoCard = handle;
|
|
254
|
+
return handle;
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
window.OttoFlightCard = { render };
|
package/hotel-card.js
CHANGED
|
@@ -43,6 +43,15 @@ function icon(names, fallback, className = "icon") {
|
|
|
43
43
|
return node;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The pick message quotes the exact price, so the model can tell apart options that round alike.
|
|
47
|
+
function formatExactMoney(value, currency) {
|
|
48
|
+
if (value === null || value === undefined) return "";
|
|
49
|
+
const symbol = SYMBOLS[currency];
|
|
50
|
+
const fraction = Number.isInteger(value) ? 0 : 2;
|
|
51
|
+
const amount = value.toLocaleString("en-US", { minimumFractionDigits: fraction, maximumFractionDigits: 2 });
|
|
52
|
+
return symbol ? `${symbol}${amount}` : `${currency} ${amount}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
function formatMoney(value, currency) {
|
|
47
56
|
if (value === null || value === undefined) return "";
|
|
48
57
|
const symbol = SYMBOLS[currency];
|
|
@@ -87,6 +96,9 @@ function renderInsights(reasons, notes) {
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function mount(root, card, spec) {
|
|
99
|
+
// A second mount on the same root replaces the first card; its lock timer must not
|
|
100
|
+
// fire later and redraw over the new card.
|
|
101
|
+
if (root.__ottoCard) root.__ottoCard.cancel();
|
|
90
102
|
let chosen = null;
|
|
91
103
|
let busy = false;
|
|
92
104
|
let timer = null;
|
|
@@ -128,7 +140,8 @@ function mount(root, card, spec) {
|
|
|
128
140
|
|
|
129
141
|
// One pick at a time: the new pick replaces the old mark, then the card locks so a
|
|
130
142
|
// second click cannot send a second message.
|
|
131
|
-
|
|
143
|
+
// Assigned, not added: a re-mount replaces the handler instead of stacking another.
|
|
144
|
+
root.onclick = (event) => {
|
|
132
145
|
if (busy) return;
|
|
133
146
|
const button = event.target.closest("[data-select]");
|
|
134
147
|
const item = button && card.items.find((i) => i.id === button.dataset.select);
|
|
@@ -143,9 +156,12 @@ function mount(root, card, spec) {
|
|
|
143
156
|
} catch (error) {
|
|
144
157
|
unlock();
|
|
145
158
|
}
|
|
146
|
-
}
|
|
159
|
+
};
|
|
147
160
|
|
|
148
161
|
draw();
|
|
162
|
+
const handle = { cancel: () => clearTimeout(timer) };
|
|
163
|
+
root.__ottoCard = handle;
|
|
164
|
+
return handle;
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
/* Otto hotel card: OttoHotelCard.render(root, card), card from render_hotels.
|
|
@@ -177,6 +193,7 @@ function hotelCallouts(items) {
|
|
|
177
193
|
function render(root, card) {
|
|
178
194
|
injectCss("otto-hotel-card-css");
|
|
179
195
|
const money = (value, item) => formatMoney(value, item?.currency || card.currency);
|
|
196
|
+
const exactMoney = (value, item) => formatExactMoney(value, item?.currency || card.currency);
|
|
180
197
|
const labels = hotelCallouts(card.items || []);
|
|
181
198
|
|
|
182
199
|
// The hotel's name, as a traveler would say it. A detail is added only when the name
|
|
@@ -189,7 +206,7 @@ function render(root, card) {
|
|
|
189
206
|
if (!twins.length && name.length >= MIN_NAME_CHARS) return name;
|
|
190
207
|
const details = (item) => [
|
|
191
208
|
oneLine(item.address || "", 60),
|
|
192
|
-
item.nightly_rate === null || item.nightly_rate === undefined ? "" : `from ${
|
|
209
|
+
item.nightly_rate === null || item.nightly_rate === undefined ? "" : `from ${exactMoney(item.nightly_rate, item)}/night`,
|
|
193
210
|
`No. ${card.items.indexOf(item) + 1} on the card`,
|
|
194
211
|
];
|
|
195
212
|
const detail = details(option).find((value, i) => value && twins.every((twin) => details(twin)[i] !== value));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottotheagent/mcp-card-assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Renderers, styles, logos and photo placeholders for the Otto flight, hotel, room and car cards that Claude draws in its widget sandbox.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"files": [
|
package/room-card.js
CHANGED
|
@@ -43,6 +43,15 @@ function icon(names, fallback, className = "icon") {
|
|
|
43
43
|
return node;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The pick message quotes the exact price, so the model can tell apart options that round alike.
|
|
47
|
+
function formatExactMoney(value, currency) {
|
|
48
|
+
if (value === null || value === undefined) return "";
|
|
49
|
+
const symbol = SYMBOLS[currency];
|
|
50
|
+
const fraction = Number.isInteger(value) ? 0 : 2;
|
|
51
|
+
const amount = value.toLocaleString("en-US", { minimumFractionDigits: fraction, maximumFractionDigits: 2 });
|
|
52
|
+
return symbol ? `${symbol}${amount}` : `${currency} ${amount}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
function formatMoney(value, currency) {
|
|
47
56
|
if (value === null || value === undefined) return "";
|
|
48
57
|
const symbol = SYMBOLS[currency];
|
|
@@ -87,6 +96,9 @@ function renderInsights(reasons, notes) {
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function mount(root, card, spec) {
|
|
99
|
+
// A second mount on the same root replaces the first card; its lock timer must not
|
|
100
|
+
// fire later and redraw over the new card.
|
|
101
|
+
if (root.__ottoCard) root.__ottoCard.cancel();
|
|
90
102
|
let chosen = null;
|
|
91
103
|
let busy = false;
|
|
92
104
|
let timer = null;
|
|
@@ -128,7 +140,8 @@ function mount(root, card, spec) {
|
|
|
128
140
|
|
|
129
141
|
// One pick at a time: the new pick replaces the old mark, then the card locks so a
|
|
130
142
|
// second click cannot send a second message.
|
|
131
|
-
|
|
143
|
+
// Assigned, not added: a re-mount replaces the handler instead of stacking another.
|
|
144
|
+
root.onclick = (event) => {
|
|
132
145
|
if (busy) return;
|
|
133
146
|
const button = event.target.closest("[data-select]");
|
|
134
147
|
const item = button && card.items.find((i) => i.id === button.dataset.select);
|
|
@@ -143,9 +156,12 @@ function mount(root, card, spec) {
|
|
|
143
156
|
} catch (error) {
|
|
144
157
|
unlock();
|
|
145
158
|
}
|
|
146
|
-
}
|
|
159
|
+
};
|
|
147
160
|
|
|
148
161
|
draw();
|
|
162
|
+
const handle = { cancel: () => clearTimeout(timer) };
|
|
163
|
+
root.__ottoCard = handle;
|
|
164
|
+
return handle;
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
/* Otto room card: OttoRoomCard.render(root, card), card from render_hotel_rooms.
|
|
@@ -161,10 +177,11 @@ const FACT_ICONS = {
|
|
|
161
177
|
function render(root, card) {
|
|
162
178
|
injectCss("otto-room-card-css");
|
|
163
179
|
const money = (value, item) => formatMoney(value, item?.currency || card.currency);
|
|
180
|
+
const exactMoney = (value, item) => formatExactMoney(value, item?.currency || card.currency);
|
|
164
181
|
|
|
165
182
|
// The pick as a traveler would say it: room, hotel, rate, and its refund terms.
|
|
166
183
|
function describe(item) {
|
|
167
|
-
const price =
|
|
184
|
+
const price = exactMoney(item.nightly_rate, item);
|
|
168
185
|
const refund = (item.facts || []).find((fact) => fact.icon === "refund");
|
|
169
186
|
return [oneLine(item.name), oneLine(card.title), price ? `${price}/night` : "", refund ? oneLine(refund.text) : ""]
|
|
170
187
|
.filter(Boolean)
|