@aiaiai-pt/design-system 0.4.4 → 0.5.1

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.
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Shared utilities for map components.
3
+ * Not exported from index.js — internal to map components only.
4
+ */
5
+
6
+ /**
7
+ * @typedef {{ type: 'osm' }} OsmSource
8
+ * @typedef {{ type: 'xyz', url: string, attributions?: string, maxZoom?: number }} XyzSource
9
+ * @typedef {{ type: 'stadia', layer: string, retina?: boolean, apiKey?: string }} StadiaSource
10
+ * @typedef {OsmSource | XyzSource | StadiaSource} TileSourceConfig
11
+ */
12
+
13
+ /**
14
+ * Creates an OL tile layer from a config object.
15
+ *
16
+ * @param {TileSourceConfig} config
17
+ * @returns {Promise<import('ol/layer/Tile.js').default>}
18
+ */
19
+ export async function createTileLayer(config) {
20
+ const { default: TileLayer } = await import("ol/layer/Tile.js");
21
+
22
+ if (config.type === "stadia") {
23
+ const { default: StadiaMaps } = await import("ol/source/StadiaMaps.js");
24
+ return new TileLayer({
25
+ source: new StadiaMaps({
26
+ layer: config.layer,
27
+ retina: config.retina ?? true,
28
+ apiKey: config.apiKey,
29
+ }),
30
+ });
31
+ }
32
+
33
+ if (config.type === "xyz") {
34
+ const { default: XYZ } = await import("ol/source/XYZ.js");
35
+ return new TileLayer({
36
+ source: new XYZ({
37
+ url: config.url,
38
+ attributions: config.attributions,
39
+ maxZoom: config.maxZoom,
40
+ crossOrigin: "anonymous",
41
+ }),
42
+ });
43
+ }
44
+
45
+ const { default: OSM } = await import("ol/source/OSM.js");
46
+ return new TileLayer({ source: new OSM() });
47
+ }
48
+
49
+ // ─── Theme-Reactive Style Factory ────────────────────────────────
50
+
51
+ /**
52
+ * @typedef {object} MapStyles
53
+ * @property {import('ol/style/Style.js').default} marker
54
+ * @property {import('ol/style/Style.js').default} polygon
55
+ * @property {(size: number) => import('ol/style/Style.js').default} cluster
56
+ * @property {() => void} refresh — re-read CSS tokens, mutate styles in place
57
+ */
58
+
59
+ /**
60
+ * Creates OL styles from DS tokens. Returns mutable style objects
61
+ * with a refresh() method that re-reads CSS vars and updates the
62
+ * styles in place — call after theme change, then source.changed().
63
+ *
64
+ * @param {Element} el
65
+ * @returns {Promise<MapStyles>}
66
+ */
67
+ export async function createMapStyles(el) {
68
+ const [
69
+ { default: Style },
70
+ { default: CircleStyle },
71
+ { default: Fill },
72
+ { default: Stroke },
73
+ { default: Text },
74
+ ] = await Promise.all([
75
+ import("ol/style/Style.js"),
76
+ import("ol/style/Circle.js"),
77
+ import("ol/style/Fill.js"),
78
+ import("ol/style/Stroke.js"),
79
+ import("ol/style/Text.js"),
80
+ ]);
81
+
82
+ // Mutable fill/stroke refs — mutated in-place on refresh()
83
+ const markerFillObj = new Fill({ color: "" });
84
+ const markerStrokeObj = new Stroke({ color: "", width: 0 });
85
+ const polyFillObj = new Fill({ color: "" });
86
+ const polyStrokeObj = new Stroke({ color: "", width: 0 });
87
+
88
+ let markerRadiusVal = 8;
89
+ let clusterBaseRadiusVal = 16;
90
+ let clusterFillColor = "";
91
+ let clusterTextColor = "";
92
+ let clusterFontStr = "";
93
+
94
+ const marker = new Style({
95
+ image: new CircleStyle({
96
+ radius: 8,
97
+ fill: markerFillObj,
98
+ stroke: markerStrokeObj,
99
+ }),
100
+ });
101
+
102
+ const polygon = new Style({
103
+ fill: polyFillObj,
104
+ stroke: polyStrokeObj,
105
+ });
106
+
107
+ /** @type {Map<string, import('ol/style/Style.js').default>} */
108
+ const clusterCache = new Map();
109
+
110
+ function readTokens() {
111
+ markerFillObj.setColor(cssVar(el, "--map-marker-fill", "#ff6b35"));
112
+ markerStrokeObj.setColor(cssVar(el, "--map-marker-stroke", "#fff"));
113
+ markerStrokeObj.setWidth(cssPx(el, "--map-marker-stroke-width", 2));
114
+ markerRadiusVal = cssPx(el, "--map-marker-radius", 8);
115
+ polyFillObj.setColor(
116
+ cssVar(el, "--map-polygon-fill", "rgba(255,107,53,0.2)"),
117
+ );
118
+ polyStrokeObj.setColor(cssVar(el, "--map-polygon-stroke", "#ff6b35"));
119
+ polyStrokeObj.setWidth(cssPx(el, "--map-polygon-stroke-width", 2));
120
+ clusterBaseRadiusVal = cssPx(el, "--map-cluster-radius", 16);
121
+ clusterFillColor = cssVar(el, "--map-cluster-fill", "#ff6b35");
122
+ clusterTextColor = cssVar(el, "--map-cluster-text-fill", "#fff");
123
+ const font = cssVar(el, "--map-cluster-font", "monospace");
124
+ const fontSize = cssVar(el, "--map-cluster-font-size", "12px");
125
+ clusterFontStr = `600 ${fontSize} ${font}`;
126
+
127
+ // Rebuild marker image with new radius (CircleStyle radius is immutable)
128
+ marker.setImage(
129
+ new CircleStyle({
130
+ radius: markerRadiusVal,
131
+ fill: markerFillObj,
132
+ stroke: markerStrokeObj,
133
+ }),
134
+ );
135
+ }
136
+
137
+ // Initial read
138
+ readTokens();
139
+
140
+ /** @param {number} size */
141
+ function cluster(size) {
142
+ const key = `c-${size}`;
143
+ let cached = clusterCache.get(key);
144
+ if (cached) return cached;
145
+
146
+ if (size === 1) {
147
+ clusterCache.set(key, marker);
148
+ return marker;
149
+ }
150
+
151
+ const style = new Style({
152
+ image: new CircleStyle({
153
+ radius: clusterBaseRadiusVal + Math.min(size, 20),
154
+ fill: new Fill({ color: clusterFillColor }),
155
+ }),
156
+ text: new Text({
157
+ text: String(size),
158
+ fill: new Fill({ color: clusterTextColor }),
159
+ font: clusterFontStr,
160
+ }),
161
+ });
162
+
163
+ clusterCache.set(key, style);
164
+ return style;
165
+ }
166
+
167
+ function refresh() {
168
+ readTokens();
169
+ clusterCache.clear(); // cluster styles must be rebuilt with new colors
170
+ }
171
+
172
+ return { marker, polygon, cluster, refresh };
173
+ }
174
+
175
+ // ─── Theme Watcher ───────────────────────────────────────────────
176
+
177
+ /**
178
+ * Watches for theme changes (data-theme attribute, class changes on <html>,
179
+ * OS prefers-color-scheme). Calls onchange when a theme switch is detected.
180
+ * Returns a dispose function.
181
+ *
182
+ * @param {() => void} onchange
183
+ * @returns {() => void} dispose
184
+ */
185
+ export function watchTheme(onchange) {
186
+ const mo = new MutationObserver(() => onchange());
187
+ mo.observe(document.documentElement, {
188
+ attributes: true,
189
+ attributeFilter: ["data-theme", "class"],
190
+ });
191
+
192
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
193
+ const handler = () => onchange();
194
+ mql.addEventListener("change", handler);
195
+
196
+ return () => {
197
+ mo.disconnect();
198
+ mql.removeEventListener("change", handler);
199
+ };
200
+ }
201
+
202
+ // ─── Error Rendering ─────────────────────────────────────────────
203
+
204
+ /**
205
+ * Renders a visible error state inside a map container.
206
+ *
207
+ * @param {HTMLElement} el
208
+ * @param {string} component
209
+ * @param {Error} error
210
+ */
211
+ export function renderMapError(el, component, error) {
212
+ const msg = error.message || String(error);
213
+ const isModuleError =
214
+ msg.includes("Failed to fetch") ||
215
+ msg.includes("Module not found") ||
216
+ msg.includes("Cannot find module");
217
+
218
+ el.innerHTML = "";
219
+ const errorEl = document.createElement("div");
220
+ errorEl.setAttribute("role", "alert");
221
+ errorEl.style.cssText = `
222
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
223
+ height: 100%; min-height: 120px; padding: 24px; text-align: center;
224
+ font-family: var(--type-body-sm-font, sans-serif); font-size: var(--type-body-sm-size, 13px);
225
+ color: var(--color-text-muted, #888); background: var(--color-surface-secondary, #f5f5f5);
226
+ border-radius: inherit;
227
+ `;
228
+
229
+ const title = document.createElement("strong");
230
+ title.style.cssText = `
231
+ display: block; margin-bottom: 4px;
232
+ font-family: var(--type-label-font, monospace); font-size: var(--type-label-size, 11px);
233
+ letter-spacing: 0.05em; color: var(--color-destructive, #c00);
234
+ `;
235
+ title.textContent = `${component} FAILED`;
236
+
237
+ const detail = document.createElement("span");
238
+ detail.textContent = isModuleError
239
+ ? "OpenLayers (ol) is not installed. Add it to your dependencies."
240
+ : msg;
241
+
242
+ errorEl.appendChild(title);
243
+ errorEl.appendChild(detail);
244
+ el.appendChild(errorEl);
245
+
246
+ console.error(`[${component}]`, error);
247
+ }
248
+
249
+ // ─── CSS Helpers ─────────────────────────────────────────────────
250
+
251
+ /**
252
+ * @param {Element} el
253
+ * @param {string} prop
254
+ * @param {string} fallback
255
+ * @returns {string}
256
+ */
257
+ export function cssVar(el, prop, fallback) {
258
+ const val = getComputedStyle(el).getPropertyValue(prop).trim();
259
+ return val || fallback;
260
+ }
261
+
262
+ /**
263
+ * Resolves a CSS custom property to a pixel number via probe element.
264
+ *
265
+ * @param {Element} el
266
+ * @param {string} prop
267
+ * @param {number} fallback
268
+ * @returns {number}
269
+ */
270
+ export function cssPx(el, prop, fallback) {
271
+ const raw = getComputedStyle(el).getPropertyValue(prop).trim();
272
+ if (!raw) return fallback;
273
+
274
+ const probe = document.createElement("div");
275
+ probe.style.cssText = `position:absolute;visibility:hidden;width:${raw}`;
276
+ el.appendChild(probe);
277
+ const px = probe.offsetWidth;
278
+ el.removeChild(probe);
279
+ return px || fallback;
280
+ }
281
+
282
+ /**
283
+ * Resolves any CSS color expression (hex, rgb, color-mix, var refs)
284
+ * to a concrete rgb()/rgba() string via a probe element.
285
+ * Required because getComputedStyle doesn't resolve color-mix() on
286
+ * custom properties — only on applied styles like backgroundColor.
287
+ *
288
+ * @param {Element} el
289
+ * @param {string} value — raw CSS color expression
290
+ * @returns {string} — resolved rgb()/rgba() string
291
+ */
292
+ function resolveColor(el, value) {
293
+ const probe = document.createElement("div");
294
+ probe.style.cssText = `position:absolute;visibility:hidden;background-color:${value}`;
295
+ el.appendChild(probe);
296
+ const resolved = getComputedStyle(probe).backgroundColor;
297
+ el.removeChild(probe);
298
+ return resolved || value;
299
+ }
300
+
301
+ /**
302
+ * Extracts RGB components from a resolved color string.
303
+ * Handles rgb(r, g, b) and rgba(r, g, b, a) formats.
304
+ *
305
+ * @param {string} color
306
+ * @returns {{ r: number, g: number, b: number }}
307
+ */
308
+ function parseRgb(color) {
309
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
310
+ if (match) return { r: +match[1], g: +match[2], b: +match[3] };
311
+ return { r: 254, g: 228, b: 142 };
312
+ }
313
+
314
+ /**
315
+ * Reads heatmap gradient stops from CSS custom properties.
316
+ * Resolves color-mix() and other CSS color expressions to concrete
317
+ * rgb values that OL's canvas gradient can consume.
318
+ *
319
+ * @param {Element} el
320
+ * @returns {string[]}
321
+ */
322
+ export function getHeatmapGradient(el) {
323
+ const styles = getComputedStyle(el);
324
+ const rawStops = [
325
+ styles.getPropertyValue("--map-heatmap-stop-1").trim(),
326
+ styles.getPropertyValue("--map-heatmap-stop-2").trim(),
327
+ styles.getPropertyValue("--map-heatmap-stop-3").trim(),
328
+ styles.getPropertyValue("--map-heatmap-stop-4").trim(),
329
+ ].filter(Boolean);
330
+
331
+ if (rawStops.length >= 2) {
332
+ const stops = rawStops.map((s) => resolveColor(el, s));
333
+ const { r, g, b } = parseRgb(stops[0]);
334
+ return [`rgba(${r}, ${g}, ${b}, 0)`, ...stops];
335
+ }
336
+
337
+ return ["rgba(251, 227, 142, 0)", "#fbe38e", "#fb923c", "#e85a28", "#ae2a1e"];
338
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.4.4",
3
+ "version": "0.5.1",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,9 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
+ "workspaces": [
15
+ "site"
16
+ ],
14
17
  "scripts": {
15
18
  "build:types": "svelte-package --input . --output dist && cp dist/components/*.d.ts components/ && rm -rf dist",
16
19
  "prepublishOnly": "npm run build:types"
@@ -51,10 +54,14 @@
51
54
  "@codemirror/state": "^6.6.0",
52
55
  "@codemirror/view": "^6.40.0",
53
56
  "@lezer/highlight": "^1.0.0",
57
+ "date-fns": "^4.1.0",
58
+ "ol": "^10.0.0",
54
59
  "svelte": "^5.0.0"
55
60
  },
56
61
  "devDependencies": {
57
62
  "@sveltejs/package": "^2.5.7",
63
+ "date-fns": "^4.1.0",
64
+ "ol": "^10.8.0",
58
65
  "svelte": "^5.55.3",
59
66
  "svelte-check": "^4.4.6",
60
67
  "typescript": "^5.9.3"
@@ -654,6 +654,221 @@
654
654
  --palette-highlight-color: var(--color-accent);
655
655
  --palette-transition: var(--duration-normal) var(--easing-enter);
656
656
 
657
+ /* ═══════════════════════════════════════════════
658
+ DATEPICKER
659
+ ═══════════════════════════════════════════════ */
660
+
661
+ --datepicker-calendar-padding: var(--space-sm);
662
+
663
+ /* Navigation bar */
664
+ --datepicker-nav-font: var(--type-label-font);
665
+ --datepicker-nav-size: var(--type-label-size);
666
+ --datepicker-nav-tracking: var(--type-label-tracking);
667
+ --datepicker-nav-color: var(--color-text);
668
+ --datepicker-nav-btn-size: var(--button-sm-height);
669
+ --datepicker-nav-btn-radius: var(--radius-sm);
670
+ --datepicker-nav-btn-hover-bg: var(--color-surface-secondary);
671
+
672
+ /* Weekday headers */
673
+ --datepicker-weekday-font: var(--type-label-font);
674
+ --datepicker-weekday-size: var(--type-caption-size);
675
+ --datepicker-weekday-tracking: var(--type-caption-tracking);
676
+ --datepicker-weekday-color: var(--color-text-muted);
677
+
678
+ /* Day cells */
679
+ --datepicker-day-size: var(--button-md-height);
680
+ --datepicker-day-font: var(--type-data-font);
681
+ --datepicker-day-font-size: var(--type-body-sm-size);
682
+ --datepicker-day-radius: var(--radius-sm);
683
+ --datepicker-day-hover-bg: var(--color-surface-secondary);
684
+ --datepicker-day-selected-bg: var(--color-accent);
685
+ --datepicker-day-selected-text: var(--color-text-on-accent);
686
+ --datepicker-day-today-border: var(--border-width) solid
687
+ var(--color-border-strong);
688
+ --datepicker-day-outside-opacity: 0.3;
689
+ --datepicker-day-disabled-opacity: 0.2;
690
+
691
+ /* Trigger (inherits --input-* for visual consistency) */
692
+ --datepicker-icon-size: var(--icon-size-sm);
693
+ --datepicker-icon-color: var(--color-text-muted);
694
+ --datepicker-placeholder-color: var(--input-placeholder);
695
+
696
+ /* Range selection (DateRangePicker) */
697
+ --datepicker-range-bg: var(--color-accent-subtle);
698
+ --datepicker-range-hover-bg: var(--color-surface-tertiary);
699
+ --datepicker-range-endpoint-bg: var(--color-accent);
700
+ --datepicker-range-endpoint-text: var(--color-text-on-accent);
701
+
702
+ /* ═══════════════════════════════════════════════
703
+ STAT CARD
704
+ ═══════════════════════════════════════════════ */
705
+
706
+ --stat-padding: var(--space-lg);
707
+ --stat-radius: var(--radius-md);
708
+ --stat-border: var(--elevation-border);
709
+ --stat-bg: var(--color-surface);
710
+
711
+ /* Value (large number) */
712
+ --stat-value-font: var(--type-display-font);
713
+ --stat-value-size: var(--type-display-size);
714
+ --stat-value-weight: var(--type-display-weight);
715
+ --stat-value-tracking: var(--type-display-tracking);
716
+
717
+ /* Label */
718
+ --stat-label-font: var(--type-label-font);
719
+ --stat-label-size: var(--type-label-size);
720
+ --stat-label-tracking: var(--type-label-tracking);
721
+ --stat-label-color: var(--color-text-muted);
722
+
723
+ /* Trend */
724
+ --stat-trend-font: var(--type-body-sm-font);
725
+ --stat-trend-size: var(--type-body-sm-size);
726
+ --stat-trend-up-color: var(--color-success);
727
+ --stat-trend-down-color: var(--color-destructive);
728
+ --stat-trend-neutral-color: var(--color-text-muted);
729
+
730
+ /* Icon */
731
+ --stat-icon-size: var(--icon-size-lg);
732
+ --stat-icon-color: var(--color-text-muted);
733
+
734
+ /* Grid */
735
+ --stat-grid-gap: var(--space-md);
736
+
737
+ /* ═══════════════════════════════════════════════
738
+ MAP
739
+ ═══════════════════════════════════════════════ */
740
+
741
+ --map-radius: var(--radius-md);
742
+ --map-border: var(--elevation-border);
743
+
744
+ /* Marker */
745
+ --map-marker-radius: var(--space-sm);
746
+ --map-marker-fill: var(--color-accent);
747
+ --map-marker-stroke: var(--color-surface);
748
+ --map-marker-stroke-width: var(--border-width-thick);
749
+
750
+ /* Cluster */
751
+ --map-cluster-radius: var(--space-md);
752
+ --map-cluster-fill: var(--color-accent);
753
+ --map-cluster-text-fill: var(--color-text-on-accent);
754
+ --map-cluster-font: var(--type-label-font);
755
+ --map-cluster-font-size: var(--type-label-size);
756
+
757
+ /* Polygon / draw */
758
+ --map-polygon-fill: color-mix(in srgb, var(--color-accent) 20%, transparent);
759
+ --map-polygon-stroke: var(--color-accent);
760
+ --map-polygon-stroke-width: var(--border-width-thick);
761
+
762
+ /* Heatmap (accent-derived sequential ramp — follows theme) */
763
+ --map-heatmap-stop-1: var(--color-accent-subtle);
764
+ --map-heatmap-stop-2: color-mix(
765
+ in srgb,
766
+ var(--color-accent) 60%,
767
+ var(--color-accent-subtle)
768
+ );
769
+ --map-heatmap-stop-3: var(--color-accent);
770
+ --map-heatmap-stop-4: var(--color-accent-hover);
771
+
772
+ /* Popup (inherits card tokens) */
773
+ --map-popup-bg: var(--card-bg);
774
+ --map-popup-border: var(--elevation-border-strong);
775
+ --map-popup-radius: var(--card-radius);
776
+ --map-popup-padding: var(--space-sm);
777
+ --map-popup-shadow: var(--elevation-overlay);
778
+ --map-popup-arrow-size: var(--space-sm);
779
+ --map-popup-max-width: var(--content-width-narrow);
780
+
781
+ /* Draw mode toolbar */
782
+ --map-toolbar-bg: var(--color-surface);
783
+ --map-toolbar-border: var(--elevation-border);
784
+ --map-toolbar-radius: var(--radius-sm);
785
+ --map-toolbar-padding: var(--space-2xs);
786
+
787
+ /* ═══════════════════════════════════════════════
788
+ CALENDAR
789
+ ═══════════════════════════════════════════════ */
790
+
791
+ /* Container */
792
+ --calendar-bg: var(--color-surface);
793
+ --calendar-border: var(--elevation-border);
794
+ --calendar-radius: var(--radius-md);
795
+ --calendar-padding: var(--space-md);
796
+
797
+ /* Toolbar */
798
+ --calendar-toolbar-gap: var(--space-sm);
799
+ --calendar-title-font: var(--type-heading-font);
800
+ --calendar-title-size: var(--type-heading-size);
801
+ --calendar-title-weight: var(--type-heading-weight);
802
+ --calendar-title-tracking: var(--type-heading-tracking);
803
+ --calendar-title-color: var(--color-text);
804
+
805
+ /* View toggle */
806
+ --calendar-toggle-font: var(--type-label-font);
807
+ --calendar-toggle-size: var(--type-label-size);
808
+ --calendar-toggle-tracking: var(--type-label-tracking);
809
+ --calendar-toggle-padding: var(--space-2xs) var(--space-sm);
810
+ --calendar-toggle-radius: var(--radius-sm);
811
+ --calendar-toggle-color: var(--color-text-secondary);
812
+ --calendar-toggle-hover-bg: var(--color-surface-secondary);
813
+ --calendar-toggle-active-bg: var(--color-accent);
814
+ --calendar-toggle-active-text: var(--color-text-on-accent);
815
+
816
+ /* Navigation buttons */
817
+ --calendar-nav-btn-size: var(--button-sm-height);
818
+ --calendar-nav-btn-radius: var(--radius-sm);
819
+ --calendar-nav-btn-hover-bg: var(--color-surface-secondary);
820
+
821
+ /* Weekday headers */
822
+ --calendar-weekday-font: var(--type-label-font);
823
+ --calendar-weekday-size: var(--type-caption-size);
824
+ --calendar-weekday-tracking: var(--type-caption-tracking);
825
+ --calendar-weekday-color: var(--color-text-muted);
826
+ --calendar-weekday-height: var(--space-xl);
827
+
828
+ /* Month cell */
829
+ --calendar-cell-min-height: var(--space-4xl);
830
+ --calendar-cell-border: var(--border-width) solid var(--color-border);
831
+ --calendar-cell-padding: var(--space-2xs);
832
+ --calendar-cell-hover-bg: var(--color-surface-secondary);
833
+ --calendar-cell-today-bg: var(--color-accent-subtle);
834
+
835
+ /* Day number */
836
+ --calendar-day-font: var(--type-data-font);
837
+ --calendar-day-size: var(--type-body-sm-size);
838
+ --calendar-day-color: var(--color-text);
839
+ --calendar-day-outside-opacity: 0.3;
840
+ --calendar-day-today-size: var(--space-lg);
841
+ --calendar-day-today-bg: var(--color-accent);
842
+ --calendar-day-today-text: var(--color-text-on-accent);
843
+
844
+ /* Time gutter (week/day views) */
845
+ --calendar-time-font: var(--type-data-font);
846
+ --calendar-time-size: var(--type-caption-size);
847
+ --calendar-time-color: var(--color-text-muted);
848
+ --calendar-time-width: var(--space-3xl);
849
+
850
+ /* Hour slot (week/day views) */
851
+ --calendar-slot-height: var(--space-2xl);
852
+ --calendar-slot-border: var(--border-width) solid var(--color-border);
853
+
854
+ /* Now indicator */
855
+ --calendar-now-color: var(--color-destructive);
856
+ --calendar-now-width: var(--border-width-thick);
857
+
858
+ /* Event block */
859
+ --calendar-event-font: var(--type-body-sm-font);
860
+ --calendar-event-size: var(--type-caption-size);
861
+ --calendar-event-weight: var(--type-body-sm-weight);
862
+ --calendar-event-radius: var(--radius-sm);
863
+ --calendar-event-padding: var(--space-2xs) var(--space-xs);
864
+ --calendar-event-default-bg: var(--color-accent);
865
+ --calendar-event-default-text: var(--color-text-on-accent);
866
+
867
+ /* Overflow indicator */
868
+ --calendar-overflow-font: var(--type-caption-font);
869
+ --calendar-overflow-size: var(--type-caption-size);
870
+ --calendar-overflow-color: var(--color-text-muted);
871
+
657
872
  /* ═══════════════════════════════════════════════
658
873
  FOCUS RING (global)
659
874
  ═══════════════════════════════════════════════ */