@asd20/ui-next 2.10.11 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.11.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.11.0...ui-next-v2.11.1) (2026-05-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * remove search parameters from url ([b6bde4c](https://github.com/academydistrict20/asd20-ui-next/commit/b6bde4cb29ea762267ecd03f9739a47d8726ce1e))
9
+
10
+ # [2.11.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.10.11...ui-next-v2.11.0) (2026-05-26)
11
+
12
+
13
+ ### Features
14
+
15
+ * allow ai search pill to pass search parameters to nav search; remove secondary ai search modal ([4bd6620](https://github.com/academydistrict20/asd20-ui-next/commit/4bd6620a72c75b26fdd6d9256152e35065e5c7d2))
16
+
3
17
  ## [2.10.11](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.10.10...ui-next-v2.10.11) (2026-05-20)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.10.11",
3
+ "version": "2.11.1",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -1,22 +1,12 @@
1
1
  <template>
2
- <div
3
- ref="container"
4
- class="asd20-ai-search"
5
- :class="{ 'asd20-ai-search--expanded': expanded }"
6
- :style="rootStyle"
7
- >
8
- <div
9
- class="asd20-ai-search__pill"
10
- :class="{ 'asd20-ai-search__pill--hidden': expanded || isClosing }"
11
- >
2
+ <div class="asd20-ai-search">
3
+ <div class="asd20-ai-search__pill">
12
4
  <div class="asd20-ai-search__question-row">
13
5
  <div class="asd20-ai-search__query">
14
6
  <asd20-search-field
15
7
  v-model="draftQuery"
16
8
  id-tag="-ai-home"
17
9
  :placeholder="placeholder"
18
- @click="onPillInputInteraction"
19
- @focusin="onPillInputInteraction"
20
10
  @keyup.enter.stop.prevent="onSubmit"
21
11
  />
22
12
  <button
@@ -39,81 +29,23 @@
39
29
  size="md"
40
30
  bordered
41
31
  reversed
42
- :disabled="!canSubmitOrResume"
32
+ :disabled="!hasDraftQuery"
43
33
  @click.stop="onSubmit"
44
34
  />
45
35
  </div>
46
36
  </div>
47
-
48
- <transition
49
- name="ai-search-panel"
50
- @before-leave="onBeforePanelLeave"
51
- @after-leave="onAfterPanelLeave"
52
- >
53
- <div
54
- v-show="expanded"
55
- class="asd20-ai-search__panel"
56
- >
57
- <button
58
- type="button"
59
- class="asd20-ai-search__backdrop"
60
- aria-label="Close AI search"
61
- @click.stop="collapse"
62
- />
63
-
64
- <button
65
- type="button"
66
- class="asd20-ai-search__close"
67
- aria-label="Close AI search"
68
- @click.stop="collapse"
69
- >
70
- <asd20-icon
71
- name="close"
72
- size="sm"
73
- />
74
- </button>
75
-
76
- <asd20-site-search
77
- ref="siteSearch"
78
- class="asd20-ai-search__site-search"
79
- :active="expanded"
80
- :inline="true"
81
- :show-contacts="showContacts"
82
- :pages="pages"
83
- :files="files"
84
- :groups="groups"
85
- :include-district-results="includeDistrictResults"
86
- :search-language-code="searchLanguageCode"
87
- :query-pages-handler="queryPagesHandler"
88
- :query-files-handler="queryFilesHandler"
89
- :query-groups-handler="queryGroupsHandler"
90
- :query-ai-site-handler="queryAiSiteHandler"
91
- :organization="organization"
92
- :organization-options="organizationOptions"
93
- :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
94
- @update:active="onSearchActiveUpdate"
95
- />
96
- </div>
97
- </transition>
98
37
  </div>
99
38
  </template>
100
39
 
101
40
  <script>
102
41
  import Asd20SearchField from '../../molecules/Asd20SearchField'
103
- import Asd20SiteSearch from '../Asd20SiteSearch'
104
42
  import Asd20Icon from '../../atoms/Asd20Icon'
105
43
  import Asd20Button from '../../atoms/Asd20Button'
106
44
 
107
- const SEARCH_SESSION_STORAGE_KEY = 'asd20-site-search-state-v1'
108
- const SEARCH_STATE_SYNC_EVENT = 'asd20-site-search:state-sync'
109
- const PHONE_MAX_WIDTH = 767
110
- const PANEL_WIDTH_RATIO = 0.8
111
-
112
45
  export default {
113
46
  name: 'Asd20AiSearch',
114
47
  components: {
115
48
  Asd20SearchField,
116
- Asd20SiteSearch,
117
49
  Asd20Icon,
118
50
  Asd20Button,
119
51
  },
@@ -136,335 +68,26 @@ export default {
136
68
  default: 'What can we help you find?',
137
69
  },
138
70
  },
71
+ emits: ['submit-search'],
139
72
  data: () => ({
140
- expanded: false,
141
73
  draftQuery: '',
142
- screenRightOffset: 0,
143
- navLeftOffset: 0,
144
- panelWidth: 0,
145
- panelRightFromContainer: 0,
146
- panelOriginX: 0,
147
- panelOriginY: 0,
148
- isClosing: false,
149
- resumeStateAvailable: false,
150
74
  }),
151
75
  computed: {
152
- rootStyle() {
153
- return {
154
- '--asd20-ai-search-screen-right-offset': `${this.screenRightOffset}px`,
155
- '--asd20-ai-search-nav-left-offset': `${this.navLeftOffset}px`,
156
- '--asd20-ai-search-panel-width': `${this.panelWidth}px`,
157
- '--asd20-ai-search-panel-right-from-container': `${this.panelRightFromContainer}px`,
158
- '--asd20-ai-search-origin-x': `${this.panelOriginX}px`,
159
- '--asd20-ai-search-origin-y': `${this.panelOriginY}px`,
160
- }
161
- },
162
-
163
76
  hasDraftQuery() {
164
77
  return !!(this.draftQuery || '').trim()
165
78
  },
166
-
167
- canSubmitOrResume() {
168
- return this.hasDraftQuery || this.resumeStateAvailable
169
- },
170
- },
171
- mounted() {
172
- this.updatePanelLayout()
173
- this.updatePanelOrigin()
174
- this.updateResumeStateAvailability()
175
- if (typeof document !== 'undefined') {
176
- document.addEventListener('mousedown', this.onDocumentPointerDown)
177
- document.addEventListener('touchstart', this.onDocumentPointerDown, {
178
- passive: true,
179
- })
180
- }
181
- if (typeof window !== 'undefined') {
182
- window.addEventListener('resize', this.onWindowResize, { passive: true })
183
- window.addEventListener('orientationchange', this.onWindowResize, {
184
- passive: true,
185
- })
186
- window.addEventListener(SEARCH_STATE_SYNC_EVENT, this.onSearchStateSync)
187
- window.addEventListener('focus', this.onWindowFocus, { passive: true })
188
- }
189
- },
190
- beforeUnmount() {
191
- this.teardownWindowBindings()
192
79
  },
193
80
  methods: {
194
- teardownWindowBindings() {
195
- if (typeof document !== 'undefined') {
196
- document.removeEventListener('mousedown', this.onDocumentPointerDown)
197
- document.removeEventListener('touchstart', this.onDocumentPointerDown)
198
- }
199
- if (typeof window !== 'undefined') {
200
- window.removeEventListener('resize', this.onWindowResize)
201
- window.removeEventListener('orientationchange', this.onWindowResize)
202
- window.removeEventListener(
203
- SEARCH_STATE_SYNC_EVENT,
204
- this.onSearchStateSync
205
- )
206
- window.removeEventListener('focus', this.onWindowFocus)
207
- }
208
- },
209
- onWindowResize() {
210
- if (typeof window !== 'undefined' && window.innerWidth < 1024) {
211
- this.collapse()
212
- }
213
- this.updatePanelLayout()
214
- if (!this.expanded) {
215
- this.updatePanelOrigin()
216
- }
217
- },
218
-
219
- updateScreenRightOffset() {
220
- if (typeof window === 'undefined') return
221
- if (!this.$refs.container || !this.$refs.container.getBoundingClientRect)
222
- return
223
- const rect = this.$refs.container.getBoundingClientRect()
224
- this.screenRightOffset = Math.max(0, window.innerWidth - rect.right)
225
- },
226
-
227
- updateNavLeftOffset() {
228
- if (typeof window === 'undefined' || typeof document === 'undefined') {
229
- return
230
- }
231
-
232
- if (window.innerWidth < 1024) {
233
- this.navLeftOffset = 0
234
- return
235
- }
236
-
237
- const nav = document.getElementById('site-navigation')
238
- if (!nav || !nav.getBoundingClientRect) {
239
- this.navLeftOffset = 0
240
- return
241
- }
242
-
243
- const rect = nav.getBoundingClientRect()
244
- const rightEdge = Math.max(0, rect.right || 0)
245
- this.navLeftOffset =
246
- rightEdge > 0 && rightEdge < window.innerWidth * 0.5 ? rightEdge : 0
247
- },
248
-
249
- panelLayoutForViewport(containerRect) {
250
- if (!containerRect || typeof window === 'undefined') {
251
- return { width: 0, rightFromContainer: 0 }
252
- }
253
-
254
- const viewportWidth = window.innerWidth
255
- const rightOffset = Math.max(0, window.innerWidth - containerRect.right)
256
-
257
- if (viewportWidth <= PHONE_MAX_WIDTH) {
258
- return {
259
- width: viewportWidth,
260
- rightFromContainer: -rightOffset,
261
- }
262
- }
263
-
264
- const navRightEdge =
265
- viewportWidth >= 1024 ? Math.max(0, this.navLeftOffset) : 0
266
- const maxAvailableWidth = Math.max(0, viewportWidth - navRightEdge)
267
- const targetWidth = viewportWidth * PANEL_WIDTH_RATIO
268
- const width = Math.max(0, Math.min(targetWidth, maxAvailableWidth))
269
- const desiredViewportRightGap = Math.max(
270
- 0,
271
- (viewportWidth - navRightEdge - width) / 2
272
- )
273
-
274
- return {
275
- width,
276
- rightFromContainer: desiredViewportRightGap - rightOffset,
277
- }
278
- },
279
-
280
- updatePanelLayout() {
281
- this.updateScreenRightOffset()
282
- this.updateNavLeftOffset()
283
- if (!this.$refs.container || !this.$refs.container.getBoundingClientRect) {
284
- return
285
- }
286
-
287
- const rect = this.$refs.container.getBoundingClientRect()
288
- const { width, rightFromContainer } = this.panelLayoutForViewport(rect)
289
- this.panelWidth = width
290
- this.panelRightFromContainer = rightFromContainer
291
- },
292
-
293
- updatePanelOrigin() {
294
- if (typeof window === 'undefined') return
295
- if (!this.$refs.container || !this.$refs.container.getBoundingClientRect) {
296
- return
297
- }
298
-
299
- const goButton = this.$el.querySelector('.asd20-ai-search__go-button')
300
- if (!goButton || !goButton.getBoundingClientRect) return
301
-
302
- const containerRect = this.$refs.container.getBoundingClientRect()
303
- const goRect = goButton.getBoundingClientRect()
304
- const { width, rightFromContainer } =
305
- this.panelLayoutForViewport(containerRect)
306
- const panelWidth = width
307
- const panelLeft =
308
- containerRect.right - rightFromContainer - panelWidth
309
-
310
- const originX = goRect.left + goRect.width / 2 - panelLeft
311
- const originY = goRect.top + goRect.height / 2 - containerRect.top
312
-
313
- this.panelOriginX = Math.max(0, Math.min(panelWidth, originX))
314
- this.panelOriginY = Math.max(0, originY)
315
- },
316
-
317
- onDocumentPointerDown(event) {
318
- if (!this.expanded) return
319
- if (!this.$refs.container) return
320
- if (!this.$refs.container.contains(event.target)) {
321
- this.collapse()
322
- }
323
- },
324
-
325
81
  onSubmit() {
326
82
  const text = (this.draftQuery || '').trim()
327
- this.expand({ submit: !!text })
328
- },
329
-
330
- onWindowFocus() {
331
- this.updateResumeStateAvailability()
332
- },
333
-
334
- onSearchStateSync() {
335
- this.updateResumeStateAvailability()
336
- },
337
-
338
- getSearchSessionStorage() {
339
- if (typeof window === 'undefined' || !window.sessionStorage) return null
340
- try {
341
- return window.sessionStorage
342
- } catch (error) {
343
- return null
344
- }
345
- },
346
-
347
- hasPersistedSearchState() {
348
- const storage = this.getSearchSessionStorage()
349
- if (!storage) return false
350
-
351
- try {
352
- const raw = storage.getItem(SEARCH_SESSION_STORAGE_KEY)
353
- if (!raw) return false
354
- const parsed = JSON.parse(raw)
355
- if (!parsed || typeof parsed !== 'object') return false
356
- const state = parsed.state && typeof parsed.state === 'object'
357
- ? parsed.state
358
- : parsed
359
- const text = typeof state.text === 'string' ? state.text.trim() : ''
360
- return !!text
361
- } catch (error) {
362
- return false
363
- }
364
- },
365
-
366
- hasExistingConversation() {
367
- const siteSearch = this.$refs.siteSearch
368
- if (!siteSearch) return false
369
- if (Array.isArray(siteSearch.aiTranscript) && siteSearch.aiTranscript.length) {
370
- return true
371
- }
372
- return !!siteSearch.hasAssistantTurns
373
- },
374
-
375
- hasExistingResults() {
376
- const siteSearch = this.$refs.siteSearch
377
- if (!siteSearch) return false
378
- const pages = Array.isArray(siteSearch.currentPages)
379
- ? siteSearch.currentPages
380
- : siteSearch.resolvedPages
381
- const files = Array.isArray(siteSearch.currentFiles)
382
- ? siteSearch.currentFiles
383
- : siteSearch.resolvedFiles
384
- const groups = Array.isArray(siteSearch.currentGroups)
385
- ? siteSearch.currentGroups
386
- : siteSearch.resolvedGroups
387
- if (
388
- Array.isArray(pages) &&
389
- pages.length
390
- ) {
391
- return true
392
- }
393
- if (
394
- Array.isArray(files) &&
395
- files.length
396
- ) {
397
- return true
398
- }
399
- if (
400
- Array.isArray(groups) &&
401
- groups.length
402
- ) {
403
- return true
404
- }
405
- return false
406
- },
407
-
408
- hasResumeState() {
409
- return (
410
- this.hasExistingConversation() ||
411
- this.hasExistingResults() ||
412
- this.hasPersistedSearchState()
413
- )
414
- },
415
-
416
- updateResumeStateAvailability() {
417
- this.resumeStateAvailable = this.hasResumeState()
418
- },
419
-
420
- onPillInputInteraction() {
421
- if (this.expanded || this.isClosing) return
422
- this.updateResumeStateAvailability()
423
- if (!this.hasResumeState()) return
424
- this.expand({ submit: false })
425
- },
426
-
427
- onSearchActiveUpdate(val) {
428
- if (!val) this.collapse()
429
- },
430
-
431
- collapse() {
432
- if (!this.expanded) return
433
- this.expanded = false
434
- this.draftQuery = ''
435
- this.updateResumeStateAvailability()
436
- },
437
-
438
- onBeforePanelLeave() {
439
- this.isClosing = true
440
- },
441
-
442
- onAfterPanelLeave() {
443
- this.isClosing = false
444
- },
445
-
446
- expand(options = {}) {
447
- const shouldSubmit = !!options.submit
448
- this.isClosing = false
449
- this.updatePanelLayout()
450
- this.updatePanelOrigin()
451
- this.expanded = true
83
+ if (!text) return
452
84
 
453
- this.$nextTick(() => {
454
- const siteSearch = this.$refs.siteSearch
455
- if (!siteSearch) return
456
-
457
- const text = this.draftQuery
458
- if (typeof siteSearch.applyInputText === 'function') {
459
- siteSearch.applyInputText(text, { submit: shouldSubmit })
460
- }
461
-
462
- if (shouldSubmit) {
463
- this.draftQuery = ''
464
- }
465
-
466
- this.updateResumeStateAvailability()
85
+ this.$emit('submit-search', {
86
+ text,
87
+ submit: true,
88
+ requestedAt: Date.now(),
467
89
  })
90
+ this.draftQuery = ''
468
91
  },
469
92
  },
470
93
  }
@@ -526,16 +149,23 @@ export default {
526
149
  }
527
150
  }
528
151
 
529
- &__pill--hidden {
530
- opacity: 0;
531
- visibility: hidden;
532
- pointer-events: none;
533
- }
534
-
535
152
  &__pill:focus-within {
536
153
  box-shadow: 0 0 0 2px var(--color__accent-t70);
537
154
  }
538
155
 
156
+ &__question-row {
157
+ display: flex;
158
+ align-items: center;
159
+ width: 100%;
160
+ }
161
+
162
+ &__query {
163
+ position: relative;
164
+ flex: 1 1 auto;
165
+ min-width: 0;
166
+ padding-left: 0.5rem;
167
+ }
168
+
539
169
  &__icon-button {
540
170
  position: absolute;
541
171
  top: 50%;
@@ -552,19 +182,6 @@ export default {
552
182
  color: var(--color__primary-t50);
553
183
  }
554
184
 
555
- &__question-row {
556
- display: flex;
557
- align-items: center;
558
- width: 100%;
559
- }
560
-
561
- &__query {
562
- position: relative;
563
- flex: 1 1 auto;
564
- min-width: 0;
565
- padding-left: 0.5rem;
566
- }
567
-
568
185
  &__go-button {
569
186
  flex: 0 0 auto;
570
187
  border-radius: 3rem;
@@ -584,133 +201,22 @@ export default {
584
201
  line-height: 1;
585
202
  }
586
203
 
587
- &__icon-button:focus,
588
- &__close:focus {
204
+ &__icon-button:focus {
589
205
  outline: none;
590
206
  box-shadow: 0 0 0 2px var(--color__accent-t70);
591
207
  border-radius: 999px;
592
208
  }
593
-
594
- &__panel {
595
- position: absolute;
596
- top: 0;
597
- left: 0;
598
- right: 0;
599
- z-index: 8;
600
- min-height: 75vh;
601
- height: 75vh;
602
- }
603
-
604
- &__backdrop {
605
- position: fixed;
606
- inset: 0;
607
- border: 0;
608
- margin: 0;
609
- padding: 0;
610
- background: var(--color__primary);
611
- opacity: 0.45;
612
- z-index: 7;
613
- cursor: pointer;
614
- }
615
-
616
- &__site-search {
617
- position: relative;
618
- z-index: 8;
619
- height: 100%;
620
-
621
- :deep(.asd20-site-search__ai-disclaimer-text) {
622
- padding-right: 3rem;
623
- }
624
- }
625
-
626
- &__close {
627
- position: absolute;
628
- top: space(0.625);
629
- right: space(0.625);
630
- z-index: 9;
631
- width: 1.75rem;
632
- height: 1.75rem;
633
- border-radius: 999px;
634
- border: 1px solid var(--color__tertiary);
635
- background: transparent;
636
- cursor: pointer;
637
- display: inline-flex;
638
- align-items: center;
639
- justify-content: center;
640
- }
641
- }
642
-
643
- .ai-search-panel-enter-active {
644
- transition: opacity 380ms ease;
645
- }
646
-
647
- .ai-search-panel-leave-active {
648
- transition: opacity 440ms ease;
649
209
  }
650
210
 
651
- .ai-search-panel-enter,
652
- .ai-search-panel-enter-from,
653
- .ai-search-panel-leave-to {
654
- opacity: 0;
655
- }
656
-
657
- .ai-search-panel-enter-active .asd20-ai-search__site-search,
658
- .ai-search-panel-enter-active .asd20-ai-search__close {
659
- transition:
660
- transform 520ms cubic-bezier(0.22, 0.8, 0.22, 1),
661
- opacity 380ms ease;
662
- transform-origin: var(--asd20-ai-search-origin-x, calc(100% - 2.5rem))
663
- var(--asd20-ai-search-origin-y, 1.375rem);
664
- will-change: transform, opacity;
665
- }
666
-
667
- .ai-search-panel-leave-active .asd20-ai-search__site-search,
668
- .ai-search-panel-leave-active .asd20-ai-search__close {
669
- transition:
670
- transform 460ms cubic-bezier(0.4, 0, 1, 1),
671
- opacity 440ms ease;
672
- transform-origin: var(--asd20-ai-search-origin-x, calc(100% - 2.5rem))
673
- var(--asd20-ai-search-origin-y, 1.375rem);
674
- will-change: transform, opacity;
675
- }
676
-
677
- .ai-search-panel-enter .asd20-ai-search__site-search,
678
- .ai-search-panel-enter-from .asd20-ai-search__site-search,
679
- .ai-search-panel-leave-to .asd20-ai-search__site-search,
680
- .ai-search-panel-enter .asd20-ai-search__close,
681
- .ai-search-panel-enter-from .asd20-ai-search__close,
682
- .ai-search-panel-leave-to .asd20-ai-search__close {
683
- transform: scale3d(0.16, 0.1, 1);
684
- opacity: 0;
685
- }
686
-
687
- .ai-search-panel-enter-active .asd20-ai-search__backdrop,
688
- .ai-search-panel-leave-active .asd20-ai-search__backdrop {
689
- transition: opacity 440ms ease;
690
- }
691
-
692
- .ai-search-panel-enter .asd20-ai-search__backdrop,
693
- .ai-search-panel-enter-from .asd20-ai-search__backdrop,
694
- .ai-search-panel-leave-to .asd20-ai-search__backdrop {
695
- opacity: 0;
696
- }
697
-
698
- @media (max-width: 1023px) {
211
+ @media (max-width: 479px) {
699
212
  .asd20-ai-search {
700
- display: none;
213
+ min-width: 0;
701
214
  }
702
215
  }
703
216
 
704
- @media (min-width: 667px) {
217
+ @media (max-width: 1023px) {
705
218
  .asd20-ai-search {
706
- &__panel {
707
- width: var(
708
- --asd20-ai-search-panel-width,
709
- calc(100vw - (2 * var(--asd20-ai-search-screen-right-offset, 0px)))
710
- );
711
- left: auto;
712
- right: var(--asd20-ai-search-panel-right-from-container, 0px);
713
- }
219
+ display: none;
714
220
  }
715
221
  }
716
222
  </style>
@@ -65,9 +65,21 @@
65
65
  <transition name="slide">
66
66
  <asd20-site-search
67
67
  v-show="searchOpen"
68
+ ref="siteSearch"
68
69
  :active="searchOpen"
70
+ :show-contacts="showContacts"
71
+ :pages="pages"
72
+ :files="files"
73
+ :groups="groups"
74
+ :include-district-results="includeDistrictResults"
75
+ :search-language-code="searchLanguageCode"
76
+ :query-pages-handler="queryPagesHandler"
77
+ :query-files-handler="queryFilesHandler"
78
+ :query-groups-handler="queryGroupsHandler"
79
+ :query-ai-site-handler="queryAiSiteHandler"
69
80
  :organization="organization"
70
81
  :organization-options="organizationOptions"
82
+ :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
71
83
  @update:active="$emit('update:searchOpen', $event)"
72
84
  >
73
85
  </asd20-site-search>
@@ -102,14 +114,27 @@ export default {
102
114
  condensed: { type: Boolean, default: false },
103
115
  menuOpen: { type: Boolean, default: false },
104
116
  searchOpen: { type: Boolean, default: false },
117
+ searchRequest: { type: Object, default: null },
118
+ pages: { type: Array, default: () => [] },
119
+ files: { type: Array, default: () => [] },
120
+ groups: { type: Array, default: () => [] },
121
+ showContacts: { type: Boolean, default: true },
122
+ includeDistrictResults: { type: Boolean, default: true },
123
+ searchLanguageCode: { type: String, default: null },
124
+ queryPagesHandler: { type: Function, default: null },
125
+ queryFilesHandler: { type: Function, default: null },
126
+ queryGroupsHandler: { type: Function, default: null },
127
+ queryAiSiteHandler: { type: Function, default: null },
105
128
  organization: { type: Object, default: null },
106
129
  organizationOptions: { type: Array, default: () => [] },
130
+ aiSearchFeedbackFormUrl: { type: String, default: '' },
107
131
  },
108
132
  data: () => ({
109
133
  keyboardTriggeredLastAction: false,
110
134
  desktop: false,
111
135
  isLoaded: false,
112
136
  zoomed: false,
137
+ pendingSearchRequest: null,
113
138
  }),
114
139
  computed: {
115
140
  emptyNav() {
@@ -124,6 +149,14 @@ export default {
124
149
  }
125
150
  })
126
151
  },
152
+ searchOpen: function (val) {
153
+ if (val) {
154
+ this.$nextTick(() => this.applyPendingSearchRequest())
155
+ }
156
+ },
157
+ searchRequest: function (request) {
158
+ this.queueSearchRequest(request)
159
+ },
127
160
  },
128
161
  mounted() {
129
162
  this.desktop = window.innerWidth >= 1024
@@ -153,6 +186,45 @@ export default {
153
186
  this.desktop = window.innerWidth >= 1024
154
187
  this.zoomed = window.innerHeight <= 650
155
188
  },
189
+ normalizeSearchRequest(request) {
190
+ if (!request || typeof request !== 'object') return null
191
+ const text = typeof request.text === 'string' ? request.text.trim() : ''
192
+ if (!text) return null
193
+
194
+ return {
195
+ text,
196
+ submit: request.submit !== false,
197
+ }
198
+ },
199
+ queueSearchRequest(request) {
200
+ const nextRequest = this.normalizeSearchRequest(request)
201
+ if (!nextRequest) return
202
+
203
+ this.pendingSearchRequest = nextRequest
204
+ const siteSearch = this.$refs.siteSearch
205
+ if (typeof siteSearch?.prepareProgrammaticInput === 'function') {
206
+ siteSearch.prepareProgrammaticInput()
207
+ }
208
+
209
+ if (this.menuOpen) {
210
+ this.$emit('update:menuOpen', false)
211
+ }
212
+ if (!this.searchOpen) {
213
+ this.$emit('update:searchOpen', true)
214
+ }
215
+
216
+ this.$nextTick(() => this.applyPendingSearchRequest())
217
+ },
218
+ applyPendingSearchRequest() {
219
+ if (!this.searchOpen || !this.pendingSearchRequest) return
220
+
221
+ const siteSearch = this.$refs.siteSearch
222
+ if (typeof siteSearch?.applyInputText !== 'function') return
223
+
224
+ const request = this.pendingSearchRequest
225
+ this.pendingSearchRequest = null
226
+ siteSearch.applyInputText(request.text, { submit: request.submit })
227
+ },
156
228
  toggleSearch() {
157
229
  this.$emit('update:searchOpen', !this.searchOpen)
158
230
  },
@@ -683,7 +683,6 @@ export default {
683
683
  searchMode: '',
684
684
  routeRestoreInProgress: false,
685
685
  lastRestoredSearchKey: '',
686
- skipNextRouteRestore: false,
687
686
  suppressAutoRestore: false,
688
687
  lastManualTabChangeAt: 0,
689
688
  lastManualTabLabel: '',
@@ -1077,9 +1076,9 @@ export default {
1077
1076
  this.focusQuery()
1078
1077
  this.refreshResultsScrollLayer()
1079
1078
  })
1080
- this.restoreSearchStateFromRoute()
1079
+ this.restoreSearchState()
1081
1080
  } else {
1082
- this.clearSearchStateFromRouteQuery()
1081
+ this.clearLegacySearchRouteQuery()
1083
1082
  if (!this.inline) this.unlockBodyScroll()
1084
1083
  this.blurQuery()
1085
1084
  }
@@ -1090,7 +1089,7 @@ export default {
1090
1089
  if (newVal === 'Answers') {
1091
1090
  this.$nextTick(() => this.refreshResultsScrollLayer())
1092
1091
  }
1093
- this.syncSearchStateToRoute({ tab: newVal })
1092
+ this.syncSearchState({ tab: newVal })
1094
1093
  },
1095
1094
 
1096
1095
  aiAnswer() {
@@ -1115,7 +1114,7 @@ export default {
1115
1114
  this.selectedGroup = null
1116
1115
  this.searchPages({ runId })
1117
1116
  this.searchFilesWithGroupOwners({ runId })
1118
- this.syncSearchStateToRoute()
1117
+ this.syncSearchState()
1119
1118
  }
1120
1119
  }, 400),
1121
1120
 
@@ -1124,7 +1123,7 @@ export default {
1124
1123
  const runId = this.beginSearchRun()
1125
1124
  this.searchPages({ runId })
1126
1125
  this.searchFilesWithGroupOwners({ runId })
1127
- this.syncSearchStateToRoute()
1126
+ this.syncSearchState()
1128
1127
  },
1129
1128
  // Clear AI answer if user clears the input
1130
1129
 
@@ -1137,34 +1136,20 @@ export default {
1137
1136
  this.aiKeywordsDisplayPages = ''
1138
1137
  }
1139
1138
  },
1140
-
1141
- '$route.query': {
1142
- handler() {
1143
- if (this.suppressAutoRestore) return
1144
- if (this.searchingAi) return
1145
- if (this.hasRecentManualTabSelection()) return
1146
- if (this.skipNextRouteRestore) {
1147
- this.skipNextRouteRestore = false
1148
- return
1149
- }
1150
- if (!this.getRouteSearchState(this.$route.query)) {
1151
- this.lastRestoredSearchKey = ''
1152
- }
1153
- if (this.active) this.restoreSearchStateFromRoute()
1154
- },
1155
- },
1156
1139
  },
1157
1140
 
1158
1141
  mounted() {
1159
1142
  if (this.active && !this.inline) {
1160
1143
  this.lockBodyScroll()
1161
1144
  }
1162
- if (this.didPageReload()) {
1145
+ const didReload = this.didPageReload()
1146
+ if (didReload) {
1163
1147
  this.persistSearchState(null)
1164
1148
  this.lastRestoredSearchKey = ''
1165
- this.clearSearchStateFromRouteQuery()
1166
- } else if (this.active) {
1167
- this.restoreSearchStateFromRoute()
1149
+ }
1150
+ this.clearLegacySearchRouteQuery()
1151
+ if (!didReload && this.active) {
1152
+ this.restoreSearchState()
1168
1153
  }
1169
1154
  if (typeof window !== 'undefined') {
1170
1155
  window.addEventListener('resize', this.onWindowReflow, { passive: true })
@@ -1323,11 +1308,6 @@ export default {
1323
1308
  }
1324
1309
  },
1325
1310
 
1326
- normalizeRouteQueryValue(value) {
1327
- if (Array.isArray(value)) return value[0]
1328
- return value
1329
- },
1330
-
1331
1311
  isLocalDevelopmentHost(hostname = '') {
1332
1312
  if (!hostname || typeof hostname !== 'string') return false
1333
1313
  const lower = hostname.toLowerCase()
@@ -1406,7 +1386,7 @@ export default {
1406
1386
  if (this.searchingAi) return
1407
1387
  if (this.hasRecentManualTabSelection()) return
1408
1388
  if (!this.active || this.routeRestoreInProgress) return
1409
- this.restoreSearchStateFromRoute()
1389
+ this.restoreSearchState()
1410
1390
  },
1411
1391
 
1412
1392
  hasRecentManualTabSelection() {
@@ -1560,7 +1540,7 @@ export default {
1560
1540
  },
1561
1541
 
1562
1542
  persistSearchCacheSnapshot(overrides = {}) {
1563
- const state = this.getCurrentSearchStateForRoute(overrides)
1543
+ const state = this.getCurrentSearchState(overrides)
1564
1544
  if (!state) return
1565
1545
  this.persistSearchState(state, {
1566
1546
  cache: this.buildSearchCacheSnapshot(),
@@ -1673,33 +1653,7 @@ export default {
1673
1653
  return true
1674
1654
  },
1675
1655
 
1676
- getRouteSearchState(routeQuery = {}) {
1677
- const rawText = this.normalizeRouteQueryValue(
1678
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.text]
1679
- )
1680
- const rawIncludeDistrictResults = this.normalizeRouteQueryValue(
1681
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.includeDistrictResults]
1682
- )
1683
- const rawMode = this.normalizeRouteQueryValue(
1684
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.mode]
1685
- )
1686
- const rawTab = this.normalizeRouteQueryValue(
1687
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.tab]
1688
- )
1689
- const rawUpdatedAt = this.normalizeRouteQueryValue(
1690
- routeQuery[SEARCH_ROUTE_QUERY_KEYS.updatedAt]
1691
- )
1692
-
1693
- return this.normalizeSearchState({
1694
- text: rawText,
1695
- mode: rawMode,
1696
- tab: rawTab,
1697
- includeDistrictResults: rawIncludeDistrictResults,
1698
- updatedAt: rawUpdatedAt,
1699
- })
1700
- },
1701
-
1702
- getCurrentSearchStateForRoute(overrides = {}) {
1656
+ getCurrentSearchState(overrides = {}) {
1703
1657
  const hasSubmitted =
1704
1658
  typeof overrides.hasSubmitted === 'boolean'
1705
1659
  ? overrides.hasSubmitted
@@ -1746,35 +1700,17 @@ export default {
1746
1700
  }
1747
1701
  },
1748
1702
 
1749
- buildRouteSearchQueryValues(state) {
1750
- if (!state) return {}
1751
- const values = {
1752
- [SEARCH_ROUTE_QUERY_KEYS.text]: state.text,
1753
- [SEARCH_ROUTE_QUERY_KEYS.mode]: state.mode,
1754
- [SEARCH_ROUTE_QUERY_KEYS.tab]: state.tab,
1755
- [SEARCH_ROUTE_QUERY_KEYS.includeDistrictResults]:
1756
- state.includeDistrictResults ? '1' : '0',
1757
- }
1758
- const updatedAt = Number(state.updatedAt)
1759
- if (Number.isFinite(updatedAt) && updatedAt > 0) {
1760
- values[SEARCH_ROUTE_QUERY_KEYS.updatedAt] = String(
1761
- Math.round(updatedAt)
1762
- )
1763
- }
1764
- return values
1765
- },
1766
-
1767
- buildQueryWithSearchState(state) {
1703
+ buildQueryWithoutSearchState() {
1768
1704
  const nextQuery = { ...((this.$route && this.$route.query) || {}) }
1769
1705
  Object.keys(SEARCH_ROUTE_QUERY_KEYS).forEach(key => {
1770
1706
  delete nextQuery[SEARCH_ROUTE_QUERY_KEYS[key]]
1771
1707
  })
1772
- return { ...nextQuery, ...this.buildRouteSearchQueryValues(state) }
1708
+ return nextQuery
1773
1709
  },
1774
1710
 
1775
- async clearSearchStateFromRouteQuery() {
1711
+ async clearLegacySearchRouteQuery() {
1776
1712
  if (!this.$router || !this.$route) return
1777
- const nextQuery = this.buildQueryWithSearchState(null)
1713
+ const nextQuery = this.buildQueryWithoutSearchState()
1778
1714
  await this.replaceRouteQuery(nextQuery)
1779
1715
  },
1780
1716
 
@@ -1800,25 +1736,20 @@ export default {
1800
1736
  async replaceRouteQuery(query) {
1801
1737
  if (!this.$router || !this.$route) return
1802
1738
  if (this.isSameQuery(this.$route.query, query)) return
1803
- this.skipNextRouteRestore = true
1804
1739
  try {
1805
1740
  await this.$router.replace({ path: this.$route.path, query })
1806
1741
  } catch (error) {
1807
- this.skipNextRouteRestore = false
1808
1742
  // Ignore duplicate navigation and transient route replacement races.
1809
1743
  }
1810
1744
  },
1811
1745
 
1812
- async syncSearchStateToRoute(overrides = {}) {
1746
+ syncSearchState(overrides = {}) {
1813
1747
  if (this.routeRestoreInProgress) return
1814
- if (!this.$router || !this.$route) return
1815
- const state = this.getCurrentSearchStateForRoute(overrides)
1748
+ const state = this.getCurrentSearchState(overrides)
1816
1749
  this.lastRestoredSearchKey = state
1817
- ? `route:${JSON.stringify(state)}`
1750
+ ? `storage:${JSON.stringify(state)}`
1818
1751
  : ''
1819
1752
  this.persistSearchState(state, { preserveCache: true })
1820
- const nextQuery = this.buildQueryWithSearchState(state)
1821
- await this.replaceRouteQuery(nextQuery)
1822
1753
  },
1823
1754
 
1824
1755
  appendSearchContextToUrl(rawUrl) {
@@ -1826,14 +1757,11 @@ export default {
1826
1757
  return rawUrl
1827
1758
  },
1828
1759
 
1829
- async restoreSearchStateFromRoute() {
1760
+ async restoreSearchState() {
1830
1761
  if (this.suppressAutoRestore) return
1831
1762
  if (this.searchingAi) return
1832
- if (!this.$route || this.routeRestoreInProgress) return
1763
+ if (this.routeRestoreInProgress) return
1833
1764
  const restoreStartedAt = Date.now()
1834
- const routeStateCandidate = this.getRouteSearchState(this.$route.query)
1835
- const routeStateFresh = this.isSearchStateFresh(routeStateCandidate)
1836
- const routeState = routeStateFresh ? routeStateCandidate : null
1837
1765
  const persistedRecordCandidate = this.readPersistedSearchRecord()
1838
1766
  const persistedRecordFresh =
1839
1767
  persistedRecordCandidate &&
@@ -1845,24 +1773,16 @@ export default {
1845
1773
  if (persistedRecordCandidate && !persistedRecordFresh) {
1846
1774
  this.persistSearchState(null)
1847
1775
  }
1848
- if (routeStateCandidate && !routeStateFresh) {
1849
- await this.clearSearchStateFromRouteQuery()
1850
- }
1851
1776
 
1852
- const persistedState = routeState
1853
- ? null
1854
- : persistedRecord && persistedRecord.state
1855
- ? persistedRecord.state
1856
- : null
1857
- const restoredState = routeState || persistedState
1777
+ const restoredState =
1778
+ persistedRecord && persistedRecord.state ? persistedRecord.state : null
1858
1779
  if (!restoredState) return
1859
1780
 
1860
- const restoreSource = routeState ? 'route' : 'storage'
1861
1781
  const cacheVersion = this.getSearchCacheVersion(
1862
1782
  persistedRecord,
1863
1783
  restoredState
1864
1784
  )
1865
- const restoreKey = `${restoreSource}:${JSON.stringify(
1785
+ const restoreKey = `storage:${JSON.stringify(
1866
1786
  restoredState
1867
1787
  )}:${cacheVersion}`
1868
1788
  if (restoreKey === this.lastRestoredSearchKey) return
@@ -1934,9 +1854,14 @@ export default {
1934
1854
  }
1935
1855
  },
1936
1856
 
1857
+ prepareProgrammaticInput() {
1858
+ this.suppressAutoRestore = true
1859
+ },
1860
+
1937
1861
  applyInputText(text, options = {}) {
1938
1862
  const submit = !!options.submit
1939
1863
  const nextText = typeof text === 'string' ? text : ''
1864
+ this.prepareProgrammaticInput()
1940
1865
  this.suppressInputClearReset = true
1941
1866
  this.inputText = nextText
1942
1867
 
@@ -1946,6 +1871,9 @@ export default {
1946
1871
  if (submit && nextText.trim()) {
1947
1872
  await this.onSubmitInput()
1948
1873
  }
1874
+ this.$nextTick(() => {
1875
+ this.suppressAutoRestore = false
1876
+ })
1949
1877
  })
1950
1878
  },
1951
1879
 
@@ -2245,7 +2173,7 @@ export default {
2245
2173
 
2246
2174
  dismiss() {
2247
2175
  if (!this.active) return
2248
- this.clearSearchStateFromRouteQuery()
2176
+ this.clearLegacySearchRouteQuery()
2249
2177
  this.$emit('update:active', false)
2250
2178
  },
2251
2179
 
@@ -2452,7 +2380,7 @@ export default {
2452
2380
  await this.clearSearchState()
2453
2381
  this.currentTab = 'Answers'
2454
2382
  this.persistSearchState(null)
2455
- await this.syncSearchStateToRoute({ hasSubmitted: false, text: '' })
2383
+ this.syncSearchState({ hasSubmitted: false, text: '' })
2456
2384
  this.lastRestoredSearchKey = ''
2457
2385
 
2458
2386
  this.$nextTick(() => {
@@ -2594,7 +2522,7 @@ export default {
2594
2522
  this.searchMode = ''
2595
2523
  await this.clearSearchState()
2596
2524
  this.persistSearchState(null)
2597
- await this.syncSearchStateToRoute({ hasSubmitted: false, text: '' })
2525
+ this.syncSearchState({ hasSubmitted: false, text: '' })
2598
2526
  this.lastRestoredSearchKey = ''
2599
2527
  this.$nextTick(() => {
2600
2528
  this.suppressAutoRestore = false
@@ -2609,7 +2537,7 @@ export default {
2609
2537
  this.searchMode = mode
2610
2538
 
2611
2539
  if (mode === 'question') {
2612
- this.syncSearchStateToRoute({ text, mode, tab: 'Answers' })
2540
+ this.syncSearchState({ text, mode, tab: 'Answers' })
2613
2541
  this.searchAi(text)
2614
2542
  } else {
2615
2543
  await this.clearSearchState()
@@ -2621,7 +2549,7 @@ export default {
2621
2549
  this.searchFilesWithGroupOwners({ runId })
2622
2550
  if (!this.isCurrentSearchRun(runId)) return
2623
2551
  this.currentTab = 'Pages'
2624
- this.syncSearchStateToRoute({ text, mode, tab: 'Pages' })
2552
+ this.syncSearchState({ text, mode, tab: 'Pages' })
2625
2553
  this.$nextTick(() => this.refreshResultsScrollLayer())
2626
2554
 
2627
2555
  // Log keyword-only search analytics
@@ -9,8 +9,19 @@
9
9
  :action-items="actionItems"
10
10
  :menu-open="menuOpen"
11
11
  :search-open="searchOpen"
12
+ :search-request="homeSearchRequest"
13
+ :pages="searchPages"
14
+ :files="searchFiles"
15
+ :groups="searchGroups"
16
+ :include-district-results="includeDistrictResults"
17
+ :search-language-code="searchLanguageCode"
18
+ :query-pages-handler="queryPagesHandler"
19
+ :query-files-handler="queryFilesHandler"
20
+ :query-groups-handler="queryGroupsHandler"
21
+ :query-ai-site-handler="queryAiSiteHandler"
12
22
  :organization="organization"
13
23
  :organization-options="organizationOptions"
24
+ :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
14
25
  @update:menuOpen="$emit('update:menuOpen', $event)"
15
26
  @update:searchOpen="$emit('update:searchOpen', $event)"
16
27
  >
@@ -63,6 +74,7 @@
63
74
  :organization="organization"
64
75
  :organization-options="organizationOptions"
65
76
  :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
77
+ @submit-search="onHomeSearchSubmit"
66
78
  />
67
79
  </client-only>
68
80
 
@@ -218,6 +230,7 @@ export default {
218
230
  aiSearchFeedbackFormUrl: { type: String, default: '' },
219
231
  },
220
232
  data: () => ({
233
+ homeSearchRequest: null,
221
234
  factoids: [
222
235
  {
223
236
  start: 0,
@@ -249,6 +262,14 @@ export default {
249
262
  },
250
263
  ],
251
264
  }),
265
+ methods: {
266
+ onHomeSearchSubmit(request) {
267
+ this.homeSearchRequest = {
268
+ ...request,
269
+ requestedAt: Date.now(),
270
+ }
271
+ },
272
+ },
252
273
  }
253
274
  </script>
254
275
 
@@ -9,8 +9,19 @@
9
9
  :action-items="actionItems"
10
10
  :menu-open="menuOpen"
11
11
  :search-open="searchOpen"
12
+ :search-request="homeSearchRequest"
13
+ :pages="searchPages"
14
+ :files="searchFiles"
15
+ :groups="searchGroups"
16
+ :include-district-results="includeDistrictResults"
17
+ :search-language-code="searchLanguageCode"
18
+ :query-pages-handler="queryPagesHandler"
19
+ :query-files-handler="queryFilesHandler"
20
+ :query-groups-handler="queryGroupsHandler"
21
+ :query-ai-site-handler="queryAiSiteHandler"
12
22
  :organization-options="organizationOptions"
13
23
  :organization="organization"
24
+ :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
14
25
  @update:menuOpen="$emit('update:menuOpen', $event)"
15
26
  @update:searchOpen="$emit('update:searchOpen', $event)"
16
27
  />
@@ -107,6 +118,7 @@
107
118
  :organization="organization"
108
119
  :organization-options="organizationOptions"
109
120
  :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
121
+ @submit-search="onHomeSearchSubmit"
110
122
  />
111
123
  </client-only>
112
124
  <Asd20BlockSchedule
@@ -233,6 +245,7 @@ export default {
233
245
 
234
246
  data: () => ({
235
247
  showLogo: true,
248
+ homeSearchRequest: null,
236
249
  }),
237
250
 
238
251
  computed: {
@@ -316,6 +329,12 @@ export default {
316
329
  download: event.currentTarget?.hasAttribute?.('download'),
317
330
  })
318
331
  },
332
+ onHomeSearchSubmit(request) {
333
+ this.homeSearchRequest = {
334
+ ...request,
335
+ requestedAt: Date.now(),
336
+ }
337
+ },
319
338
  },
320
339
  }
321
340
  </script>
@@ -9,8 +9,19 @@
9
9
  :action-items="actionItems"
10
10
  :menu-open="menuOpen"
11
11
  :search-open="searchOpen"
12
+ :search-request="homeSearchRequest"
13
+ :pages="searchPages"
14
+ :files="searchFiles"
15
+ :groups="searchGroups"
16
+ :include-district-results="includeDistrictResults"
17
+ :search-language-code="searchLanguageCode"
18
+ :query-pages-handler="queryPagesHandler"
19
+ :query-files-handler="queryFilesHandler"
20
+ :query-groups-handler="queryGroupsHandler"
21
+ :query-ai-site-handler="queryAiSiteHandler"
12
22
  :organization="organization"
13
23
  :organization-options="organizationOptions"
24
+ :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
14
25
  @update:menuOpen="$emit('update:menuOpen', $event)"
15
26
  @update:searchOpen="$emit('update:searchOpen', $event)"
16
27
  />
@@ -108,6 +119,7 @@
108
119
  :organization="organization"
109
120
  :organization-options="organizationOptions"
110
121
  :ai-search-feedback-form-url="aiSearchFeedbackFormUrl"
122
+ @submit-search="onHomeSearchSubmit"
111
123
  />
112
124
  </client-only>
113
125
 
@@ -235,6 +247,9 @@ export default {
235
247
  queryAiSiteHandler: { type: Function, default: null },
236
248
  aiSearchFeedbackFormUrl: { type: String, default: '' },
237
249
  },
250
+ data: () => ({
251
+ homeSearchRequest: null,
252
+ }),
238
253
  computed: {
239
254
  blockEvent() {
240
255
  if (!this.events || this.events.length === 0) return null
@@ -269,6 +284,12 @@ export default {
269
284
  }
270
285
  },
271
286
  methods: {
287
+ onHomeSearchSubmit(request) {
288
+ this.homeSearchRequest = {
289
+ ...request,
290
+ requestedAt: Date.now(),
291
+ }
292
+ },
272
293
  onLogoClick(event, link) {
273
294
  navigateInternalHref(this, event, link, {
274
295
  target: event.currentTarget?.getAttribute?.('target'),