zero-hour 1.0.0-dev.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ux-ui.pro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ <p align="center"><strong>zero-hour</strong></p>
2
+
3
+ <div align="center">
4
+ <p align="center">ZeroHour is a tiny countdown Web Component. It registers <code>&lt;countdown-timer&gt;</code> that renders a <code>DD:HH:MM:SS</code> countdown (with configurable visible units), counts down to a target date/time with an optional UTC offset, ticks on exact second boundaries, and fires a <code>done</code> event when it reaches zero.</p>
5
+
6
+ [![npm](https://img.shields.io/npm/v/zero-hour.svg?colorB=brightgreen)](https://www.npmjs.com/package/zero-hour)
7
+ [![GitHub package version](https://img.shields.io/github/package-json/v/ux-ui-pro/zero-hour.svg)](https://github.com/ux-ui-pro/zero-hour)
8
+ [![NPM Downloads](https://img.shields.io/npm/dm/zero-hour.svg?style=flat)](https://www.npmjs.org/package/zero-hour)
9
+
10
+ <sup>Web Component • TypeScript • ESM/CJS</sup>
11
+ </div>
12
+ <br>
13
+
14
+ ➠ **Install**
15
+
16
+ ```console
17
+ yarn add zero-hour
18
+ ```
19
+ <br>
20
+
21
+ ➠ **Import**
22
+
23
+ ```javascript
24
+ // Registers <countdown-timer> via customElements.define(...)
25
+ import 'zero-hour';
26
+
27
+ // Optional helper: subscribe to `done` and apply stylesheets
28
+ import { initCountdownTimers } from 'zero-hour';
29
+ ```
30
+ <br>
31
+
32
+ ➠ **Usage**
33
+
34
+ ```javascript
35
+ // After importing 'zero-hour', the element is registered.
36
+ // Then just place <countdown-timer> in your HTML (see below).
37
+ ```
38
+
39
+ <sub>HTML: default start (autostart=true)</sub>
40
+ ```html
41
+ <countdown-timer
42
+ digits-url="/sprites/digits.webp"
43
+ separator-url="/sprites/sep.webp"
44
+ date="2025-12-31"
45
+ time="23:59:59"
46
+ utc="+03:00"
47
+ ></countdown-timer>
48
+ ```
49
+
50
+ <sub>JS: subscribe to completion + optional styles</sub>
51
+ ```javascript
52
+ import { initCountdownTimers } from 'zero-hour';
53
+
54
+ initCountdownTimers({
55
+ selector: 'countdown-timer',
56
+ onDone: (el) => {
57
+ // The component dispatches: el.dispatchEvent(new CustomEvent('done'))
58
+ el.classList.add('is-done');
59
+ },
60
+ // Optional styles:
61
+ // - CSSStyleSheet (constructable stylesheet)
62
+ // - string (e.g. imported via ?raw from your CSS/SCSS pipeline)
63
+ // stylesheet: myCssStyleSheet,
64
+ });
65
+ ```
66
+
67
+ <sub>JS: custom styles from `?raw` (CSS/SCSS)</sub>
68
+ ```javascript
69
+ import { initCountdownTimers } from 'zero-hour';
70
+ import ZeroHourCss from './assets/scss/components/zero-hour.scss?raw';
71
+
72
+ initCountdownTimers({
73
+ stylesheet: ZeroHourCss,
74
+ });
75
+ ```
76
+
77
+ <sub>JS: default styles shipped with the package</sub>
78
+ ```javascript
79
+ import { initCountdownTimers, zeroHourCssText } from 'zero-hour';
80
+
81
+ // Option A: use the built-in CSS text export
82
+ initCountdownTimers({
83
+ stylesheet: zeroHourCssText,
84
+ });
85
+
86
+ // Option B: import the package CSS file as raw text (your bundler must support ?raw)
87
+ // import ZeroHourCss from 'zero-hour/zero-hour.css?raw';
88
+ // initCountdownTimers({ stylesheet: ZeroHourCss });
89
+ ```
90
+
91
+ <sub>JS: manual control (start/stop/reset)</sub>
92
+ ```javascript
93
+ const el = document.querySelector('countdown-timer');
94
+ // @ts-expect-error: methods exist on the custom element instance after import
95
+ el?.stop();
96
+ // @ts-expect-error
97
+ el?.reset();
98
+ // @ts-expect-error
99
+ el?.start();
100
+ ```
101
+
102
+ <sub>Units (units)</sub>
103
+ ```html
104
+ <countdown-timer
105
+ digits-url="/sprites/digits.webp"
106
+ separator-url="/sprites/sep.webp"
107
+ date="2025-12-31"
108
+ time="23:59:59"
109
+ utc="UTC+0"
110
+ units="h:m:s"
111
+ ></countdown-timer>
112
+ ```
113
+ <br>
114
+
115
+ ➠ **Options**
116
+
117
+ | Option (attribute) | Type | Default | Description |
118
+ |:--------------------:|:-----------------------:|:------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
119
+ | `digits-url` | `string` | — | URL to the digits sprite sheet. **Required** for the graphical display (otherwise only a text fallback is updated in the a11y layer). |
120
+ | `separator-url` | `string` | `null` | URL to the separator sprite (e.g. a colon). If omitted, separators are hidden. |
121
+ | `autostart` | `boolean` | `true` | Auto-start on connect. Can be a boolean attribute (`autostart`) or a string (`autostart="false"`). |
122
+ | `date` | `YYYY-MM-DD` | — | Target date. Without `date` the timer resolves to zero. |
123
+ | `time` | `HH:MM[:SS]` | `00:00:00` | Target time. |
124
+ | `utc` | `UTC±H[:MM]` or `±H[:MM]` | `UTC+0` | UTC offset used to compute the target moment. Examples: `utc="+03:00"`, `utc="UTC-5"`. |
125
+ | `units` | `string` | `"d:h:m:s"` | Visible groups pattern using `d`, `h`, `m`, `s` separated by `:` (e.g. `"h:m:s"`). Empty/invalid value falls back to showing all. |
126
+
127
+ <br>
128
+
129
+ ➠ **API Methods**
130
+
131
+ | Method | Description |
132
+ |-------------------|--------------------------------------------------------------------------------------------------|
133
+ | `initCountdownTimers({ selector?, onDone?, stylesheet? }): HTMLElement[]` | Finds elements by selector (default: `countdown-timer`), subscribes to the `done` event (when `onDone` is provided), and optionally applies styles to each element (`stylesheet?: CSSStyleSheet \| string \| null`). When a string is provided, it is applied via `adoptedStyleSheets` when supported, otherwise via a `<style>` fallback inside the shadow root. |
134
+ | `start(): void` | Starts/restarts the countdown (only runs when `digits-url` is set). |
135
+ | `stop(): void` | Stops the timer and clears the scheduled tick. |
136
+ | `reset(): void` | Clears the “done fired” flag and either starts again (if `autostart=true`) or renders a static initial value. |
137
+ | `isRunning(): boolean` | Returns `true` if the timer is running and the next tick is scheduled. |
138
+ | `adoptStylesheet(sheet: CSSStyleSheet): void` | Replaces `adoptedStyleSheets` inside the component’s shadow root. |
139
+
140
+ <br>
141
+
142
+ ➠ **Notes**
143
+
144
+ - Updates tick **exactly on second boundaries** (schedules the next tick to the next full second) to keep the display stable.
145
+ - Days render as **two digits** and are capped at **99**.
146
+ - `units` controls which groups (d/h/m/s) are visible. Separators are auto-hidden when `separator-url` is not set, or when a separator is not needed between visible groups.
147
+ - The `done` event fires once per run (after `reset()` it can fire again).
148
+
149
+ <br>
150
+
151
+ ➠ **License**
152
+
153
+ zero-hour is released under MIT license
@@ -0,0 +1,53 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=`:host {
2
+ display: block;
3
+ width: 100%;
4
+ container-type: size;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .zh {
9
+ display: flex;
10
+ align-items: stretch;
11
+ gap: 0;
12
+ position: relative;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .zh__group {
17
+ display: grid;
18
+ grid-template-columns: 1fr 1fr;
19
+ grid-template-rows: auto;
20
+ gap: 0;
21
+ width: 100%;
22
+ flex: 1 1 0;
23
+ min-width: 0;
24
+ }
25
+
26
+ .zh__sep {
27
+ flex: 0 0 var(--zh-sep-width, 3%);
28
+ width: var(--zh-sep-width, 3%);
29
+ background-image: var(--sep-url);
30
+ background-repeat: no-repeat;
31
+ background-position: center;
32
+ background-size: contain;
33
+ }
34
+
35
+ .zh__digit {
36
+ aspect-ratio: 9 / 12;
37
+ width: 100%;
38
+ background-image: var(--digits-url);
39
+ background-repeat: no-repeat;
40
+ background-size: 100% calc(var(--zh-frames, 10) * 100%);
41
+ background-position: 0 calc(var(--sheet-index, 0) * 100% / (var(--zh-frames, 10) - 1));
42
+ }
43
+
44
+ .zh__a11y {
45
+ position: absolute;
46
+ width: 1px;
47
+ height: 1px;
48
+ overflow: hidden;
49
+ clip: rect(0 0 0 0);
50
+ white-space: nowrap;
51
+ clip-path: inset(50%);
52
+ }
53
+ `,S=m;function b(n){if(n==="0")return 9;const t=n.charCodeAt(0)-48;return t>=1&&t<=9?t-1:9}function u(n){return n<0?0:n}const d={showDays:!0,showHours:!0,showMinutes:!0,showSeconds:!0},y={hours:0,minutes:0,seconds:0};function p(n){return"adoptedStyleSheets"in n}function w(n){try{const t=new CSSStyleSheet;return t.replaceSync(n),t}catch{return null}}const M=w(m);function f(n){const t=Math.floor(n/1e3),e=t%60,s=Math.floor(t/60)%60,o=Math.floor(t/3600),i=Math.floor(o/24),r=o%24;return{d:i,h:r,m:s,s:e,totalSec:t}}function E(n){return String(n).padStart(2,"0")}function g(n,t){return[...String(n).padStart(t,"0")]}function T(n,t,e){if(!n.hasAttribute(t))return e;const s=n.getAttribute(t);return s==null||s===""?!0:s!=="false"}function D(n){if(n==null)return null;const t=n.trim();if(!t)return null;const e=t.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(!e)return null;const s=Number(e[1]),o=Number(e[2]),i=Number(e[3]);return!Number.isFinite(s)||!Number.isFinite(o)||!Number.isFinite(i)?null:{year:s,month:o,day:i}}function x(n){if(n==null)return{...y};const t=n.trim();if(!t)return{...y};const e=t.split(":"),s=Number(e[0]??"0"),o=Number(e[1]??"0"),i=Number(e[2]??"0");return{hours:Number.isFinite(s)?s:0,minutes:Number.isFinite(o)?o:0,seconds:Number.isFinite(i)?i:0}}function N(n){if(n==null)return null;let t=n.trim();if(!t)return null;/^utc/i.test(t)&&(t=t.slice(3));let e=1;if(t[0]==="+"?t=t.slice(1):t[0]==="-"&&(e=-1,t=t.slice(1)),!t)return null;const[s,o="0"]=t.split(":"),i=Number(s),r=Number(o);if(!Number.isFinite(i)||!Number.isFinite(r))return null;const h=i*60+r;return e*h}function _(n){const t=n.getAttribute("date"),e=D(t);if(!e)return null;const s=x(n.getAttribute("time")),o=N(n.getAttribute("utc"))??0;return Date.UTC(e.year,e.month-1,e.day,s.hours,s.minutes,s.seconds)-o*60*1e3}function C(n){const t=(n??"").trim().toLowerCase();if(!t)return d;const e=t.split(":").map(a=>a.trim()).filter(Boolean);if(!e.length)return d;const s=new Set(e),o=s.has("d"),i=s.has("h"),r=s.has("m"),h=s.has("s");return!o&&!i&&!r&&!h?d:{showDays:o,showHours:i,showMinutes:r,showSeconds:h}}class c extends HTMLElement{static defaultStylesheet=M;static observedAttributes=["digits-url","separator-url","autostart","date","time","utc","units"];shadow=this.attachShadow({mode:"open"});digitsUrl=null;separatorUrl=null;autostart=!0;durationMs=0;targetEpochMs=null;startEpochMs=null;nextTickTimeout=null;doneFired=!1;rootEl;daysEl;hoursEl;minutesEl;secondsEl;a11yEl;sep0El;sep1El;sep2El;styleEl=null;showDays=!0;showHours=!0;showMinutes=!0;showSeconds=!0;connectedCallback(){this.render(),this.readAttributes(),this.autostart?this.start():this.renderStaticInitial()}disconnectedCallback(){this.stop()}attributeChangedCallback(t,e,s){if(!this.isConnected)return;const o=this.isRunning();this.readAttributes(),this.doneFired=!1,o&&this.autostart?this.start():this.renderStaticInitial()}start(){this.stop(),this.digitsUrl&&(this.durationMs=this.targetEpochMs!=null?this.targetEpochMs-Date.now():0,this.startEpochMs=Date.now(),this.tick(),this.scheduleNextSecondBoundary())}stop(){this.nextTickTimeout!=null&&(window.clearTimeout(this.nextTickTimeout),this.nextTickTimeout=null),this.startEpochMs=null}reset(){this.doneFired=!1,this.autostart?this.start():this.renderStaticInitial()}isRunning(){return this.startEpochMs!=null&&this.nextTickTimeout!=null}readAttributes(){this.digitsUrl=this.getAttribute("digits-url"),this.separatorUrl=this.getAttribute("separator-url"),this.autostart=T(this,"autostart",!0);const t=C(this.getAttribute("units"));this.showDays=t.showDays,this.showHours=t.showHours,this.showMinutes=t.showMinutes,this.showSeconds=t.showSeconds;const e=_(this);if(this.targetEpochMs=e,this.durationMs=0,!this.digitsUrl){this.setTextFallback("—:—:—:—");return}this.rootEl.style.setProperty("--digits-url",`url("${this.digitsUrl}")`),this.separatorUrl?this.rootEl.style.setProperty("--sep-url",`url("${this.separatorUrl}")`):this.rootEl.style.removeProperty("--sep-url"),this.applyUnitsVisibility()}renderStaticInitial(){if(this.targetEpochMs!=null){const t=Date.now(),e=u(this.targetEpochMs-t),{d:s,h:o,m:i,s:r}=f(e);this.setDigits({d:s,h:o,m:i,s:r})}else this.setDigits({d:0,h:0,m:0,s:0})}render(){this.applyStyles(null),this.rootEl=document.createElement("div"),this.rootEl.className="zh",this.daysEl=document.createElement("div"),this.daysEl.className="zh__group",this.hoursEl=document.createElement("div"),this.hoursEl.className="zh__group",this.minutesEl=document.createElement("div"),this.minutesEl.className="zh__group",this.secondsEl=document.createElement("div"),this.secondsEl.className="zh__group",this.sep0El=document.createElement("span"),this.sep0El.className="zh__sep",this.sep1El=document.createElement("span"),this.sep1El.className="zh__sep",this.sep2El=document.createElement("span"),this.sep2El.className="zh__sep",this.a11yEl=document.createElement("span"),this.a11yEl.className="zh__a11y",this.a11yEl.setAttribute("aria-live","polite"),this.rootEl.append(this.daysEl,this.sep0El,this.hoursEl,this.sep1El,this.minutesEl,this.sep2El,this.secondsEl,this.a11yEl),this.shadow.innerHTML="",this.styleEl&&this.shadow.append(this.styleEl),this.shadow.append(this.rootEl),this.setDigits({d:0,h:0,m:0,s:0})}setTextFallback(t){this.a11yEl.textContent=t}applyUnitsVisibility(){if(!this.rootEl)return;if(this.daysEl.style.display=this.showDays?"":"none",this.hoursEl.style.display=this.showHours?"":"none",this.minutesEl.style.display=this.showMinutes?"":"none",this.secondsEl.style.display=this.showSeconds?"":"none",!!!this.separatorUrl){this.sep0El.style.display="none",this.sep1El.style.display="none",this.sep2El.style.display="none";return}const e=[this.showDays,this.showHours,this.showMinutes,this.showSeconds],s=[];for(let i=0;i<e.length;i++)e[i]&&s.push(i);this.rootEl.style.setProperty("--zh-groups",String(s.length));const o=[!1,!1,!1];if(s.length>=2)for(let i=0;i<s.length-1;i++){const r=s[i+1],h=Math.min(2,Math.max(0,r-1));o[h]=!0}this.sep0El.style.display=o[0]?"":"none",this.sep1El.style.display=o[1]?"":"none",this.sep2El.style.display=o[2]?"":"none"}setDigits({d:t,h:e,m:s,s:o}){const i=g(Math.min(t,99),2),r=g(e,2),h=[...E(s)],a=[...E(o)];this.syncDigitGroup(this.daysEl,i),this.syncDigitGroup(this.hoursEl,r),this.syncDigitGroup(this.minutesEl,h),this.syncDigitGroup(this.secondsEl,a);const l=[];this.showDays&&l.push(i.join("")),this.showHours&&l.push(r.join("")),this.showMinutes&&l.push(h.join("")),this.showSeconds&&l.push(a.join("")),this.a11yEl.textContent=l.length?l.join(":"):"—"}syncDigitGroup(t,e){for(;t.children.length<e.length;){const s=document.createElement("span");s.className="zh__digit",t.appendChild(s)}for(;t.children.length>e.length;){const s=t.lastElementChild;if(!s)break;t.removeChild(s)}for(let s=0;s<e.length;s++){const o=t.children[s],i=b(e[s]);o.style.setProperty("--sheet-index",String(i))}}tick(){if(!this.digitsUrl)return;const t=u(this.durationMs);if(t===0){this.setDigits({d:0,h:0,m:0,s:0}),this.fireDoneOnce(),this.stop();return}const e=this.startEpochMs??Date.now(),s=u(Date.now()-e),o=u(t-s),{d:i,h:r,m:h,s:a,totalSec:l}=f(o);this.setDigits({d:i,h:r,m:h,s:a}),l===0&&(this.fireDoneOnce(),this.stop())}fireDoneOnce(){this.doneFired||(this.doneFired=!0,this.dispatchEvent(new CustomEvent("done")))}scheduleNextSecondBoundary(){const e=1e3-Date.now()%1e3;this.nextTickTimeout=window.setTimeout(()=>{this.tick(),this.isRunning()&&this.scheduleNextSecondBoundary()},e)}adoptStylesheet(t){this.applyStyles(t)}adoptStyles(t){this.applyStyles(t)}applyStyles(t){if(typeof t=="string"){if(p(this.shadow)){const e=w(t);if(e){this.shadow.adoptedStyleSheets=[e],this.styleEl=null;return}}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=t;return}if(t&&p(this.shadow)){this.shadow.adoptedStyleSheets=[t],this.styleEl=null;return}if(c.defaultStylesheet&&p(this.shadow)){this.shadow.adoptedStyleSheets=[c.defaultStylesheet],this.styleEl=null;return}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=m}}customElements.get("countdown-timer")||customElements.define("countdown-timer",c);function z(n={}){const{selector:t="countdown-timer",onDone:e,stylesheet:s}=n,o=Array.from(document.querySelectorAll(t));return e&&o.forEach(i=>{i.addEventListener("done",()=>e(i))}),s&&o.forEach(i=>{const r=i;typeof s=="string"?r.adoptStyles(s):r.adoptStylesheet(s)}),o}exports.initCountdownTimers=z;exports.zeroHourCssText=S;
@@ -0,0 +1,60 @@
1
+ export declare const zeroHourCssText: string;
2
+ type StylesInput = CSSStyleSheet | string | null | undefined;
3
+ declare class ZeroHour extends HTMLElement {
4
+ static defaultStylesheet: CSSStyleSheet | null;
5
+ static observedAttributes: string[];
6
+ private readonly shadow;
7
+ private digitsUrl;
8
+ private separatorUrl;
9
+ private autostart;
10
+ private durationMs;
11
+ private targetEpochMs;
12
+ private startEpochMs;
13
+ private nextTickTimeout;
14
+ private doneFired;
15
+ private rootEl;
16
+ private daysEl;
17
+ private hoursEl;
18
+ private minutesEl;
19
+ private secondsEl;
20
+ private a11yEl;
21
+ private sep0El;
22
+ private sep1El;
23
+ private sep2El;
24
+ private styleEl;
25
+ private showDays;
26
+ private showHours;
27
+ private showMinutes;
28
+ private showSeconds;
29
+ connectedCallback(): void;
30
+ disconnectedCallback(): void;
31
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
32
+ start(): void;
33
+ stop(): void;
34
+ reset(): void;
35
+ isRunning(): boolean;
36
+ private readAttributes;
37
+ private renderStaticInitial;
38
+ private render;
39
+ private setTextFallback;
40
+ private applyUnitsVisibility;
41
+ private setDigits;
42
+ private syncDigitGroup;
43
+ private tick;
44
+ private fireDoneOnce;
45
+ private scheduleNextSecondBoundary;
46
+ adoptStylesheet(sheet: CSSStyleSheet): void;
47
+ adoptStyles(styles: StylesInput): void;
48
+ private applyStyles;
49
+ }
50
+ declare global {
51
+ interface HTMLElementTagNameMap {
52
+ 'countdown-timer': ZeroHour;
53
+ }
54
+ }
55
+ export declare function initCountdownTimers(options?: {
56
+ selector?: string;
57
+ onDone?: (el: HTMLElement) => void;
58
+ stylesheet?: CSSStyleSheet | string | null;
59
+ }): HTMLElement[];
60
+ export {};
@@ -0,0 +1,341 @@
1
+ const m = `:host {
2
+ display: block;
3
+ width: 100%;
4
+ container-type: size;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .zh {
9
+ display: flex;
10
+ align-items: stretch;
11
+ gap: 0;
12
+ position: relative;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .zh__group {
17
+ display: grid;
18
+ grid-template-columns: 1fr 1fr;
19
+ grid-template-rows: auto;
20
+ gap: 0;
21
+ width: 100%;
22
+ flex: 1 1 0;
23
+ min-width: 0;
24
+ }
25
+
26
+ .zh__sep {
27
+ flex: 0 0 var(--zh-sep-width, 3%);
28
+ width: var(--zh-sep-width, 3%);
29
+ background-image: var(--sep-url);
30
+ background-repeat: no-repeat;
31
+ background-position: center;
32
+ background-size: contain;
33
+ }
34
+
35
+ .zh__digit {
36
+ aspect-ratio: 9 / 12;
37
+ width: 100%;
38
+ background-image: var(--digits-url);
39
+ background-repeat: no-repeat;
40
+ background-size: 100% calc(var(--zh-frames, 10) * 100%);
41
+ background-position: 0 calc(var(--sheet-index, 0) * 100% / (var(--zh-frames, 10) - 1));
42
+ }
43
+
44
+ .zh__a11y {
45
+ position: absolute;
46
+ width: 1px;
47
+ height: 1px;
48
+ overflow: hidden;
49
+ clip: rect(0 0 0 0);
50
+ white-space: nowrap;
51
+ clip-path: inset(50%);
52
+ }
53
+ `, C = m;
54
+ function S(n) {
55
+ if (n === "0") return 9;
56
+ const t = n.charCodeAt(0) - 48;
57
+ return t >= 1 && t <= 9 ? t - 1 : 9;
58
+ }
59
+ function u(n) {
60
+ return n < 0 ? 0 : n;
61
+ }
62
+ const d = {
63
+ showDays: !0,
64
+ showHours: !0,
65
+ showMinutes: !0,
66
+ showSeconds: !0
67
+ }, f = { hours: 0, minutes: 0, seconds: 0 };
68
+ function p(n) {
69
+ return "adoptedStyleSheets" in n;
70
+ }
71
+ function w(n) {
72
+ try {
73
+ const t = new CSSStyleSheet();
74
+ return t.replaceSync(n), t;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ const b = w(m);
80
+ function y(n) {
81
+ const t = Math.floor(n / 1e3), e = t % 60, s = Math.floor(t / 60) % 60, o = Math.floor(t / 3600), i = Math.floor(o / 24), r = o % 24;
82
+ return { d: i, h: r, m: s, s: e, totalSec: t };
83
+ }
84
+ function E(n) {
85
+ return String(n).padStart(2, "0");
86
+ }
87
+ function g(n, t) {
88
+ return [...String(n).padStart(t, "0")];
89
+ }
90
+ function M(n, t, e) {
91
+ if (!n.hasAttribute(t)) return e;
92
+ const s = n.getAttribute(t);
93
+ return s == null || s === "" ? !0 : s !== "false";
94
+ }
95
+ function D(n) {
96
+ if (n == null) return null;
97
+ const t = n.trim();
98
+ if (!t) return null;
99
+ const e = t.match(/^(\d{4})-(\d{2})-(\d{2})$/);
100
+ if (!e) return null;
101
+ const s = Number(e[1]), o = Number(e[2]), i = Number(e[3]);
102
+ return !Number.isFinite(s) || !Number.isFinite(o) || !Number.isFinite(i) ? null : { year: s, month: o, day: i };
103
+ }
104
+ function T(n) {
105
+ if (n == null) return { ...f };
106
+ const t = n.trim();
107
+ if (!t) return { ...f };
108
+ const e = t.split(":"), s = Number(e[0] ?? "0"), o = Number(e[1] ?? "0"), i = Number(e[2] ?? "0");
109
+ return {
110
+ hours: Number.isFinite(s) ? s : 0,
111
+ minutes: Number.isFinite(o) ? o : 0,
112
+ seconds: Number.isFinite(i) ? i : 0
113
+ };
114
+ }
115
+ function x(n) {
116
+ if (n == null) return null;
117
+ let t = n.trim();
118
+ if (!t) return null;
119
+ /^utc/i.test(t) && (t = t.slice(3));
120
+ let e = 1;
121
+ if (t[0] === "+" ? t = t.slice(1) : t[0] === "-" && (e = -1, t = t.slice(1)), !t) return null;
122
+ const [s, o = "0"] = t.split(":"), i = Number(s), r = Number(o);
123
+ if (!Number.isFinite(i) || !Number.isFinite(r)) return null;
124
+ const h = i * 60 + r;
125
+ return e * h;
126
+ }
127
+ function N(n) {
128
+ const t = n.getAttribute("date"), e = D(t);
129
+ if (!e) return null;
130
+ const s = T(n.getAttribute("time")), o = x(n.getAttribute("utc")) ?? 0;
131
+ return Date.UTC(
132
+ e.year,
133
+ e.month - 1,
134
+ e.day,
135
+ s.hours,
136
+ s.minutes,
137
+ s.seconds
138
+ ) - o * 60 * 1e3;
139
+ }
140
+ function _(n) {
141
+ const t = (n ?? "").trim().toLowerCase();
142
+ if (!t)
143
+ return d;
144
+ const e = t.split(":").map((a) => a.trim()).filter(Boolean);
145
+ if (!e.length)
146
+ return d;
147
+ const s = new Set(e), o = s.has("d"), i = s.has("h"), r = s.has("m"), h = s.has("s");
148
+ return !o && !i && !r && !h ? d : { showDays: o, showHours: i, showMinutes: r, showSeconds: h };
149
+ }
150
+ class c extends HTMLElement {
151
+ static defaultStylesheet = b;
152
+ static observedAttributes = [
153
+ "digits-url",
154
+ "separator-url",
155
+ "autostart",
156
+ "date",
157
+ "time",
158
+ "utc",
159
+ "units"
160
+ ];
161
+ shadow = this.attachShadow({ mode: "open" });
162
+ digitsUrl = null;
163
+ separatorUrl = null;
164
+ autostart = !0;
165
+ durationMs = 0;
166
+ targetEpochMs = null;
167
+ startEpochMs = null;
168
+ nextTickTimeout = null;
169
+ doneFired = !1;
170
+ rootEl;
171
+ daysEl;
172
+ hoursEl;
173
+ minutesEl;
174
+ secondsEl;
175
+ a11yEl;
176
+ sep0El;
177
+ sep1El;
178
+ sep2El;
179
+ styleEl = null;
180
+ showDays = !0;
181
+ showHours = !0;
182
+ showMinutes = !0;
183
+ showSeconds = !0;
184
+ connectedCallback() {
185
+ this.render(), this.readAttributes(), this.autostart ? this.start() : this.renderStaticInitial();
186
+ }
187
+ disconnectedCallback() {
188
+ this.stop();
189
+ }
190
+ attributeChangedCallback(t, e, s) {
191
+ if (!this.isConnected) return;
192
+ const o = this.isRunning();
193
+ this.readAttributes(), this.doneFired = !1, o && this.autostart ? this.start() : this.renderStaticInitial();
194
+ }
195
+ start() {
196
+ this.stop(), this.digitsUrl && (this.durationMs = this.targetEpochMs != null ? this.targetEpochMs - Date.now() : 0, this.startEpochMs = Date.now(), this.tick(), this.scheduleNextSecondBoundary());
197
+ }
198
+ stop() {
199
+ this.nextTickTimeout != null && (window.clearTimeout(this.nextTickTimeout), this.nextTickTimeout = null), this.startEpochMs = null;
200
+ }
201
+ reset() {
202
+ this.doneFired = !1, this.autostart ? this.start() : this.renderStaticInitial();
203
+ }
204
+ isRunning() {
205
+ return this.startEpochMs != null && this.nextTickTimeout != null;
206
+ }
207
+ readAttributes() {
208
+ this.digitsUrl = this.getAttribute("digits-url"), this.separatorUrl = this.getAttribute("separator-url"), this.autostart = M(this, "autostart", !0);
209
+ const t = _(this.getAttribute("units"));
210
+ this.showDays = t.showDays, this.showHours = t.showHours, this.showMinutes = t.showMinutes, this.showSeconds = t.showSeconds;
211
+ const e = N(this);
212
+ if (this.targetEpochMs = e, this.durationMs = 0, !this.digitsUrl) {
213
+ this.setTextFallback("—:—:—:—");
214
+ return;
215
+ }
216
+ this.rootEl.style.setProperty("--digits-url", `url("${this.digitsUrl}")`), this.separatorUrl ? this.rootEl.style.setProperty("--sep-url", `url("${this.separatorUrl}")`) : this.rootEl.style.removeProperty("--sep-url"), this.applyUnitsVisibility();
217
+ }
218
+ renderStaticInitial() {
219
+ if (this.targetEpochMs != null) {
220
+ const t = Date.now(), e = u(this.targetEpochMs - t), { d: s, h: o, m: i, s: r } = y(e);
221
+ this.setDigits({ d: s, h: o, m: i, s: r });
222
+ } else
223
+ this.setDigits({ d: 0, h: 0, m: 0, s: 0 });
224
+ }
225
+ render() {
226
+ this.applyStyles(null), this.rootEl = document.createElement("div"), this.rootEl.className = "zh", this.daysEl = document.createElement("div"), this.daysEl.className = "zh__group", this.hoursEl = document.createElement("div"), this.hoursEl.className = "zh__group", this.minutesEl = document.createElement("div"), this.minutesEl.className = "zh__group", this.secondsEl = document.createElement("div"), this.secondsEl.className = "zh__group", this.sep0El = document.createElement("span"), this.sep0El.className = "zh__sep", this.sep1El = document.createElement("span"), this.sep1El.className = "zh__sep", this.sep2El = document.createElement("span"), this.sep2El.className = "zh__sep", this.a11yEl = document.createElement("span"), this.a11yEl.className = "zh__a11y", this.a11yEl.setAttribute("aria-live", "polite"), this.rootEl.append(
227
+ this.daysEl,
228
+ this.sep0El,
229
+ this.hoursEl,
230
+ this.sep1El,
231
+ this.minutesEl,
232
+ this.sep2El,
233
+ this.secondsEl,
234
+ this.a11yEl
235
+ ), this.shadow.innerHTML = "", this.styleEl && this.shadow.append(this.styleEl), this.shadow.append(this.rootEl), this.setDigits({ d: 0, h: 0, m: 0, s: 0 });
236
+ }
237
+ setTextFallback(t) {
238
+ this.a11yEl.textContent = t;
239
+ }
240
+ applyUnitsVisibility() {
241
+ if (!this.rootEl) return;
242
+ if (this.daysEl.style.display = this.showDays ? "" : "none", this.hoursEl.style.display = this.showHours ? "" : "none", this.minutesEl.style.display = this.showMinutes ? "" : "none", this.secondsEl.style.display = this.showSeconds ? "" : "none", !!!this.separatorUrl) {
243
+ this.sep0El.style.display = "none", this.sep1El.style.display = "none", this.sep2El.style.display = "none";
244
+ return;
245
+ }
246
+ const e = [this.showDays, this.showHours, this.showMinutes, this.showSeconds], s = [];
247
+ for (let i = 0; i < e.length; i++)
248
+ e[i] && s.push(i);
249
+ this.rootEl.style.setProperty("--zh-groups", String(s.length));
250
+ const o = [!1, !1, !1];
251
+ if (s.length >= 2)
252
+ for (let i = 0; i < s.length - 1; i++) {
253
+ const r = s[i + 1], h = Math.min(2, Math.max(0, r - 1));
254
+ o[h] = !0;
255
+ }
256
+ this.sep0El.style.display = o[0] ? "" : "none", this.sep1El.style.display = o[1] ? "" : "none", this.sep2El.style.display = o[2] ? "" : "none";
257
+ }
258
+ setDigits({ d: t, h: e, m: s, s: o }) {
259
+ const i = g(Math.min(t, 99), 2), r = g(e, 2), h = [...E(s)], a = [...E(o)];
260
+ this.syncDigitGroup(this.daysEl, i), this.syncDigitGroup(this.hoursEl, r), this.syncDigitGroup(this.minutesEl, h), this.syncDigitGroup(this.secondsEl, a);
261
+ const l = [];
262
+ this.showDays && l.push(i.join("")), this.showHours && l.push(r.join("")), this.showMinutes && l.push(h.join("")), this.showSeconds && l.push(a.join("")), this.a11yEl.textContent = l.length ? l.join(":") : "—";
263
+ }
264
+ syncDigitGroup(t, e) {
265
+ for (; t.children.length < e.length; ) {
266
+ const s = document.createElement("span");
267
+ s.className = "zh__digit", t.appendChild(s);
268
+ }
269
+ for (; t.children.length > e.length; ) {
270
+ const s = t.lastElementChild;
271
+ if (!s) break;
272
+ t.removeChild(s);
273
+ }
274
+ for (let s = 0; s < e.length; s++) {
275
+ const o = t.children[s], i = S(e[s]);
276
+ o.style.setProperty("--sheet-index", String(i));
277
+ }
278
+ }
279
+ tick() {
280
+ if (!this.digitsUrl) return;
281
+ const t = u(this.durationMs);
282
+ if (t === 0) {
283
+ this.setDigits({ d: 0, h: 0, m: 0, s: 0 }), this.fireDoneOnce(), this.stop();
284
+ return;
285
+ }
286
+ const e = this.startEpochMs ?? Date.now(), s = u(Date.now() - e), o = u(t - s), { d: i, h: r, m: h, s: a, totalSec: l } = y(o);
287
+ this.setDigits({ d: i, h: r, m: h, s: a }), l === 0 && (this.fireDoneOnce(), this.stop());
288
+ }
289
+ fireDoneOnce() {
290
+ this.doneFired || (this.doneFired = !0, this.dispatchEvent(new CustomEvent("done")));
291
+ }
292
+ scheduleNextSecondBoundary() {
293
+ const e = 1e3 - Date.now() % 1e3;
294
+ this.nextTickTimeout = window.setTimeout(() => {
295
+ this.tick(), this.isRunning() && this.scheduleNextSecondBoundary();
296
+ }, e);
297
+ }
298
+ adoptStylesheet(t) {
299
+ this.applyStyles(t);
300
+ }
301
+ // Accept CSSStyleSheet OR raw CSS text (e.g. imported with ?raw from SCSS/CSS pipelines)
302
+ adoptStyles(t) {
303
+ this.applyStyles(t);
304
+ }
305
+ applyStyles(t) {
306
+ if (typeof t == "string") {
307
+ if (p(this.shadow)) {
308
+ const e = w(t);
309
+ if (e) {
310
+ this.shadow.adoptedStyleSheets = [e], this.styleEl = null;
311
+ return;
312
+ }
313
+ }
314
+ this.styleEl || (this.styleEl = document.createElement("style")), this.styleEl.textContent = t;
315
+ return;
316
+ }
317
+ if (t && p(this.shadow)) {
318
+ this.shadow.adoptedStyleSheets = [t], this.styleEl = null;
319
+ return;
320
+ }
321
+ if (c.defaultStylesheet && p(this.shadow)) {
322
+ this.shadow.adoptedStyleSheets = [c.defaultStylesheet], this.styleEl = null;
323
+ return;
324
+ }
325
+ this.styleEl || (this.styleEl = document.createElement("style")), this.styleEl.textContent = m;
326
+ }
327
+ }
328
+ customElements.get("countdown-timer") || customElements.define("countdown-timer", c);
329
+ function k(n = {}) {
330
+ const { selector: t = "countdown-timer", onDone: e, stylesheet: s } = n, o = Array.from(document.querySelectorAll(t));
331
+ return e && o.forEach((i) => {
332
+ i.addEventListener("done", () => e(i));
333
+ }), s && o.forEach((i) => {
334
+ const r = i;
335
+ typeof s == "string" ? r.adoptStyles(s) : r.adoptStylesheet(s);
336
+ }), o;
337
+ }
338
+ export {
339
+ k as initCountdownTimers,
340
+ C as zeroHourCssText
341
+ };
@@ -0,0 +1,53 @@
1
+ (function(a,u){typeof exports=="object"&&typeof module<"u"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(a=typeof globalThis<"u"?globalThis:a||self,u(a.ZeroHour={}))})(this,(function(a){"use strict";const u=`:host {
2
+ display: block;
3
+ width: 100%;
4
+ container-type: size;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .zh {
9
+ display: flex;
10
+ align-items: stretch;
11
+ gap: 0;
12
+ position: relative;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .zh__group {
17
+ display: grid;
18
+ grid-template-columns: 1fr 1fr;
19
+ grid-template-rows: auto;
20
+ gap: 0;
21
+ width: 100%;
22
+ flex: 1 1 0;
23
+ min-width: 0;
24
+ }
25
+
26
+ .zh__sep {
27
+ flex: 0 0 var(--zh-sep-width, 3%);
28
+ width: var(--zh-sep-width, 3%);
29
+ background-image: var(--sep-url);
30
+ background-repeat: no-repeat;
31
+ background-position: center;
32
+ background-size: contain;
33
+ }
34
+
35
+ .zh__digit {
36
+ aspect-ratio: 9 / 12;
37
+ width: 100%;
38
+ background-image: var(--digits-url);
39
+ background-repeat: no-repeat;
40
+ background-size: 100% calc(var(--zh-frames, 10) * 100%);
41
+ background-position: 0 calc(var(--sheet-index, 0) * 100% / (var(--zh-frames, 10) - 1));
42
+ }
43
+
44
+ .zh__a11y {
45
+ position: absolute;
46
+ width: 1px;
47
+ height: 1px;
48
+ overflow: hidden;
49
+ clip: rect(0 0 0 0);
50
+ white-space: nowrap;
51
+ clip-path: inset(50%);
52
+ }
53
+ `,b=u;function M(n){if(n==="0")return 9;const t=n.charCodeAt(0)-48;return t>=1&&t<=9?t-1:9}function d(n){return n<0?0:n}const m={showDays:!0,showHours:!0,showMinutes:!0,showSeconds:!0},y={hours:0,minutes:0,seconds:0};function f(n){return"adoptedStyleSheets"in n}function E(n){try{const t=new CSSStyleSheet;return t.replaceSync(n),t}catch{return null}}const T=E(u);function g(n){const t=Math.floor(n/1e3),e=t%60,s=Math.floor(t/60)%60,o=Math.floor(t/3600),i=Math.floor(o/24),r=o%24;return{d:i,h:r,m:s,s:e,totalSec:t}}function w(n){return String(n).padStart(2,"0")}function S(n,t){return[...String(n).padStart(t,"0")]}function D(n,t,e){if(!n.hasAttribute(t))return e;const s=n.getAttribute(t);return s==null||s===""?!0:s!=="false"}function x(n){if(n==null)return null;const t=n.trim();if(!t)return null;const e=t.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(!e)return null;const s=Number(e[1]),o=Number(e[2]),i=Number(e[3]);return!Number.isFinite(s)||!Number.isFinite(o)||!Number.isFinite(i)?null:{year:s,month:o,day:i}}function N(n){if(n==null)return{...y};const t=n.trim();if(!t)return{...y};const e=t.split(":"),s=Number(e[0]??"0"),o=Number(e[1]??"0"),i=Number(e[2]??"0");return{hours:Number.isFinite(s)?s:0,minutes:Number.isFinite(o)?o:0,seconds:Number.isFinite(i)?i:0}}function _(n){if(n==null)return null;let t=n.trim();if(!t)return null;/^utc/i.test(t)&&(t=t.slice(3));let e=1;if(t[0]==="+"?t=t.slice(1):t[0]==="-"&&(e=-1,t=t.slice(1)),!t)return null;const[s,o="0"]=t.split(":"),i=Number(s),r=Number(o);if(!Number.isFinite(i)||!Number.isFinite(r))return null;const h=i*60+r;return e*h}function C(n){const t=n.getAttribute("date"),e=x(t);if(!e)return null;const s=N(n.getAttribute("time")),o=_(n.getAttribute("utc"))??0;return Date.UTC(e.year,e.month-1,e.day,s.hours,s.minutes,s.seconds)-o*60*1e3}function z(n){const t=(n??"").trim().toLowerCase();if(!t)return m;const e=t.split(":").map(c=>c.trim()).filter(Boolean);if(!e.length)return m;const s=new Set(e),o=s.has("d"),i=s.has("h"),r=s.has("m"),h=s.has("s");return!o&&!i&&!r&&!h?m:{showDays:o,showHours:i,showMinutes:r,showSeconds:h}}class p extends HTMLElement{static defaultStylesheet=T;static observedAttributes=["digits-url","separator-url","autostart","date","time","utc","units"];shadow=this.attachShadow({mode:"open"});digitsUrl=null;separatorUrl=null;autostart=!0;durationMs=0;targetEpochMs=null;startEpochMs=null;nextTickTimeout=null;doneFired=!1;rootEl;daysEl;hoursEl;minutesEl;secondsEl;a11yEl;sep0El;sep1El;sep2El;styleEl=null;showDays=!0;showHours=!0;showMinutes=!0;showSeconds=!0;connectedCallback(){this.render(),this.readAttributes(),this.autostart?this.start():this.renderStaticInitial()}disconnectedCallback(){this.stop()}attributeChangedCallback(t,e,s){if(!this.isConnected)return;const o=this.isRunning();this.readAttributes(),this.doneFired=!1,o&&this.autostart?this.start():this.renderStaticInitial()}start(){this.stop(),this.digitsUrl&&(this.durationMs=this.targetEpochMs!=null?this.targetEpochMs-Date.now():0,this.startEpochMs=Date.now(),this.tick(),this.scheduleNextSecondBoundary())}stop(){this.nextTickTimeout!=null&&(window.clearTimeout(this.nextTickTimeout),this.nextTickTimeout=null),this.startEpochMs=null}reset(){this.doneFired=!1,this.autostart?this.start():this.renderStaticInitial()}isRunning(){return this.startEpochMs!=null&&this.nextTickTimeout!=null}readAttributes(){this.digitsUrl=this.getAttribute("digits-url"),this.separatorUrl=this.getAttribute("separator-url"),this.autostart=D(this,"autostart",!0);const t=z(this.getAttribute("units"));this.showDays=t.showDays,this.showHours=t.showHours,this.showMinutes=t.showMinutes,this.showSeconds=t.showSeconds;const e=C(this);if(this.targetEpochMs=e,this.durationMs=0,!this.digitsUrl){this.setTextFallback("—:—:—:—");return}this.rootEl.style.setProperty("--digits-url",`url("${this.digitsUrl}")`),this.separatorUrl?this.rootEl.style.setProperty("--sep-url",`url("${this.separatorUrl}")`):this.rootEl.style.removeProperty("--sep-url"),this.applyUnitsVisibility()}renderStaticInitial(){if(this.targetEpochMs!=null){const t=Date.now(),e=d(this.targetEpochMs-t),{d:s,h:o,m:i,s:r}=g(e);this.setDigits({d:s,h:o,m:i,s:r})}else this.setDigits({d:0,h:0,m:0,s:0})}render(){this.applyStyles(null),this.rootEl=document.createElement("div"),this.rootEl.className="zh",this.daysEl=document.createElement("div"),this.daysEl.className="zh__group",this.hoursEl=document.createElement("div"),this.hoursEl.className="zh__group",this.minutesEl=document.createElement("div"),this.minutesEl.className="zh__group",this.secondsEl=document.createElement("div"),this.secondsEl.className="zh__group",this.sep0El=document.createElement("span"),this.sep0El.className="zh__sep",this.sep1El=document.createElement("span"),this.sep1El.className="zh__sep",this.sep2El=document.createElement("span"),this.sep2El.className="zh__sep",this.a11yEl=document.createElement("span"),this.a11yEl.className="zh__a11y",this.a11yEl.setAttribute("aria-live","polite"),this.rootEl.append(this.daysEl,this.sep0El,this.hoursEl,this.sep1El,this.minutesEl,this.sep2El,this.secondsEl,this.a11yEl),this.shadow.innerHTML="",this.styleEl&&this.shadow.append(this.styleEl),this.shadow.append(this.rootEl),this.setDigits({d:0,h:0,m:0,s:0})}setTextFallback(t){this.a11yEl.textContent=t}applyUnitsVisibility(){if(!this.rootEl)return;if(this.daysEl.style.display=this.showDays?"":"none",this.hoursEl.style.display=this.showHours?"":"none",this.minutesEl.style.display=this.showMinutes?"":"none",this.secondsEl.style.display=this.showSeconds?"":"none",!!!this.separatorUrl){this.sep0El.style.display="none",this.sep1El.style.display="none",this.sep2El.style.display="none";return}const e=[this.showDays,this.showHours,this.showMinutes,this.showSeconds],s=[];for(let i=0;i<e.length;i++)e[i]&&s.push(i);this.rootEl.style.setProperty("--zh-groups",String(s.length));const o=[!1,!1,!1];if(s.length>=2)for(let i=0;i<s.length-1;i++){const r=s[i+1],h=Math.min(2,Math.max(0,r-1));o[h]=!0}this.sep0El.style.display=o[0]?"":"none",this.sep1El.style.display=o[1]?"":"none",this.sep2El.style.display=o[2]?"":"none"}setDigits({d:t,h:e,m:s,s:o}){const i=S(Math.min(t,99),2),r=S(e,2),h=[...w(s)],c=[...w(o)];this.syncDigitGroup(this.daysEl,i),this.syncDigitGroup(this.hoursEl,r),this.syncDigitGroup(this.minutesEl,h),this.syncDigitGroup(this.secondsEl,c);const l=[];this.showDays&&l.push(i.join("")),this.showHours&&l.push(r.join("")),this.showMinutes&&l.push(h.join("")),this.showSeconds&&l.push(c.join("")),this.a11yEl.textContent=l.length?l.join(":"):"—"}syncDigitGroup(t,e){for(;t.children.length<e.length;){const s=document.createElement("span");s.className="zh__digit",t.appendChild(s)}for(;t.children.length>e.length;){const s=t.lastElementChild;if(!s)break;t.removeChild(s)}for(let s=0;s<e.length;s++){const o=t.children[s],i=M(e[s]);o.style.setProperty("--sheet-index",String(i))}}tick(){if(!this.digitsUrl)return;const t=d(this.durationMs);if(t===0){this.setDigits({d:0,h:0,m:0,s:0}),this.fireDoneOnce(),this.stop();return}const e=this.startEpochMs??Date.now(),s=d(Date.now()-e),o=d(t-s),{d:i,h:r,m:h,s:c,totalSec:l}=g(o);this.setDigits({d:i,h:r,m:h,s:c}),l===0&&(this.fireDoneOnce(),this.stop())}fireDoneOnce(){this.doneFired||(this.doneFired=!0,this.dispatchEvent(new CustomEvent("done")))}scheduleNextSecondBoundary(){const e=1e3-Date.now()%1e3;this.nextTickTimeout=window.setTimeout(()=>{this.tick(),this.isRunning()&&this.scheduleNextSecondBoundary()},e)}adoptStylesheet(t){this.applyStyles(t)}adoptStyles(t){this.applyStyles(t)}applyStyles(t){if(typeof t=="string"){if(f(this.shadow)){const e=E(t);if(e){this.shadow.adoptedStyleSheets=[e],this.styleEl=null;return}}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=t;return}if(t&&f(this.shadow)){this.shadow.adoptedStyleSheets=[t],this.styleEl=null;return}if(p.defaultStylesheet&&f(this.shadow)){this.shadow.adoptedStyleSheets=[p.defaultStylesheet],this.styleEl=null;return}this.styleEl||(this.styleEl=document.createElement("style")),this.styleEl.textContent=u}}customElements.get("countdown-timer")||customElements.define("countdown-timer",p);function k(n={}){const{selector:t="countdown-timer",onDone:e,stylesheet:s}=n,o=Array.from(document.querySelectorAll(t));return e&&o.forEach(i=>{i.addEventListener("done",()=>e(i))}),s&&o.forEach(i=>{const r=i;typeof s=="string"?r.adoptStyles(s):r.adoptStylesheet(s)}),o}a.initCountdownTimers=k,a.zeroHourCssText=b,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1 @@
1
+ :host{width:100%;display:block;overflow:hidden;container-type:size}.zh{box-sizing:border-box;align-items:stretch;gap:0;display:flex;position:relative}.zh__group{flex:1 1 0;grid-template-rows:auto;grid-template-columns:1fr 1fr;gap:0;width:100%;min-width:0;display:grid}.zh__sep{flex:0 0 var(--zh-sep-width,3%);width:var(--zh-sep-width,3%);background-image:var(--sep-url);background-position:50%;background-repeat:no-repeat;background-size:contain}.zh__digit{aspect-ratio:9/12;background-image:var(--digits-url);background-repeat:no-repeat;background-size:100% calc(var(--zh-frames,10)*100%);background-position:0 calc(var(--sheet-index,0)*100%/(var(--zh-frames,10) - 1));width:100%}.zh__a11y{clip:rect(0 0 0 0);white-space:nowrap;clip-path:inset(50%);width:1px;height:1px;position:absolute;overflow:hidden}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "zero-hour",
3
+ "version": "1.0.0-dev.0",
4
+ "description": "Tiny countdown Web Component. Registers <countdown-timer> that renders a configurable DD:HH:MM:SS countdown to a target date/time (optional UTC offset) and fires a done event at zero.",
5
+ "author": "ux-ui.pro",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ux-ui-pro/zero-hour.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ux-ui-pro/zero-hour/issues"
13
+ },
14
+ "homepage": "https://github.com/ux-ui-pro/zero-hour",
15
+ "sideEffects": false,
16
+ "scripts": {
17
+ "clean": "rimraf dist",
18
+ "build": "vite build",
19
+ "verify": "yarn lint && yarn typecheck",
20
+ "lint": "biome check src",
21
+ "lint:fix": "biome check --write src",
22
+ "format": "biome format --write src",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit"
24
+ },
25
+ "source": "src/index.ts",
26
+ "main": "dist/index.cjs.js",
27
+ "module": "dist/index.es.js",
28
+ "types": "dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.es.js",
33
+ "require": "./dist/index.cjs.js"
34
+ },
35
+ "./zero-hour.css": "./dist/zero-hour.css",
36
+ "./dist/*": "./dist/*"
37
+ },
38
+ "files": [
39
+ "dist/"
40
+ ],
41
+ "devDependencies": {
42
+ "@biomejs/biome": "2.3.9",
43
+ "@types/node": "25.0.2",
44
+ "lightningcss": "1.30.2",
45
+ "rimraf": "6.1.2",
46
+ "typescript": "5.9.3",
47
+ "vite": "7.3.0",
48
+ "vite-plugin-dts": "4.5.4"
49
+ },
50
+ "keywords": [
51
+ "typescript",
52
+ "frontend",
53
+ "library",
54
+ "countdown",
55
+ "countdown timer",
56
+ "timer",
57
+ "web component",
58
+ "web-component",
59
+ "custom elements",
60
+ "custom-elements",
61
+ "shadow dom",
62
+ "shadow-dom",
63
+ "dom",
64
+ "ui",
65
+ "component",
66
+ "digital",
67
+ "sprite",
68
+ "date",
69
+ "time",
70
+ "utc",
71
+ "accessibility",
72
+ "a11y",
73
+ "zero-hour"
74
+ ]
75
+ }