@ottotheagent/mcp-card-assets 0.14.0 → 0.16.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/README.md +1 -1
- package/package.json +1 -1
- package/seat-map.css +1 -1
- package/seat-map.js +170 -49
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Renderers, logos and photo placeholders for the Otto flight, hotel, room and car
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
9
|
- `flight-card.js`, `hotel-card.js`, `room-card.js` and `car-card.js` each draw one card and expose `OttoFlightCard`, `OttoHotelCard`, `OttoRoomCard` or `OttoCarCard` with `render(rootElement, card)`. Each carries its own CSS and only the icons it uses inline, so the sandbox loads one script per card. The server's snippet loads jsDelivr's minified copy (`*-card.min.js`). Since 0.10.0, `room-card.js` draws only a row of room buttons, and since 0.11.0 `car-card.js` draws only a row of car buttons: the host draws the rooms or cars, with photos, from the same tool's MCP Apps resource.
|
|
10
|
-
- `seat-map.js` exposes `OttoSeatMap.render(rootElement, card)` (since 0.12.0). It draws one flight's cabin from `render_elastic_seat_map`. The server lays out the columns, aisles and rows for the aircraft and cabin, so the script only draws them. Clicking a seat marks it New selection without sending anything; since 0.13.0 the **Update seat to 15D** button then sends "Update seat to 15D (route)". Since 0.14.0 it tags exit rows and bulkhead rows, from the card's `exit` and `bulkhead` row flags. Also since 0.14.0, each row of the flight card's fare list starts its facts line with the fare's refund wording.
|
|
10
|
+
- `seat-map.js` exposes `OttoSeatMap.render(rootElement, card)` (since 0.12.0). It draws one flight's cabin from `render_elastic_seat_map`. The server lays out the columns, aisles and rows for the aircraft and cabin, so the script only draws them. Clicking a seat marks it New selection without sending anything; since 0.13.0 the **Update seat to 15D** button then sends "Update seat to 15D (route)". Since 0.14.0 it tags exit rows and bulkhead rows, from the card's `exit` and `bulkhead` row flags. Also since 0.14.0, each row of the flight card's fare list starts its facts line with the fare's refund wording. Since 0.15.0 the seat map draws each lavatory in `card.lavs` as one labeled block over its seat columns. Since 0.16.0 it draws a whole party (`card.travelers`): a side panel lists each traveler's seat, a change shows as "23A → 24C" without sending anything, and **Update seats** sends every change in one message. A lone traveler keeps the one-seat layout.
|
|
11
11
|
- Select (Show rooms on the hotel card) sends the pick as the traveler's message through the host's `sendPrompt()`: "Book flight …", "Show rooms at …", "Select room …" or "Select car …". The message carries no id.
|
|
12
12
|
- `*-card.css` holds each card's CSS as a separate file, for reference.
|
|
13
13
|
- `marks/<IATA>.png` holds airline logos, and `marks/cars/<code>.svg` holds car-company logos.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottotheagent/mcp-card-assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Renderers, styles, logos and photo placeholders for the Otto flight, hotel, room and car cards and the seat map that Claude draws in its widget sandbox.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"files": [
|
package/seat-map.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme="dark"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smRow.bulkhead{padding-top:var(--otto-space-2);border-top:3px solid var(--otto-text-secondary);}.smTag{flex-shrink:0;margin-left:var(--otto-space-2);font-size:var(--otto-text-sm);color:var(--otto-text-secondary);white-space:nowrap;}.smTag.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role="button"]{cursor:pointer;user-select:none;}.smSeat[role="button"]:hover{border-color:var(--otto-accent);}.smSeat[role="button"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}
|
|
1
|
+
:root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme="dark"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smBody{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--otto-space-5);}.smBody .smScroll{flex:0 1 auto;min-width:0;}.smSide{flex:0 0 168px;display:flex;flex-direction:column;gap:var(--otto-space-2);}.smSideHead{font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smTraveler{box-sizing:border-box;padding:var(--otto-space-2) var(--otto-space-3);border:1px solid var(--otto-card-border);border-radius:8px;cursor:pointer;user-select:none;}.smTraveler:hover,.smTraveler.active{border-color:var(--otto-accent);}.smTraveler:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smTravelerName{font-size:var(--otto-text-md);color:var(--otto-text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.smTravelerSeat{display:flex;align-items:baseline;gap:var(--otto-space-2);font-size:var(--otto-text-base);font-weight:var(--otto-weight-semibold);font-variant-numeric:tabular-nums;}.smTravelerSeat .smFrom{color:var(--otto-text-secondary);font-weight:var(--otto-weight-medium);text-decoration:line-through;}.smTravelerSeat .smArrow{color:var(--otto-text-secondary);}.smTravelerSeat .smTo{color:var(--otto-accent);}.smTravelerNote{font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smLav{box-sizing:border-box;flex-shrink:0;height:var(--sm-seat);display:grid;place-items:center;overflow:hidden;border:1px dashed var(--otto-card-border);border-radius:6px;background:var(--otto-strip-bg);color:var(--otto-text-secondary);font-size:var(--otto-text-sm);white-space:nowrap;}.smRow.bulkhead{padding-top:var(--otto-space-2);border-top:3px solid var(--otto-text-secondary);}.smTag{flex-shrink:0;margin-left:var(--otto-space-2);font-size:var(--otto-text-sm);color:var(--otto-text-secondary);white-space:nowrap;}.smTag.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role="button"]{cursor:pointer;user-select:none;}.smSeat[role="button"]:hover{border-color:var(--otto-accent);}.smSeat[role="button"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.mine.active{outline:2px solid var(--otto-text-primary);outline-offset:1px;}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}
|
package/seat-map.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* spec.bar draws the options as one wrapping row of buttons with no title, for a card
|
|
12
12
|
* whose details the host already shows above it.
|
|
13
13
|
*/
|
|
14
|
-
const CSS = ":root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme=\"dark\"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smRow.bulkhead{padding-top:var(--otto-space-2);border-top:3px solid var(--otto-text-secondary);}.smTag{flex-shrink:0;margin-left:var(--otto-space-2);font-size:var(--otto-text-sm);color:var(--otto-text-secondary);white-space:nowrap;}.smTag.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role=\"button\"]{cursor:pointer;user-select:none;}.smSeat[role=\"button\"]:hover{border-color:var(--otto-accent);}.smSeat[role=\"button\"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}";
|
|
14
|
+
const CSS = ":root{--otto-card-bg:#ffffff;--otto-card-border:#d4d4d4;--otto-btn-border:#525252;--otto-accent:#08c0cc;--otto-divider:#e5e5e5;--otto-strip-bg:#f5f5f5;--otto-fare-bg:#262624;--otto-on-fare:#ffffff;--otto-chip-bg:#ccfbf1;--otto-chip-text:#0a0a0a;--otto-text-primary:#0a0a0a;--otto-text-body:#0a0a0a;--otto-text-secondary:#737373;--otto-link-hover:#0369a1;--otto-banner-bg:#171717;--otto-on-banner:#ffffff;--otto-caution-bg:#fdf0dd;--otto-caution-text:#96590b;--otto-positive-text:#1c7a3e;--otto-photo-bg:#f5f5f5;--otto-font:Inter,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\";--otto-line:1.5;--otto-text-xs:10px;--otto-text-sm:11px;--otto-text-md:12px;--otto-text-base:13px;--otto-text-lg:14px;--otto-text-xl:18px;--otto-text-2xl:19px;--otto-weight-regular:400;--otto-weight-medium:500;--otto-weight-semibold:600;--otto-space-1:2px;--otto-space-2:6px;--otto-space-3:8px;--otto-space-4:10px;--otto-space-5:12px;--otto-space-6:14px;--otto-space-7:16px;--otto-space-8:18px;--otto-radius-card:12px;--otto-radius-fare:10px;--otto-radius-md:8px;--otto-radius-pill:999px;--otto-card-width:304px;--otto-icon-size:14px;--otto-photo-height:170px;--otto-mark-size:24px;--otto-chip-height:24px;--otto-tap-min:44px;}@media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}}:root[data-theme=\"dark\"]{--otto-card-bg:#262624;--otto-card-border:#3d3d3a;--otto-btn-border:#a3a3a3;--otto-divider:#3d3d3a;--otto-strip-bg:#1c1c1b;--otto-fare-bg:#3d3d3a;--otto-text-primary:#ffffff;--otto-text-body:#f5f5f5;--otto-text-secondary:#a3a3a3;--otto-link-hover:#38bdf8;--otto-banner-bg:#3d3d3a;--otto-chip-bg:#0d3d3a;--otto-chip-text:#ccfbf1;--otto-caution-bg:#3b2a12;--otto-caution-text:#f5b65b;--otto-positive-text:#5fd08a;--otto-photo-bg:#1c1c1b;}html,body{margin:0;background:transparent;font-family:var(--otto-font);line-height:var(--otto-line);color:var(--otto-text-primary);}#root{min-height:120px;padding:0;}button{font:inherit;}.icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--otto-icon-size);height:var(--otto-icon-size);line-height:1;}.icon svg{width:100%;height:100%;fill:currentColor;}.head{margin-bottom:var(--otto-space-5);}.title{font-size:var(--otto-text-lg);line-height:22px;font-weight:var(--otto-weight-semibold);}.subtitle,.empty{font-size:var(--otto-text-base);color:var(--otto-text-secondary);}.row{display:flex;gap:var(--otto-space-5);overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:var(--otto-space-2);}.opt{flex:0 0 var(--otto-card-width);width:var(--otto-card-width);min-width:0;overflow-wrap:anywhere;box-sizing:border-box;scroll-snap-align:start;display:flex;flex-direction:column;padding:var(--otto-space-7);background:var(--otto-card-bg);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);}.opt.chosen{border-color:var(--otto-accent);}@media (hover:hover){.opt:hover{border-color:var(--otto-accent);}}.chip{display:inline-flex;align-items:center;height:var(--otto-chip-height);padding:0 var(--otto-space-4);box-sizing:border-box;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;border-radius:var(--otto-radius-pill);font-size:var(--otto-text-sm);font-weight:var(--otto-weight-semibold);background:var(--otto-chip-bg);color:var(--otto-chip-text);}.chip.caution{background:var(--otto-caution-bg);color:var(--otto-caution-text);}.btn{flex-shrink:0;min-height:41px;padding:9px var(--otto-space-7);box-sizing:border-box;white-space:nowrap;cursor:pointer;border-radius:var(--otto-radius-md);font-size:var(--otto-text-lg);font-weight:var(--otto-weight-medium);display:inline-flex;align-items:center;justify-content:center;gap:var(--otto-space-2);background:transparent;color:var(--otto-text-primary);border:1px solid var(--otto-btn-border);}.btn:disabled{cursor:default;opacity:0.45;}.why{margin-top:var(--otto-space-7);font-size:var(--otto-text-base);color:var(--otto-text-body);}.whyHead{font-weight:var(--otto-weight-semibold);}.whyLine{display:flex;gap:var(--otto-space-3);}.tick{--otto-icon-size:12px;height:19.5px;font-size:var(--otto-text-md);}.tick.note{color:var(--otto-text-secondary);}@media (max-width:480px){.opt{flex-basis:min(var(--otto-card-width),82vw);width:min(var(--otto-card-width),82vw);}.btn{min-height:var(--otto-tap-min);}}.sm{--sm-seat:28px;--sm-gap:4px;--sm-aisle:16px;box-sizing:border-box;max-width:100%;padding:var(--otto-space-7);border:1px solid var(--otto-card-border);border-radius:var(--otto-radius-card);background:var(--otto-card-bg);color:var(--otto-text-primary);}.sm.roomy{--sm-seat:34px;}.sm.suite{--sm-seat:40px;--sm-gap:6px;--sm-aisle:22px;}.smHead{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--otto-space-5);margin-bottom:var(--otto-space-5);}.smHead .title{font-size:var(--otto-text-lg);font-weight:var(--otto-weight-semibold);}.smHead .subtitle{font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smToggle{min-height:32px;padding:4px var(--otto-space-5);font-size:var(--otto-text-md);}.smBody{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--otto-space-5);}.smBody .smScroll{flex:0 1 auto;min-width:0;}.smSide{flex:0 0 168px;display:flex;flex-direction:column;gap:var(--otto-space-2);}.smSideHead{font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smTraveler{box-sizing:border-box;padding:var(--otto-space-2) var(--otto-space-3);border:1px solid var(--otto-card-border);border-radius:8px;cursor:pointer;user-select:none;}.smTraveler:hover,.smTraveler.active{border-color:var(--otto-accent);}.smTraveler:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smTravelerName{font-size:var(--otto-text-md);color:var(--otto-text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.smTravelerSeat{display:flex;align-items:baseline;gap:var(--otto-space-2);font-size:var(--otto-text-base);font-weight:var(--otto-weight-semibold);font-variant-numeric:tabular-nums;}.smTravelerSeat .smFrom{color:var(--otto-text-secondary);font-weight:var(--otto-weight-medium);text-decoration:line-through;}.smTravelerSeat .smArrow{color:var(--otto-text-secondary);}.smTravelerSeat .smTo{color:var(--otto-accent);}.smTravelerNote{font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smScroll{overflow-x:auto;}.smGrid{display:inline-flex;flex-direction:column;gap:var(--otto-space-2);}.smRow{display:flex;align-items:center;gap:var(--sm-gap);}.smNum{width:28px;flex-shrink:0;font-size:var(--otto-text-md);color:var(--otto-text-secondary);font-variant-numeric:tabular-nums;}.smNum.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smLav{box-sizing:border-box;flex-shrink:0;height:var(--sm-seat);display:grid;place-items:center;overflow:hidden;border:1px dashed var(--otto-card-border);border-radius:6px;background:var(--otto-strip-bg);color:var(--otto-text-secondary);font-size:var(--otto-text-sm);white-space:nowrap;}.smRow.bulkhead{padding-top:var(--otto-space-2);border-top:3px solid var(--otto-text-secondary);}.smTag{flex-shrink:0;margin-left:var(--otto-space-2);font-size:var(--otto-text-sm);color:var(--otto-text-secondary);white-space:nowrap;}.smTag.exit{color:var(--otto-caution-text);font-weight:var(--otto-weight-semibold);}.smAisle{width:var(--sm-aisle);flex-shrink:0;}.smLetter,.smSeat,.smEmpty{width:var(--sm-seat);height:var(--sm-seat);flex-shrink:0;}.smLetter{height:auto;text-align:center;font-size:var(--otto-text-sm);color:var(--otto-text-secondary);}.smSeat{box-sizing:border-box;display:grid;place-items:center;padding:0;border:1px solid var(--otto-card-border);border-radius:6px;background:var(--otto-card-bg);color:var(--otto-text-body);font-size:var(--otto-text-md);font-weight:var(--otto-weight-medium);line-height:1;}.smSeat[role=\"button\"]{cursor:pointer;user-select:none;}.smSeat[role=\"button\"]:hover{border-color:var(--otto-accent);}.smSeat[role=\"button\"]:focus-visible{outline:2px solid var(--otto-accent);outline-offset:1px;}.smSeat.paid{background:var(--otto-caution-bg);}.smSeat.legroom{border-top-width:3px;}.smSeat.off{border-color:var(--otto-divider);background:var(--otto-strip-bg);color:var(--otto-text-secondary);cursor:default;}.smSeat.mine{border-color:var(--otto-accent);background:var(--otto-accent);color:#0a0a0a;font-weight:var(--otto-weight-semibold);}.smSeat.mine.active{outline:2px solid var(--otto-text-primary);outline-offset:1px;}.smSeat.pick{border:2px solid var(--otto-accent);background:var(--otto-chip-bg);color:var(--otto-chip-text);font-weight:var(--otto-weight-semibold);}.smLegend{display:flex;flex-wrap:wrap;gap:var(--otto-space-2) var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-md);color:var(--otto-text-secondary);}.smKey{display:inline-flex;align-items:center;gap:var(--otto-space-2);}.smKey .smSeat{--sm-seat:16px;font-size:9px;border-radius:4px;}.smFoot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--otto-space-5);margin-top:var(--otto-space-5);font-size:var(--otto-text-base);}.smFoot .btn{border:1px solid var(--otto-btn-border);color:var(--otto-text-primary);}.smFoot .btn:hover:not(:disabled){border-color:var(--otto-accent);}.sm.busy button,.sm.busy .smSeat{cursor:default;}";
|
|
15
15
|
const ICONS = {"hotels/check-regular-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M534 132.5C544.8 140.2 547.2 155.2 539.5 166L275.5 534C271.4 539.7 265 543.4 258 543.9C251 544.4 244 542 239 537L103 401C93.6 391.6 93.6 376.4 103 367.1C112.4 357.8 127.6 357.7 136.9 367.1L253 483L500.5 138C508.2 127.2 523.2 124.8 534 132.5z\"/></svg>","hotels/circle-info-regular-full":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><path d=\"M320 112C434.9 112 528 205.1 528 320C528 434.9 434.9 528 320 528C205.1 528 112 434.9 112 320C112 205.1 205.1 112 320 112zM320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM280 400C266.7 400 256 410.7 256 424C256 437.3 266.7 448 280 448L360 448C373.3 448 384 437.3 384 424C384 410.7 373.3 400 360 400L352 400L352 312C352 298.7 341.3 288 328 288L280 288C266.7 288 256 298.7 256 312C256 325.3 266.7 336 280 336L304 336L304 400L280 400zM320 256C337.7 256 352 241.7 352 224C352 206.3 337.7 192 320 192C302.3 192 288 206.3 288 224C288 241.7 302.3 256 320 256z\"/></svg>"};
|
|
16
16
|
const SYMBOLS = { USD: "$", EUR: "€", GBP: "£", JPY: "¥", CNY: "¥", AUD: "A$", CAD: "C$" };
|
|
17
17
|
// The widget cannot see when the host's reply ends, so the lock lifts on a timer.
|
|
@@ -171,35 +171,57 @@ function mount(root, card, spec) {
|
|
|
171
171
|
|
|
172
172
|
/* Otto seat map: OttoSeatMap.render(root, card), card from render_elastic_seat_map.
|
|
173
173
|
* The server lays the cabin out (columns, aisles, rows aligned to columns), so any aircraft
|
|
174
|
-
* and cabin draws the same way. Collapsed, it shows the
|
|
175
|
-
* side; Expand shows the whole cabin.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
174
|
+
* and cabin draws the same way. Collapsed, it shows the travelers' rows and one row either
|
|
175
|
+
* side; Expand shows the whole cabin. A lavatory (card.lavs) draws as one labeled block over its
|
|
176
|
+
* seat columns; a lavatory row without seats becomes a row of its own.
|
|
177
|
+
*
|
|
178
|
+
* One traveler: clicking another seat marks it New selection and shows "Update seat to 15D",
|
|
179
|
+
* which sends "Update seat to 15D (SEA → JFK)" as the traveler's message.
|
|
180
|
+
* A party (2+ card.travelers): a side panel lists each traveler and seat. Click a party seat,
|
|
181
|
+
* then a new seat: nothing is sent, the panel shows "23A → 24C". "Update seats" sends every
|
|
182
|
+
* change at once: "Update seats (SEA → JFK): Ana 23A → 24C, Ben 23B → 24D". */
|
|
178
183
|
|
|
179
184
|
function render(root, card) {
|
|
180
185
|
injectCss("otto-seat-map-css");
|
|
181
186
|
if (root.__ottoCard) root.__ottoCard.cancel();
|
|
182
|
-
|
|
187
|
+
const travelers = (card && card.travelers) || [];
|
|
188
|
+
if (!card || !card.rows || !card.rows.length || !travelers.length) {
|
|
183
189
|
root.replaceChildren(el("p", "empty", "No seat map to show."));
|
|
184
190
|
return;
|
|
185
191
|
}
|
|
186
192
|
|
|
193
|
+
// Row number → lavatory seat letters. Seatless lavatory rows join the rows in number order.
|
|
194
|
+
const lavs = new Map((card.lavs || []).map((lav) => [lav.row, lav.cols]));
|
|
195
|
+
const rows = [...card.rows];
|
|
196
|
+
for (const number of lavs.keys()) {
|
|
197
|
+
if (!rows.some((row) => row.number === number)) rows.push({ number, seats: card.columns.map(() => null), lavOnly: true });
|
|
198
|
+
}
|
|
199
|
+
rows.sort((a, b) => a.number - b.number);
|
|
187
200
|
const seats = new Map();
|
|
188
|
-
|
|
189
|
-
const home = seats.get(card.current_seat)?.index ?? 0;
|
|
201
|
+
rows.forEach((row, index) => row.seats.forEach((seat) => seat && seats.set(seat.id, { seat, index })));
|
|
190
202
|
const aisles = new Set(card.aisles_after || []);
|
|
203
|
+
|
|
204
|
+
const party = travelers.length > 1;
|
|
205
|
+
const owner = new Map(travelers.map((traveler, i) => [traveler.seat, i])); // seat id → traveler index
|
|
206
|
+
const homes = travelers.map((t) => seats.get(t.seat)?.index ?? 0);
|
|
207
|
+
const first = Math.min(...homes);
|
|
208
|
+
const last = Math.max(...homes);
|
|
209
|
+
|
|
191
210
|
let expanded = false;
|
|
192
|
-
let pick = null;
|
|
193
211
|
let busy = false;
|
|
194
212
|
let timer = null;
|
|
213
|
+
let pick = null; // one traveler: the new seat
|
|
214
|
+
let active = null; // a party: the traveler whose seat is being moved
|
|
215
|
+
const moves = new Map(); // a party: traveler index → new seat id
|
|
195
216
|
|
|
196
217
|
const letter = (id) => String(id).replace(/^\d+/, "");
|
|
197
218
|
const cost = (seat) => (seat.price ? formatExactMoney(seat.price, card.currency || "USD") : "free");
|
|
198
219
|
const describe = (seat) => [seat.id, seat.type, cost(seat)].filter(Boolean).join(" · ");
|
|
220
|
+
const movedTo = (id) => [...moves.entries()].find(([, to]) => to === id)?.[0];
|
|
199
221
|
|
|
200
222
|
function seatClass(seat) {
|
|
201
|
-
if (seat.id ===
|
|
202
|
-
if (seat.id === pick) return "pick";
|
|
223
|
+
if (owner.has(seat.id)) return "mine" + (party && active === owner.get(seat.id) ? " active" : "");
|
|
224
|
+
if (party ? movedTo(seat.id) !== undefined : seat.id === pick) return "pick";
|
|
203
225
|
return { free: "free", paid: "paid", unavailable: "off" }[seat.state] || "off";
|
|
204
226
|
}
|
|
205
227
|
|
|
@@ -222,11 +244,13 @@ function render(root, card) {
|
|
|
222
244
|
button.setAttribute("role", "button");
|
|
223
245
|
button.tabIndex = busy ? -1 : 0;
|
|
224
246
|
button.dataset.seat = seat.id;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
const who = party && owner.has(seat.id) ? travelers[owner.get(seat.id)].name : null;
|
|
248
|
+
button.title =
|
|
249
|
+
[who, describe(seat), seat.extra_legroom && "extra legroom", ...facts.map((f) => f.toLowerCase())]
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join(" · ") + (cls === "pick" ? " · new selection" : "");
|
|
228
252
|
button.setAttribute("aria-label", button.title);
|
|
229
|
-
button.setAttribute("aria-pressed", String(cls
|
|
253
|
+
button.setAttribute("aria-pressed", String(cls.startsWith("mine") || cls === "pick"));
|
|
230
254
|
return button;
|
|
231
255
|
}
|
|
232
256
|
|
|
@@ -240,14 +264,36 @@ function render(root, card) {
|
|
|
240
264
|
return line;
|
|
241
265
|
}
|
|
242
266
|
|
|
267
|
+
// One block over a run of empty lavatory cells, as wide as the seats and aisles it replaces.
|
|
268
|
+
function lavBlock(count, aisleCount) {
|
|
269
|
+
const block = el("span", "smLav", count >= 3 ? "Lavatory" : "WC");
|
|
270
|
+
block.style.width = `calc(var(--sm-seat) * ${count} + var(--sm-aisle) * ${aisleCount} + var(--sm-gap) * ${count + aisleCount - 1})`;
|
|
271
|
+
block.title = "Lavatory";
|
|
272
|
+
block.setAttribute("role", "img");
|
|
273
|
+
block.setAttribute("aria-label", "Lavatory");
|
|
274
|
+
return block;
|
|
275
|
+
}
|
|
276
|
+
|
|
243
277
|
// An exit row or bulkhead row gets a tag after its last seat; a bulkhead also draws the wall above it.
|
|
244
278
|
function seatRow(row) {
|
|
245
279
|
const facts = rowFacts(row);
|
|
280
|
+
const lav = lavs.get(row.number) || "";
|
|
281
|
+
const isLav = (i) => !row.seats[i] && lav.includes(card.columns[i]);
|
|
246
282
|
const line = el("div", "smRow" + (row.bulkhead ? " bulkhead" : ""));
|
|
247
|
-
const number = el("span", "smNum" + (row.exit ? " exit" : ""), row.number);
|
|
283
|
+
const number = el("span", "smNum" + (row.exit ? " exit" : ""), row.lavOnly ? "" : row.number);
|
|
248
284
|
if (facts.length) number.title = facts.join(" · ");
|
|
249
285
|
line.append(number);
|
|
250
286
|
row.seats.forEach((seat, i) => {
|
|
287
|
+
if (isLav(i)) {
|
|
288
|
+
if (i > 0 && isLav(i - 1)) return;
|
|
289
|
+
let end = i;
|
|
290
|
+
while (end + 1 < row.seats.length && isLav(end + 1)) end += 1;
|
|
291
|
+
let inner = 0;
|
|
292
|
+
for (let k = i; k < end; k += 1) if (aisles.has(k)) inner += 1;
|
|
293
|
+
line.append(lavBlock(end - i + 1, inner));
|
|
294
|
+
if (aisles.has(end)) line.append(el("span", "smAisle"));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
251
297
|
line.append(seat ? seatNode(seat, facts) : el("span", "smEmpty"));
|
|
252
298
|
if (aisles.has(i)) line.append(el("span", "smAisle"));
|
|
253
299
|
});
|
|
@@ -260,7 +306,7 @@ function render(root, card) {
|
|
|
260
306
|
function legend() {
|
|
261
307
|
const states = new Set([...seats.values()].map(({ seat }) => seat.state));
|
|
262
308
|
const keys = [
|
|
263
|
-
["mine", "Your seat", true],
|
|
309
|
+
["mine", party ? "Your party" : "Your seat", true],
|
|
264
310
|
["pick", "New selection", true],
|
|
265
311
|
["free", "Free", states.has("free")],
|
|
266
312
|
["paid", "Paid", states.has("paid")],
|
|
@@ -283,15 +329,66 @@ function render(root, card) {
|
|
|
283
329
|
return box;
|
|
284
330
|
}
|
|
285
331
|
|
|
332
|
+
// A party's side panel: each traveler's seat, or "23A → 24C" with the new seat's details.
|
|
333
|
+
function sidePanel() {
|
|
334
|
+
const side = el("div", "smSide");
|
|
335
|
+
const hint =
|
|
336
|
+
active !== null ? `Pick a new seat for ${travelers[active].name}` : moves.size ? "Seat changes" : "Tap a seat to change it";
|
|
337
|
+
side.append(el("div", "smSideHead", hint));
|
|
338
|
+
travelers.forEach((traveler, i) => {
|
|
339
|
+
const to = moves.get(i);
|
|
340
|
+
const row = el("div", "smTraveler" + (active === i ? " active" : ""));
|
|
341
|
+
row.setAttribute("role", "button");
|
|
342
|
+
row.tabIndex = busy ? -1 : 0;
|
|
343
|
+
row.dataset.traveler = String(i);
|
|
344
|
+
const seatLine = el("div", "smTravelerSeat");
|
|
345
|
+
if (to) {
|
|
346
|
+
seatLine.append(el("span", "smFrom", traveler.seat), el("span", "smArrow", "→"), el("span", "smTo", to));
|
|
347
|
+
} else {
|
|
348
|
+
seatLine.append(el("span", null, traveler.seat));
|
|
349
|
+
}
|
|
350
|
+
row.append(el("div", "smTravelerName", traveler.name), seatLine);
|
|
351
|
+
const target = to && seats.get(to)?.seat;
|
|
352
|
+
if (target) row.append(el("div", "smTravelerNote", [target.type, cost(target)].filter(Boolean).join(" · ")));
|
|
353
|
+
side.append(row);
|
|
354
|
+
});
|
|
355
|
+
return side;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function footer() {
|
|
359
|
+
const foot = el("div", "smFoot");
|
|
360
|
+
if (party) {
|
|
361
|
+
const count = moves.size;
|
|
362
|
+
foot.append(el("span", null, count ? `${count} seat change${count === 1 ? "" : "s"}` : ""));
|
|
363
|
+
if (count) {
|
|
364
|
+
const update = el("button", "btn", "Update seats");
|
|
365
|
+
update.type = "button";
|
|
366
|
+
update.dataset.update = "party";
|
|
367
|
+
foot.append(update);
|
|
368
|
+
}
|
|
369
|
+
return foot;
|
|
370
|
+
}
|
|
371
|
+
const mine = seats.get(travelers[0].seat)?.seat;
|
|
372
|
+
const chosen = pick && seats.get(pick)?.seat;
|
|
373
|
+
foot.append(el("span", null, chosen ? `New selection: ${describe(chosen)}` : mine ? `Your seat: ${describe(mine)}` : ""));
|
|
374
|
+
if (chosen) {
|
|
375
|
+
const update = el("button", "btn", `Update seat to ${chosen.id}`);
|
|
376
|
+
update.type = "button";
|
|
377
|
+
update.dataset.update = chosen.id;
|
|
378
|
+
foot.append(update);
|
|
379
|
+
}
|
|
380
|
+
return foot;
|
|
381
|
+
}
|
|
382
|
+
|
|
286
383
|
function draw() {
|
|
287
384
|
const scroll = root.querySelector(".smScroll")?.scrollLeft || 0;
|
|
288
|
-
const box = el("div", `sm ${card.size || "standard"}${busy ? " busy" : ""}`);
|
|
385
|
+
const box = el("div", `sm ${card.size || "standard"}${party ? " party" : ""}${busy ? " busy" : ""}`);
|
|
289
386
|
|
|
290
387
|
const head = el("div", "smHead");
|
|
291
388
|
const text = el("div");
|
|
292
389
|
text.append(el("div", "title", card.title), el("div", "subtitle", card.subtitle));
|
|
293
390
|
head.append(text);
|
|
294
|
-
if (
|
|
391
|
+
if (rows.length > last - first + 3) {
|
|
295
392
|
const toggle = el("button", "btn smToggle", expanded ? "Collapse" : "Expand");
|
|
296
393
|
toggle.type = "button";
|
|
297
394
|
toggle.dataset.toggle = "1";
|
|
@@ -299,24 +396,16 @@ function render(root, card) {
|
|
|
299
396
|
head.append(toggle);
|
|
300
397
|
}
|
|
301
398
|
|
|
302
|
-
const
|
|
399
|
+
const visible = expanded ? rows : rows.slice(Math.max(0, first - 1), last + 2);
|
|
303
400
|
const grid = el("div", "smGrid");
|
|
304
|
-
grid.append(lettersRow(), ...
|
|
401
|
+
grid.append(lettersRow(), ...visible.map(seatRow));
|
|
305
402
|
const scroller = el("div", "smScroll");
|
|
306
403
|
scroller.append(grid);
|
|
404
|
+
const body = el("div", "smBody");
|
|
405
|
+
body.append(scroller);
|
|
406
|
+
if (party) body.append(sidePanel());
|
|
307
407
|
|
|
308
|
-
|
|
309
|
-
const mine = seats.get(card.current_seat)?.seat;
|
|
310
|
-
const chosen = pick && seats.get(pick)?.seat;
|
|
311
|
-
foot.append(el("span", null, chosen ? `New selection: ${describe(chosen)}` : mine ? `Your seat: ${describe(mine)}` : ""));
|
|
312
|
-
if (chosen) {
|
|
313
|
-
const update = el("button", "btn", `Update seat to ${chosen.id}`);
|
|
314
|
-
update.type = "button";
|
|
315
|
-
update.dataset.update = chosen.id;
|
|
316
|
-
foot.append(update);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
box.append(head, scroller, legend(), foot);
|
|
408
|
+
box.append(head, body, legend(), footer());
|
|
320
409
|
root.replaceChildren(box);
|
|
321
410
|
scroller.scrollLeft = scroll;
|
|
322
411
|
root.querySelectorAll("button").forEach((b) => (b.disabled = busy));
|
|
@@ -328,38 +417,70 @@ function render(root, card) {
|
|
|
328
417
|
draw();
|
|
329
418
|
}
|
|
330
419
|
|
|
420
|
+
function send(message) {
|
|
421
|
+
busy = true;
|
|
422
|
+
timer = setTimeout(unlock, BUSY_MS);
|
|
423
|
+
try {
|
|
424
|
+
const sent = window.sendPrompt(message);
|
|
425
|
+
if (sent && typeof sent.catch === "function") sent.catch(unlock);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
unlock();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// A party: a party seat (or its row in the panel) picks who moves; another seat is the new
|
|
432
|
+
// seat; clicking a pending new seat again cancels that change.
|
|
433
|
+
function partyClick(id) {
|
|
434
|
+
if (owner.has(id)) {
|
|
435
|
+
const i = owner.get(id);
|
|
436
|
+
active = active === i ? null : i;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const cancelled = movedTo(id);
|
|
440
|
+
if (cancelled !== undefined) {
|
|
441
|
+
moves.delete(cancelled);
|
|
442
|
+
active = null;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (active === null) return;
|
|
446
|
+
moves.set(active, id);
|
|
447
|
+
active = null;
|
|
448
|
+
}
|
|
449
|
+
|
|
331
450
|
// Assigned, not added: a re-mount replaces the handler instead of stacking another.
|
|
332
451
|
root.onclick = (event) => {
|
|
333
452
|
if (busy) return;
|
|
334
|
-
const target = event.target.closest("button, [data-seat]");
|
|
453
|
+
const target = event.target.closest("button, [data-seat], [data-traveler]");
|
|
335
454
|
if (!target) return;
|
|
336
455
|
if (target.dataset.toggle) {
|
|
337
456
|
expanded = !expanded;
|
|
457
|
+
} else if (target.dataset.traveler) {
|
|
458
|
+
const i = Number(target.dataset.traveler);
|
|
459
|
+
active = active === i ? null : i;
|
|
338
460
|
} else if (target.dataset.seat) {
|
|
339
461
|
const id = target.dataset.seat;
|
|
340
|
-
|
|
462
|
+
if (party) partyClick(id);
|
|
463
|
+
else pick = id === travelers[0].seat || id === pick ? null : id;
|
|
464
|
+
} else if (target.dataset.update === "party") {
|
|
465
|
+
const changes = travelers
|
|
466
|
+
.map((traveler, i) => moves.has(i) && `${traveler.name} ${traveler.seat} → ${moves.get(i)}`)
|
|
467
|
+
.filter(Boolean);
|
|
468
|
+
send(`Update seats (${card.route}): ${changes.join(", ")}`);
|
|
341
469
|
} else if (target.dataset.update) {
|
|
342
|
-
|
|
343
|
-
timer = setTimeout(unlock, BUSY_MS);
|
|
344
|
-
try {
|
|
345
|
-
const sent = window.sendPrompt(`Update seat to ${target.dataset.update} (${card.route})`);
|
|
346
|
-
if (sent && typeof sent.catch === "function") sent.catch(unlock);
|
|
347
|
-
} catch (error) {
|
|
348
|
-
unlock();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
470
|
+
send(`Update seat to ${target.dataset.update} (${card.route})`);
|
|
351
471
|
}
|
|
352
472
|
draw();
|
|
353
473
|
};
|
|
354
474
|
|
|
355
|
-
// Enter and Space
|
|
475
|
+
// Enter and Space act as a click on a seat or a traveler, as they would on a native button.
|
|
356
476
|
root.onkeydown = (event) => {
|
|
357
|
-
|
|
477
|
+
const data = event.target.dataset || {};
|
|
478
|
+
if ((event.key === "Enter" || event.key === " ") && (data.seat || data.traveler)) {
|
|
358
479
|
event.preventDefault();
|
|
359
|
-
const
|
|
480
|
+
const selector = data.seat ? `[data-seat="${data.seat}"]` : `[data-traveler="${data.traveler}"]`;
|
|
360
481
|
event.target.click();
|
|
361
|
-
// draw() replaced the node; keep keyboard focus on the same
|
|
362
|
-
root.querySelector(
|
|
482
|
+
// draw() replaced the node; keep keyboard focus on the same one.
|
|
483
|
+
root.querySelector(selector)?.focus();
|
|
363
484
|
}
|
|
364
485
|
};
|
|
365
486
|
|