sync
50
examples/app_hub/assets/continents-meta.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"source": "mercator-source.jpg",
|
||||||
|
"size": [
|
||||||
|
800,
|
||||||
|
519
|
||||||
|
],
|
||||||
|
"components": 8,
|
||||||
|
"layers": {
|
||||||
|
"vertical_snapshot": {
|
||||||
|
"translate": [
|
||||||
|
0.0,
|
||||||
|
238.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
1.2408477842003853,
|
||||||
|
1.2408477842003853
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inner": {
|
||||||
|
"translate": [
|
||||||
|
223.66088631984587,
|
||||||
|
238.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
1.2408477842003853,
|
||||||
|
1.2408477842003853
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outer": {
|
||||||
|
"scale": [
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": [
|
||||||
|
720.0,
|
||||||
|
560.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"areas_top10": [
|
||||||
|
78439,
|
||||||
|
42423,
|
||||||
|
21849,
|
||||||
|
5520,
|
||||||
|
5428,
|
||||||
|
3434,
|
||||||
|
546,
|
||||||
|
540
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Extract red land polygons from Mercator reference image -> SVG paths."""
|
"""Extract red land polygons from Mercator reference image -> layered SVG paths."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -14,10 +14,17 @@ from PIL import Image
|
|||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
SRC = ROOT / "mercator-source.jpg"
|
SRC = ROOT / "mercator-source.jpg"
|
||||||
OUT_FLAT = ROOT / "hub-logo-continents-mercator-flat.svg"
|
OUT_FLAT = ROOT / "hub-logo-continents-mercator-flat.svg"
|
||||||
|
OUT_GLOBE = ROOT / "hub-logo-continents-mercator-globe.svg"
|
||||||
|
OUT_GLOBE_SNAP_VERTICAL = ROOT / "hub-logo-continents-mercator-globe.snapshot-vertical.svg"
|
||||||
OUT_META = ROOT / "continents-meta.json"
|
OUT_META = ROOT / "continents-meta.json"
|
||||||
|
|
||||||
# Source image is 800x519 Mercator-style plate.
|
# Globe framing in 1920x1080 logo scene.
|
||||||
W, H = 800, 519
|
GLOBE_CX = 720.0
|
||||||
|
GLOBE_CY = 560.0
|
||||||
|
GLOBE_R = 322.0
|
||||||
|
# Orthographic center (visible hemisphere).
|
||||||
|
CENTER_LAT = 18.0
|
||||||
|
CENTER_LON = 12.0
|
||||||
|
|
||||||
|
|
||||||
def red_mask(rgb: np.ndarray) -> np.ndarray:
|
def red_mask(rgb: np.ndarray) -> np.ndarray:
|
||||||
@@ -26,7 +33,6 @@ def red_mask(rgb: np.ndarray) -> np.ndarray:
|
|||||||
|
|
||||||
|
|
||||||
def dilate(mask: np.ndarray, radius: int = 3) -> np.ndarray:
|
def dilate(mask: np.ndarray, radius: int = 3) -> np.ndarray:
|
||||||
"""Bridge thin white grid lines between red land cells."""
|
|
||||||
h, w = mask.shape
|
h, w = mask.shape
|
||||||
out = mask.copy()
|
out = mask.copy()
|
||||||
ys, xs = np.where(mask)
|
ys, xs = np.where(mask)
|
||||||
@@ -61,18 +67,15 @@ def label_components(mask: np.ndarray) -> tuple[np.ndarray, int]:
|
|||||||
|
|
||||||
|
|
||||||
def trace_boundary(labels: np.ndarray, label_id: int) -> list[tuple[int, int]]:
|
def trace_boundary(labels: np.ndarray, label_id: int) -> list[tuple[int, int]]:
|
||||||
"""Moore-neighbor contour for one connected component."""
|
|
||||||
h, w = labels.shape
|
h, w = labels.shape
|
||||||
comp = labels == label_id
|
comp = labels == label_id
|
||||||
if not comp.any():
|
if not comp.any():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Start at topmost-leftmost pixel of component.
|
|
||||||
ys, xs = np.where(comp)
|
ys, xs = np.where(comp)
|
||||||
start = (int(xs[0]), int(ys[0]))
|
start = (int(xs[0]), int(ys[0]))
|
||||||
x, y = start
|
x, y = start
|
||||||
path = [start]
|
path = [start]
|
||||||
# Directions: E, SE, S, SW, W, NW, N, NE
|
|
||||||
dirs = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
|
dirs = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
|
||||||
dir_idx = 0
|
dir_idx = 0
|
||||||
|
|
||||||
@@ -123,35 +126,30 @@ def path_d(points: list[tuple[float, float]]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def mercator_y_to_lat(y: float, h: float) -> float:
|
def mercator_y_to_lat(y: float, h: float) -> float:
|
||||||
# Map top->north, bottom->south; clamp to avoid infinities at poles.
|
|
||||||
t = 1.0 - (y / h)
|
t = 1.0 - (y / h)
|
||||||
t = min(max(t, 0.02), 0.98)
|
t = min(max(t, 0.02), 0.98)
|
||||||
return math.degrees(math.atan(math.sinh(math.pi * (2 * t - 1))))
|
return math.degrees(math.atan(math.sinh(math.pi * (2 * t - 1))))
|
||||||
|
|
||||||
|
|
||||||
def lat_lon_to_globe(lat: float, lon: float, cx: float, cy: float, r: float) -> tuple[float, float]:
|
def lat_lon_to_globe(lat: float, lon: float) -> tuple[float, float]:
|
||||||
"""Simple orthographic-ish placement on visible globe disk."""
|
|
||||||
lat_r = math.radians(lat)
|
lat_r = math.radians(lat)
|
||||||
lon_r = math.radians(lon)
|
lon_r = math.radians(lon)
|
||||||
# Visible hemisphere centered around lon 20, lat 20 for this logo framing.
|
lon0 = math.radians(CENTER_LON)
|
||||||
lon0 = math.radians(20.0)
|
|
||||||
lat0 = math.radians(15.0)
|
|
||||||
# Rotate lon relative to center.
|
|
||||||
dlon = lon_r - lon0
|
dlon = lon_r - lon0
|
||||||
x = cx + r * math.cos(lat_r) * math.sin(dlon)
|
x = GLOBE_CX + GLOBE_R * math.cos(lat_r) * math.sin(dlon)
|
||||||
y = cy - r * math.sin(lat_r)
|
y = GLOBE_CY - GLOBE_R * math.sin(lat_r)
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
|
|
||||||
def map_to_globe(points: list[tuple[int, int]], w: int, h: int) -> list[tuple[float, float]]:
|
def map_to_globe(points: list[tuple[int, int]], w: int, h: int) -> str:
|
||||||
out: list[tuple[float, float]] = []
|
mapped: list[tuple[float, float]] = []
|
||||||
cx, cy, r = 720.0, 560.0, 318.0
|
|
||||||
for x, y in points:
|
for x, y in points:
|
||||||
lon = (x / w) * 360.0 - 180.0
|
lon = (x / w) * 360.0 - 180.0
|
||||||
lat = mercator_y_to_lat(float(y), float(h))
|
lat = mercator_y_to_lat(float(y), float(h))
|
||||||
gx, gy = lat_lon_to_globe(lat, lon, cx, cy, r)
|
mapped.append(lat_lon_to_globe(lat, lon))
|
||||||
out.append((gx, gy))
|
if len(mapped) < 3:
|
||||||
return out
|
return ""
|
||||||
|
return path_d(mapped)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -164,31 +162,34 @@ def main() -> None:
|
|||||||
components: list[dict] = []
|
components: list[dict] = []
|
||||||
for label_id in range(1, n + 1):
|
for label_id in range(1, n + 1):
|
||||||
area = int((labels == label_id).sum())
|
area = int((labels == label_id).sum())
|
||||||
if area < 500: # drop tiny noise / watermark specks
|
if area < 500:
|
||||||
continue
|
continue
|
||||||
boundary = trace_boundary(labels, label_id)
|
boundary = trace_boundary(labels, label_id)
|
||||||
if len(boundary) < 20:
|
if len(boundary) < 20:
|
||||||
continue
|
continue
|
||||||
boundary = simplify(boundary, min_dist=3.5)
|
boundary = simplify(boundary, min_dist=3.5)
|
||||||
|
flat = path_d([(float(x), float(y)) for x, y in boundary])
|
||||||
|
globe = map_to_globe(boundary, w, h)
|
||||||
|
if not flat:
|
||||||
|
continue
|
||||||
ys, xs = np.where(labels == label_id)
|
ys, xs = np.where(labels == label_id)
|
||||||
bbox = [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())]
|
|
||||||
components.append(
|
components.append(
|
||||||
{
|
{
|
||||||
"id": label_id,
|
"id": label_id,
|
||||||
"area": area,
|
"area": area,
|
||||||
"bbox": bbox,
|
"bbox": [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())],
|
||||||
"flat": path_d([(float(x), float(y)) for x, y in boundary]),
|
"flat": flat,
|
||||||
"globe": path_d(map_to_globe(boundary, w, h)),
|
"globe": globe,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
components.sort(key=lambda c: c["area"], reverse=True)
|
components.sort(key=lambda c: c["area"], reverse=True)
|
||||||
|
|
||||||
flat_paths = "\n ".join(
|
flat_paths = "\n ".join(
|
||||||
f'<path id="continent-{c["id"]}" data-area="{c["area"]}" d="{c["flat"]}"/>'
|
f'<path id="continent-{c["id"]}" data-area="{c["area"]}" d="{c["flat"]}"/>'
|
||||||
for c in components
|
for c in components
|
||||||
)
|
)
|
||||||
globe_paths = "\n ".join(
|
globe_paths = "\n ".join(
|
||||||
f'<path id="continent-{c["id"]}" data-area="{c["area"]}" d="{c["globe"]}"/>'
|
f'<path id="continent-{c["id"]}" data-area="{c["area"]}" d="{c["globe"]}"/>'
|
||||||
for c in components
|
for c in components
|
||||||
)
|
)
|
||||||
@@ -196,7 +197,7 @@ def main() -> None:
|
|||||||
OUT_FLAT.write_text(
|
OUT_FLAT.write_text(
|
||||||
f"""<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" aria-label="Extracted continents (flat Mercator plate)">
|
f"""<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" aria-label="Extracted continents (flat Mercator plate)">
|
||||||
<rect width="100%" height="100%" fill="#b8d4e8"/>
|
<rect width="100%" height="100%" fill="#b8d4e8"/>
|
||||||
<g id="continents-extracted" fill="#c41e1a" fill-opacity="0.92" stroke="#8f1410" stroke-width="0.8">
|
<g id="continents-inner-layer" fill="#9ab3cc" fill-opacity="0.82" stroke="#dce8f6" stroke-opacity="0.78" stroke-width="1.1">
|
||||||
{flat_paths}
|
{flat_paths}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -204,17 +205,73 @@ def main() -> None:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
globe_svg = ROOT.parent.parent / "examples" / "app_hub" / "assets" / "hub-logo-continents-mercator-globe.svg"
|
# Two-step alignment:
|
||||||
globe_svg.write_text(
|
# 1) Vertical fit: Mercator north/south poles match globe top/bottom.
|
||||||
f"""<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Extracted continents mapped to globe viewBox">
|
# 2) Horizontal fit: layer centers/pivots match at globe center.
|
||||||
|
globe_diameter = GLOBE_R * 2
|
||||||
|
inner_sy = globe_diameter / h
|
||||||
|
inner_sx = inner_sy
|
||||||
|
inner_ty = GLOBE_CY - GLOBE_R
|
||||||
|
inner_tx_vertical = 0.0
|
||||||
|
inner_tx = GLOBE_CX - (w * inner_sx) / 2
|
||||||
|
outer_sx = 1.0
|
||||||
|
outer_sy = 1.0
|
||||||
|
|
||||||
|
def globe_svg(
|
||||||
|
inner_tx_val: float,
|
||||||
|
inner_ty_val: float,
|
||||||
|
inner_sx_val: float,
|
||||||
|
inner_sy_val: float,
|
||||||
|
outer_sx_val: float,
|
||||||
|
outer_sy_val: float,
|
||||||
|
label: str,
|
||||||
|
) -> str:
|
||||||
|
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice" role="img" aria-label="{label}">
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="globeClip"><circle cx="720" cy="560" r="322"/></clipPath>
|
<clipPath id="globeClip"><circle cx="{GLOBE_CX:.0f}" cy="{GLOBE_CY:.0f}" r="{GLOBE_R:.0f}"/></clipPath>
|
||||||
|
<radialGradient id="continentRelief" cx="34%" cy="22%" r="78%">
|
||||||
|
<stop offset="0%" stop-color="#dbe7f6" stop-opacity="0.45"/>
|
||||||
|
<stop offset="58%" stop-color="#9ab3cc" stop-opacity="0.22"/>
|
||||||
|
<stop offset="100%" stop-color="#6a7f97" stop-opacity="0.18"/>
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="continentDepth" x="-30%" y="-30%" width="160%" height="160%">
|
||||||
|
<feDropShadow dx="-4" dy="-6" stdDeviation="3" flood-color="#f4fbff" flood-opacity="0.2"/>
|
||||||
|
<feDropShadow dx="4" dy="6" stdDeviation="4" flood-color="#0a1220" flood-opacity="0.28"/>
|
||||||
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<g id="continents-globe" clip-path="url(#globeClip)" fill="#7287a1" fill-opacity="0.88" stroke="#a8b8cc" stroke-opacity="0.45" stroke-width="1">
|
<g id="continents-outer-layer" clip-path="url(#globeClip)" transform="translate({GLOBE_CX:.0f} {GLOBE_CY:.0f}) scale({outer_sx_val:.3f} {outer_sy_val:.3f}) translate(-{GLOBE_CX:.0f} -{GLOBE_CY:.0f})">
|
||||||
{globe_paths}
|
<g id="continents-inner-layer" transform="translate({inner_tx_val:.2f} {inner_ty_val:.2f}) scale({inner_sx_val:.4f} {inner_sy_val:.4f})">
|
||||||
|
<g id="continents-flat" fill="url(#continentRelief)" fill-opacity="0.9" stroke="#dce8f6" stroke-opacity="0.78" stroke-width="1.2" filter="url(#continentDepth)">
|
||||||
|
{flat_paths}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
""",
|
"""
|
||||||
|
|
||||||
|
OUT_GLOBE_SNAP_VERTICAL.write_text(
|
||||||
|
globe_svg(
|
||||||
|
inner_tx_vertical,
|
||||||
|
inner_ty,
|
||||||
|
inner_sx,
|
||||||
|
inner_sy,
|
||||||
|
outer_sx,
|
||||||
|
outer_sy,
|
||||||
|
"Continents vertical-fit snapshot",
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
OUT_GLOBE.write_text(
|
||||||
|
globe_svg(
|
||||||
|
inner_tx,
|
||||||
|
inner_ty,
|
||||||
|
inner_sx,
|
||||||
|
inner_sy,
|
||||||
|
outer_sx,
|
||||||
|
outer_sy,
|
||||||
|
"Continents layered Mercator overlay",
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,6 +281,15 @@ def main() -> None:
|
|||||||
"source": str(SRC.name),
|
"source": str(SRC.name),
|
||||||
"size": [w, h],
|
"size": [w, h],
|
||||||
"components": len(components),
|
"components": len(components),
|
||||||
|
"layers": {
|
||||||
|
"vertical_snapshot": {
|
||||||
|
"translate": [inner_tx_vertical, inner_ty],
|
||||||
|
"scale": [inner_sx, inner_sy],
|
||||||
|
},
|
||||||
|
"inner": {"translate": [inner_tx, inner_ty], "scale": [inner_sx, inner_sy]},
|
||||||
|
"outer": {"scale": [outer_sx, outer_sy]},
|
||||||
|
"pivot": [GLOBE_CX, GLOBE_CY],
|
||||||
|
},
|
||||||
"areas_top10": [c["area"] for c in components[:10]],
|
"areas_top10": [c["area"] for c in components[:10]],
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
@@ -233,7 +299,7 @@ def main() -> None:
|
|||||||
|
|
||||||
print(f"components={len(components)}")
|
print(f"components={len(components)}")
|
||||||
print(f"wrote {OUT_FLAT}")
|
print(f"wrote {OUT_FLAT}")
|
||||||
print(f"wrote {globe_svg}")
|
print(f"wrote {OUT_GLOBE}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
14
examples/app_hub/assets/hub-logo-fragment-compass.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Compass layer">
|
||||||
|
<g id="globe-compass" transform="translate(1050 820)">
|
||||||
|
<circle cx="0" cy="0" r="52" fill="#0a0e14" fill-opacity="0.42" stroke="#c7d3e1" stroke-opacity="0.52" stroke-width="2"/>
|
||||||
|
<circle cx="0" cy="0" r="40" fill="none" stroke="#dbe5f1" stroke-opacity="0.64" stroke-width="1.5"/>
|
||||||
|
<path d="M 0 -38 L 9 0 L 0 38 L -9 0 Z" fill="#e7ecf3" fill-opacity="0.9"/>
|
||||||
|
<path d="M -38 0 L 0 -9 L 38 0 L 0 9 Z" fill="#8ea1b9" fill-opacity="0.86"/>
|
||||||
|
<path d="M 0 -28 L 6 0 L 0 28 L -6 0 Z" fill="#223246" fill-opacity="0.9"/>
|
||||||
|
<circle cx="0" cy="0" r="4.2" fill="#e7ecf3" fill-opacity="0.92"/>
|
||||||
|
<text x="0" y="-56" text-anchor="middle" fill="#e7ecf3" fill-opacity="0.72" font-size="12" font-family="Segoe UI, sans-serif">N</text>
|
||||||
|
<text x="0" y="68" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">S</text>
|
||||||
|
<text x="-66" y="4" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">W</text>
|
||||||
|
<text x="66" y="4" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">E</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -31,18 +31,4 @@
|
|||||||
<ellipse id="globe-south-pole" cx="720" cy="728" rx="260" ry="39" stroke="url(#gradSouthPole)" stroke-width="7.5"/>
|
<ellipse id="globe-south-pole" cx="720" cy="728" rx="260" ry="39" stroke="url(#gradSouthPole)" stroke-width="7.5"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g id="globe-compass" transform="translate(1050 820)">
|
|
||||||
<circle cx="0" cy="0" r="52" fill="#0a0e14" fill-opacity="0.42" stroke="#c7d3e1" stroke-opacity="0.52" stroke-width="2"/>
|
|
||||||
<circle cx="0" cy="0" r="40" fill="none" stroke="#dbe5f1" stroke-opacity="0.64" stroke-width="1.5"/>
|
|
||||||
<path d="M 0 -38 L 9 0 L 0 38 L -9 0 Z" fill="#e7ecf3" fill-opacity="0.9"/>
|
|
||||||
<path d="M -38 0 L 0 -9 L 38 0 L 0 9 Z" fill="#8ea1b9" fill-opacity="0.86"/>
|
|
||||||
<path d="M 0 -28 L 6 0 L 0 28 L -6 0 Z" fill="#223246" fill-opacity="0.9"/>
|
|
||||||
<circle cx="0" cy="0" r="4.2" fill="#e7ecf3" fill-opacity="0.92"/>
|
|
||||||
<text x="0" y="-56" text-anchor="middle" fill="#e7ecf3" fill-opacity="0.72" font-size="12" font-family="Segoe UI, sans-serif">N</text>
|
|
||||||
<text x="0" y="68" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">S</text>
|
|
||||||
<text x="-66" y="4" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">W</text>
|
|
||||||
<text x="66" y="4" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">E</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<circle id="globe-border" cx="720" cy="560" r="330" fill="none" stroke="#e7ecf3" stroke-width="15"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -27,6 +27,14 @@
|
|||||||
<stop offset="52%" stop-color="#d7e0ec" stop-opacity="0.78"/>
|
<stop offset="52%" stop-color="#d7e0ec" stop-opacity="0.78"/>
|
||||||
<stop offset="100%" stop-color="#4f6178" stop-opacity="0.92"/>
|
<stop offset="100%" stop-color="#4f6178" stop-opacity="0.92"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<linearGradient id="sunRim" x1="390" y1="560" x2="1050" y2="560" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#f5f8ff" stop-opacity="0.98"/>
|
||||||
|
<stop offset="38%" stop-color="#dce7f5" stop-opacity="0.82"/>
|
||||||
|
<stop offset="100%" stop-color="#4a5c72" stop-opacity="0.7"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="sunRimGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="3.2"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
<clipPath id="gridClip"><circle cx="720" cy="560" r="322"/></clipPath>
|
<clipPath id="gridClip"><circle cx="720" cy="560" r="322"/></clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
@@ -45,18 +53,6 @@
|
|||||||
<ellipse id="globe-south-pole" cx="720" cy="728" rx="260" ry="39" stroke="url(#gradSouthPole)" stroke-width="7.5"/>
|
<ellipse id="globe-south-pole" cx="720" cy="728" rx="260" ry="39" stroke="url(#gradSouthPole)" stroke-width="7.5"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g id="globe-compass" transform="translate(1050 820)">
|
<circle id="globe-border-glow" cx="720" cy="560" r="330" fill="none" stroke="url(#sunRim)" stroke-opacity="0.75" stroke-width="20" filter="url(#sunRimGlow)"/>
|
||||||
<circle cx="0" cy="0" r="52" fill="#0a0e14" fill-opacity="0.42" stroke="#c7d3e1" stroke-opacity="0.52" stroke-width="2"/>
|
<circle id="globe-border" cx="720" cy="560" r="330" fill="none" stroke="url(#sunRim)" stroke-width="14"/>
|
||||||
<circle cx="0" cy="0" r="40" fill="none" stroke="#dbe5f1" stroke-opacity="0.64" stroke-width="1.5"/>
|
|
||||||
<path d="M 0 -38 L 9 0 L 0 38 L -9 0 Z" fill="#e7ecf3" fill-opacity="0.9"/>
|
|
||||||
<path d="M -38 0 L 0 -9 L 38 0 L 0 9 Z" fill="#8ea1b9" fill-opacity="0.86"/>
|
|
||||||
<path d="M 0 -28 L 6 0 L 0 28 L -6 0 Z" fill="#223246" fill-opacity="0.9"/>
|
|
||||||
<circle cx="0" cy="0" r="4.2" fill="#e7ecf3" fill-opacity="0.92"/>
|
|
||||||
<text x="0" y="-56" text-anchor="middle" fill="#e7ecf3" fill-opacity="0.72" font-size="12" font-family="Segoe UI, sans-serif">N</text>
|
|
||||||
<text x="0" y="68" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">S</text>
|
|
||||||
<text x="-66" y="4" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">W</text>
|
|
||||||
<text x="66" y="4" text-anchor="middle" fill="#a9bbce" fill-opacity="0.68" font-size="12" font-family="Segoe UI, sans-serif">E</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<circle id="globe-border" cx="720" cy="560" r="330" fill="none" stroke="#e7ecf3" stroke-width="15"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -1,22 +1,14 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="340" height="46" viewBox="0 0 340 46" role="img" aria-label="Thunder schematic underline">
|
<svg xmlns="http://www.w3.org/2000/svg" width="340" height="46" viewBox="0 0 340 46" role="img" aria-label="Thunder schematic underline">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="thunderCore" x1="0%" y1="50%" x2="100%" y2="50%">
|
<linearGradient id="thunderFill" x1="0%" y1="50%" x2="100%" y2="50%">
|
||||||
<stop offset="0%" stop-color="#7dd3fc" stop-opacity="0.15"/>
|
<stop offset="0%" stop-color="#8ecfff" stop-opacity="0.22"/>
|
||||||
<stop offset="15%" stop-color="#bae6fd" stop-opacity="0.95"/>
|
<stop offset="18%" stop-color="#c9ecff" stop-opacity="0.98"/>
|
||||||
<stop offset="48%" stop-color="#ffffff" stop-opacity="1"/>
|
<stop offset="50%" stop-color="#ffffff" stop-opacity="1"/>
|
||||||
<stop offset="82%" stop-color="#bae6fd" stop-opacity="0.95"/>
|
<stop offset="82%" stop-color="#c9ecff" stop-opacity="0.98"/>
|
||||||
<stop offset="100%" stop-color="#7dd3fc" stop-opacity="0.15"/>
|
<stop offset="100%" stop-color="#8ecfff" stop-opacity="0.22"/>
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="thunderEdge" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#9ddcff" stop-opacity="0"/>
|
|
||||||
<stop offset="24%" stop-color="#9ddcff" stop-opacity="0.8"/>
|
|
||||||
<stop offset="76%" stop-color="#9ddcff" stop-opacity="0.8"/>
|
|
||||||
<stop offset="100%" stop-color="#9ddcff" stop-opacity="0"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
<path d="M 10 9 L 196 9" fill="none" stroke="url(#thunderFill)" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
<path d="M 12 24 L 86 24 L 98 16 L 126 31 L 154 13 L 178 28 L 210 11 L 236 27 L 262 17 L 286 25 L 328 25"
|
<path d="M 196 9 L 248 31" fill="none" stroke="url(#thunderFill)" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
fill="none" stroke="url(#thunderCore)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 248 31 L 330 31" fill="none" stroke="url(#thunderFill)" stroke-width="5.5" stroke-linecap="round"/>
|
||||||
<path d="M 12 24 L 86 24 L 98 16 L 126 31 L 154 13 L 178 28 L 210 11 L 236 27 L 262 17 L 286 25 L 328 25"
|
|
||||||
fill="none" stroke="url(#thunderEdge)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 932 B |
7
examples/app_hub/assets/hub-widget-resize-arrow.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.279 45.279" aria-hidden="true">
|
||||||
|
<symbol id="resizeArrow" viewBox="0 0 45.279 45.279">
|
||||||
|
<path d="M44.106,42.943H3.987L44.933,1.995c0.456-0.455,0.456-1.196,0-1.651c-0.452-0.455-1.195-0.455-1.651,0L2.336,41.292V1.168
|
||||||
|
C2.336,0.524,1.81,0,1.168,0C0.523,0,0,0.524,0,1.168v44.111h44.111c0.644,0,1.168-0.522,1.168-1.168
|
||||||
|
C45.279,43.469,44.755,42.943,44.106,42.943z"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 451 B |
22
examples/app_hub/assets/overtime-01-alphabet.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
|
||||||
|
<!-- Overtime 01 alphabet: 7-segment LCD glyphs for hub primary clock -->
|
||||||
|
<defs>
|
||||||
|
<symbol id="ot-0" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/><rect x="1" y="17" width="3" height="11" rx="1"/><rect x="1" y="4" width="3" height="11" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-1" viewBox="0 0 20 32"><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-2" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="1" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-3" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-4" viewBox="0 0 20 32"><rect x="1" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-5" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="1" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-6" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="1" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="1" y="17" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-7" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-8" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="1" y="4" width="3" height="11" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="1" y="17" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-9" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="1" y="4" width="3" height="11" rx="1"/><rect x="3" y="15" width="14" height="3" rx="1"/><rect x="16" y="4" width="3" height="11" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-colon" viewBox="0 0 12 32"><rect x="4" y="10" width="4" height="4" rx="1"/><rect x="4" y="20" width="4" height="4" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-plus" viewBox="0 0 16 32"><rect x="6" y="8" width="4" height="16" rx="1"/><rect x="2" y="14" width="12" height="4" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-minus" viewBox="0 0 16 32"><rect x="2" y="14" width="12" height="4" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-space" viewBox="0 0 8 32"></symbol>
|
||||||
|
<symbol id="ot-G" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="1" y="4" width="3" height="11" rx="1"/><rect x="1" y="17" width="3" height="11" rx="1"/><rect x="3" y="28" width="14" height="3" rx="1"/><rect x="16" y="17" width="3" height="11" rx="1"/><rect x="10" y="15" width="9" height="3" rx="1"/></symbol>
|
||||||
|
<symbol id="ot-M" viewBox="0 0 24 32"><rect x="1" y="4" width="3" height="24" rx="1"/><rect x="20" y="4" width="3" height="24" rx="1"/><rect x="7.1" y="5" width="2.6" height="13.2" rx="1" transform="rotate(28 8.4 11.6)"/><rect x="14.3" y="5" width="2.6" height="13.2" rx="1" transform="rotate(-28 15.6 11.6)"/></symbol>
|
||||||
|
<symbol id="ot-T" viewBox="0 0 20 32"><rect x="3" y="1" width="14" height="3" rx="1"/><rect x="8" y="4" width="4" height="24" rx="1"/></symbol>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
482
examples/app_hub/clock-widget.html
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Clock Widget</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0f1218;
|
||||||
|
--panel: #171c24;
|
||||||
|
--panel-2: #131820;
|
||||||
|
--border: rgba(255, 255, 255, 0.72);
|
||||||
|
--text: #eef3ff;
|
||||||
|
--muted: #9aa5ba;
|
||||||
|
--accent: #dbe7ff;
|
||||||
|
--digit: #d8f6b8;
|
||||||
|
--digit-dim: #76906a;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: radial-gradient(circle at 20% 15%, #1f2a3b 0%, var(--bg) 50%, #090c12 100%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "Segoe UI", Tahoma, sans-serif;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-widget {
|
||||||
|
width: min(980px, 100%);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
|
||||||
|
background: linear-gradient(180deg, #141a22 0%, #0f131a 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:first-child {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-1 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-2 {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-3 {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-clock {
|
||||||
|
min-height: 220px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)),
|
||||||
|
repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0.02) 0,
|
||||||
|
rgba(255, 255, 255, 0.02) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 6px
|
||||||
|
),
|
||||||
|
#111822;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-zone {
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-time {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 16px 8px;
|
||||||
|
border: 1px solid rgba(220, 235, 180, 0.45);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(180deg, #202a1e 0%, #181f16 100%);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04), 0 8px 18px rgba(0, 0, 0, 0.45);
|
||||||
|
font-family: "Overtime LCD", "DS-Digital", "Digital-7 Mono", "Consolas", monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--digit);
|
||||||
|
text-shadow: 0 0 5px rgba(190, 240, 150, 0.35), 0 0 1px rgba(180, 220, 145, 0.7);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-time .hh,
|
||||||
|
.primary-time .mm {
|
||||||
|
font-size: clamp(3.1rem, 9.2vw, 5.7rem);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-time .colon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.55em;
|
||||||
|
text-align: center;
|
||||||
|
font-size: clamp(3.1rem, 9.2vw, 5.7rem);
|
||||||
|
animation: casioBlink 1.25s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-time .ss {
|
||||||
|
min-width: 2ch;
|
||||||
|
font-size: clamp(1.35rem, 3.1vw, 2rem);
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: var(--digit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--panel-2);
|
||||||
|
color: #e8ecf6;
|
||||||
|
font-family: "Consolas", "Menlo", monospace;
|
||||||
|
font-size: clamp(0.92rem, 1.5vw, 1.1rem);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
background: linear-gradient(180deg, #161d27 0%, #121923 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analog-cell {
|
||||||
|
min-height: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 55%), #121923;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tz-label {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-clock {
|
||||||
|
width: min(94%, 220px);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
border: 4px solid rgba(239, 243, 252, 0.78);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0) 36%),
|
||||||
|
radial-gradient(circle at 50% 50%, #f6f8fc 0%, #e5ebf6 62%, #d3dceb 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 24px rgba(0, 0, 0, 0.45),
|
||||||
|
inset -8px -8px 18px rgba(0, 0, 0, 0.12),
|
||||||
|
inset 8px 10px 14px rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roman {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: -10px;
|
||||||
|
transform: rotate(calc(var(--i) * 30deg)) translateY(calc(var(--radius) * -1)) rotate(calc(var(--i) * -30deg));
|
||||||
|
font-family: "Times New Roman", serif;
|
||||||
|
font-size: clamp(0.76rem, 1.1vw, 0.95rem);
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2a3447;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
transform: translate(-50%, -100%) rotate(0deg);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: transform 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand.hour {
|
||||||
|
width: 6px;
|
||||||
|
height: 28%;
|
||||||
|
background: #1f2f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand.minute {
|
||||||
|
width: 4px;
|
||||||
|
height: 38%;
|
||||||
|
background: #2e4767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand.second {
|
||||||
|
width: 2px;
|
||||||
|
height: 42%;
|
||||||
|
background: #b63b3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-top: -6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #111928;
|
||||||
|
border: 2px solid #f3f6ff;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digital-subtime {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-family: "Consolas", monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #d6deef;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes casioBlink {
|
||||||
|
0%, 20% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20.01%, 60% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
60.01%, 100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.row-2 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="clock-widget" id="clockWidget">
|
||||||
|
<div class="row row-1">
|
||||||
|
<div class="cell main-clock">
|
||||||
|
<div class="primary-zone" id="primaryZoneLabel"></div>
|
||||||
|
<div class="primary-time" aria-label="Primary time">
|
||||||
|
<span class="hh" id="primaryHH">00</span>
|
||||||
|
<span class="colon">:</span>
|
||||||
|
<span class="mm" id="primaryMM">00</span>
|
||||||
|
<span class="ss" id="primarySS">00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-2">
|
||||||
|
<div class="cell placeholder"></div>
|
||||||
|
<div class="cell placeholder"></div>
|
||||||
|
<div class="cell placeholder"></div>
|
||||||
|
<div class="cell date-cell" id="dateCell">MON 01 JAN 2026</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-3" id="secondaryRow"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var CITY_LABELS = {
|
||||||
|
"America/New_York": "NEW YORK",
|
||||||
|
"Europe/Moscow": "MOSCOW",
|
||||||
|
"Asia/Shanghai": "BEIJING",
|
||||||
|
"Africa/Johannesburg": "CAPETOWN"
|
||||||
|
};
|
||||||
|
|
||||||
|
var ROMAN = ["XII", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI"];
|
||||||
|
var WEEK = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
||||||
|
var MONTH = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
||||||
|
|
||||||
|
function pad2(v) {
|
||||||
|
return String(v).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemTimeZone() {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSecondaryZones(primaryZone) {
|
||||||
|
var preferred = ["America/New_York", "Europe/Moscow", "Asia/Shanghai"];
|
||||||
|
var secondaries = preferred.filter(function (z) { return z !== primaryZone; });
|
||||||
|
while (secondaries.length < 3) {
|
||||||
|
if (!secondaries.includes("Africa/Johannesburg") && primaryZone !== "Africa/Johannesburg") {
|
||||||
|
secondaries.push("Africa/Johannesburg");
|
||||||
|
} else {
|
||||||
|
secondaries.push("UTC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secondaries.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDatePrimary(date, zone) {
|
||||||
|
var parts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: zone,
|
||||||
|
weekday: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric"
|
||||||
|
}).formatToParts(date);
|
||||||
|
|
||||||
|
var map = {};
|
||||||
|
parts.forEach(function (p) {
|
||||||
|
if (p.type !== "literal") map[p.type] = p.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
var day = (map.weekday || "---").slice(0, 3).toUpperCase();
|
||||||
|
var dd = map.day || "00";
|
||||||
|
var mon = (map.month || "---").slice(0, 3).toUpperCase();
|
||||||
|
var yyyy = map.year || "0000";
|
||||||
|
return day + " " + dd + " " + mon + " " + yyyy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeParts(date, zone) {
|
||||||
|
var parts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: zone,
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit"
|
||||||
|
}).formatToParts(date);
|
||||||
|
|
||||||
|
var map = {};
|
||||||
|
parts.forEach(function (p) {
|
||||||
|
if (p.type !== "literal") map[p.type] = p.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hh: map.hour || "00",
|
||||||
|
mm: map.minute || "00",
|
||||||
|
ss: map.second || "00"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSecondaryClock(tz) {
|
||||||
|
var cell = document.createElement("div");
|
||||||
|
cell.className = "cell analog-cell";
|
||||||
|
|
||||||
|
var label = document.createElement("div");
|
||||||
|
label.className = "tz-label";
|
||||||
|
label.textContent = CITY_LABELS[tz] || tz.toUpperCase();
|
||||||
|
cell.appendChild(label);
|
||||||
|
|
||||||
|
var clock = document.createElement("div");
|
||||||
|
clock.className = "wall-clock";
|
||||||
|
|
||||||
|
for (var i = 0; i < 12; i++) {
|
||||||
|
var numeral = document.createElement("div");
|
||||||
|
numeral.className = "roman";
|
||||||
|
numeral.style.setProperty("--i", String(i));
|
||||||
|
numeral.style.setProperty("--radius", "41%");
|
||||||
|
numeral.textContent = ROMAN[i];
|
||||||
|
clock.appendChild(numeral);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hour = document.createElement("div");
|
||||||
|
hour.className = "hand hour";
|
||||||
|
var minute = document.createElement("div");
|
||||||
|
minute.className = "hand minute";
|
||||||
|
var second = document.createElement("div");
|
||||||
|
second.className = "hand second";
|
||||||
|
var pin = document.createElement("div");
|
||||||
|
pin.className = "pin";
|
||||||
|
|
||||||
|
clock.appendChild(hour);
|
||||||
|
clock.appendChild(minute);
|
||||||
|
clock.appendChild(second);
|
||||||
|
clock.appendChild(pin);
|
||||||
|
cell.appendChild(clock);
|
||||||
|
|
||||||
|
var digital = document.createElement("div");
|
||||||
|
digital.className = "digital-subtime";
|
||||||
|
digital.textContent = "--:--:--";
|
||||||
|
cell.appendChild(digital);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tz: tz,
|
||||||
|
root: cell,
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
second: second,
|
||||||
|
digital: digital
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var primaryZone = getSystemTimeZone();
|
||||||
|
var secondaryZones = computeSecondaryZones(primaryZone);
|
||||||
|
|
||||||
|
var secondaryRow = document.getElementById("secondaryRow");
|
||||||
|
var secondaryClocks = secondaryZones.map(function (tz) {
|
||||||
|
var c = buildSecondaryClock(tz);
|
||||||
|
secondaryRow.appendChild(c.root);
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("primaryZoneLabel").textContent = "PRIMARY TZ: " + (CITY_LABELS[primaryZone] || primaryZone.toUpperCase());
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
var now = new Date();
|
||||||
|
var primary = getTimeParts(now, primaryZone);
|
||||||
|
|
||||||
|
document.getElementById("primaryHH").textContent = primary.hh;
|
||||||
|
document.getElementById("primaryMM").textContent = primary.mm;
|
||||||
|
document.getElementById("primarySS").textContent = primary.ss;
|
||||||
|
document.getElementById("dateCell").textContent = formatDatePrimary(now, primaryZone);
|
||||||
|
|
||||||
|
secondaryClocks.forEach(function (clock) {
|
||||||
|
var p = getTimeParts(now, clock.tz);
|
||||||
|
var hNum = Number(p.hh);
|
||||||
|
var mNum = Number(p.mm);
|
||||||
|
var sNum = Number(p.ss);
|
||||||
|
|
||||||
|
var hourDeg = ((hNum % 12) + mNum / 60 + sNum / 3600) * 30;
|
||||||
|
var minuteDeg = (mNum + sNum / 60) * 6;
|
||||||
|
var secondDeg = sNum * 6;
|
||||||
|
|
||||||
|
clock.hour.style.transform = "translate(-50%, -100%) rotate(" + hourDeg + "deg)";
|
||||||
|
clock.minute.style.transform = "translate(-50%, -100%) rotate(" + minuteDeg + "deg)";
|
||||||
|
clock.second.style.transform = "translate(-50%, -100%) rotate(" + secondDeg + "deg)";
|
||||||
|
clock.digital.textContent = p.hh + ":" + p.mm + ":" + p.ss;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
setInterval(update, 1000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -38,6 +38,26 @@
|
|||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hub-logo-layer--compass {
|
||||||
|
z-index: 5;
|
||||||
|
left: 54.7%;
|
||||||
|
top: 75.9%;
|
||||||
|
width: 11.5%;
|
||||||
|
height: auto;
|
||||||
|
inset: auto;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.22s ease, filter 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-logo-layer--compass:hover,
|
||||||
|
.hub-logo-layer--compass:focus-visible {
|
||||||
|
transform: translate(-50%, -50%) scale(1.12);
|
||||||
|
filter: drop-shadow(0 0 16px rgba(186, 228, 255, 0.55));
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.hub-logo-layer--tail {
|
.hub-logo-layer--tail {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,70 +169,307 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit {
|
.hub-clock-widget {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: calc(50% + 252px);
|
left: calc(50% + 290px);
|
||||||
top: calc(50% + 226px);
|
top: calc(50% - 300px);
|
||||||
width: 110px;
|
width: min(420px, 42vw);
|
||||||
height: 110px;
|
min-width: 330px;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
border-radius: 50%;
|
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||||
border: 2px solid rgba(219, 229, 241, 0.45);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.48);
|
||||||
background:
|
border-radius: 18px;
|
||||||
radial-gradient(circle at 35% 35%, rgba(231, 236, 243, 0.35), rgba(10, 14, 20, 0.22) 68%, rgba(10, 14, 20, 0.1) 100%);
|
overflow: hidden;
|
||||||
box-shadow:
|
background: linear-gradient(180deg, rgba(18, 24, 34, 0.92), rgba(13, 18, 28, 0.9));
|
||||||
-6px -8px 8px rgba(160, 215, 255, 0.12),
|
backdrop-filter: blur(1.5px);
|
||||||
8px 10px 14px rgba(9, 13, 19, 0.24);
|
transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: move;
|
||||||
transform: translate(-50%, -50%) scale(1);
|
user-select: none;
|
||||||
transition: transform 0.22s ease, border-width 0.22s ease, box-shadow 0.22s ease;
|
touch-action: none;
|
||||||
|
padding: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit::before,
|
.hub-clock-widget:hover {
|
||||||
.hub-compass-hit::after {
|
transform: scale(1.035);
|
||||||
content: "";
|
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.62), 0 0 26px rgba(155, 205, 255, 0.22);
|
||||||
|
filter: saturate(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-row {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-row:first-child { border-top: 0; }
|
||||||
|
.cw-row-main { grid-template-columns: 1fr; }
|
||||||
|
.cw-row-mid { grid-template-columns: repeat(4, 1fr); }
|
||||||
|
.cw-row-world { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
|
||||||
|
.cw-cell {
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.32);
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-cell:first-child { border-left: 0; }
|
||||||
|
|
||||||
|
.cw-main-cell {
|
||||||
|
min-height: 168px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
background: linear-gradient(180deg, rgba(30, 40, 28, 0.85), rgba(20, 28, 20, 0.85));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-line-time {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-line-time .cw-time-main {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-time-main {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-line {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-bold .ot-glyph-wrap--bold {
|
||||||
|
transform: scale(1.12, 1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#cw-hm-lcd .ot-glyph {
|
||||||
|
height: 68px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-sec {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-sec .ot-glyph-wrap:first-child {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-sec .ot-glyph {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-tz .ot-glyph {
|
||||||
|
height: 30px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-lcd-tz {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-glyph-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 0;
|
||||||
|
transform-origin: bottom center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-glyph-wrap--bold {
|
||||||
|
transform: scale(1.08, 1.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-glyph {
|
||||||
|
fill: #d8f6b8;
|
||||||
|
filter: drop-shadow(0 0 5px rgba(190, 240, 150, 0.38));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-glyph .ot-grid rect {
|
||||||
|
fill: rgba(20, 30, 24, 0.85);
|
||||||
|
filter: drop-shadow(0 0 1.4px rgba(135, 205, 255, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-glyph .ot-active {
|
||||||
|
fill: #d8f6b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ot-colon-blink {
|
||||||
|
animation: cwBlink 1.25s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-line-region {
|
||||||
|
color: #b8c2d4;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
font-weight: 300;
|
||||||
|
text-transform: uppercase;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-line-date {
|
||||||
|
color: #c5d0e2;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-row-mid {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-world-cell {
|
||||||
|
min-height: 178px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 25px 6px;
|
||||||
|
background: rgba(20, 28, 40, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-world-label {
|
||||||
|
color: #a6b0c2;
|
||||||
|
font-size: 9.5px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-dial {
|
||||||
|
width: 95px;
|
||||||
|
height: 95px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid rgba(239, 243, 252, 0.8);
|
||||||
|
background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.36), rgba(255, 255, 255, 0) 36%), #edf2fb;
|
||||||
|
box-shadow: inset -5px -5px 10px rgba(0, 0, 0, 0.14), inset 6px 7px 9px rgba(255, 255, 255, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-roman {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
width: 15px;
|
||||||
border-radius: 1px;
|
height: 15px;
|
||||||
background: rgba(231, 236, 243, 0.82);
|
margin: -7.5px;
|
||||||
transition: width 0.22s ease, height 0.22s ease;
|
transform: rotate(calc(var(--i) * 30deg)) translateY(-34px) rotate(calc(var(--i) * -30deg));
|
||||||
|
font-family: "Times New Roman", serif;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #223147;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit::before {
|
.cw-roman-major {
|
||||||
width: 3px;
|
font-size: 11.5px;
|
||||||
height: 46px;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit::after {
|
.cw-roman-minor {
|
||||||
width: 46px;
|
font-size: 3.2px;
|
||||||
height: 3px;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit:hover,
|
.cw-resize-handle {
|
||||||
.hub-compass-hit:focus-visible {
|
position: absolute;
|
||||||
transform: translate(-50%, -50%) scale(1.25);
|
width: 32px;
|
||||||
border-width: 2.3px;
|
height: 32px;
|
||||||
box-shadow:
|
z-index: 6;
|
||||||
-8px -10px 10px rgba(180, 228, 255, 0.22),
|
display: grid;
|
||||||
14px 16px 22px rgba(7, 10, 16, 0.34),
|
color: rgba(220, 236, 255, 0.9);
|
||||||
22px 26px 34px rgba(6, 8, 14, 0.42);
|
filter: drop-shadow(0 0 4px rgba(150, 200, 255, 0.35));
|
||||||
outline: none;
|
pointer-events: auto;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit:hover::before,
|
.cw-resize-handle svg {
|
||||||
.hub-compass-hit:hover::after,
|
width: 16px;
|
||||||
.hub-compass-hit:focus-visible::before,
|
height: 16px;
|
||||||
.hub-compass-hit:focus-visible::after {
|
fill: currentColor;
|
||||||
width: 52px;
|
pointer-events: none;
|
||||||
height: 3.45px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-compass-hit:hover::before,
|
.cw-resize-sw {
|
||||||
.hub-compass-hit:focus-visible::before {
|
left: 0;
|
||||||
width: 3.45px;
|
bottom: 0;
|
||||||
height: 52px;
|
cursor: nesw-resize;
|
||||||
|
place-items: end start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-resize-nw {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
place-items: start start;
|
||||||
|
transform: scaleY(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-resize-ne {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: nesw-resize;
|
||||||
|
place-items: start end;
|
||||||
|
transform: scale(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-resize-se {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
place-items: end end;
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-hand {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
transform: translate(-50%, -100%) rotate(0deg);
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cw-hour { width: 4px; height: 26%; background: #1f2f46; }
|
||||||
|
.cw-minute { width: 3px; height: 35%; background: #2e4767; }
|
||||||
|
.cw-second { width: 1.5px; height: 39%; background: #b63b3b; }
|
||||||
|
|
||||||
|
.cw-pin {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
margin-left: -3.5px;
|
||||||
|
margin-top: -3.5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #101928;
|
||||||
|
border: 1px solid #f3f6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes cwBlink {
|
||||||
|
0%, 20% { opacity: 0; }
|
||||||
|
20.01%, 60% { opacity: 1; }
|
||||||
|
60.01%, 100% { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-docs-modal {
|
.hub-docs-modal {
|
||||||
@@ -252,12 +489,89 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: min(980px, calc(100vw - 40px));
|
width: 90vw;
|
||||||
max-height: calc(100vh - 56px);
|
height: 90vh;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hub-docs-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 28%) 1fr;
|
||||||
|
gap: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc-group + .hub-docs-toc-group {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc-head,
|
||||||
|
.hub-docs-toc-leaf {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc-head {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc-leaf {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-toc-head:hover,
|
||||||
|
.hub-docs-toc-leaf:hover,
|
||||||
|
.hub-docs-toc-head:focus-visible,
|
||||||
|
.hub-docs-toc-leaf:focus-visible {
|
||||||
|
background: var(--row-hover);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-panels {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px 12px 14px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-docs-section .detail-tree-body {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.hub-docs-header {
|
.hub-docs-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -321,6 +635,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
|
.hub-clock-widget {
|
||||||
|
left: auto;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
width: min(94vw, 420px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.hub-cards {
|
.hub-cards {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
|
|||||||
@@ -11,12 +11,34 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
||||||
<link rel="stylesheet" href="hub.css">
|
<link rel="stylesheet" href="hub.css?v=20260527t">
|
||||||
<link rel="stylesheet" href="hub-logo-layers.css">
|
<link rel="stylesheet" href="hub-logo-layers.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="hub-page">
|
<body class="hub-page">
|
||||||
|
<div id="overtime-alphabet-host" hidden aria-hidden="true"></div>
|
||||||
<div id="hub-logo-stage" class="hub-logo-stage" hidden aria-hidden="true"></div>
|
<div id="hub-logo-stage" class="hub-logo-stage" hidden aria-hidden="true"></div>
|
||||||
<button id="hub-compass-hit" class="hub-compass-hit" type="button" aria-label="Open deployment model"></button>
|
<section class="hub-clock-widget" id="hub-clock-widget" aria-label="World clock widget">
|
||||||
|
<div class="cw-row cw-row-main">
|
||||||
|
<div class="cw-cell cw-main-cell">
|
||||||
|
<div class="cw-line cw-line-time">
|
||||||
|
<strong class="cw-time-main">
|
||||||
|
<span id="cw-hm-lcd" class="ot-lcd-line ot-lcd-bold" aria-label="Hours and minutes"></span>
|
||||||
|
<span id="cw-ss-lcd" class="ot-lcd-line ot-lcd-sec ot-lcd-bold" aria-label="Seconds"></span>
|
||||||
|
<span id="cw-tz-text" class="ot-lcd-line ot-lcd-tz ot-lcd-bold" aria-label="Timezone offset"></span>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="cw-line cw-line-region" id="cw-region">EUROPE/WARSAW</div>
|
||||||
|
<div class="cw-line cw-line-date" id="cw-date-line">WED 27 MAY 2026</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cw-row cw-row-mid">
|
||||||
|
<div class="cw-cell"></div>
|
||||||
|
<div class="cw-cell"></div>
|
||||||
|
<div class="cw-cell"></div>
|
||||||
|
<div class="cw-cell"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cw-row cw-row-world" id="cw-world-row"></div>
|
||||||
|
</section>
|
||||||
<div id="hub-docs-modal" class="hub-docs-modal" hidden aria-hidden="true">
|
<div id="hub-docs-modal" class="hub-docs-modal" hidden aria-hidden="true">
|
||||||
<div class="hub-docs-backdrop" data-close-docs></div>
|
<div class="hub-docs-backdrop" data-close-docs></div>
|
||||||
<section class="hub-docs-panel card card--lift" role="dialog" aria-modal="true" aria-labelledby="hub-docs-title">
|
<section class="hub-docs-panel card card--lift" role="dialog" aria-modal="true" aria-labelledby="hub-docs-title">
|
||||||
@@ -24,8 +46,11 @@
|
|||||||
<h2 id="hub-docs-title">Deployment model</h2>
|
<h2 id="hub-docs-title">Deployment model</h2>
|
||||||
<button type="button" class="hub-docs-close" data-close-docs aria-label="Close">x</button>
|
<button type="button" class="hub-docs-close" data-close-docs aria-label="Close">x</button>
|
||||||
</header>
|
</header>
|
||||||
<div id="hub-docs-body" class="hub-docs-body">
|
<div class="hub-docs-layout">
|
||||||
<p class="muted">Loading...</p>
|
<aside class="hub-docs-toc detail-tree" id="hub-docs-toc" aria-label="Table of contents">
|
||||||
|
<p class="muted hub-docs-toc-title">Table of contents</p>
|
||||||
|
</aside>
|
||||||
|
<div class="hub-docs-panels" id="hub-docs-panels"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +60,6 @@
|
|||||||
<div class="hub-brand">
|
<div class="hub-brand">
|
||||||
<img class="hub-logo-mark" src="assets/hub-logo-transparent.svg" alt="" width="112" height="84" decoding="async">
|
<img class="hub-logo-mark" src="assets/hub-logo-transparent.svg" alt="" width="112" height="84" decoding="async">
|
||||||
<h1 class="hub-title">Android Cast</h1>
|
<h1 class="hub-title">Android Cast</h1>
|
||||||
<img class="hub-thunder" src="assets/hub-thunder.svg" alt="Thunder underline" width="312" height="46" decoding="async">
|
|
||||||
</div>
|
</div>
|
||||||
<p class="muted hub-subtitle">Project hub</p>
|
<p class="muted hub-subtitle">Project hub</p>
|
||||||
<label class="toolbar-select hub-theme">
|
<label class="toolbar-select hub-theme">
|
||||||
@@ -90,17 +114,18 @@
|
|||||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||||
if (!supportsSvg || !supportsLayout) return;
|
if (!supportsSvg || !supportsLayout) return;
|
||||||
|
|
||||||
var logoBuild = '20260527j';
|
var logoBuild = '20260527s';
|
||||||
var fragments = {
|
var fragments = {
|
||||||
space: 'assets/hub-logo-fragment-space.svg',
|
space: 'assets/hub-logo-fragment-space.svg',
|
||||||
globe: 'assets/hub-logo-fragment-globe.svg',
|
globe: 'assets/hub-logo-fragment-globe.svg',
|
||||||
continents: 'assets/hub-logo-continents-mercator-globe.svg',
|
continents: 'assets/hub-logo-continents-mercator-globe.svg',
|
||||||
gridOverlay: 'assets/hub-logo-fragment-globe-grid-overlay.svg'
|
gridOverlay: 'assets/hub-logo-fragment-globe-grid-overlay.svg',
|
||||||
|
compass: 'assets/hub-logo-fragment-compass.svg'
|
||||||
};
|
};
|
||||||
var parts = {
|
var parts = {
|
||||||
full: ['space', 'globe', 'continents', 'gridOverlay'],
|
full: ['space', 'globe', 'continents', 'gridOverlay', 'compass'],
|
||||||
space: ['space'],
|
space: ['space'],
|
||||||
globe: ['globe', 'continents', 'gridOverlay']
|
globe: ['globe', 'continents', 'gridOverlay', 'compass']
|
||||||
};
|
};
|
||||||
|
|
||||||
var params = new URLSearchParams(window.location.search);
|
var params = new URLSearchParams(window.location.search);
|
||||||
@@ -123,6 +148,12 @@
|
|||||||
img.alt = '';
|
img.alt = '';
|
||||||
img.decoding = 'async';
|
img.decoding = 'async';
|
||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
|
if (name === 'compass') {
|
||||||
|
img.id = 'hub-compass-layer';
|
||||||
|
img.setAttribute('role', 'button');
|
||||||
|
img.setAttribute('tabindex', '0');
|
||||||
|
img.setAttribute('aria-label', 'Open deployment model');
|
||||||
|
}
|
||||||
stage.appendChild(img);
|
stage.appendChild(img);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,9 +166,14 @@
|
|||||||
stage.setAttribute('aria-hidden', 'true');
|
stage.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
var docsModal = document.getElementById('hub-docs-modal');
|
var docsModal = document.getElementById('hub-docs-modal');
|
||||||
var docsBody = document.getElementById('hub-docs-body');
|
var docsToc = document.getElementById('hub-docs-toc');
|
||||||
var compassHit = document.getElementById('hub-compass-hit');
|
var docsPanels = document.getElementById('hub-docs-panels');
|
||||||
if (!docsModal || !docsBody || !compassHit) return;
|
var compassLayer = document.getElementById('hub-compass-layer');
|
||||||
|
var docsApi = '/app/androidcast_project/crashes/api/hub_deploy_docs.php';
|
||||||
|
var docsQueue = [];
|
||||||
|
var docsLoading = false;
|
||||||
|
var docsCache = {};
|
||||||
|
var docsPanelsById = {};
|
||||||
|
|
||||||
function closeDocs() {
|
function closeDocs() {
|
||||||
docsModal.hidden = true;
|
docsModal.hidden = true;
|
||||||
@@ -149,42 +185,540 @@
|
|||||||
docsModal.setAttribute('aria-hidden', 'false');
|
docsModal.setAttribute('aria-hidden', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDocs(section) {
|
function xhrJson(url, onOk, onErr) {
|
||||||
var url = '/app/androidcast_project/crashes/api/hub_deploy_docs.php';
|
var xhr = new XMLHttpRequest();
|
||||||
if (section) url += '?section=' + encodeURIComponent(section);
|
xhr.open('GET', url, true);
|
||||||
docsBody.innerHTML = '<p class="muted">Loading...</p>';
|
xhr.setRequestHeader('Accept', 'application/json');
|
||||||
openDocs();
|
xhr.onreadystatechange = function () {
|
||||||
fetch(url, { headers: { 'Accept': 'application/json' } })
|
if (xhr.readyState !== 4) return;
|
||||||
.then(function (res) { return res.json(); })
|
if (xhr.status < 200 || xhr.status >= 300) {
|
||||||
.then(function (payload) {
|
if (onErr) onErr();
|
||||||
if (!payload || !payload.ok) {
|
return;
|
||||||
docsBody.innerHTML = '<p class="muted">Failed to load deployment model.</p>';
|
}
|
||||||
return;
|
try {
|
||||||
}
|
onOk(JSON.parse(xhr.responseText));
|
||||||
docsBody.innerHTML = payload.html || '<p class="muted">No content.</p>';
|
} catch (e) {
|
||||||
})
|
if (onErr) onErr();
|
||||||
.catch(function () {
|
}
|
||||||
docsBody.innerHTML = '<p class="muted">Network error while loading deployment model.</p>';
|
};
|
||||||
});
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
compassHit.addEventListener('click', function () { loadDocs('intro'); });
|
function ensureDocsPanel(sectionId, title) {
|
||||||
|
if (docsPanelsById[sectionId]) return docsPanelsById[sectionId];
|
||||||
|
var item = document.createElement('div');
|
||||||
|
item.className = 'detail-tree-item hub-docs-section';
|
||||||
|
item.dataset.section = sectionId;
|
||||||
|
var panelId = 'hub-doc-panel-' + sectionId;
|
||||||
|
item.innerHTML =
|
||||||
|
'<div class="detail-tree-row report-row" role="button" tabindex="0" data-tree-toggle data-controls="' + panelId + '">' +
|
||||||
|
'<span class="report-tree-cell">' +
|
||||||
|
'<button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="' + panelId + '" tabindex="-1">' +
|
||||||
|
'<span class="report-tree-arrow" aria-hidden="true"></span>' +
|
||||||
|
'</button>' +
|
||||||
|
'</span>' +
|
||||||
|
'<span class="detail-tree-caption">' + title + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="detail-tree-body" id="' + panelId + '" hidden><p class="muted">Loading...</p></div>';
|
||||||
|
docsPanels.appendChild(item);
|
||||||
|
docsPanelsById[sectionId] = item;
|
||||||
|
bindDocsTree(item);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindDocsTree(root) {
|
||||||
|
root.querySelectorAll('[data-tree-toggle]').forEach(function (row) {
|
||||||
|
var panelId = row.getAttribute('data-controls');
|
||||||
|
var panel = panelId ? document.getElementById(panelId) : null;
|
||||||
|
var btn = row.querySelector('.report-tree-toggle');
|
||||||
|
if (!panel) return;
|
||||||
|
var toggle = function () {
|
||||||
|
var open = panel.hidden;
|
||||||
|
panel.hidden = !open;
|
||||||
|
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
|
row.classList.toggle('report-row--open', open);
|
||||||
|
};
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
row.addEventListener('click', toggle);
|
||||||
|
row.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSectionHtml(sectionId, html) {
|
||||||
|
var item = docsPanelsById[sectionId];
|
||||||
|
if (!item) return;
|
||||||
|
var body = item.querySelector('.detail-tree-body');
|
||||||
|
var row = item.querySelector('.detail-tree-row');
|
||||||
|
var btn = item.querySelector('.report-tree-toggle');
|
||||||
|
if (body) body.innerHTML = html || '<p class="muted">No content.</p>';
|
||||||
|
if (body && row && body.hidden) {
|
||||||
|
body.hidden = false;
|
||||||
|
row.classList.add('report-row--open');
|
||||||
|
if (btn) btn.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pumpDocsQueue() {
|
||||||
|
if (docsLoading || docsQueue.length === 0) return;
|
||||||
|
docsLoading = true;
|
||||||
|
var sectionId = docsQueue.shift();
|
||||||
|
if (docsCache[sectionId]) {
|
||||||
|
renderSectionHtml(sectionId, docsCache[sectionId]);
|
||||||
|
docsLoading = false;
|
||||||
|
pumpDocsQueue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xhrJson(docsApi + '?section=' + encodeURIComponent(sectionId), function (payload) {
|
||||||
|
if (payload && payload.ok) {
|
||||||
|
docsCache[sectionId] = payload.html || '';
|
||||||
|
renderSectionHtml(sectionId, docsCache[sectionId]);
|
||||||
|
} else {
|
||||||
|
renderSectionHtml(sectionId, '<p class="muted">Failed to load section.</p>');
|
||||||
|
}
|
||||||
|
docsLoading = false;
|
||||||
|
pumpDocsQueue();
|
||||||
|
}, function () {
|
||||||
|
renderSectionHtml(sectionId, '<p class="muted">Network error while loading section.</p>');
|
||||||
|
docsLoading = false;
|
||||||
|
pumpDocsQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueSection(sectionId, front) {
|
||||||
|
if (!sectionId) return;
|
||||||
|
if (front) docsQueue.unshift(sectionId);
|
||||||
|
else docsQueue.push(sectionId);
|
||||||
|
pumpDocsQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTocNode(node, parentEl) {
|
||||||
|
var children = node.children || [];
|
||||||
|
if (children.length) {
|
||||||
|
children.forEach(function (child) {
|
||||||
|
ensureDocsPanel(child.id, child.title);
|
||||||
|
queueSection(child.id, false);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureDocsPanel(node.id, node.title);
|
||||||
|
queueSection(node.id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeploymentModel(prioritySection) {
|
||||||
|
openDocs();
|
||||||
|
docsPanels.innerHTML = '';
|
||||||
|
docsPanelsById = {};
|
||||||
|
docsCache = {};
|
||||||
|
docsQueue = [];
|
||||||
|
docsLoading = false;
|
||||||
|
docsToc.innerHTML = '<p class="muted hub-docs-toc-title">Table of contents</p>';
|
||||||
|
|
||||||
|
xhrJson(docsApi + '?action=toc', function (payload) {
|
||||||
|
if (!payload || !payload.ok || !payload.sections) {
|
||||||
|
docsPanels.innerHTML = '<p class="muted">Failed to load table of contents.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.sections.forEach(function (node) {
|
||||||
|
var group = document.createElement('div');
|
||||||
|
group.className = 'hub-docs-toc-group';
|
||||||
|
var head = document.createElement('button');
|
||||||
|
head.type = 'button';
|
||||||
|
head.className = 'hub-docs-toc-head';
|
||||||
|
head.textContent = node.title;
|
||||||
|
head.dataset.section = node.id;
|
||||||
|
head.addEventListener('click', function () {
|
||||||
|
var kids = node.children || [];
|
||||||
|
if (kids.length) queueSection(kids[0].id, true);
|
||||||
|
else queueSection(node.id, true);
|
||||||
|
});
|
||||||
|
group.appendChild(head);
|
||||||
|
(node.children || []).forEach(function (child) {
|
||||||
|
var leaf = document.createElement('button');
|
||||||
|
leaf.type = 'button';
|
||||||
|
leaf.className = 'hub-docs-toc-leaf';
|
||||||
|
leaf.textContent = child.title;
|
||||||
|
leaf.dataset.section = child.id;
|
||||||
|
leaf.addEventListener('click', function () { queueSection(child.id, true); });
|
||||||
|
group.appendChild(leaf);
|
||||||
|
});
|
||||||
|
docsToc.appendChild(group);
|
||||||
|
buildTocNode(node, docsPanels);
|
||||||
|
});
|
||||||
|
if (prioritySection) queueSection(prioritySection, true);
|
||||||
|
else if (payload.default_section) queueSection(payload.default_section, true);
|
||||||
|
}, function () {
|
||||||
|
docsPanels.innerHTML = '<p class="muted">Network error while loading deployment model.</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCompassOpen() {
|
||||||
|
if (!compassLayer) return;
|
||||||
|
var open = function () { openDeploymentModel(); };
|
||||||
|
compassLayer.addEventListener('click', open);
|
||||||
|
compassLayer.addEventListener('keydown', function (ev) {
|
||||||
|
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||||
|
ev.preventDefault();
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bindCompassOpen();
|
||||||
|
|
||||||
docsModal.addEventListener('click', function (ev) {
|
docsModal.addEventListener('click', function (ev) {
|
||||||
var target = ev.target;
|
var target = ev.target;
|
||||||
if (!(target instanceof Element)) return;
|
if (!(target instanceof Element)) return;
|
||||||
if (target.hasAttribute('data-close-docs')) {
|
if (target.hasAttribute('data-close-docs')) closeDocs();
|
||||||
closeDocs();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var link = target.closest('[data-doc-link]');
|
|
||||||
if (link) {
|
|
||||||
ev.preventDefault();
|
|
||||||
loadDocs(link.getAttribute('data-doc-link') || 'intro');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', function (ev) {
|
document.addEventListener('keydown', function (ev) {
|
||||||
if (ev.key === 'Escape' && !docsModal.hidden) closeDocs();
|
if (ev.key === 'Escape' && !docsModal.hidden) closeDocs();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var cityLabels = {
|
||||||
|
'America/New_York': 'NEW YORK',
|
||||||
|
'Europe/Moscow': 'MOSCOW',
|
||||||
|
'Asia/Shanghai': 'BEIJING',
|
||||||
|
'Africa/Johannesburg': 'CAPETOWN'
|
||||||
|
};
|
||||||
|
var roman = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
|
||||||
|
|
||||||
|
function pad2(v) { return String(v).padStart(2, '0'); }
|
||||||
|
function systemZone() { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; }
|
||||||
|
|
||||||
|
function secondaryZones(primary) {
|
||||||
|
var preferred = ['America/New_York', 'Europe/Moscow', 'Asia/Shanghai'];
|
||||||
|
var list = preferred.filter(function (z) { return z !== primary; });
|
||||||
|
while (list.length < 3) {
|
||||||
|
if (!list.includes('Africa/Johannesburg') && primary !== 'Africa/Johannesburg') list.push('Africa/Johannesburg');
|
||||||
|
else list.push('UTC');
|
||||||
|
}
|
||||||
|
return list.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeParts(date, zone) {
|
||||||
|
var parts = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: zone, hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||||
|
}).formatToParts(date);
|
||||||
|
var map = {};
|
||||||
|
parts.forEach(function (p) { if (p.type !== 'literal') map[p.type] = p.value; });
|
||||||
|
return { hh: map.hour || '00', mm: map.minute || '00', ss: map.second || '00' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date, zone) {
|
||||||
|
var parts = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: zone, weekday: 'short', day: '2-digit', month: 'short', year: 'numeric'
|
||||||
|
}).formatToParts(date);
|
||||||
|
var map = {};
|
||||||
|
parts.forEach(function (p) { if (p.type !== 'literal') map[p.type] = p.value; });
|
||||||
|
return (map.weekday || '---').slice(0, 3).toUpperCase() + ' '
|
||||||
|
+ (map.day || '00') + ' '
|
||||||
|
+ (map.month || '---').slice(0, 3).toUpperCase() + ' '
|
||||||
|
+ (map.year || '0000');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorldClock(tz) {
|
||||||
|
var cell = document.createElement('div');
|
||||||
|
cell.className = 'cw-cell cw-world-cell';
|
||||||
|
var label = document.createElement('div');
|
||||||
|
label.className = 'cw-world-label';
|
||||||
|
label.textContent = cityLabels[tz] || tz.toUpperCase();
|
||||||
|
cell.appendChild(label);
|
||||||
|
|
||||||
|
var dial = document.createElement('div');
|
||||||
|
dial.className = 'cw-dial';
|
||||||
|
for (var i = 0; i < 12; i++) {
|
||||||
|
var n = document.createElement('div');
|
||||||
|
n.className = 'cw-roman';
|
||||||
|
if (i === 0 || i === 3 || i === 6 || i === 9) n.className += ' cw-roman-major';
|
||||||
|
else n.className += ' cw-roman-minor';
|
||||||
|
n.style.setProperty('--i', String(i));
|
||||||
|
n.textContent = roman[i];
|
||||||
|
dial.appendChild(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hour = document.createElement('div');
|
||||||
|
hour.className = 'cw-hand cw-hour';
|
||||||
|
var minute = document.createElement('div');
|
||||||
|
minute.className = 'cw-hand cw-minute';
|
||||||
|
var second = document.createElement('div');
|
||||||
|
second.className = 'cw-hand cw-second';
|
||||||
|
var pin = document.createElement('div');
|
||||||
|
pin.className = 'cw-pin';
|
||||||
|
dial.appendChild(hour);
|
||||||
|
dial.appendChild(minute);
|
||||||
|
dial.appendChild(second);
|
||||||
|
dial.appendChild(pin);
|
||||||
|
cell.appendChild(dial);
|
||||||
|
|
||||||
|
return { tz: tz, hour: hour, minute: minute, second: second, root: cell };
|
||||||
|
}
|
||||||
|
|
||||||
|
var overtimeReady = false;
|
||||||
|
var overtimeSymbolIds = {};
|
||||||
|
|
||||||
|
function overtimeSymbolId(ch) {
|
||||||
|
if (ch >= '0' && ch <= '9') return 'ot-' + ch;
|
||||||
|
if (ch === ':') return 'ot-colon';
|
||||||
|
if (ch === '+') return 'ot-plus';
|
||||||
|
if (ch === '-') return 'ot-minus';
|
||||||
|
if (ch === ' ') return 'ot-space';
|
||||||
|
if (ch >= 'A' && ch <= 'Z') return 'ot-' + ch;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOvertimeAlphabet(done) {
|
||||||
|
var host = document.getElementById('overtime-alphabet-host');
|
||||||
|
if (!host) { done(false); return; }
|
||||||
|
fetch('assets/overtime-01-alphabet.svg?v=' + logoBuild)
|
||||||
|
.then(function (res) { return res.text(); })
|
||||||
|
.then(function (svgText) {
|
||||||
|
host.innerHTML = svgText;
|
||||||
|
host.querySelectorAll('symbol').forEach(function (sym) {
|
||||||
|
if (sym.id) overtimeSymbolIds[sym.id] = sym;
|
||||||
|
});
|
||||||
|
overtimeReady = true;
|
||||||
|
done(true);
|
||||||
|
})
|
||||||
|
.catch(function () { done(false); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGlyphGrid(svg, vb) {
|
||||||
|
var seg = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
|
seg.setAttribute('class', 'ot-grid');
|
||||||
|
var w = vb.width || 20;
|
||||||
|
var h = vb.height || 32;
|
||||||
|
var hPad = Math.max(1, w * 0.12);
|
||||||
|
var topY = Math.max(1, h * 0.04);
|
||||||
|
var midY = h * 0.47;
|
||||||
|
var botY = h * 0.86;
|
||||||
|
var segW = Math.max(6, w - hPad * 2);
|
||||||
|
var segH = Math.max(2, h * 0.09);
|
||||||
|
var vW = Math.max(2, w * 0.14);
|
||||||
|
var vH = Math.max(6, h * 0.33);
|
||||||
|
var leftX = Math.max(1, hPad * 0.45);
|
||||||
|
var rightX = Math.max(leftX + vW + 1, w - leftX - vW);
|
||||||
|
var upperY = topY + segH;
|
||||||
|
var lowerY = midY + segH * 0.6;
|
||||||
|
var defs = [
|
||||||
|
[hPad, topY, segW, segH],
|
||||||
|
[leftX, upperY, vW, vH],
|
||||||
|
[rightX, upperY, vW, vH],
|
||||||
|
[hPad, midY, segW, segH],
|
||||||
|
[leftX, lowerY, vW, vH],
|
||||||
|
[rightX, lowerY, vW, vH],
|
||||||
|
[hPad, botY, segW, segH]
|
||||||
|
];
|
||||||
|
defs.forEach(function (d) {
|
||||||
|
var r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
r.setAttribute('x', d[0].toFixed(2));
|
||||||
|
r.setAttribute('y', d[1].toFixed(2));
|
||||||
|
r.setAttribute('width', d[2].toFixed(2));
|
||||||
|
r.setAttribute('height', d[3].toFixed(2));
|
||||||
|
r.setAttribute('rx', '1');
|
||||||
|
seg.appendChild(r);
|
||||||
|
});
|
||||||
|
if (w >= 22) {
|
||||||
|
var d1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
d1.setAttribute('x', (w * 0.34).toFixed(2));
|
||||||
|
d1.setAttribute('y', (h * 0.15).toFixed(2));
|
||||||
|
d1.setAttribute('width', Math.max(2, w * 0.10).toFixed(2));
|
||||||
|
d1.setAttribute('height', Math.max(6, h * 0.34).toFixed(2));
|
||||||
|
d1.setAttribute('rx', '1');
|
||||||
|
d1.setAttribute('transform', 'rotate(22 ' + (w * 0.39).toFixed(2) + ' ' + (h * 0.32).toFixed(2) + ')');
|
||||||
|
seg.appendChild(d1);
|
||||||
|
var d2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
d2.setAttribute('x', (w * 0.56).toFixed(2));
|
||||||
|
d2.setAttribute('y', (h * 0.15).toFixed(2));
|
||||||
|
d2.setAttribute('width', Math.max(2, w * 0.10).toFixed(2));
|
||||||
|
d2.setAttribute('height', Math.max(6, h * 0.34).toFixed(2));
|
||||||
|
d2.setAttribute('rx', '1');
|
||||||
|
d2.setAttribute('transform', 'rotate(-22 ' + (w * 0.61).toFixed(2) + ' ' + (h * 0.32).toFixed(2) + ')');
|
||||||
|
seg.appendChild(d2);
|
||||||
|
}
|
||||||
|
svg.appendChild(seg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOvertimeText(el, text, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = '';
|
||||||
|
if (!overtimeReady) {
|
||||||
|
el.textContent = text;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String(text).toUpperCase().split('').forEach(function (ch) {
|
||||||
|
var sid = overtimeSymbolId(ch);
|
||||||
|
if (!sid || !overtimeSymbolIds[sid]) return;
|
||||||
|
var wrap = document.createElement('span');
|
||||||
|
wrap.className = 'ot-glyph-wrap' + (opts.bold ? ' ot-glyph-wrap--bold' : '');
|
||||||
|
if (opts.blinkColon && ch === ':') wrap.className += ' ot-colon-blink';
|
||||||
|
var sym = overtimeSymbolIds[sid];
|
||||||
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
var viewBox = sym.getAttribute('viewBox') || '0 0 20 32';
|
||||||
|
svg.setAttribute('viewBox', viewBox);
|
||||||
|
svg.setAttribute('class', 'ot-glyph');
|
||||||
|
var vb = viewBox.split(/\s+/).map(function (v) { return Number(v) || 0; });
|
||||||
|
buildGlyphGrid(svg, { width: vb[2] || 20, height: vb[3] || 32 });
|
||||||
|
var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||||
|
use.setAttribute('href', '#' + sid);
|
||||||
|
use.setAttribute('class', 'ot-active');
|
||||||
|
svg.appendChild(use);
|
||||||
|
wrap.appendChild(svg);
|
||||||
|
el.appendChild(wrap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOvertimeDigits(el, text, blinkColon, bold) {
|
||||||
|
renderOvertimeText(el, text, { blinkColon: blinkColon, bold: bold });
|
||||||
|
}
|
||||||
|
|
||||||
|
function regionLabel(tz) {
|
||||||
|
var parts = tz.split('/');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return parts[0].replace(/_/g, ' ').toUpperCase() + '/' + parts[parts.length - 1].replace(/_/g, ' ').toUpperCase();
|
||||||
|
}
|
||||||
|
return tz.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function gmtOffsetLabel(date, tz) {
|
||||||
|
try {
|
||||||
|
var parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'shortOffset' }).formatToParts(date);
|
||||||
|
var name = parts.find(function (p) { return p.type === 'timeZoneName'; });
|
||||||
|
if (name && name.value) {
|
||||||
|
return name.value.replace('GMT', 'GMT').replace('UTC', 'GMT').toUpperCase();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return 'GMT+0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateLine(date, zone) {
|
||||||
|
var parts = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: zone, weekday: 'short', day: '2-digit', month: 'short', year: 'numeric'
|
||||||
|
}).formatToParts(date);
|
||||||
|
var map = {};
|
||||||
|
parts.forEach(function (p) { if (p.type !== 'literal') map[p.type] = p.value; });
|
||||||
|
return (map.weekday || '---').slice(0, 3).toUpperCase() + ' '
|
||||||
|
+ String(Number(map.day || '0')).padStart(2, '0') + ' '
|
||||||
|
+ (map.month || '---').slice(0, 3).toUpperCase() + ' '
|
||||||
|
+ (map.year || '0000');
|
||||||
|
}
|
||||||
|
|
||||||
|
var primaryTz = systemZone();
|
||||||
|
var worldRow = document.getElementById('cw-world-row');
|
||||||
|
var secondary = secondaryZones(primaryTz).map(function (tz) {
|
||||||
|
var model = buildWorldClock(tz);
|
||||||
|
worldRow.appendChild(model.root);
|
||||||
|
return model;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateClockWidget() {
|
||||||
|
var now = new Date();
|
||||||
|
var p = timeParts(now, primaryTz);
|
||||||
|
renderOvertimeDigits(document.getElementById('cw-hm-lcd'), p.hh + ':' + p.mm, false, true);
|
||||||
|
renderOvertimeDigits(document.getElementById('cw-ss-lcd'), ':' + p.ss, true, true);
|
||||||
|
renderOvertimeText(document.getElementById('cw-tz-text'), gmtOffsetLabel(now, primaryTz), { bold: true });
|
||||||
|
document.getElementById('cw-region').textContent = regionLabel(primaryTz);
|
||||||
|
document.getElementById('cw-date-line').textContent = formatDateLine(now, primaryTz);
|
||||||
|
|
||||||
|
secondary.forEach(function (clock) {
|
||||||
|
var t = timeParts(now, clock.tz);
|
||||||
|
var h = Number(t.hh), m = Number(t.mm), s = Number(t.ss);
|
||||||
|
var hDeg = ((h % 12) + (m / 60) + (s / 3600)) * 30;
|
||||||
|
var mDeg = (m + (s / 60)) * 6;
|
||||||
|
var sDeg = s * 6;
|
||||||
|
clock.hour.style.transform = 'translate(-50%, -100%) rotate(' + hDeg + 'deg)';
|
||||||
|
clock.minute.style.transform = 'translate(-50%, -100%) rotate(' + mDeg + 'deg)';
|
||||||
|
clock.second.style.transform = 'translate(-50%, -100%) rotate(' + sDeg + 'deg)';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(function makeClockWidgetInteractive() {
|
||||||
|
var widget = document.getElementById('hub-clock-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
var arrowPath = 'M44.106,42.943H3.987L44.933,1.995c0.456-0.455,0.456-1.196,0-1.651c-0.452-0.455-1.195-0.455-1.651,0L2.336,41.292V1.168C2.336,0.524,1.81,0,1.168,0C0.523,0,0,0.524,0,1.168v44.111h44.111c0.644,0,1.168-0.522,1.168-1.168C45.279,43.469,44.755,42.943,44.106,42.943z';
|
||||||
|
var handles = ['nw', 'ne', 'sw', 'se'];
|
||||||
|
handles.forEach(function (pos) {
|
||||||
|
var h = document.createElement('span');
|
||||||
|
h.className = 'cw-resize-handle cw-resize-' + pos;
|
||||||
|
h.dataset.resize = pos;
|
||||||
|
h.setAttribute('aria-hidden', 'true');
|
||||||
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
svg.setAttribute('viewBox', '0 0 45.279 45.279');
|
||||||
|
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
|
path.setAttribute('d', arrowPath);
|
||||||
|
svg.appendChild(path);
|
||||||
|
h.appendChild(svg);
|
||||||
|
widget.appendChild(h);
|
||||||
|
});
|
||||||
|
|
||||||
|
var drag = null;
|
||||||
|
widget.addEventListener('pointerdown', function (ev) {
|
||||||
|
var target = ev.target;
|
||||||
|
if (!(target instanceof Element)) return;
|
||||||
|
var box = widget.getBoundingClientRect();
|
||||||
|
var handle = target.closest('[data-resize]');
|
||||||
|
var resizeDir = handle ? handle.getAttribute('data-resize') : null;
|
||||||
|
if (resizeDir) {
|
||||||
|
drag = {
|
||||||
|
mode: 'resize',
|
||||||
|
dir: resizeDir,
|
||||||
|
x: ev.clientX, y: ev.clientY,
|
||||||
|
left: box.left, top: box.top, width: box.width, height: box.height
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
drag = { mode: 'move', dx: ev.clientX - box.left, dy: ev.clientY - box.top };
|
||||||
|
}
|
||||||
|
widget.setPointerCapture(ev.pointerId);
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.addEventListener('pointermove', function (ev) {
|
||||||
|
if (!drag) return;
|
||||||
|
if (drag.mode === 'move') {
|
||||||
|
widget.style.left = Math.max(0, ev.clientX - drag.dx) + 'px';
|
||||||
|
widget.style.top = Math.max(0, ev.clientY - drag.dy) + 'px';
|
||||||
|
} else {
|
||||||
|
var minW = 280, minH = 210;
|
||||||
|
var dx = ev.clientX - drag.x;
|
||||||
|
var dy = ev.clientY - drag.y;
|
||||||
|
var left = drag.left, top = drag.top, width = drag.width, height = drag.height;
|
||||||
|
if (drag.dir.indexOf('e') !== -1) width = Math.max(minW, drag.width + dx);
|
||||||
|
if (drag.dir.indexOf('s') !== -1) height = Math.max(minH, drag.height + dy);
|
||||||
|
if (drag.dir.indexOf('w') !== -1) {
|
||||||
|
width = Math.max(minW, drag.width - dx);
|
||||||
|
left = drag.left + (drag.width - width);
|
||||||
|
}
|
||||||
|
if (drag.dir.indexOf('n') !== -1) {
|
||||||
|
height = Math.max(minH, drag.height - dy);
|
||||||
|
top = drag.top + (drag.height - height);
|
||||||
|
}
|
||||||
|
widget.style.left = Math.max(0, left) + 'px';
|
||||||
|
widget.style.top = Math.max(0, top) + 'px';
|
||||||
|
widget.style.width = width + 'px';
|
||||||
|
widget.style.minWidth = width + 'px';
|
||||||
|
widget.style.transform = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.addEventListener('pointerup', function (ev) {
|
||||||
|
if (!drag) return;
|
||||||
|
drag = null;
|
||||||
|
widget.releasePointerCapture(ev.pointerId);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
loadOvertimeAlphabet(function () {
|
||||||
|
updateClockWidget();
|
||||||
|
setInterval(updateClockWidget, 1000);
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -4,93 +4,143 @@ require_once __DIR__ . '/../../src/bootstrap.php';
|
|||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
$section = strtolower(trim((string)($_GET['section'] ?? 'intro')));
|
$action = strtolower(trim((string)($_GET['action'] ?? '')));
|
||||||
if ($section === '') $section = 'intro';
|
$section = strtolower(trim((string)($_GET['section'] ?? '')));
|
||||||
|
|
||||||
function hub_docs_nav(): string {
|
function hub_docs_toc(): array
|
||||||
return '<nav><strong>(2) TABLE OF CONTENTS</strong><ul>'
|
{
|
||||||
. '<li><a data-doc-link="intro" href="?section=intro">(1) INTRODUCTION</a></li>'
|
return [
|
||||||
. '<li><a data-doc-link="deploy" href="?section=deploy">(3) LOCAL SINGLE-STEP TEST DEPLOYMENT</a></li>'
|
[
|
||||||
. '<li><a data-doc-link="orchestration" href="?section=orchestration">(4) ORCHESTRATION MODEL</a></li>'
|
'id' => 'intro',
|
||||||
. '<li><a data-doc-link="routing" href="?section=routing">(5) XHR SUPERVISED VIEW MODEL</a></li>'
|
'title' => 'INTRODUCTION',
|
||||||
. '<li><a data-doc-link="ops" href="?section=ops">(6) OPERATIONS CHECKLIST</a></li>'
|
'children' => [
|
||||||
. '</ul></nav>';
|
['id' => 'intro-authors', 'title' => 'Authors'],
|
||||||
|
['id' => 'intro-revision', 'title' => 'Revision details'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'deploy',
|
||||||
|
'title' => 'LOCAL SINGLE-STEP TEST DEPLOYMENT',
|
||||||
|
'children' => [
|
||||||
|
['id' => 'deploy-prep', 'title' => 'Preparations, environment'],
|
||||||
|
['id' => 'deploy-packages', 'title' => 'Package prerequisites by distro'],
|
||||||
|
['id' => 'deploy-checks', 'title' => 'Local checks'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['id' => 'orchestration', 'title' => 'ORCHESTRATION MODEL', 'children' => []],
|
||||||
|
['id' => 'routing', 'title' => 'XHR SUPERVISED VIEW MODEL', 'children' => []],
|
||||||
|
[
|
||||||
|
'id' => 'ops',
|
||||||
|
'title' => 'OPERATIONS CHECKLIST',
|
||||||
|
'children' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function hub_docs_intro(): string {
|
function hub_docs_section_html(string $section): string
|
||||||
return '<h3>(1) INTRODUCTION</h3>'
|
{
|
||||||
. '<h4>(1.1) authors</h4>'
|
switch ($section) {
|
||||||
. '<p>Project owner: Anton Afanaasyeu. Implementation assistant: Cursor coding agent.</p>'
|
case 'intro-authors':
|
||||||
. '<h4>(1.2) revision details</h4>'
|
return '<p>Project owner: Anton Afanaasyeu. Implementation assistant: Cursor coding agent.</p>';
|
||||||
. '<p>Revision scope: landing compass interaction, XHR-driven deployment docs view, Docker orchestration model integration for next branch.</p>'
|
case 'intro-revision':
|
||||||
. hub_docs_nav();
|
return '<p>Revision scope: landing compass interaction, XHR-driven deployment docs view, Docker orchestration model integration for next branch.</p>';
|
||||||
|
case 'deploy-prep':
|
||||||
|
return '<p>Prerequisites: docker + docker compose plugin, git checkout of android cast on branch <code>next</code>.</p>'
|
||||||
|
. '<p>Run one command from repo: <code>cd orchestration && ./deploy.sh</code></p>';
|
||||||
|
case 'deploy-packages':
|
||||||
|
return '<table><thead><tr><th>Distro</th><th>Install steps</th></tr></thead><tbody>'
|
||||||
|
. '<tr><td>Alpine</td><td><code>apk add docker docker-cli-compose bash git curl</code><br><code>rc-update add docker default && service docker start</code></td></tr>'
|
||||||
|
. '<tr><td>Debian / Ubuntu</td><td><code>apt update && apt install -y docker.io docker-compose-plugin git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
||||||
|
. '<tr><td>Fedora</td><td><code>dnf install -y docker docker-compose-plugin git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
||||||
|
. '<tr><td>Arch</td><td><code>pacman -S --noconfirm docker docker-compose git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
||||||
|
. '</tbody></table>';
|
||||||
|
case 'deploy-checks':
|
||||||
|
return '<ul>'
|
||||||
|
. '<li><code>http://localhost:8080/app/androidcast_project/</code> (hub)</li>'
|
||||||
|
. '<li><code>http://localhost:8080/app/androidcast_project/crashes/</code> (crashes/tasks)</li>'
|
||||||
|
. '<li><code>http://localhost:8080/app/androidcast_project/git/</code> (redirect to gitea FE)</li>'
|
||||||
|
. '</ul>';
|
||||||
|
case 'orchestration':
|
||||||
|
return '<p>All infra runs in docker with shared source bind-mount where possible.</p>'
|
||||||
|
. '<ul>'
|
||||||
|
. '<li>landing FE (nginx): serves hub and routes subpaths</li>'
|
||||||
|
. '<li>gitea FE (nginx) + gitea app</li>'
|
||||||
|
. '<li>crashes FE (nginx) + php-fpm + mariadb</li>'
|
||||||
|
. '<li>single command create/update: <code>orchestration/deploy.sh</code></li>'
|
||||||
|
. '</ul>'
|
||||||
|
. '<p>Base images: nginx, php-fpm, mariadb. This stays minimal and reproducible on laptop devices.</p>';
|
||||||
|
case 'routing':
|
||||||
|
return '<p>The Compass click opens a modal. Content is requested via XMLHttpRequest/fetch from BE endpoint '
|
||||||
|
. '<code>/app/androidcast_project/crashes/api/hub_deploy_docs.php</code>.</p>'
|
||||||
|
. '<p>Internal links in this modal use tree-list captions and are intercepted on frontend, then re-requested through BE. '
|
||||||
|
. 'Browser URL bar is not changed.</p>'
|
||||||
|
. '<p>Static assets remain at original paths and are not rewritten in this view.</p>';
|
||||||
|
case 'ops':
|
||||||
|
return '<ol>'
|
||||||
|
. '<li>Pull latest <code>next</code></li>'
|
||||||
|
. '<li>Run <code>orchestration/deploy.sh</code></li>'
|
||||||
|
. '<li>Verify hub/crashes/gitea endpoints</li>'
|
||||||
|
. '<li>For app changes: edit source in repo, rerun deploy script</li>'
|
||||||
|
. '</ol>';
|
||||||
|
case 'intro':
|
||||||
|
return '<p>Select a topic in the table of contents to load section content from backend.</p>';
|
||||||
|
default:
|
||||||
|
return '<p class="muted">Section not found.</p>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hub_docs_deploy(): string {
|
function hub_docs_section_title(string $section): string
|
||||||
return '<h3>(3) LOCAL SINGLE-STEP TEST DEPLOYMENT</h3>'
|
{
|
||||||
. '<h4>(3.1) preparations, environment</h4>'
|
foreach (hub_docs_toc() as $node) {
|
||||||
. '<p>Prerequisites: docker + docker compose plugin, git checkout of android cast on branch <code>next</code>.</p>'
|
if ($node['id'] === $section) {
|
||||||
. '<p>Run one command from repo: <code>cd orchestration && ./deploy.sh</code></p>'
|
return (string)$node['title'];
|
||||||
. '<h4>(3.2) package prerequisites by distro</h4>'
|
}
|
||||||
. '<table><thead><tr><th>Distro</th><th>Install steps</th></tr></thead><tbody>'
|
foreach ($node['children'] as $child) {
|
||||||
. '<tr><td>Alpine</td><td><code>apk add docker docker-cli-compose bash git curl</code><br><code>rc-update add docker default && service docker start</code></td></tr>'
|
if ($child['id'] === $section) {
|
||||||
. '<tr><td>Debian / Ubuntu</td><td><code>apt update && apt install -y docker.io docker-compose-plugin git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
return (string)$child['title'];
|
||||||
. '<tr><td>Fedora</td><td><code>dnf install -y docker docker-compose-plugin git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
}
|
||||||
. '<tr><td>Arch</td><td><code>pacman -S --noconfirm docker docker-compose git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
}
|
||||||
. '</tbody></table>'
|
}
|
||||||
. '<h4>(3.3) local checks</h4>'
|
return 'Section';
|
||||||
. '<ul><li><code>http://localhost:8080/app/androidcast_project/</code> (hub)</li>'
|
|
||||||
. '<li><code>http://localhost:8080/app/androidcast_project/crashes/</code> (crashes/tasks)</li>'
|
|
||||||
. '<li><code>http://localhost:8080/app/androidcast_project/git/</code> (redirect to gitea FE)</li></ul>'
|
|
||||||
. hub_docs_nav();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hub_docs_orchestration(): string {
|
function hub_docs_all_section_ids(): array
|
||||||
return '<h3>(4) ORCHESTRATION MODEL</h3>'
|
{
|
||||||
. '<p>All infra runs in docker with shared source bind-mount where possible.</p>'
|
$ids = [];
|
||||||
. '<ul>'
|
foreach (hub_docs_toc() as $node) {
|
||||||
. '<li>landing FE (nginx): serves hub and routes subpaths</li>'
|
if (!empty($node['children'])) {
|
||||||
. '<li>gitea FE (nginx) + gitea app</li>'
|
foreach ($node['children'] as $child) {
|
||||||
. '<li>crashes FE (nginx) + php-fpm + mariadb</li>'
|
$ids[] = (string)$child['id'];
|
||||||
. '<li>single command create/update: <code>orchestration/deploy.sh</code></li>'
|
}
|
||||||
. '</ul>'
|
} else {
|
||||||
. '<p>Base images: nginx, php-fpm, mariadb. This stays minimal and reproducible on laptop devices.</p>'
|
$ids[] = (string)$node['id'];
|
||||||
. hub_docs_nav();
|
}
|
||||||
|
}
|
||||||
|
return $ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hub_docs_routing(): string {
|
if ($action === 'toc') {
|
||||||
return '<h3>(5) XHR SUPERVISED VIEW MODEL</h3>'
|
echo safe_json_encode([
|
||||||
. '<p>The Compass click opens a modal. Content is requested via XMLHttpRequest/fetch from BE endpoint '
|
'ok' => true,
|
||||||
. '<code>/app/androidcast_project/crashes/api/hub_deploy_docs.php</code>.</p>'
|
'sections' => hub_docs_toc(),
|
||||||
. '<p>Internal links in this modal use <code>data-doc-link</code> and are intercepted on frontend, then re-requested through BE. '
|
'default_section' => 'intro-authors',
|
||||||
. 'Browser URL bar is not changed.</p>'
|
'preload' => hub_docs_all_section_ids(),
|
||||||
. '<p>Static assets remain at original paths and are not rewritten in this view.</p>'
|
]);
|
||||||
. hub_docs_nav();
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hub_docs_ops(): string {
|
if ($section === '') {
|
||||||
return '<h3>(6) OPERATIONS CHECKLIST</h3>'
|
$section = 'intro-authors';
|
||||||
. '<ol>'
|
|
||||||
. '<li>Pull latest <code>next</code></li>'
|
|
||||||
. '<li>Run <code>orchestration/deploy.sh</code></li>'
|
|
||||||
. '<li>Verify hub/crashes/gitea endpoints</li>'
|
|
||||||
. '<li>For app changes: edit source in repo, rerun deploy script</li>'
|
|
||||||
. '</ol>'
|
|
||||||
. hub_docs_nav();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$map = [
|
$known = hub_docs_all_section_ids();
|
||||||
'intro' => 'hub_docs_intro',
|
if (!in_array($section, $known, true)) {
|
||||||
'deploy' => 'hub_docs_deploy',
|
$section = 'intro-authors';
|
||||||
'orchestration' => 'hub_docs_orchestration',
|
}
|
||||||
'routing' => 'hub_docs_routing',
|
|
||||||
'ops' => 'hub_docs_ops',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!isset($map[$section])) $section = 'intro';
|
|
||||||
|
|
||||||
echo safe_json_encode([
|
echo safe_json_encode([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'section' => $section,
|
'section' => $section,
|
||||||
'html' => $map[$section](),
|
'title' => hub_docs_section_title($section),
|
||||||
|
'html' => hub_docs_section_html($section),
|
||||||
]);
|
]);
|
||||||
|
|||||||