timezone-overlap-map 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  A web component that shows your timezone next to your visitor's on a world map, and how your working hours overlap. Built with React and TypeScript, shipped as a standard custom element, so it works in any framework or in plain HTML.
4
4
 
5
- - A world map with each zone's UTC-offset strip, a dot per city, and the live day/night line with the sun's position.
6
- - Both clocks and a zone picker that starts at the visitor's zone.
7
- - A 24-hour timeline in the visitor's local time, and the overlap as text for screen readers.
5
+ - A world map read as your day, 00 to 24: your working hours stay put, the visitor's slide with their zone, and the overlap is labeled with its length ("5 h").
6
+ - A dot per city and the live day/night line with the sun's position.
7
+ - Both clocks, a zone picker that starts at the visitor's zone, and the overlap in the visitor's own time as text (also for screen readers).
8
8
 
9
9
  About 15 KB gzipped, map included, plus React.
10
10
 
@@ -1,5 +1,14 @@
1
- export declare function WorldMap({ homeZone, visitorZone, now }: {
1
+ import { type Segment } from './overlap';
2
+ export type WorldMapProps = {
2
3
  homeZone: string;
3
4
  visitorZone: string;
4
5
  now: Date;
5
- }): import("react").JSX.Element;
6
+ /** Working hours in the home zone's day (minutes), already split at midnight. */
7
+ home: Segment[];
8
+ visitor: Segment[];
9
+ both: Segment[];
10
+ nowMinute: number;
11
+ /** Written on the overlap, e.g. "4 h". */
12
+ label: string;
13
+ };
14
+ export declare function WorldMap({ homeZone, visitorZone, now, home, visitor, both, nowMinute, label }: WorldMapProps): import("react").JSX.Element;
@@ -4,6 +4,8 @@ export type Messages = {
4
4
  /** `duration` is already formatted ("2 h 30 min"), `ranges` is in the visitor's local time. */
5
5
  overlap: (duration: string, home: string, ranges: string) => string;
6
6
  none: (home: string) => string;
7
+ /** Under the map, whose clock its hours follow. */
8
+ axis: (home: string) => string;
7
9
  };
8
10
  export declare const MESSAGES: {
9
11
  en: Messages;
@@ -90,12 +90,14 @@ var b = {
90
90
  en: {
91
91
  zoneLabel: "Your time zone",
92
92
  overlap: (e, t, n) => `${e} of overlap with ${t}: ${n} your time.`,
93
- none: (e) => `No working-hours overlap with ${e}.`
93
+ none: (e) => `No working-hours overlap with ${e}.`,
94
+ axis: (e) => `${e}’s time`
94
95
  },
95
96
  es: {
96
97
  zoneLabel: "Tu zona horaria",
97
98
  overlap: (e, t, n) => `${e} en común con ${t}: ${n} en tu horario.`,
98
- none: (e) => `No hay horario laboral en común con ${e}.`
99
+ none: (e) => `No hay horario laboral en común con ${e}.`,
100
+ axis: (e) => `hora de ${e}`
99
101
  }
100
102
  };
101
103
  function x(e) {
@@ -675,22 +677,7 @@ function k(e, t = 2) {
675
677
  return `${i}L360 ${r}Z`;
676
678
  }
677
679
  // src/WorldMap.tsx
678
- function A({ zone: e, date: t, side: n }) {
679
- let r = u(e, t) / 60 * 15 + 180 - 7.5;
680
- return /* @__PURE__ */ a("g", {
681
- className: `band ${n}`,
682
- children: [
683
- r - 360,
684
- r,
685
- r + 360
686
- ].map((e) => /* @__PURE__ */ a("rect", {
687
- x: e,
688
- y: 0,
689
- width: 15,
690
- height: 142
691
- }, e))
692
- });
693
- }
680
+ var A = (e) => e / s * 360;
694
681
  function j({ zone: e, side: t }) {
695
682
  let n = C[e];
696
683
  if (!n) return null;
@@ -702,8 +689,19 @@ function j({ zone: e, side: t }) {
702
689
  r: 2.5
703
690
  });
704
691
  }
705
- function M({ homeZone: e, visitorZone: t, now: r }) {
706
- let i = n(() => k(r), [r]), s = D(r);
692
+ function M({ segments: e, className: t }) {
693
+ return /* @__PURE__ */ a("g", {
694
+ className: `window ${t}`,
695
+ children: e.map((e) => /* @__PURE__ */ a("rect", {
696
+ x: A(e.start),
697
+ y: 0,
698
+ width: A(e.end - e.start),
699
+ height: 142
700
+ }, e.start))
701
+ });
702
+ }
703
+ function N({ homeZone: e, visitorZone: t, now: r, home: i, visitor: s, both: c, nowMinute: l, label: u }) {
704
+ let d = n(() => k(r), [r]), f = D(r), p = c.reduce((e, t) => !e || t.end - t.start > e.end - e.start ? t : e, void 0);
707
705
  return /* @__PURE__ */ o("svg", {
708
706
  className: "map",
709
707
  viewBox: "0 0 360 142",
@@ -713,22 +711,24 @@ function M({ homeZone: e, visitorZone: t, now: r }) {
713
711
  className: "land",
714
712
  d: S
715
713
  }),
716
- /* @__PURE__ */ a(A, {
717
- zone: e,
718
- date: r,
719
- side: "home"
720
- }),
721
- /* @__PURE__ */ a(A, {
722
- zone: t,
723
- date: r,
724
- side: "visitor"
725
- }),
726
714
  /* @__PURE__ */ a("path", {
727
715
  className: "night",
728
- d: i
716
+ d
717
+ }),
718
+ /* @__PURE__ */ a(M, {
719
+ segments: i,
720
+ className: "home"
721
+ }),
722
+ /* @__PURE__ */ a(M, {
723
+ segments: s,
724
+ className: "visitor"
725
+ }),
726
+ /* @__PURE__ */ a(M, {
727
+ segments: c,
728
+ className: "both"
729
729
  }),
730
730
  /* @__PURE__ */ o("g", {
731
- transform: `translate(${s.lon + 180} ${84 - s.lat})`,
731
+ transform: `translate(${f.lon + 180} ${84 - f.lat})`,
732
732
  children: [/* @__PURE__ */ a("circle", {
733
733
  className: "sun-halo",
734
734
  r: 7
@@ -744,75 +744,105 @@ function M({ homeZone: e, visitorZone: t, now: r }) {
744
744
  /* @__PURE__ */ a(j, {
745
745
  zone: t,
746
746
  side: "visitor"
747
+ }),
748
+ /* @__PURE__ */ a("line", {
749
+ className: "now",
750
+ x1: A(l),
751
+ x2: A(l),
752
+ y1: 0,
753
+ y2: 142
754
+ }),
755
+ p && /* @__PURE__ */ a("text", {
756
+ className: "label",
757
+ x: A((p.start + p.end) / 2),
758
+ y: 71,
759
+ children: u
747
760
  })
748
761
  ]
749
762
  });
750
763
  }
751
764
  // src/zones.ts
752
- function N(e) {
765
+ function P(e) {
753
766
  return (e.split("/").pop() ?? e).replaceAll("_", " ");
754
767
  }
755
- function P() {
768
+ function F() {
756
769
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
757
770
  }
758
- function F(e = []) {
771
+ function I(e = []) {
759
772
  let t = Intl.supportedValuesOf("timeZone");
760
773
  return [.../* @__PURE__ */ new Set([...t, ...e])].sort();
761
774
  }
762
775
  // src/TimezoneOverlap.tsx
763
- function I(e = 6e4) {
776
+ function L(e = 6e4) {
764
777
  let [n, i] = r(() => /* @__PURE__ */ new Date());
765
778
  return t(() => {
766
779
  let t = setInterval(() => i(/* @__PURE__ */ new Date()), e);
767
780
  return () => clearInterval(t);
768
781
  }, [e]), n;
769
782
  }
770
- var L = (e, t, n) => new Intl.DateTimeFormat(n, {
783
+ var R = (e, t, n) => new Intl.DateTimeFormat(n, {
771
784
  timeZone: t,
772
785
  hour: "2-digit",
773
786
  minute: "2-digit",
774
787
  hourCycle: "h23"
775
- }).format(e), R = (e) => {
788
+ }).format(e), z = (e) => {
776
789
  let t = c(e);
777
790
  return `${String(Math.floor(t / 60)).padStart(2, "0")}:${String(t % 60).padStart(2, "0")}`;
778
- }, z = (e) => {
791
+ }, B = (e) => {
779
792
  let t = Math.floor(e / 60), n = e % 60;
780
793
  return n ? `${t} h ${n} min` : `${t} h`;
781
- }, B = (e) => h(e).map((e) => `${R(e.start)}–${R(e.end)}`).join(", ");
782
- function V({ segments: e, className: t }) {
783
- return /* @__PURE__ */ a("div", {
784
- className: "track",
785
- children: e.map((e) => /* @__PURE__ */ a("span", {
786
- className: t,
787
- style: {
788
- left: `${e.start / s * 100}%`,
789
- width: `${(e.end - e.start) / s * 100}%`
790
- }
791
- }, e.start))
792
- });
793
- }
794
- function H({ homeZone: e, homeLabel: t, visitorZone: i, workStart: l, workEnd: d, lang: p, onVisitorZoneChange: m }) {
795
- let h = b[p], g = I(), [v, y] = r(i), x = n(() => F([e, v]), [e, v]), S = n(() => _(e, v, {
796
- workStart: l,
797
- workEnd: d,
798
- date: g
794
+ }, V = (e) => h(e).map((e) => `${z(e.start)}–${z(e.end)}`).join(", ");
795
+ function H({ homeZone: e, homeLabel: t, visitorZone: i, workStart: s, workEnd: l, lang: d, onVisitorZoneChange: p }) {
796
+ let m = b[d], h = L(), [g, v] = r(i), y = n(() => I([e, g]), [e, g]), x = n(() => _(e, g, {
797
+ workStart: s,
798
+ workEnd: l,
799
+ date: h
799
800
  }), [
800
801
  e,
801
- v,
802
+ g,
803
+ s,
802
804
  l,
803
- d,
804
- g
805
- ]), C = u(v, g), w = c(g.getUTCHours() * 60 + g.getUTCMinutes() + C), T = t || N(e);
806
- function E(e) {
807
- y(e), m?.(e);
805
+ h
806
+ ]), S = u(e, h), C = c(h.getUTCHours() * 60 + h.getUTCMinutes() + S), w = t || P(e), T = f(x.both, S), E = f(x.both, u(g, h));
807
+ function D(e) {
808
+ v(e), p?.(e);
808
809
  }
809
810
  return /* @__PURE__ */ o("div", {
810
811
  className: "widget",
811
812
  children: [
812
- /* @__PURE__ */ a(M, {
813
- homeZone: e,
814
- visitorZone: v,
815
- now: g
813
+ /* @__PURE__ */ o("div", {
814
+ className: "chart",
815
+ children: [
816
+ /* @__PURE__ */ a(N, {
817
+ homeZone: e,
818
+ visitorZone: g,
819
+ now: h,
820
+ home: f(x.home, S),
821
+ visitor: f(x.visitor, S),
822
+ both: T,
823
+ nowMinute: C,
824
+ label: B(x.minutes)
825
+ }),
826
+ /* @__PURE__ */ a("div", {
827
+ className: "ticks",
828
+ "aria-hidden": "true",
829
+ children: [
830
+ 0,
831
+ 6,
832
+ 12,
833
+ 18,
834
+ 24
835
+ ].map((e) => /* @__PURE__ */ a("span", {
836
+ style: { left: `${e / 24 * 100}%` },
837
+ children: z(e * 60).slice(0, 2)
838
+ }, e))
839
+ }),
840
+ /* @__PURE__ */ a("p", {
841
+ className: "axis-name",
842
+ "aria-hidden": "true",
843
+ children: m.axis(w)
844
+ })
845
+ ]
816
846
  }),
817
847
  /* @__PURE__ */ o("div", {
818
848
  className: "clocks",
@@ -823,9 +853,9 @@ function H({ homeZone: e, homeLabel: t, visitorZone: i, workStart: l, workEnd: d
823
853
  className: "swatch home",
824
854
  "aria-hidden": "true"
825
855
  }),
826
- T,
856
+ w,
827
857
  " ",
828
- /* @__PURE__ */ a("time", { children: L(g, e, p) })
858
+ /* @__PURE__ */ a("time", { children: R(h, e, d) })
829
859
  ]
830
860
  }), /* @__PURE__ */ o("label", {
831
861
  className: "clock",
@@ -835,63 +865,28 @@ function H({ homeZone: e, homeLabel: t, visitorZone: i, workStart: l, workEnd: d
835
865
  "aria-hidden": "true"
836
866
  }),
837
867
  /* @__PURE__ */ a("select", {
838
- "aria-label": h.zoneLabel,
839
- value: v,
840
- onChange: (e) => E(e.target.value),
841
- children: x.map((e) => /* @__PURE__ */ a("option", {
868
+ "aria-label": m.zoneLabel,
869
+ value: g,
870
+ onChange: (e) => D(e.target.value),
871
+ children: y.map((e) => /* @__PURE__ */ a("option", {
842
872
  value: e,
843
873
  children: e.replaceAll("_", " ")
844
874
  }, e))
845
875
  }),
846
- /* @__PURE__ */ a("time", { children: L(g, v, p) })
876
+ /* @__PURE__ */ a("time", { children: R(h, g, d) })
847
877
  ]
848
878
  })]
849
879
  }),
850
- /* @__PURE__ */ o("div", {
851
- className: "timeline",
852
- "aria-hidden": "true",
853
- children: [
854
- /* @__PURE__ */ a(V, {
855
- segments: f(S.home, C),
856
- className: "home"
857
- }),
858
- /* @__PURE__ */ a(V, {
859
- segments: f(S.visitor, C),
860
- className: "visitor"
861
- }),
862
- /* @__PURE__ */ a(V, {
863
- segments: f(S.both, C),
864
- className: "both"
865
- }),
866
- /* @__PURE__ */ a("span", {
867
- className: "now",
868
- style: { left: `${w / s * 100}%` }
869
- }),
870
- /* @__PURE__ */ a("div", {
871
- className: "ticks",
872
- children: [
873
- 0,
874
- 6,
875
- 12,
876
- 18,
877
- 24
878
- ].map((e) => /* @__PURE__ */ a("span", {
879
- style: { left: `${e / 24 * 100}%` },
880
- children: R(e * 60).slice(0, 2)
881
- }, e))
882
- })
883
- ]
884
- }),
885
880
  /* @__PURE__ */ a("p", {
886
881
  className: "summary",
887
882
  role: "status",
888
- children: S.minutes > 0 ? h.overlap(z(S.minutes), T, B(f(S.both, C))) : h.none(T)
883
+ children: x.minutes > 0 ? m.overlap(B(x.minutes), w, V(E)) : m.none(w)
889
884
  })
890
885
  ]
891
886
  });
892
887
  }
893
888
  // src/styles.css?inline
894
- var U = ":host{--_bg:var(--tom-bg,transparent);--_fg:var(--tom-fg,CanvasText);--_muted:var(--tom-muted,color-mix(in oklch, var(--_fg) 55%, transparent));--_track:var(--tom-track,color-mix(in oklch, var(--_fg) 10%, transparent));--_home:var(--tom-home,oklch(72% .14 200));--_visitor:var(--tom-visitor,oklch(75% .15 70));--_both:var(--tom-overlap,oklch(72% .17 145));--_land:var(--tom-land,color-mix(in oklch, var(--_fg) 22%, transparent));--_night:var(--tom-night,var(--lightningcss-light,oklch(30% .03 260/.16))var(--lightningcss-dark,oklch(0% 0 0/.42)));--_sun:var(--tom-sun,var(--lightningcss-light,oklch(78% .16 75))var(--lightningcss-dark,oklch(97% .06 95)));--_radius:var(--tom-radius,4px);font:var(--tom-font,inherit);color:var(--_fg);background:var(--_bg);display:block}:host([hidden]){display:none}.widget{gap:1rem;display:grid;container-type:inline-size}.map{block-size:auto;inline-size:100%;display:block;overflow:hidden}.land{fill:var(--_land)}.band rect{opacity:.28}.band.home rect{fill:var(--_home)}.band.visitor rect{fill:var(--_visitor)}.night{fill:var(--_night)}.sun{fill:var(--_sun)}.sun-halo{fill:var(--_sun);opacity:.22}.dot{stroke:var(--_fg);stroke-width:.75px}.dot.home{fill:var(--_home)}.dot.visitor{fill:var(--_visitor)}@container (width<480px){.dot{r:4;stroke-width:1px}.sun{r:3.5}.sun-halo{r:9}}.clocks{flex-wrap:wrap;justify-content:space-between;gap:.5rem 1.5rem;display:flex}.clock{align-items:center;gap:.5rem;margin:0;display:flex}.clock time{font-variant-numeric:tabular-nums;color:var(--_muted)}select{font:inherit;color:inherit;border:1px solid var(--_track);border-radius:var(--_radius);background:0 0;max-width:16rem;padding:.125rem .25rem}.swatch{border-radius:50%;block-size:.75rem;inline-size:.75rem}.timeline{gap:.25rem;padding-block-end:1.25rem;display:grid;position:relative}.track{border-radius:var(--_radius);background:var(--_track);block-size:.5rem;position:relative}.track>span{border-radius:var(--_radius);position:absolute;inset-block:0}.home{background:var(--_home)}.visitor{background:var(--_visitor)}.both{background:var(--_both)}.now{background:var(--_fg);inline-size:2px;position:absolute;inset-block:-.25rem 1rem;translate:-1px}.ticks{color:var(--_muted);font-size:.75em;position:absolute;inset-block-end:0;inset-inline:0}.ticks>span{position:absolute;translate:-50%}.ticks>span:first-child{translate:0}.ticks>span:last-child{translate:-100%}.summary{margin:0}", W = "timezone-overlap-map", G = [
889
+ var U = ":host{--_bg:var(--tom-bg,transparent);--_fg:var(--tom-fg,CanvasText);--_muted:var(--tom-muted,color-mix(in oklch, var(--_fg) 55%, transparent));--_track:var(--tom-track,color-mix(in oklch, var(--_fg) 10%, transparent));--_home:var(--tom-home,oklch(72% .14 200));--_visitor:var(--tom-visitor,oklch(75% .15 70));--_both:var(--tom-overlap,oklch(72% .17 145));--_land:var(--tom-land,color-mix(in oklch, var(--_fg) 22%, transparent));--_night:var(--tom-night,oklch(0% 0 0/.42));--_sun:var(--tom-sun,oklch(97% .06 95));--_radius:var(--tom-radius,4px);font:var(--tom-font,inherit);color:var(--_fg);background:var(--_bg);display:block}@supports (color:light-dark(black, white)){:host{--_night:var(--tom-night,light-dark(oklch(30% .03 260/.16),oklch(0% 0 0/.42)));--_sun:var(--tom-sun,light-dark(oklch(78% .16 75),oklch(97% .06 95)))}}:host([hidden]){display:none}.widget{gap:1rem;display:grid;container-type:inline-size}.map{block-size:auto;inline-size:100%;display:block;overflow:hidden}.land{fill:var(--_land)}.window rect{opacity:.22}.window.home rect{fill:var(--_home)}.window.visitor rect{fill:var(--_visitor)}.window.both rect{fill:var(--_both);opacity:.5}.map .now{stroke:var(--_fg);stroke-width:.75px;stroke-dasharray:2 2}.label{fill:var(--_fg);text-anchor:middle;dominant-baseline:central;paint-order:stroke;stroke:var(--tom-bg,Canvas);stroke-width:3px;stroke-linejoin:round;font-size:14px;font-weight:700}.night{fill:var(--_night)}.sun{fill:var(--_sun)}.sun-halo{fill:var(--_sun);opacity:.22}.dot{stroke:var(--_fg);stroke-width:.75px}.dot.home{fill:var(--_home)}.dot.visitor{fill:var(--_visitor)}@container (width<480px){.dot{r:4;stroke-width:1px}.sun{r:3.5}.sun-halo{r:9}}.clocks{flex-wrap:wrap;justify-content:space-between;gap:.5rem 1.5rem;display:flex}.clock{align-items:center;gap:.5rem;margin:0;display:flex}.clock time{font-variant-numeric:tabular-nums;color:var(--_muted)}select{font:inherit;color:inherit;border:1px solid var(--_track);border-radius:var(--_radius);background:0 0;max-width:16rem;padding:.125rem .25rem}.swatch{border-radius:50%;block-size:.75rem;inline-size:.75rem}.swatch.home{background:var(--_home)}.swatch.visitor{background:var(--_visitor)}.ticks{block-size:1.5em;color:var(--_muted);font-size:.75em;position:relative}.ticks>span{position:absolute;translate:-50%}.ticks>span:first-child{translate:0}.ticks>span:last-child{translate:-100%}.axis-name{text-align:center;color:var(--_muted);margin:0;font-size:.75em}.summary{margin:0}", W = "timezone-overlap-map", G = [
895
890
  "home-zone",
896
891
  "home-label",
897
892
  "visitor-zone",
@@ -932,7 +927,7 @@ var X = class extends K {
932
927
  this.#e && this.#n();
933
928
  }
934
929
  #n() {
935
- let t = this.getAttribute("home-zone"), n = this.getAttribute("visitor-zone"), r = n && l(n) ? n : P();
930
+ let t = this.getAttribute("home-zone"), n = this.getAttribute("visitor-zone"), r = n && l(n) ? n : F();
936
931
  this.#e?.render(/* @__PURE__ */ a(e, { children: /* @__PURE__ */ a(H, {
937
932
  homeZone: t && l(t) ? t : "UTC",
938
933
  homeLabel: this.getAttribute("home-label") ?? void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "timezone-overlap-map",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A web component that shows two timezones and how their working hours overlap",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Palenque",