BareGit

Support multiple floors. Use 98 style.

Author: MetroWind <chris.corsair@gmail.com>
Date: Sun Aug 31 09:10:28 2025 -0700
Commit: 3204043e5cd855855b045d8f410b320dcb772d25

Changes

diff --git a/index.html b/index.html
index f46f57c..54d6aa3 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,10 @@
     <title>My test page</title>
   </head>
   <body>
-    <div id="Plan">
+    <div class="Window Dialog">
+      <div class="Titlebar">NMS Freighter Planner</div>
+      <div id="Plan">
+      </div>
     </div>
   </body>
 </html>
diff --git a/scripts/main.js b/scripts/main.js
index 7347f37..b8de9e4 100644
--- a/scripts/main.js
+++ b/scripts/main.js
@@ -1,6 +1,7 @@
 const DEFAULT_APP_STATE = {
-    floor: 1,
-    plan: new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0),
+    floor: 0,                   // This is 0-based.
+    plan: new Array(FLOOR_COUNT).fill(
+        new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0)),
     // mouse_state can be “normal”, or “dnd”.
     mouse_state: "normal",
     plan_code: "",
@@ -9,7 +10,9 @@ const DEFAULT_APP_STATE = {
 async function serializePlan(app_state)
 {
     let rest = new Array(GRID_SIZE_X * GRID_SIZE_Y * (FLOOR_COUNT - 1)).fill(0);
-    return compressBytes(Uint8Array.from(app_state.plan.concat(rest)));
+    let concated_plans =
+        app_state.plan.reduce((big, floor_plan) => big.concat(floor_plan), []);
+    return compressBytes(Uint8Array.from(concated_plans));
 }
 
 function genGrid(x_count, y_count, cell_size)
@@ -105,11 +108,21 @@ ${cell_y * (CELL_SIZE + 1) + 1})`}, ASSET_WHITE_BG, TILES[idx].inner_svg);
 
 function PlanView({plan, mouse_state})
 {
+    console.debug("Looking at plan ", plan);
     return h("div", {},
-             h("p", {}, "The plan:"),
              h(PlanGridView, {content: plan, mouse_state: mouse_state}));
 }
 
+// on_floor_change takes one argument, which is the 0-based floor number.
+function FloorSelector({floor, on_floor_change})
+{
+    return h("div", {"id": "FloorSelectorWrapper", "class": "ButtonRow"},
+             h("label", {}, "Floor"),
+             h("input", {"id": "InputFloor", "type": "number", "step": 1, "min": 1,
+                         "max": 15, "value": floor,
+                         onchange: e => on_floor_change(e.target.value - 1),},));
+}
+
 function App({initial_state})
 {
     const [state, setState] = preactHooks.useState(initial_state);
@@ -134,7 +147,7 @@ function App({initial_state})
         let asset_index = parseInt(e.target.getAttribute("data-asset-index"));
 
         let new_state = structuredClone(state);
-        new_state.plan[cell_index] = asset_index;
+        new_state.plan[new_state.floor][cell_index] = asset_index;
         new_state.mouse_state = "normal";
         serializePlan(new_state).then((s) => {
             new_state.plan_code = s;
@@ -168,16 +181,27 @@ function App({initial_state})
             encodeURIComponent(svg_str);
     }
 
+    // new_floor is 0-based.
+    function onFloorChange(new_floor)
+    {
+        let new_state = structuredClone(state);
+        new_state.floor = new_floor;
+        setState(new_state);
+    }
+
     return h("div", {},
-             h("h2", {}, `Floor ${state.floor}`),
+             h(FloorSelector, {floor: state.floor + 1,
+                               on_floor_change: onFloorChange}),
              h(AssetsView, {on_drag_begin: onDragAssetBegin,
                             on_drag_end: onDragAssetEnd}),
-             h(PlanView, {plan: state.plan, mouse_state: state.mouse_state}),
-             h("div", {},
-               h("a", {"href": "javascript:void(0);", onclick: onClickSaveImg},
-                 "Open Image!")),
+             h("hr", {}),
+             h(PlanView, {plan: state.plan[state.floor],
+                          mouse_state: state.mouse_state}),
              h("div", {},
                h("textarea", {readonly: true}, state.plan_code)),
+             h("div", {"class": "ButtonRow"},
+               h("button", {onclick: onClickSaveImg, type: "button"},
+                 "Open as Image!")),
             );
 }
 
@@ -198,7 +222,17 @@ else
 {
     decompressBytes(encoded_plan).then(a => {
         let state = structuredClone(DEFAULT_APP_STATE);
-        state.plan = Array.from(a.subarray(0, GRID_SIZE_X * GRID_SIZE_Y));
+        let concated_plan = Array.from(a);
+        console.debug(concated_plan);
+        state.plan = [];
+        for(let i = 0; i < GRID_SIZE_X * GRID_SIZE_Y * FLOOR_COUNT;
+            i += GRID_SIZE_X * GRID_SIZE_Y)
+        {
+            let floor_slice =
+                concated_plan.slice(i, i + GRID_SIZE_X * GRID_SIZE_Y);
+            console.debug(floor_slice);
+            state.plan.push(floor_slice);
+        }
         state.plan_code = encoded_plan;
         render(state);
     });
diff --git a/styles.css b/styles.css
index 73e6899..69ab118 100644
--- a/styles.css
+++ b/styles.css
@@ -1,5 +1,64 @@
-body
+/* http://meyerweb.com/eric/tools/css/reset/
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: unset;
+	font: inherit;
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+
+/* ========== Actual styles ======================================> */
+
+:root
 {
+    --color-bg: #c0c0c0;
+    --color-fg: black;
+    --color-border-dark-1: #808080;
+    --color-border-dark-2: black;
+    --color-border-light-1: #dfdfdf;
+    --color-border-light-2: white;
+    --color-titlebar-1: navy;
+    --color-titlebar-2: #1084d0;
 }
 
 *
@@ -7,6 +66,188 @@ body
     box-sizing: border-box;
 }
 
+html
+{
+    background: #008080;
+    color: var(--color-fg);
+    font-family: sans-serif;
+    font-size: 12px;
+    padding: 12px;
+}
+
+.Window
+{
+    box-shadow: inset -1px -1px var(--color-border-dark-2),
+                inset 1px 1px var(--color-border-light-1),
+                inset -2px -2px var(--color-border-dark-1),
+                inset 2px 2px var(--color-border-light-2);
+    background: var(--color-bg);
+    padding: 10px;
+
+    .Titlebar
+    {
+        background: linear-gradient(90deg, var(--color-titlebar-1), var(--color-titlebar-2));
+        margin: -6px -6px 10px -6px;
+        color: white;
+        padding: 3px;
+    }
+}
+
+.Dialog
+{
+    max-width: 800px;
+    margin: 0px auto;
+}
+
+input[type="number"]
+{
+    appearance: none;
+    border: none;
+    outline: none;
+    box-shadow: inset -1px -1px var(--color-border-light-2),
+                inset 1px 1px var(--color-border-dark-1),
+                inset -2px -2px var(--color-border-light-1),
+                inset 2px 2px var(--color-border-dark-2);
+    padding: 4px 0px 4px 6px;
+    font-family: monospace;
+    font-size: 0.9rem;
+}
+
+input[type="text"], input[type="url"], input[type="email"], textarea
+{
+    appearance: none;
+    border: none;
+    outline: none;
+    box-shadow: inset -1px -1px var(--color-border-light-2),
+                inset 1px 1px var(--color-border-dark-1),
+                inset -2px -2px var(--color-border-light-1),
+                inset 2px 2px var(--color-border-dark-2);
+    width: 100%;
+    padding: 4px 6px;
+    font-family: monospace;
+    font-size: 0.9rem;
+}
+
+a.Button, a.FloatButton
+{
+    color: black;
+    text-decoration: none;
+}
+
+button, input[type="submit"]
+{
+    appearance: none;
+    border: none;
+    outline: none;
+    box-shadow: inset -1px -1px var(--color-border-dark-2),
+                inset 1px 1px var(--color-border-light-1),
+                inset -2px -2px var(--color-border-dark-1),
+                inset 2px 2px var(--color-border-light-2);
+    background: var(--color-bg);
+    min-height: 23px;
+    min-width: 75px;
+    padding: 0 12px;
+    text-align: center;
+}
+
+.IconButton
+{
+    padding: 4px;
+    min-width: unset;
+    min-height: unset;
+}
+
+.FloatButton
+{
+    padding: 4px;
+    min-width: unset;
+    min-height: unset;
+    box-shadow: none;
+}
+
+.FloatButton:hover
+{
+    padding: 4px;
+    min-width: unset;
+    min-height: unset;
+    box-shadow: inset 1px 1px var(--color-border-light-1),
+                inset -1px -1px var(--color-border-dark-1);
+}
+
+button:active, .FloatButton:active
+{
+    box-shadow: inset -1px -1px var(--color-border-light-2),
+                inset 1px 1px var(--color-border-dark-1),
+                inset -2px -2px var(--color-border-light-1),
+                inset 2px 2px var(--color-border-dark-2);
+}
+
+hr
+{
+    border: none;
+    border-top: solid var(--color-border-dark-1) 1px;
+    border-bottom: solid var(--color-border-light-1) 1px;
+}
+
+.ButtonRow
+{
+    display: flex;
+    justify-content: right;
+    gap: 10px;
+    margin: 10px 0;
+    align-items: baseline;
+}
+
+.ButtonRowLeft
+{
+    text-align: Left;
+    margin-top: 10px;
+}
+
+.Toolbar
+{
+    display: flex;
+    border-bottom: solid var(--color-border-dark-1) 1px;
+    box-shadow: 0px 1px var(--color-border-light-1);
+    position: relative;
+    left: -10px;
+    top: -10px;
+    width: calc(100% + 2ex);
+    padding: 2px 8px;
+}
+
+table.InputFields
+{
+    border-collapse: collapse;
+    width: 100%;
+    table-layout: auto;
+
+    td
+    {
+        padding: 2px 2px;
+    }
+
+    td:nth-child(2)
+    {
+        width: 100%;
+    }
+
+}
+
+.StatusBar
+{
+    margin: 10px -6px -7px -6px;
+    display: flex;
+}
+
+.StatusCell
+{
+    box-shadow: inset -2px -2px var(--color-border-light-2),
+                inset 2px 2px var(--color-border-dark-1);
+    padding: 4px;
+    flex-grow: 1;
+}
+
 .Asset
 {
     display: inline-block;
@@ -15,8 +256,74 @@ body
     height: 34px;
 }
 
+
+
 /* Prevent SVG elements from interferring with drag n drop. */
 #Grid *
 {
     pointer-events: none;
 }
+
+nav
+{
+    display: flex;
+    justify-content: space-between;
+}
+
+#Links
+{
+    box-shadow: inset -1px -1px var(--color-border-light-2),
+                inset 1px 1px var(--color-border-dark-1),
+                inset -2px -2px var(--color-border-light-1),
+                inset 2px 2px var(--color-border-dark-2);
+    padding: 2px;
+}
+
+table.TableView
+{
+    background-color: white;
+    border-collapse: collapse;
+    width: 100%;
+
+    thead th
+    {
+        box-shadow: inset -1px -1px var(--color-border-dark-2),
+                    inset 1px 1px var(--color-border-light-1),
+                    inset -2px -2px var(--color-border-dark-1),
+                    inset 2px 2px var(--color-border-light-2);
+        background-color: var(--color-bg);
+        padding: 2px 4px 4px 4px;
+        text-align: left;
+    }
+
+    td
+    {
+        padding: 2px;
+    }
+}
+
+#Links table
+{
+    table-layout: fixed;
+
+    th:nth-child(1)
+    {
+        width: 10em;
+    }
+
+    td:nth-child(2)
+    {
+        overflow: hidden;
+        white-space: nowrap;
+    }
+
+    th:nth-child(3), th:nth-child(4)
+    {
+        width: 55px;
+    }
+
+    td:nth-child(3), td:nth-child(4)
+    {
+        text-align: center;
+    }
+}