@tzslot/theme 0.1.0-beta.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 +21 -0
- package/README.md +152 -0
- package/contrast.css +61 -0
- package/package.json +41 -0
- package/tailwind.css +55 -0
- package/tzslot.css +182 -0
- package/tzslot.scss +247 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Plinthworks
|
|
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,152 @@
|
|
|
1
|
+
# tzslot
|
|
2
|
+
|
|
3
|
+
Date and time pickers that know what daylight saving does.
|
|
4
|
+
|
|
5
|
+
Most pickers — flatpickr, Air Datepicker, nearly every Angular wrapper — are
|
|
6
|
+
built on `Date`, which has no notion of an IANA time zone. They will let
|
|
7
|
+
someone book 02:30 on a morning when 02:30 does not happen, or on one when it
|
|
8
|
+
happens twice, and store whichever reading the browser guessed. They call a
|
|
9
|
+
night shift from 23:00 to 05:00 six hours on the night it lasts seven.
|
|
10
|
+
|
|
11
|
+
tzslot is built on [Temporal](https://tc39.es/proposal-temporal/docs/). It
|
|
12
|
+
shows the hour that does not exist, offers both readings of the one that
|
|
13
|
+
happens twice, and says in words when an interval lasts something other than
|
|
14
|
+
what the clock faces suggest.
|
|
15
|
+
|
|
16
|
+
## Packages
|
|
17
|
+
|
|
18
|
+
| | |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `@tzslot/core` | The arithmetic. No DOM, no framework — usable on a server too. |
|
|
21
|
+
| `@tzslot/dom` | Every widget, in plain DOM. Works in any framework, or none. |
|
|
22
|
+
| `@tzslot/angular` | Angular components over `@tzslot/dom`: signals, forms, zoneless or not. Angular 18 to 21. |
|
|
23
|
+
| `@tzslot/theme` | Optional. CSS (or Sass) setting the widgets' colours; light, dark, high contrast, a Tailwind v4 bridge. |
|
|
24
|
+
|
|
25
|
+
The widgets live in `@tzslot/dom`; a framework package only translates its own
|
|
26
|
+
idioms into calls on them. A behaviour fixed there is fixed everywhere.
|
|
27
|
+
|
|
28
|
+
## The widgets
|
|
29
|
+
|
|
30
|
+
| Angular | Plain DOM | What it chooses |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `<tz-calendar>` | `createCalendar` | a day — month and year views, keyboard, Today / Clear |
|
|
33
|
+
| `<tz-multi-date>` | `createMultiDate` | several days, not necessarily adjacent |
|
|
34
|
+
| `<tz-date-field>` | `createDateField` | a day, from a field that opens a panel, anchored or centred |
|
|
35
|
+
| `<tz-date-range>` | `createDateRange` | two days, refusing a range across a closed one |
|
|
36
|
+
| `<tz-time-slots>` | `createTimeSlots` | a moment on one day: the skipped hour struck through, the repeated one offered twice |
|
|
37
|
+
| `<tz-datetime-range>` | `createDateTimeRange` | an interval with a time at both ends, and the hour it hides |
|
|
38
|
+
| `<tz-daily-range>` | `createDailyRange` | a range of days with the same hours on each, overnight allowed |
|
|
39
|
+
|
|
40
|
+
`renderCell` puts a price, places left or your own class on any day.
|
|
41
|
+
|
|
42
|
+
## Angular
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @tzslot/angular @tzslot/theme
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// angular.json → build.options
|
|
50
|
+
"styles": ["@tzslot/theme/tzslot.css", "src/styles.css"]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Component, signal } from '@angular/core';
|
|
55
|
+
import { Calendar, TimeSlotPicker, provideTzslotMessages, FR } from '@tzslot/angular';
|
|
56
|
+
import type { Instant, PlainDate } from '@tzslot/core';
|
|
57
|
+
|
|
58
|
+
@Component({
|
|
59
|
+
imports: [Calendar, TimeSlotPicker],
|
|
60
|
+
template: `
|
|
61
|
+
<tz-calendar [(value)]="day" [buttons]="['today', 'clear']" />
|
|
62
|
+
@if (day(); as d) {
|
|
63
|
+
<tz-time-slots [date]="d" timeZone="Europe/Paris" [(value)]="moment" />
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
})
|
|
67
|
+
export class Booking {
|
|
68
|
+
readonly day = signal<PlainDate | null>(null);
|
|
69
|
+
readonly moment = signal<Instant | null>(null);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// French everywhere: bootstrapApplication(App, { providers: [provideTzslotMessages(FR)] })
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Every component is a `ControlValueAccessor` — `formControlName`, `ngModel` and
|
|
76
|
+
`[(value)]` all work — and takes `valueAs="date"` to keep an existing
|
|
77
|
+
`FormControl<Date>` untouched. See `docs/migrating-from-flatpickr.md`.
|
|
78
|
+
|
|
79
|
+
## Without a framework
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install @tzslot/dom @tzslot/theme
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { createCalendar } from '@tzslot/dom';
|
|
87
|
+
import { Temporal } from '@tzslot/core';
|
|
88
|
+
import '@tzslot/theme';
|
|
89
|
+
|
|
90
|
+
const calendar = createCalendar(document.querySelector('#day'), {
|
|
91
|
+
onChange: (day) => console.log(day?.toString()),
|
|
92
|
+
});
|
|
93
|
+
calendar.update({ min: Temporal.Now.plainDateISO() });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Each `create…` returns an instance with `update`, `clear` and `destroy`.
|
|
97
|
+
Layout CSS is injected once; pass `injectStyles: false` under a strict
|
|
98
|
+
Content-Security-Policy and include the exported `*_CSS` strings yourself.
|
|
99
|
+
|
|
100
|
+
## The arithmetic
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { getDaySlots, getDailyWindows } from '@tzslot/core';
|
|
104
|
+
|
|
105
|
+
getDaySlots('2026-10-25', 'Europe/Paris').filter((s) => s.time.hour === 2);
|
|
106
|
+
// 02:00 is ambiguous: offsets ['+02:00', '+01:00'], two instants an hour apart
|
|
107
|
+
|
|
108
|
+
getDailyWindows('2026-10-23', '2026-10-26', '22:00', '06:00', 'Europe/Paris').minutes / 60;
|
|
109
|
+
// 33 — four night shifts, one of them nine hours long
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Store an instant, never a wall time: it is unambiguous everywhere and
|
|
113
|
+
survives a change of zone, of the rules, and being read back next year.
|
|
114
|
+
|
|
115
|
+
## Theming
|
|
116
|
+
|
|
117
|
+
Nine palette colours, each `light-dark(light, dark)`. `data-theme="dark"` on
|
|
118
|
+
any element themes everything inside it; `--tz-accent` on any element
|
|
119
|
+
recolours the selection, the range tint and the focus ring beneath it.
|
|
120
|
+
`@tzslot/theme/contrast.css` follows `prefers-contrast: more`, and
|
|
121
|
+
`@tzslot/theme/tailwind.css` maps the palette to a Tailwind v4 theme. From
|
|
122
|
+
Sass, `@use '@tzslot/theme/tzslot' with ($accent: …)`. All of it in
|
|
123
|
+
`docs/customising.md`.
|
|
124
|
+
|
|
125
|
+
## Browsers
|
|
126
|
+
|
|
127
|
+
Temporal is used natively where it exists and polyfilled where it does not.
|
|
128
|
+
The polyfill is a static import, so a bundle carries it for every visitor —
|
|
129
|
+
about 19 kB gzipped. There is deliberately no fallback to `Date`: it would
|
|
130
|
+
bring back the exact bug this library exists to fix.
|
|
131
|
+
|
|
132
|
+
The theme uses `light-dark()` and `color-mix()`: Chrome 123, Safari 17.5,
|
|
133
|
+
Firefox 120 and later.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm install
|
|
139
|
+
npm start # playground: http://localhost:4500/playground/index.html
|
|
140
|
+
npm test # 263 tests, against real time zone rules
|
|
141
|
+
npm run typecheck
|
|
142
|
+
npm run build # every package into its own dist/, ready to publish
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The playground has a page per way of using it: Angular, Tailwind, and plain
|
|
146
|
+
DOM.
|
|
147
|
+
|
|
148
|
+
## Licence
|
|
149
|
+
|
|
150
|
+
[MIT](LICENSE). Free to use, in personal and commercial projects alike, free
|
|
151
|
+
to copy, modify and redistribute; keep the copyright notice. It comes with no
|
|
152
|
+
warranty.
|
package/contrast.css
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tzslot/theme/contrast.css — more contrast, for people who need it.
|
|
3
|
+
*
|
|
4
|
+
* import '@tzslot/theme';
|
|
5
|
+
* import '@tzslot/theme/contrast.css';
|
|
6
|
+
*
|
|
7
|
+
* Applies on its own when the system asks for more contrast
|
|
8
|
+
* (prefers-contrast: more), and on request with data-contrast="more" on any
|
|
9
|
+
* element. data-contrast="normal" on the root opts the page out of the
|
|
10
|
+
* automatic version.
|
|
11
|
+
*
|
|
12
|
+
* A separate axis from light and dark, not a third theme: someone who needs
|
|
13
|
+
* more contrast still has a preference between the two.
|
|
14
|
+
*
|
|
15
|
+
* Contrast: text and the selection meet WCAG AAA (7:1); faded states that
|
|
16
|
+
* carry meaning (outside the month, unavailable) stay above 4.5:1.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@media (prefers-contrast: more) {
|
|
20
|
+
:root:not([data-contrast='normal']) {
|
|
21
|
+
--tz-bg: light-dark(#ffffff, #000000);
|
|
22
|
+
--tz-bg-raised: light-dark(#e4e4e7, #27272a);
|
|
23
|
+
--tz-fg: light-dark(#000000, #ffffff);
|
|
24
|
+
--tz-fg-muted: light-dark(#27272a, #e4e4e7);
|
|
25
|
+
--tz-border: light-dark(#000000, #ffffff);
|
|
26
|
+
--tz-accent: light-dark(#1d3fb8, #ffd400);
|
|
27
|
+
--tz-accent-fg: light-dark(#ffffff, #000000);
|
|
28
|
+
--tz-danger: light-dark(#8b0000, #ff8a8a);
|
|
29
|
+
--tz-warning: light-dark(#6b3a00, #ffd27a);
|
|
30
|
+
--tz-cal-outside-opacity: 0.7;
|
|
31
|
+
--tz-slot-missing-opacity: 0.75;
|
|
32
|
+
--tz-slot-unavailable-opacity: 0.75;
|
|
33
|
+
--tz-focus-ring: 3px solid var(--tz-fg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-contrast='more'] {
|
|
38
|
+
--tz-bg: light-dark(#ffffff, #000000);
|
|
39
|
+
--tz-bg-raised: light-dark(#e4e4e7, #27272a);
|
|
40
|
+
--tz-fg: light-dark(#000000, #ffffff);
|
|
41
|
+
--tz-fg-muted: light-dark(#27272a, #e4e4e7);
|
|
42
|
+
--tz-border: light-dark(#000000, #ffffff);
|
|
43
|
+
--tz-accent: light-dark(#1d3fb8, #ffd400);
|
|
44
|
+
--tz-accent-fg: light-dark(#ffffff, #000000);
|
|
45
|
+
--tz-danger: light-dark(#8b0000, #ff8a8a);
|
|
46
|
+
--tz-warning: light-dark(#6b3a00, #ffd27a);
|
|
47
|
+
--tz-cal-outside-opacity: 0.7;
|
|
48
|
+
--tz-slot-missing-opacity: 0.75;
|
|
49
|
+
--tz-slot-unavailable-opacity: 0.75;
|
|
50
|
+
--tz-focus-ring: 3px solid var(--tz-fg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Borders on every cell, so the grid reads without relying on colour. */
|
|
54
|
+
:is([data-contrast='more']) :is(.tz-cal__day, .tz-range__day):not(:disabled) {
|
|
55
|
+
box-shadow: inset 0 0 0 1px var(--tz-border);
|
|
56
|
+
}
|
|
57
|
+
@media (prefers-contrast: more) {
|
|
58
|
+
:root:not([data-contrast='normal']) :is(.tz-cal__day, .tz-range__day):not(:disabled) {
|
|
59
|
+
box-shadow: inset 0 0 0 1px var(--tz-border);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tzslot/theme",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "An optional stylesheet for the tzslot components. Plain CSS, no framework.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./tzslot.css",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./tzslot.css",
|
|
9
|
+
"./tzslot.css": "./tzslot.css",
|
|
10
|
+
"./contrast.css": "./contrast.css",
|
|
11
|
+
"./tzslot.scss": "./tzslot.scss",
|
|
12
|
+
"./tzslot": {
|
|
13
|
+
"sass": "./tzslot.scss",
|
|
14
|
+
"default": "./tzslot.css"
|
|
15
|
+
},
|
|
16
|
+
"./tailwind.css": "./tailwind.css"
|
|
17
|
+
},
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"*.css"
|
|
20
|
+
],
|
|
21
|
+
"sass": "./tzslot.scss",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/plinthworks/tzslot.git",
|
|
25
|
+
"directory": "packages/theme"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/plinthworks/tzslot#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/plinthworks/tzslot/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"date",
|
|
33
|
+
"time",
|
|
34
|
+
"datepicker",
|
|
35
|
+
"timezone",
|
|
36
|
+
"dst",
|
|
37
|
+
"temporal",
|
|
38
|
+
"calendar",
|
|
39
|
+
"time-slots"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/tailwind.css
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tzslot/theme/tailwind.css — the widgets in your Tailwind v4 theme.
|
|
3
|
+
*
|
|
4
|
+
* In the CSS file that imports Tailwind, after it:
|
|
5
|
+
*
|
|
6
|
+
* @import "tailwindcss";
|
|
7
|
+
* @import "@tzslot/theme";
|
|
8
|
+
* @import "@tzslot/theme/tailwind.css";
|
|
9
|
+
*
|
|
10
|
+
* It has to go through Tailwind, not beside it: Tailwind v4 only emits the
|
|
11
|
+
* theme variables something references, and this file is what references
|
|
12
|
+
* them. Imported on its own, it would point at variables that do not exist.
|
|
13
|
+
*
|
|
14
|
+
* ── What it maps ────────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* surfaces and text → the zinc scale
|
|
17
|
+
* accent → your --color-primary-600 / -400 if your @theme has
|
|
18
|
+
* them, Tailwind's blue otherwise
|
|
19
|
+
* danger / warning → red / amber
|
|
20
|
+
* font, radius → --font-sans, --radius-md
|
|
21
|
+
*
|
|
22
|
+
* Another grey (slate, neutral, stone…) is a matter of overriding the five
|
|
23
|
+
* surface lines below with your own scale.
|
|
24
|
+
*
|
|
25
|
+
* ── Dark mode ───────────────────────────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Tailwind's default dark mode follows the system, and so do the widgets:
|
|
28
|
+
* nothing to do. With the class strategy (a .dark class toggled by your
|
|
29
|
+
* app), the .dark rule below reaches them; add
|
|
30
|
+
*
|
|
31
|
+
* :root { --tz-color-scheme: light; }
|
|
32
|
+
*
|
|
33
|
+
* so that no .dark means light even when the system is dark.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
:root {
|
|
37
|
+
--tz-bg: light-dark(var(--color-white), var(--color-zinc-900));
|
|
38
|
+
--tz-bg-raised: light-dark(var(--color-zinc-100), var(--color-zinc-800));
|
|
39
|
+
--tz-fg: light-dark(var(--color-zinc-900), var(--color-zinc-100));
|
|
40
|
+
--tz-fg-muted: light-dark(var(--color-zinc-600), var(--color-zinc-400));
|
|
41
|
+
--tz-border: light-dark(var(--color-zinc-300), var(--color-zinc-700));
|
|
42
|
+
--tz-accent: light-dark(
|
|
43
|
+
var(--color-primary-600, var(--color-blue-600)),
|
|
44
|
+
var(--color-primary-400, var(--color-blue-400))
|
|
45
|
+
);
|
|
46
|
+
--tz-accent-fg: light-dark(var(--color-white), var(--color-zinc-950));
|
|
47
|
+
--tz-danger: light-dark(var(--color-red-700), var(--color-red-400));
|
|
48
|
+
--tz-warning: light-dark(var(--color-amber-700), var(--color-amber-400));
|
|
49
|
+
--tz-font: var(--font-sans);
|
|
50
|
+
--tz-radius: var(--radius-md);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dark {
|
|
54
|
+
--tz-color-scheme: dark;
|
|
55
|
+
}
|
package/tzslot.css
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
2
|
+
/**
|
|
3
|
+
* @tzslot/theme — one import and the widgets are presentable.
|
|
4
|
+
*
|
|
5
|
+
* import '@tzslot/theme'; // plain CSS
|
|
6
|
+
* @use '@tzslot/theme/tzslot' with ($accent: #e11d48, $accent-fg: #fff);
|
|
7
|
+
*
|
|
8
|
+
* Generated from tzslot.scss — edit that file, then `npm run build:theme`.
|
|
9
|
+
*
|
|
10
|
+
* Optional by design. The widgets carry structural class names and read their
|
|
11
|
+
* colours from custom properties; this file only sets those properties and a
|
|
12
|
+
* handful of interaction rules. Replacing it means writing your own values for
|
|
13
|
+
* the same names, not fighting selectors.
|
|
14
|
+
*
|
|
15
|
+
* ── How it is built ──────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* One palette, nine colours, each written light-dark(light, dark). Which half
|
|
18
|
+
* applies is decided by `color-scheme` at the element that uses the colour, so:
|
|
19
|
+
*
|
|
20
|
+
* - by default the widgets follow the system;
|
|
21
|
+
* - data-theme="dark" or "light" on ANY element forces it for everything
|
|
22
|
+
* inside — a dark card on a light page is one attribute;
|
|
23
|
+
* - there is no second copy of the dark values to keep in step.
|
|
24
|
+
*
|
|
25
|
+
* Nothing derived is declared here. A variable like "the selected day's
|
|
26
|
+
* background" defaults to the accent at the place it is drawn, not on :root —
|
|
27
|
+
* a custom property that refers to another is frozen where it is declared, and
|
|
28
|
+
* declaring it on :root would stop a card's own --tz-accent from reaching it.
|
|
29
|
+
* Every such default lives in the widgets' base CSS as a var() fallback.
|
|
30
|
+
*
|
|
31
|
+
* Contrast: every text/background pair meets WCAG AA at the sizes used.
|
|
32
|
+
* light-dark() needs Chrome 123, Safari 17.5 or Firefox 120 (all of 2024).
|
|
33
|
+
*/
|
|
34
|
+
:root {
|
|
35
|
+
--tz-bg: light-dark(#ffffff, #18181b);
|
|
36
|
+
--tz-bg-raised: light-dark(#f4f4f5, #27272a);
|
|
37
|
+
--tz-fg: light-dark(#18181b, #f4f4f5);
|
|
38
|
+
--tz-fg-muted: light-dark(#52525b, #a1a1aa);
|
|
39
|
+
--tz-border: light-dark(#d4d4d8, #3f3f46);
|
|
40
|
+
--tz-accent: light-dark(#2563eb, #60a5fa);
|
|
41
|
+
--tz-accent-fg: light-dark(#ffffff, #0b1220);
|
|
42
|
+
--tz-danger: light-dark(#b91c1c, #f87171);
|
|
43
|
+
--tz-warning: light-dark(#b45309, #fbbf24);
|
|
44
|
+
/**
|
|
45
|
+
* The widgets inherit the page's font by default, which is almost always
|
|
46
|
+
* right. Set this only to make them differ from their surroundings.
|
|
47
|
+
*/
|
|
48
|
+
--tz-font: inherit;
|
|
49
|
+
--tz-radius: 0.375rem;
|
|
50
|
+
--tz-gap: 0.25rem;
|
|
51
|
+
--tz-cal-cell-size: 2.25rem;
|
|
52
|
+
--tz-cal-weekday-size: 0.6875rem;
|
|
53
|
+
--tz-cal-outside-opacity: 0.32;
|
|
54
|
+
--tz-slot-gap: 0.375rem;
|
|
55
|
+
--tz-slot-padding: 0.4375rem 0.25rem;
|
|
56
|
+
--tz-slot-note-size: 0.6875rem;
|
|
57
|
+
--tz-field-width: 13rem;
|
|
58
|
+
--tz-dtr-gap: 1.25rem;
|
|
59
|
+
--tz-dtr-leg-min: 15rem;
|
|
60
|
+
--tz-dtr-legend-size: 0.6875rem;
|
|
61
|
+
--tz-dtr-summary-size: 1.375rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Which half of every light-dark() applies. Declared on the widgets rather than
|
|
66
|
+
* the page, so importing the theme never changes the page's own form controls
|
|
67
|
+
* and scrollbars. --tz-color-scheme is inherited, which is how data-theme on
|
|
68
|
+
* any ancestor reaches them.
|
|
69
|
+
*/
|
|
70
|
+
.tz-cal,
|
|
71
|
+
.tz-range,
|
|
72
|
+
.tz-slots,
|
|
73
|
+
.tz-field,
|
|
74
|
+
.tz-field__panel,
|
|
75
|
+
.tz-field__backdrop,
|
|
76
|
+
.tz-dtr,
|
|
77
|
+
.tz-daily {
|
|
78
|
+
color-scheme: var(--tz-color-scheme, light dark);
|
|
79
|
+
color: var(--tz-fg);
|
|
80
|
+
font: var(--tz-font);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-theme=light] {
|
|
84
|
+
--tz-color-scheme: light;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[data-theme=dark] {
|
|
88
|
+
--tz-color-scheme: dark;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The rules the variables cannot express: hover, focus, the panel surface.
|
|
93
|
+
* Kept short — every rule here is one a consumer may have to override.
|
|
94
|
+
*/
|
|
95
|
+
.tz-cal__day:hover:not(:disabled),
|
|
96
|
+
.tz-cal__coarse-cell:hover:not(:disabled),
|
|
97
|
+
.tz-range__day:hover:not(:disabled),
|
|
98
|
+
.tz-cal__nav:hover:not(:disabled),
|
|
99
|
+
.tz-cal__title:hover:not(:disabled),
|
|
100
|
+
.tz-cal__action:hover:not(:disabled),
|
|
101
|
+
.tz-range__nav:hover:not(:disabled) {
|
|
102
|
+
background: var(--tz-bg-raised);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Hovering the selection must not hide it. */
|
|
106
|
+
.tz-cal__day--selected:hover:not(:disabled),
|
|
107
|
+
.tz-cal__coarse-cell--selected:hover:not(:disabled),
|
|
108
|
+
.tz-range__day--start:hover:not(:disabled),
|
|
109
|
+
.tz-range__day--end:hover:not(:disabled) {
|
|
110
|
+
background: color-mix(in srgb, var(--tz-accent) 85%, var(--tz-fg));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.tz-slots__slot:hover:not(:disabled),
|
|
114
|
+
.tz-field__trigger:hover:not(:disabled) {
|
|
115
|
+
border-color: var(--tz-accent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Focus must be visible on every control, including those inside the panel —
|
|
119
|
+
a keyboard user who cannot see where they are is stuck. */
|
|
120
|
+
.tz-cal__day:focus-visible,
|
|
121
|
+
.tz-cal__coarse-cell:focus-visible,
|
|
122
|
+
.tz-cal__title:focus-visible,
|
|
123
|
+
.tz-range__day:focus-visible,
|
|
124
|
+
.tz-cal__nav:focus-visible,
|
|
125
|
+
.tz-cal__action:focus-visible,
|
|
126
|
+
.tz-range__nav:focus-visible,
|
|
127
|
+
.tz-slots__slot:focus-visible,
|
|
128
|
+
.tz-field__trigger:focus-visible {
|
|
129
|
+
outline: var(--tz-focus-ring, 2px solid var(--tz-accent));
|
|
130
|
+
outline-offset: 2px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.tz-field__panel {
|
|
134
|
+
border: 1px solid var(--tz-border);
|
|
135
|
+
border-radius: calc(var(--tz-radius) * 1.5);
|
|
136
|
+
background: var(--tz-bg);
|
|
137
|
+
box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.35);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.tz-field__panel--dialog {
|
|
141
|
+
box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.5);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.tz-field__backdrop {
|
|
145
|
+
background: rgba(0, 0, 0, 0.45);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.tz-cal__weekday,
|
|
149
|
+
.tz-range__weekday,
|
|
150
|
+
.tz-dtr__legend,
|
|
151
|
+
.tz-daily__legend {
|
|
152
|
+
color: var(--tz-fg-muted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Windows high contrast and other forced-colour modes replace every colour
|
|
157
|
+
* with the system's, backgrounds included — so a selection shown only by its
|
|
158
|
+
* background would vanish. System colours keep it visible.
|
|
159
|
+
*/
|
|
160
|
+
@media (forced-colors: active) {
|
|
161
|
+
.tz-cal__day--selected,
|
|
162
|
+
.tz-cal__coarse-cell--selected,
|
|
163
|
+
.tz-range__day--start,
|
|
164
|
+
.tz-range__day--end,
|
|
165
|
+
.tz-slots__slot--selected {
|
|
166
|
+
forced-color-adjust: none;
|
|
167
|
+
background: Highlight;
|
|
168
|
+
color: HighlightText;
|
|
169
|
+
}
|
|
170
|
+
.tz-cal__day--today {
|
|
171
|
+
outline-color: CanvasText;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
175
|
+
.tz-cal__day,
|
|
176
|
+
.tz-cal__coarse-cell,
|
|
177
|
+
.tz-range__day,
|
|
178
|
+
.tz-slots__slot,
|
|
179
|
+
.tz-field__trigger {
|
|
180
|
+
transition: background-color 120ms ease, border-color 120ms ease;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/tzslot.scss
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
@use 'sass:list';
|
|
2
|
+
@use 'sass:meta';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// The source of @tzslot/theme. tzslot.css is compiled from this file and a
|
|
6
|
+
// test fails when the two drift apart, so there is one theme, not two.
|
|
7
|
+
//
|
|
8
|
+
// Sass is optional for consumers. What it adds over the CSS:
|
|
9
|
+
//
|
|
10
|
+
// // configure the palette at compile time
|
|
11
|
+
// @use '@tzslot/theme/tzslot' with ($accent: (#be123c, #fb7185));
|
|
12
|
+
//
|
|
13
|
+
// // or scope one to a selector, from your own palette variables
|
|
14
|
+
// @use '@tzslot/theme/tzslot' as tz;
|
|
15
|
+
// .brand-card { @include tz.palette((accent: $brand, accent-fg: #fff)); }
|
|
16
|
+
//
|
|
17
|
+
// A colour is either one value, used in both schemes, or a pair
|
|
18
|
+
// (light, dark) that becomes light-dark(light, dark).
|
|
19
|
+
//
|
|
20
|
+
// Everything still ends up as --tz-* custom properties: that is what lets a
|
|
21
|
+
// theme change at runtime, per element, which Sass variables alone cannot do.
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
// ── palette ──────────────────────────────────────────────────────────────────
|
|
25
|
+
$bg: (#ffffff, #18181b) !default;
|
|
26
|
+
$bg-raised: (#f4f4f5, #27272a) !default;
|
|
27
|
+
$fg: (#18181b, #f4f4f5) !default;
|
|
28
|
+
$fg-muted: (#52525b, #a1a1aa) !default;
|
|
29
|
+
$border: (#d4d4d8, #3f3f46) !default;
|
|
30
|
+
$accent: (#2563eb, #60a5fa) !default;
|
|
31
|
+
$accent-fg: (#ffffff, #0b1220) !default;
|
|
32
|
+
$danger: (#b91c1c, #f87171) !default;
|
|
33
|
+
$warning: (#b45309, #fbbf24) !default;
|
|
34
|
+
|
|
35
|
+
$font: inherit !default;
|
|
36
|
+
|
|
37
|
+
// ── shape and size ───────────────────────────────────────────────────────────
|
|
38
|
+
$sizes: (
|
|
39
|
+
radius: 0.375rem,
|
|
40
|
+
gap: 0.25rem,
|
|
41
|
+
cal-cell-size: 2.25rem,
|
|
42
|
+
cal-weekday-size: 0.6875rem,
|
|
43
|
+
cal-outside-opacity: 0.32,
|
|
44
|
+
slot-gap: 0.375rem,
|
|
45
|
+
slot-padding: 0.4375rem 0.25rem,
|
|
46
|
+
slot-note-size: 0.6875rem,
|
|
47
|
+
field-width: 13rem,
|
|
48
|
+
dtr-gap: 1.25rem,
|
|
49
|
+
dtr-leg-min: 15rem,
|
|
50
|
+
dtr-legend-size: 0.6875rem,
|
|
51
|
+
dtr-summary-size: 1.375rem,
|
|
52
|
+
) !default;
|
|
53
|
+
|
|
54
|
+
/// False to load only the functions and mixins, with no CSS emitted.
|
|
55
|
+
$emit: true !default;
|
|
56
|
+
|
|
57
|
+
/// A single colour, or a (light, dark) pair turned into light-dark().
|
|
58
|
+
@function value($colour) {
|
|
59
|
+
@if meta.type-of($colour) == 'list' and list.length($colour) == 2 {
|
|
60
|
+
@return light-dark(list.nth($colour, 1), list.nth($colour, 2));
|
|
61
|
+
}
|
|
62
|
+
@return $colour;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Sets palette variables on the current selector. Keys are the names without
|
|
66
|
+
/// the prefix: (accent: #e11d48) sets --tz-accent.
|
|
67
|
+
@mixin palette($colours) {
|
|
68
|
+
@each $name, $colour in $colours {
|
|
69
|
+
--tz-#{$name}: #{value($colour)};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@if $emit {
|
|
74
|
+
/**
|
|
75
|
+
* @tzslot/theme — one import and the widgets are presentable.
|
|
76
|
+
*
|
|
77
|
+
* import '@tzslot/theme'; // plain CSS
|
|
78
|
+
* @use '@tzslot/theme/tzslot' with ($accent: #e11d48, $accent-fg: #fff);
|
|
79
|
+
*
|
|
80
|
+
* Generated from tzslot.scss — edit that file, then `npm run build:theme`.
|
|
81
|
+
*
|
|
82
|
+
* Optional by design. The widgets carry structural class names and read their
|
|
83
|
+
* colours from custom properties; this file only sets those properties and a
|
|
84
|
+
* handful of interaction rules. Replacing it means writing your own values for
|
|
85
|
+
* the same names, not fighting selectors.
|
|
86
|
+
*
|
|
87
|
+
* ── How it is built ──────────────────────────────────────────────────────────
|
|
88
|
+
*
|
|
89
|
+
* One palette, nine colours, each written light-dark(light, dark). Which half
|
|
90
|
+
* applies is decided by `color-scheme` at the element that uses the colour, so:
|
|
91
|
+
*
|
|
92
|
+
* - by default the widgets follow the system;
|
|
93
|
+
* - data-theme="dark" or "light" on ANY element forces it for everything
|
|
94
|
+
* inside — a dark card on a light page is one attribute;
|
|
95
|
+
* - there is no second copy of the dark values to keep in step.
|
|
96
|
+
*
|
|
97
|
+
* Nothing derived is declared here. A variable like "the selected day's
|
|
98
|
+
* background" defaults to the accent at the place it is drawn, not on :root —
|
|
99
|
+
* a custom property that refers to another is frozen where it is declared, and
|
|
100
|
+
* declaring it on :root would stop a card's own --tz-accent from reaching it.
|
|
101
|
+
* Every such default lives in the widgets' base CSS as a var() fallback.
|
|
102
|
+
*
|
|
103
|
+
* Contrast: every text/background pair meets WCAG AA at the sizes used.
|
|
104
|
+
* light-dark() needs Chrome 123, Safari 17.5 or Firefox 120 (all of 2024).
|
|
105
|
+
*/
|
|
106
|
+
:root {
|
|
107
|
+
@include palette(
|
|
108
|
+
(
|
|
109
|
+
bg: $bg,
|
|
110
|
+
bg-raised: $bg-raised,
|
|
111
|
+
fg: $fg,
|
|
112
|
+
fg-muted: $fg-muted,
|
|
113
|
+
border: $border,
|
|
114
|
+
accent: $accent,
|
|
115
|
+
accent-fg: $accent-fg,
|
|
116
|
+
danger: $danger,
|
|
117
|
+
warning: $warning,
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The widgets inherit the page's font by default, which is almost always
|
|
123
|
+
* right. Set this only to make them differ from their surroundings.
|
|
124
|
+
*/
|
|
125
|
+
--tz-font: #{$font};
|
|
126
|
+
|
|
127
|
+
@each $name, $size in $sizes {
|
|
128
|
+
--tz-#{$name}: #{$size};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Which half of every light-dark() applies. Declared on the widgets rather than
|
|
134
|
+
* the page, so importing the theme never changes the page's own form controls
|
|
135
|
+
* and scrollbars. --tz-color-scheme is inherited, which is how data-theme on
|
|
136
|
+
* any ancestor reaches them.
|
|
137
|
+
*/
|
|
138
|
+
.tz-cal,
|
|
139
|
+
.tz-range,
|
|
140
|
+
.tz-slots,
|
|
141
|
+
.tz-field,
|
|
142
|
+
.tz-field__panel,
|
|
143
|
+
.tz-field__backdrop,
|
|
144
|
+
.tz-dtr,
|
|
145
|
+
.tz-daily {
|
|
146
|
+
color-scheme: var(--tz-color-scheme, light dark);
|
|
147
|
+
color: var(--tz-fg);
|
|
148
|
+
font: var(--tz-font);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[data-theme='light'] { --tz-color-scheme: light; }
|
|
152
|
+
[data-theme='dark'] { --tz-color-scheme: dark; }
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The rules the variables cannot express: hover, focus, the panel surface.
|
|
156
|
+
* Kept short — every rule here is one a consumer may have to override.
|
|
157
|
+
*/
|
|
158
|
+
.tz-cal__day:hover:not(:disabled),
|
|
159
|
+
.tz-cal__coarse-cell:hover:not(:disabled),
|
|
160
|
+
.tz-range__day:hover:not(:disabled),
|
|
161
|
+
.tz-cal__nav:hover:not(:disabled),
|
|
162
|
+
.tz-cal__title:hover:not(:disabled),
|
|
163
|
+
.tz-cal__action:hover:not(:disabled),
|
|
164
|
+
.tz-range__nav:hover:not(:disabled) {
|
|
165
|
+
background: var(--tz-bg-raised);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Hovering the selection must not hide it. */
|
|
169
|
+
.tz-cal__day--selected:hover:not(:disabled),
|
|
170
|
+
.tz-cal__coarse-cell--selected:hover:not(:disabled),
|
|
171
|
+
.tz-range__day--start:hover:not(:disabled),
|
|
172
|
+
.tz-range__day--end:hover:not(:disabled) {
|
|
173
|
+
background: color-mix(in srgb, var(--tz-accent) 85%, var(--tz-fg));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.tz-slots__slot:hover:not(:disabled),
|
|
177
|
+
.tz-field__trigger:hover:not(:disabled) {
|
|
178
|
+
border-color: var(--tz-accent);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Focus must be visible on every control, including those inside the panel —
|
|
182
|
+
a keyboard user who cannot see where they are is stuck. */
|
|
183
|
+
.tz-cal__day:focus-visible,
|
|
184
|
+
.tz-cal__coarse-cell:focus-visible,
|
|
185
|
+
.tz-cal__title:focus-visible,
|
|
186
|
+
.tz-range__day:focus-visible,
|
|
187
|
+
.tz-cal__nav:focus-visible,
|
|
188
|
+
.tz-cal__action:focus-visible,
|
|
189
|
+
.tz-range__nav:focus-visible,
|
|
190
|
+
.tz-slots__slot:focus-visible,
|
|
191
|
+
.tz-field__trigger:focus-visible {
|
|
192
|
+
outline: var(--tz-focus-ring, 2px solid var(--tz-accent));
|
|
193
|
+
outline-offset: 2px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.tz-field__panel {
|
|
197
|
+
border: 1px solid var(--tz-border);
|
|
198
|
+
border-radius: calc(var(--tz-radius) * 1.5);
|
|
199
|
+
background: var(--tz-bg);
|
|
200
|
+
box-shadow: 0 10px 30px -12px rgb(0 0 0 / 0.35);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.tz-field__panel--dialog {
|
|
204
|
+
box-shadow: 0 25px 60px -15px rgb(0 0 0 / 0.5);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.tz-field__backdrop {
|
|
208
|
+
background: rgb(0 0 0 / 0.45);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.tz-cal__weekday,
|
|
212
|
+
.tz-range__weekday,
|
|
213
|
+
.tz-dtr__legend,
|
|
214
|
+
.tz-daily__legend {
|
|
215
|
+
color: var(--tz-fg-muted);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Windows high contrast and other forced-colour modes replace every colour
|
|
220
|
+
* with the system's, backgrounds included — so a selection shown only by its
|
|
221
|
+
* background would vanish. System colours keep it visible.
|
|
222
|
+
*/
|
|
223
|
+
@media (forced-colors: active) {
|
|
224
|
+
.tz-cal__day--selected,
|
|
225
|
+
.tz-cal__coarse-cell--selected,
|
|
226
|
+
.tz-range__day--start,
|
|
227
|
+
.tz-range__day--end,
|
|
228
|
+
.tz-slots__slot--selected {
|
|
229
|
+
forced-color-adjust: none;
|
|
230
|
+
background: Highlight;
|
|
231
|
+
color: HighlightText;
|
|
232
|
+
}
|
|
233
|
+
.tz-cal__day--today { outline-color: CanvasText; }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
237
|
+
.tz-cal__day,
|
|
238
|
+
.tz-cal__coarse-cell,
|
|
239
|
+
.tz-range__day,
|
|
240
|
+
.tz-slots__slot,
|
|
241
|
+
.tz-field__trigger {
|
|
242
|
+
transition:
|
|
243
|
+
background-color 120ms ease,
|
|
244
|
+
border-color 120ms ease;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|