@getdom/studio 0.1.1 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +31 -0
- package/SKILL.md +21 -1
- package/package.json +4 -2
- package/public/llms.txt +13 -2
- package/src/pages/_layout/componentCatalog.generated.js +1185 -44
- package/src/pages/_layout/componentLoaders.js +41 -0
- package/src/pages/_layout/componentManager.js +38 -27
- package/src/pages/_layout/componentRuntimeRecords.js +3 -1
- package/src/pages/charts/_shared/ChartContainer.vue +67 -0
- package/src/pages/charts/_shared/ChartLegend.vue +22 -0
- package/src/pages/charts/_shared/ChartTooltip.vue +18 -0
- package/src/pages/charts/_shared/DonutRenderer.vue +33 -0
- package/src/pages/charts/_shared/XYChart.vue +140 -0
- package/src/pages/charts/_shared/XYRenderer.vue +90 -0
- package/src/pages/charts/_shared/chartProps.js +23 -0
- package/src/pages/charts/_shared/chartUtils.js +95 -0
- package/src/pages/charts/area-chart/DomAreaChart.vue +27 -0
- package/src/pages/charts/bar-chart/DomBarChart.vue +29 -0
- package/src/pages/charts/date-range-picker/DomDateRangePicker.vue +104 -0
- package/src/pages/charts/date-range-picker/dateRange.js +69 -0
- package/src/pages/charts/donut-chart/DomDonutChart.vue +117 -0
- package/src/pages/charts/line-chart/DomLineChart.vue +26 -0
- package/src/pages/charts/metric-card/DomMetricCard.vue +49 -0
- package/src/pages/charts/sparkline/DomSparkline.vue +30 -0
- package/src/pages/components/_shared/media/MediaDeviceSelect.vue +28 -0
- package/src/pages/components/_shared/media/useMediaInput.js +200 -0
- package/src/pages/components/audio-controls/DomAudioControls.vue +91 -0
- package/src/pages/components/audio-input/DomAudioInput.vue +58 -0
- package/src/pages/components/button/DomButton.vue +10 -4
- package/src/pages/components/button/buttonStyles.js +5 -0
- package/src/pages/components/camera-input/DomCameraInput.vue +75 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +12 -2
- package/src/pages/components/dialog/DomDialog.vue +338 -36
- package/src/pages/components/icon-button/DomIconButton.vue +8 -2
- package/src/pages/components/menu-stack/DomMenuStack.vue +111 -0
- package/src/pages/components/menu-stack/menuTree.js +35 -0
- package/src/pages/components/popover/DomPopover.vue +1 -53
- package/src/pages/components/popover/popover.css +50 -0
- package/src/pages/components/video-feed/DomVideoFeed.vue +551 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +1 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +1 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +1 -0
- package/src/pages/data/gantt/DomGantt.vue +788 -0
- package/src/pages/data/gantt/ganttTime.js +223 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +84 -9
- package/src/pages/data/notification-inbox/DomNotificationInbox.vue +505 -0
- package/src/pages/data/notification-inbox/notificationPresentation.js +34 -0
- package/src/pages/designer/_shared/ControlIconButton.vue +62 -0
- package/src/pages/designer/_shared/StyleControlSection.vue +59 -0
- package/src/pages/designer/_shared/cssEditorContext.js +19 -0
- package/src/pages/designer/backdrop-editor/BackdropControl.vue +57 -0
- package/src/pages/designer/backdrop-editor/DomBackdropEditor.vue +33 -0
- package/src/pages/designer/backdrop-editor/backdropModel.js +39 -0
- package/src/pages/designer/background-editor/BackgroundLayers.vue +210 -0
- package/src/pages/designer/background-editor/DomBackgroundEditor.vue +33 -0
- package/src/pages/designer/background-editor/backgroundModel.js +102 -0
- package/src/pages/designer/border-editor/BorderControl.vue +72 -0
- package/src/pages/designer/border-editor/DomBorderEditor.vue +34 -0
- package/src/pages/designer/border-editor/borderModel.js +21 -0
- package/src/pages/designer/color-swatch/DomColorSwatch.vue +95 -0
- package/src/pages/designer/frame/DomDesignerFrame.vue +16 -0
- package/src/pages/designer/frame/DomFrameLayoutEditor.vue +41 -0
- package/src/pages/designer/frame/frameLayout.js +93 -0
- package/src/pages/designer/layers/DomDesignerLayers.vue +11 -0
- package/src/pages/designer/shadow-editor/DomShadowEditor.vue +32 -0
- package/src/pages/designer/shadow-editor/ShadowLayers.vue +185 -0
- package/src/pages/designer/shadow-editor/shadowModel.js +73 -0
- package/src/pages/designer/stage/DesignerBounds.vue +31 -0
- package/src/pages/designer/stage/DomDesignerStage.vue +237 -0
- package/src/pages/designer/stage/SpacingGuides.vue +33 -0
- package/src/pages/designer/stage/resizeGeometry.js +15 -0
- package/src/pages/designer/stage/snapGeometry.js +69 -0
- package/src/pages/designer/stage/snapPipeline.js +17 -0
- package/src/pages/designer/stage/spacingGeometry.js +129 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +8 -3
- package/src/pages/forms/number-input/DomNumberInput.vue +39 -3
- package/src/pages/forms/range-input/DomRangeInput.vue +75 -23
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +91 -9
- package/src/pages/forms/toggle/DomToggle.vue +1 -1
- package/src/pages/lib/headless/base.js +10 -5
- package/src/pages/lib/headless/dialog.js +25 -6
- package/src/pages/lib/headless/drawer.js +49 -21
- package/src/pages/lib/vue/index.d.ts +1161 -14
- package/src/pages/lib/vue/index.js +43 -0
- package/src/pages/mobile/_shared/interactionMath.js +46 -0
- package/src/pages/mobile/_shared/useKeyboardViewport.js +91 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +102 -65
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +4 -2
- package/src/pages/mobile/bottom-sheet/DomBottomSheet.vue +221 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +6 -6
- package/src/pages/mobile/profile/DomAppProfile.vue +42 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +6 -6
- package/src/pages/mobile/shell/DomAppShell.vue +60 -9
- package/src/pages/mobile/stack/DomAppStack.vue +214 -55
- package/src/pages/mobile/swipe-actions/DomSwipeActions.vue +346 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +3 -3
- package/src/pages/visual/audio-visualizer/DomAudioVisualizer.vue +143 -0
- package/src/theme.css +13 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { resolveResourceKey, resolveResourceValue } from '../_shared/resourceRecord.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses an instant or local calendar date without interpreting YYYY-MM-DD as UTC.
|
|
5
|
+
* Invalid, missing, and overflowing calendar dates return null.
|
|
6
|
+
* @param {unknown} value Date, timestamp, or ISO-compatible string.
|
|
7
|
+
* @returns {Date|null} Independent valid date.
|
|
8
|
+
*/
|
|
9
|
+
export function parseGanttDate(value) {
|
|
10
|
+
if (value === null || value === undefined || value === '') return null;
|
|
11
|
+
let date;
|
|
12
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
13
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
14
|
+
date = new Date(year, month - 1, day);
|
|
15
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
|
|
16
|
+
} else if (value instanceof Date || typeof value === 'string' || typeof value === 'number') {
|
|
17
|
+
date = new Date(value);
|
|
18
|
+
}
|
|
19
|
+
return date && Number.isFinite(date.getTime()) ? date : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Advances local calendar days, preserving wall-clock time across DST boundaries.
|
|
24
|
+
* @param {Date} date Starting date.
|
|
25
|
+
* @param {number} days Number of calendar days.
|
|
26
|
+
* @returns {Date} Shifted copy.
|
|
27
|
+
*/
|
|
28
|
+
export function addGanttDays(date, days) {
|
|
29
|
+
const result = new Date(date);
|
|
30
|
+
result.setDate(result.getDate() + days);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates fixed-width visible cells; omitted time retains its original timestamps.
|
|
36
|
+
* Hour cells follow elapsed time so DST days contain their actual 23 or 25 hours.
|
|
37
|
+
* @param {object} options Range, zoom, workingHours, weekendDays, and hiding flags.
|
|
38
|
+
* @returns {{cells: Array<object>, width: number, cellWidth: number, start: Date, end: Date}} Axis geometry.
|
|
39
|
+
*/
|
|
40
|
+
export function createGanttAxis(options) {
|
|
41
|
+
const start = parseGanttDate(options.startDate) || new Date();
|
|
42
|
+
start.setHours(0, 0, 0, 0);
|
|
43
|
+
const days = Math.min(3660, Math.max(1, Math.floor(Number(options.days) || 84)));
|
|
44
|
+
const end = addGanttDays(start, days);
|
|
45
|
+
const hourly = options.zoom === 'days';
|
|
46
|
+
const cellWidth = hourly ? 32 : options.zoom === 'months' ? 14 : 48;
|
|
47
|
+
const workingHours = options.workingHours || [6, 18];
|
|
48
|
+
const hourStart = Math.max(0, Math.min(23, Number(workingHours[0]) || 0));
|
|
49
|
+
const hourEnd = Math.max(hourStart + 1, Math.min(24, Number(workingHours[1]) || 24));
|
|
50
|
+
const weekends = Array.isArray(options.weekendDays) ? options.weekendDays : [0, 6];
|
|
51
|
+
const cells = [];
|
|
52
|
+
for (let day = start; day < end; day = addGanttDays(day, 1)) {
|
|
53
|
+
const weekend = weekends.includes(day.getDay());
|
|
54
|
+
if (weekend && options.hideWeekends) continue;
|
|
55
|
+
const nextDay = addGanttDays(day, 1);
|
|
56
|
+
for (let cursor = day; cursor < nextDay;) {
|
|
57
|
+
const next = hourly ? new Date(Math.min(+cursor + 3600000, +nextDay)) : nextDay;
|
|
58
|
+
const outsideHours = hourly && (cursor.getHours() < hourStart || cursor.getHours() >= hourEnd);
|
|
59
|
+
if (!(outsideHours && options.hideNonWorkingHours)) {
|
|
60
|
+
cells.push({ start: +cursor, end: +next, left: cells.length * cellWidth, width: cellWidth, muted: weekend || outsideHours });
|
|
61
|
+
}
|
|
62
|
+
cursor = next;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { cells, width: cells.length * cellWidth, cellWidth, start, end };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Maps an instant to compressed-axis pixels. Hidden periods map to their boundary.
|
|
70
|
+
* @param {object} axis Result of createGanttAxis.
|
|
71
|
+
* @param {number|Date} value Instant to locate.
|
|
72
|
+
* @returns {number} Clamped horizontal coordinate.
|
|
73
|
+
*/
|
|
74
|
+
export function ganttDateToX(axis, value) {
|
|
75
|
+
const time = +value;
|
|
76
|
+
let low = 0;
|
|
77
|
+
let high = axis.cells.length;
|
|
78
|
+
while (low < high) {
|
|
79
|
+
const middle = (low + high) >> 1;
|
|
80
|
+
if (axis.cells[middle].end <= time) low = middle + 1;
|
|
81
|
+
else high = middle;
|
|
82
|
+
}
|
|
83
|
+
const cell = axis.cells[low];
|
|
84
|
+
if (!cell) return axis.width;
|
|
85
|
+
return cell.left + Math.max(0, (time - cell.start) / (cell.end - cell.start)) * cell.width;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Converts pixels back into real time, distinguishing range starts from ends at a gap.
|
|
90
|
+
* @param {object} axis Visible axis.
|
|
91
|
+
* @param {number} x Horizontal coordinate.
|
|
92
|
+
* @param {'start'|'end'} edge Boundary bias at omitted periods.
|
|
93
|
+
* @returns {number|null} Timestamp, or null for an empty axis.
|
|
94
|
+
*/
|
|
95
|
+
export function ganttXToDate(axis, x, edge = 'start') {
|
|
96
|
+
if (!axis.cells.length) return null;
|
|
97
|
+
const bounded = Math.max(0, Math.min(axis.width, x));
|
|
98
|
+
const index = Math.min(axis.cells.length - 1, Math.max(0, edge === 'end' ? Math.ceil(bounded / axis.cellWidth) - 1 : Math.floor(bounded / axis.cellWidth)));
|
|
99
|
+
const cell = axis.cells[index];
|
|
100
|
+
return cell.start + ((bounded - cell.left) / cell.width) * (cell.end - cell.start);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolves an item span. Explicit ends take precedence; durations must be positive.
|
|
105
|
+
* Day durations are whole local calendar days; hours/minutes are elapsed time.
|
|
106
|
+
* @param {object} item Application-owned record.
|
|
107
|
+
* @param {object} fields Field resolvers and durationUnit.
|
|
108
|
+
* @param {number} index Source index supplied to resolvers.
|
|
109
|
+
* @returns {{start: number, end: number}|null} Valid exclusive-end interval.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveGanttSpan(item, fields, index = 0) {
|
|
112
|
+
const start = parseGanttDate(resolveResourceValue(item, fields.startKey, index));
|
|
113
|
+
if (!start) return null;
|
|
114
|
+
const rawEnd = resolveResourceValue(item, fields.endKey, index);
|
|
115
|
+
let end = parseGanttDate(rawEnd);
|
|
116
|
+
if (rawEnd === undefined || rawEnd === null || rawEnd === '') {
|
|
117
|
+
const duration = Number(resolveResourceValue(item, fields.durationKey, index));
|
|
118
|
+
if (!Number.isFinite(duration) || duration <= 0) return null;
|
|
119
|
+
if (fields.durationUnit === 'days') {
|
|
120
|
+
if (!Number.isInteger(duration)) return null;
|
|
121
|
+
end = addGanttDays(start, duration);
|
|
122
|
+
} else {
|
|
123
|
+
end = new Date(+start + duration * (fields.durationUnit === 'minutes' ? 60000 : 3600000));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return end && +end > +start ? { start: +start, end: +end } : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Projects item records into lane geometry and stacks overlapping spans.
|
|
131
|
+
* Source records are never sorted in place or mutated. Invalid/off-axis items are omitted.
|
|
132
|
+
* @param {Array<object>} rows Application-owned resource or feature rows.
|
|
133
|
+
* @param {Array<object>} items Application-owned scheduled items.
|
|
134
|
+
* @param {object} fields Logical field mappings and rowHeight.
|
|
135
|
+
* @param {object} axis Visible axis.
|
|
136
|
+
* @returns {Array<object>} Rows retaining source identity, with stacked item records.
|
|
137
|
+
*/
|
|
138
|
+
export function layoutGanttRows(rows, items, fields, axis) {
|
|
139
|
+
const groups = new Map();
|
|
140
|
+
for (const [index, item] of items.entries()) {
|
|
141
|
+
const span = resolveGanttSpan(item, fields, index);
|
|
142
|
+
if (!span) continue;
|
|
143
|
+
const left = ganttDateToX(axis, span.start);
|
|
144
|
+
const right = ganttDateToX(axis, span.end);
|
|
145
|
+
if (right <= left) continue;
|
|
146
|
+
const rowId = resolveResourceValue(item, fields.itemRowKey, index);
|
|
147
|
+
const group = groups.get(rowId) || [];
|
|
148
|
+
group.push({ item, key: resolveResourceKey(item, fields.itemKey, index), title: resolveResourceValue(item, fields.titleKey, index), tone: resolveResourceValue(item, fields.toneKey, index), ...span, left, width: right - left });
|
|
149
|
+
groups.set(rowId, group);
|
|
150
|
+
}
|
|
151
|
+
return rows.map((row, index) => {
|
|
152
|
+
const key = resolveResourceKey(row, fields.rowKey, index);
|
|
153
|
+
const records = (groups.get(key) || []).sort((a, b) => a.start - b.start || a.end - b.end);
|
|
154
|
+
const tracks = [];
|
|
155
|
+
for (const record of records) {
|
|
156
|
+
let track = tracks.findIndex((end) => end <= record.left);
|
|
157
|
+
if (track === -1) track = tracks.length;
|
|
158
|
+
tracks[track] = record.left + record.width;
|
|
159
|
+
record.top = 12 + track * 36;
|
|
160
|
+
}
|
|
161
|
+
return { row, key, label: resolveResourceValue(row, fields.rowLabelKey, index), records, height: Math.max(Number(fields.rowHeight) || 64, 24 + tracks.length * 36) };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolves the shared movement, resize, and creation increment in axis pixels.
|
|
167
|
+
* @param {object} axis Visible time axis.
|
|
168
|
+
* @param {string} zoom Current zoom level.
|
|
169
|
+
* @param {number} selectionStep Hourly-view increment in minutes.
|
|
170
|
+
* @returns {number} Positive pixel increment.
|
|
171
|
+
*/
|
|
172
|
+
export function ganttSnapWidth(axis, zoom, selectionStep = 30) {
|
|
173
|
+
return axis.cellWidth * (zoom === 'days' ? Math.min(60, Math.max(1, Number(selectionStep) || 30)) / 60 : 1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Moves a span by snapped pointer distance without changing its source record.
|
|
178
|
+
* Week/month movement preserves both local wall-clock endpoints across DST;
|
|
179
|
+
* hourly movement preserves elapsed duration. Hidden cells affect where the
|
|
180
|
+
* pointer lands, but are never subtracted from the stored interval.
|
|
181
|
+
* @param {object} axis Visible time axis.
|
|
182
|
+
* @param {{start: number, end: number}} span Original exclusive interval.
|
|
183
|
+
* @param {number} anchorX Axis coordinate where the item was grabbed.
|
|
184
|
+
* @param {number} deltaX Pointer travel in axis pixels, including scrolling.
|
|
185
|
+
* @param {string} zoom Current zoom.
|
|
186
|
+
* @param {number} selectionStep Hourly increment in minutes.
|
|
187
|
+
* @returns {{start: number, end: number}} Proposed interval.
|
|
188
|
+
*/
|
|
189
|
+
export function moveGanttSpan(axis, span, anchorX, deltaX, zoom, selectionStep = 30) {
|
|
190
|
+
const step = ganttSnapWidth(axis, zoom, selectionStep);
|
|
191
|
+
const delta = Math.round(deltaX / step) * step;
|
|
192
|
+
if (!axis.cells.length || !delta) return { start: span.start, end: span.end };
|
|
193
|
+
const anchor = Math.max(0, Math.min(axis.width - step, Math.floor(anchorX / step) * step));
|
|
194
|
+
const target = Math.max(0, Math.min(axis.width - step, anchor + delta));
|
|
195
|
+
const from = ganttXToDate(axis, anchor);
|
|
196
|
+
const to = ganttXToDate(axis, target);
|
|
197
|
+
if (zoom === 'days') return { start: span.start + to - from, end: span.end + to - from };
|
|
198
|
+
const first = new Date(from);
|
|
199
|
+
const last = new Date(to);
|
|
200
|
+
const days = (Date.UTC(last.getFullYear(), last.getMonth(), last.getDate()) - Date.UTC(first.getFullYear(), first.getMonth(), first.getDate())) / 86400000;
|
|
201
|
+
return { start: +addGanttDays(new Date(span.start), days), end: +addGanttDays(new Date(span.end), days) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Adjusts only an item's exclusive end using the shared visible-time increment.
|
|
206
|
+
* A changed span retains at least one visible selection increment. The true end
|
|
207
|
+
* is preserved for zero movement, including ends inside hidden nights/weekends.
|
|
208
|
+
* @param {object} axis Visible time axis.
|
|
209
|
+
* @param {{start: number, end: number}} span Original exclusive interval.
|
|
210
|
+
* @param {number} deltaX Pointer travel from the original right edge.
|
|
211
|
+
* @param {string} zoom Current zoom.
|
|
212
|
+
* @param {number} selectionStep Hourly increment in minutes.
|
|
213
|
+
* @returns {{start: number, end: number}} Proposed interval.
|
|
214
|
+
*/
|
|
215
|
+
export function resizeGanttSpan(axis, span, deltaX, zoom, selectionStep = 30) {
|
|
216
|
+
const step = ganttSnapWidth(axis, zoom, selectionStep);
|
|
217
|
+
const delta = Math.round(deltaX / step) * step;
|
|
218
|
+
if (!axis.cells.length || !delta) return { start: span.start, end: span.end };
|
|
219
|
+
const minimum = Math.min(axis.width, ganttDateToX(axis, span.start) + step);
|
|
220
|
+
const position = Math.max(minimum, Math.min(axis.width, ganttDateToX(axis, span.end) + delta));
|
|
221
|
+
const end = ganttXToDate(axis, position, 'end');
|
|
222
|
+
return { start: span.start, end: end > span.start ? end : span.end };
|
|
223
|
+
}
|
|
@@ -245,6 +245,36 @@ export const monthCalendarProps = {
|
|
|
245
245
|
default: true,
|
|
246
246
|
_edit: { description: 'Render weekday headers.' },
|
|
247
247
|
},
|
|
248
|
+
stickyDayHeaders: {
|
|
249
|
+
type: Boolean,
|
|
250
|
+
default: null,
|
|
251
|
+
_edit: { description: 'Pin weekday headers to the active scrollport, below sticky month titles. When omitted, enabled in continuous mode and disabled in separated months.' },
|
|
252
|
+
},
|
|
253
|
+
showDayNames: {
|
|
254
|
+
type: Boolean,
|
|
255
|
+
default: true,
|
|
256
|
+
_edit: { description: 'Show the weekday name beside the date inside each day cell, including the mobile day list.' },
|
|
257
|
+
},
|
|
258
|
+
todayIndicator: {
|
|
259
|
+
type: String,
|
|
260
|
+
default: 'cell',
|
|
261
|
+
/**
|
|
262
|
+
* Accepts supported visual treatments for the current date.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} value Requested indicator style.
|
|
265
|
+
* @returns {boolean} Whether the indicator is supported.
|
|
266
|
+
*/
|
|
267
|
+
validator: (value) => ['cell', 'circle', 'none'].includes(value),
|
|
268
|
+
_edit: { group: 'Styling', options: ['cell', 'circle', 'none'], description: 'Highlight today with a cell outline, a coloured circle behind the date, or no visual indicator.' },
|
|
269
|
+
},
|
|
270
|
+
/**
|
|
271
|
+
* @type {MonthCalendarDayClass}
|
|
272
|
+
*/
|
|
273
|
+
todayDateClass: {
|
|
274
|
+
type: [String, Array, Object, Function],
|
|
275
|
+
default: 'bg-primary text-primary-fg',
|
|
276
|
+
_edit: { group: 'Styling', description: 'Classes for today’s date circle. Replaces the default primary background and foreground colours when todayIndicator is circle.' },
|
|
277
|
+
},
|
|
248
278
|
/**
|
|
249
279
|
* @type {MonthCalendarDayClass}
|
|
250
280
|
*/
|
|
@@ -302,6 +332,8 @@ const dragState = ref(createEmptyDragState());
|
|
|
302
332
|
const activeContinuousMonthValue = ref('');
|
|
303
333
|
const calendarRoot = ref(null);
|
|
304
334
|
const externalContinuousScrollHost = ref(null);
|
|
335
|
+
let monthHeaderResizeObserver = null;
|
|
336
|
+
const resolvedStickyDayHeaders = computed(() => props.stickyDayHeaders ?? props.continuous);
|
|
305
337
|
const resolvedWeekStartsOn = computed(() => clampWeekStartsOn(props.weekStartsOn));
|
|
306
338
|
const defaultMutedDayClass = 'bg-secondary/20 text-muted-fg/55';
|
|
307
339
|
const rangeStart = computed(() => parseDate(props.startDate));
|
|
@@ -367,12 +399,16 @@ const wideWeekGridColumns = computed(() => {
|
|
|
367
399
|
|
|
368
400
|
onMounted(() => {
|
|
369
401
|
nextTick(configureExternalContinuousScrollTracking);
|
|
402
|
+
nextTick(configureMonthHeaderOffsets);
|
|
370
403
|
});
|
|
371
404
|
|
|
372
405
|
onBeforeUnmount(() => {
|
|
373
406
|
removeExternalContinuousScrollTracking();
|
|
407
|
+
monthHeaderResizeObserver?.disconnect();
|
|
374
408
|
});
|
|
375
409
|
|
|
410
|
+
watch([calendarSections, () => props.stickyMonthHeaders], () => nextTick(configureMonthHeaderOffsets), { flush: 'post' });
|
|
411
|
+
|
|
376
412
|
watch(
|
|
377
413
|
() => [props.continuous, props.containedContinuousScroll, props.startDate, props.months, props.weekStartsOn],
|
|
378
414
|
() => {
|
|
@@ -678,6 +714,7 @@ function rowHeaderLabelFor(week) {
|
|
|
678
714
|
* @returns {Date|null} First day of the configured month, or null when the props are incomplete.
|
|
679
715
|
*/
|
|
680
716
|
function initialDateFromProps() {
|
|
717
|
+
if (props.initialYear == null || props.initialMonth == null) return null;
|
|
681
718
|
const year = Number(props.initialYear);
|
|
682
719
|
const month = Number(props.initialMonth);
|
|
683
720
|
if (!Number.isFinite(year) || !Number.isFinite(month)) return null;
|
|
@@ -1354,6 +1391,33 @@ function onSectionScroll(section, event) {
|
|
|
1354
1391
|
updateActiveContinuousMonthForScrollHost(scrollHost, scrollHost);
|
|
1355
1392
|
}
|
|
1356
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* Measures month headings so sticky weekday rows clear wrapped titles and slots.
|
|
1396
|
+
* Reconnects after range or layout changes and releases the previous observer.
|
|
1397
|
+
*
|
|
1398
|
+
* @returns {void}
|
|
1399
|
+
*/
|
|
1400
|
+
function configureMonthHeaderOffsets() {
|
|
1401
|
+
monthHeaderResizeObserver?.disconnect();
|
|
1402
|
+
if (!calendarRoot.value || typeof ResizeObserver === 'undefined') return;
|
|
1403
|
+
monthHeaderResizeObserver = new ResizeObserver(updateMonthHeaderOffsets);
|
|
1404
|
+
for (const header of calendarRoot.value.querySelectorAll('[data-month-heading]')) {
|
|
1405
|
+
monthHeaderResizeObserver.observe(header);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Stores each heading's actual height on its section for sibling sticky rows.
|
|
1411
|
+
*
|
|
1412
|
+
* @param {ResizeObserverEntry[]} entries Month headings whose sizes changed.
|
|
1413
|
+
* @returns {void}
|
|
1414
|
+
*/
|
|
1415
|
+
function updateMonthHeaderOffsets(entries) {
|
|
1416
|
+
for (const { target } of entries) {
|
|
1417
|
+
target.parentElement?.style.setProperty('--dom-month-calendar-month-heading-height', `${target.getBoundingClientRect().height}px`);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1357
1421
|
/**
|
|
1358
1422
|
* Attaches active-month tracking to the nearest parent scrollport when continuous mode is externally scrolled.
|
|
1359
1423
|
*
|
|
@@ -1437,7 +1501,8 @@ function updateActiveContinuousMonthForScrollHost(scrollHost, scope) {
|
|
|
1437
1501
|
|
|
1438
1502
|
const hostTop = scrollHost === window ? 0 : scrollHost.getBoundingClientRect().top;
|
|
1439
1503
|
const header = scope.querySelector('[data-continuous-day-headers]');
|
|
1440
|
-
const
|
|
1504
|
+
const stickyHeader = header instanceof HTMLElement && resolvedStickyDayHeaders.value && getComputedStyle(header).display !== 'none';
|
|
1505
|
+
const headerHeight = stickyHeader ? header.getBoundingClientRect().height : 0;
|
|
1441
1506
|
const activationTop = hostTop + headerHeight + 1;
|
|
1442
1507
|
const activeRow = scopedRows.reduce((current, row) => {
|
|
1443
1508
|
if (!(row instanceof HTMLElement)) return current;
|
|
@@ -1484,6 +1549,7 @@ function onDayClick(cell, event) {
|
|
|
1484
1549
|
>
|
|
1485
1550
|
<div
|
|
1486
1551
|
v-if="section.continuous && stickyMonthHeaders"
|
|
1552
|
+
data-month-heading
|
|
1487
1553
|
class="dom-month-calendar__continuous-mobile-header sticky top-0 z-30 mb-3 items-center justify-between gap-3 border-b border-border bg-canvas/95 py-3 shadow-sm backdrop-blur"
|
|
1488
1554
|
>
|
|
1489
1555
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-muted-fg">Month</span>
|
|
@@ -1491,8 +1557,9 @@ function onDayClick(cell, event) {
|
|
|
1491
1557
|
</div>
|
|
1492
1558
|
<header
|
|
1493
1559
|
v-if="!section.continuous"
|
|
1560
|
+
data-month-heading
|
|
1494
1561
|
class="mb-4 flex flex-wrap items-end justify-between gap-3"
|
|
1495
|
-
:class="stickyMonthHeaders && 'sticky top-0 z-
|
|
1562
|
+
:class="stickyMonthHeaders && 'sticky top-0 z-30 border-b border-border bg-canvas/95 py-3 shadow-sm backdrop-blur'"
|
|
1496
1563
|
>
|
|
1497
1564
|
<h2 :id="section.id" class="text-3xl font-bold tracking-tight text-canvas-fg">{{ section.label }}</h2>
|
|
1498
1565
|
<div class="text-sm font-medium text-muted-fg">
|
|
@@ -1508,13 +1575,14 @@ function onDayClick(cell, event) {
|
|
|
1508
1575
|
</header>
|
|
1509
1576
|
<h2 v-else :id="section.id" class="sr-only">{{ section.label }}</h2>
|
|
1510
1577
|
|
|
1578
|
+
<!-- Parent-scrolled grids use clip to round resting corners without trapping sticky headers. -->
|
|
1511
1579
|
<div
|
|
1512
1580
|
class="rounded-2xl border border-border bg-canvas"
|
|
1513
1581
|
:data-continuous-section="section.continuous ? '' : undefined"
|
|
1514
1582
|
:class="[
|
|
1515
1583
|
section.continuous && containedContinuousScroll && 'max-h-[70vh] overflow-auto overscroll-contain',
|
|
1516
|
-
section.continuous && !containedContinuousScroll && 'overflow-x-auto md:overflow-
|
|
1517
|
-
!section.continuous && 'overflow-x-auto',
|
|
1584
|
+
section.continuous && !containedContinuousScroll && (resolvedStickyDayHeaders ? 'overflow-clip' : 'overflow-x-auto md:overflow-clip'),
|
|
1585
|
+
!section.continuous && (resolvedStickyDayHeaders ? 'overflow-clip' : 'overflow-x-auto'),
|
|
1518
1586
|
]"
|
|
1519
1587
|
@scroll.passive="onSectionScroll(section, $event)"
|
|
1520
1588
|
>
|
|
@@ -1530,8 +1598,9 @@ function onDayClick(cell, event) {
|
|
|
1530
1598
|
:data-continuous-day-headers="section.continuous ? '' : undefined"
|
|
1531
1599
|
class="dom-month-calendar__day-headers grid border-b border-border"
|
|
1532
1600
|
:class="[
|
|
1533
|
-
|
|
1601
|
+
resolvedStickyDayHeaders ? 'sticky z-20 bg-secondary shadow-sm' : 'bg-secondary/30',
|
|
1534
1602
|
]"
|
|
1603
|
+
:style="resolvedStickyDayHeaders ? { top: stickyMonthHeaders && !(section.continuous && containedContinuousScroll) ? 'var(--dom-month-calendar-month-heading-height, 0px)' : '0px' } : undefined"
|
|
1535
1604
|
>
|
|
1536
1605
|
<div
|
|
1537
1606
|
v-if="hasRowRail"
|
|
@@ -1612,7 +1681,7 @@ function onDayClick(cell, event) {
|
|
|
1612
1681
|
clickable && !cell.disabled && hoverableDays && 'hover:bg-secondary/50',
|
|
1613
1682
|
clickable && cell.disabled && 'cursor-not-allowed opacity-55',
|
|
1614
1683
|
!clickable && 'cursor-default',
|
|
1615
|
-
cell.today && !cell.hidden && 'ring-2 ring-inset ring-ring/40',
|
|
1684
|
+
cell.today && !cell.hidden && todayIndicator === 'cell' && 'ring-2 ring-inset ring-ring/40',
|
|
1616
1685
|
dayClassFor(cell),
|
|
1617
1686
|
dragAndDrop && dragState.active && canDropOnCell(cell) && !isDropTarget(cell) && 'bg-primary/5',
|
|
1618
1687
|
dragAndDrop && isDropTarget(cell) && 'bg-primary/10 ring-2 ring-inset ring-ring/60',
|
|
@@ -1627,9 +1696,15 @@ function onDayClick(cell, event) {
|
|
|
1627
1696
|
@drop="onDayDrop(cell, $event)"
|
|
1628
1697
|
>
|
|
1629
1698
|
<template v-if="!cell.hidden">
|
|
1630
|
-
<div class="flex items-
|
|
1631
|
-
<span
|
|
1632
|
-
|
|
1699
|
+
<div class="flex items-center justify-between gap-2">
|
|
1700
|
+
<span
|
|
1701
|
+
class="text-base font-bold leading-none tabular-nums"
|
|
1702
|
+
:class="[
|
|
1703
|
+
todayIndicator === 'circle' && 'inline-flex size-7 shrink-0 items-center justify-center rounded-full',
|
|
1704
|
+
cell.today && todayIndicator === 'circle' && resolveDayClass(todayDateClass, cell),
|
|
1705
|
+
]"
|
|
1706
|
+
>{{ cell.day }}</span>
|
|
1707
|
+
<span v-if="showDayNames" class="text-base lowercase leading-none tabular-nums">{{ cell.weekday }}</span>
|
|
1633
1708
|
</div>
|
|
1634
1709
|
<div class="mt-3 min-h-0 flex-1 h-full">
|
|
1635
1710
|
<slot
|