aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-23-world-clock-vertical-even.html
blob: 58d24a175cb83181da872be0ed29554506fff5f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dupre World — vertical, even spacing</title>
<style>
  :root{
    --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
    --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
    --night:#39435a;
    --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
  }
  * { box-sizing: border-box; }
  body {
    margin:0; min-height:100vh; background:var(--ground);
    display:flex; flex-direction:column; justify-content:center;
    padding:48px 64px; gap:22px;
    font-family:var(--mono); color:var(--silver);
    cursor:none; overflow:hidden;
  }
  .masthead {
    color:var(--steel); font-size:12px; letter-spacing:0.35em;
    text-transform:uppercase; margin-bottom:8px;
  }
  .masthead b { color:var(--gold); font-weight:400; }
  .row { display:flex; align-items:baseline; gap:16px; }
  .city { color:var(--steel); font-size:13px; letter-spacing:0.22em;
          text-transform:uppercase; min-width:150px; }
  .time { color:var(--cream); font-size:30px; letter-spacing:0.05em;
          min-width:150px; }
  .meta { color:var(--dim); font-size:12px; letter-spacing:0.12em; }
  .meta .tz { color:var(--steel); }
  .row.night .time { color:var(--slate); }
  .row.night .city::after { content:" ☾"; color:var(--night); }
  .row.home .time { color:var(--gold-hi); }
  .row.home .city { color:var(--gold); }
</style>
</head>
<body>
  <div class="masthead"><b>DUPRE</b> · WORLD WATCH</div>
  <div id="stack"></div>
<script>
/* Roster mirrors ~/.config/waybar/worldclock.conf (tz|label|lat|lon). The
   real face receives this as ?cities=<json>; embedded here for the prototype. */
const ROSTER = [
  { tz:"Pacific/Auckland",     label:"Wellington",  lon:174.78 },
  { tz:"Australia/Sydney",     label:"Sydney",      lon:151.21 },
  { tz:"Asia/Tokyo",           label:"Tokyo",       lon:139.69 },
  { tz:"Asia/Shanghai",        label:"Shanghai",    lon:121.47 },
  { tz:"Asia/Kolkata",         label:"Delhi",       lon:77.21  },
  { tz:"Asia/Yerevan",         label:"Yerevan",     lon:44.51  },
  { tz:"Europe/Istanbul",      label:"Istanbul",    lon:28.98  },
  { tz:"Europe/Athens",        label:"Athens",      lon:23.73  },
  { tz:"Europe/Paris",         label:"Paris",       lon:2.35   },
  { tz:"Europe/London",        label:"London",      lon:-0.13  },
  { tz:"America/New_York",     label:"New York",    lon:-74.01 },
  { tz:"America/Chicago",      label:"New Orleans", lon:-90.07 },
  { tz:"America/Los_Angeles",  label:"Berkeley",    lon:-122.27},
  { tz:"America/Anchorage",    label:"Anchorage",   lon:-149.90},
  { tz:"Pacific/Honolulu",     label:"Honolulu",    lon:-157.86},
];
/* Region names in Craig's "US Central" style, not the tz's major city.
   Intl longGeneric is the fallback for a zone not in the map. */
const TZNAME = {
  "Pacific/Honolulu":"US Hawaii", "America/Anchorage":"US Alaska",
  "America/Los_Angeles":"US Pacific", "America/Chicago":"US Central",
  "America/New_York":"US Eastern", "Europe/London":"UK",
  "Europe/Paris":"Central Europe", "Europe/Athens":"Eastern Europe",
  "Europe/Istanbul":"Türkiye", "Asia/Yerevan":"Armenia",
  "Asia/Kolkata":"India", "Asia/Shanghai":"China", "Asia/Tokyo":"Japan",
  "Australia/Sydney":"Australia East", "Pacific/Auckland":"New Zealand",
};
function tzName(tz) {
  if (TZNAME[tz]) return TZNAME[tz];
  try {
    const parts = new Intl.DateTimeFormat("en-US",
      { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date());
    const p = parts.find((x) => x.type === "timeZoneName");
    return p ? p.value : tz.split("/").pop().replace(/_/g, " ");
  } catch (e) { return tz.split("/").pop().replace(/_/g, " "); }
}
const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
const stack = document.getElementById("stack");
stack.className = "";
const rows = ROSTER.map((c) => {
  const row = document.createElement("div");
  row.className = "row";
  row.style.marginBottom = "18px";
  const city = document.createElement("div"); city.className = "city";
  city.textContent = c.label;
  const time = document.createElement("div"); time.className = "time";
  const meta = document.createElement("div"); meta.className = "meta";
  row.append(city, time, meta);
  stack.appendChild(row);
  return { c, row, time, meta };
});
function fmt(tz) {
  const now = new Date();
  const p = new Intl.DateTimeFormat("en-US", {
    timeZone: tz, hour12: true, weekday: "short",
    day: "numeric", hour: "numeric", minute: "2-digit",
  }).formatToParts(now);
  const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
  const h24 = new Intl.DateTimeFormat("en-US",
    { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
    .find((x) => x.type === "hour").value;
  return {
    time: g("hour") + ":" + g("minute") + " " + g("dayPeriod"),
    date: g("weekday") + " " + g("day"),
    night: (parseInt(h24, 10) % 24) >= 19 || (parseInt(h24, 10) % 24) < 7,
  };
}
function tick() {
  for (const e of rows) {
    const f = fmt(e.c.tz);
    e.time.textContent = f.time;
    e.meta.innerHTML = f.date + " &nbsp;·&nbsp; <span class='tz'>"
      + tzName(e.c.tz) + "</span>";
    const isHome = e.c.tz === home;
    e.row.classList.toggle("home", isHome);
    e.row.classList.toggle("night", f.night && !isHome);
  }
}
tick();
setInterval(tick, 5000);
</script>
</body>
</html>