@glatam/calendar-ui 1.0.1 → 1.0.3

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.
@@ -1,197 +0,0 @@
1
- import { LitElement, html } from 'lit';
2
- import { customElement, property, state } from 'lit/decorators.js';
3
- import { generateMonthDays, generateWeekDays, formatISODate, isTimeBlocked, BlockingRule, TimeSlot } from '@glatam/calendar-core';
4
- import { variablesStyles } from './styles/variables.css.js';
5
- import { calendarStyles } from './styles/calendar.css.js';
6
- import { getTimezoneOffsetDiff, shiftSlot } from './utils/timezone-utils.js';
7
-
8
- import './views/month-view.js';
9
- import './views/week-view.js';
10
- import './views/day-view.js';
11
- import './components/calendar-modal.js';
12
-
13
- const DEFAULT_SLOTS: TimeSlot[] = [
14
- { start: '09:00', end: '10:00' }, { start: '10:00', end: '11:00' },
15
- { start: '11:00', end: '12:00' }, { start: '12:00', end: '13:00' },
16
- { start: '13:00', end: '14:00' }, { start: '14:00', end: '15:00' },
17
- { start: '15:00', end: '16:00' }, { start: '16:00', end: '17:00' },
18
- { start: '17:00', end: '18:00' },
19
- ];
20
-
21
- @customElement('glatam-calendar')
22
- export class GlatamCalendar extends LitElement {
23
- static styles = [variablesStyles, calendarStyles];
24
-
25
- @property({ type: String }) role = 'provider';
26
- @property({ type: String, reflect: true }) size = 'medium';
27
- @property({ type: String }) view: 'month' | 'week' | 'day' = 'month';
28
- @property({ type: String }) locale = 'es';
29
- @property({ type: Number }) startOfWeekDay = 0;
30
- @property({ type: Array }) rules: BlockingRule[] = [];
31
- @property({ type: Array }) selectedDates: string[] = [];
32
- @property({ type: Object }) selectedRange: { dateString: string; start: string; end: string } | null = null;
33
- @property({ type: String }) hostTimezone = 'America/Bogota'; @property({ type: String }) activeTimezone = 'local';
34
- @property({ type: Array }) slots: TimeSlot[] = DEFAULT_SLOTS;
35
- @property({ type: String }) minDate = ''; @property({ type: String }) maxDate = '';
36
- @property({ type: Boolean }) showNeighboringMonth = true;
37
- @property({ attribute: false }) tileClassName: ((data: { date: Date; dateString: string }) => string) | null = null;
38
-
39
- @state() private activeDate = new Date();
40
- @state() private localRules: BlockingRule[] = [];
41
- @state() private darkMode = false;
42
- @state() private modalOpen = false;
43
- @state() private modalDateString = '';
44
- @state() private modalStartTime = '';
45
- @state() private modalEndTime = '';
46
- @state() private modalIsRange = false;
47
- @state() private modalExistingRule: BlockingRule | null = null;
48
-
49
- firstUpdated() { this.darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; this.localRules = [...this.rules]; }
50
- willUpdate(changedProps: Map<string, any>) { if (changedProps.has('rules') && this.rules.length > 0 && this.localRules.length === 0) this.localRules = [...this.rules]; }
51
- updated(changedProps: Map<string, any>) { if (changedProps.has('darkMode')) this.classList.toggle('dark-mode', this.darkMode); }
52
-
53
- private handlePrev() {
54
- const d = new Date(this.activeDate);
55
- this.view === 'month' ? d.setMonth(d.getMonth() - 1) : this.view === 'week' ? d.setDate(d.getDate() - 7) : d.setDate(d.getDate() - 1);
56
- this.activeDate = d;
57
- }
58
-
59
- private handleNext() {
60
- const d = new Date(this.activeDate);
61
- this.view === 'month' ? d.setMonth(d.getMonth() + 1) : this.view === 'week' ? d.setDate(d.getDate() + 7) : d.setDate(d.getDate() + 1);
62
- this.activeDate = d;
63
- }
64
-
65
- private getHeaderTitle(): string {
66
- const opt: Intl.DateTimeFormatOptions = this.view === 'month' ? { month: 'long', year: 'numeric' } : this.view === 'week' ? { month: 'short', year: 'numeric' } : { day: 'numeric', month: 'long', year: 'numeric' };
67
- return new Intl.DateTimeFormat(this.locale, opt).format(this.activeDate);
68
- }
69
-
70
- private handleDaySelect(e: CustomEvent<{ dateString: string; isBlocked: boolean }>) {
71
- const { dateString, isBlocked } = e.detail;
72
- if (this.role === 'buyer' && isBlocked) return;
73
- this.activeDate = new Date(dateString + 'T00:00:00');
74
- this.view = 'day';
75
- this.dispatchEvent(new CustomEvent('date-selected', { detail: { dateString }, bubbles: true, composed: true }));
76
- }
77
-
78
- private handleRangeSelect(e: CustomEvent<{ dateString: string; start: string; end: string }>) {
79
- const { dateString, start, end } = e.detail, offset = this.activeTimezone === 'local' ? getTimezoneOffsetDiff(this.activeDate, this.hostTimezone, 'local') : 0;
80
- const displaySlots = this.getDisplaySlots(offset), startIdx = displaySlots.findIndex(s => s.start === start), endIdx = displaySlots.findIndex(s => s.end === end);
81
- const hostStart = this.slots[startIdx]?.start || start, hostEnd = this.slots[endIdx]?.end || end;
82
- if (this.role === 'buyer') {
83
- this.selectedRange = { dateString, start, end };
84
- this.dispatchEvent(new CustomEvent('booking-selected', { detail: { dateString, start, end, hostStart, hostEnd }, bubbles: true, composed: true }));
85
- return;
86
- }
87
- this.openModal(dateString, hostStart, hostEnd, true, null);
88
- }
89
-
90
- private handleSlotClick(e: CustomEvent<{ dateString: string; slot: TimeSlot }>) {
91
- const { dateString, slot } = e.detail, date = new Date(dateString + 'T00:00:00'), offset = this.activeTimezone === 'local' ? getTimezoneOffsetDiff(this.activeDate, this.hostTimezone, 'local') : 0;
92
- const displaySlots = this.getDisplaySlots(offset), idx = displaySlots.findIndex(s => s.start === slot.start), hostSlot = this.slots[idx] || slot;
93
- if (this.role === 'buyer') {
94
- this.selectedRange = { dateString, start: slot.start, end: slot.end };
95
- this.dispatchEvent(new CustomEvent('booking-selected', { detail: { dateString, start: slot.start, end: slot.end, hostStart: hostSlot.start, hostEnd: hostSlot.end }, bubbles: true, composed: true }));
96
- return;
97
- }
98
- const rule = this.localRules.find(r => isTimeBlocked(date, hostSlot, [r]));
99
- this.openModal(dateString, hostSlot.start, hostSlot.end, true, rule || null);
100
- }
101
-
102
- private handleBlockDayAction() {
103
- const dateStr = formatISODate(this.activeDate), rule = this.localRules.find(r => isTimeBlocked(this.activeDate, undefined, [r]));
104
- this.openModal(dateStr, '', '', false, rule || null);
105
- }
106
-
107
- private openModal(date: string, start: string, end: string, isRange: boolean, rule: BlockingRule | null) {
108
- this.modalDateString = date; this.modalStartTime = start; this.modalEndTime = end; this.modalIsRange = isRange; this.modalExistingRule = rule; this.modalOpen = true;
109
- }
110
-
111
- private handleSaveRule(e: CustomEvent) {
112
- const data = e.detail, ruleId = this.modalExistingRule ? this.modalExistingRule.id : `rule-${Date.now()}`;
113
- const newRule: BlockingRule = {
114
- id: ruleId, type: data.isRecurring ? 'weekly' : 'date-range',
115
- slots: data.blockAllDay ? undefined : [{ start: data.startTime, end: data.endTime }],
116
- daysOfWeek: data.isRecurring ? data.selectedDays : undefined,
117
- startDate: data.isRecurring ? undefined : data.dateString,
118
- endDate: data.isRecurring ? undefined : data.dateString,
119
- description: data.title
120
- };
121
- this.localRules = this.modalExistingRule ? this.localRules.map(r => r.id === ruleId ? newRule : r) : [...this.localRules, newRule];
122
- this.modalOpen = false;
123
- this.dispatchEvent(new CustomEvent('rules-changed', { detail: { rules: this.localRules }, bubbles: true, composed: true }));
124
- }
125
-
126
- private handleDeleteRule(e: CustomEvent<{ id: string }>) {
127
- this.localRules = this.localRules.filter(r => r.id !== e.detail.id);
128
- this.modalOpen = false;
129
- this.dispatchEvent(new CustomEvent('rules-changed', { detail: { rules: this.localRules }, bubbles: true, composed: true }));
130
- }
131
-
132
- private getDisplaySlots(offset: number): TimeSlot[] {
133
- if (offset === 0) return this.slots;
134
- return this.slots.map(s => {
135
- const shifted = shiftSlot(s, offset);
136
- const suffix = shifted.dayShift > 0 ? ' (+1d)' : shifted.dayShift < 0 ? ' (-1d)' : '';
137
- return { start: shifted.start + suffix, end: shifted.end + suffix };
138
- });
139
- }
140
-
141
- render() {
142
- const y = this.activeDate.getFullYear(), m = this.activeDate.getMonth(), isCurrentDayBlocked = isTimeBlocked(this.activeDate, undefined, this.localRules);
143
- const offset = this.activeTimezone === 'local' ? getTimezoneOffsetDiff(this.activeDate, this.hostTimezone, 'local') : 0, displaySlots = this.getDisplaySlots(offset);
144
- const mapDays = (days: any[]) => days.map(d => ({ ...d, slots: d.slots.map((s: any, idx: number) => ({ ...s, start: displaySlots[idx]?.start || s.start, end: displaySlots[idx]?.end || s.end })) }));
145
- const tzCity = this.hostTimezone.split('/').pop()?.replace('_', ' ') || 'Anfitrión';
146
-
147
- return html`
148
- <div class="calendar-header">
149
- <div class="nav-group">
150
- <button class="btn" @click=${() => this.activeDate = new Date()}>Hoy</button>
151
- <button class="btn" @click=${this.handlePrev}>&lt;</button>
152
- <button class="btn" @click=${this.handleNext}>&gt;</button>
153
- <span class="nav-title" style="text-transform: capitalize;">${this.getHeaderTitle()}</span>
154
- </div>
155
-
156
- <div class="view-group">
157
- ${this.role === 'provider' ? html`
158
- <div class="timezone-badge">
159
- 🌐 Zona:
160
- <select class="timezone-select" @change=${(e: any) => this.activeTimezone = e.target.value}>
161
- <option value="local" ?selected=${this.activeTimezone === 'local'}>Mi Hora</option>
162
- <option value="host" ?selected=${this.activeTimezone === 'host'}>Hora ${tzCity}</option>
163
- </select>
164
- </div>
165
- ` : ''}
166
- ${this.role === 'provider' && this.view === 'day'
167
- ? html`<button class="btn btn-primary" @click=${this.handleBlockDayAction} style="margin-right: 8px;">${isCurrentDayBlocked ? 'Liberar Día' : 'Bloquear Día'}</button>`
168
- : ''}
169
- <button class="btn" @click=${() => this.darkMode = !this.darkMode} style="margin-right: 8px;">${this.darkMode ? '☀️' : '🌙'}</button>
170
- <div class="btn-group">
171
- <button class="btn ${this.view === 'month' ? 'active' : ''}" @click=${() => this.view = 'month'}>Mes</button>
172
- <button class="btn ${this.view === 'week' ? 'active' : ''}" @click=${() => this.view = 'week'}>Semana</button>
173
- <button class="btn ${this.view === 'day' ? 'active' : ''}" @click=${() => this.view = 'day'}>Día</button>
174
- </div>
175
- </div>
176
- </div>
177
-
178
- <div class="calendar-body">
179
- ${this.view === 'month'
180
- ? html`<glatam-calendar-month-view .days=${generateMonthDays(y, m, this.localRules, this.slots, this.startOfWeekDay)} .locale=${this.locale} .startOfWeekDay=${this.startOfWeekDay} .role=${this.role} .size=${this.size} .minDate=${this.minDate} .maxDate=${this.maxDate} .showNeighboringMonth=${this.showNeighboringMonth} .tileClassName=${this.tileClassName} @day-select=${this.handleDaySelect}></glatam-calendar-month-view>`
181
- : this.view === 'week'
182
- ? html`<glatam-calendar-week-view .days=${mapDays(generateWeekDays(this.activeDate, this.localRules, this.slots, this.startOfWeekDay))} .slots=${displaySlots} .locale=${this.locale} .selectedRange=${this.selectedRange} .role=${this.role} @range-select=${this.handleRangeSelect} @slot-click=${this.handleSlotClick}></glatam-calendar-week-view>`
183
- : html`<glatam-calendar-day-view .day=${mapDays(generateWeekDays(this.activeDate, this.localRules, this.slots, this.startOfWeekDay)).find(d => d.dateString === formatISODate(this.activeDate)) || null} .slots=${displaySlots} .locale=${this.locale} .selectedRange=${this.selectedRange} .role=${this.role} @range-select=${this.handleRangeSelect} @slot-click=${this.handleSlotClick}></glatam-calendar-day-view>`}
184
- </div>
185
-
186
- <glatam-calendar-modal
187
- .open=${this.modalOpen} .dateString=${this.modalDateString} .startTime=${this.modalStartTime} .endTime=${this.modalEndTime}
188
- .isRange=${this.modalIsRange} .existingRule=${this.modalExistingRule} @save-rule=${this.handleSaveRule}
189
- @delete-rule=${this.handleDeleteRule} @close=${() => this.modalOpen = false}
190
- ></glatam-calendar-modal>
191
- `;
192
- }
193
- }
194
-
195
- declare global {
196
- interface HTMLElementTagNameMap { 'glatam-calendar': GlatamCalendar; }
197
- }
package/src/index.ts DELETED
@@ -1,9 +0,0 @@
1
- export * from './glatam-calendar.js';
2
- export * from './glatam-calendar-mini.js';
3
- export * from './views/month-view.js';
4
- export * from './views/week-view.js';
5
- export * from './views/day-view.js';
6
- export * from './components/calendar-modal.js';
7
- export * from './styles/variables.css.js';
8
- export * from './styles/calendar.css.js';
9
- export { formatISODate, parseISODate, isTimeBlocked } from '@glatam/calendar-core';
@@ -1,259 +0,0 @@
1
- import { css } from 'lit';
2
-
3
- export const calendarStyles = css`
4
- :host {
5
- display: grid;
6
- min-width: 0;
7
- font-family: var(--glatam-font-family);
8
- color: var(--glatam-text);
9
- background-color: var(--glatam-bg);
10
- border-radius: var(--glatam-border-radius);
11
- padding: var(--glatam-padding);
12
- box-shadow: 0 4px 25px rgba(0, 0, 0, 0.05);
13
- border: 1px solid var(--glatam-border);
14
- box-sizing: border-box;
15
- width: 100%;
16
- max-width: 100%;
17
- overflow: hidden;
18
- transition: background-color var(--glatam-transition-normal), border-color var(--glatam-transition-normal);
19
- }
20
-
21
- *, *::before, *::after {
22
- box-sizing: border-box;
23
- }
24
-
25
- .calendar-header {
26
- display: flex;
27
- justify-content: space-between;
28
- align-items: center;
29
- margin-bottom: var(--glatam-padding);
30
- flex-wrap: wrap;
31
- gap: 12px;
32
- }
33
-
34
- .nav-group, .view-group {
35
- display: flex;
36
- align-items: center;
37
- gap: 8px;
38
- }
39
-
40
- .nav-title {
41
- font-size: 1.25rem;
42
- font-weight: 600;
43
- min-width: 140px;
44
- text-align: center;
45
- letter-spacing: -0.01em;
46
- }
47
-
48
- .btn {
49
- font-family: inherit;
50
- font-size: 0.875rem;
51
- font-weight: 500;
52
- padding: 8px 12px;
53
- border-radius: 8px;
54
- border: 1px solid var(--glatam-border);
55
- background-color: var(--glatam-bg);
56
- color: var(--glatam-text);
57
- cursor: pointer;
58
- transition: background-color var(--glatam-transition-fast), border-color var(--glatam-transition-fast), transform var(--glatam-transition-fast);
59
- display: inline-flex;
60
- align-items: center;
61
- justify-content: center;
62
- min-width: 36px;
63
- height: 36px;
64
- }
65
-
66
- .btn:hover:not(:disabled) {
67
- background-color: var(--glatam-surface);
68
- border-color: var(--glatam-text-secondary);
69
- }
70
-
71
- .btn:active:not(:disabled) {
72
- transform: scale(0.97);
73
- }
74
-
75
- .btn:disabled {
76
- opacity: 0.5;
77
- cursor: not-allowed;
78
- }
79
-
80
- .btn-primary {
81
- background-color: var(--glatam-primary);
82
- color: var(--glatam-text-light);
83
- border: none;
84
- }
85
-
86
- .btn-primary:hover:not(:disabled) {
87
- background-color: var(--glatam-primary-hover);
88
- }
89
-
90
- .btn-group {
91
- display: flex;
92
- border-radius: 8px;
93
- overflow: hidden;
94
- border: 1px solid var(--glatam-border);
95
- }
96
-
97
- .btn-group .btn {
98
- border-radius: 0;
99
- border: none;
100
- border-right: 1px solid var(--glatam-border);
101
- }
102
-
103
- .btn-group .btn:last-child {
104
- border-right: none;
105
- }
106
-
107
- .btn-group .btn.active {
108
- background-color: var(--glatam-primary-light);
109
- color: var(--glatam-primary);
110
- font-weight: 600;
111
- }
112
-
113
- /* Timezone Selector Styles */
114
- .timezone-badge {
115
- font-size: 0.75rem;
116
- padding: 4px 8px;
117
- background: var(--glatam-surface);
118
- border: 1px solid var(--glatam-border);
119
- border-radius: 20px;
120
- display: inline-flex;
121
- align-items: center;
122
- gap: 4px;
123
- color: var(--glatam-text-secondary);
124
- }
125
-
126
- .timezone-select {
127
- background: transparent;
128
- border: none;
129
- color: var(--glatam-text);
130
- font-family: inherit;
131
- font-size: 0.75rem;
132
- font-weight: 600;
133
- cursor: pointer;
134
- outline: none;
135
- padding-right: 4px;
136
- }
137
-
138
- /* Mini Popover Variant Styles */
139
- .dropdown-container {
140
- position: relative;
141
- display: inline-block;
142
- width: 100%;
143
- }
144
-
145
- .dropdown-toggle {
146
- width: 100%;
147
- justify-content: space-between;
148
- padding: 12px 16px;
149
- border-radius: 12px;
150
- font-weight: 600;
151
- height: 48px;
152
- }
153
-
154
- .dropdown-card {
155
- position: absolute;
156
- top: calc(100% + 8px);
157
- left: 0;
158
- width: 320px;
159
- background: var(--glatam-bg);
160
- border-radius: 16px;
161
- border: 1px solid var(--glatam-border);
162
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.1);
163
- z-index: 100;
164
- padding: 16px;
165
- display: flex;
166
- flex-direction: column;
167
- gap: 12px;
168
- animation: fadeInUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
169
- }
170
-
171
- .slot-list {
172
- display: flex;
173
- flex-direction: column;
174
- gap: 8px;
175
- max-height: 220px;
176
- overflow-y: auto;
177
- padding-right: 4px;
178
- }
179
-
180
- .slot-btn {
181
- width: 100%;
182
- padding: 10px;
183
- border-radius: 8px;
184
- border: 1px solid var(--glatam-border);
185
- background: var(--glatam-bg);
186
- color: var(--glatam-text);
187
- cursor: pointer;
188
- font-family: inherit;
189
- font-size: 0.85rem;
190
- font-weight: 500;
191
- text-align: center;
192
- transition: background-color var(--glatam-transition-fast);
193
- }
194
-
195
- .slot-btn:hover:not(.blocked) {
196
- background-color: var(--glatam-surface);
197
- border-color: var(--glatam-text-secondary);
198
- }
199
-
200
- .slot-btn.blocked {
201
- background-color: var(--glatam-surface);
202
- color: var(--glatam-blocked-text);
203
- text-decoration: line-through;
204
- cursor: not-allowed;
205
- border-color: var(--glatam-border);
206
- opacity: 0.6;
207
- }
208
-
209
- /* Smooth Apple Animation */
210
- .calendar-body {
211
- position: relative;
212
- width: 100%;
213
- overflow-x: auto;
214
- animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
215
- }
216
-
217
- @keyframes fadeInUp {
218
- from {
219
- opacity: 0;
220
- transform: translateY(8px) scale(0.995);
221
- }
222
- to {
223
- opacity: 1;
224
- transform: translateY(0) scale(1);
225
- }
226
- }
227
-
228
- @media (max-width: 600px) {
229
- :host {
230
- padding: 8px;
231
- }
232
- .calendar-header {
233
- flex-direction: column;
234
- align-items: stretch;
235
- gap: 10px;
236
- }
237
- .nav-group, .view-group {
238
- width: 100%;
239
- justify-content: space-between;
240
- flex-wrap: wrap;
241
- gap: 6px;
242
- }
243
- .nav-title {
244
- flex: 1;
245
- font-size: 1.1rem;
246
- min-width: unset;
247
- text-align: right;
248
- white-space: nowrap;
249
- overflow: hidden;
250
- text-overflow: ellipsis;
251
- }
252
- .btn {
253
- font-size: 0.8rem;
254
- padding: 6px 10px;
255
- height: 32px;
256
- min-width: 32px;
257
- }
258
- }
259
- `;
@@ -1,91 +0,0 @@
1
- import { css } from 'lit';
2
-
3
- export const variablesStyles = css`
4
- :host {
5
- /* Fonts */
6
- --glatam-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
7
-
8
- /* Colors - Premium Apple Light Theme */
9
- --glatam-primary: #5856d6;
10
- --glatam-primary-hover: #4745b4;
11
- --glatam-primary-light: rgba(88, 86, 214, 0.1);
12
- --glatam-primary-light-hover: rgba(88, 86, 214, 0.25);
13
-
14
- --glatam-bg: #ffffff;
15
- --glatam-surface: #f5f5f7;
16
- --glatam-border: #e5e5ea;
17
- --glatam-text: #1d1d1f;
18
- --glatam-text-secondary: #86868b;
19
- --glatam-text-light: #ffffff;
20
-
21
- /* Blocked/Unavailable states */
22
- --glatam-blocked-bg: #fcfcfd;
23
- --glatam-blocked-stripe: #f2f2f7;
24
- --glatam-blocked-text: #aeaeae;
25
- --glatam-blocked-border: #e5e5ea;
26
-
27
- /* Selection Colors */
28
- --glatam-selection-bg: rgba(88, 86, 214, 0.15);
29
- --glatam-selection-border: #5856d6;
30
-
31
- /* Today highlighting */
32
- --glatam-today-text: #5856d6;
33
- --glatam-today-bg: rgba(88, 86, 214, 0.08);
34
-
35
- /* Dimensions & Layout */
36
- --glatam-border-radius: 12px;
37
- --glatam-padding: 16px;
38
- --glatam-grid-border-radius: 8px;
39
- --glatam-day-min-height: 72px;
40
- --glatam-time-col-width: 60px;
41
- --glatam-slot-height: 48px;
42
-
43
- /* Animation duration */
44
- --glatam-transition-fast: 0.15s ease;
45
- --glatam-transition-normal: 0.25s ease;
46
- }
47
-
48
- :host(.dark-mode) {
49
- /* Colors - Premium Apple Dark Theme */
50
- --glatam-primary: #5e5ce6;
51
- --glatam-primary-hover: #7d7aff;
52
- --glatam-primary-light: rgba(94, 92, 230, 0.15);
53
- --glatam-primary-light-hover: rgba(94, 92, 230, 0.3);
54
-
55
- --glatam-bg: #1c1c1e;
56
- --glatam-surface: #2c2c2e;
57
- --glatam-border: #38383a;
58
- --glatam-text: #f5f5f7;
59
- --glatam-text-secondary: #8e8e93;
60
-
61
- /* Blocked/Unavailable states */
62
- --glatam-blocked-bg: #242426;
63
- --glatam-blocked-stripe: #2c2c2e;
64
- --glatam-blocked-text: #707074;
65
- --glatam-blocked-border: #38383a;
66
-
67
- /* Selection Colors */
68
- --glatam-selection-bg: rgba(94, 92, 230, 0.25);
69
- --glatam-selection-border: #5e5ce6;
70
-
71
- /* Today highlighting */
72
- --glatam-today-text: #7d7aff;
73
- --glatam-today-bg: rgba(94, 92, 230, 0.12);
74
- }
75
-
76
- :host([size="small"]) {
77
- --glatam-day-min-height: 48px;
78
- --glatam-slot-height: 36px;
79
- --glatam-time-col-width: 48px;
80
- --glatam-padding: 10px;
81
- font-size: 0.8rem;
82
- }
83
-
84
- :host([size="large"]) {
85
- --glatam-day-min-height: 96px;
86
- --glatam-slot-height: 60px;
87
- --glatam-time-col-width: 72px;
88
- --glatam-padding: 24px;
89
- font-size: 1.1rem;
90
- }
91
- `;
@@ -1,87 +0,0 @@
1
- import { TimeSlot } from '@glatam/calendar-core';
2
-
3
- /**
4
- * Calculates the difference in minutes between two timezones on a specific date.
5
- * 'local' represents the user's local timezone.
6
- */
7
- export function getTimezoneOffsetDiff(date: Date, fromTz: string, toTz: string): number {
8
- if (fromTz === toTz) return 0;
9
-
10
- const getTzDate = (d: Date, tz: string) => {
11
- const options: Intl.DateTimeFormatOptions = {
12
- year: 'numeric', month: 'numeric', day: 'numeric',
13
- hour: 'numeric', minute: 'numeric', second: 'numeric',
14
- hour12: false,
15
- };
16
- if (tz !== 'local') {
17
- options.timeZone = tz;
18
- }
19
- const formatter = new Intl.DateTimeFormat('en-US', options);
20
- const parts = formatter.formatToParts(d);
21
- const map = new Map(parts.map(p => [p.type, p.value]));
22
-
23
- const hourVal = Number(map.get('hour'));
24
- const hour = hourVal === 24 ? 0 : hourVal;
25
-
26
- return new Date(
27
- Number(map.get('year')),
28
- Number(map.get('month')) - 1,
29
- Number(map.get('day')),
30
- hour,
31
- Number(map.get('minute')),
32
- Number(map.get('second'))
33
- );
34
- };
35
-
36
- try {
37
- const d1 = getTzDate(date, fromTz);
38
- const d2 = getTzDate(date, toTz);
39
- return (d2.getTime() - d1.getTime()) / (60 * 1000);
40
- } catch (e) {
41
- // Fallback if timezone name is invalid
42
- return 0;
43
- }
44
- }
45
-
46
- /**
47
- * Shifts a single TimeSlot's times by a given offset in minutes and returns the shifted slot and optional day offset.
48
- */
49
- export function shiftSlot(slot: TimeSlot, offsetMinutes: number): { start: string; end: string; dayShift: number } {
50
- if (offsetMinutes === 0) {
51
- return { start: slot.start, end: slot.end, dayShift: 0 };
52
- }
53
-
54
- const parseToMinutes = (timeStr: string) => {
55
- const [h, m] = timeStr.split(':').map(Number);
56
- return h * 60 + m;
57
- };
58
-
59
- const formatFromMinutes = (totalMinutes: number) => {
60
- // Normalize minutes to [0, 1439]
61
- let normalized = totalMinutes % 1440;
62
- if (normalized < 0) normalized += 1440;
63
- const h = String(Math.floor(normalized / 60)).padStart(2, '0');
64
- const m = String(normalized % 60).padStart(2, '0');
65
- return `${h}:${m}`;
66
- };
67
-
68
- const startMin = parseToMinutes(slot.start);
69
- const endMin = parseToMinutes(slot.end);
70
-
71
- const shiftedStart = startMin + offsetMinutes;
72
- const shiftedEnd = endMin + offsetMinutes;
73
-
74
- // Determine if we crossed midnight (day shift)
75
- let dayShift = 0;
76
- if (shiftedStart < 0) {
77
- dayShift = -1;
78
- } else if (shiftedStart >= 1440) {
79
- dayShift = 1;
80
- }
81
-
82
- return {
83
- start: formatFromMinutes(shiftedStart),
84
- end: formatFromMinutes(shiftedEnd),
85
- dayShift
86
- };
87
- }