@acecss/core 0.0.0-stage → 1.0.4

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) 2026 Kosowwwa LLC
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 CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # AceCSS
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Buttons, inputs, dropdowns and a date range picker in one CSS file.** No framework, no build step, ~12 KB gzipped. Dark mode built in. MIT licensed.
4
+
5
+ **Guide and live demo: https://acecss.dev**
6
+
7
+ ![AceCSS components](https://acecss.dev/og.png)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @acecss/core
13
+ ```
14
+
15
+ ```js
16
+ // Vite, Webpack, Next.js, Nuxt… (load it after any CSS reset / Tailwind)
17
+ import '@acecss/core' // the stylesheet (dist/ace.css)
18
+
19
+ // Optional, browser-side behaviour:
20
+ import '@acecss/core/datepicker' // date picker + pick-up → return range
21
+ import '@acecss/core/select' // dropdown for <select data-ace-select>
22
+ ```
23
+
24
+ Nuxt: `css: ['@acecss/core']` in `nuxt.config.ts`.
25
+ Plain CSS: `@import '@acecss/core/ace.css';`
26
+
27
+ ### Or straight from a CDN
28
+
29
+ ```html
30
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@acecss/core@1/dist/ace.min.css">
31
+ <script src="https://cdn.jsdelivr.net/npm/@acecss/core@1/dist/ace-datepicker.min.js" defer></script>
32
+ <script src="https://cdn.jsdelivr.net/npm/@acecss/core@1/dist/ace-select.min.js" defer></script>
33
+ ```
34
+
35
+ Pinned links with SRI hashes: https://acecss.dev/#install
36
+
37
+ ## Use it
38
+
39
+ ```html
40
+ <button class="ace-btn ace-btn--primary">Save</button>
41
+ <button class="ace-btn ace-btn--secondary">Cancel</button>
42
+
43
+ <div class="ace-field">
44
+ <label class="ace-label" for="email">Email</label>
45
+ <input id="email" type="email" class="ace-input">
46
+ </div>
47
+
48
+ <select name="plan" class="ace-select" data-ace-select>
49
+ <option value="basic">Basic</option>
50
+ <option value="full">Full cover</option>
51
+ </select>
52
+
53
+ <div class="ace-datepicker" data-ace-daterange
54
+ data-start-name="pickup_date" data-end-name="return_date" data-min="today"></div>
55
+
56
+ <span class="ace-badge ace-badge--success">Paid</span>
57
+ ```
58
+
59
+ Every component, with copy-paste code and usage rules: **https://acecss.dev**
60
+
61
+ - **Dark mode:** add `class="dark"` (or `data-theme="dark"`) to `<html>`, or `class="ace-auto-dark"` to follow the OS.
62
+ - **Re-brand:** override the tokens, e.g. `:root { --ace-primary: #2563eb; --ace-radius: 12px; }`
63
+ - **Button colours:** `ace-btn--blue`, `--green`, `--purple`, `--pink`, `--black`, or any colour with
64
+ `style="--ace-tone:#FF0022"` on a primary button (or a whole section). Light colour? Add `--ace-tone-fg:#111`.
65
+ Tailwind: `class="ace-btn ace-btn--primary [--ace-tone:#FF0022]"`
66
+ - **Markup added later** (Vue, React, HTMX): call `AceDatePicker.init(el)` / `AceSelect.init(el)`.
67
+
68
+ ## Files
69
+
70
+ | File | |
71
+ |---|---|
72
+ | `dist/ace.css`, `dist/ace.min.css` | All components (required) |
73
+ | `dist/ace-datepicker.js`, `.min.js` | Date picker and range picker (optional) |
74
+ | `dist/ace-select.js`, `.min.js` | Dropdown (optional) |
75
+
76
+ ## Credits & license
77
+
78
+ AceCSS started as the component set of [PRC.rent](https://prc.rent), built by Kosowwwa LLC.
79
+ MIT License © 2026 Kosowwwa LLC. Source: https://gitlab.com/erblin.kosowwwa/acecss
@@ -0,0 +1,518 @@
1
+ /* GENERATED by build.py from the PRC kit — do not edit by hand. */
2
+ /* ═══════════════════════════════════════════════════════════════════
3
+ ACECSS DATE PICKER — v1.0.4 · MIT License · (c) 2026 Kosowwwa LLC
4
+ Dependency-free single-date and range picker. Styles live in ace.css (section 8).
5
+
6
+ SINGLE DATE
7
+ <label class="ace-label" for="pickup">Pick-up date</label>
8
+ <div class="ace-datepicker" data-ace-datepicker
9
+ data-id="pickup" data-name="pickup_date" data-min="today"></div>
10
+
11
+ RANGE (pick-up → return, with times)
12
+ <div class="ace-datepicker" data-ace-daterange
13
+ data-start-name="pickup_date" data-end-name="return_date"
14
+ data-start-time-name="pickup_time" data-end-time-name="return_time"
15
+ data-min="today"></div>
16
+
17
+ Options (data-*): value | start / end | start-time / end-time
18
+ min / max (YYYY-MM-DD or "today") · placeholder · locale (BCP 47,
19
+ default = page language) · disabled · invalid · time="false"
20
+ start-label / end-label (time labels) · clear-label / close-label
21
+
22
+ Values are submitted with the form as hidden inputs in ISO format
23
+ (YYYY-MM-DD, times HH:MM). Every change fires a `ace:change` event on
24
+ the .ace-datepicker element; event.detail holds the current values.
25
+
26
+ Auto-initialises on DOMContentLoaded. For markup added later (Vue,
27
+ React, HTMX…) call AceDatePicker.init(containerElement).
28
+ ═══════════════════════════════════════════════════════════════════ */
29
+ (function () {
30
+ 'use strict';
31
+
32
+ // Server-side rendering (Nuxt, Next.js, …) imports this too: there is
33
+ // no DOM there, so do nothing and let the browser bundle initialise.
34
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
35
+
36
+ var SVG = {
37
+ cal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
38
+ prev: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>',
39
+ next: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>',
40
+ down: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>',
41
+ check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>',
42
+ clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>'
43
+ };
44
+
45
+ /* ── date helpers (local dates, never UTC) ── */
46
+ function pad(n) { return String(n).padStart(2, '0'); }
47
+ function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
48
+ function parseISO(s) {
49
+ var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s || '');
50
+ return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
51
+ }
52
+ function isoToDisplay(iso) {
53
+ var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso || '');
54
+ return m ? m[3] + '/' + m[2] + '/' + m[1] : '';
55
+ }
56
+ function resolveBound(v) { return v === 'today' ? toISO(new Date()) : (v || ''); }
57
+ function esc(s) {
58
+ return String(s).replace(/[&<>"']/g, function (c) {
59
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
60
+ });
61
+ }
62
+
63
+ /* 24h HH:MM masking */
64
+ function maskTime(raw) {
65
+ var v = raw.replace(/[^\d]/g, '');
66
+ // A first digit of 3-9 can only be a single-digit hour: "930" → 09:30
67
+ if (/^[3-9]/.test(v)) v = '0' + v;
68
+ v = v.slice(0, 4);
69
+ if (v.length >= 2) { var h = Math.min(parseInt(v.slice(0, 2), 10), 23); v = pad(h) + v.slice(2); }
70
+ if (v.length >= 4) { var mi = Math.min(parseInt(v.slice(2, 4), 10), 59); v = v.slice(0, 2) + pad(mi); }
71
+ if (v.length >= 3) v = v.slice(0, 2) + ':' + v.slice(2);
72
+ return v;
73
+ }
74
+ function padTime(raw) {
75
+ if (!raw) return '';
76
+ var d = raw.replace(/[^\d]/g, ''), v = raw;
77
+ if (d.length === 1) v = '0' + d + ':00';
78
+ else if (d.length === 2) v = d + ':00';
79
+ else if (d.length === 3) v = '0' + d.slice(0, 1) + ':' + d.slice(1);
80
+ else if (d.length === 4) v = d.slice(0, 2) + ':' + d.slice(2);
81
+ return maskTime(v);
82
+ }
83
+
84
+ function weekdays(locale) {
85
+ var out = [];
86
+ for (var i = 0; i < 7; i++) {
87
+ out.push(new Date(2024, 0, 7 + i).toLocaleDateString(locale, { weekday: 'short' }).slice(0, 2));
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /* Month grid: Sunday-first, trailing rows trimmed */
93
+ function monthCells(year, month) {
94
+ var start = new Date(year, month, 1).getDay();
95
+ var dim = new Date(year, month + 1, 0).getDate();
96
+ var cells = [];
97
+ for (var i = 0; i < 42; i++) {
98
+ var dt = new Date(year, month, 1 - start + i);
99
+ cells.push({ date: dt.getDate(), month: dt.getMonth(), year: dt.getFullYear(), iso: toISO(dt), current: dt.getMonth() === month });
100
+ if (i >= start + dim - 1 && (i + 1) % 7 === 0) break;
101
+ }
102
+ return cells;
103
+ }
104
+
105
+ function hidden(el, name, value) {
106
+ var h = document.createElement('input');
107
+ h.type = 'hidden';
108
+ if (name) h.name = name;
109
+ h.value = value || '';
110
+ el.appendChild(h);
111
+ return h;
112
+ }
113
+
114
+ function emit(el, detail) {
115
+ el.dispatchEvent(new CustomEvent('ace:change', { bubbles: true, detail: detail }));
116
+ }
117
+
118
+ /* Shared calendar header + grid markup */
119
+ function calendarHTML(st, opts) {
120
+ var label = new Date(st.viewYear, st.viewMonth, 1).toLocaleDateString(st.locale, { month: 'long', year: 'numeric' });
121
+ var html = '';
122
+ if (opts.yearPicker) {
123
+ var monthName = new Date(st.viewYear, st.viewMonth, 1).toLocaleDateString(st.locale, { month: 'long' });
124
+ html += '<div class="ace-dp-head"><span class="ace-dp-title">' + esc(monthName) + '</span>' +
125
+ '<div class="ace-dp-year"><button type="button" class="ace-btn ace-btn--secondary ace-dp-yearbtn" data-act="years" aria-haspopup="listbox" aria-expanded="' + !!st.yearsOpen + '">' +
126
+ st.viewYear + SVG.down + '</button>';
127
+ if (st.yearsOpen) {
128
+ html += '<div class="ace-dp-years" role="listbox">';
129
+ for (var y = st.minYear; y <= st.maxYear; y++) {
130
+ var on = y === st.viewYear;
131
+ html += '<button type="button" role="option" data-year="' + y + '" aria-selected="' + on + '">' +
132
+ (on ? SVG.check : '<span class="ace-dp-check"></span>') + y + '</button>';
133
+ }
134
+ html += '</div>';
135
+ }
136
+ html += '</div></div>';
137
+ }
138
+ html += '<div class="ace-dp-nav">' +
139
+ '<button type="button" class="ace-btn ace-btn--secondary ace-btn--icon ace-dp-navbtn" data-act="prev" aria-label="Previous month">' + SVG.prev + '</button>' +
140
+ '<span class="ace-dp-month" aria-live="polite">' + esc(label) + '</span>' +
141
+ '<button type="button" class="ace-btn ace-btn--secondary ace-btn--icon ace-dp-navbtn" data-act="next" aria-label="Next month">' + SVG.next + '</button></div>';
142
+ html += '<div class="ace-dp-weekdays">' + weekdays(st.locale).map(function (w) { return '<div>' + esc(w) + '</div>'; }).join('') + '</div>';
143
+ html += '<div class="ace-dp-grid" role="grid">' + opts.cells.map(opts.cellHTML).join('') + '</div>';
144
+ return html;
145
+ }
146
+
147
+ function dayHTML(c, attrs, cellAttrs) {
148
+ var full = parseISO(c.iso).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' });
149
+ return '<div class="ace-dp-cell"' + (cellAttrs || '') + '><button type="button" class="ace-dp-day" data-iso="' + c.iso + '"' +
150
+ ' aria-label="' + esc(full) + '"' + attrs + '>' + c.date + '</button></div>';
151
+ }
152
+
153
+ function stepMonth(st, delta) {
154
+ var d = new Date(st.viewYear, st.viewMonth + delta, 1);
155
+ st.viewYear = d.getFullYear();
156
+ st.viewMonth = d.getMonth();
157
+ }
158
+
159
+ /* ════════════════════════ single date ════════════════════════ */
160
+ function initSingle(el) {
161
+ var ds = el.dataset;
162
+ var today = toISO(new Date());
163
+ var st = {
164
+ value: ds.value || '',
165
+ min: resolveBound(ds.min), max: resolveBound(ds.max),
166
+ locale: ds.locale || document.documentElement.lang || undefined,
167
+ open: false, yearsOpen: false
168
+ };
169
+ var cur = new Date().getFullYear();
170
+ st.minYear = st.min ? +st.min.slice(0, 4) : cur - 20;
171
+ st.maxYear = st.max ? +st.max.slice(0, 4) : cur + 10;
172
+
173
+ el.innerHTML = '';
174
+ var field = document.createElement('div');
175
+ field.className = 'ace-input-group ace-date-field' + (ds.invalid != null ? ' is-invalid' : '') + (ds.disabled != null ? ' is-disabled' : '');
176
+ field.innerHTML = '<input type="text" inputmode="numeric" autocomplete="off"' +
177
+ (ds.id ? ' id="' + esc(ds.id) + '"' : '') +
178
+ ' placeholder="' + esc(ds.placeholder || 'DD/MM/YYYY') + '"' +
179
+ (ds.invalid != null ? ' aria-invalid="true"' : '') +
180
+ (ds.disabled != null ? ' disabled' : '') + '>' +
181
+ '<button type="button" class="ace-date-toggle" aria-label="Choose date" aria-haspopup="dialog" aria-expanded="false"' +
182
+ (ds.disabled != null ? ' disabled' : '') + '>' + SVG.cal + '</button>';
183
+ el.appendChild(field);
184
+ var input = field.querySelector('input');
185
+ var toggleBtn = field.querySelector('.ace-date-toggle');
186
+ var out = hidden(el, ds.name, st.value);
187
+ input.value = isoToDisplay(st.value);
188
+ var pop = null;
189
+
190
+ function setValue(iso, silent) {
191
+ st.value = iso || '';
192
+ out.value = st.value;
193
+ input.value = isoToDisplay(st.value);
194
+ if (!silent) {
195
+ emit(el, { value: st.value });
196
+ out.dispatchEvent(new Event('change', { bubbles: true }));
197
+ }
198
+ }
199
+
200
+ function commitTyped() {
201
+ var s = input.value.trim();
202
+ if (!s) { if (st.value) setValue(''); return; }
203
+ var m = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4})$/.exec(s) || /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s);
204
+ if (!m) { input.value = isoToDisplay(st.value); return; }
205
+ var y, mo, d;
206
+ if (m[1].length === 4) { y = +m[1]; mo = +m[2]; d = +m[3]; } else { d = +m[1]; mo = +m[2]; y = +m[3]; }
207
+ var dt = new Date(y, mo - 1, d);
208
+ if (mo < 1 || mo > 12 || dt.getDate() !== d) { input.value = isoToDisplay(st.value); return; }
209
+ var iso = toISO(dt);
210
+ if ((st.min && iso < st.min) || (st.max && iso > st.max)) { input.value = isoToDisplay(st.value); return; }
211
+ if (iso !== st.value) setValue(iso); else input.value = isoToDisplay(iso);
212
+ }
213
+
214
+ function render() {
215
+ if (!pop) return;
216
+ var cells = monthCells(st.viewYear, st.viewMonth);
217
+ pop.innerHTML = calendarHTML(st, {
218
+ yearPicker: true,
219
+ cells: cells,
220
+ cellHTML: function (c) {
221
+ var dis = (st.min && c.iso < st.min) || (st.max && c.iso > st.max);
222
+ var sel = c.iso === st.value;
223
+ return dayHTML(c,
224
+ (sel ? ' data-selected aria-pressed="true"' : ' aria-pressed="false"') +
225
+ (c.iso === today ? ' data-today aria-current="date"' : '') +
226
+ (!c.current ? ' data-outside' : '') +
227
+ (dis ? ' disabled' : ''));
228
+ }
229
+ });
230
+ }
231
+
232
+ function position() {
233
+ if (!pop) return;
234
+ var r = field.getBoundingClientRect();
235
+ var w = pop.offsetWidth, h = pop.offsetHeight;
236
+ var top = r.bottom + 8;
237
+ if (top + h > window.innerHeight - 8) top = Math.max(8, r.top - h - 8);
238
+ var left = r.left;
239
+ if (left + w > window.innerWidth - 8) left = Math.max(8, window.innerWidth - w - 8);
240
+ pop.style.top = top + 'px';
241
+ pop.style.left = left + 'px';
242
+ }
243
+
244
+ function open() {
245
+ if (st.open || ds.disabled != null) return;
246
+ var d = parseISO(st.value) || new Date();
247
+ st.viewYear = d.getFullYear(); st.viewMonth = d.getMonth();
248
+ st.open = true; st.yearsOpen = false;
249
+ pop = document.createElement('div');
250
+ pop.className = 'ace-dp';
251
+ pop.setAttribute('role', 'dialog');
252
+ pop.setAttribute('aria-label', 'Choose date');
253
+ document.body.appendChild(pop);
254
+ render();
255
+ position();
256
+ toggleBtn.setAttribute('aria-expanded', 'true');
257
+ var focusEl = pop.querySelector('.ace-dp-day[data-selected]') || pop.querySelector('.ace-dp-day[data-today]');
258
+ if (focusEl && !focusEl.disabled) focusEl.focus({ preventScroll: true });
259
+ pop.addEventListener('click', onPopClick);
260
+ pop.addEventListener('keydown', onKey);
261
+ document.addEventListener('mousedown', onDocDown);
262
+ window.addEventListener('scroll', position, true);
263
+ window.addEventListener('resize', position);
264
+ }
265
+ function close(refocus) {
266
+ if (!st.open) return;
267
+ st.open = false;
268
+ pop.remove(); pop = null;
269
+ toggleBtn.setAttribute('aria-expanded', 'false');
270
+ document.removeEventListener('mousedown', onDocDown);
271
+ window.removeEventListener('scroll', position, true);
272
+ window.removeEventListener('resize', position);
273
+ if (refocus) toggleBtn.focus();
274
+ }
275
+ function onPopClick(e) {
276
+ var t = e.target.closest('button');
277
+ if (!t) return;
278
+ if (t.dataset.act === 'prev' || t.dataset.act === 'next') {
279
+ stepMonth(st, t.dataset.act === 'prev' ? -1 : 1); render();
280
+ pop.querySelector('[data-act="' + t.dataset.act + '"]').focus();
281
+ }
282
+ else if (t.dataset.act === 'years') {
283
+ st.yearsOpen = !st.yearsOpen; render();
284
+ var sel = pop.querySelector('.ace-dp-years [aria-selected="true"]');
285
+ if (sel) sel.scrollIntoView({ block: 'center' });
286
+ }
287
+ else if (t.dataset.year) { st.viewYear = +t.dataset.year; st.yearsOpen = false; render(); }
288
+ else if (t.dataset.iso && !t.disabled) { setValue(t.dataset.iso); close(true); }
289
+ }
290
+ function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); close(true); } }
291
+ function onDocDown(e) { if (!el.contains(e.target) && pop && !pop.contains(e.target)) close(false); }
292
+
293
+ toggleBtn.addEventListener('click', function () { st.open ? close(false) : open(); });
294
+ input.addEventListener('blur', commitTyped);
295
+ input.addEventListener('keydown', function (e) {
296
+ if (e.key === 'Enter') { e.preventDefault(); commitTyped(); }
297
+ if (e.key === 'ArrowDown' && e.altKey) { e.preventDefault(); open(); }
298
+ });
299
+
300
+ el.aceDatepicker = {
301
+ getValue: function () { return st.value; },
302
+ setValue: function (iso) { setValue(iso, true); },
303
+ open: open, close: close
304
+ };
305
+ }
306
+
307
+ /* ════════════════════════ date range ════════════════════════ */
308
+ function initRange(el) {
309
+ var ds = el.dataset;
310
+ var today = toISO(new Date());
311
+ var withTime = ds.time !== 'false';
312
+ var st = {
313
+ start: ds.start || '', end: ds.end || '',
314
+ startTime: ds.startTime || '', endTime: ds.endTime || '',
315
+ min: resolveBound(ds.min), max: resolveBound(ds.max),
316
+ locale: ds.locale || document.documentElement.lang || undefined,
317
+ hover: '', open: false
318
+ };
319
+
320
+ el.innerHTML = '';
321
+ var trigger = document.createElement('button');
322
+ trigger.type = 'button';
323
+ trigger.className = 'ace-input-group ace-date-trigger' + (ds.invalid != null ? ' is-invalid' : '');
324
+ if (ds.id) trigger.id = ds.id;
325
+ if (ds.invalid != null) trigger.setAttribute('aria-invalid', 'true');
326
+ if (ds.disabled != null) trigger.disabled = true;
327
+ trigger.setAttribute('aria-haspopup', 'dialog');
328
+ trigger.setAttribute('aria-expanded', 'false');
329
+ el.appendChild(trigger);
330
+ var outStart = hidden(el, ds.startName, st.start);
331
+ var outEnd = hidden(el, ds.endName, st.end);
332
+ var outST = withTime ? hidden(el, ds.startTimeName, st.startTime) : null;
333
+ var outET = withTime ? hidden(el, ds.endTimeName, st.endTime) : null;
334
+ var pop = null, backdrop = null;
335
+
336
+ function triggerLabel() {
337
+ var s = parseISO(st.start), e = parseISO(st.end);
338
+ var short = { day: 'numeric', month: 'short' }, full = { day: 'numeric', month: 'short', year: 'numeric' };
339
+ var txt = '';
340
+ if (s && e) txt = s.toLocaleDateString(st.locale, short) + (st.startTime ? ' ' + st.startTime : '') +
341
+ ' → ' + e.toLocaleDateString(st.locale, full) + (st.endTime ? ' ' + st.endTime : '');
342
+ else if (s) txt = s.toLocaleDateString(st.locale, full) + (st.startTime ? ' ' + st.startTime : '');
343
+ return txt;
344
+ }
345
+ function paintTrigger() {
346
+ var txt = triggerLabel();
347
+ trigger.innerHTML = SVG.cal + '<span class="ace-date-label' + (txt ? '' : ' is-empty') + '">' +
348
+ esc(txt || ds.placeholder || 'Pick-up → return') + '</span>';
349
+ }
350
+ function sync() {
351
+ outStart.value = st.start; outEnd.value = st.end;
352
+ if (outST) outST.value = st.startTime;
353
+ if (outET) outET.value = st.endTime;
354
+ paintTrigger();
355
+ emit(el, { start: st.start, end: st.end, startTime: st.startTime, endTime: st.endTime });
356
+ outEnd.dispatchEvent(new Event('change', { bubbles: true }));
357
+ }
358
+
359
+ function previewEnd() {
360
+ var h = st.hover;
361
+ if (!st.start || st.end || !h || h <= st.start) return '';
362
+ if ((st.min && h < st.min) || (st.max && h > st.max)) return '';
363
+ return h;
364
+ }
365
+
366
+ function renderGrid() {
367
+ var pe = previewEnd();
368
+ var cells = monthCells(st.viewYear, st.viewMonth);
369
+ var html = calendarHTML(st, {
370
+ yearPicker: false,
371
+ cells: cells,
372
+ cellHTML: function (c) {
373
+ var iso = c.iso;
374
+ var isStart = !!st.start && iso === st.start;
375
+ var isEnd = !!st.end && iso === st.end;
376
+ var inRange = !!st.start && !!st.end && iso >= st.start && iso <= st.end;
377
+ var inPreview = !!pe && iso >= st.start && iso <= pe;
378
+ var band = inRange ? 'range' : inPreview ? 'preview' : '';
379
+ var bStart = iso === st.start;
380
+ var bEnd = inRange ? isEnd : iso === pe;
381
+ var edge = !band ? '' : bStart && bEnd ? 'both' : bStart ? 'start' : bEnd ? 'end' : '';
382
+ var dis = (st.min && iso < st.min) || (st.max && iso > st.max);
383
+ return dayHTML(c,
384
+ (isStart || isEnd ? ' data-selected' : '') +
385
+ (band === 'range' && !isStart && !isEnd ? ' data-in-range' : '') +
386
+ (pe && iso === pe && pe !== st.start ? ' data-preview-end' : '') +
387
+ (iso === today ? ' data-today aria-current="date"' : '') +
388
+ (!c.current ? ' data-outside' : '') +
389
+ ' aria-pressed="' + (isStart || isEnd || band === 'range') + '"' +
390
+ (dis ? ' disabled' : ''),
391
+ (band ? ' data-band="' + band + '"' : '') + (edge ? ' data-band-edge="' + edge + '"' : ''));
392
+ }
393
+ });
394
+ pop.querySelector('.ace-dp-cal').innerHTML = html;
395
+ }
396
+
397
+ function timeField(which, label) {
398
+ var id = 'ace-dp-' + which + '-' + Math.random().toString(36).slice(2, 8);
399
+ return '<div><label for="' + id + '">' + esc(label) + '</label>' +
400
+ '<div class="ace-input-group">' + SVG.clock +
401
+ '<input id="' + id + '" type="text" inputmode="numeric" placeholder="00:00" maxlength="5" data-time="' + which + '" value="' +
402
+ esc(which === 'start' ? st.startTime : st.endTime) + '"></div></div>';
403
+ }
404
+
405
+ function open() {
406
+ if (st.open || trigger.disabled) return;
407
+ var d = parseISO(st.start) || new Date();
408
+ st.viewYear = d.getFullYear(); st.viewMonth = d.getMonth();
409
+ st.open = true; st.hover = '';
410
+ backdrop = document.createElement('div');
411
+ backdrop.className = 'ace-dp-backdrop';
412
+ pop = document.createElement('div');
413
+ pop.className = 'ace-dp ace-dp--range';
414
+ pop.setAttribute('role', 'dialog');
415
+ pop.setAttribute('aria-modal', 'true');
416
+ pop.setAttribute('aria-label', 'Choose pick-up and return dates');
417
+ pop.innerHTML = '<div class="ace-dp-cal"></div><div class="ace-dp-foot">' +
418
+ (withTime ? '<div class="ace-dp-times">' + timeField('start', ds.startLabel || 'Pick-up time') + timeField('end', ds.endLabel || 'Return time') + '</div>' : '') +
419
+ '<div class="ace-dp-actions"><button type="button" class="ace-btn ace-btn--secondary ace-btn--sm" data-act="clear">' + esc(ds.clearLabel || 'Clear') + '</button>' +
420
+ '<button type="button" class="ace-btn ace-btn--primary ace-btn--sm" data-act="close">' + esc(ds.closeLabel || 'Close') + '</button></div></div>';
421
+ document.body.appendChild(backdrop);
422
+ document.body.appendChild(pop);
423
+ renderGrid();
424
+ trigger.setAttribute('aria-expanded', 'true');
425
+ var f = pop.querySelector('.ace-dp-day[data-selected]') || pop.querySelector('.ace-dp-day[data-today]') || pop.querySelector('.ace-dp-navbtn');
426
+ if (f && !f.disabled) f.focus({ preventScroll: true });
427
+ backdrop.addEventListener('click', function () { close(true); });
428
+ pop.addEventListener('click', onPopClick);
429
+ pop.addEventListener('keydown', onKey);
430
+ pop.addEventListener('mouseover', onHover);
431
+ pop.addEventListener('mouseleave', function () { if (st.hover) { st.hover = ''; renderGrid(); } });
432
+ pop.addEventListener('input', onTimeInput);
433
+ pop.addEventListener('focusout', onTimeBlur);
434
+ }
435
+ function close(refocus) {
436
+ if (!st.open) return;
437
+ st.open = false;
438
+ pop.remove(); backdrop.remove(); pop = backdrop = null;
439
+ trigger.setAttribute('aria-expanded', 'false');
440
+ if (refocus) trigger.focus();
441
+ }
442
+ function onPopClick(e) {
443
+ var t = e.target.closest('button');
444
+ if (!t) return;
445
+ if (t.dataset.act === 'prev' || t.dataset.act === 'next') {
446
+ stepMonth(st, t.dataset.act === 'prev' ? -1 : 1); renderGrid();
447
+ pop.querySelector('[data-act="' + t.dataset.act + '"]').focus();
448
+ }
449
+ else if (t.dataset.act === 'close') close(true);
450
+ else if (t.dataset.act === 'clear') {
451
+ st.start = st.end = ''; st.hover = '';
452
+ renderGrid(); sync();
453
+ }
454
+ else if (t.dataset.iso && !t.disabled) {
455
+ var iso = t.dataset.iso;
456
+ // No start yet, or a full range already → start a new range.
457
+ // Clicking before the start moves the start instead.
458
+ if (!st.start || st.end || iso < st.start) { st.start = iso; st.end = ''; }
459
+ else st.end = iso;
460
+ renderGrid(); sync();
461
+ var again = pop.querySelector('.ace-dp-day[data-iso="' + iso + '"]');
462
+ if (again) again.focus({ preventScroll: true });
463
+ }
464
+ }
465
+ function onHover(e) {
466
+ var t = e.target.closest('.ace-dp-day');
467
+ var iso = t && !t.disabled ? t.dataset.iso : '';
468
+ if (iso && iso !== st.hover && st.start && !st.end) { st.hover = iso; renderGrid(); }
469
+ }
470
+ function onKey(e) {
471
+ if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
472
+ if (e.key === 'Tab') { // keep focus inside the modal
473
+ var f = pop.querySelectorAll('button:not(:disabled), input');
474
+ var first = f[0], last = f[f.length - 1];
475
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
476
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
477
+ }
478
+ }
479
+ function onTimeInput(e) {
480
+ var w = e.target.dataset && e.target.dataset.time;
481
+ if (!w) return;
482
+ e.target.value = maskTime(e.target.value);
483
+ }
484
+ function onTimeBlur(e) {
485
+ var w = e.target.dataset && e.target.dataset.time;
486
+ if (!w) return;
487
+ var v = padTime(e.target.value);
488
+ e.target.value = v;
489
+ if (w === 'start') st.startTime = v; else st.endTime = v;
490
+ sync();
491
+ }
492
+
493
+ trigger.addEventListener('click', open);
494
+ paintTrigger();
495
+
496
+ el.aceDatepicker = {
497
+ getValue: function () { return { start: st.start, end: st.end, startTime: st.startTime, endTime: st.endTime }; },
498
+ setValue: function (v) {
499
+ ['start', 'end', 'startTime', 'endTime'].forEach(function (k) { if (v && k in v) st[k] = v[k] || ''; });
500
+ outStart.value = st.start; outEnd.value = st.end;
501
+ if (outST) outST.value = st.startTime;
502
+ if (outET) outET.value = st.endTime;
503
+ paintTrigger();
504
+ },
505
+ open: open, close: close
506
+ };
507
+ }
508
+
509
+ function init(root) {
510
+ root = root || document;
511
+ root.querySelectorAll('[data-ace-datepicker]').forEach(function (el) { if (!el.aceDatepicker) initSingle(el); });
512
+ root.querySelectorAll('[data-ace-daterange]').forEach(function (el) { if (!el.aceDatepicker) initRange(el); });
513
+ }
514
+
515
+ window.AceDatePicker = { init: init };
516
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(); });
517
+ else init();
518
+ })();