@mongoosejs/studio 0.5.7 → 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.
@@ -60,6 +60,172 @@ module.exports = {
60
60
  };
61
61
 
62
62
 
63
+ /***/ },
64
+
65
+ /***/ "./frontend/src/_util/calendar.js"
66
+ /*!****************************************!*\
67
+ !*** ./frontend/src/_util/calendar.js ***!
68
+ \****************************************/
69
+ (module) {
70
+
71
+ "use strict";
72
+
73
+
74
+ function pad2(n) {
75
+ return String(n).padStart(2, '0');
76
+ }
77
+
78
+ function pad3(n) {
79
+ return String(n).padStart(3, '0');
80
+ }
81
+
82
+ /**
83
+ * @param {number} value
84
+ * @param {number} min
85
+ * @param {number} max
86
+ */
87
+ function clampInt(value, min, max) {
88
+ const n = Math.floor(Number(value));
89
+ if (Number.isNaN(n)) {
90
+ return min;
91
+ }
92
+ return Math.min(max, Math.max(min, n));
93
+ }
94
+
95
+ /**
96
+ * @param {Date} date
97
+ * @returns {string} YYYY-MM-DDTHH:mm:ss.SSS
98
+ */
99
+ function dateToDatetimeLocal(date) {
100
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
101
+ return '';
102
+ }
103
+ return [
104
+ date.getFullYear(),
105
+ '-',
106
+ pad2(date.getMonth() + 1),
107
+ '-',
108
+ pad2(date.getDate()),
109
+ 'T',
110
+ pad2(date.getHours()),
111
+ ':',
112
+ pad2(date.getMinutes()),
113
+ ':',
114
+ pad2(date.getSeconds()),
115
+ '.',
116
+ pad3(date.getMilliseconds())
117
+ ].join('');
118
+ }
119
+
120
+ /**
121
+ * @param {Date} date
122
+ * @returns {string} YYYY-MM-DD
123
+ */
124
+ function dateToDateOnly(date) {
125
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
126
+ return '';
127
+ }
128
+ return [
129
+ date.getFullYear(),
130
+ '-',
131
+ pad2(date.getMonth() + 1),
132
+ '-',
133
+ pad2(date.getDate())
134
+ ].join('');
135
+ }
136
+
137
+ /**
138
+ * @param {unknown} value
139
+ * @returns {Date|null}
140
+ */
141
+ function parseValueToDate(value) {
142
+ if (value == null || value === '') {
143
+ return null;
144
+ }
145
+ const date = new Date(value);
146
+ if (Number.isNaN(date.getTime())) {
147
+ return null;
148
+ }
149
+ return date;
150
+ }
151
+
152
+ /**
153
+ * @param {string} localValue - YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.SSS
154
+ * @returns {Date|null}
155
+ */
156
+ function localStringToDate(localValue) {
157
+ if (!localValue) {
158
+ return null;
159
+ }
160
+ const date = new Date(localValue);
161
+ if (Number.isNaN(date.getTime())) {
162
+ return null;
163
+ }
164
+ return date;
165
+ }
166
+
167
+ function isSameDay(a, b) {
168
+ if (!a || !b) {
169
+ return false;
170
+ }
171
+ return (
172
+ a.getFullYear() === b.getFullYear()
173
+ && a.getMonth() === b.getMonth()
174
+ && a.getDate() === b.getDate()
175
+ );
176
+ }
177
+
178
+ function isSameMonth(date, year, month) {
179
+ return date.getFullYear() === year && date.getMonth() === month;
180
+ }
181
+
182
+ /**
183
+ * Build a 6-row calendar grid for the given month (local timezone).
184
+ * @param {number} year
185
+ * @param {number} month - 0-indexed
186
+ * @returns {Array<{ key: string, label: number, date: Date, inMonth: boolean }>}
187
+ */
188
+ function buildCalendarDays(year, month) {
189
+ const firstOfMonth = new Date(year, month, 1);
190
+ const startOffset = firstOfMonth.getDay();
191
+ const gridStart = new Date(year, month, 1 - startOffset);
192
+
193
+ const days = [];
194
+ for (let i = 0; i < 42; i++) {
195
+ const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);
196
+ days.push({
197
+ key: `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
198
+ label: date.getDate(),
199
+ date,
200
+ inMonth: date.getMonth() === month
201
+ });
202
+ }
203
+ return days;
204
+ }
205
+
206
+ const WEEKDAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
207
+
208
+ const MONTH_LABELS = [
209
+ 'January', 'February', 'March', 'April', 'May', 'June',
210
+ 'July', 'August', 'September', 'October', 'November', 'December'
211
+ ];
212
+
213
+ module.exports = {
214
+ pad2,
215
+ pad3,
216
+ clampInt,
217
+ dateToDatetimeLocal,
218
+ dateToDateOnly,
219
+ parseValueToDate,
220
+ localStringToDate,
221
+ isSameDay,
222
+ isSameMonth,
223
+ buildCalendarDays,
224
+ WEEKDAY_LABELS,
225
+ MONTH_LABELS
226
+ };
227
+
228
+
63
229
  /***/ },
64
230
 
65
231
  /***/ "./frontend/src/_util/dateRange.js"
@@ -228,6 +394,8 @@ module.exports = deepEqual;
228
394
  "use strict";
229
395
 
230
396
 
397
+ const { dateToDatetimeLocal } = __webpack_require__(/*! ./calendar */ "./frontend/src/_util/calendar.js");
398
+
231
399
  const { Trie } = __webpack_require__(/*! ../models/trie */ "./frontend/src/models/trie.js");
232
400
 
233
401
  const QUERY_SELECTORS = [
@@ -277,7 +445,7 @@ function buildAutocompleteTrie(schemaPaths) {
277
445
  trie.bulkInsert(QUERY_SELECTORS, 5, 'operator');
278
446
  trie.bulkInsert(QUERY_SELECTORS, 5, 'fieldName');
279
447
  trie.bulkInsert(VALUE_HELPERS, 5, 'value');
280
-
448
+
281
449
  if (Array.isArray(schemaPaths) && schemaPaths.length > 0) {
282
450
  const paths = schemaPaths
283
451
  .map(path => path?.path)
@@ -289,13 +457,13 @@ function buildAutocompleteTrie(schemaPaths) {
289
457
  }
290
458
  trie.bulkInsert(paths, 10, 'fieldName');
291
459
  }
292
-
460
+
293
461
  return trie;
294
462
  }
295
463
 
296
464
  function getAutocompleteContext(searchText, cursorPos) {
297
465
  const before = searchText.slice(0, cursorPos);
298
-
466
+
299
467
  // Check if we're in a field name context (after { or ,)
300
468
  // This takes precedence over value context to handle cases like { _id: { $gt
301
469
  const fieldMatch = before.match(/(?:\{|,)\s*([^:\s]*)$/);
@@ -307,7 +475,7 @@ function getAutocompleteContext(searchText, cursorPos) {
307
475
  startPos: cursorPos - token.length
308
476
  };
309
477
  }
310
-
478
+
311
479
  // Check if we're in a value context (after a colon)
312
480
  // Match the last colon followed by optional whitespace and capture everything after
313
481
  const valueMatch = before.match(/:\s*(\{?\s*)([^\s,\}\]:]*)$/);
@@ -319,19 +487,19 @@ function getAutocompleteContext(searchText, cursorPos) {
319
487
  startPos: cursorPos - token.length
320
488
  };
321
489
  }
322
-
490
+
323
491
  return null;
324
492
  }
325
493
 
326
494
  function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
327
495
  const context = getAutocompleteContext(searchText, cursorPos);
328
-
496
+
329
497
  if (!context) {
330
498
  return [];
331
499
  }
332
-
500
+
333
501
  const { token, role } = context;
334
-
502
+
335
503
  // Extract the actual term without quotes
336
504
  const leadingQuoteMatch = token.match(/^["']/);
337
505
  const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
@@ -341,14 +509,14 @@ function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
341
509
  .replace(/^["']/, '')
342
510
  .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
343
511
  .trim();
344
-
512
+
345
513
  if (!term) {
346
514
  return [];
347
515
  }
348
-
516
+
349
517
  const primarySuggestions = trie.getSuggestions(term, 10, role);
350
518
  const suggestionsSet = new Set(primarySuggestions);
351
-
519
+
352
520
  // Add schema path suggestions for field names
353
521
  if (role === 'fieldName' && Array.isArray(schemaPaths) && schemaPaths.length > 0) {
354
522
  for (const schemaPath of schemaPaths) {
@@ -365,9 +533,9 @@ function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
365
533
  }
366
534
  }
367
535
  }
368
-
536
+
369
537
  let suggestions = Array.from(suggestionsSet);
370
-
538
+
371
539
  // Preserve quotes if present
372
540
  if (leadingQuoteMatch) {
373
541
  const leadingQuote = leadingQuoteMatch[0];
@@ -378,14 +546,14 @@ function getAutocompleteSuggestions(trie, searchText, cursorPos, schemaPaths) {
378
546
  suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
379
547
  );
380
548
  }
381
-
549
+
382
550
  return suggestions;
383
551
  }
384
552
 
385
553
  function applySuggestion(searchText, cursorPos, suggestion) {
386
554
  const before = searchText.slice(0, cursorPos);
387
555
  const after = searchText.slice(cursorPos);
388
-
556
+
389
557
  // Check if we're in a value context
390
558
  const valueMatch = before.match(/:\s*(\{?\s*)([^\s,\}\]:]*)$/);
391
559
  if (valueMatch) {
@@ -393,30 +561,30 @@ function applySuggestion(searchText, cursorPos, suggestion) {
393
561
  const start = cursorPos - token.length;
394
562
  let replacement = suggestion;
395
563
  let cursorOffset = replacement.length;
396
-
564
+
397
565
  // Add parentheses for function helpers and position cursor inside
398
566
  if (FUNCTION_HELPERS.has(suggestion)) {
399
567
  replacement = `${suggestion}()`;
400
568
  cursorOffset = suggestion.length + 1; // Position cursor between ()
401
569
  }
402
-
570
+
403
571
  return {
404
572
  text: searchText.slice(0, start) + replacement + after,
405
573
  newCursorPos: start + cursorOffset
406
574
  };
407
575
  }
408
-
576
+
409
577
  // Check if we're in a field name context
410
578
  const fieldMatch = before.match(/(?:\{|,)\s*([^:\s]*)$/);
411
579
  if (fieldMatch) {
412
580
  const token = fieldMatch[1];
413
581
  const start = cursorPos - token.length;
414
582
  let replacement = suggestion;
415
-
583
+
416
584
  const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
417
585
  const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
418
586
  const colonNeeded = !/^\s*:/.test(after);
419
-
587
+
420
588
  // If suggestion already has quotes, use it as-is
421
589
  const suggestionHasQuotes = (suggestion.startsWith('"') || suggestion.startsWith('\'')) &&
422
590
  (suggestion.endsWith('"') || suggestion.endsWith('\''));
@@ -430,27 +598,107 @@ function applySuggestion(searchText, cursorPos, suggestion) {
430
598
  replacement = `${replacement}${trailingQuote}`;
431
599
  }
432
600
  }
433
-
601
+
434
602
  // Only insert : if we know the user isn't entering in a nested path
435
603
  // If suggestion has full quotes or user typed both quotes, add colon
436
604
  if (colonNeeded && (suggestionHasQuotes || !leadingQuote || trailingQuote)) {
437
605
  replacement = `${replacement}:`;
438
606
  }
439
-
607
+
440
608
  return {
441
609
  text: searchText.slice(0, start) + replacement + after,
442
610
  newCursorPos: start + replacement.length
443
611
  };
444
612
  }
445
-
613
+
446
614
  return null;
447
615
  }
448
616
 
617
+ /**
618
+ * When the cursor is inside a Date(…) or new Date(…) argument, returns the
619
+ * slice indices of that argument so a picker can replace it with a quoted ISO string.
620
+ */
621
+ function getDatePickerInsertionRange(searchText, cursorPos) {
622
+ const before = searchText.slice(0, cursorPos);
623
+ const re = /((?:new\s+)?Date\s*\(\s*)([^)]*)$/i;
624
+ const m = before.match(re);
625
+ if (!m) {
626
+ return null;
627
+ }
628
+ const innerStart = m.index + m[1].length;
629
+ const after = searchText.slice(cursorPos);
630
+ let closeIdx = -1;
631
+ let parenDepth = 0;
632
+ for (let k = 0; k < after.length; k++) {
633
+ const ch = after[k];
634
+ if (ch === '(') {
635
+ parenDepth++;
636
+ } else if (ch === ')') {
637
+ if (parenDepth === 0) {
638
+ closeIdx = cursorPos + k;
639
+ break;
640
+ }
641
+ parenDepth--;
642
+ }
643
+ }
644
+ const innerEnd = closeIdx >= 0 ? closeIdx : cursorPos;
645
+ const needsClosingParen = closeIdx < 0;
646
+ return { innerStart, innerEnd, needsClosingParen };
647
+ }
648
+
649
+ function dateArgumentSliceToDatetimeLocal(slice) {
650
+ const t = slice.trim();
651
+ if (!t) {
652
+ return '';
653
+ }
654
+ const unquoted = (t.startsWith('"') && t.endsWith('"')) || (t.startsWith('\'') && t.endsWith('\''))
655
+ ? t.slice(1, -1)
656
+ : t;
657
+ const d = new Date(unquoted);
658
+ if (Number.isNaN(d.getTime())) {
659
+ return '';
660
+ }
661
+ return dateToDatetimeLocal(d);
662
+ }
663
+
664
+ /**
665
+ * @param {Date} date
666
+ * @param {'quoted'|'timestamp'} format
667
+ */
668
+ function formatDateArgumentValue(date, format) {
669
+ if (format === 'quoted') {
670
+ return JSON.stringify(date.toISOString());
671
+ }
672
+ return String(date.getTime());
673
+ }
674
+
675
+ /**
676
+ * Inserts a Date argument value as timestamp or quoted ISO string.
677
+ * @param {string} searchText
678
+ * @param {{ innerStart: number, innerEnd: number, needsClosingParen?: boolean }} range
679
+ * @param {Date} date
680
+ * @param {'quoted'|'timestamp'} [format] - when omitted, inferred from the existing argument
681
+ */
682
+ function insertDateInDateArgument(searchText, range, date, format) {
683
+ const slice = searchText.slice(range.innerStart, range.innerEnd);
684
+ const insertion = formatDateArgumentValue(date, format);
685
+ const { innerStart, innerEnd } = range;
686
+ const closing = range.needsClosingParen === true ? ')' : '';
687
+ return {
688
+ text: searchText.slice(0, innerStart) + insertion + closing + searchText.slice(innerEnd),
689
+ newCursorPos: innerStart + insertion.length + closing.length
690
+ };
691
+ }
692
+
449
693
  module.exports = {
450
694
  buildAutocompleteTrie,
451
695
  getAutocompleteContext,
452
696
  getAutocompleteSuggestions,
453
697
  applySuggestion,
698
+ getDatePickerInsertionRange,
699
+ dateArgumentSliceToDatetimeLocal,
700
+ formatDateArgumentValue,
701
+ insertDateInDateArgument,
454
702
  QUERY_SELECTORS,
455
703
  VALUE_HELPERS,
456
704
  FUNCTION_HELPERS
@@ -2464,6 +2712,424 @@ module.exports = app => app.component('create-document', {
2464
2712
  });
2465
2713
 
2466
2714
 
2715
+ /***/ },
2716
+
2717
+ /***/ "./frontend/src/custom-date-picker/custom-date-picker.js"
2718
+ /*!***************************************************************!*\
2719
+ !*** ./frontend/src/custom-date-picker/custom-date-picker.js ***!
2720
+ \***************************************************************/
2721
+ (module, __unused_webpack_exports, __webpack_require__) {
2722
+
2723
+ "use strict";
2724
+
2725
+
2726
+ const template = __webpack_require__(/*! ./custom-date-picker.html */ "./frontend/src/custom-date-picker/custom-date-picker.html");
2727
+ const {
2728
+ dateToDatetimeLocal,
2729
+ dateToDateOnly,
2730
+ parseValueToDate,
2731
+ buildCalendarDays,
2732
+ clampInt,
2733
+ isSameDay,
2734
+ pad2,
2735
+ WEEKDAY_LABELS,
2736
+ MONTH_LABELS
2737
+ } = __webpack_require__(/*! ../_util/calendar */ "./frontend/src/_util/calendar.js");
2738
+
2739
+ const WHEEL_PAD = 2;
2740
+ const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => i);
2741
+ const MINUTE_OPTIONS = Array.from({ length: 60 }, (_, i) => i);
2742
+ const SECOND_OPTIONS = Array.from({ length: 60 }, (_, i) => i);
2743
+
2744
+ function timePartsFromDate(date) {
2745
+ if (!date) {
2746
+ return { hour: 0, minute: 0, second: 0, millisecond: 0 };
2747
+ }
2748
+ return {
2749
+ hour: date.getHours(),
2750
+ minute: date.getMinutes(),
2751
+ second: date.getSeconds(),
2752
+ millisecond: date.getMilliseconds()
2753
+ };
2754
+ }
2755
+
2756
+ module.exports = app => app.component('custom-date-picker', {
2757
+ template,
2758
+ props: {
2759
+ value: {
2760
+ default: null
2761
+ },
2762
+ mode: {
2763
+ type: String,
2764
+ default: 'datetime' // 'date' | 'datetime'
2765
+ },
2766
+ compact: {
2767
+ type: Boolean,
2768
+ default: false
2769
+ },
2770
+ mini: {
2771
+ type: Boolean,
2772
+ default: false
2773
+ },
2774
+ immediate: {
2775
+ type: Boolean,
2776
+ default: true
2777
+ },
2778
+ horizontal: {
2779
+ type: Boolean,
2780
+ default: false
2781
+ },
2782
+ embedded: {
2783
+ type: Boolean,
2784
+ default: false
2785
+ }
2786
+ },
2787
+ emits: ['input', 'commit'],
2788
+ data() {
2789
+ const selected = parseValueToDate(this.value);
2790
+ const anchor = selected || new Date();
2791
+ const time = timePartsFromDate(anchor);
2792
+ return {
2793
+ viewYear: anchor.getFullYear(),
2794
+ viewMonth: anchor.getMonth(),
2795
+ selectedDate: anchor,
2796
+ hour: time.hour,
2797
+ minute: time.minute,
2798
+ second: time.second,
2799
+ millisecond: time.millisecond,
2800
+ pendingValue: null,
2801
+ wheelScrollLock: false,
2802
+ timeApplyTimer: null,
2803
+ hourOptions: HOUR_OPTIONS,
2804
+ minuteOptions: MINUTE_OPTIONS,
2805
+ secondOptions: SECOND_OPTIONS
2806
+ };
2807
+ },
2808
+ watch: {
2809
+ value() {
2810
+ this.syncFromValue();
2811
+ },
2812
+ horizontal() {
2813
+ this.$nextTick(() => this.scrollWheelsToValues());
2814
+ }
2815
+ },
2816
+ computed: {
2817
+ rootClasses() {
2818
+ const sizeClass = this.mini
2819
+ ? 'p-0.5 text-[10px]'
2820
+ : (this.compact ? 'p-2 text-xs' : 'p-3 text-sm');
2821
+ const chromeClass = this.embedded
2822
+ ? ''
2823
+ : (this.mini ? '' : 'bg-surface border border-edge-strong rounded-md shadow-sm');
2824
+ return [sizeClass, chromeClass];
2825
+ },
2826
+ timeSectionClasses() {
2827
+ if (this.horizontal) {
2828
+ return 'flex-1 min-w-0 border-l border-edge-strong pl-2';
2829
+ }
2830
+ return [
2831
+ 'border-t border-edge-strong',
2832
+ this.mini ? 'mt-1 pt-1' : 'mt-3 pt-3'
2833
+ ];
2834
+ },
2835
+ wheelPadCount() {
2836
+ return 2;
2837
+ },
2838
+ wheelVisibleRows() {
2839
+ return 5;
2840
+ },
2841
+ weekdayLabels() {
2842
+ return WEEKDAY_LABELS;
2843
+ },
2844
+ monthLabel() {
2845
+ return `${MONTH_LABELS[this.viewMonth]} ${this.viewYear}`;
2846
+ },
2847
+ calendarDays() {
2848
+ return buildCalendarDays(this.viewYear, this.viewMonth);
2849
+ },
2850
+ today() {
2851
+ const now = new Date();
2852
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
2853
+ },
2854
+ showTime() {
2855
+ return this.mode === 'datetime';
2856
+ },
2857
+ dayCellClass() {
2858
+ if (this.mini) {
2859
+ return 'h-6';
2860
+ }
2861
+ if (this.compact) {
2862
+ return 'h-7';
2863
+ }
2864
+ return 'aspect-square';
2865
+ },
2866
+ calendarSectionClass() {
2867
+ if (this.mini) return 'w-36';
2868
+ if (this.compact) return 'w-44';
2869
+ return 'w-48';
2870
+ },
2871
+ wheelItemPx() {
2872
+ if (this.mini) {
2873
+ return 32;
2874
+ }
2875
+ if (this.compact) {
2876
+ return 36;
2877
+ }
2878
+ return 40;
2879
+ },
2880
+ wheelListClass() {
2881
+ return [
2882
+ 'overflow-y-auto overscroll-y-contain snap-y snap-mandatory',
2883
+ '[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
2884
+ ];
2885
+ },
2886
+ wheelListStyle() {
2887
+ return {
2888
+ height: `${this.wheelItemPx * this.wheelVisibleRows}px`
2889
+ };
2890
+ },
2891
+ wheelItemClass() {
2892
+ return [
2893
+ 'snap-center shrink-0 w-full flex items-center justify-center font-mono transition-colors',
2894
+ this.mini ? 'h-8 text-sm' : (this.compact ? 'h-9 text-sm' : 'h-10 text-base')
2895
+ ];
2896
+ },
2897
+ wheelHighlightClass() {
2898
+ return this.mini ? 'h-8' : (this.compact ? 'h-9' : 'h-10');
2899
+ },
2900
+ wheelColumnClass() {
2901
+ return this.mini ? 'w-11' : (this.compact ? 'w-12' : 'w-14');
2902
+ }
2903
+ },
2904
+ mounted() {
2905
+ this.$nextTick(() => this.scrollWheelsToValues());
2906
+ },
2907
+ beforeUnmount() {
2908
+ if (this.timeApplyTimer) {
2909
+ clearTimeout(this.timeApplyTimer);
2910
+ }
2911
+ },
2912
+ methods: {
2913
+ syncFromValue() {
2914
+ const selected = parseValueToDate(this.value);
2915
+ const anchor = selected || new Date();
2916
+ this.selectedDate = anchor;
2917
+ this.viewYear = anchor.getFullYear();
2918
+ this.viewMonth = anchor.getMonth();
2919
+ const time = timePartsFromDate(anchor);
2920
+ this.hour = time.hour;
2921
+ this.minute = time.minute;
2922
+ this.second = time.second;
2923
+ this.millisecond = time.millisecond;
2924
+ this.$nextTick(() => this.scrollWheelsToValues());
2925
+ },
2926
+ clampedTimeParts() {
2927
+ return {
2928
+ hour: clampInt(this.hour, 0, 23),
2929
+ minute: clampInt(this.minute, 0, 59),
2930
+ second: clampInt(this.second, 0, 59),
2931
+ millisecond: clampInt(this.millisecond, 0, 999)
2932
+ };
2933
+ },
2934
+ formatWheelValue(value) {
2935
+ return pad2(value);
2936
+ },
2937
+ wheelItemClasses(value, selectedValue) {
2938
+ return value === selectedValue
2939
+ ? 'text-content-primary font-semibold'
2940
+ : 'text-content-tertiary hover:text-content-secondary';
2941
+ },
2942
+ wheelIndexFromScroll(el, max) {
2943
+ return clampInt(Math.round(el.scrollTop / this.wheelItemPx), 0, max);
2944
+ },
2945
+ scrollWheelTo(el, index) {
2946
+ if (!el) {
2947
+ return;
2948
+ }
2949
+ el.scrollTop = index * this.wheelItemPx;
2950
+ },
2951
+ scrollWheelsToValues() {
2952
+ this.wheelScrollLock = true;
2953
+ this.scrollWheelTo(this.$refs.hourWheel, this.hour);
2954
+ this.scrollWheelTo(this.$refs.minuteWheel, this.minute);
2955
+ this.scrollWheelTo(this.$refs.secondWheel, this.second);
2956
+ requestAnimationFrame(() => {
2957
+ this.wheelScrollLock = false;
2958
+ });
2959
+ },
2960
+ scheduleTimeApply() {
2961
+ if (this.timeApplyTimer) {
2962
+ clearTimeout(this.timeApplyTimer);
2963
+ }
2964
+ this.timeApplyTimer = setTimeout(() => {
2965
+ this.timeApplyTimer = null;
2966
+ this.applyTimeToSelection();
2967
+ }, 120);
2968
+ },
2969
+ onHourWheelScroll(event) {
2970
+ if (this.wheelScrollLock) {
2971
+ return;
2972
+ }
2973
+ const hour = this.wheelIndexFromScroll(event.target, 23);
2974
+ if (hour === this.hour) {
2975
+ return;
2976
+ }
2977
+ this.hour = hour;
2978
+ this.scheduleTimeApply();
2979
+ },
2980
+ onMinuteWheelScroll(event) {
2981
+ if (this.wheelScrollLock) {
2982
+ return;
2983
+ }
2984
+ const minute = this.wheelIndexFromScroll(event.target, 59);
2985
+ if (minute === this.minute) {
2986
+ return;
2987
+ }
2988
+ this.minute = minute;
2989
+ this.scheduleTimeApply();
2990
+ },
2991
+ onSecondWheelScroll(event) {
2992
+ if (this.wheelScrollLock) {
2993
+ return;
2994
+ }
2995
+ const second = this.wheelIndexFromScroll(event.target, 59);
2996
+ if (second === this.second) {
2997
+ return;
2998
+ }
2999
+ this.second = second;
3000
+ this.scheduleTimeApply();
3001
+ },
3002
+ selectHour(hour) {
3003
+ this.hour = hour;
3004
+ this.scrollWheelTo(this.$refs.hourWheel, hour);
3005
+ this.applyTimeToSelection();
3006
+ },
3007
+ selectMinute(minute) {
3008
+ this.minute = minute;
3009
+ this.scrollWheelTo(this.$refs.minuteWheel, minute);
3010
+ this.applyTimeToSelection();
3011
+ },
3012
+ selectSecond(second) {
3013
+ this.second = second;
3014
+ this.scrollWheelTo(this.$refs.secondWheel, second);
3015
+ this.applyTimeToSelection();
3016
+ },
3017
+ dayClasses(day) {
3018
+ const selected = this.selectedDate;
3019
+ const isSelected = selected && isSameDay(day.date, selected);
3020
+ const isToday = isSameDay(day.date, this.today);
3021
+ return {
3022
+ 'text-content-tertiary': !day.inMonth,
3023
+ 'bg-merlot-700 text-white hover:bg-merlot-800': isSelected,
3024
+ 'hover:bg-surface-hover': day.inMonth && !isSelected,
3025
+ 'ring-1 ring-merlot-700 ring-inset': isToday && !isSelected && day.inMonth,
3026
+ 'font-medium': day.inMonth
3027
+ };
3028
+ },
3029
+ prevMonth() {
3030
+ if (this.viewMonth === 0) {
3031
+ this.viewMonth = 11;
3032
+ this.viewYear -= 1;
3033
+ } else {
3034
+ this.viewMonth -= 1;
3035
+ }
3036
+ },
3037
+ nextMonth() {
3038
+ if (this.viewMonth === 11) {
3039
+ this.viewMonth = 0;
3040
+ this.viewYear += 1;
3041
+ } else {
3042
+ this.viewMonth += 1;
3043
+ }
3044
+ },
3045
+ goToToday() {
3046
+ const now = new Date();
3047
+ this.viewYear = now.getFullYear();
3048
+ this.viewMonth = now.getMonth();
3049
+ this.selectDay({
3050
+ date: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
3051
+ inMonth: true
3052
+ });
3053
+ },
3054
+ selectDay(day) {
3055
+ const { hour, minute, second, millisecond } = this.clampedTimeParts();
3056
+ const next = new Date(day.date);
3057
+ if (this.showTime) {
3058
+ next.setHours(hour, minute, second, millisecond);
3059
+ } else {
3060
+ next.setHours(0, 0, 0, 0);
3061
+ }
3062
+ this.selectedDate = next;
3063
+ if (!day.inMonth) {
3064
+ this.viewYear = next.getFullYear();
3065
+ this.viewMonth = next.getMonth();
3066
+ }
3067
+ this.emitValue(next);
3068
+ },
3069
+ applyTimeToSelection() {
3070
+ const { hour, minute, second, millisecond } = this.clampedTimeParts();
3071
+ this.hour = hour;
3072
+ this.minute = minute;
3073
+ this.second = second;
3074
+ this.millisecond = millisecond;
3075
+
3076
+ if (!this.selectedDate) {
3077
+ const now = new Date();
3078
+ this.selectedDate = new Date(
3079
+ now.getFullYear(),
3080
+ now.getMonth(),
3081
+ now.getDate(),
3082
+ hour,
3083
+ minute,
3084
+ second,
3085
+ millisecond
3086
+ );
3087
+ this.viewYear = this.selectedDate.getFullYear();
3088
+ this.viewMonth = this.selectedDate.getMonth();
3089
+ } else {
3090
+ this.selectedDate = new Date(
3091
+ this.selectedDate.getFullYear(),
3092
+ this.selectedDate.getMonth(),
3093
+ this.selectedDate.getDate(),
3094
+ hour,
3095
+ minute,
3096
+ second,
3097
+ millisecond
3098
+ );
3099
+ }
3100
+ this.emitValue(this.selectedDate);
3101
+ },
3102
+ valuePayload(date) {
3103
+ if (this.mode === 'date') {
3104
+ return dateToDateOnly(date);
3105
+ }
3106
+ return dateToDatetimeLocal(date);
3107
+ },
3108
+ emitValue(date) {
3109
+ const payload = this.valuePayload(date);
3110
+ if (!this.immediate) {
3111
+ this.pendingValue = payload;
3112
+ return;
3113
+ }
3114
+ this.$emit('input', payload);
3115
+ },
3116
+ commit() {
3117
+ const payload = this.pendingValue != null
3118
+ ? this.pendingValue
3119
+ : (this.selectedDate ? this.valuePayload(this.selectedDate) : null);
3120
+ if (payload == null) {
3121
+ return;
3122
+ }
3123
+ this.$emit('commit', payload);
3124
+ this.pendingValue = null;
3125
+ },
3126
+ isSelectedDay(day) {
3127
+ return this.selectedDate && isSameDay(day.date, this.selectedDate);
3128
+ }
3129
+ }
3130
+ });
3131
+
3132
+
2467
3133
  /***/ },
2468
3134
 
2469
3135
  /***/ "./frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js"
@@ -4880,7 +5546,7 @@ const template = __webpack_require__(/*! ./date-view-mode-picker.html */ "./fron
4880
5546
 
4881
5547
  module.exports = app => app.component('date-view-mode-picker', {
4882
5548
  template,
4883
- props: ['viewMode', 'path'],
5549
+ props: ['viewMode', 'path', 'editting'],
4884
5550
  emits: ['update:viewMode'],
4885
5551
  computed: {
4886
5552
  format() {
@@ -4937,20 +5603,12 @@ appendCSS(__webpack_require__(/*! ./document-details.css */ "./frontend/src/docu
4937
5603
 
4938
5604
  module.exports = app => app.component('document-details', {
4939
5605
  template,
4940
- props: ['document', 'schemaPaths', 'virtualPaths', 'editting', 'changes', 'invalid', 'viewMode', 'model'],
5606
+ props: ['document', 'schemaPaths', 'virtualPaths', 'editting', 'changes', 'invalid', 'viewMode'],
4941
5607
  data() {
4942
5608
  return {
4943
5609
  searchQuery: '',
4944
5610
  selectedType: '',
4945
- collapsedVirtuals: new Set(),
4946
- showAddFieldModal: false,
4947
- fieldData: {
4948
- name: '',
4949
- type: '',
4950
- value: ''
4951
- },
4952
- fieldErrors: {},
4953
- isSubmittingField: false
5611
+ collapsedVirtuals: new Set()
4954
5612
  };
4955
5613
  },
4956
5614
  mounted() {
@@ -5020,20 +5678,6 @@ module.exports = app => app.component('document-details', {
5020
5678
 
5021
5679
  return Array.from(types).sort();
5022
5680
  },
5023
- allFieldTypes() {
5024
- const schemaTypes = this.availableTypes;
5025
- const commonTypes = ['String', 'Number', 'Boolean', 'Date', 'Array', 'Object'];
5026
-
5027
- // Combine schema types with common types, avoiding duplicates
5028
- const allTypes = new Set([...schemaTypes, ...commonTypes]);
5029
- return Array.from(allTypes).sort();
5030
- },
5031
- shouldUseAce() {
5032
- return ['Array', 'Object', 'Embedded'].includes(this.fieldData.type);
5033
- },
5034
- shouldUseDatePicker() {
5035
- return this.fieldData.type === 'Date';
5036
- },
5037
5681
  typeFilteredSchemaPaths() {
5038
5682
  let paths = this.schemaPaths || [];
5039
5683
 
@@ -5245,120 +5889,6 @@ module.exports = app => app.component('document-details', {
5245
5889
  isVirtualFieldCollapsed(fieldName) {
5246
5890
  return this.collapsedVirtuals.has(fieldName);
5247
5891
  },
5248
- openAddFieldModal() {
5249
- this.showAddFieldModal = true;
5250
- },
5251
- closeAddFieldModal() {
5252
- this.showAddFieldModal = false;
5253
- this.resetFieldForm();
5254
- },
5255
- async addNewField(fieldData) {
5256
- // Emit event to parent component to handle the field addition
5257
- this.$emit('add-field', fieldData);
5258
- this.closeAddFieldModal();
5259
- },
5260
- validateFieldForm() {
5261
- this.fieldErrors = {};
5262
-
5263
- // Validate field name
5264
- const trimmedName = this.fieldData.name.trim();
5265
- if (!trimmedName) {
5266
- this.fieldErrors.name = 'Field name is required';
5267
- } else {
5268
- const transformedName = this.getTransformedFieldName();
5269
- if (!transformedName) {
5270
- this.fieldErrors.name = 'Field name contains only invalid characters';
5271
- } else if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(transformedName)) {
5272
- this.fieldErrors.name = 'Field name must start with a letter, underscore, or $ and contain only letters, numbers, underscores, and $';
5273
- }
5274
- }
5275
-
5276
- // Validate field type
5277
- if (!this.fieldData.type) {
5278
- this.fieldErrors.type = 'Field type is required';
5279
- }
5280
-
5281
- // Validate field value if provided
5282
- if (this.fieldData.value && this.fieldData.value.trim()) {
5283
- if (['Object', 'Array'].includes(this.fieldData.type)) {
5284
- try {
5285
- JSON.parse(this.fieldData.value);
5286
- } catch (e) {
5287
- this.fieldErrors.value = 'Invalid JSON format for object/array type';
5288
- }
5289
- } else if (this.fieldData.type === 'Number') {
5290
- if (isNaN(Number(this.fieldData.value))) {
5291
- this.fieldErrors.value = 'Invalid number format';
5292
- }
5293
- } else if (this.fieldData.type === 'Boolean') {
5294
- const lowerValue = this.fieldData.value.toLowerCase();
5295
- if (!['true', 'false', '1', '0', 'yes', 'no'].includes(lowerValue)) {
5296
- this.fieldErrors.value = 'Invalid boolean value (use true/false, 1/0, yes/no)';
5297
- }
5298
- } else if (this.fieldData.type === 'Date') {
5299
- // Date picker provides YYYY-MM-DD format, validate it
5300
- const dateValue = new Date(this.fieldData.value);
5301
- if (isNaN(dateValue.getTime())) {
5302
- this.fieldErrors.value = 'Invalid date format';
5303
- }
5304
- }
5305
- }
5306
-
5307
- return Object.keys(this.fieldErrors).length === 0;
5308
- },
5309
- parseFieldValue(value, type) {
5310
- if (!value || !value.trim()) {
5311
- return null;
5312
- }
5313
-
5314
- switch (type) {
5315
- case 'Number':
5316
- return Number(value);
5317
- case 'Boolean':
5318
- const lowerValue = value.toLowerCase();
5319
- return ['true', '1', 'yes'].includes(lowerValue);
5320
- case 'Date':
5321
- // For date picker, value is already in YYYY-MM-DD format
5322
- return new Date(value);
5323
- case 'Object':
5324
- case 'Array':
5325
- return JSON.parse(value);
5326
- default:
5327
- return value;
5328
- }
5329
- },
5330
- async handleAddFieldSubmit() {
5331
- if (!this.validateFieldForm()) {
5332
- return;
5333
- }
5334
-
5335
- this.isSubmittingField = true;
5336
-
5337
- try {
5338
- const fieldData = {
5339
- name: this.fieldData.name,
5340
- type: this.fieldData.type,
5341
- value: this.parseFieldValue(this.fieldData.value, this.fieldData.type)
5342
- };
5343
-
5344
- this.$emit('add-field', fieldData);
5345
- this.closeAddFieldModal();
5346
- } catch (error) {
5347
- console.error('Error adding field:', error);
5348
- this.fieldErrors.value = 'Error processing field value';
5349
- } finally {
5350
- this.isSubmittingField = false;
5351
- }
5352
- },
5353
- resetFieldForm() {
5354
- this.fieldData = {
5355
- name: '',
5356
- type: '',
5357
- value: ''
5358
- };
5359
- this.fieldErrors = {};
5360
- this.isSubmittingField = false;
5361
- },
5362
5892
  getVirtualFieldType(virtual) {
5363
5893
  const value = virtual.value;
5364
5894
  if (value === null || value === undefined) {
@@ -5941,7 +6471,15 @@ module.exports = app => app.component('document', {
5941
6471
  shouldShowDeleteModal: false,
5942
6472
  shouldShowCloneModal: false,
5943
6473
  shouldShowValidationModal: false,
6474
+ showAddFieldModal: false,
5944
6475
  validationResult: null,
6476
+ fieldData: {
6477
+ name: '',
6478
+ type: '',
6479
+ value: ''
6480
+ },
6481
+ fieldErrors: {},
6482
+ isSubmittingField: false,
5945
6483
  previousQuery: null,
5946
6484
  lastUpdatedAt: null,
5947
6485
  isRefreshing: false,
@@ -6003,6 +6541,33 @@ module.exports = app => app.component('document', {
6003
6541
  }
6004
6542
  return shortcuts;
6005
6543
  },
6544
+ allFieldTypes() {
6545
+ const types = new Set(['String', 'Number', 'Boolean', 'Date', 'Array', 'Object']);
6546
+
6547
+ (this.schemaPaths || []).forEach(path => {
6548
+ if (path.instance) {
6549
+ types.add(path.instance);
6550
+ }
6551
+ });
6552
+
6553
+ Object.keys(this.document || {}).forEach(key => {
6554
+ if ((this.schemaPaths || []).some(path => path.path === key)) {
6555
+ return;
6556
+ }
6557
+ const fieldType = this.getFieldType(this.document[key]);
6558
+ if (fieldType && fieldType !== 'unknown' && fieldType !== 'null') {
6559
+ types.add(fieldType);
6560
+ }
6561
+ });
6562
+
6563
+ return Array.from(types).sort();
6564
+ },
6565
+ shouldUseAce() {
6566
+ return ['Array', 'Object', 'Embedded'].includes(this.fieldData.type);
6567
+ },
6568
+ shouldUseDatePicker() {
6569
+ return this.fieldData.type === 'Date';
6570
+ },
6006
6571
  isLambda() {
6007
6572
  return !!window?.MONGOOSE_STUDIO_CONFIG?.isLambda;
6008
6573
  }
@@ -6199,16 +6764,143 @@ module.exports = app => app.component('document', {
6199
6764
  showClonedDocument(doc) {
6200
6765
  this.$router.push({ path: `/model/${this.model}/document/${doc._id}` });
6201
6766
  },
6202
- async addField(fieldData) {
6203
- const { doc } = await api.Model.addField({
6204
- model: this.model,
6205
- _id: this.document._id,
6206
- fieldName: fieldData.name,
6207
- fieldValue: fieldData.value
6208
- });
6209
- this.document = doc;
6767
+ addField() {
6768
+ this.showAddFieldModal = true;
6769
+ },
6770
+ closeAddFieldModal() {
6771
+ this.showAddFieldModal = false;
6772
+ this.resetFieldForm();
6773
+ },
6774
+ resetFieldForm() {
6775
+ this.fieldData = {
6776
+ name: '',
6777
+ type: '',
6778
+ value: ''
6779
+ };
6780
+ this.fieldErrors = {};
6781
+ this.isSubmittingField = false;
6782
+ },
6783
+ getTransformedFieldName() {
6784
+ const trimmedName = this.fieldData.name.trim();
6785
+ return trimmedName
6786
+ .replace(/[^a-zA-Z0-9_$]/g, '_')
6787
+ .replace(/^[^a-zA-Z_$]+/, '');
6788
+ },
6789
+ validateFieldForm() {
6790
+ this.fieldErrors = {};
6210
6791
 
6211
- this.$toast.success(`Field added! Field "${fieldData.name}" has been added to the document`);
6792
+ const trimmedName = this.fieldData.name.trim();
6793
+ if (!trimmedName) {
6794
+ this.fieldErrors.name = 'Field name is required';
6795
+ } else {
6796
+ const transformedName = this.getTransformedFieldName();
6797
+ if (!transformedName) {
6798
+ this.fieldErrors.name = 'Field name contains only invalid characters';
6799
+ } else if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(transformedName)) {
6800
+ this.fieldErrors.name = 'Field name must start with a letter, underscore, or $ and contain only letters, numbers, underscores, and $';
6801
+ }
6802
+ }
6803
+
6804
+ if (!this.fieldData.type) {
6805
+ this.fieldErrors.type = 'Field type is required';
6806
+ }
6807
+
6808
+ if (this.fieldData.value && this.fieldData.value.trim()) {
6809
+ if (['Object', 'Array'].includes(this.fieldData.type)) {
6810
+ try {
6811
+ JSON.parse(this.fieldData.value);
6812
+ } catch (err) {
6813
+ this.fieldErrors.value = 'Invalid JSON format for object/array type';
6814
+ }
6815
+ } else if (this.fieldData.type === 'Number') {
6816
+ if (isNaN(Number(this.fieldData.value))) {
6817
+ this.fieldErrors.value = 'Invalid number format';
6818
+ }
6819
+ } else if (this.fieldData.type === 'Boolean') {
6820
+ const lowerValue = this.fieldData.value.toLowerCase();
6821
+ if (!['true', 'false', '1', '0', 'yes', 'no'].includes(lowerValue)) {
6822
+ this.fieldErrors.value = 'Invalid boolean value (use true/false, 1/0, yes/no)';
6823
+ }
6824
+ } else if (this.fieldData.type === 'Date') {
6825
+ const dateValue = new Date(this.fieldData.value);
6826
+ if (isNaN(dateValue.getTime())) {
6827
+ this.fieldErrors.value = 'Invalid date format';
6828
+ }
6829
+ }
6830
+ }
6831
+
6832
+ return Object.keys(this.fieldErrors).length === 0;
6833
+ },
6834
+ parseFieldValue(value, type) {
6835
+ if (!value || !value.trim()) {
6836
+ return null;
6837
+ }
6838
+
6839
+ switch (type) {
6840
+ case 'Number':
6841
+ return Number(value);
6842
+ case 'Boolean':
6843
+ return ['true', '1', 'yes'].includes(value.toLowerCase());
6844
+ case 'Date':
6845
+ return new Date(value);
6846
+ case 'Object':
6847
+ case 'Array':
6848
+ return JSON.parse(value);
6849
+ default:
6850
+ return value;
6851
+ }
6852
+ },
6853
+ async handleAddFieldSubmit() {
6854
+ if (!this.validateFieldForm()) {
6855
+ return;
6856
+ }
6857
+
6858
+ this.isSubmittingField = true;
6859
+
6860
+ try {
6861
+ const fieldName = this.getTransformedFieldName();
6862
+ const fieldValue = this.parseFieldValue(this.fieldData.value, this.fieldData.type);
6863
+
6864
+ const { doc } = await api.Model.addField({
6865
+ model: this.model,
6866
+ _id: this.document._id,
6867
+ fieldName,
6868
+ fieldValue
6869
+ });
6870
+ this.document = doc;
6871
+ this.closeAddFieldModal();
6872
+
6873
+ this.$toast.success(`Field added! Field "${fieldName}" has been added to the document`);
6874
+ } catch (error) {
6875
+ console.error('Error adding field:', error);
6876
+ this.fieldErrors.value = 'Error adding field';
6877
+ } finally {
6878
+ this.isSubmittingField = false;
6879
+ }
6880
+ },
6881
+ getFieldType(value) {
6882
+ if (value === null || value === undefined) {
6883
+ return 'null';
6884
+ }
6885
+ if (Array.isArray(value)) {
6886
+ return 'Array';
6887
+ }
6888
+ if (value instanceof Date) {
6889
+ return 'Date';
6890
+ }
6891
+ if (typeof value === 'object') {
6892
+ return 'Object';
6893
+ }
6894
+ if (typeof value === 'number') {
6895
+ return 'Number';
6896
+ }
6897
+ if (typeof value === 'boolean') {
6898
+ return 'Boolean';
6899
+ }
6900
+ if (typeof value === 'string') {
6901
+ return 'String';
6902
+ }
6903
+ return 'unknown';
6212
6904
  },
6213
6905
  updateViewMode(mode) {
6214
6906
  this.viewMode = mode;
@@ -6639,9 +7331,6 @@ module.exports = app => app.component('edit-date', {
6639
7331
  template: template,
6640
7332
  props: ['value', 'format'],
6641
7333
  emits: ['input'],
6642
- data: () => ({
6643
- inputType: ''
6644
- }),
6645
7334
  methods: {
6646
7335
  updateFromISO($event) {
6647
7336
  const value = $event.target.value;
@@ -6661,23 +7350,6 @@ module.exports = app => app.component('edit-date', {
6661
7350
  }
6662
7351
  },
6663
7352
  computed: {
6664
- valueAsLocalString() {
6665
- if (this.value == null) {
6666
- return this.value;
6667
- }
6668
- const date = new Date(this.value);
6669
- return [
6670
- date.getFullYear(),
6671
- '-',
6672
- (date.getMonth() + 1).toString().padStart(2, '0'),
6673
- '-',
6674
- date.getDate().toString().padStart(2, '0'),
6675
- 'T',
6676
- date.getHours().toString().padStart(2, '0'),
6677
- ':',
6678
- date.getMinutes().toString().padStart(2, '0')
6679
- ].join('');
6680
- },
6681
7353
  valueAsISOString() {
6682
7354
  if (this.value == null) {
6683
7355
  return '' + this.value;
@@ -8350,7 +9022,10 @@ const template = __webpack_require__(/*! ./document-search.html */ "./frontend/s
8350
9022
  const {
8351
9023
  buildAutocompleteTrie,
8352
9024
  getAutocompleteSuggestions,
8353
- applySuggestion
9025
+ applySuggestion,
9026
+ getDatePickerInsertionRange,
9027
+ dateArgumentSliceToDatetimeLocal,
9028
+ insertDateInDateArgument
8354
9029
  } = __webpack_require__(/*! ../../_util/document-search-autocomplete */ "./frontend/src/_util/document-search-autocomplete.js");
8355
9030
 
8356
9031
  module.exports = app => app.component('document-search', {
@@ -8370,7 +9045,11 @@ module.exports = app => app.component('document-search', {
8370
9045
  autocompleteSuggestions: [],
8371
9046
  autocompleteIndex: 0,
8372
9047
  autocompleteTrie: null,
8373
- searchText: this.value || ''
9048
+ searchText: this.value || '',
9049
+ datePickerContext: null,
9050
+ datePickerLocalValue: '',
9051
+ dateInsertFormat: 'quoted', // 'timestamp' | 'quoted'
9052
+ datePickerMinimized: false
8374
9053
  };
8375
9054
  },
8376
9055
  watch: {
@@ -8412,10 +9091,31 @@ module.exports = app => app.component('document-search', {
8412
9091
  });
8413
9092
  }
8414
9093
  },
9094
+ onSearchKeyup(ev) {
9095
+ if (
9096
+ this.autocompleteSuggestions.length > 0 &&
9097
+ (ev.key === 'ArrowUp' || ev.key === 'ArrowDown')
9098
+ ) {
9099
+ return;
9100
+ }
9101
+ this.updateAutocomplete();
9102
+ },
8415
9103
  updateAutocomplete() {
8416
9104
  const input = this.$refs.searchInput;
8417
9105
  const cursorPos = input ? input.selectionStart : 0;
8418
9106
 
9107
+ const dateRange = getDatePickerInsertionRange(this.searchText, cursorPos);
9108
+ if (dateRange && !this.datePickerContext) {
9109
+ this.datePickerMinimized = false;
9110
+ }
9111
+ this.datePickerContext = dateRange;
9112
+ if (dateRange) {
9113
+ const argSlice = this.searchText.slice(dateRange.innerStart, dateRange.innerEnd);
9114
+ this.datePickerLocalValue = dateArgumentSliceToDatetimeLocal(argSlice);
9115
+ } else {
9116
+ this.datePickerLocalValue = '';
9117
+ }
9118
+
8419
9119
  if (this.autocompleteTrie) {
8420
9120
  this.autocompleteSuggestions = getAutocompleteSuggestions(
8421
9121
  this.autocompleteTrie,
@@ -8428,6 +9128,34 @@ module.exports = app => app.component('document-search', {
8428
9128
  this.autocompleteSuggestions = [];
8429
9129
  }
8430
9130
  },
9131
+ applyDateFromPicker(localDateTime) {
9132
+ if (!localDateTime || !this.datePickerContext) {
9133
+ return;
9134
+ }
9135
+ const picked = new Date(localDateTime);
9136
+ if (Number.isNaN(picked.getTime())) {
9137
+ if (this.$toast) {
9138
+ this.$toast.error('Invalid date or time. Enter a valid date and time, then try again.');
9139
+ }
9140
+ return;
9141
+ }
9142
+ const result = insertDateInDateArgument(
9143
+ this.searchText,
9144
+ this.datePickerContext,
9145
+ picked,
9146
+ this.dateInsertFormat
9147
+ );
9148
+ const input = this.$refs.searchInput;
9149
+ this.searchText = result.text;
9150
+ this.autocompleteSuggestions = [];
9151
+ this.$nextTick(() => {
9152
+ if (input) {
9153
+ input.focus();
9154
+ input.setSelectionRange(result.newCursorPos, result.newCursorPos);
9155
+ }
9156
+ this.updateAutocomplete();
9157
+ });
9158
+ },
8431
9159
  handleKeyDown(ev) {
8432
9160
  if (this.autocompleteSuggestions.length === 0) {
8433
9161
  return;
@@ -8462,6 +9190,12 @@ module.exports = app => app.component('document-search', {
8462
9190
  });
8463
9191
  this.autocompleteSuggestions = [];
8464
9192
  },
9193
+ onWrapperFocusOut(ev) {
9194
+ if (!this.$el.contains(ev.relatedTarget)) {
9195
+ this.datePickerContext = null;
9196
+ this.datePickerMinimized = false;
9197
+ }
9198
+ },
8465
9199
  addPathFilter(path) {
8466
9200
  if (this.searchText) {
8467
9201
  if (this.searchText.endsWith('}')) {
@@ -12379,6 +13113,8 @@ var map = {
12379
13113
  "./": "./frontend/src/index.js",
12380
13114
  "./_util/baseComponent": "./frontend/src/_util/baseComponent.js",
12381
13115
  "./_util/baseComponent.js": "./frontend/src/_util/baseComponent.js",
13116
+ "./_util/calendar": "./frontend/src/_util/calendar.js",
13117
+ "./_util/calendar.js": "./frontend/src/_util/calendar.js",
12382
13118
  "./_util/dateRange": "./frontend/src/_util/dateRange.js",
12383
13119
  "./_util/dateRange.js": "./frontend/src/_util/dateRange.js",
12384
13120
  "./_util/deepEqual": "./frontend/src/_util/deepEqual.js",
@@ -12421,6 +13157,9 @@ var map = {
12421
13157
  "./create-document/create-document.css": "./frontend/src/create-document/create-document.css",
12422
13158
  "./create-document/create-document.html": "./frontend/src/create-document/create-document.html",
12423
13159
  "./create-document/create-document.js": "./frontend/src/create-document/create-document.js",
13160
+ "./custom-date-picker/custom-date-picker": "./frontend/src/custom-date-picker/custom-date-picker.js",
13161
+ "./custom-date-picker/custom-date-picker.html": "./frontend/src/custom-date-picker/custom-date-picker.html",
13162
+ "./custom-date-picker/custom-date-picker.js": "./frontend/src/custom-date-picker/custom-date-picker.js",
12424
13163
  "./dashboard-result/dashboard-chart/dashboard-chart": "./frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js",
12425
13164
  "./dashboard-result/dashboard-chart/dashboard-chart.html": "./frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html",
12426
13165
  "./dashboard-result/dashboard-chart/dashboard-chart.js": "./frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js",
@@ -52856,6 +53595,17 @@ module.exports = "<div class=\"px-1\">\n <div class=\"mb-4\">\n <label class
52856
53595
 
52857
53596
  /***/ },
52858
53597
 
53598
+ /***/ "./frontend/src/custom-date-picker/custom-date-picker.html"
53599
+ /*!*****************************************************************!*\
53600
+ !*** ./frontend/src/custom-date-picker/custom-date-picker.html ***!
53601
+ \*****************************************************************/
53602
+ (module) {
53603
+
53604
+ "use strict";
53605
+ module.exports = "<div\n class=\"custom-date-picker\"\n :class=\"rootClasses\"\n>\n <div :class=\"horizontal ? 'flex gap-3 items-start' : ''\">\n <div :class=\"[horizontal ? 'shrink-0' : '', calendarSectionClass]\">\n <div class=\"flex items-center justify-between gap-1 mb-1\" :class=\"mini ? 'mb-0.5' : 'mb-2'\">\n <button\n type=\"button\"\n class=\"shrink-0 flex items-center justify-center rounded border border-edge-strong hover:bg-surface-hover text-content-secondary\"\n :class=\"mini ? 'w-5 h-5 text-[10px]' : 'w-7 h-7'\"\n aria-label=\"Previous month\"\n @click=\"prevMonth\"\n >‹</button>\n <span\n class=\"flex-1 min-w-0 text-center font-medium text-content-primary truncate\"\n :class=\"mini ? 'text-[10px]' : ''\"\n >{{ monthLabel }}</span>\n <button\n type=\"button\"\n class=\"shrink-0 flex items-center justify-center rounded border border-edge-strong hover:bg-surface-hover text-content-secondary\"\n :class=\"mini ? 'w-5 h-5 text-[10px]' : 'w-7 h-7'\"\n aria-label=\"Next month\"\n @click=\"nextMonth\"\n >›</button>\n </div>\n\n <div class=\"grid grid-cols-7 gap-px mb-0.5\">\n <div\n v-for=\"label in weekdayLabels\"\n :key=\"label\"\n class=\"flex items-center justify-center text-center text-content-tertiary font-medium min-w-0\"\n :class=\"[\n dayCellClass,\n mini ? 'text-[9px]' : (compact ? 'text-[10px]' : 'text-xs')\n ]\"\n >{{ mini ? label.charAt(0) : label }}</div>\n </div>\n\n <div class=\"grid grid-cols-7 gap-px\">\n <button\n v-for=\"day in calendarDays\"\n :key=\"day.key\"\n type=\"button\"\n class=\"min-w-0 w-full flex items-center justify-center rounded transition-colors\"\n :class=\"[\n dayCellClass,\n mini ? 'text-[10px]' : (compact ? 'text-xs' : 'text-sm'),\n dayClasses(day)\n ]\"\n :aria-label=\"day.label\"\n :aria-pressed=\"isSelectedDay(day)\"\n @click=\"selectDay(day)\"\n >{{ day.label }}</button>\n </div>\n </div>\n\n <div\n v-if=\"showTime\"\n :class=\"timeSectionClasses\"\n @mousedown.stop\n >\n <div\n class=\"text-content-secondary\"\n :class=\"[\n horizontal ? 'mb-1 text-xs' : 'mb-2',\n mini ? 'text-xs' : (horizontal ? '' : 'text-sm')\n ]\"\n >Time</div>\n <div class=\"relative flex items-center justify-center\">\n <div\n class=\"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 border-y border-edge-strong bg-surface-hover/40\"\n :class=\"wheelHighlightClass\"\n aria-hidden=\"true\"\n ></div>\n <div class=\"relative flex items-center justify-center gap-1\">\n <div\n ref=\"hourWheel\"\n class=\"wheel-column\"\n :class=\"[wheelListClass, wheelColumnClass]\"\n :style=\"wheelListStyle\"\n @scroll=\"onHourWheelScroll\"\n >\n <div\n v-for=\"n in wheelPadCount\"\n :key=\"'hour-pad-top-' + n\"\n :class=\"wheelItemClass\"\n aria-hidden=\"true\"\n ></div>\n <button\n v-for=\"h in hourOptions\"\n :key=\"'hour-' + h\"\n type=\"button\"\n :class=\"[wheelItemClass, wheelItemClasses(h, hour)]\"\n :aria-label=\"'Hour ' + h\"\n :aria-selected=\"hour === h\"\n @click=\"selectHour(h)\"\n >{{ formatWheelValue(h) }}</button>\n <div\n v-for=\"n in wheelPadCount\"\n :key=\"'hour-pad-bottom-' + n\"\n :class=\"wheelItemClass\"\n aria-hidden=\"true\"\n ></div>\n </div>\n <span class=\"text-content-secondary font-medium\" :class=\"mini ? 'text-sm' : 'text-base'\">:</span>\n <div\n ref=\"minuteWheel\"\n class=\"wheel-column\"\n :class=\"[wheelListClass, wheelColumnClass]\"\n :style=\"wheelListStyle\"\n @scroll=\"onMinuteWheelScroll\"\n >\n <div\n v-for=\"n in wheelPadCount\"\n :key=\"'minute-pad-top-' + n\"\n :class=\"wheelItemClass\"\n aria-hidden=\"true\"\n ></div>\n <button\n v-for=\"m in minuteOptions\"\n :key=\"'minute-' + m\"\n type=\"button\"\n :class=\"[wheelItemClass, wheelItemClasses(m, minute)]\"\n :aria-label=\"'Minute ' + m\"\n :aria-selected=\"minute === m\"\n @click=\"selectMinute(m)\"\n >{{ formatWheelValue(m) }}</button>\n <div\n v-for=\"n in wheelPadCount\"\n :key=\"'minute-pad-bottom-' + n\"\n :class=\"wheelItemClass\"\n aria-hidden=\"true\"\n ></div>\n </div>\n <span class=\"text-content-secondary font-medium\" :class=\"mini ? 'text-sm' : 'text-base'\">:</span>\n <div\n ref=\"secondWheel\"\n class=\"wheel-column\"\n :class=\"[wheelListClass, wheelColumnClass]\"\n :style=\"wheelListStyle\"\n @scroll=\"onSecondWheelScroll\"\n >\n <div\n v-for=\"n in wheelPadCount\"\n :key=\"'second-pad-top-' + n\"\n :class=\"wheelItemClass\"\n aria-hidden=\"true\"\n ></div>\n <button\n v-for=\"s in secondOptions\"\n :key=\"'second-' + s\"\n type=\"button\"\n :class=\"[wheelItemClass, wheelItemClasses(s, second)]\"\n :aria-label=\"'Second ' + s\"\n :aria-selected=\"second === s\"\n @click=\"selectSecond(s)\"\n >{{ formatWheelValue(s) }}</button>\n <div\n v-for=\"n in wheelPadCount\"\n :key=\"'second-pad-bottom-' + n\"\n :class=\"wheelItemClass\"\n aria-hidden=\"true\"\n ></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div\n class=\"flex justify-end items-center gap-2\"\n :class=\"mini ? 'mt-1' : 'mt-2'\"\n >\n <button\n v-if=\"!immediate\"\n type=\"button\"\n class=\"text-xs font-medium text-white bg-merlot-700 hover:bg-merlot-800 px-2 py-1 rounded\"\n :class=\"mini ? 'py-0.5 px-1.5' : ''\"\n @click=\"commit\"\n >Apply</button>\n </div>\n</div>\n";
53606
+
53607
+ /***/ },
53608
+
52859
53609
  /***/ "./frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html"
52860
53610
  /*!****************************************************************************!*\
52861
53611
  !*** ./frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html ***!
@@ -53039,7 +53789,7 @@ module.exports = "<pre class=\"w-full whitespace-pre-wrap break-words font-mono
53039
53789
  (module) {
53040
53790
 
53041
53791
  "use strict";
53042
- module.exports = "<div class=\"flex items-center gap-2\" @click.stop>\n <select\n :value=\"format\"\n @input=\"onFormatChange($event.target.value)\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1\"\n >\n <option value=\"utc_iso\">UTC (ISO)</option>\n <option value=\"local_browser\">Local (Browser)</option>\n <option value=\"unix_ms\">Unix (ms)</option>\n <option value=\"unix_seconds\">Unix (seconds)</option>\n <option value=\"duration_relative\">Duration relative to now</option>\n <option value=\"custom_tz\">Custom TZ...</option>\n </select>\n <input\n v-if=\"format === 'custom_tz'\"\n :value=\"timezone\"\n @input=\"onTimezoneChange($event.target.value.trim())\"\n :list=\"timezoneDatalistId\"\n type=\"text\"\n placeholder=\"America/New_York\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1 w-40\"\n >\n <datalist v-if=\"format === 'custom_tz'\" :id=\"timezoneDatalistId\">\n <option v-for=\"tz in timezones\" :key=\"tz\" :value=\"tz\"></option>\n </datalist>\n</div>\n";
53792
+ module.exports = "<div class=\"flex items-center gap-2\" @click.stop>\n <select\n :value=\"format\"\n @input=\"onFormatChange($event.target.value)\"\n :disabled=\"editting\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1\"\n >\n <option value=\"utc_iso\">UTC (ISO)</option>\n <option value=\"local_browser\">Local (Browser)</option>\n <option value=\"unix_ms\">Unix (ms)</option>\n <option value=\"unix_seconds\">Unix (seconds)</option>\n <option value=\"duration_relative\">Duration relative to now</option>\n <option value=\"custom_tz\">Custom TZ...</option>\n </select>\n <input\n v-if=\"format === 'custom_tz'\"\n :value=\"timezone\"\n @input=\"onTimezoneChange($event.target.value.trim())\"\n :list=\"timezoneDatalistId\"\n type=\"text\"\n placeholder=\"America/New_York\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1 w-40\"\n >\n <datalist v-if=\"format === 'custom_tz'\" :id=\"timezoneDatalistId\">\n <option v-for=\"tz in timezones\" :key=\"tz\" :value=\"tz\"></option>\n </datalist>\n</div>\n";
53043
53793
 
53044
53794
  /***/ },
53045
53795
 
@@ -53050,7 +53800,7 @@ module.exports = "<div class=\"flex items-center gap-2\" @click.stop>\n <select
53050
53800
  (module) {
53051
53801
 
53052
53802
  "use strict";
53053
- module.exports = ".document-details {\n width: 100%;\n}\n\n.document-details .value {\n padding-top: 10px;\n padding-bottom: 10px;\n}\n\n.document-details .path-key {\n background-color: #f0f0f0;\n margin-bottom: 0.5em;\n}\n\n.document-details .path-type {\n color: rgba(0,0,0,.36);\n font-size: 0.8em;\n}\n\n.document-details .date-position {\n float: right;\n margin-top: -7px;\n}\n\n/* Add Field Modal Styles */\n.add-field-modal {\n max-width: 500px;\n width: 100%;\n}\n\n.add-field-modal .modal-exit {\n position: absolute;\n top: 15px;\n right: 20px;\n font-size: 24px;\n cursor: pointer;\n color: #6b7280;\n z-index: 10;\n}\n\n.add-field-modal .modal-exit:hover {\n color: #374151;\n}\n\n.add-field-modal form {\n max-height: 70vh;\n overflow-y: auto;\n}\n\n.add-field-modal input[type=\"text\"],\n.add-field-modal input[type=\"email\"],\n.add-field-modal input[type=\"password\"],\n.add-field-modal select,\n.add-field-modal textarea {\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.add-field-modal input:focus,\n.add-field-modal select:focus,\n.add-field-modal textarea:focus {\n border-color: #3b82f6;\n box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n}\n\n.add-field-modal .border-red-500 {\n border-color: #ef4444 !important;\n}\n\n.add-field-modal .border-red-500:focus {\n box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;\n}\n\n/* Ace editor styling in modal */\n.add-field-modal .add-field-ace-editor.ace_editor {\n border: 1px solid #d1d5db;\n border-radius: 0.375rem;\n font-size: 14px;\n min-height: 100px;\n}\n\n.add-field-modal .add-field-ace-editor:focus-within.ace_editor,\n.add-field-modal .add-field-ace-editor.ace_focus {\n border-color: #3b82f6;\n box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n}\n\n/* JSON View Styles */\n.json-view {\n width: 100%;\n}\n\n.json-view pre {\n margin: 0;\n max-height: 70vh;\n overflow: auto;\n line-height: 1.5;\n}\n\n.json-view pre::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.json-view pre::-webkit-scrollbar-track {\n background: #f1f1f1;\n border-radius: 4px;\n}\n\n.json-view pre::-webkit-scrollbar-thumb {\n background: #888;\n border-radius: 4px;\n}\n\n.json-view pre::-webkit-scrollbar-thumb:hover {\n background: #555;\n}";
53803
+ module.exports = ".document-details {\n width: 100%;\n}\n\n.document-details .value {\n padding-top: 10px;\n padding-bottom: 10px;\n}\n\n.document-details .path-key {\n background-color: #f0f0f0;\n margin-bottom: 0.5em;\n}\n\n.document-details .path-type {\n color: rgba(0,0,0,.36);\n font-size: 0.8em;\n}\n\n.document-details .date-position {\n float: right;\n margin-top: -7px;\n}\n\n/* JSON View Styles */\n.json-view {\n width: 100%;\n}\n\n.json-view pre {\n margin: 0;\n max-height: 70vh;\n overflow: auto;\n line-height: 1.5;\n}\n\n.json-view pre::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.json-view pre::-webkit-scrollbar-track {\n background: #f1f1f1;\n border-radius: 4px;\n}\n\n.json-view pre::-webkit-scrollbar-thumb {\n background: #888;\n border-radius: 4px;\n}\n\n.json-view pre::-webkit-scrollbar-thumb:hover {\n background: #555;\n}\n";
53054
53804
 
53055
53805
  /***/ },
53056
53806
 
@@ -53061,7 +53811,7 @@ module.exports = ".document-details {\n width: 100%;\n}\n\n.document-details .v
53061
53811
  (module) {
53062
53812
 
53063
53813
  "use strict";
53064
- module.exports = "<div class=\"document-details\">\n <!-- View Toggle and Search/Filter Bar -->\n <div class=\"sticky top-[60px] z-40 bg-surface rounded-md p-4 border-b border-edge shadow-sm\">\n\n <!-- Search and Filter Bar (only show in fields view) -->\n <div v-if=\"viewMode === 'fields'\" class=\"flex md:gap-3\">\n <!-- Search Bar -->\n <div class=\"relative flex-1\">\n <input\n ref=\"searchInput\"\n v-model=\"searchQuery\"\n type=\"text\"\n placeholder=\"Search fields...\"\n class=\"w-full px-4 py-2 pl-10 pr-4 text-sm border border-edge-strong rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n />\n <div class=\"absolute inset-y-0 left-0 flex items-center pl-3\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"></path>\n </svg>\n </div>\n </div>\n\n <!-- Data Type Filter -->\n <div class=\"relative hidden md:block\">\n <select\n v-model=\"selectedType\"\n class=\"px-4 py-2 pr-8 text-sm border border-edge-strong rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-surface min-w-[140px] appearance-none\"\n >\n <option value=\"\">All Types</option>\n <option v-for=\"type in availableTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <div class=\"absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Fields View -->\n <div v-if=\"viewMode === 'fields'\">\n <!-- Matched Schema Paths (shown first when searching) -->\n <div\n v-for=\"path in matchedSchemaPaths\"\n :key=\"path.path || path\"\n class=\"value\"\n >\n <document-property\n :path=\"path\"\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :highlight=\"true\"\n :searchQuery=\"searchQuery\"\n :invalid=\"invalid\"></document-property>\n </div>\n\n <!-- Matched Virtual Fields (shown after matched schema paths when searching) -->\n <div\n v-for=\"path in matchedVirtuals\"\n :key=\"path.name\"\n class=\"border border-edge rounded-lg mb-2 transition-all duration-200 ease-in-out mt-4\"\n >\n <!-- Virtual Field Header (Always Visible) -->\n <div\n @click=\"toggleVirtualField(path.name)\"\n class=\"p-3 bg-slate-100 hover:bg-muted cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n >\n <div class=\"flex items-center\">\n <svg\n :class=\"isVirtualFieldCollapsed(path.name) ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">\n <span\n v-for=\"(part, index) in getSearchMatchParts(path.name)\"\n :key=\"index\"\n :class=\"{ 'font-bold': part.matched }\">{{ part.text }}</span>\n </span>\n <span v-if=\"path.isVirtual\" class=\"ml-2 text-sm text-purple-600\">(virtual - {{getVirtualFieldType(path)}})</span>\n <span v-else class=\"ml-2 text-sm text-blue-600\">(user-added - {{getVirtualFieldType(path)}})</span>\n </div>\n </div>\n\n <!-- Virtual Field Content (Collapsible) -->\n <div v-if=\"!isVirtualFieldCollapsed(path.name)\" class=\"p-3\">\n <div v-if=\"path.value == null\" class=\"text-sky-800\">\n {{'' + path.value}}\n </div>\n <div v-else>\n {{path.value}}\n </div>\n </div>\n </div>\n\n <!-- Unmatched Schema Paths (shown after matched items when searching, or all when not searching) -->\n <div\n v-for=\"path in unmatchedSchemaPaths\"\n :key=\"path.path || path\"\n class=\"value\"\n >\n <document-property\n :path=\"path\"\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :highlight=\"false\"\n :searchQuery=\"searchQuery\"\n :invalid=\"invalid\"></document-property>\n </div>\n\n <!-- Unmatched Virtual Fields (shown last) -->\n <div\n v-for=\"path in unmatchedVirtuals\"\n :key=\"path.name\"\n class=\"border border-edge rounded-lg mb-2 transition-all duration-200 ease-in-out\"\n >\n <!-- Virtual Field Header (Always Visible) -->\n <div\n @click=\"toggleVirtualField(path.name)\"\n class=\"p-3 bg-slate-100 hover:bg-muted cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n >\n <div class=\"flex items-center\">\n <svg\n :class=\"isVirtualFieldCollapsed(path.name) ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">{{path.name}}</span>\n <span v-if=\"path.isVirtual\" class=\"ml-2 text-sm text-purple-600\">(virtual - {{getVirtualFieldType(path)}})</span>\n <span v-else class=\"ml-2 text-sm text-blue-600\">(user-added - {{getVirtualFieldType(path)}})</span>\n </div>\n </div>\n\n <!-- Virtual Field Content (Collapsible) -->\n <div v-if=\"!isVirtualFieldCollapsed(path.name)\" class=\"p-3\">\n <div v-if=\"path.value == null\" class=\"text-sky-800\">\n {{'' + path.value}}\n </div>\n <div v-else>\n {{path.value}}\n </div>\n </div>\n </div>\n\n <!-- No Results Message -->\n <div v-if=\"searchQuery.trim() && !hasSearchMatches\" class=\"text-center py-8 text-content-tertiary\">\n <svg class=\"w-12 h-12 mx-auto mb-4 text-gray-300\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709M15 6.291A7.962 7.962 0 0012 5c-2.34 0-4.29 1.009-5.824 2.709\"></path>\n </svg>\n <p>No fields found matching \"{{searchQuery}}\"</p>\n </div>\n </div>\n\n <!-- JSON View -->\n <div v-if=\"viewMode === 'json'\" class=\"json-view\">\n <div class=\"border border-edge-strong rounded-lg bg-page p-4 overflow-auto\">\n <pre class=\"text-sm font-mono text-gray-800 whitespace-pre\">{{formattedJson}}</pre>\n </div>\n </div>\n\n <!-- Add Field Modal -->\n <modal v-if=\"showAddFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"closeAddFieldModal\">&times;</div>\n <div class=\"add-field-modal\">\n <div class=\"mb-6\">\n <h2 class=\"text-xl font-semibold text-content mb-2\">Add New Field</h2>\n <p class=\"text-sm text-gray-600\">Create a new field for this document</p>\n </div>\n\n <form @submit.prevent=\"handleAddFieldSubmit\" class=\"space-y-4\">\n <!-- Field Name -->\n <div>\n <label for=\"fieldName\" class=\"block text-sm font-medium text-content-secondary mb-1\">\n Field Name *\n </label>\n <input\n id=\"fieldName\"\n v-model=\"fieldData.name\"\n type=\"text\"\n required\n placeholder=\"Enter field name...\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.name }\"\n />\n <p v-if=\"fieldErrors.name\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.name}}</p>\n <p v-if=\"fieldData.name && getTransformedFieldName() !== fieldData.name.trim()\" class=\"mt-1 text-sm text-blue-600\">\n Field name will be: <code class=\"bg-blue-50 px-1 py-0.5 rounded text-xs\">{{getTransformedFieldName()}}</code>\n </p>\n </div>\n\n <!-- Field Type -->\n <div>\n <label for=\"fieldType\" class=\"block text-sm font-medium text-content-secondary mb-1\">\n Field Type *\n </label>\n <select\n id=\"fieldType\"\n v-model=\"fieldData.type\"\n required\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.type }\"\n >\n <option value=\"\">Select field type...</option>\n <option v-for=\"type in allFieldTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <p v-if=\"fieldErrors.type\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.type}}</p>\n </div>\n\n <!-- Field Value -->\n <div>\n <label for=\"fieldValue\" class=\"block text-sm font-medium text-content-secondary mb-1\">\n Initial Value\n </label>\n\n <!-- Date picker for Date type -->\n <input\n v-if=\"shouldUseDatePicker\"\n v-model=\"fieldData.value\"\n type=\"date\"\n id=\"fieldValue\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <!-- Simple input for basic types -->\n <input\n v-else-if=\"!shouldUseAce\"\n v-model=\"fieldData.value\"\n type=\"text\"\n id=\"fieldValue\"\n placeholder=\"Enter initial value (optional)...\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <!-- Ace editor for complex types (Array/Object/Embedded) -->\n <ace-editor\n v-else\n id=\"fieldValue\"\n v-model=\"fieldData.value\"\n mode=\"json\"\n :line-numbers=\"true\"\n :min-lines=\"3\"\n class=\"add-field-ace-editor w-full min-h-[100px]\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <p v-if=\"fieldErrors.value\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.value}}</p>\n <p class=\"mt-1 text-xs text-content-tertiary\">\n <span v-if=\"shouldUseDatePicker\">Select a date or leave empty for null/undefined values.</span>\n <span v-else-if=\"shouldUseAce\">Leave empty for null/undefined values. Use valid JSON format.</span>\n <span v-else>Leave empty for null/undefined values.</span>\n </p>\n </div>\n\n\n <!-- Action Buttons -->\n <div class=\"flex justify-end gap-3 pt-4 border-t border-edge\">\n <button\n type=\"button\"\n @click=\"closeAddFieldModal\"\n class=\"px-4 py-2 text-sm font-medium text-content-secondary bg-surface border border-edge-strong rounded-md hover:bg-page focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n :disabled=\"isSubmittingField\"\n class=\"px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <span v-if=\"isSubmittingField\">Adding...</span>\n <span v-else>Add Field</span>\n </button>\n </div>\n </form>\n </div>\n </template>\n </modal>\n</div>\n";
53814
+ module.exports = "<div class=\"document-details\">\n <!-- View Toggle and Search/Filter Bar -->\n <div class=\"sticky top-[60px] z-40 bg-surface rounded-md p-4 border-b border-edge shadow-sm\">\n\n <!-- Search and Filter Bar (only show in fields view) -->\n <div v-if=\"viewMode === 'fields'\" class=\"flex md:gap-3\">\n <!-- Search Bar -->\n <div class=\"relative flex-1\">\n <input\n ref=\"searchInput\"\n v-model=\"searchQuery\"\n type=\"text\"\n placeholder=\"Search fields...\"\n class=\"w-full px-4 py-2 pl-10 pr-4 text-sm border border-edge-strong rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n />\n <div class=\"absolute inset-y-0 left-0 flex items-center pl-3\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"></path>\n </svg>\n </div>\n </div>\n\n <!-- Data Type Filter -->\n <div class=\"relative hidden md:block\">\n <select\n v-model=\"selectedType\"\n class=\"px-4 py-2 pr-8 text-sm border border-edge-strong rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-surface min-w-[140px] appearance-none\"\n >\n <option value=\"\">All Types</option>\n <option v-for=\"type in availableTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <div class=\"absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Fields View -->\n <div v-if=\"viewMode === 'fields'\">\n <!-- Matched Schema Paths (shown first when searching) -->\n <div\n v-for=\"path in matchedSchemaPaths\"\n :key=\"path.path || path\"\n class=\"value\"\n >\n <document-property\n :path=\"path\"\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :highlight=\"true\"\n :searchQuery=\"searchQuery\"\n :invalid=\"invalid\"></document-property>\n </div>\n\n <!-- Matched Virtual Fields (shown after matched schema paths when searching) -->\n <div\n v-for=\"path in matchedVirtuals\"\n :key=\"path.name\"\n class=\"border border-edge rounded-lg mb-2 transition-all duration-200 ease-in-out mt-4\"\n >\n <!-- Virtual Field Header (Always Visible) -->\n <div\n @click=\"toggleVirtualField(path.name)\"\n class=\"p-3 bg-slate-100 hover:bg-muted cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n >\n <div class=\"flex items-center\">\n <svg\n :class=\"isVirtualFieldCollapsed(path.name) ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">\n <span\n v-for=\"(part, index) in getSearchMatchParts(path.name)\"\n :key=\"index\"\n :class=\"{ 'font-bold': part.matched }\">{{ part.text }}</span>\n </span>\n <span v-if=\"path.isVirtual\" class=\"ml-2 text-sm text-purple-600\">(virtual - {{getVirtualFieldType(path)}})</span>\n <span v-else class=\"ml-2 text-sm text-blue-600\">(user-added - {{getVirtualFieldType(path)}})</span>\n </div>\n </div>\n\n <!-- Virtual Field Content (Collapsible) -->\n <div v-if=\"!isVirtualFieldCollapsed(path.name)\" class=\"p-3\">\n <div v-if=\"path.value == null\" class=\"text-sky-800\">\n {{'' + path.value}}\n </div>\n <div v-else>\n {{path.value}}\n </div>\n </div>\n </div>\n\n <!-- Unmatched Schema Paths (shown after matched items when searching, or all when not searching) -->\n <div\n v-for=\"path in unmatchedSchemaPaths\"\n :key=\"path.path || path\"\n class=\"value\"\n >\n <document-property\n :path=\"path\"\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :highlight=\"false\"\n :searchQuery=\"searchQuery\"\n :invalid=\"invalid\"></document-property>\n </div>\n\n <!-- Unmatched Virtual Fields (shown last) -->\n <div\n v-for=\"path in unmatchedVirtuals\"\n :key=\"path.name\"\n class=\"border border-edge rounded-lg mb-2 transition-all duration-200 ease-in-out\"\n >\n <!-- Virtual Field Header (Always Visible) -->\n <div\n @click=\"toggleVirtualField(path.name)\"\n class=\"p-3 bg-slate-100 hover:bg-muted cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n >\n <div class=\"flex items-center\">\n <svg\n :class=\"isVirtualFieldCollapsed(path.name) ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">{{path.name}}</span>\n <span v-if=\"path.isVirtual\" class=\"ml-2 text-sm text-purple-600\">(virtual - {{getVirtualFieldType(path)}})</span>\n <span v-else class=\"ml-2 text-sm text-blue-600\">(user-added - {{getVirtualFieldType(path)}})</span>\n </div>\n </div>\n\n <!-- Virtual Field Content (Collapsible) -->\n <div v-if=\"!isVirtualFieldCollapsed(path.name)\" class=\"p-3\">\n <div v-if=\"path.value == null\" class=\"text-sky-800\">\n {{'' + path.value}}\n </div>\n <div v-else>\n {{path.value}}\n </div>\n </div>\n </div>\n\n <!-- No Results Message -->\n <div v-if=\"searchQuery.trim() && !hasSearchMatches\" class=\"text-center py-8 text-content-tertiary\">\n <svg class=\"w-12 h-12 mx-auto mb-4 text-gray-300\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709M15 6.291A7.962 7.962 0 0012 5c-2.34 0-4.29 1.009-5.824 2.709\"></path>\n </svg>\n <p>No fields found matching \"{{searchQuery}}\"</p>\n </div>\n </div>\n\n <!-- JSON View -->\n <div v-if=\"viewMode === 'json'\" class=\"json-view\">\n <div class=\"border border-edge-strong rounded-lg bg-page p-4 overflow-auto\">\n <pre class=\"text-sm font-mono text-gray-800 whitespace-pre\">{{formattedJson}}</pre>\n </div>\n </div>\n</div>\n";
53065
53815
 
53066
53816
  /***/ },
53067
53817
 
@@ -53083,7 +53833,7 @@ module.exports = ".document-details {\n width: 100%;\n }\n \n .document-de
53083
53833
  (module) {
53084
53834
 
53085
53835
  "use strict";
53086
- module.exports = "<div class=\"border border-edge bg-surface rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-edge bg-slate-100 hover:bg-muted transition-colors duration-200 ease-in-out\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">\n <span\n v-for=\"(part, index) in pathNameParts\"\n :key=\"index\"\n :class=\"{ 'font-bold': part.matched }\">{{ part.text }}</span>\n </span>\n <span class=\"ml-2 text-sm text-content-tertiary\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"path.instance === 'Array' && canUseArrayTableView\" class=\"ml-3 inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"arrayDetailViewMode === 'list' ? 'bg-merlot-700 text-white shadow' : 'text-content-secondary hover:text-content'\"\n @click.stop=\"setArrayDetailViewMode('list')\">\n List\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"arrayDetailViewMode === 'table' ? 'bg-merlot-700 text-white shadow' : 'text-content-secondary hover:text-content'\"\n @click.stop=\"setArrayDetailViewMode('table')\">\n Table\n </button>\n </div>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <date-view-mode-picker\n v-if=\"isDatePath\"\n :viewMode=\"dateViewMode\"\n :path=\"path\"\n @update:viewMode=\"dateViewMode = $event\"\n ></date-view-mode-picker>\n <objectid-view-mode-picker\n v-if=\"isObjectIdPath\"\n :viewMode=\"objectIdViewMode\"\n @update:viewMode=\"objectIdViewMode = $event\"\n ></objectid-view-mode-picker>\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-primary hover:bg-primary-hover text-primary-text px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :viewMode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Arrays own their truncation and array search behavior in detail-array. -->\n <component\n v-if=\"isArray\"\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"propertyDetailViewMode\"\n :truncate=\"needsTruncation\"\n @updated=\"renderedValue = $event\"></component>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-content-secondary whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"propertyDetailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"propertyDetailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n </div>\n </div>\n </div>\n</div>\n";
53836
+ module.exports = "<div class=\"border border-edge bg-surface rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-edge bg-slate-100 hover:bg-muted transition-colors duration-200 ease-in-out\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">\n <span\n v-for=\"(part, index) in pathNameParts\"\n :key=\"index\"\n :class=\"{ 'font-bold': part.matched }\">{{ part.text }}</span>\n </span>\n <span class=\"ml-2 text-sm text-content-tertiary\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"path.instance === 'Array' && canUseArrayTableView\" class=\"ml-3 inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"arrayDetailViewMode === 'list' ? 'bg-merlot-700 text-white shadow' : 'text-content-secondary hover:text-content'\"\n @click.stop=\"setArrayDetailViewMode('list')\">\n List\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"arrayDetailViewMode === 'table' ? 'bg-merlot-700 text-white shadow' : 'text-content-secondary hover:text-content'\"\n @click.stop=\"setArrayDetailViewMode('table')\">\n Table\n </button>\n </div>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <date-view-mode-picker\n v-if=\"isDatePath\"\n :viewMode=\"dateViewMode\"\n :path=\"path\"\n :editting=\"editting\"\n @update:viewMode=\"dateViewMode = $event\"\n ></date-view-mode-picker>\n <objectid-view-mode-picker\n v-if=\"isObjectIdPath\"\n :viewMode=\"objectIdViewMode\"\n @update:viewMode=\"objectIdViewMode = $event\"\n ></objectid-view-mode-picker>\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-primary hover:bg-primary-hover text-primary-text px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-merlot-700' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-merlot-700' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :viewMode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Arrays own their truncation and array search behavior in detail-array. -->\n <component\n v-if=\"isArray\"\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"propertyDetailViewMode\"\n :truncate=\"needsTruncation\"\n @updated=\"renderedValue = $event\"></component>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-content-secondary whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"propertyDetailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"propertyDetailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n </div>\n </div>\n </div>\n</div>\n";
53087
53837
 
53088
53838
  /***/ },
53089
53839
 
@@ -53127,7 +53877,7 @@ module.exports = "<div>\n <h2>\n Are you sure you want to delete the follo
53127
53877
  (module) {
53128
53878
 
53129
53879
  "use strict";
53130
- module.exports = ".document .document-menu {\n display: flex;\n position: sticky;\n top: 0;\n z-index: 100;\n background-color: var(--studio-bg-surface);\n border-radius: 5px;\n padding: 15px 15px;\n margin: -15px 0 15px 0;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.document .document-menu .left {\n flex-grow: 1;\n}\n\n.document .document-menu .right {\n flex-grow: 1;\n text-align: right;\n}\n\n.document .document-menu .right button:not(:last-child) {\n margin-right: 0.5em;\n}\n\n.document button img {\n height: 1em;\n}\n";
53880
+ module.exports = ".document .document-menu {\n display: flex;\n position: sticky;\n top: 0;\n z-index: 100;\n background-color: var(--studio-bg-surface);\n border-radius: 5px;\n padding: 15px 15px;\n margin: -15px 0 15px 0;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.document .document-menu .left {\n flex-grow: 1;\n}\n\n.document .document-menu .right {\n flex-grow: 1;\n text-align: right;\n}\n\n.document .document-menu .right button:not(:last-child) {\n margin-right: 0.5em;\n}\n\n.document button img {\n height: 1em;\n}\n\n/* Add Field Modal Styles */\n.add-field-modal {\n max-width: 500px;\n width: 100%;\n}\n\n.add-field-modal .modal-exit {\n position: absolute;\n top: 15px;\n right: 20px;\n font-size: 24px;\n cursor: pointer;\n color: #6b7280;\n z-index: 10;\n}\n\n.add-field-modal .modal-exit:hover {\n color: #374151;\n}\n\n.add-field-modal form {\n max-height: 70vh;\n overflow-y: auto;\n}\n\n.add-field-modal input[type=\"text\"],\n.add-field-modal input[type=\"email\"],\n.add-field-modal input[type=\"password\"],\n.add-field-modal select,\n.add-field-modal textarea {\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.add-field-modal input:focus,\n.add-field-modal select:focus,\n.add-field-modal textarea:focus {\n border-color: #3b82f6;\n box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n}\n\n.add-field-modal .border-red-500 {\n border-color: #ef4444 !important;\n}\n\n.add-field-modal .border-red-500:focus {\n box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;\n}\n\n/* Ace editor styling in modal */\n.add-field-modal .add-field-ace-editor.ace_editor {\n border: 1px solid #d1d5db;\n border-radius: 0.375rem;\n font-size: 14px;\n min-height: 100px;\n}\n\n.add-field-modal .add-field-ace-editor:focus-within.ace_editor,\n.add-field-modal .add-field-ace-editor.ace_focus {\n border-color: #3b82f6;\n box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);\n}\n";
53131
53881
 
53132
53882
  /***/ },
53133
53883
 
@@ -53138,7 +53888,7 @@ module.exports = ".document .document-menu {\n display: flex;\n position: stic
53138
53888
  (module) {
53139
53889
 
53140
53890
  "use strict";
53141
- module.exports = "<div class=\"document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full\">\n <div class=\"max-w-7xl mx-auto\">\n <div class=\"flex gap-4 items-center sticky top-0 z-50 bg-surface p-4 border-b border-edge shadow-sm\">\n <div class=\"font-bold overflow-hidden text-ellipsis\">{{model}}: {{documentId}}</div>\n <div class=\"flex grow\">\n <button\n @click=\"viewMode = 'fields'\"\n :class=\"viewMode === 'fields'\n ? 'bg-primary text-white z-10'\n : 'bg-gray-200 text-content-secondary hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-edge-strong border-r-0 rounded-l-lg rounded-r-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 10h16M4 14h16M4 18h16\"></path>\n </svg>\n Fields\n </button>\n <button\n @click=\"viewMode = 'json'\"\n :class=\"viewMode === 'json'\n ? 'bg-primary text-white z-10'\n : 'bg-gray-200 text-content-secondary hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-edge-strong rounded-r-lg rounded-l-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"></path>\n </svg>\n JSON\n </button>\n </div>\n\n <div class=\"hidden md:flex items-center gap-3 text-sm text-slate-600\">\n <div class=\"text-right leading-tight\">\n <div class=\"text-xs uppercase tracking-wide text-slate-400 flex items-center gap-2 justify-end\">\n <span>Loaded at</span>\n <span class=\"inline-flex items-center gap-1 text-[10px] font-semibold\" :class=\"autoRefreshEnabled ? 'text-forest-green-600' : 'text-slate-400'\">\n <span class=\"inline-block h-1.5 w-1.5 rounded-full\" :class=\"autoRefreshEnabled ? 'bg-forest-green-500' : 'bg-slate-300'\"></span>\n </span>\n </div>\n <div class=\"font-medium text-slate-700\">{{lastUpdatedLabel}}</div>\n </div>\n </div>\n\n <div class=\"gap-2 hidden md:flex items-center\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canEdit\"\n :class=\"{'cursor-not-allowed opacity-50': !canEdit}\"\n type=\"button\"\n class=\"rounded-md bg-primary px-2.5 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n &times; Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n\n <!-- 3-dot menu -->\n <div class=\"relative\">\n <button\n @click=\"desktopMenuOpen = !desktopMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-medium text-content-secondary hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"desktopMenuOpen\"\n aria-label=\"More options\"\n >\n <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"5\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"19\" r=\"2\"></circle>\n </svg>\n </button>\n <div\n v-show=\"desktopMenuOpen\"\n @click.away=\"desktopMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-surface ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n @click=\"refreshDocument({ force: true, source: 'manual' }); desktopMenuOpen = false\"\n :disabled=\"isRefreshing\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', isRefreshing ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-100']\"\n type=\"button\"\n >\n {{isRefreshing ? 'Refreshing...' : 'Refresh'}}\n </button>\n <button\n @click=\"toggleAutoRefresh(); desktopMenuOpen = false\"\n :disabled=\"isLambda\"\n type=\"button\"\n :class=\"['flex items-center px-4 py-2 text-sm', isLambda ? 'text-gray-400 cursor-not-allowed' : 'text-content-secondary hover:bg-slate-100']\"\n :title=\"isLambda ? 'Auto-refresh only available on Express deployments' : ''\"\n >\n {{autoRefreshEnabled ? 'Disable Auto-Refresh' : 'Enable Auto-Refresh'}}\n </button>\n <button\n @click=\"addField(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"copyDocument(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-blue-100\"\n >\n Copy Document\n </button>\n <button\n @click=\"openValidationModal()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Validate\n </button>\n <button\n @click=\"openScriptDrawer()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-indigo-100\"\n >\n Run Script\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"md:hidden flex items-center\">\n <div class=\"relative\">\n <button\n @click=\"mobileMenuOpen = !mobileMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-content-secondary hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"mobileMenuOpen\"\n aria-label=\"Open menu\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M4 6h16M4 12h16M4 18h16\"></path>\n </svg>\n </button>\n <div\n v-show=\"mobileMenuOpen\"\n @click.away=\"mobileMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-52 rounded-md shadow-lg bg-surface ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true; mobileMenuOpen = false\"\n :disabled=\"!canEdit\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canEdit ? 'cursor-not-allowed opacity-50' : 'hover:bg-primary-subtle']\"\n type=\"button\"\n >\n Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false; mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-slate-100\"\n >\n Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-green-100']\"\n @click=\"shouldShowConfirmModal=true; mobileMenuOpen = false\"\n type=\"button\"\n >\n Save\n </button>\n <button\n @click=\"refreshDocument({ force: true, source: 'manual' }); mobileMenuOpen = false\"\n :disabled=\"isRefreshing\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', isRefreshing ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-100']\"\n type=\"button\"\n >\n {{isRefreshing ? 'Refreshing...' : 'Refresh'}}\n </button>\n <button\n @click=\"toggleAutoRefresh(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-slate-100\"\n >\n Auto-Refresh {{autoRefreshEnabled ? 'ON' : 'OFF'}}\n </button>\n <button\n @click=\"addField(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"copyDocument(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-blue-100\"\n >\n Copy Document\n </button>\n <button\n @click=\"openValidationModal()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Validate\n </button>\n <button\n @click=\"openScriptDrawer()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-indigo-100\"\n >\n Run Script\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :virtualPaths=\"virtualPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"\n :viewMode=\"viewMode\"\n :model=\"model\"\n @add-field=\"addField\"\n @view-mode-change=\"updateViewMode\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">&times;</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteModal = false;\">&times;</div>\n <confirm-delete @close=\"shouldShowDeleteModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n <modal v-if=\"shouldShowCloneModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCloneModal = false;\">&times;</div>\n <clone-document :currentModel=\"model\" :doc=\"document\" :schemaPaths=\"schemaPaths\" @close=\"showClonedDocument\"></clone-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowValidationModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowValidationModal = false;\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Validate Document</div>\n <div v-if=\"!validationResult\" class=\"space-y-3\">\n <div class=\"h-4 w-32 animate-pulse rounded bg-muted\"></div>\n <div class=\"h-20 w-full animate-pulse rounded bg-muted\"></div>\n </div>\n <div v-else class=\"space-y-3\">\n <div\n class=\"rounded px-3 py-2 text-sm font-semibold\"\n :class=\"validationResult.valid ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'\"\n >\n {{ validationResult.valid ? 'Document is valid' : 'Document is invalid' }}\n </div>\n <div v-if=\"!validationResult.valid\" class=\"rounded border border-edge bg-muted p-3 text-sm text-content-secondary\">\n {{ validationResult.error }}\n </div>\n <div v-if=\"validationResult?.errors && Object.keys(validationResult.errors).length > 0\" class=\"rounded border border-edge\">\n <div class=\"border-b border-edge px-3 py-2 text-sm font-semibold text-content-secondary\">Invalid Paths</div>\n <div class=\"max-h-72 overflow-auto\">\n <div\n v-for=\"[path, pathError] in Object.entries(validationResult.errors)\"\n :key=\"path\"\n class=\"grid gap-2 border-b border-edge px-3 py-2 text-sm last:border-b-0 md:grid-cols-[minmax(160px,220px)_1fr]\"\n >\n <div class=\"font-mono text-content\">{{ path }}</div>\n <div class=\"text-content-secondary\">\n <div>{{ pathError.message }}</div>\n <div v-if=\"pathError.kind\" class=\"mt-1 text-xs text-content-tertiary\">Kind: {{ pathError.kind }}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </modal>\n </div>\n <execute-script\n :visible=\"scriptDrawerOpen\"\n :model=\"model\"\n :document-id=\"documentId\"\n :editting=\"editting\"\n @close=\"closeScriptDrawer\"\n @refresh=\"handleScriptRefresh\"\n ></execute-script>\n <div\n v-if=\"keyboardShortcuts.length > 0\"\n class=\"fixed bottom-4 left-4 z-40 group\"\n >\n <button\n type=\"button\"\n aria-label=\"Keyboard shortcuts\"\n title=\"Keyboard shortcuts\"\n class=\"rounded-full border border-slate-300 bg-surface p-2 text-slate-700 shadow-sm transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"2.5\" y=\"6.5\" width=\"19\" height=\"11\" rx=\"2\" stroke-width=\"1.5\"></rect>\n <path d=\"M6 10.5h1.5M9 10.5h1.5M12 10.5h1.5M15 10.5h1.5M18 10.5h.01M6 13.5h1.5M9 13.5h6M16.5 13.5h1.5\" stroke-width=\"1.5\" stroke-linecap=\"round\"></path>\n </svg>\n </button>\n <div class=\"pointer-events-none absolute left-0 bottom-full mb-2 min-w-[220px] translate-y-1 rounded-lg bg-slate-900 px-3 py-2.5 text-slate-200 opacity-0 shadow-xl transition-all duration-150 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-y-0 group-focus-within:opacity-100\">\n <div\n v-for=\"shortcut in keyboardShortcuts\"\n :key=\"shortcut.command + shortcut.description\"\n class=\"flex items-center justify-between gap-3 text-xs\"\n >\n <span class=\"font-mono font-semibold text-slate-50\">{{shortcut.command}}</span>\n <span class=\"text-slate-300\">{{shortcut.description}}</span>\n </div>\n </div>\n </div>\n </div>\n</div>\n";
53891
+ module.exports = "<div class=\"document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full\">\n <div class=\"max-w-7xl mx-auto\">\n <div class=\"flex gap-4 items-center sticky top-0 z-50 bg-surface p-4 border-b border-edge shadow-sm\">\n <div class=\"font-bold overflow-hidden text-ellipsis\">{{model}}: {{documentId}}</div>\n <div class=\"flex grow\">\n <button\n @click=\"viewMode = 'fields'\"\n :class=\"viewMode === 'fields'\n ? 'bg-primary text-white z-10'\n : 'bg-gray-200 text-content-secondary hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-edge-strong border-r-0 rounded-l-lg rounded-r-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 10h16M4 14h16M4 18h16\"></path>\n </svg>\n Fields\n </button>\n <button\n @click=\"viewMode = 'json'\"\n :class=\"viewMode === 'json'\n ? 'bg-primary text-white z-10'\n : 'bg-gray-200 text-content-secondary hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-edge-strong rounded-r-lg rounded-l-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"></path>\n </svg>\n JSON\n </button>\n </div>\n\n <div class=\"hidden md:flex items-center gap-3 text-sm text-slate-600\">\n <div class=\"text-right leading-tight\">\n <div class=\"text-xs uppercase tracking-wide text-slate-400 flex items-center gap-2 justify-end\">\n <span>Loaded at</span>\n <span class=\"inline-flex items-center gap-1 text-[10px] font-semibold\" :class=\"autoRefreshEnabled ? 'text-forest-green-600' : 'text-slate-400'\">\n <span class=\"inline-block h-1.5 w-1.5 rounded-full\" :class=\"autoRefreshEnabled ? 'bg-forest-green-500' : 'bg-slate-300'\"></span>\n </span>\n </div>\n <div class=\"font-medium text-slate-700\">{{lastUpdatedLabel}}</div>\n </div>\n </div>\n\n <div class=\"gap-2 hidden md:flex items-center\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canEdit\"\n :class=\"{'cursor-not-allowed opacity-50': !canEdit}\"\n type=\"button\"\n class=\"rounded-md bg-primary px-2.5 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n &times; Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n\n <!-- 3-dot menu -->\n <div class=\"relative\">\n <button\n @click=\"desktopMenuOpen = !desktopMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-medium text-content-secondary hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"desktopMenuOpen\"\n aria-label=\"More options\"\n >\n <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"5\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"19\" r=\"2\"></circle>\n </svg>\n </button>\n <div\n v-show=\"desktopMenuOpen\"\n @click.away=\"desktopMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-surface ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n @click=\"refreshDocument({ force: true, source: 'manual' }); desktopMenuOpen = false\"\n :disabled=\"isRefreshing\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', isRefreshing ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-100']\"\n type=\"button\"\n >\n {{isRefreshing ? 'Refreshing...' : 'Refresh'}}\n </button>\n <button\n @click=\"toggleAutoRefresh(); desktopMenuOpen = false\"\n :disabled=\"isLambda\"\n type=\"button\"\n :class=\"['flex items-center px-4 py-2 text-sm', isLambda ? 'text-gray-400 cursor-not-allowed' : 'text-content-secondary hover:bg-slate-100']\"\n :title=\"isLambda ? 'Auto-refresh only available on Express deployments' : ''\"\n >\n {{autoRefreshEnabled ? 'Disable Auto-Refresh' : 'Enable Auto-Refresh'}}\n </button>\n <button\n @click=\"addField(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"copyDocument(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-blue-100\"\n >\n Copy Document\n </button>\n <button\n @click=\"openValidationModal()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Validate\n </button>\n <button\n @click=\"openScriptDrawer()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-indigo-100\"\n >\n Run Script\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"md:hidden flex items-center\">\n <div class=\"relative\">\n <button\n @click=\"mobileMenuOpen = !mobileMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-content-secondary hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"mobileMenuOpen\"\n aria-label=\"Open menu\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M4 6h16M4 12h16M4 18h16\"></path>\n </svg>\n </button>\n <div\n v-show=\"mobileMenuOpen\"\n @click.away=\"mobileMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-52 rounded-md shadow-lg bg-surface ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true; mobileMenuOpen = false\"\n :disabled=\"!canEdit\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canEdit ? 'cursor-not-allowed opacity-50' : 'hover:bg-primary-subtle']\"\n type=\"button\"\n >\n Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false; mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-slate-100\"\n >\n Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-green-100']\"\n @click=\"shouldShowConfirmModal=true; mobileMenuOpen = false\"\n type=\"button\"\n >\n Save\n </button>\n <button\n @click=\"refreshDocument({ force: true, source: 'manual' }); mobileMenuOpen = false\"\n :disabled=\"isRefreshing\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', isRefreshing ? 'cursor-not-allowed opacity-50' : 'hover:bg-slate-100']\"\n type=\"button\"\n >\n {{isRefreshing ? 'Refreshing...' : 'Refresh'}}\n </button>\n <button\n @click=\"toggleAutoRefresh(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-slate-100\"\n >\n Auto-Refresh {{autoRefreshEnabled ? 'ON' : 'OFF'}}\n </button>\n <button\n @click=\"addField(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"copyDocument(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-blue-100\"\n >\n Copy Document\n </button>\n <button\n @click=\"openValidationModal()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100\"\n >\n Validate\n </button>\n <button\n @click=\"openScriptDrawer()\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-indigo-100\"\n >\n Run Script\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-content-secondary', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :virtualPaths=\"virtualPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"\n :viewMode=\"viewMode\"\n @view-mode-change=\"updateViewMode\"></document-details>\n <modal v-if=\"showAddFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"closeAddFieldModal\">&times;</div>\n <div class=\"add-field-modal\">\n <div class=\"mb-6\">\n <h2 class=\"text-xl font-semibold text-content mb-2\">Add New Field</h2>\n <p class=\"text-sm text-gray-600\">Create a new field for this document</p>\n </div>\n\n <form @submit.prevent=\"handleAddFieldSubmit\" class=\"space-y-4\">\n <div>\n <label for=\"fieldName\" class=\"block text-sm font-medium text-content-secondary mb-1\">\n Field Name *\n </label>\n <input\n id=\"fieldName\"\n v-model=\"fieldData.name\"\n type=\"text\"\n required\n placeholder=\"Enter field name...\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.name }\"\n />\n <p v-if=\"fieldErrors.name\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.name}}</p>\n <p v-if=\"fieldData.name && getTransformedFieldName() !== fieldData.name.trim()\" class=\"mt-1 text-sm text-blue-600\">\n Field name will be: <code class=\"bg-blue-50 px-1 py-0.5 rounded text-xs\">{{getTransformedFieldName()}}</code>\n </p>\n </div>\n\n <div>\n <label for=\"fieldType\" class=\"block text-sm font-medium text-content-secondary mb-1\">\n Field Type *\n </label>\n <select\n id=\"fieldType\"\n v-model=\"fieldData.type\"\n required\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.type }\"\n >\n <option value=\"\">Select field type...</option>\n <option v-for=\"type in allFieldTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <p v-if=\"fieldErrors.type\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.type}}</p>\n </div>\n\n <div>\n <label for=\"fieldValue\" class=\"block text-sm font-medium text-content-secondary mb-1\">\n Initial Value\n </label>\n\n <custom-date-picker\n v-if=\"shouldUseDatePicker\"\n id=\"fieldValue\"\n mode=\"date\"\n :value=\"fieldData.value\"\n :class=\"{ 'border-red-500': fieldErrors.value, 'max-w-[400px]': true }\"\n :horizontal=\"true\"\n @input=\"fieldData.value = $event\"\n />\n\n <input\n v-else-if=\"!shouldUseAce\"\n v-model=\"fieldData.value\"\n type=\"text\"\n id=\"fieldValue\"\n placeholder=\"Enter initial value (optional)...\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <ace-editor\n v-else\n id=\"fieldValue\"\n v-model=\"fieldData.value\"\n mode=\"json\"\n :line-numbers=\"true\"\n :min-lines=\"3\"\n class=\"add-field-ace-editor w-full min-h-[100px]\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <p v-if=\"fieldErrors.value\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.value}}</p>\n <p class=\"mt-1 text-xs text-content-tertiary\">\n <span v-if=\"shouldUseDatePicker\">Select a date or leave empty for null/undefined values.</span>\n <span v-else-if=\"shouldUseAce\">Leave empty for null/undefined values. Use valid JSON format.</span>\n <span v-else>Leave empty for null/undefined values.</span>\n </p>\n </div>\n\n <div class=\"flex justify-end gap-3 pt-4 border-t border-edge\">\n <button\n type=\"button\"\n @click=\"closeAddFieldModal\"\n class=\"px-4 py-2 text-sm font-medium text-content-secondary bg-surface border border-edge-strong rounded-md hover:bg-page focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n :disabled=\"isSubmittingField\"\n class=\"px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <span v-if=\"isSubmittingField\">Adding...</span>\n <span v-else>Add Field</span>\n </button>\n </div>\n </form>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">&times;</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteModal = false;\">&times;</div>\n <confirm-delete @close=\"shouldShowDeleteModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n <modal v-if=\"shouldShowCloneModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCloneModal = false;\">&times;</div>\n <clone-document :currentModel=\"model\" :doc=\"document\" :schemaPaths=\"schemaPaths\" @close=\"showClonedDocument\"></clone-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowValidationModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowValidationModal = false;\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Validate Document</div>\n <div v-if=\"!validationResult\" class=\"space-y-3\">\n <div class=\"h-4 w-32 animate-pulse rounded bg-muted\"></div>\n <div class=\"h-20 w-full animate-pulse rounded bg-muted\"></div>\n </div>\n <div v-else class=\"space-y-3\">\n <div\n class=\"rounded px-3 py-2 text-sm font-semibold\"\n :class=\"validationResult.valid ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'\"\n >\n {{ validationResult.valid ? 'Document is valid' : 'Document is invalid' }}\n </div>\n <div v-if=\"!validationResult.valid\" class=\"rounded border border-edge bg-muted p-3 text-sm text-content-secondary\">\n {{ validationResult.error }}\n </div>\n <div v-if=\"validationResult?.errors && Object.keys(validationResult.errors).length > 0\" class=\"rounded border border-edge\">\n <div class=\"border-b border-edge px-3 py-2 text-sm font-semibold text-content-secondary\">Invalid Paths</div>\n <div class=\"max-h-72 overflow-auto\">\n <div\n v-for=\"[path, pathError] in Object.entries(validationResult.errors)\"\n :key=\"path\"\n class=\"grid gap-2 border-b border-edge px-3 py-2 text-sm last:border-b-0 md:grid-cols-[minmax(160px,220px)_1fr]\"\n >\n <div class=\"font-mono text-content\">{{ path }}</div>\n <div class=\"text-content-secondary\">\n <div>{{ pathError.message }}</div>\n <div v-if=\"pathError.kind\" class=\"mt-1 text-xs text-content-tertiary\">Kind: {{ pathError.kind }}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </modal>\n </div>\n <execute-script\n :visible=\"scriptDrawerOpen\"\n :model=\"model\"\n :document-id=\"documentId\"\n :editting=\"editting\"\n @close=\"closeScriptDrawer\"\n @refresh=\"handleScriptRefresh\"\n ></execute-script>\n <div\n v-if=\"keyboardShortcuts.length > 0\"\n class=\"fixed bottom-4 left-4 z-40 group\"\n >\n <button\n type=\"button\"\n aria-label=\"Keyboard shortcuts\"\n title=\"Keyboard shortcuts\"\n class=\"rounded-full border border-slate-300 bg-surface p-2 text-slate-700 shadow-sm transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"2.5\" y=\"6.5\" width=\"19\" height=\"11\" rx=\"2\" stroke-width=\"1.5\"></rect>\n <path d=\"M6 10.5h1.5M9 10.5h1.5M12 10.5h1.5M15 10.5h1.5M18 10.5h.01M6 13.5h1.5M9 13.5h6M16.5 13.5h1.5\" stroke-width=\"1.5\" stroke-linecap=\"round\"></path>\n </svg>\n </button>\n <div class=\"pointer-events-none absolute left-0 bottom-full mb-2 min-w-[220px] translate-y-1 rounded-lg bg-slate-900 px-3 py-2.5 text-slate-200 opacity-0 shadow-xl transition-all duration-150 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-y-0 group-focus-within:opacity-100\">\n <div\n v-for=\"shortcut in keyboardShortcuts\"\n :key=\"shortcut.command + shortcut.description\"\n class=\"flex items-center justify-between gap-3 text-xs\"\n >\n <span class=\"font-mono font-semibold text-slate-50\">{{shortcut.command}}</span>\n <span class=\"text-slate-300\">{{shortcut.description}}</span>\n </div>\n </div>\n </div>\n </div>\n</div>\n";
53142
53892
 
53143
53893
  /***/ },
53144
53894
 
@@ -53193,7 +53943,7 @@ module.exports = "<div class=\"edit-boolean\">\n <div class=\"flex flex-wrap ga
53193
53943
  (module) {
53194
53944
 
53195
53945
  "use strict";
53196
- module.exports = "<div>\n <input\n v-if=\"dateSelection == 'picker'\"\n class=\"w-64 h-8 border border-edge-strong outline-0\"\n type=\"datetime-local\"\n :value=\"valueAsLocalString\"\n @input=\"$emit('input', $event.target.value)\">\n <input\n v-if=\"dateSelection == 'iso'\"\n type=\"text\"\n class=\"w-64 h-8 border border-edge-strong outline-0\"\n :value=\"valueAsISOString\"\n @input=\"updateFromISO\">\n</div>";
53946
+ module.exports = "<div>\n <custom-date-picker\n v-if=\"dateSelection == 'picker'\"\n :value=\"value\"\n mode=\"datetime\"\n :horizontal=\"true\"\n class=\"max-w-[420px]\"\n @input=\"$emit('input', $event)\"\n />\n <input\n v-if=\"dateSelection == 'iso'\"\n type=\"text\"\n class=\"w-64 h-8 border border-edge-strong outline-0\"\n :value=\"valueAsISOString\"\n @input=\"updateFromISO\">\n</div>\n";
53197
53947
 
53198
53948
  /***/ },
53199
53949
 
@@ -53435,7 +54185,7 @@ module.exports = "<div>\n <div class=\"modal-exit\" @click=\"close\">&times;</d
53435
54185
  (module) {
53436
54186
 
53437
54187
  "use strict";
53438
- module.exports = "<form @submit.prevent=\"emitSearch\" class=\"relative flex-grow m-0\">\n <input\n ref=\"searchInput\"\n class=\"w-full font-mono rounded-md p-1 border border-edge-strong outline-edge-strong text-lg focus:ring-1 focus:ring-primary-subtle focus:ring-offset-0 focus:outline-none\"\n type=\"text\"\n placeholder=\"Filter (supports JS, dates, ObjectIds, and more)\"\n v-model=\"searchText\"\n @click=\"initFilter\"\n @input=\"updateAutocomplete\"\n @keydown=\"handleKeyDown\"\n />\n <ul v-if=\"autocompleteSuggestions.length\" class=\"absolute z-[9999] bg-surface border border-edge-strong rounded mt-1 w-full max-h-40 overflow-y-auto shadow\">\n <li\n v-for=\"(suggestion, index) in autocompleteSuggestions\"\n :key=\"suggestion\"\n class=\"px-2 py-1 cursor-pointer\"\n :class=\"{ 'bg-primary-subtle': index === autocompleteIndex }\"\n @mousedown.prevent=\"applySuggestion(index)\"\n >\n {{ suggestion }}\n </li>\n </ul>\n</form>\n";
54188
+ module.exports = "<div class=\"relative flex-grow m-0\" @focusout=\"onWrapperFocusOut\">\n <form @submit.prevent=\"emitSearch\" class=\"w-full mb-0\">\n <input\n ref=\"searchInput\"\n class=\"w-full font-mono rounded-md p-1 border border-edge-strong outline-edge-strong text-lg focus:ring-1 focus:ring-primary-subtle focus:ring-offset-0 focus:outline-none\"\n type=\"text\"\n placeholder=\"Filter (supports JS, dates, ObjectIds, and more)\"\n v-model=\"searchText\"\n @click=\"initFilter($event); updateAutocomplete()\"\n @input=\"updateAutocomplete\"\n @keyup=\"onSearchKeyup\"\n @keydown=\"handleKeyDown\"\n />\n <div\n v-if=\"autocompleteSuggestions.length\"\n ref=\"autocompleteDropdown\"\n class=\"absolute z-[9999] bg-surface border border-edge-strong rounded mt-1 w-full shadow\"\n >\n <ul class=\"max-h-40 overflow-y-auto\">\n <li\n v-for=\"(suggestion, index) in autocompleteSuggestions\"\n :key=\"suggestion\"\n class=\"px-2 py-1 cursor-pointer\"\n :class=\"{ 'bg-primary-subtle': index === autocompleteIndex }\"\n @mousedown.prevent=\"applySuggestion(index)\"\n >\n {{ suggestion }}\n </li>\n </ul>\n </div>\n </form>\n\n <div\n v-if=\"datePickerContext\"\n class=\"absolute top-full left-0 z-[10000] mt-0 w-[400px] max-w-[calc(100vw-2rem)] bg-surface border border-edge-strong rounded-lg shadow-xl\"\n role=\"dialog\"\n aria-label=\"Date picker\"\n @mousedown.stop\n >\n <div class=\"flex items-center justify-between gap-2 px-3 py-2 border-b border-edge-strong\">\n <span class=\"text-sm font-medium text-content-primary\">Insert date into filter</span>\n <div class=\"flex items-center gap-2 shrink-0\">\n <select\n v-if=\"!datePickerMinimized\"\n v-model=\"dateInsertFormat\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1\"\n @mousedown.stop\n >\n <option value=\"timestamp\">Milliseconds</option>\n <option value=\"quoted\">Date string</option>\n </select>\n <button\n type=\"button\"\n class=\"flex items-center justify-center w-5 h-5 rounded hover:bg-surface-hover text-content-secondary\"\n :aria-label=\"datePickerMinimized ? 'Expand date picker' : 'Minimize date picker'\"\n @click.stop=\"datePickerMinimized = !datePickerMinimized\"\n >\n <svg v-if=\"datePickerMinimized\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-3.5 h-3.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m19.5 8.25-7.5 7.5-7.5-7.5\" />\n </svg>\n <svg v-else xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-3.5 h-3.5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4.5 15.75 7.5-7.5 7.5 7.5\" />\n </svg>\n </button>\n </div>\n </div>\n <div v-if=\"!datePickerMinimized\" class=\"p-2\">\n <custom-date-picker\n compact\n horizontal\n embedded\n :immediate=\"false\"\n mode=\"datetime\"\n :value=\"datePickerLocalValue\"\n @commit=\"applyDateFromPicker\"\n />\n </div>\n </div>\n</div>\n";
53439
54189
 
53440
54190
  /***/ },
53441
54191
 
@@ -64975,7 +65725,7 @@ var src_default = VueToastificationPlugin;
64975
65725
  (module) {
64976
65726
 
64977
65727
  "use strict";
64978
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.7","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"@nestjs/common":"10.x || 11.x","mongoose":"7.x || 8.x || ^9.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true}},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","@nestjs/common":"^11.1.9","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
65728
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.8","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"@nestjs/common":"10.x || 11.x","mongoose":"7.x || 8.x || ^9.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true}},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","@nestjs/common":"^11.1.9","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js","eval:agent":"node eval/agent/run.js"}}');
64979
65729
 
64980
65730
  /***/ }
64981
65731