@mongoosejs/studio 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ <div
2
+ class="custom-date-picker"
3
+ :class="rootClasses"
4
+ >
5
+ <div :class="horizontal ? 'flex gap-3 items-start' : ''">
6
+ <div :class="[horizontal ? 'shrink-0' : '', calendarSectionClass]">
7
+ <div class="flex items-center justify-between gap-1 mb-1" :class="mini ? 'mb-0.5' : 'mb-2'">
8
+ <button
9
+ type="button"
10
+ class="shrink-0 flex items-center justify-center rounded border border-edge-strong hover:bg-surface-hover text-content-secondary"
11
+ :class="mini ? 'w-5 h-5 text-[10px]' : 'w-7 h-7'"
12
+ aria-label="Previous month"
13
+ @click="prevMonth"
14
+ >‹</button>
15
+ <span
16
+ class="flex-1 min-w-0 text-center font-medium text-content-primary truncate"
17
+ :class="mini ? 'text-[10px]' : ''"
18
+ >{{ monthLabel }}</span>
19
+ <button
20
+ type="button"
21
+ class="shrink-0 flex items-center justify-center rounded border border-edge-strong hover:bg-surface-hover text-content-secondary"
22
+ :class="mini ? 'w-5 h-5 text-[10px]' : 'w-7 h-7'"
23
+ aria-label="Next month"
24
+ @click="nextMonth"
25
+ >›</button>
26
+ </div>
27
+
28
+ <div class="grid grid-cols-7 gap-px mb-0.5">
29
+ <div
30
+ v-for="label in weekdayLabels"
31
+ :key="label"
32
+ class="flex items-center justify-center text-center text-content-tertiary font-medium min-w-0"
33
+ :class="[
34
+ dayCellClass,
35
+ mini ? 'text-[9px]' : (compact ? 'text-[10px]' : 'text-xs')
36
+ ]"
37
+ >{{ mini ? label.charAt(0) : label }}</div>
38
+ </div>
39
+
40
+ <div class="grid grid-cols-7 gap-px">
41
+ <button
42
+ v-for="day in calendarDays"
43
+ :key="day.key"
44
+ type="button"
45
+ class="min-w-0 w-full flex items-center justify-center rounded transition-colors"
46
+ :class="[
47
+ dayCellClass,
48
+ mini ? 'text-[10px]' : (compact ? 'text-xs' : 'text-sm'),
49
+ dayClasses(day)
50
+ ]"
51
+ :aria-label="day.label"
52
+ :aria-pressed="isSelectedDay(day)"
53
+ @click="selectDay(day)"
54
+ >{{ day.label }}</button>
55
+ </div>
56
+ </div>
57
+
58
+ <div
59
+ v-if="showTime"
60
+ :class="timeSectionClasses"
61
+ @mousedown.stop
62
+ >
63
+ <div
64
+ class="text-content-secondary"
65
+ :class="[
66
+ horizontal ? 'mb-1 text-xs' : 'mb-2',
67
+ mini ? 'text-xs' : (horizontal ? '' : 'text-sm')
68
+ ]"
69
+ >Time</div>
70
+ <div class="relative flex items-center justify-center">
71
+ <div
72
+ class="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 border-y border-edge-strong bg-surface-hover/40"
73
+ :class="wheelHighlightClass"
74
+ aria-hidden="true"
75
+ ></div>
76
+ <div class="relative flex items-center justify-center gap-1">
77
+ <div
78
+ ref="hourWheel"
79
+ class="wheel-column"
80
+ :class="[wheelListClass, wheelColumnClass]"
81
+ :style="wheelListStyle"
82
+ @scroll="onHourWheelScroll"
83
+ >
84
+ <div
85
+ v-for="n in wheelPadCount"
86
+ :key="'hour-pad-top-' + n"
87
+ :class="wheelItemClass"
88
+ aria-hidden="true"
89
+ ></div>
90
+ <button
91
+ v-for="h in hourOptions"
92
+ :key="'hour-' + h"
93
+ type="button"
94
+ :class="[wheelItemClass, wheelItemClasses(h, hour)]"
95
+ :aria-label="'Hour ' + h"
96
+ :aria-selected="hour === h"
97
+ @click="selectHour(h)"
98
+ >{{ formatWheelValue(h) }}</button>
99
+ <div
100
+ v-for="n in wheelPadCount"
101
+ :key="'hour-pad-bottom-' + n"
102
+ :class="wheelItemClass"
103
+ aria-hidden="true"
104
+ ></div>
105
+ </div>
106
+ <span class="text-content-secondary font-medium" :class="mini ? 'text-sm' : 'text-base'">:</span>
107
+ <div
108
+ ref="minuteWheel"
109
+ class="wheel-column"
110
+ :class="[wheelListClass, wheelColumnClass]"
111
+ :style="wheelListStyle"
112
+ @scroll="onMinuteWheelScroll"
113
+ >
114
+ <div
115
+ v-for="n in wheelPadCount"
116
+ :key="'minute-pad-top-' + n"
117
+ :class="wheelItemClass"
118
+ aria-hidden="true"
119
+ ></div>
120
+ <button
121
+ v-for="m in minuteOptions"
122
+ :key="'minute-' + m"
123
+ type="button"
124
+ :class="[wheelItemClass, wheelItemClasses(m, minute)]"
125
+ :aria-label="'Minute ' + m"
126
+ :aria-selected="minute === m"
127
+ @click="selectMinute(m)"
128
+ >{{ formatWheelValue(m) }}</button>
129
+ <div
130
+ v-for="n in wheelPadCount"
131
+ :key="'minute-pad-bottom-' + n"
132
+ :class="wheelItemClass"
133
+ aria-hidden="true"
134
+ ></div>
135
+ </div>
136
+ <span class="text-content-secondary font-medium" :class="mini ? 'text-sm' : 'text-base'">:</span>
137
+ <div
138
+ ref="secondWheel"
139
+ class="wheel-column"
140
+ :class="[wheelListClass, wheelColumnClass]"
141
+ :style="wheelListStyle"
142
+ @scroll="onSecondWheelScroll"
143
+ >
144
+ <div
145
+ v-for="n in wheelPadCount"
146
+ :key="'second-pad-top-' + n"
147
+ :class="wheelItemClass"
148
+ aria-hidden="true"
149
+ ></div>
150
+ <button
151
+ v-for="s in secondOptions"
152
+ :key="'second-' + s"
153
+ type="button"
154
+ :class="[wheelItemClass, wheelItemClasses(s, second)]"
155
+ :aria-label="'Second ' + s"
156
+ :aria-selected="second === s"
157
+ @click="selectSecond(s)"
158
+ >{{ formatWheelValue(s) }}</button>
159
+ <div
160
+ v-for="n in wheelPadCount"
161
+ :key="'second-pad-bottom-' + n"
162
+ :class="wheelItemClass"
163
+ aria-hidden="true"
164
+ ></div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <div
172
+ class="flex justify-end items-center gap-2"
173
+ :class="mini ? 'mt-1' : 'mt-2'"
174
+ >
175
+ <button
176
+ v-if="!immediate"
177
+ type="button"
178
+ class="text-xs font-medium text-white bg-merlot-700 hover:bg-merlot-800 px-2 py-1 rounded"
179
+ :class="mini ? 'py-0.5 px-1.5' : ''"
180
+ @click="commit"
181
+ >Apply</button>
182
+ </div>
183
+ </div>
@@ -0,0 +1,407 @@
1
+ 'use strict';
2
+
3
+ const template = require('./custom-date-picker.html');
4
+ const {
5
+ dateToDatetimeLocal,
6
+ dateToDateOnly,
7
+ parseValueToDate,
8
+ buildCalendarDays,
9
+ clampInt,
10
+ isSameDay,
11
+ pad2,
12
+ WEEKDAY_LABELS,
13
+ MONTH_LABELS
14
+ } = require('../_util/calendar');
15
+
16
+ const WHEEL_PAD = 2;
17
+ const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => i);
18
+ const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, i) => i);
19
+ const SECOND_OPTIONS = Array.from({ length: 60 }, (_, i) => i);
20
+
21
+ function timePartsFromDate(date) {
22
+ if (!date) {
23
+ return { hour: 0, minute: 0, second: 0, millisecond: 0 };
24
+ }
25
+ return {
26
+ hour: date.getHours(),
27
+ minute: date.getMinutes(),
28
+ second: date.getSeconds(),
29
+ millisecond: date.getMilliseconds()
30
+ };
31
+ }
32
+
33
+ module.exports = app => app.component('custom-date-picker', {
34
+ template,
35
+ props: {
36
+ value: {
37
+ default: null
38
+ },
39
+ mode: {
40
+ type: String,
41
+ default: 'datetime' // 'date' | 'datetime'
42
+ },
43
+ compact: {
44
+ type: Boolean,
45
+ default: false
46
+ },
47
+ mini: {
48
+ type: Boolean,
49
+ default: false
50
+ },
51
+ immediate: {
52
+ type: Boolean,
53
+ default: true
54
+ },
55
+ horizontal: {
56
+ type: Boolean,
57
+ default: false
58
+ },
59
+ embedded: {
60
+ type: Boolean,
61
+ default: false
62
+ }
63
+ },
64
+ emits: ['input', 'commit'],
65
+ data() {
66
+ const selected = parseValueToDate(this.value);
67
+ const anchor = selected || new Date();
68
+ const time = timePartsFromDate(anchor);
69
+ return {
70
+ viewYear: anchor.getFullYear(),
71
+ viewMonth: anchor.getMonth(),
72
+ selectedDate: anchor,
73
+ hour: time.hour,
74
+ minute: time.minute,
75
+ second: time.second,
76
+ millisecond: time.millisecond,
77
+ pendingValue: null,
78
+ wheelScrollLock: false,
79
+ timeApplyTimer: null,
80
+ hourOptions: HOUR_OPTIONS,
81
+ minuteOptions: MINUTE_OPTIONS,
82
+ secondOptions: SECOND_OPTIONS
83
+ };
84
+ },
85
+ watch: {
86
+ value() {
87
+ this.syncFromValue();
88
+ },
89
+ horizontal() {
90
+ this.$nextTick(() => this.scrollWheelsToValues());
91
+ }
92
+ },
93
+ computed: {
94
+ rootClasses() {
95
+ const sizeClass = this.mini
96
+ ? 'p-0.5 text-[10px]'
97
+ : (this.compact ? 'p-2 text-xs' : 'p-3 text-sm');
98
+ const chromeClass = this.embedded
99
+ ? ''
100
+ : (this.mini ? '' : 'bg-surface border border-edge-strong rounded-md shadow-sm');
101
+ return [sizeClass, chromeClass];
102
+ },
103
+ timeSectionClasses() {
104
+ if (this.horizontal) {
105
+ return 'flex-1 min-w-0 border-l border-edge-strong pl-2';
106
+ }
107
+ return [
108
+ 'border-t border-edge-strong',
109
+ this.mini ? 'mt-1 pt-1' : 'mt-3 pt-3'
110
+ ];
111
+ },
112
+ wheelPadCount() {
113
+ return 2;
114
+ },
115
+ wheelVisibleRows() {
116
+ return 5;
117
+ },
118
+ weekdayLabels() {
119
+ return WEEKDAY_LABELS;
120
+ },
121
+ monthLabel() {
122
+ return `${MONTH_LABELS[this.viewMonth]} ${this.viewYear}`;
123
+ },
124
+ calendarDays() {
125
+ return buildCalendarDays(this.viewYear, this.viewMonth);
126
+ },
127
+ today() {
128
+ const now = new Date();
129
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
130
+ },
131
+ showTime() {
132
+ return this.mode === 'datetime';
133
+ },
134
+ dayCellClass() {
135
+ if (this.mini) {
136
+ return 'h-6';
137
+ }
138
+ if (this.compact) {
139
+ return 'h-7';
140
+ }
141
+ return 'aspect-square';
142
+ },
143
+ calendarSectionClass() {
144
+ if (this.mini) return 'w-36';
145
+ if (this.compact) return 'w-44';
146
+ return 'w-48';
147
+ },
148
+ wheelItemPx() {
149
+ if (this.mini) {
150
+ return 32;
151
+ }
152
+ if (this.compact) {
153
+ return 36;
154
+ }
155
+ return 40;
156
+ },
157
+ wheelListClass() {
158
+ return [
159
+ 'overflow-y-auto overscroll-y-contain snap-y snap-mandatory',
160
+ '[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
161
+ ];
162
+ },
163
+ wheelListStyle() {
164
+ return {
165
+ height: `${this.wheelItemPx * this.wheelVisibleRows}px`
166
+ };
167
+ },
168
+ wheelItemClass() {
169
+ return [
170
+ 'snap-center shrink-0 w-full flex items-center justify-center font-mono transition-colors',
171
+ this.mini ? 'h-8 text-sm' : (this.compact ? 'h-9 text-sm' : 'h-10 text-base')
172
+ ];
173
+ },
174
+ wheelHighlightClass() {
175
+ return this.mini ? 'h-8' : (this.compact ? 'h-9' : 'h-10');
176
+ },
177
+ wheelColumnClass() {
178
+ return this.mini ? 'w-11' : (this.compact ? 'w-12' : 'w-14');
179
+ }
180
+ },
181
+ mounted() {
182
+ this.$nextTick(() => this.scrollWheelsToValues());
183
+ },
184
+ beforeUnmount() {
185
+ if (this.timeApplyTimer) {
186
+ clearTimeout(this.timeApplyTimer);
187
+ }
188
+ },
189
+ methods: {
190
+ syncFromValue() {
191
+ const selected = parseValueToDate(this.value);
192
+ const anchor = selected || new Date();
193
+ this.selectedDate = anchor;
194
+ this.viewYear = anchor.getFullYear();
195
+ this.viewMonth = anchor.getMonth();
196
+ const time = timePartsFromDate(anchor);
197
+ this.hour = time.hour;
198
+ this.minute = time.minute;
199
+ this.second = time.second;
200
+ this.millisecond = time.millisecond;
201
+ this.$nextTick(() => this.scrollWheelsToValues());
202
+ },
203
+ clampedTimeParts() {
204
+ return {
205
+ hour: clampInt(this.hour, 0, 23),
206
+ minute: clampInt(this.minute, 0, 59),
207
+ second: clampInt(this.second, 0, 59),
208
+ millisecond: clampInt(this.millisecond, 0, 999)
209
+ };
210
+ },
211
+ formatWheelValue(value) {
212
+ return pad2(value);
213
+ },
214
+ wheelItemClasses(value, selectedValue) {
215
+ return value === selectedValue
216
+ ? 'text-content-primary font-semibold'
217
+ : 'text-content-tertiary hover:text-content-secondary';
218
+ },
219
+ wheelIndexFromScroll(el, max) {
220
+ return clampInt(Math.round(el.scrollTop / this.wheelItemPx), 0, max);
221
+ },
222
+ scrollWheelTo(el, index) {
223
+ if (!el) {
224
+ return;
225
+ }
226
+ el.scrollTop = index * this.wheelItemPx;
227
+ },
228
+ scrollWheelsToValues() {
229
+ this.wheelScrollLock = true;
230
+ this.scrollWheelTo(this.$refs.hourWheel, this.hour);
231
+ this.scrollWheelTo(this.$refs.minuteWheel, this.minute);
232
+ this.scrollWheelTo(this.$refs.secondWheel, this.second);
233
+ requestAnimationFrame(() => {
234
+ this.wheelScrollLock = false;
235
+ });
236
+ },
237
+ scheduleTimeApply() {
238
+ if (this.timeApplyTimer) {
239
+ clearTimeout(this.timeApplyTimer);
240
+ }
241
+ this.timeApplyTimer = setTimeout(() => {
242
+ this.timeApplyTimer = null;
243
+ this.applyTimeToSelection();
244
+ }, 120);
245
+ },
246
+ onHourWheelScroll(event) {
247
+ if (this.wheelScrollLock) {
248
+ return;
249
+ }
250
+ const hour = this.wheelIndexFromScroll(event.target, 23);
251
+ if (hour === this.hour) {
252
+ return;
253
+ }
254
+ this.hour = hour;
255
+ this.scheduleTimeApply();
256
+ },
257
+ onMinuteWheelScroll(event) {
258
+ if (this.wheelScrollLock) {
259
+ return;
260
+ }
261
+ const minute = this.wheelIndexFromScroll(event.target, 59);
262
+ if (minute === this.minute) {
263
+ return;
264
+ }
265
+ this.minute = minute;
266
+ this.scheduleTimeApply();
267
+ },
268
+ onSecondWheelScroll(event) {
269
+ if (this.wheelScrollLock) {
270
+ return;
271
+ }
272
+ const second = this.wheelIndexFromScroll(event.target, 59);
273
+ if (second === this.second) {
274
+ return;
275
+ }
276
+ this.second = second;
277
+ this.scheduleTimeApply();
278
+ },
279
+ selectHour(hour) {
280
+ this.hour = hour;
281
+ this.scrollWheelTo(this.$refs.hourWheel, hour);
282
+ this.applyTimeToSelection();
283
+ },
284
+ selectMinute(minute) {
285
+ this.minute = minute;
286
+ this.scrollWheelTo(this.$refs.minuteWheel, minute);
287
+ this.applyTimeToSelection();
288
+ },
289
+ selectSecond(second) {
290
+ this.second = second;
291
+ this.scrollWheelTo(this.$refs.secondWheel, second);
292
+ this.applyTimeToSelection();
293
+ },
294
+ dayClasses(day) {
295
+ const selected = this.selectedDate;
296
+ const isSelected = selected && isSameDay(day.date, selected);
297
+ const isToday = isSameDay(day.date, this.today);
298
+ return {
299
+ 'text-content-tertiary': !day.inMonth,
300
+ 'bg-merlot-700 text-white hover:bg-merlot-800': isSelected,
301
+ 'hover:bg-surface-hover': day.inMonth && !isSelected,
302
+ 'ring-1 ring-merlot-700 ring-inset': isToday && !isSelected && day.inMonth,
303
+ 'font-medium': day.inMonth
304
+ };
305
+ },
306
+ prevMonth() {
307
+ if (this.viewMonth === 0) {
308
+ this.viewMonth = 11;
309
+ this.viewYear -= 1;
310
+ } else {
311
+ this.viewMonth -= 1;
312
+ }
313
+ },
314
+ nextMonth() {
315
+ if (this.viewMonth === 11) {
316
+ this.viewMonth = 0;
317
+ this.viewYear += 1;
318
+ } else {
319
+ this.viewMonth += 1;
320
+ }
321
+ },
322
+ goToToday() {
323
+ const now = new Date();
324
+ this.viewYear = now.getFullYear();
325
+ this.viewMonth = now.getMonth();
326
+ this.selectDay({
327
+ date: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
328
+ inMonth: true
329
+ });
330
+ },
331
+ selectDay(day) {
332
+ const { hour, minute, second, millisecond } = this.clampedTimeParts();
333
+ const next = new Date(day.date);
334
+ if (this.showTime) {
335
+ next.setHours(hour, minute, second, millisecond);
336
+ } else {
337
+ next.setHours(0, 0, 0, 0);
338
+ }
339
+ this.selectedDate = next;
340
+ if (!day.inMonth) {
341
+ this.viewYear = next.getFullYear();
342
+ this.viewMonth = next.getMonth();
343
+ }
344
+ this.emitValue(next);
345
+ },
346
+ applyTimeToSelection() {
347
+ const { hour, minute, second, millisecond } = this.clampedTimeParts();
348
+ this.hour = hour;
349
+ this.minute = minute;
350
+ this.second = second;
351
+ this.millisecond = millisecond;
352
+
353
+ if (!this.selectedDate) {
354
+ const now = new Date();
355
+ this.selectedDate = new Date(
356
+ now.getFullYear(),
357
+ now.getMonth(),
358
+ now.getDate(),
359
+ hour,
360
+ minute,
361
+ second,
362
+ millisecond
363
+ );
364
+ this.viewYear = this.selectedDate.getFullYear();
365
+ this.viewMonth = this.selectedDate.getMonth();
366
+ } else {
367
+ this.selectedDate = new Date(
368
+ this.selectedDate.getFullYear(),
369
+ this.selectedDate.getMonth(),
370
+ this.selectedDate.getDate(),
371
+ hour,
372
+ minute,
373
+ second,
374
+ millisecond
375
+ );
376
+ }
377
+ this.emitValue(this.selectedDate);
378
+ },
379
+ valuePayload(date) {
380
+ if (this.mode === 'date') {
381
+ return dateToDateOnly(date);
382
+ }
383
+ return dateToDatetimeLocal(date);
384
+ },
385
+ emitValue(date) {
386
+ const payload = this.valuePayload(date);
387
+ if (!this.immediate) {
388
+ this.pendingValue = payload;
389
+ return;
390
+ }
391
+ this.$emit('input', payload);
392
+ },
393
+ commit() {
394
+ const payload = this.pendingValue != null
395
+ ? this.pendingValue
396
+ : (this.selectedDate ? this.valuePayload(this.selectedDate) : null);
397
+ if (payload == null) {
398
+ return;
399
+ }
400
+ this.$emit('commit', payload);
401
+ this.pendingValue = null;
402
+ },
403
+ isSelectedDay(day) {
404
+ return this.selectedDate && isSameDay(day.date, this.selectedDate);
405
+ }
406
+ }
407
+ });
@@ -26,3 +26,65 @@
26
26
  .document button img {
27
27
  height: 1em;
28
28
  }
29
+
30
+ /* Add Field Modal Styles */
31
+ .add-field-modal {
32
+ max-width: 500px;
33
+ width: 100%;
34
+ }
35
+
36
+ .add-field-modal .modal-exit {
37
+ position: absolute;
38
+ top: 15px;
39
+ right: 20px;
40
+ font-size: 24px;
41
+ cursor: pointer;
42
+ color: #6b7280;
43
+ z-index: 10;
44
+ }
45
+
46
+ .add-field-modal .modal-exit:hover {
47
+ color: #374151;
48
+ }
49
+
50
+ .add-field-modal form {
51
+ max-height: 70vh;
52
+ overflow-y: auto;
53
+ }
54
+
55
+ .add-field-modal input[type="text"],
56
+ .add-field-modal input[type="email"],
57
+ .add-field-modal input[type="password"],
58
+ .add-field-modal select,
59
+ .add-field-modal textarea {
60
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
61
+ }
62
+
63
+ .add-field-modal input:focus,
64
+ .add-field-modal select:focus,
65
+ .add-field-modal textarea:focus {
66
+ border-color: #3b82f6;
67
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
68
+ }
69
+
70
+ .add-field-modal .border-red-500 {
71
+ border-color: #ef4444 !important;
72
+ }
73
+
74
+ .add-field-modal .border-red-500:focus {
75
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
76
+ }
77
+
78
+ /* Ace editor styling in modal */
79
+ .add-field-modal .add-field-ace-editor.ace_editor {
80
+ border: 1px solid #d1d5db;
81
+ border-radius: 0.375rem;
82
+ font-size: 14px;
83
+ min-height: 100px;
84
+ }
85
+
86
+ .add-field-modal .add-field-ace-editor:focus-within.ace_editor,
87
+ .add-field-modal .add-field-ace-editor.ace_focus {
88
+ border-color: #3b82f6;
89
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
90
+ }