@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,130 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property, state } from 'lit/decorators.js';
3
- import { classMap } from 'lit/directives/class-map.js';
4
- import { CalendarDay, TimeSlot } from '@glatam/calendar-core';
5
-
6
- @customElement('glatam-calendar-day-view')
7
- export class GlatamCalendarDayView extends LitElement {
8
- static styles = css`
9
- :host {
10
- display: grid; grid-template-columns: var(--glatam-time-col-width) 1fr; gap: 2px;
11
- width: 100%; background-color: var(--glatam-border); border: 1px solid var(--glatam-border);
12
- border-radius: var(--glatam-grid-border-radius); overflow: hidden; user-select: none;
13
- }
14
- .time-col { background-color: var(--glatam-surface); display: flex; flex-direction: column; }
15
- .day-col { background-color: var(--glatam-bg); display: flex; flex-direction: column; }
16
- .header-cell {
17
- height: 60px; display: flex; flex-direction: column; align-items: center; justify-content: center;
18
- background-color: var(--glatam-surface); border-bottom: 2px solid var(--glatam-border);
19
- font-size: 0.875rem; font-weight: 500;
20
- }
21
- .header-cell.today { color: var(--glatam-primary); font-weight: 700; }
22
- .slot-cell {
23
- height: var(--glatam-slot-height); border-bottom: 1px solid var(--glatam-border);
24
- display: flex; align-items: center; justify-content: center; font-size: 0.75rem;
25
- cursor: pointer; transition: background-color var(--glatam-transition-fast); position: relative;
26
- }
27
- .time-slot-label { font-size: 0.7rem; color: var(--glatam-text-secondary); }
28
- .slot-cell.available:hover { background-color: var(--glatam-surface); }
29
- .slot-cell.blocked {
30
- background-color: var(--glatam-blocked-bg);
31
- background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, var(--glatam-blocked-stripe) 5px, var(--glatam-blocked-stripe) 10px);
32
- color: var(--glatam-blocked-text); cursor: not-allowed; font-weight: 600; padding: 0 10px;
33
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
34
- }
35
- .slot-cell.selected { background-color: var(--glatam-selection-bg); border-left: 3px solid var(--glatam-selection-border); }
36
- .slot-cell.drag-selecting { background-color: var(--glatam-primary-light); }
37
- `;
38
-
39
- @property({ type: Object }) day: CalendarDay | null = null;
40
- @property({ type: Array }) slots: TimeSlot[] = [];
41
- @property({ type: String }) locale = 'es';
42
- @property({ type: Object }) selectedRange: { dateString: string; start: string; end: string } | null = null;
43
- @property({ type: String }) role = 'provider';
44
-
45
- @state() private isDragging = false;
46
- @state() private dragStartSlotIndex: number | null = null;
47
- @state() private dragEndSlotIndex: number | null = null;
48
-
49
- connectedCallback() { super.connectedCallback(); window.addEventListener('mouseup', this.handleMouseUp); }
50
- disconnectedCallback() { window.removeEventListener('mouseup', this.handleMouseUp); super.disconnectedCallback(); }
51
-
52
- private handleMouseDown(slotIndex: number, isBlocked: boolean) {
53
- if (isBlocked && this.day) {
54
- if (this.role === 'buyer') return;
55
- this.dispatchEvent(new CustomEvent('slot-click', {
56
- detail: { dateString: this.day.dateString, slot: this.day.slots[slotIndex], isBlocked: true },
57
- bubbles: true, composed: true
58
- }));
59
- return;
60
- }
61
- this.isDragging = true; this.dragStartSlotIndex = slotIndex; this.dragEndSlotIndex = slotIndex;
62
- }
63
-
64
- private handleMouseEnter(slotIndex: number, isBlocked: boolean) {
65
- if (this.isDragging && !isBlocked) this.dragEndSlotIndex = slotIndex;
66
- }
67
-
68
- private handleMouseUp = () => {
69
- if (!this.isDragging || !this.day || this.dragStartSlotIndex === null || this.dragEndSlotIndex === null) {
70
- this.isDragging = false; return;
71
- }
72
- this.isDragging = false;
73
- const startIdx = Math.min(this.dragStartSlotIndex, this.dragEndSlotIndex);
74
- const endIdx = Math.max(this.dragStartSlotIndex, this.dragEndSlotIndex);
75
- const rangeSlots = this.day.slots.slice(startIdx, endIdx + 1);
76
- if (!rangeSlots.some(s => s.isBlocked)) {
77
- this.dispatchEvent(new CustomEvent('range-select', {
78
- detail: { dateString: this.day.dateString, start: rangeSlots[0].start, end: rangeSlots[rangeSlots.length - 1].end },
79
- bubbles: true, composed: true
80
- }));
81
- }
82
- this.dragStartSlotIndex = null; this.dragEndSlotIndex = null;
83
- };
84
-
85
- private isSlotSelected(slot: TimeSlot): boolean {
86
- if (!this.day || !this.selectedRange || this.selectedRange.dateString !== this.day.dateString) return false;
87
- return slot.start >= this.selectedRange.start && slot.end <= this.selectedRange.end;
88
- }
89
-
90
- private isSlotDragSelecting(slotIndex: number): boolean {
91
- if (!this.isDragging) return false;
92
- const start = Math.min(this.dragStartSlotIndex!, this.dragEndSlotIndex!), end = Math.max(this.dragStartSlotIndex!, this.dragEndSlotIndex!);
93
- return slotIndex >= start && slotIndex <= end;
94
- }
95
-
96
- render() {
97
- if (!this.day) return html`<div>Cargando...</div>`;
98
- const isToday = this.day.dateString === new Date().toISOString().split('T')[0];
99
- const formattedDate = new Intl.DateTimeFormat(this.locale, { weekday: 'long', day: 'numeric', month: 'long' }).format(this.day.date);
100
-
101
- return html`
102
- <div class="time-col">
103
- <div class="header-cell">Hora</div>
104
- ${this.slots.map(slot => html`<div class="slot-cell time-slot-label">${slot.start}</div>`)}
105
- </div>
106
-
107
- <div class="day-col">
108
- <div class="header-cell ${isToday ? 'today' : ''}">
109
- <div style="text-transform: capitalize;">${formattedDate}</div>
110
- </div>
111
-
112
- ${this.day.slots.map((slot, sIdx) => {
113
- const blocked = slot.isBlocked, selected = this.isSlotSelected(slot), dragSelecting = this.isSlotDragSelecting(sIdx);
114
- return html`
115
- <div
116
- class=${classMap({
117
- 'slot-cell': true, 'available': !blocked, 'blocked': blocked, 'selected': selected, 'drag-selecting': dragSelecting
118
- })}
119
- @mousedown=${() => this.handleMouseDown(sIdx, blocked)}
120
- @mouseenter=${() => this.handleMouseEnter(sIdx, blocked)}
121
- title=${blocked && slot.rule?.description ? slot.rule.description : ''}
122
- >
123
- ${blocked ? (slot.rule?.description || 'Ocupado') : selected ? 'Reservado' : ''}
124
- </div>
125
- `;
126
- })}
127
- </div>
128
- `;
129
- }
130
- }
@@ -1,116 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
- import { classMap } from 'lit/directives/class-map.js';
4
- import { CalendarDay } from '@glatam/calendar-core';
5
-
6
- @customElement('glatam-calendar-month-view')
7
- export class GlatamCalendarMonthView extends LitElement {
8
- static styles = css`
9
- :host { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; width: 100%; }
10
- .weekday { font-size: 0.75rem; font-weight: 600; color: var(--glatam-text-secondary); text-transform: uppercase; text-align: center; padding: 8px 0; letter-spacing: 0.05em; }
11
- .day-cell {
12
- min-height: var(--glatam-day-min-height); border: 1px solid var(--glatam-border); border-radius: var(--glatam-grid-border-radius);
13
- padding: 8px; display: flex; flex-direction: column; justify-content: space-between; cursor: pointer;
14
- transition: background-color var(--glatam-transition-fast), border-color var(--glatam-transition-fast); background-color: var(--glatam-bg); position: relative;
15
- }
16
- .day-cell:hover:not(.blocked):not(.disabled) { background-color: var(--glatam-surface); border-color: var(--glatam-text-secondary); }
17
- .day-cell.padding { opacity: 0.4; }
18
- .day-cell.blocked {
19
- background: repeating-linear-gradient(45deg, var(--glatam-blocked-bg), var(--glatam-blocked-bg) 10px, var(--glatam-blocked-stripe) 10px, var(--glatam-blocked-stripe) 20px);
20
- border-color: var(--glatam-blocked-border); color: var(--glatam-blocked-text); cursor: not-allowed;
21
- }
22
- .day-cell.today { background-color: var(--glatam-today-bg); border-color: var(--glatam-primary); }
23
- .day-cell.selected { background-color: var(--glatam-selection-bg); border-color: var(--glatam-selection-border); box-shadow: inset 0 0 0 1px var(--glatam-selection-border); }
24
- .day-cell.disabled { opacity: 0.2; cursor: not-allowed; background-color: var(--glatam-surface); border-color: var(--glatam-border); pointer-events: none; }
25
- .day-cell.empty { border: none; background: transparent; cursor: default; pointer-events: none; }
26
- .day-number { font-size: 0.875rem; font-weight: 500; margin-bottom: 4px; }
27
- .day-cell.today .day-number { color: var(--glatam-today-text); font-weight: 700; }
28
- .slot-indicator { display: flex; flex-direction: column; gap: 3px; font-size: 0.7rem; overflow: hidden; margin-top: 4px; }
29
- .badge { padding: 2px 6px; border-radius: 4px; font-weight: 500; text-align: center; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
30
- .badge.blocked-day { background-color: rgba(255, 69, 58, 0.12); color: #ff453a; font-weight: 600; }
31
- .badge.blocked-slot { background-color: var(--glatam-surface); border: 1px solid var(--glatam-border); color: var(--glatam-text-secondary); }
32
- :host([size="small"]) .day-cell { min-height: 38px; padding: 4px; justify-content: center; align-items: center; }
33
- :host([size="small"]) .slot-indicator { display: none; }
34
- :host([size="small"]) .day-number { margin-bottom: 0; font-size: 0.8rem; }
35
- @media (max-width: 600px) {
36
- .day-cell { min-height: 38px; padding: 4px; justify-content: center; align-items: center; }
37
- .slot-indicator { display: none; }
38
- .day-number { margin-bottom: 0; font-size: 0.8rem; }
39
- }
40
- `;
41
-
42
- @property({ type: Array }) days: CalendarDay[] = [];
43
- @property({ type: String }) locale = 'es';
44
- @property({ type: Number }) startOfWeekDay = 0;
45
- @property({ type: Array }) selectedDates: string[] = [];
46
- @property({ type: String }) role = 'provider';
47
- @property({ type: String, reflect: true }) size = 'medium';
48
- @property({ type: String }) minDate = '';
49
- @property({ type: String }) maxDate = '';
50
- @property({ type: Boolean }) showNeighboringMonth = true;
51
- @property({ attribute: false }) tileClassName: ((data: { date: Date; dateString: string }) => string) | null = null;
52
-
53
- private getWeekdayNames(): string[] {
54
- const names: string[] = [];
55
- const baseDate = new Date(2026, 6, 12 + this.startOfWeekDay);
56
- const formatter = new Intl.DateTimeFormat(this.locale, { weekday: 'short' });
57
- for (let i = 0; i < 7; i++) {
58
- names.push(formatter.format(baseDate));
59
- baseDate.setDate(baseDate.getDate() + 1);
60
- }
61
- return names;
62
- }
63
-
64
- private handleDayClick(day: CalendarDay) {
65
- if (this.role === 'buyer' && day.isBlocked) return;
66
- this.dispatchEvent(new CustomEvent('day-select', {
67
- detail: { dateString: day.dateString, isBlocked: day.isBlocked },
68
- bubbles: true, composed: true
69
- }));
70
- }
71
-
72
- render() {
73
- const weekdayNames = this.getWeekdayNames();
74
-
75
- return html`
76
- ${weekdayNames.map(name => html`<div class="weekday">${name}</div>`)}
77
-
78
- ${this.days.map(day => {
79
- if (!day.isCurrentMonth && !this.showNeighboringMonth) {
80
- return html`<div class="day-cell empty"></div>`;
81
- }
82
-
83
- const isToday = day.dateString === new Date().toISOString().split('T')[0];
84
- const isSelected = this.selectedDates.includes(day.dateString);
85
- const blockedSlots = day.slots.filter(s => s.isBlocked);
86
- const isDisabled = (this.minDate && day.dateString < this.minDate) || (this.maxDate && day.dateString > this.maxDate);
87
- const customClass = this.tileClassName ? this.tileClassName({ date: day.date, dateString: day.dateString }) : '';
88
-
89
- return html`
90
- <div
91
- class=${classMap({
92
- 'day-cell': true, 'padding': !day.isCurrentMonth, 'blocked': day.isBlocked, 'today': isToday, 'selected': isSelected, 'disabled': !!isDisabled, [customClass]: !!customClass
93
- })}
94
- part="day-cell ${customClass}"
95
- @click=${() => this.handleDayClick(day)}
96
- >
97
- <div class="day-number">${day.dayNumber}</div>
98
-
99
- <div class="slot-indicator">
100
- ${day.isBlocked
101
- ? html`<div class="badge blocked-day">${day.rule?.description || 'Bloqueado'}</div>`
102
- : blockedSlots.slice(0, 2).map(slot => html`
103
- <div class="badge blocked-slot" title=${slot.rule?.description || ''}>
104
- 🚫 ${slot.rule?.description || slot.start}
105
- </div>
106
- `)}
107
- ${!day.isBlocked && blockedSlots.length > 2
108
- ? html`<div style="text-align: center; font-size: 0.65rem; color: var(--glatam-text-secondary);">+${blockedSlots.length - 2} tareas</div>`
109
- : ''}
110
- </div>
111
- </div>
112
- `;
113
- })}
114
- `;
115
- }
116
- }
@@ -1,158 +0,0 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property, state } from 'lit/decorators.js';
3
- import { classMap } from 'lit/directives/class-map.js';
4
- import { CalendarDay, TimeSlot } from '@glatam/calendar-core';
5
-
6
- @customElement('glatam-calendar-week-view')
7
- export class GlatamCalendarWeekView extends LitElement {
8
- static styles = css`
9
- :host {
10
- display: grid; grid-template-columns: var(--glatam-time-col-width) repeat(7, 1fr); gap: 2px;
11
- width: 100%; background-color: var(--glatam-border); border: 1px solid var(--glatam-border);
12
- border-radius: var(--glatam-grid-border-radius); overflow: hidden; user-select: none;
13
- }
14
- @media (max-width: 600px) {
15
- :host {
16
- grid-template-columns: 40px repeat(7, 1fr);
17
- overflow-x: hidden;
18
- }
19
- .slot-cell {
20
- font-size: 0.65rem;
21
- }
22
- .slot-cell.blocked, .slot-cell.selected {
23
- font-size: 0;
24
- padding: 0;
25
- }
26
- .time-slot-label {
27
- font-size: 0.65rem;
28
- }
29
- .header-cell {
30
- font-size: 0.7rem;
31
- }
32
- .header-cell .day-num {
33
- font-size: 0.8rem;
34
- }
35
- }
36
- .time-col { background-color: var(--glatam-surface); display: flex; flex-direction: column; }
37
- .day-col { background-color: var(--glatam-bg); display: flex; flex-direction: column; }
38
- .header-cell {
39
- height: 60px; display: flex; flex-direction: column; align-items: center; justify-content: center;
40
- background-color: var(--glatam-surface); border-bottom: 2px solid var(--glatam-border);
41
- font-size: 0.8rem; font-weight: 500; text-transform: capitalize;
42
- }
43
- .header-cell.today { color: var(--glatam-primary); font-weight: 700; }
44
- .header-cell .day-num { font-size: 0.95rem; font-weight: 600; margin-top: 2px; }
45
- .slot-cell {
46
- height: var(--glatam-slot-height); border-bottom: 1px solid var(--glatam-border);
47
- display: flex; align-items: center; justify-content: center; font-size: 0.75rem;
48
- cursor: pointer; transition: background-color var(--glatam-transition-fast); position: relative;
49
- }
50
- .time-slot-label { font-size: 0.7rem; color: var(--glatam-text-secondary); }
51
- .slot-cell.available:hover { background-color: var(--glatam-surface); }
52
- .slot-cell.blocked {
53
- background-color: var(--glatam-blocked-bg);
54
- background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, var(--glatam-blocked-stripe) 5px, var(--glatam-blocked-stripe) 10px);
55
- color: var(--glatam-blocked-text); cursor: not-allowed; font-weight: 600; padding: 0 4px;
56
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center;
57
- }
58
- .slot-cell.selected { background-color: var(--glatam-selection-bg); border-left: 3px solid var(--glatam-selection-border); }
59
- .slot-cell.drag-selecting { background-color: var(--glatam-primary-light); }
60
- `;
61
-
62
- @property({ type: Array }) days: CalendarDay[] = [];
63
- @property({ type: Array }) slots: TimeSlot[] = [];
64
- @property({ type: String }) locale = 'es';
65
- @property({ type: Object }) selectedRange: { dateString: string; start: string; end: string } | null = null;
66
- @property({ type: String }) role = 'provider';
67
-
68
- @state() private isDragging = false;
69
- @state() private dragDayIndex: number | null = null;
70
- @state() private dragStartSlotIndex: number | null = null;
71
- @state() private dragEndSlotIndex: number | null = null;
72
-
73
- connectedCallback() { super.connectedCallback(); window.addEventListener('mouseup', this.handleMouseUp); }
74
- disconnectedCallback() { window.removeEventListener('mouseup', this.handleMouseUp); super.disconnectedCallback(); }
75
-
76
- private handleMouseDown(dayIndex: number, slotIndex: number, isBlocked: boolean) {
77
- if (isBlocked) {
78
- if (this.role === 'buyer') return;
79
- this.dispatchEvent(new CustomEvent('slot-click', {
80
- detail: { dateString: this.days[dayIndex].dateString, slot: this.days[dayIndex].slots[slotIndex], isBlocked: true },
81
- bubbles: true, composed: true
82
- }));
83
- return;
84
- }
85
- this.isDragging = true; this.dragDayIndex = dayIndex; this.dragStartSlotIndex = slotIndex; this.dragEndSlotIndex = slotIndex;
86
- }
87
-
88
- private handleMouseEnter(dayIndex: number, slotIndex: number, isBlocked: boolean) {
89
- if (this.isDragging && dayIndex === this.dragDayIndex && !isBlocked) this.dragEndSlotIndex = slotIndex;
90
- }
91
-
92
- private handleMouseUp = () => {
93
- if (!this.isDragging || this.dragDayIndex === null || this.dragStartSlotIndex === null || this.dragEndSlotIndex === null) {
94
- this.isDragging = false; return;
95
- }
96
- this.isDragging = false;
97
- const startIdx = Math.min(this.dragStartSlotIndex, this.dragEndSlotIndex);
98
- const endIdx = Math.max(this.dragStartSlotIndex, this.dragEndSlotIndex);
99
- const day = this.days[this.dragDayIndex], rangeSlots = day.slots.slice(startIdx, endIdx + 1);
100
- if (!rangeSlots.some(s => s.isBlocked)) {
101
- this.dispatchEvent(new CustomEvent('range-select', {
102
- detail: { dateString: day.dateString, start: rangeSlots[0].start, end: rangeSlots[rangeSlots.length - 1].end },
103
- bubbles: true, composed: true
104
- }));
105
- }
106
- this.dragDayIndex = null; this.dragStartSlotIndex = null; this.dragEndSlotIndex = null;
107
- };
108
-
109
- private isSlotSelected(dayString: string, slot: TimeSlot): boolean {
110
- if (!this.selectedRange || this.selectedRange.dateString !== dayString) return false;
111
- return slot.start >= this.selectedRange.start && slot.end <= this.selectedRange.end;
112
- }
113
-
114
- private isSlotDragSelecting(dayIndex: number, slotIndex: number): boolean {
115
- if (!this.isDragging || dayIndex !== this.dragDayIndex) return false;
116
- const start = Math.min(this.dragStartSlotIndex!, this.dragEndSlotIndex!), end = Math.max(this.dragStartSlotIndex!, this.dragEndSlotIndex!);
117
- return slotIndex >= start && slotIndex <= end;
118
- }
119
-
120
- render() {
121
- const todayStr = new Date().toISOString().split('T')[0];
122
-
123
- return html`
124
- <div class="time-col">
125
- <div class="header-cell">Hora</div>
126
- ${this.slots.map(slot => html`<div class="slot-cell time-slot-label">${slot.start}</div>`)}
127
- </div>
128
-
129
- ${this.days.map((day, dIdx) => {
130
- const isToday = day.dateString === todayStr, dayLabel = new Intl.DateTimeFormat(this.locale, { weekday: 'short' }).format(day.date);
131
- return html`
132
- <div class="day-col">
133
- <div class="header-cell ${isToday ? 'today' : ''}">
134
- <div>${dayLabel}</div>
135
- <div class="day-num">${day.dayNumber}</div>
136
- </div>
137
-
138
- ${day.slots.map((slot, sIdx) => {
139
- const blocked = slot.isBlocked, selected = this.isSlotSelected(day.dateString, slot), dragSelecting = this.isSlotDragSelecting(dIdx, sIdx);
140
- return html`
141
- <div
142
- class=${classMap({
143
- 'slot-cell': true, 'available': !blocked, 'blocked': blocked, 'selected': selected, 'drag-selecting': dragSelecting
144
- })}
145
- @mousedown=${() => this.handleMouseDown(dIdx, sIdx, blocked)}
146
- @mouseenter=${() => this.handleMouseEnter(dIdx, sIdx, blocked)}
147
- title=${blocked && slot.rule?.description ? slot.rule.description : ''}
148
- >
149
- ${blocked ? (slot.rule?.description || 'Ocupado') : selected ? 'Reservado' : ''}
150
- </div>
151
- `;
152
- })}
153
- </div>
154
- `;
155
- })}
156
- `;
157
- }
158
- }
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"]
8
- }
package/vite.config.ts DELETED
@@ -1,35 +0,0 @@
1
- import { defineConfig } from 'vite';
2
- import dts from 'vite-plugin-dts';
3
- import { resolve } from 'path';
4
-
5
- export default defineConfig({
6
- build: {
7
- lib: {
8
- entry: resolve(__dirname, 'src/index.ts'),
9
- name: 'GlatamCalendar',
10
- fileName: (format) => `glatam-calendar.${format === 'es' ? 'js' : 'cjs'}`,
11
- formats: ['es', 'cjs'],
12
- },
13
- rollupOptions: {
14
- external: [
15
- 'lit',
16
- 'lit/decorators.js',
17
- 'lit/directives/class-map.js',
18
- 'lit/directives/style-map.js',
19
- '@glatam/calendar-core',
20
- ],
21
- output: {
22
- globals: {
23
- lit: 'Lit',
24
- '@glatam/calendar-core': 'GlatamCalendarCore',
25
- },
26
- },
27
- },
28
- },
29
- plugins: [
30
- dts({
31
- insertTypesEntry: true,
32
- tsconfigPath: './tsconfig.json',
33
- }),
34
- ],
35
- });