← back to Dw Activation Calendar
Restyle DW Calendar to luxe Figma spec (Cormorant/Inter, plum/brass palette, white-card layout) — preserve all Google Calendar CRUD, 409 not-connected handling, tabs, modal-rig drag/resize, and SKU Activation grid
73ec16d403b8909f51d63a1a32e9eb60848e04a5 · 2026-06-24 14:03:44 -0700 · Steve
Files touched
Diff
commit 73ec16d403b8909f51d63a1a32e9eb60848e04a5
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jun 24 14:03:44 2026 -0700
Restyle DW Calendar to luxe Figma spec (Cormorant/Inter, plum/brass palette, white-card layout) — preserve all Google Calendar CRUD, 409 not-connected handling, tabs, modal-rig drag/resize, and SKU Activation grid
---
public/index.html | 368 ++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 259 insertions(+), 109 deletions(-)
diff --git a/public/index.html b/public/index.html
index d65886f..fd7f57f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,132 +4,235 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DW Calendar</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
- --bg:#0e0f12; --panel:#15171c; --panel2:#1b1e25; --line:#2a2e38;
- --ink:#ece8df; --ink-2:#b8b3a6; --ink-3:#7c7768; --gold:#c2a064; --gold-dim:#8a7240;
- --card:#16181d; --border:#2a2e38;
+ /* Luxe DW palette (Figma tokens — verbatim) */
+ --ground:#FAF8F3; --surface:#FFFFFF; --ink:#1F1B17; --muted:#8A8178;
+ --faint:#C2BBAE; --hairline:#E7E1D6; --plum:#5E2B3C; --brass:#B0894F;
+ --green:#3E7C5A; --red:#A23B3B;
+ --out-month:#FBF9F5;
+ /* chip tints */
+ --chip-brass-bg:#F4ECDD; --chip-brass-fg:#7A571C;
+ --chip-plum-bg:#F3E7EC; --chip-plum-fg:#6E2A40;
+ --chip-green-bg:#E8F0EA; --chip-green-fg:#2F5C43;
+ /* not-connected banner */
+ --warn-bg:#FBF3E3; --warn-bd:#E8D6A8; --warn-fg:#7A571C;
+ --serif:"Cormorant Garamond",Georgia,"Times New Roman",serif;
+ --ui:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
}
*{box-sizing:border-box}
- body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
- header{display:flex;align-items:center;gap:20px;padding:14px 22px;border-bottom:1px solid var(--line);flex-wrap:wrap;position:sticky;top:0;background:var(--bg);z-index:5}
- .wordmark{font-family:Georgia,"Times New Roman",serif;font-size:21px;letter-spacing:.3px}
- .wordmark .dot{color:var(--gold)}
- .sub{font-size:11px;color:var(--ink-3);text-transform:uppercase;letter-spacing:1.5px}
+ html,body{height:100%}
+ body{margin:0;background:var(--ground);color:var(--ink);font:14px/1.5 var(--ui);-webkit-font-smoothing:antialiased}
+ .wrap{max-width:1200px;margin:32px auto;background:var(--surface);border:1px solid var(--hairline);border-radius:20px;padding:40px 48px 48px;box-shadow:0 1px 2px rgba(31,27,23,.03)}
+
+ /* ---- header ---- */
+ header{display:flex;align-items:flex-start;gap:20px;padding-bottom:24px;border-bottom:1px solid var(--hairline)}
+ .brand{flex:1;min-width:0}
+ .wordmark{font-family:var(--serif);font-weight:600;font-size:32px;line-height:1.05;color:var(--ink);letter-spacing:.2px}
+ .sub{font-family:var(--ui);font-size:11px;font-weight:600;color:var(--brass);text-transform:uppercase;letter-spacing:4px;margin-top:4px}
.spacer{flex:1}
- .ctrl{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2)}
- .ctrl input{background:var(--panel2);border:1px solid var(--line);border-radius:6px;color:var(--ink);font:inherit;font-size:12px;padding:5px 8px;width:64px}
- .ctrl input[type=date]{width:130px}
- button{background:var(--gold-dim);border:none;color:#1a1a1a;font:inherit;font-size:12px;font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer}
- button:hover{background:var(--gold)}
- button.ghost{background:none;border:1px solid var(--line);color:var(--ink-2)}
- button.ghost:hover{color:var(--ink);border-color:var(--gold-dim)}
- button.danger{background:#7a3a3a;color:#f3e6e6}
- button.danger:hover{background:#9b4848}
- /* tabs */
- .tabs{display:flex;gap:4px;padding:0 22px;border-bottom:1px solid var(--line);background:var(--bg);position:sticky;top:53px;z-index:4}
- .tab{background:none;border:none;border-bottom:2px solid transparent;color:var(--ink-3);font-weight:600;padding:11px 16px;cursor:pointer;font-size:13px;border-radius:0}
- .tab:hover{color:var(--ink-2);background:none}
- .tab.active{color:var(--gold);border-bottom-color:var(--gold)}
+ button{font:inherit;cursor:pointer;border:none}
+ .btn-primary{background:var(--plum);color:#fff;font-weight:600;font-size:14px;padding:11px 20px;border-radius:10px;box-shadow:0 1px 2px rgba(94,43,60,.25)}
+ .btn-primary:hover{background:#4d2230}
+ .btn-ghost{background:var(--surface);border:1px solid var(--hairline);color:var(--ink);font-size:13px;font-weight:500;padding:8px 16px;border-radius:999px}
+ .btn-ghost:hover{border-color:var(--faint)}
+ .btn-pill{background:var(--surface);border:1px solid var(--hairline);color:var(--ink);font-size:13px;font-weight:500;padding:8px 18px;border-radius:999px}
+ .btn-pill:hover{border-color:var(--faint)}
+ /* SKU-view inline controls */
+ #skuCtrls{display:none;align-items:center;gap:16px;margin-left:18px}
+ .ctrl{display:flex;flex-direction:column;gap:3px;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
+ .ctrl input{font:inherit;font-size:13px;color:var(--ink);background:var(--surface);border:1px solid var(--hairline);border-radius:8px;padding:7px 9px;width:74px}
+ .ctrl input[type=date]{width:140px}
+ #apply{background:var(--plum);color:#fff;font-weight:600;font-size:13px;padding:9px 16px;border-radius:8px;align-self:flex-end}
+
+ /* ---- tab bar ---- */
+ .tabbar{display:flex;align-items:center;gap:8px;margin:24px 0 4px;padding:5px;background:var(--ground);border:1px solid var(--hairline);border-radius:14px}
+ .tab{background:transparent;color:var(--muted);font-weight:500;font-size:14px;padding:9px 18px;border-radius:10px;transition:all .12s}
+ .tab:hover{color:var(--ink)}
+ .tab.active{background:var(--surface);color:var(--ink);font-weight:600;box-shadow:0 1px 3px rgba(31,27,23,.08)}
+ .conn-pill{margin-left:auto;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);background:var(--surface);border:1px solid var(--hairline);border-radius:999px;padding:7px 16px}
+ .conn-pill .dot{width:8px;height:8px;border-radius:50%;background:var(--green);flex:0 0 auto}
+ .conn-pill.off .dot{background:var(--faint)}
+ .conn-pill code{font-family:var(--ui);font-size:12px;color:var(--ink)}
+ .conn-pill button{background:none;color:var(--muted);font-size:12px;text-decoration:underline;padding:0 0 0 6px}
+ .conn-pill a{text-decoration:none}
+ .conn-pill a button{color:var(--plum);text-decoration:underline}
+
.view{display:none}
.view.active{display:block}
- /* connection banner */
- .banner{margin:14px 22px;padding:11px 16px;border-radius:9px;border:1px solid var(--gold-dim);background:rgba(194,160,100,.08);color:var(--ink-2);font-size:13px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
- .banner.ok{border-color:#3c6d3c;background:rgba(109,179,109,.08)}
- .banner b{color:var(--ink)}
- /* SKU view (existing) */
- .summary{display:flex;gap:22px;padding:10px 22px;border-bottom:1px solid var(--line);font-size:12px;color:var(--ink-2);flex-wrap:wrap}
- .summary b{color:var(--ink);font-size:15px}
- .summary .k{display:flex;flex-direction:column;gap:1px}
- .monthbar{display:flex;align-items:center;justify-content:center;gap:18px;padding:12px;font-size:15px}
- .monthbar .ml{min-width:200px;text-align:center;font-family:Georgia,serif;font-size:18px}
- .cal{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;padding:0 22px 30px}
- .dow{font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:1px;text-align:center;padding:4px 0}
- .cell{background:var(--panel);border:1px solid var(--line);border-radius:9px;min-height:150px;max-height:340px;display:flex;flex-direction:column;overflow:hidden}
- .cell.empty{background:transparent;border:none;min-height:0}
- .cell.blank{background:#101216;opacity:.5}
- .cell .dh{display:flex;align-items:center;justify-content:space-between;padding:6px 9px;border-bottom:1px solid var(--line)}
- .cell .dn{font-size:13px;color:var(--ink)}
- .cell .cnt{font-size:10px;color:var(--gold);background:var(--panel2);border:1px solid var(--gold-dim);border-radius:10px;padding:1px 7px}
- .chips{flex:1;overflow-y:auto;padding:7px;display:grid;grid-template-columns:repeat(auto-fill,minmax(58px,1fr));gap:5px;align-content:start}
- .chip{position:relative;border-radius:6px;overflow:hidden;background:var(--panel2);border:1px solid var(--line);aspect-ratio:1/1;cursor:default}
- .chip img{width:100%;height:100%;object-fit:cover;display:block;background:#222}
- .chip .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:#1a1a1a}
- .chip .vn{position:absolute;left:0;right:0;bottom:0;font-size:7px;line-height:1.15;background:rgba(0,0,0,.72);color:#e9d9a8;padding:2px 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.2px}
- .chip .fx{position:absolute;top:2px;left:2px;width:6px;height:6px;border-radius:50%}
- .chip .fx.s{background:#6db36d}.chip .fx.r{background:#c2a064}
- .chips::-webkit-scrollbar{width:7px}.chips::-webkit-scrollbar-thumb{background:var(--line);border-radius:4px}
- .legend{display:flex;gap:10px;flex-wrap:wrap;padding:8px 22px 18px;font-size:11px;color:var(--ink-3)}
+
+ /* ---- not-connected banner ---- */
+ .banner{margin:18px 0 0;padding:13px 18px;border-radius:12px;border:1px solid var(--warn-bd);background:var(--warn-bg);color:var(--warn-fg);font-size:13.5px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
+ .banner.hidden{display:none}
+ .banner b{color:var(--warn-fg);font-weight:600}
+ .banner a{text-decoration:none}
+
+ /* ---- month toolbar ---- */
+ .monthbar{display:flex;align-items:center;gap:16px;margin:22px 0 14px}
+ .monthbar .nav{background:none;color:var(--muted);font-size:22px;line-height:1;padding:2px 6px;border-radius:8px}
+ .monthbar .nav:hover{color:var(--ink);background:var(--ground)}
+ .monthbar .ml{font-family:var(--serif);font-weight:500;font-size:26px;color:var(--ink);min-width:200px;letter-spacing:.2px}
+ .monthbar .spacer{flex:1}
+
+ /* ---- month card grid ---- */
+ .cal{border:1px solid var(--hairline);border-radius:14px;overflow:hidden;background:var(--surface)}
+ .cal .dow-row{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--hairline)}
+ .dow{height:40px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px}
+ .grid{display:grid;grid-template-columns:repeat(7,1fr)}
+ .cell{min-height:112px;border-right:1px solid var(--hairline);border-bottom:1px solid var(--hairline);padding:8px 9px;display:flex;flex-direction:column;gap:5px;overflow:hidden}
+ .cell:nth-child(7n){border-right:none}
+ .grid .cell:nth-last-child(-n+7){border-bottom:none}
+ .cell.out{background:var(--out-month)}
+ .cell.out .dn{color:var(--faint)}
+ .cell .dn{font-size:13px;font-weight:500;color:var(--ink);width:28px;height:28px;display:flex;align-items:center;justify-content:center}
+ .cell.today .dn{background:var(--plum);color:#fff;border-radius:50%}
+ .cell .cnt{font-size:10px;font-weight:600;color:var(--brass)}
+ .cell .dh{display:flex;align-items:center;justify-content:space-between}
+
+ /* Google event chips */
+ .gcell .evs{display:flex;flex-direction:column;gap:4px;overflow-y:auto}
+ .ev{display:block;width:100%;text-align:left;border:none;border-radius:6px;padding:4px 8px;cursor:pointer;font-size:11.5px;line-height:1.3;overflow:hidden;
+ background:var(--chip-brass-bg);color:var(--chip-brass-fg)}
+ .ev.cat-plum{background:var(--chip-plum-bg);color:var(--chip-plum-fg)}
+ .ev.cat-green{background:var(--chip-green-bg);color:var(--chip-green-fg)}
+ .ev.cat-brass{background:var(--chip-brass-bg);color:var(--chip-brass-fg)}
+ .ev:hover{filter:brightness(.97)}
+ .ev .et{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .ev .ewhen{opacity:.85;font-size:10.5px}
+ .ev .ecreated{opacity:.65;font-size:9px;margin-top:1px}
+ .addbtn{margin-top:auto;align-self:flex-start;font-size:11px;color:var(--faint);background:none;padding:2px 0;opacity:0;transition:opacity .12s}
+ .gcell:hover .addbtn{opacity:1}
+ .addbtn:hover{color:var(--muted)}
+
+ /* ---- SKU activation view ---- */
+ .summary{display:flex;gap:32px;margin:18px 0 6px;flex-wrap:wrap}
+ .summary .k{display:flex;flex-direction:column;gap:2px}
+ .summary .k span{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px}
+ .summary .k b{font-family:var(--serif);font-weight:500;font-size:24px;color:var(--ink)}
+ .note{font-size:12px;color:var(--muted);margin:6px 0 0;line-height:1.5}
+ .note b{color:var(--ink);font-weight:600}
+ .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;font-size:11px;color:var(--muted)}
.legend .li{display:flex;align-items:center;gap:5px}
.legend .sw{width:10px;height:10px;border-radius:3px;display:inline-block}
- .note{padding:0 22px 10px;font-size:11px;color:var(--ink-3)}
- .loading{padding:60px;text-align:center;color:var(--ink-3)}
- /* Google cal events */
- .gcell .ev{display:block;width:100%;text-align:left;background:var(--panel2);border:1px solid var(--gold-dim);border-radius:5px;color:var(--ink);font-size:10px;line-height:1.3;padding:3px 5px;margin:0 0 4px;cursor:pointer;overflow:hidden}
- .gcell .ev:hover{border-color:var(--gold);background:#23262e}
- .gcell .ev .et{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
- .gcell .ev .ewhen{color:var(--gold)}
- .gcell .ev .ecreated{color:var(--ink-3);font-size:8px;margin-top:1px}
- .gcell .addbtn{margin:4px 0 0;font-size:10px;padding:2px 8px;background:none;border:1px dashed var(--line);color:var(--ink-3)}
- .gcell .addbtn:hover{border-color:var(--gold-dim);color:var(--ink-2);background:none}
- /* modal form */
- .mform label{display:block;font-size:11px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.5px;margin:12px 0 4px}
+ .loading{padding:60px;text-align:center;color:var(--muted)}
+
+ /* SKU chips (image tiles) */
+ .scell .chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:5px;overflow-y:auto;align-content:start}
+ .chip{position:relative;border-radius:6px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);aspect-ratio:1/1}
+ .chip img{width:100%;height:100%;object-fit:cover;display:block}
+ .chip .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:#fff}
+ .chip .vn{position:absolute;left:0;right:0;bottom:0;font-size:7px;line-height:1.15;background:rgba(31,27,23,.72);color:#f3ead7;padding:2px 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.2px}
+ .chip .fx{position:absolute;top:2px;left:2px;width:6px;height:6px;border-radius:50%}
+ .chip .fx.s{background:var(--green)}.chip .fx.r{background:var(--brass)}
+ .chips::-webkit-scrollbar,.evs::-webkit-scrollbar{width:7px}
+ .chips::-webkit-scrollbar-thumb,.evs::-webkit-scrollbar-thumb{background:var(--hairline);border-radius:4px}
+
+ /* ===== modal (rig-built panel restyled to the luxe Figma look) ===== */
+ /* The rig sets inline cssText on backdrop/panel/head/body; override with high
+ specificity + !important so drag/resize/close wiring stays intact. */
+ [id^="modalrig-bd-"]{background:rgba(31,27,23,.28)!important;backdrop-filter:blur(1px)}
+ [id^="modalrig-bd-"] > div{ /* the panel */
+ background:var(--surface)!important;border:1px solid var(--hairline)!important;
+ border-radius:16px!important;box-shadow:0 24px 64px rgba(31,27,23,.22)!important;overflow:hidden!important}
+ [id^="modalrig-bd-"] > div > div:first-child{ /* the rig head → grab-handle bar */
+ background:var(--surface)!important;border-bottom:none!important;border-radius:16px 16px 0 0!important;
+ justify-content:center!important;padding:10px 0 0!important;min-height:18px}
+ [id^="modalrig-bd-"] > div > div:first-child span:first-child{display:none!important} /* hide rig title */
+ [id^="modalrig-bd-"] > div > div:first-child::before{
+ content:"";width:40px;height:4px;border-radius:2px;background:var(--hairline);display:block}
+ [id^="modalrig-bd-"] [data-rig-btn]{display:none!important} /* hide rig × + maximize; we add our own close */
+ [id^="modalrig-bd-"] > div > div:nth-child(2){padding:8px 28px 24px!important} /* body */
+
+ .mform{font-family:var(--ui)}
+ .mform .mhead{display:flex;align-items:center;justify-content:space-between;margin:4px 0 18px}
+ .mform .mtitle{font-family:var(--serif);font-weight:600;font-size:24px;color:var(--ink)}
+ .mform .mclose{width:30px;height:30px;border-radius:50%;background:var(--ground);color:var(--muted);font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;border:none}
+ .mform .mclose:hover{background:var(--hairline);color:var(--ink)}
+ .mform label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.7px;font-weight:600;margin:14px 0 5px}
.mform input[type=text],.mform textarea,.mform input[type=datetime-local],.mform input[type=date]{
- width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:6px;color:var(--ink);font:inherit;font-size:13px;padding:8px 10px}
- .mform textarea{min-height:64px;resize:vertical}
- .mform .row{display:flex;gap:12px}.mform .row>div{flex:1}
- .mform .chk{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:13px;color:var(--ink-2)}
- .mform .actions{display:flex;justify-content:space-between;align-items:center;margin-top:18px;gap:10px}
- .mform .msg{font-size:12px;min-height:16px}
- .mform .msg.err{color:#e08a8a}.mform .msg.ok{color:#8fd08f}
+ width:100%;font:inherit;font-size:14px;color:var(--ink);background:var(--surface);border:1px solid var(--hairline);border-radius:8px;padding:10px 12px}
+ .mform input:focus,.mform textarea:focus{outline:none;border-color:var(--brass)}
+ .mform textarea{min-height:86px;resize:vertical;line-height:1.45}
+ .mform .row{display:flex;gap:16px}.mform .row>div{flex:1}
+ /* all-day pill toggle */
+ .mform .chk{display:flex;align-items:center;justify-content:space-between;margin-top:16px;font-size:14px;font-weight:500;color:var(--ink)}
+ .mform .chk input{position:absolute;opacity:0;pointer-events:none}
+ .mform .toggle{width:44px;height:24px;border-radius:999px;background:var(--hairline);position:relative;transition:background .15s;cursor:pointer;flex:0 0 auto}
+ .mform .toggle::after{content:"";position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform .15s}
+ .mform .chk input:checked + .toggle{background:var(--plum)}
+ .mform .chk input:checked + .toggle::after{transform:translateX(20px)}
+ /* color picker dots */
+ .mform .colorpick{display:flex;align-items:center;justify-content:space-between;margin-top:18px}
+ .mform .colorpick .lab{font-size:14px;font-weight:500;color:var(--ink)}
+ .mform .dots{display:flex;gap:10px}
+ .mform .dot{width:22px;height:22px;border-radius:50%;cursor:pointer;border:2px solid transparent;box-shadow:0 0 0 1px rgba(31,27,23,.06) inset}
+ .mform .dot.sel{box-shadow:0 0 0 2px var(--surface) inset,0 0 0 4px currentColor}
+ /* footer */
+ .mform .actions{display:flex;justify-content:space-between;align-items:center;margin-top:24px;padding-top:20px;border-top:1px solid var(--hairline)}
+ .mform .actions .right{display:flex;gap:10px}
+ .mform .btn-del{background:none;color:var(--red);font-weight:600;font-size:14px;padding:9px 4px}
+ .mform .btn-del:hover{text-decoration:underline}
+ .mform .btn-cancel{background:var(--surface);border:1px solid var(--hairline);color:var(--ink);font-weight:500;font-size:14px;padding:9px 20px;border-radius:10px}
+ .mform .btn-cancel:hover{border-color:var(--faint)}
+ .mform .btn-save{background:var(--plum);color:#fff;font-weight:600;font-size:14px;padding:9px 24px;border-radius:10px}
+ .mform .btn-save:hover{background:#4d2230}
+ .mform .msg{font-size:12.5px;min-height:16px;margin-top:10px}
+ .mform .msg.err{color:var(--red)}.mform .msg.ok{color:var(--green)}
</style>
</head>
<body>
+<div class="wrap">
<header>
- <div>
- <div class="sub">Designer Wallcoverings</div>
- <div class="wordmark">DW Calendar<span class="dot">.</span></div>
+ <div class="brand">
+ <div class="wordmark">Designer Wallcoverings</div>
+ <div class="sub">Calendar</div>
</div>
<div class="spacer"></div>
<!-- SKU-view controls (only shown on the SKU tab) -->
- <div id="skuCtrls" style="display:none;gap:18px">
+ <div id="skuCtrls">
<div class="ctrl"><label>Per day</label><input id="perDay" type="number" value="500" min="1"></div>
<div class="ctrl"><label>Per vendor/day</label><input id="perVendorDay" type="number" value="500" min="1"></div>
<div class="ctrl"><label>Start</label><input id="start" type="date"></div>
<button id="apply">Apply</button>
</div>
+ <button class="btn-primary" id="gnew">+ New Event</button>
</header>
-<div class="tabs">
+<div class="tabbar">
<button class="tab" id="tab-gcal" data-view="gcal">Google Calendar</button>
<button class="tab" id="tab-sku" data-view="sku">SKU Activation</button>
+ <span class="conn-pill off" id="connPill"><span class="dot"></span><span id="connTxt">Checking…</span></span>
</div>
<!-- ====================== GOOGLE CALENDAR VIEW ====================== -->
<div class="view" id="view-gcal">
- <div class="banner" id="gbanner">Checking calendar connection…</div>
+ <div class="banner hidden" id="gbanner"></div>
<div class="monthbar">
- <button class="ghost" id="gprev">‹ Prev</button>
+ <button class="nav" id="gprev" aria-label="Previous month">‹</button>
<div class="ml" id="gmlabel">—</div>
- <button class="ghost" id="gnext">Next ›</button>
- <button id="gnew" style="margin-left:18px">+ New Event</button>
+ <button class="nav" id="gnext" aria-label="Next month">›</button>
+ <div class="spacer"></div>
+ <button class="btn-pill" id="gtoday">Today</button>
</div>
<div id="gcal"><div class="loading">Loading events…</div></div>
- <div class="note">Live, editable view of the shared DW Google Calendar. Click any day or an event to create / edit. Events are written straight to Google Calendar (events.insert / patch / delete).</div>
</div>
-<!-- ====================== SKU ACTIVATION VIEW (existing) ====================== -->
+<!-- ====================== SKU ACTIVATION VIEW ====================== -->
<div class="view" id="view-sku">
<div class="summary" id="summary"></div>
- <div class="note">Projected go-active schedule for every SKU staged for activation — published at <b>500 variants/day</b> (the 5-field backlog half of the 1,000/day cap), round-robin breadth-first across all vendors, in date order, until the list is cleared. Each chip is a SKU: product image + vendor. <span style="color:#6db36d">●</span> sample-add · <span style="color:#c2a064">●</span> roll/price build.</div>
+ <div class="note">Projected go-active schedule for every SKU staged for activation — published at <b>500 variants/day</b> (the 5-field backlog half of the 1,000/day cap), round-robin breadth-first across all vendors, in date order, until the list is cleared. Each chip is a SKU: product image + vendor. <span style="color:var(--green)">●</span> sample-add · <span style="color:var(--brass)">●</span> roll/price build.</div>
<div class="monthbar">
- <button class="ghost" id="prev">‹ Prev</button>
+ <button class="nav" id="prev" aria-label="Previous month">‹</button>
<div class="ml" id="mlabel">—</div>
- <button class="ghost" id="next">Next ›</button>
+ <button class="nav" id="next" aria-label="Next month">›</button>
</div>
<div id="cal"><div class="loading">Loading schedule…</div></div>
<div class="legend" id="legend"></div>
</div>
+</div><!-- /wrap -->
<script src="/modal-rig.js"></script>
<script>
@@ -139,6 +242,7 @@ function setTab(v){
document.getElementById('view-gcal').classList.toggle('active',v==='gcal');
document.getElementById('view-sku').classList.toggle('active',v==='sku');
document.getElementById('skuCtrls').style.display = v==='sku' ? 'flex':'none';
+ document.getElementById('gnew').style.display = v==='gcal' ? '' : 'none';
localStorage.setItem('dwcal_tab',v);
if(v==='gcal' && !GDATA) loadGcal();
if(v==='sku' && !DATA) load();
@@ -158,22 +262,34 @@ function fmtWhen(ev){
try{return new Date(ev.start).toLocaleTimeString(undefined,{hour:'numeric',minute:'2-digit'});}
catch(e){return '';}
}
+/* category tint: map a Google colorId / a guessed bucket to a chip tint class */
+function evCat(ev){
+ const c=String(ev.colorId||'');
+ // Google green-ish ids → green, red/plum-ish → plum, else brass (default)
+ if(['2','10','5'].includes(c)) return 'cat-green';
+ if(['3','4','11','6'].includes(c)) return 'cat-plum';
+ return 'cat-brass';
+}
async function refreshStatus(){
try{ GSTATUS = await (await fetch('/api/calendar/status')).json(); }
catch(e){ GSTATUS={connected:false,oauth_configured:false,error:e.message}; }
+ const pill=document.getElementById('connPill'), txt=document.getElementById('connTxt');
const b=document.getElementById('gbanner');
if(GSTATUS.connected){
- b.className='banner ok';
- b.innerHTML='<b>● Connected</b> — editing calendar <code>'+(GSTATUS.calendar_id||'primary')+'</code>. '+
- '<button class="ghost" id="gdisc">Disconnect</button>';
+ pill.className='conn-pill';
+ txt.innerHTML='Connected · <code>'+(GSTATUS.calendar_id||'info@designerwallcoverings.com')+'</code> '+
+ '<button id="gdisc">Disconnect</button>';
document.getElementById('gdisc').onclick=async()=>{await fetch('/api/oauth/revoke',{method:'POST'});refreshStatus();loadGcal();};
+ b.className='banner hidden'; b.innerHTML='';
}else{
+ pill.className='conn-pill off';
+ txt.textContent='Not connected';
b.className='banner';
b.innerHTML='<b>Calendar not connected — Steve must authorize.</b> '+
(GSTATUS.oauth_configured
? 'OAuth is configured — click to grant access. '
: 'The Google OAuth credential has not been added yet (queued in pending-approval). ')+
- '<a href="/api/oauth/start"><button>Connect</button></a>';
+ '<a href="/api/oauth/start"><button class="btn-primary">Connect</button></a>';
}
}
async function loadGcal(){
@@ -194,17 +310,24 @@ function renderGcal(){
(GDATA.events||[]).forEach(ev=>{ const d=(ev.start||'').slice(0,10); (byDate[d]=byDate[d]||[]).push(ev); });
const y=gMonth.getFullYear(), m=gMonth.getMonth();
const cal=document.getElementById('gcal'); cal.className='cal'; cal.innerHTML='';
- ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dow';h.textContent=d;cal.appendChild(h);});
+ const dowRow=document.createElement('div'); dowRow.className='dow-row';
+ ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dow';h.textContent=d;dowRow.appendChild(h);});
+ cal.appendChild(dowRow);
+ const grid=document.createElement('div'); grid.className='grid'; cal.appendChild(grid);
+
const first=new Date(y,m,1), startDow=first.getDay(), days=new Date(y,m+1,0).getDate();
- for(let i=0;i<startDow;i++){const c=document.createElement('div');c.className='cell empty';cal.appendChild(c);}
+ const prevDays=new Date(y,m,0).getDate();
+ const todayISO=new Date().toISOString().slice(0,10);
+ // leading out-of-month days
+ for(let i=startDow-1;i>=0;i--){ grid.appendChild(outCell(prevDays-i)); }
for(let dn=1;dn<=days;dn++){
const iso=`${y}-${String(m+1).padStart(2,'0')}-${String(dn).padStart(2,'0')}`;
- const cell=document.createElement('div'); cell.className='cell gcell';
+ const cell=document.createElement('div'); cell.className='cell gcell'+(iso===todayISO?' today':'');
const evs=byDate[iso]||[];
cell.innerHTML=`<div class="dh"><span class="dn">${dn}</span>${evs.length?`<span class="cnt">${evs.length}</span>`:''}</div>`;
- const body=document.createElement('div'); body.className='chips'; body.style.gridTemplateColumns='1fr'; body.style.display='block';
+ const body=document.createElement('div'); body.className='evs';
evs.forEach(ev=>{
- const b=document.createElement('button'); b.className='ev';
+ const b=document.createElement('button'); b.className='ev '+evCat(ev);
b.innerHTML=`<div class="et">${esc(ev.summary)}</div>`+
`<div class="ewhen">${fmtWhen(ev)}</div>`+
`<div class="ecreated" title="${ev.created||''}">🕓 ${fmtCreated(ev.created)}</div>`;
@@ -215,8 +338,16 @@ function renderGcal(){
add.onclick=(e)=>{e.stopPropagation();openEventModal(null,iso);};
body.appendChild(add);
cell.appendChild(body);
- cal.appendChild(cell);
+ grid.appendChild(cell);
}
+ // trailing out-of-month days to fill the last row
+ const totalCells=startDow+days; const trail=(7-(totalCells%7))%7;
+ for(let dn=1;dn<=trail;dn++){ grid.appendChild(outCell(dn)); }
+}
+function outCell(dn){
+ const c=document.createElement('div'); c.className='cell out';
+ c.innerHTML=`<div class="dh"><span class="dn">${dn}</span></div>`;
+ return c;
}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
@@ -227,32 +358,44 @@ function toLocalInput(iso){ // ISO -> value for datetime-local (local tz)
const p=n=>String(n).padStart(2,'0');
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
}
+const CAL_DOTS=[{id:'plum',c:'#5E2B3C'},{id:'brass',c:'#B0894F'},{id:'green',c:'#3E7C5A'},{id:'ink',c:'#1F1B17'}];
function openEventModal(ev, dayISO){
const isEdit=!!ev;
const startVal = ev ? (ev.allDay ? '' : toLocalInput(ev.start)) : (dayISO+'T09:00');
const endVal = ev ? (ev.allDay ? '' : toLocalInput(ev.end)) : (dayISO+'T10:00');
const allDay = ev ? !!ev.allDay : false;
+ const dots=CAL_DOTS.map((d,i)=>`<span class="dot${i===0?' sel':''}" data-color="${d.id}" style="background:${d.c};color:${d.c}"></span>`).join('');
const bodyHtml=`<form class="mform" id="evForm" onsubmit="return false">
+ <div class="mhead">
+ <div class="mtitle">${isEdit?'Edit Event':'New Event'}</div>
+ <button type="button" class="mclose" id="f_x" aria-label="Close">✕</button>
+ </div>
<label>Title *</label><input type="text" id="f_summary" value="${esc(ev?ev.summary:'')}" placeholder="Event title" required>
- <label>Description</label><textarea id="f_desc" placeholder="Notes">${esc(ev?ev.description:'')}</textarea>
- <label>Location</label><input type="text" id="f_loc" value="${esc(ev?ev.location:'')}" placeholder="Optional">
- <label class="chk"><input type="checkbox" id="f_allday" ${allDay?'checked':''}> All-day event</label>
<div class="row" id="timedRow" style="${allDay?'display:none':''}">
- <div><label>Start</label><input type="datetime-local" id="f_start" value="${startVal}"></div>
- <div><label>End</label><input type="datetime-local" id="f_end" value="${endVal}"></div>
+ <div><label>Starts</label><input type="datetime-local" id="f_start" value="${startVal}"></div>
+ <div><label>Ends</label><input type="datetime-local" id="f_end" value="${endVal}"></div>
</div>
<div class="row" id="dayRow" style="${allDay?'':'display:none'}">
<div><label>Date</label><input type="date" id="f_date" value="${(ev?ev.start:dayISO||'').slice(0,10)}"></div>
</div>
+ <label class="chk">All-day event<input type="checkbox" id="f_allday" ${allDay?'checked':''}><span class="toggle"></span></label>
+ <label>Location</label><input type="text" id="f_loc" value="${esc(ev?ev.location:'')}" placeholder="Optional">
+ <label>Description</label><textarea id="f_desc" placeholder="Notes">${esc(ev?ev.description:'')}</textarea>
+ <div class="colorpick"><span class="lab">Calendar color</span><div class="dots" id="f_dots">${dots}</div></div>
<div class="msg" id="f_msg"></div>
<div class="actions">
- <div>${isEdit?'<button type="button" class="danger" id="f_del">Delete</button>':''}</div>
- <div><button type="button" class="ghost" id="f_cancel">Cancel</button> <button type="button" id="f_save">${isEdit?'Save':'Create'}</button></div>
+ <div>${isEdit?'<button type="button" class="btn-del" id="f_del">Delete</button>':'<span></span>'}</div>
+ <div class="right"><button type="button" class="btn-cancel" id="f_cancel">Cancel</button> <button type="button" class="btn-save" id="f_save">${isEdit?'Save':'Create'}</button></div>
</div>
</form>`;
- const M=ModalRig.open({key:'dwcal-event',title:isEdit?'Edit Event':'New Event',bodyHtml,width:440,height:520,bg:'var(--card)'});
+ const M=ModalRig.open({key:'dwcal-event',title:isEdit?'Edit Event':'New Event',bodyHtml,width:460,height:660,bg:'var(--surface)'});
const $=id=>M.body.querySelector('#'+id);
+ $('f_x').onclick=M.close;
+ // wire the all-day toggle (the visible toggle is the styled label; the checkbox drives it)
$('f_allday').onchange=()=>{const on=$('f_allday').checked;$('timedRow').style.display=on?'none':'';$('dayRow').style.display=on?'':'none';};
+ // color dot selection (cosmetic ring)
+ M.body.querySelectorAll('#f_dots .dot').forEach(d=>d.onclick=()=>{
+ M.body.querySelectorAll('#f_dots .dot').forEach(x=>x.classList.remove('sel'));d.classList.add('sel');});
$('f_cancel').onclick=M.close;
if(isEdit) $('f_del').onclick=async()=>{
if(!confirm('Delete this event?')) return;
@@ -283,15 +426,16 @@ function showMsg($,t,err){const m=$('f_msg');m.textContent=t;m.className='msg '+
document.getElementById('gprev').onclick=()=>{gMonth=new Date(gMonth.getFullYear(),gMonth.getMonth()-1,1);loadGcal();};
document.getElementById('gnext').onclick=()=>{gMonth=new Date(gMonth.getFullYear(),gMonth.getMonth()+1,1);loadGcal();};
+document.getElementById('gtoday').onclick=()=>{const n=new Date();gMonth=new Date(n.getFullYear(),n.getMonth(),1);loadGcal();};
document.getElementById('gnew').onclick=()=>{const n=new Date();openEventModal(null,n.toISOString().slice(0,10));};
-/* ======================= SKU ACTIVATION (existing, unchanged logic) ======================= */
+/* ======================= SKU ACTIVATION (existing logic) ======================= */
let DATA=null, viewMonth=null;
const VCOLORS={};
function vcolor(v){
if(VCOLORS[v]) return VCOLORS[v];
let h=0; for(const c of v) h=(h*31+c.charCodeAt(0))%360;
- return VCOLORS[v]=`hsl(${h},42%,58%)`;
+ return VCOLORS[v]=`hsl(${h},38%,52%)`;
}
async function load(){
const perDay=document.getElementById('perDay').value||500;
@@ -312,26 +456,31 @@ function renderSummary(){
`<div class="k"><span>Vendors</span><b>${Object.keys(s.vendorTotals).length}</b></div>`+
`<div class="k"><span>Days to clear</span><b>${s.totalDays}</b></div>`+
`<div class="k"><span>Finishes</span><b>${s.endDate||'—'}</b></div>`+
- `<div class="k"><span>Cadence</span><b>${s.perDay}/day · ${s.perVendorDay}/vendor</b></div>`;
+ `<div class="k"><span>Cadence</span><b>${s.perDay}/day</b></div>`;
}
function renderLegend(){
const vt=DATA.vendorTotals;
const top=Object.entries(vt).sort((a,b)=>b[1]-a[1]);
document.getElementById('legend').innerHTML=top.map(([v,n])=>
- `<span class="li"><span class="sw" style="background:${vcolor(v)}"></span>${v} <span style="color:var(--ink-2)">${n.toLocaleString()}</span></span>`).join('');
+ `<span class="li"><span class="sw" style="background:${vcolor(v)}"></span>${v} <span style="color:var(--ink)">${n.toLocaleString()}</span></span>`).join('');
}
function renderMonth(){
const byDate={}; DATA.days.forEach(d=>byDate[d.date]=d);
const y=viewMonth.getFullYear(), m=viewMonth.getMonth();
document.getElementById('mlabel').textContent=fmtMonth(viewMonth);
const cal=document.getElementById('cal'); cal.className='cal'; cal.innerHTML='';
- ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dow';h.textContent=d;cal.appendChild(h);});
+ const dowRow=document.createElement('div'); dowRow.className='dow-row';
+ ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dow';h.textContent=d;dowRow.appendChild(h);});
+ cal.appendChild(dowRow);
+ const grid=document.createElement('div'); grid.className='grid'; cal.appendChild(grid);
const first=new Date(y,m,1), startDow=first.getDay(), days=new Date(y,m+1,0).getDate();
- for(let i=0;i<startDow;i++){const c=document.createElement('div');c.className='cell empty';cal.appendChild(c);}
+ const prevDays=new Date(y,m,0).getDate();
+ const todayISO=new Date().toISOString().slice(0,10);
+ for(let i=startDow-1;i>=0;i--){ grid.appendChild(outCell(prevDays-i)); }
for(let dn=1;dn<=days;dn++){
const iso=`${y}-${String(m+1).padStart(2,'0')}-${String(dn).padStart(2,'0')}`;
const day=byDate[iso];
- const cell=document.createElement('div'); cell.className='cell'+(day?'':' blank');
+ const cell=document.createElement('div'); cell.className='cell scell'+(iso===todayISO?' today':'');
cell.innerHTML=`<div class="dh"><span class="dn">${dn}</span>${day?`<span class="cnt">${day.count}</span>`:''}</div>`;
if(day){
const chips=document.createElement('div'); chips.className='chips';
@@ -348,15 +497,16 @@ function renderMonth(){
});
cell.appendChild(chips);
}
- cal.appendChild(cell);
+ grid.appendChild(cell);
}
+ const totalCells=startDow+days; const trail=(7-(totalCells%7))%7;
+ for(let dn=1;dn<=trail;dn++){ grid.appendChild(outCell(dn)); }
}
document.getElementById('prev').onclick=()=>{viewMonth=new Date(viewMonth.getFullYear(),viewMonth.getMonth()-1,1);renderMonth();};
document.getElementById('next').onclick=()=>{viewMonth=new Date(viewMonth.getFullYear(),viewMonth.getMonth()+1,1);renderMonth();};
document.getElementById('apply').onclick=load;
/* ======================= boot ======================= */
-// if landing via OAuth callback (?connected=1), open the gcal tab
const initialTab = (new URLSearchParams(location.search).has('connected')) ? 'gcal'
: (localStorage.getItem('dwcal_tab') || 'gcal');
setTab(initialTab);
← c3c31ac Document Google Calendar layer: README + updated server head
·
back to Dw Activation Calendar
·
Default calendar to populated SKU Activation tab on first lo b585adf →