@nomideusz/svelte-calendar 0.1.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.
Files changed (77) hide show
  1. package/README.md +168 -0
  2. package/dist/adapters/index.d.ts +4 -0
  3. package/dist/adapters/index.js +2 -0
  4. package/dist/adapters/memory.d.ts +14 -0
  5. package/dist/adapters/memory.js +33 -0
  6. package/dist/adapters/rest.d.ts +20 -0
  7. package/dist/adapters/rest.js +48 -0
  8. package/dist/adapters/types.d.ts +23 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/assets/favicon.svg +1 -0
  11. package/dist/calendar/Calendar.svelte +213 -0
  12. package/dist/calendar/Calendar.svelte.d.ts +45 -0
  13. package/dist/calendar/Toolbar.svelte +241 -0
  14. package/dist/calendar/Toolbar.svelte.d.ts +19 -0
  15. package/dist/calendar/index.d.ts +3 -0
  16. package/dist/calendar/index.js +3 -0
  17. package/dist/core/clock.svelte.d.ts +21 -0
  18. package/dist/core/clock.svelte.js +53 -0
  19. package/dist/core/index.d.ts +6 -0
  20. package/dist/core/index.js +8 -0
  21. package/dist/core/locale.d.ts +38 -0
  22. package/dist/core/locale.js +96 -0
  23. package/dist/core/time.d.ts +37 -0
  24. package/dist/core/time.js +68 -0
  25. package/dist/core/types.d.ts +91 -0
  26. package/dist/core/types.js +11 -0
  27. package/dist/engine/drag.svelte.d.ts +31 -0
  28. package/dist/engine/drag.svelte.js +58 -0
  29. package/dist/engine/event-store.svelte.d.ts +47 -0
  30. package/dist/engine/event-store.svelte.js +120 -0
  31. package/dist/engine/index.d.ts +8 -0
  32. package/dist/engine/index.js +5 -0
  33. package/dist/engine/selection.svelte.d.ts +24 -0
  34. package/dist/engine/selection.svelte.js +58 -0
  35. package/dist/engine/view-state.svelte.d.ts +24 -0
  36. package/dist/engine/view-state.svelte.js +74 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.js +14 -0
  39. package/dist/primitives/DayHeader.svelte +103 -0
  40. package/dist/primitives/DayHeader.svelte.d.ts +15 -0
  41. package/dist/primitives/EmptySlot.svelte +101 -0
  42. package/dist/primitives/EmptySlot.svelte.d.ts +16 -0
  43. package/dist/primitives/EventBlock.svelte +241 -0
  44. package/dist/primitives/EventBlock.svelte.d.ts +24 -0
  45. package/dist/primitives/NowIndicator.svelte +175 -0
  46. package/dist/primitives/NowIndicator.svelte.d.ts +22 -0
  47. package/dist/primitives/TimeGutter.svelte +105 -0
  48. package/dist/primitives/TimeGutter.svelte.d.ts +14 -0
  49. package/dist/primitives/index.d.ts +5 -0
  50. package/dist/primitives/index.js +6 -0
  51. package/dist/theme/index.d.ts +2 -0
  52. package/dist/theme/index.js +2 -0
  53. package/dist/theme/presets.d.ts +24 -0
  54. package/dist/theme/presets.js +89 -0
  55. package/dist/views/agenda/Agenda.svelte +1404 -0
  56. package/dist/views/agenda/Agenda.svelte.d.ts +20 -0
  57. package/dist/views/agenda/index.d.ts +1 -0
  58. package/dist/views/agenda/index.js +1 -0
  59. package/dist/views/day/DayGrid.svelte +1063 -0
  60. package/dist/views/day/DayGrid.svelte.d.ts +33 -0
  61. package/dist/views/day/DayTimeline.svelte +505 -0
  62. package/dist/views/day/DayTimeline.svelte.d.ts +4 -0
  63. package/dist/views/day/index.d.ts +2 -0
  64. package/dist/views/day/index.js +2 -0
  65. package/dist/views/index.d.ts +5 -0
  66. package/dist/views/index.js +5 -0
  67. package/dist/views/settings/Settings.svelte +303 -0
  68. package/dist/views/settings/Settings.svelte.d.ts +23 -0
  69. package/dist/views/settings/index.d.ts +2 -0
  70. package/dist/views/settings/index.js +1 -0
  71. package/dist/views/week/WeekGrid.svelte +594 -0
  72. package/dist/views/week/WeekGrid.svelte.d.ts +21 -0
  73. package/dist/views/week/WeekHeatmap.svelte +453 -0
  74. package/dist/views/week/WeekHeatmap.svelte.d.ts +4 -0
  75. package/dist/views/week/index.d.ts +2 -0
  76. package/dist/views/week/index.js +2 -0
  77. package/package.json +62 -0
@@ -0,0 +1,453 @@
1
+ <script lang="ts">
2
+ /**
3
+ * WeekTimelineV6 — "Heatmap"
4
+ *
5
+ * No hour ruler. No event blocks. Just intensity.
6
+ * Each day gets a row of 24 tiny cells (one per hour).
7
+ * Color saturation tells you: busy or free?
8
+ * Click to reveal — events appear in a detail pane.
9
+ *
10
+ * At a glance: the SHAPE of your week's density.
11
+ * GitHub contribution graph, but for your calendar.
12
+ *
13
+ * Aesthetic: Dark void with hot accent for busy. Cool blue
14
+ * for empty. Dense but legible. Data-viz meets calendar.
15
+ */
16
+ import { createClock } from '../../core/clock.svelte.js';
17
+ import type { WeekTimelineProps, TimelineEvent } from '../../core/types.js';
18
+ import { DAY_MS, sod, startOfWeek, dayNum } from '../../core/time.js';
19
+ import { weekdayShort, monthShort, fmtWeekRange, fmtH } from '../../core/locale.js';
20
+
21
+ let {
22
+ weekOffset = 0,
23
+ mondayStart = true,
24
+ height = 400,
25
+ events = [],
26
+ style = '',
27
+ focusDate,
28
+ oneventclick,
29
+ selectedEventId = null,
30
+ }: WeekTimelineProps = $props();
31
+
32
+ const clock = createClock();
33
+
34
+ const weekStartMs = $derived(
35
+ focusDate
36
+ ? startOfWeek(sod(focusDate.getTime()), mondayStart)
37
+ : startOfWeek(clock.today, mondayStart) + weekOffset * 7 * DAY_MS,
38
+ );
39
+ const currentWeekVisible = $derived(clock.today >= weekStartMs && clock.today < weekStartMs + 7 * DAY_MS);
40
+ const weekLabel = $derived(fmtWeekRange(weekStartMs));
41
+
42
+ let selectedCell = $state<{ day: number; hour: number } | null>(null);
43
+
44
+ interface HeatDay {
45
+ ms: number;
46
+ label: string;
47
+ num: number;
48
+ today: boolean;
49
+ past: boolean;
50
+ cells: HeatCell[];
51
+ }
52
+
53
+ interface HeatCell {
54
+ hour: number;
55
+ events: TimelineEvent[];
56
+ intensity: number; // 0–1: fraction of hour covered
57
+ isNow: boolean;
58
+ }
59
+
60
+ const heatDays = $derived.by(() => {
61
+ const out: HeatDay[] = [];
62
+ const currentHour = new Date(clock.tick).getHours();
63
+
64
+ for (let i = 0; i < 7; i++) {
65
+ const ms = weekStartMs + i * DAY_MS;
66
+ const isToday = ms === clock.today;
67
+
68
+ const cells: HeatCell[] = [];
69
+ for (let h = 0; h < 24; h++) {
70
+ const cellStart = ms + h * 3600000;
71
+ const cellEnd = cellStart + 3600000;
72
+ const overlaps = events.filter((ev) => ev.start.getTime() < cellEnd && ev.end.getTime() > cellStart);
73
+
74
+ // Calculate coverage: what fraction of this hour is booked?
75
+ let coveredMs = 0;
76
+ for (const ev of overlaps) {
77
+ const s = Math.max(ev.start.getTime(), cellStart);
78
+ const e = Math.min(ev.end.getTime(), cellEnd);
79
+ coveredMs += e - s;
80
+ }
81
+ const intensity = Math.min(coveredMs / 3600000, 1);
82
+
83
+ cells.push({
84
+ hour: h,
85
+ events: overlaps,
86
+ intensity,
87
+ isNow: isToday && h === currentHour,
88
+ });
89
+ }
90
+
91
+ out.push({
92
+ ms,
93
+ label: weekdayShort(ms),
94
+ num: dayNum(ms),
95
+ today: isToday,
96
+ past: ms < clock.today,
97
+ cells,
98
+ });
99
+ }
100
+ return out;
101
+ });
102
+
103
+ // Selected cell detail
104
+ const selectedEvents = $derived.by(() => {
105
+ if (!selectedCell) return [];
106
+ const day = heatDays[selectedCell.day];
107
+ if (!day) return [];
108
+ return day.cells[selectedCell.hour]?.events ?? [];
109
+ });
110
+
111
+ const selectedLabel = $derived.by(() => {
112
+ if (!selectedCell) return '';
113
+ const day = heatDays[selectedCell.day];
114
+ if (!day) return '';
115
+ return `${day.label} ${day.num}, ${fmtH(selectedCell.hour)}`;
116
+ });
117
+
118
+ function fmtTime(d: Date): string {
119
+ return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase();
120
+ }
121
+
122
+ function duration(ev: TimelineEvent): string {
123
+ const mins = Math.round((ev.end.getTime() - ev.start.getTime()) / 60000);
124
+ if (mins < 60) return `${mins}m`;
125
+ const h = Math.floor(mins / 60);
126
+ const m = mins % 60;
127
+ return m > 0 ? `${h}h${m}m` : `${h}h`;
128
+ }
129
+
130
+ // Intensity → color (cold blue → neutral → hot orange/red)
131
+ function heatColor(intensity: number): string {
132
+ if (intensity === 0) return 'var(--dt-hm-empty)';
133
+ if (intensity < 0.3) return 'var(--dt-hm-low)';
134
+ if (intensity < 0.6) return 'var(--dt-hm-mid)';
135
+ if (intensity < 0.9) return 'var(--dt-hm-high)';
136
+ return 'var(--dt-hm-max)';
137
+ }
138
+
139
+ // Weekly totals
140
+ const busiestDay = $derived.by(() => {
141
+ let max = 0;
142
+ let label = '';
143
+ for (const d of heatDays) {
144
+ const total = d.cells.reduce((s, c) => s + c.intensity, 0);
145
+ if (total > max) {
146
+ max = total;
147
+ label = d.label;
148
+ }
149
+ }
150
+ return label;
151
+ });
152
+
153
+ const totalBusy = $derived(
154
+ Math.round(heatDays.reduce((s, d) => s + d.cells.reduce((cs, c) => cs + c.intensity, 0), 0) * 10) / 10
155
+ );
156
+
157
+ function selectCell(day: number, hour: number) {
158
+ if (selectedCell?.day === day && selectedCell?.hour === hour) {
159
+ selectedCell = null;
160
+ } else {
161
+ selectedCell = { day, hour };
162
+ }
163
+ }
164
+ </script>
165
+
166
+ <div class="hm" style={style || undefined} style:height="{height}px">
167
+ <!-- Header -->
168
+ <div class="hm-head">
169
+ <div class="hm-head-left">
170
+ <span class="hm-week">{weekLabel}</span>
171
+ <span class="hm-clock">{clock.hm}</span>
172
+ </div>
173
+ <div class="hm-head-right">
174
+ <span class="hm-stat">{totalBusy}h booked</span>
175
+ <span class="hm-stat">busiest: {busiestDay}</span>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Hour labels row -->
180
+ <div class="hm-hours-row">
181
+ <div class="hm-row-label"></div>
182
+ {#each Array(24) as _, h}
183
+ {#if h % 3 === 0}
184
+ <span class="hm-hour-mark" style:left="calc({h} * (100% / 24))">{fmtH(h)}</span>
185
+ {/if}
186
+ {/each}
187
+ </div>
188
+
189
+ <!-- Heatmap grid -->
190
+ <div class="hm-grid">
191
+ {#each heatDays as day, di (day.ms)}
192
+ <div class="hm-row" class:hm-row--today={day.today} class:hm-row--past={day.past && !day.today}>
193
+ <div class="hm-row-label">
194
+ <span class="hm-row-day">{day.label}</span>
195
+ <span class="hm-row-num">{day.num}</span>
196
+ </div>
197
+ <div class="hm-cells">
198
+ {#each day.cells as cell, hi}
199
+ <button
200
+ class="hm-cell"
201
+ class:hm-cell--now={cell.isNow}
202
+ class:hm-cell--selected={selectedCell?.day === di && selectedCell?.hour === hi}
203
+ style:background={heatColor(cell.intensity)}
204
+ onclick={() => selectCell(di, hi)}
205
+ title="{fmtH(cell.hour)}: {cell.events.length > 0 ? cell.events.map(e => e.title).join(', ') : 'free'}"
206
+ >
207
+ {#if cell.isNow}
208
+ <span class="hm-cell-now"></span>
209
+ {/if}
210
+ </button>
211
+ {/each}
212
+ </div>
213
+ </div>
214
+ {/each}
215
+ </div>
216
+
217
+ <!-- Detail pane -->
218
+ {#if selectedCell && selectedEvents.length > 0}
219
+ <div class="hm-detail">
220
+ <div class="hm-detail-head">
221
+ <span class="hm-detail-label">{selectedLabel}</span>
222
+ <button class="hm-detail-close" onclick={() => (selectedCell = null)}>✕</button>
223
+ </div>
224
+ {#each selectedEvents as ev (ev.id)}
225
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
226
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
227
+ <div
228
+ class="hm-detail-event"
229
+ class:hm-detail-event--selected={selectedEventId === ev.id}
230
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
231
+ onclick={() => oneventclick?.(ev)}
232
+ >
233
+ <span class="hm-detail-title">{ev.title}</span>
234
+ <span class="hm-detail-time">{fmtTime(ev.start)} — {fmtTime(ev.end)} · {duration(ev)}</span>
235
+ </div>
236
+ {/each}
237
+ </div>
238
+ {/if}
239
+ </div>
240
+
241
+ <style>
242
+ .hm {
243
+ /* Heatmap-specific tokens (fallback defaults) */
244
+ --dt-hm-empty: rgba(80, 80, 120, 0.06);
245
+ --dt-hm-low: rgba(255, 167, 38, 0.18);
246
+ --dt-hm-mid: rgba(255, 120, 50, 0.35);
247
+ --dt-hm-high: rgba(255, 80, 30, 0.55);
248
+ --dt-hm-max: rgba(255, 50, 20, 0.72);
249
+ --dt-glow: rgba(255, 107, 53, 0.6);
250
+
251
+ position: relative;
252
+ background: var(--dt-bg, #0e0e12);
253
+ border: 1px solid var(--dt-border, rgba(200, 200, 240, 0.06));
254
+ border-radius: 12px;
255
+ overflow: hidden;
256
+ user-select: none;
257
+ display: flex;
258
+ flex-direction: column;
259
+ }
260
+
261
+ .hm-head {
262
+ display: flex;
263
+ justify-content: space-between;
264
+ align-items: center;
265
+ padding: 14px 20px 6px;
266
+ flex-shrink: 0;
267
+ }
268
+ .hm-head-left {
269
+ display: flex;
270
+ align-items: baseline;
271
+ gap: 12px;
272
+ }
273
+ .hm-week {
274
+ font: 500 13px / 1 var(--dt-sans);
275
+ color: var(--dt-text);
276
+ }
277
+ .hm-clock {
278
+ font: 600 11px / 1 var(--dt-mono);
279
+ color: var(--dt-accent);
280
+ }
281
+ .hm-head-right {
282
+ display: flex;
283
+ gap: 14px;
284
+ }
285
+ .hm-stat {
286
+ font: 400 10px / 1 var(--dt-mono);
287
+ color: var(--dt-text-3);
288
+ }
289
+
290
+ /* Hour markers */
291
+ .hm-hours-row {
292
+ position: relative;
293
+ height: 16px;
294
+ margin: 0 20px 0 76px;
295
+ flex-shrink: 0;
296
+ }
297
+ .hm-hour-mark {
298
+ position: absolute;
299
+ top: 0;
300
+ font: 400 8px / 1 var(--dt-mono);
301
+ color: var(--dt-text-3);
302
+ transform: translateX(-50%);
303
+ }
304
+
305
+ /* Grid */
306
+ .hm-grid {
307
+ flex: 1;
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 3px;
311
+ padding: 0 20px 14px;
312
+ min-height: 0;
313
+ }
314
+
315
+ .hm-row {
316
+ display: flex;
317
+ align-items: stretch;
318
+ gap: 8px;
319
+ flex: 1;
320
+ min-height: 24px;
321
+ }
322
+ .hm-row--past {
323
+ opacity: 0.45;
324
+ }
325
+ .hm-row--today {
326
+ background: var(--dt-today-bg);
327
+ }
328
+
329
+ .hm-row-label {
330
+ width: 48px;
331
+ flex-shrink: 0;
332
+ display: flex;
333
+ align-items: center;
334
+ align-self: center;
335
+ gap: 4px;
336
+ }
337
+ .hm-row-day {
338
+ font: 500 10px / 1 var(--dt-sans);
339
+ color: var(--dt-text-2);
340
+ text-transform: uppercase;
341
+ letter-spacing: 0.04em;
342
+ }
343
+ .hm-row-num {
344
+ font: 400 10px / 1 var(--dt-mono);
345
+ color: var(--dt-text-3);
346
+ }
347
+ .hm-row--today .hm-row-day {
348
+ color: var(--dt-accent);
349
+ font-weight: 600;
350
+ }
351
+
352
+ .hm-cells {
353
+ display: flex;
354
+ gap: 2px;
355
+ flex: 1;
356
+ align-items: stretch;
357
+ }
358
+
359
+ .hm-cell {
360
+ flex: 1;
361
+ border: none;
362
+ border-radius: 3px;
363
+ cursor: pointer;
364
+ padding: 0;
365
+ position: relative;
366
+ transition: background 200ms, transform 100ms;
367
+ }
368
+ .hm-cell:hover {
369
+ transform: scaleY(1.3);
370
+ z-index: 2;
371
+ }
372
+ .hm-cell--now {
373
+ outline: 1.5px solid var(--dt-glow);
374
+ outline-offset: 1px;
375
+ z-index: 3;
376
+ }
377
+ .hm-cell--selected {
378
+ outline: 1.5px solid var(--dt-accent);
379
+ outline-offset: 1px;
380
+ z-index: 4;
381
+ }
382
+ .hm-cell-now {
383
+ position: absolute;
384
+ inset: 0;
385
+ border-radius: 3px;
386
+ animation: hm-pulse 2.5s ease-in-out infinite;
387
+ background: var(--dt-accent);
388
+ opacity: 0.15;
389
+ }
390
+ @keyframes hm-pulse {
391
+ 0%, 100% { opacity: 0.15; }
392
+ 50% { opacity: 0.35; }
393
+ }
394
+
395
+ /* Detail pane */
396
+ .hm-detail {
397
+ position: absolute;
398
+ bottom: 14px;
399
+ right: 20px;
400
+ width: 240px;
401
+ background: var(--dt-surface);
402
+ border: 1px solid var(--dt-border);
403
+ border-radius: 10px;
404
+ padding: 10px 12px;
405
+ z-index: 20;
406
+ box-shadow: 0 8px 32px color-mix(in srgb, var(--dt-bg) 80%, transparent);
407
+ }
408
+ .hm-detail-head {
409
+ display: flex;
410
+ justify-content: space-between;
411
+ align-items: center;
412
+ margin-bottom: 8px;
413
+ }
414
+ .hm-detail-label {
415
+ font: 500 11px / 1 var(--dt-sans);
416
+ color: var(--dt-text);
417
+ }
418
+ .hm-detail-close {
419
+ background: none;
420
+ border: none;
421
+ color: var(--dt-text-3);
422
+ cursor: pointer;
423
+ font-size: 12px;
424
+ padding: 2px 4px;
425
+ }
426
+ .hm-detail-close:hover {
427
+ color: var(--dt-text);
428
+ }
429
+ .hm-detail-event {
430
+ padding: 6px 0;
431
+ border-top: 1px solid var(--dt-border);
432
+ border-left: 3px solid var(--ev-color, var(--dt-accent));
433
+ padding-left: 8px;
434
+ cursor: pointer;
435
+ transition: background 120ms;
436
+ }
437
+ .hm-detail-event:hover {
438
+ background: color-mix(in srgb, var(--ev-color) 10%, transparent);
439
+ }
440
+ .hm-detail-event--selected {
441
+ background: color-mix(in srgb, var(--ev-color) 15%, transparent);
442
+ }
443
+ .hm-detail-title {
444
+ display: block;
445
+ font: 500 11px / 1.2 var(--dt-sans);
446
+ color: var(--dt-text);
447
+ margin-bottom: 2px;
448
+ }
449
+ .hm-detail-time {
450
+ font: 400 9px / 1 var(--dt-mono);
451
+ color: var(--dt-text-2);
452
+ }
453
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { WeekTimelineProps } from '../../core/types.js';
2
+ declare const WeekHeatmap: import("svelte").Component<WeekTimelineProps, {}, "">;
3
+ type WeekHeatmap = ReturnType<typeof WeekHeatmap>;
4
+ export default WeekHeatmap;
@@ -0,0 +1,2 @@
1
+ export { default as WeekGrid } from './WeekGrid.svelte';
2
+ export { default as WeekHeatmap } from './WeekHeatmap.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as WeekGrid } from './WeekGrid.svelte';
2
+ export { default as WeekHeatmap } from './WeekHeatmap.svelte';
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@nomideusz/svelte-calendar",
3
+ "version": "0.1.0",
4
+ "description": "A themeable, pluggable Svelte 5 calendar with Day and Week views — Grid, Timeline, Agenda, Heatmap.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "svelte": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "svelte": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "!dist/**/*.test.*",
19
+ "!dist/**/*.spec.*"
20
+ ],
21
+ "peerDependencies": {
22
+ "svelte": "^5.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@sveltejs/adapter-auto": "^7.0.0",
26
+ "@sveltejs/kit": "^2.50.2",
27
+ "@sveltejs/package": "^2.5.7",
28
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
29
+ "svelte": "^5.51.0",
30
+ "svelte-check": "^4.3.6",
31
+ "typescript": "^5.9.3",
32
+ "vite": "^7.3.1",
33
+ "vitest": "^4.0.18"
34
+ },
35
+ "dependencies": {
36
+ "date-fns": "^4.1.0"
37
+ },
38
+ "keywords": [
39
+ "svelte",
40
+ "calendar",
41
+ "scheduler",
42
+ "timeline",
43
+ "week",
44
+ "day",
45
+ "booking",
46
+ "svelte5"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/nomideusz/svelte-calendar"
51
+ },
52
+ "scripts": {
53
+ "dev": "vite dev",
54
+ "build": "vite build",
55
+ "package": "svelte-kit sync && svelte-package",
56
+ "preview": "vite preview",
57
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
58
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest"
61
+ }
62
+ }