1(function () { 2 const THEMES = { 3 "black-white": { 4 bg: "#ffffff", 5 panel: "#f7f7f8", 6 text: "#111827", 7 muted: "#64748b", 8 soft: "#eef2f7", 9 border: "#d9e2ec", 10 accent: "#111827", 11 accentSoft: "#f1f5f9", 12 card: "#ffffff", 13 shadow: "rgba(15, 23, 42, .08)", 14 imageBg: "#f8fafc", 15 }, 16 "blue-white": { 17 bg: "#ffffff", 18 panel: "#f7fbff", 19 text: "#102033", 20 muted: "#64748b", 21 soft: "#eaf4ff", 22 border: "#d7e7fb", 23 accent: "#2563eb", 24 accentSoft: "#eaf2ff", 25 card: "#ffffff", 26 shadow: "rgba(37, 99, 235, .12)", 27 imageBg: "#f4f9ff", 28 }, 29 }; 30 31 const QUERY = "I'm looking for a soft rug that makes my living room feel warmer and more cozy"; 32 33 const HERO_PRODUCTS = [ 34 ["NOMA LIVING", "Nordic Wool Rug", "$399.00", "rug-a"], 35 ["HAVEN FORM", "Oak Coffee Table", "$549.00", "table-a"], 36 ["ROWAN & CO.", "Linen Lounge Chair", "$699.00", "chair-a"], 37 ["STUDIO HOME", "Floor Lamp", "$189.00", "lamp-a"], 38 ["ASHEN GOODS", "Bookshelf", "$449.00", "shelf-a"], 39 ["FIELDFORM", "Side Table", "$229.00", "table-b"], 40 ["KESTREL", "Fabric Sofa", "$1,299.00", "sofa-a"], 41 ["TERNARY HOUSE", "Dining Chair", "$159.00", "chair-b"], 42 ]; 43 44 const SEARCH_RESULTS = [ 45 ["NOMA LIVING", "Nordic Wool Rug", "$399.00", "rug-a"], 46 ["HAVEN FORM", "Handwoven Cozy Rug", "$459.00", "rug-b"], 47 ]; 48 49 const PRODUCT_IMAGES = { 50 "rug-a": "./images/rug-a.png", 51 "table-a": "./images/table-a.png", 52 "chair-a": "./images/chair-a.png", 53 "lamp-a": "./images/lamp-a.png", 54 "shelf-a": "./images/shelf-a.png", 55 "table-b": "./images/table-b.png", 56 "sofa-a": "./images/sofa-a.png", 57 "chair-b": "./images/chair-b.png", 58 "rug-b": "./images/rug-b.png", 59 }; 60 61 const DEFAULT_COPY = { 62 query: QUERY, 63 heroProducts: HERO_PRODUCTS, 64 searchResults: SEARCH_RESULTS, 65 startingAt: "Starting at", 66 resultsTitle: "Search Results", 67 tabs: [ 68 ["Products", "2", true], 69 ["Articles", "0", false], 70 ["Pages", "0", false], 71 ], 72 sortBy: "Sort by", 73 relevance: "Relevance", 74 resultsFound: '2 results found for "rug"', 75 filters: "Filters", 76 price: "Price", 77 min: "Min", 78 max: "Max", 79 availableRange: "Available range: $0.00 to $1,999.00", 80 collection: "Collection", 81 collections: [ 82 ["Lights", 10], 83 ["Rugs", 8], 84 ["Tables", 25], 85 ["Top Picks", 3], 86 ], 87 tags: "Tags", 88 tagItems: ["Minimal", "Standing", "Wood", "Home Office"], 89 kicker: "AI Search for ecommerce discovery", 90 title: "Understand shopper intent", 91 description: 92 "Ask naturally. The AI understands intent, context and desired atmosphere instead of relying on keyword matching.", 93 summaryLabel: "SUMMARY", 94 summaryText: 95 "The Nordic Wool Rug is the best match for a soft, cozy living-room feel because itâs plush, tufted wool, and has a cushioned pile that should add warmth underfoot. If you want other similarly inviting options, the Handwoven Cozy Rug offers a very soft sink-in feel, which can also make the room feel more comfortable and lived-in.", 96 searchIcon: "â", 97 searchButton: "Search", 98 }; 99 100 const I18N_URL = 101 document.currentScript && document.currentScript.src 102 ? new URL("i18n.json", document.currentScript.src).href 103 : "/window/i18n.json"; 104 105 let cachedI18n = null; 106 let didLoadI18n = false; 107 108 function detectLocale() { 109 const segments = window.location.pathname 110 .split("/") 111 .map((part) => part.toLowerCase()) 112 .filter(Boolean); 113 114 if (segments.includes("ja")) return "ja"; 115 if (segments.includes("en")) return "en"; 116 if (segments.includes("zh")) return "zh"; 117 118 return "zh"; 119 } 120 121 function loadI18n() { 122 if (didLoadI18n) return cachedI18n; 123 didLoadI18n = true; 124 125 try { 126 const request = new XMLHttpRequest(); 127 request.open("GET", I18N_URL, false); 128 request.overrideMimeType("application/json"); 129 request.send(null); 130 131 if (request.status >= 200 && request.status < 300) { 132 cachedI18n = JSON.parse(request.responseText); 133 } 134 } catch (error) { 135 cachedI18n = null; 136 } 137 138 return cachedI18n; 139 } 140 141 function getCopy(options) { 142 const locale = (options && options.locale) || detectLocale(); 143 const section = (loadI18n() || {})[locale] || {}; 144 const copy = section.esearch || {}; 145 146 return { 147 ...DEFAULT_COPY, 148 ...copy, 149 tabs: copy.tabs || DEFAULT_COPY.tabs, 150 collections: copy.collections || DEFAULT_COPY.collections, 151 tagItems: copy.tagItems || DEFAULT_COPY.tagItems, 152 heroProducts: copy.heroProducts || DEFAULT_COPY.heroProducts, 153 searchResults: copy.searchResults || DEFAULT_COPY.searchResults, 154 }; 155 } 156 157 function injectStyles() { 158 if (document.getElementById("ai-search-demo-style")) return; 159 160 const style = document.createElement("style"); 161 style.id = "ai-search-demo-style"; 162 style.textContent = ` 163 .ai-search-demo { 164 --bg:#fff; 165 --panel:#f7f7f8; 166 --text:#111827; 167 --muted:#64748b; 168 --soft:#eef2f7; 169 --border:#d9e2ec; 170 --accent:#111827; 171 --accent-soft:#f1f5f9; 172 --card:#fff; 173 --shadow:rgba(15,23,42,.08); 174 --image-bg:#f8fafc; 175 176 width:100%; 177 max-width:1180px; 178 height:680px; 179 background:var(--bg); 180 color:var(--text); 181 border:1px solid var(--border); 182 border-radius:24px; 183 overflow:auto; 184 box-shadow:0 26px 90px var(--shadow); 185 font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; 186 position:relative; 187 } 188 189 .ai-search-restart { 190 position:absolute; 191 z-index:20; 192 top:14px; 193 right:14px; 194 display:grid; 195 place-items:center; 196 min-width:0; 197 width:34px; 198 height:34px; 199 padding:0; 200 border:1px solid var(--border); 201 border-radius:999px; 202 background:rgba(255,255,255,.82); 203 color:var(--accent); 204 box-shadow:0 10px 26px var(--shadow); 205 cursor:pointer; 206 opacity:0; 207 pointer-events:none; 208 transition:opacity .18s ease, transform .18s ease; 209 } 210 211 .ai-search-restart img { 212 width:16px; 213 height:16px; 214 display:block; 215 object-fit:contain; 216 } 217 218 .ai-search-restart.show { 219 opacity:1; 220 pointer-events:auto; 221 } 222 223 .ai-search-restart:hover { 224 transform:translateY(-1px); 225 } 226 227 .ai-search-stage { 228 height:100%; 229 position:relative; 230 background:radial-gradient(circle at 50% 0%, var(--accent-soft), transparent 38%), var(--bg); 231 } 232 233 .ai-search-hero { 234 height:100%; 235 display:flex; 236 align-items:flex-start; 237 justify-content:center; 238 padding:42px; 239 } 240 241 .ai-search-panel { 242 width:min(760px,100%); 243 text-align:center; 244 } 245 246 .ai-search-kicker { 247 display:inline-flex; 248 gap:8px; 249 align-items:center; 250 padding:8px 12px; 251 border:1px solid var(--border); 252 border-radius:999px; 253 color:var(--muted); 254 background:rgba(255,255,255,.72); 255 font-size:13px; 256 margin-bottom:20px; 257 } 258 259 .ai-search-kicker::before { 260 content:""; 261 width:8px; 262 height:8px; 263 border-radius:99px; 264 background:var(--accent); 265 } 266 267 .ai-search-title { 268 font-family:Georgia,"Times New Roman",serif; 269 font-size:clamp(44px,6vw,76px); 270 line-height:.98; 271 font-weight:400; 272 margin:0 0 18px; 273 letter-spacing:-.04em; 274 } 275 276 .ai-search-desc { 277 color:var(--muted); 278 margin:0 auto 28px; 279 max-width:520px; 280 line-height:1.65; 281 } 282 283 .ai-search-box { 284 display:flex; 285 align-items:center; 286 gap:12px; 287 min-height:64px; 288 background:var(--card); 289 border:1px solid var(--border); 290 border-radius:22px; 291 padding:10px 12px 10px 20px; 292 box-shadow:0 18px 60px var(--shadow); 293 text-align:left; 294 } 295 296 .ai-search-icon { 297 color:var(--muted); 298 font-size:20px; 299 } 300 301 .ai-search-query { 302 flex:1; 303 font-size:16px; 304 color:var(--text); 305 min-width:0; 306 min-height:24px; 307 line-height:1.5; 308 overflow:hidden; 309 text-overflow:ellipsis; 310 white-space:nowrap; 311 } 312 313 .ai-search-caret { 314 display:inline-block; 315 width:8px; 316 height:18px; 317 margin-left:2px; 318 background:var(--accent); 319 vertical-align:-3px; 320 animation:aiSearchBlink 1s infinite; 321 } 322 323 .ai-search-button { 324 border:0; 325 background:var(--accent); 326 color:#fff; 327 border-radius:16px; 328 min-width:106px; 329 height:44px; 330 padding:0 18px; 331 font-weight:750; 332 cursor:default; 333 transition:transform .16s ease; 334 } 335 336 .ai-fake-cursor { 337 position:absolute; 338 width:30px; 339 height:30px; 340 object-fit:contain; 341 z-index:9999; 342 pointer-events:none; 343 opacity:0; 344 left:var(--cursor-start-x, 30%); 345 top:var(--cursor-start-y, 70%); 346 transform:translate(-4px, -4px); 347 filter:drop-shadow(0 4px 10px rgba(0,0,0,.18)); 348 } 349 350 .ai-fake-cursor.move { 351 opacity:1; 352 transition: 353 left 1.05s cubic-bezier(.22,.61,.36,1), 354 top 1.05s cubic-bezier(.22,.61,.36,1); 355 left:var(--cursor-end-x); 356 top:var(--cursor-end-y); 357 } 358 359 .ai-fake-cursor.tracking { 360 opacity:1; 361 transition:none; 362 } 363 364 .ai-fake-cursor.click { 365 transform:translate(-4px, -4px) scale(.86); 366 } 367 368 .ai-search-loader { 369 position:absolute; 370 top:0; 371 left:0; 372 width:100%; 373 height:100%; 374 background:rgba(255,255,255,.78); 375 -webkit-backdrop-filter:blur(10px); 376 backdrop-filter:blur(10px); 377 display:none; 378 align-items:center; 379 justify-content:center; 380 z-index:12; 381 } 382 383 .ai-search-loader.show { 384 display:flex; 385 } 386 387 .ai-search-spinner { 388 width:56px; 389 height:56px; 390 border-radius:999px; 391 border:4px solid var(--border); 392 border-top-color:var(--accent); 393 animation:aiSearchSpin .8s linear infinite; 394 } 395 396 .ai-results-page { 397 display:none; 398 height:100%; 399 background:var(--bg); 400 overflow:hidden; 401 padding:20px; 402 } 403 404 .ai-results-page.show { 405 display:flex; 406 flex-direction:column; 407 animation:aiSearchFade .38s ease both; 408 } 409 410 .ai-results-chrome {
411 flex:1; 412 min-height:0; 413 display:flex; 414 flex-direction:column; 415 border:1px solid var(--border); 416 border-radius:18px; 417 background:linear-gradient(180deg, rgba(255,255,255,.92), var(--card)); 418 overflow:hidden; 419 } 420 421 .ai-results-header { 422 display:flex; 423 align-items:center; 424 justify-content:space-between; 425 gap:18px; 426 padding:18px 20px; 427 border-bottom:1px solid var(--border); 428 background:rgba(255,255,255,.72); 429 } 430 431 .ai-results-title-wrap { 432 min-width:0; 433 } 434 435 .ai-results-eyebrow { 436 color:var(--muted); 437 font-size:12px; 438 font-weight:800; 439 letter-spacing:.12em; 440 text-transform:uppercase; 441 margin-bottom:6px; 442 } 443 444 .ai-results-title { 445 font-family:Georgia,"Times New Roman",serif; 446 font-size:32px; 447 line-height:1.04; 448 font-weight:400; 449 margin:0 0 14px; 450 letter-spacing:0; 451 white-space:nowrap; 452 } 453 454 .ai-results-page > .ai-results-title { 455 flex:0 0 auto; 456 padding:18px 20px 0; 457 } 458 459 .ai-results-toolbar { 460 flex:0 0 auto; 461 display:flex; 462 align-items:center; 463 justify-content:space-between; 464 gap:12px; 465 margin:0 20px 14px; 466 padding:14px; 467 border:1px solid var(--border); 468 border-radius:18px; 469 background:var(--panel); 470 } 471 472 .ai-result-tabs { 473 display:flex; 474 gap:8px; 475 flex:0 0 auto; 476 min-width:0; 477 } 478 479 .ai-tab { 480 height:36px; 481 padding:0 12px; 482 display:inline-flex; 483 align-items:center; 484 gap:7px; 485 flex:0 0 auto; 486 border:1px solid var(--border); 487 border-radius:999px; 488 background:var(--card); 489 color:var(--muted); 490 font-weight:750; 491 font-size:12px; 492 white-space:nowrap; 493 } 494 495 .ai-tab.active { 496 color:var(--accent); 497 background:var(--accent-soft); 498 } 499 500 .ai-count { 501 flex:0 0 auto; 502 background:var(--soft); 503 border-radius:999px; 504 padding:2px 8px; 505 font-size:11px; 506 } 507 508 .ai-results-searchbar { 509 display:flex; 510 align-items:center; 511 gap:10px; 512 min-height:52px; 513 margin:16px 18px 14px; 514 padding:0 14px; 515 border:1px solid var(--border); 516 border-radius:16px; 517 background:var(--panel); 518 color:var(--text); 519 } 520 521 .ai-results-searchbar strong { 522 flex:1; 523 min-width:0; 524 overflow:hidden; 525 text-overflow:ellipsis; 526 white-space:nowrap; 527 font-size:14px; 528 } 529 530 .ai-results-searchbar span { 531 color:var(--muted); 532 font-size:12px; 533 white-space:nowrap; 534 } 535 536 .ai-sortline { 537 min-width:0; 538 display:flex; 539 align-items:center; 540 justify-content:flex-end; 541 gap:8px; 542 color:var(--muted); 543 font-size:12px; 544 } 545 546 .ai-sortline > strong, 547 .ai-sortline > span:last-child { 548 display:none; 549 } 550 551 .ai-results-controls { 552 display:flex; 553 align-items:center; 554 justify-content:space-between; 555 gap:12px; 556 padding:0 18px 16px; 557 } 558 559 .ai-filter-chips { 560 display:flex; 561 gap:8px; 562 min-width:0; 563 overflow:hidden; 564 } 565 566 .ai-chip { 567 flex:0 0 auto; 568 height:32px; 569 display:inline-flex; 570 align-items:center; 571 border:1px solid var(--border); 572 border-radius:999px; 573 padding:0 11px; 574 color:var(--muted); 575 background:var(--card); 576 font-size:12px; 577 font-weight:750; 578 } 579 580 .ai-select { 581 min-width:126px; 582 height:34px; 583 border:1px solid var(--border); 584 border-radius:999px; 585 background:var(--card); 586 display:flex; 587 align-items:center; 588 justify-content:space-between; 589 gap:8px; 590 padding:0 12px; 591 color:var(--text); 592 font-weight:700; 593 font-size:12px; 594 flex:0 0 auto; 595 } 596 597 .ai-layout-buttons { 598 display:none; 599 overflow:hidden; 600 border:1px solid var(--border); 601 border-radius:999px; 602 background:var(--card); 603 flex:0 0 auto; 604 } 605 606 .ai-layout-buttons span { 607 width:34px; 608 height:34px; 609 display:grid; 610 place-items:center; 611 color:var(--muted); 612 transition:background .16s ease, color .16s ease; 613 } 614 615 .ai-layout-buttons span.active { 616 color:var(--accent); 617 background:var(--accent-soft); 618 } 619 620 .ai-results-main { 621 flex:1; 622 min-height:0; 623 overflow:auto; 624 padding:0 18px 18px; 625 } 626 627 .ai-summary { 628 border:1px solid var(--border); 629 background:linear-gradient(180deg, rgba(255,255,255,.95), var(--card)); 630 border-radius:16px; 631 padding:14px 16px; 632 margin-bottom:14px; 633 box-shadow:0 12px 32px var(--shadow); 634 } 635 636 .ai-summary-label { 637 color:var(--accent); 638 font-size:11px; 639 font-weight:850; 640 letter-spacing:.12em; 641 margin-bottom:8px; 642 } 643 644 .ai-summary-text { 645 color:var(--text); 646 font-size:14px; 647 line-height:1.62; 648 margin:0; 649 } 650 651 .ai-sidebar { 652 display:none; 653 } 654 655 .ai-product-grid { 656 display:grid; 657 grid-template-columns:repeat(3,minmax(0,1fr)); 658 gap:10px; 659 } 660 661 .ai-product { 662 border:1px solid var(--border); 663 background:var(--card); 664 min-height:224px; 665 padding:10px; 666 border-radius:14px; 667 overflow:hidden; 668 display:flex; 669 flex-direction:column; 670 transition:transform .18s ease, box-shadow .18s ease, border-color .18s ease; 671 } 672 673 .ai-product:hover { 674 transform:translateY(-3px); 675 border-color:var(--accent); 676 box-shadow:0 16px 38px var(--shadow); 677 } 678 679 .ai-product-image { 680 width:100%; 681 flex:0 0 auto; 682 aspect-ratio:4 / 3; 683 min-height:86px; 684 background:var(--image-bg); 685 border:2px solid rgba(255,255,255,.85); 686 border-radius:12px; 687 display:grid; 688 place-items:center; 689 overflow:hidden; 690 margin-bottom:10px; 691 } 692 693 .ai-product-image img { 694 width:calc(100% - 12px); 695 height:calc(100% - 12px); 696 object-fit:contain; 697 display:block; 698 border-radius:10px; 699 } 700 701 .ai-swatches { 702 display:flex; 703 gap:6px; 704 margin-bottom:12px; 705 } 706 707 .ai-swatch { 708 width:28px; 709 height:20px; 710 border-radius:8px; 711 background:var(--card); 712 border:1px solid var(--border); 713 display:grid; 714 place-items:center; 715 } 716 717 .ai-swatch img { 718 width:24px; 719 height:16px; 720 object-fit:cover; 721 display:block; 722 border-radius:6px; 723 } 724 725 .ai-brand { 726 color:var(--muted); 727 letter-spacing:.12em; 728 font-size:9px; 729 margin-bottom:6px; 730 min-height:11px; 731 white-space:nowrap; 732 overflow:hidden; 733 text-overflow:ellipsis; 734 } 735 736 .ai-name { 737 font-family:Georgia,"Times New Roman",serif; 738 font-size:15px; 739 line-height:1.18; 740 min-height:36px; 741 margin-bottom:10px; 742 overflow-wrap:anywhere; 743 display:-webkit-box; 744 -webkit-line-clamp:2; 745 -webkit-box-orient:vertical; 746 overflow:hidden; 747 } 748 749 .ai-product-bottom { 750 display:flex; 751 justify-content:space-between; 752 align-items:baseline; 753 gap:5px; 754 margin-top:auto; 755 } 756 757 .ai-product-bottom span { 758 color:var(--muted); 759 font-size:12px; 760 font-weight:700; 761 } 762 763 .ai-product-bottom strong { 764 font-size:16px; 765 } 766 767 .ai-preview-grid { 768 margin-top:24px; 769 display:grid; 770 grid-template-columns:repeat(auto-fit,minmax(132px,1fr)); 771 gap:10px; 772 } 773 774 .ai-preview-grid .ai-product { 775 min-height:174px; 776 padding:8px; 777 border-radius:12px; 778 } 779 780 .ai-preview-grid .ai-product-image { 781 aspect-ratio:16 / 9; 782 min-height:0; 783 height:auto; 784 margin-bottom:8px; 785 } 786 787 .ai-preview-grid .ai-swatches { 788 display:none; 789 } 790 791 .ai-preview-grid .ai-brand { 792 font-size:8px; 793 min-height:10px; 794 margin-bottom:5px; 795 } 796 797 .ai-preview-grid .ai-name { 798 font-size:13px; 799 line-height:1.16; 800 min-height:31px; 801 margin-bottom:8px; 802 } 803 804 .ai-preview-grid .ai-product-bottom { 805 min-width:0; 806 } 807 808 .ai-preview-grid .ai-product-bottom span { 809 font-size:10px; 810 min-width:0; 811 overflow:hidden; 812 text-overflow:ellipsis; 813 white-space:nowrap; 814 } 815 816 .ai-preview-grid .ai-product-bottom strong { 817 font-size:13px; 818 flex:0 0 auto; 819 white-space:nowrap; 820 } 821 822 .ai-tab{ 823 cursor:pointer; 824 } 825 826 .ai-select{ 827 cursor:pointer; 828 } 829 830 .ai-layout-buttons span{ 831 cursor:pointer; 832 } 833 834 .ai-product{ 835 cursor:pointer; 836 } 837 838 @keyframes aiSearchBlink { 839 50% { opacity:0; } 840 } 841 842 @keyframes aiSearchSpin { 843 to { transform:rotate(360deg); } 844 } 845 846 @keyframes aiSearchFade { 847 from { opacity:0; transform:translateY(8px); } 848 to { opacity:1; transform:translateY(0); } 849 } 850 851 @media (max-width:991px) { 852 .ai-layout-buttons { 853 display:none; 854 pointer-events:none; 855 } 856 } 857
858 @media (max-width:900px) { 859 .ai-search-demo { height:720px; } 860 861 .ai-preview-grid { 862 grid-template-columns:repeat(4,minmax(0,1fr)); 863 } 864 865 .ai-preview-grid .ai-product-image { 866 min-height:48px; 867 } 868 869 .ai-product-grid { 870 grid-template-columns:repeat(2,minmax(0,1fr)); 871 } 872 } 873 874 @media (max-width:560px) { 875 .ai-search-demo { 876 border-radius:18px; 877 height:700px; 878 } 879 880 .ai-search-hero { 881 padding:22px; 882 } 883 884 .ai-search-title { 885 font-size:44px; 886 } 887 888 .ai-search-button { 889 min-width:78px; 890 } 891 892 .ai-results-page { 893 padding:12px; 894 } 895 896 .ai-results-header { 897 align-items:flex-start; 898 flex-direction:column; 899 gap:12px; 900 padding:16px; 901 } 902 903 .ai-result-tabs { 904 width:100%; 905 max-width:100%; 906 overflow-x:auto; 907 overflow-y:hidden; 908 padding-bottom:2px; 909 -webkit-overflow-scrolling:touch; 910 scrollbar-width:none; 911 } 912 913 .ai-result-tabs::-webkit-scrollbar {
914 display:none; 915 } 916 917 .ai-tab { 918 min-width:max-content; 919 } 920 921 .ai-results-toolbar { 922 align-items:flex-start; 923 flex-direction:column; 924 margin-left:12px; 925 margin-right:12px; 926 } 927 928 .ai-sortline { 929 width:100%; 930 justify-content:space-between; 931 } 932 933 .ai-results-title { 934 white-space:normal; 935 font-size:28px; 936 } 937 938 .ai-results-controls { 939 align-items:flex-start; 940 flex-direction:column; 941 } 942 943 .ai-filter-chips { 944 width:100%; 945 overflow-x:auto; 946 } 947 948 .ai-preview-grid { 949 grid-template-columns:repeat(2,minmax(0,1fr)); 950 gap:8px; 951 margin-top:18px; 952 } 953 954 .ai-preview-grid .ai-product-image { 955 min-height:58px; 956 } 957 958 .ai-preview-grid .ai-product:nth-child(n+5) { 959 display:none; 960 } 961 962 .ai-product-grid { 963 grid-template-columns:1fr; 964 } 965 } 966 `; 967 968 document.head.appendChild(style); 969 } 970 971 function el(tag, className, text) { 972 const node = document.createElement(tag); 973 if (className) node.className = className; 974 if (typeof text === "string") node.textContent = text; 975 return node; 976 } 977 978 function applyTheme(root, themeName) { 979 const theme = THEMES[themeName] || THEMES["blue-white"]; 980 981 Object.keys(theme).forEach((key) => { 982 const cssKey = 983 "--" + key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()); 984 root.style.setProperty(cssKey, theme[key]); 985 }); 986 } 987 988 function sleep(ms) { 989 return new Promise((resolve) => setTimeout(resolve, ms)); 990 } 991 992 function syncLoaderToDemoViewport(loader) { 993 const root = loader.closest(".ai-search-demo"); 994 if (!root) return; 995 996 loader.style.top = root.scrollTop + "px"; 997 loader.style.left = root.scrollLeft + "px"; 998 loader.style.width = root.clientWidth + "px"; 999 loader.style.height = root.clientHeight + "px"; 1000 } 1001 1002 function getSearchButtonPoint(button) { 1003 const root = button.closest(".ai-search-demo"); 1004 const buttonRect = button.getBoundingClientRect(); 1005 const rootRect = root.getBoundingClientRect(); 1006 1007 return { 1008 x: buttonRect.left - rootRect.left + root.scrollLeft + 16, 1009 y: buttonRect.top - rootRect.top + root.scrollTop + buttonRect.height * 0.5, 1010 }; 1011 } 1012 1013 function setCursorPoint(cursor, point) { 1014 cursor.style.setProperty("--cursor-end-x", point.x + "px"); 1015 cursor.style.setProperty("--cursor-end-y", point.y + "px"); 1016 cursor.style.left = point.x + "px"; 1017 cursor.style.top = point.y + "px"; 1018 } 1019 1020 function trackCursorToButton(cursor, button, duration) { 1021 const startTime = performance.now(); 1022 const root = button.closest(".ai-search-demo"); 1023 const start = { 1024 x: root.scrollLeft + root.clientWidth * 0.28, 1025 y: root.scrollTop + root.clientHeight * 0.72, 1026 }; 1027 1028 cursor.style.setProperty("--cursor-start-x", start.x + "px"); 1029 cursor.style.setProperty("--cursor-start-y", start.y + "px"); 1030 cursor.style.left = start.x + "px"; 1031 cursor.style.top = start.y + "px"; 1032 cursor.classList.add("tracking"); 1033 1034 return new Promise((resolve) => { 1035 function step(now) { 1036 const target = getSearchButtonPoint(button); 1037 const progress = Math.min((now - startTime) / duration, 1); 1038 const eased = 1 - Math.pow(1 - progress, 3); 1039 1040 setCursorPoint(cursor, { 1041 x: start.x + (target.x - start.x) * eased, 1042 y: start.y + (target.y - start.y) * eased, 1043 }); 1044 1045 if (progress < 1) { 1046 requestAnimationFrame(step); 1047 return; 1048 } 1049 1050 setCursorPoint(cursor, getSearchButtonPoint(button)); 1051 resolve(); 1052 } 1053 1054 requestAnimationFrame(step); 1055 }); 1056 } 1057 1058 function deskSvg(type) { 1059 const top = 1060 type === "desk-b" || type === "desk-c" 1061 ? "#caa978" 1062 : type === "desk-f" 1063 ? "#9b7652" 1064 : "#d9c4a4"; 1065 1066 const leg = 1067 type === "desk-b" || type === "desk-c" 1068 ? "#d9dde2" 1069 : type === "desk-d" 1070 ? "#b99769" 1071 : "#c8aa7c"; 1072 1073 const extra = 1074 type === "desk-b" 1075 ? '<rect x="58" y="72" width="104" height="9" rx="2" fill="#111827" opacity=".88"/>' 1076 : ""; 1077 1078 return ` 1079 <svg viewBox="0 0 220 150" aria-hidden="true"> 1080 <ellipse cx="110" cy="128" rx="86" ry="10" fill="#dbe2ea" opacity=".6"/> 1081 <path d="M28 54 L170 43 L202 56 L58 72 Z" fill="${top}"/> 1082 <path d="M58 72 L202 56 L202 62 L58 78 Z" fill="#b89463" opacity=".55"/> 1083 ${extra} 1084 <path d="M52 75 L62 75 L54 124 L43 124 Z" fill="${leg}"/> 1085 <path d="M184 66 L194 65 L198 124 L187 124 Z" fill="${leg}"/> 1086 <path d="M62 112 L188 112 L188 119 L60 119 Z" fill="${leg}" opacity=".7"/> 1087 </svg> 1088 `; 1089 } 1090 1091 function iconGrid() { 1092 return ` 1093 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 1094 <rect x="4" y="4" width="6" height="6"/> 1095 <rect x="14" y="4" width="6" height="6"/> 1096 <rect x="4" y="14" width="6" height="6"/> 1097 <rect x="14" y="14" width="6" height="6"/> 1098 </svg> 1099 `; 1100 } 1101 1102 function iconList() { 1103 return ` 1104 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 1105 <rect x="4" y="6" width="4" height="4"/> 1106 <rect x="10" y="7" width="10" height="2"/> 1107 <rect x="4" y="14" width="4" height="4"/> 1108 <rect x="10" y="15" width="10" height="2"/> 1109 </svg> 1110 `; 1111 } 1112 1113 function appendProductImage(target, product, decorative) { 1114 const src = PRODUCT_IMAGES[product.img]; 1115 1116 if (!src) { 1117 return; 1118 } 1119 1120 const img = el("img"); 1121 img.src = src; 1122 img.alt = decorative ? "" : [product.brand, product.name].join(" "); 1123 img.loading = "lazy"; 1124 target.appendChild(img); 1125 } 1126 1127 async function streamQuery(textNode, button, cursor, loader, hero, results, copy, restartButton) { 1128 textNode.textContent = ""; 1129 1130 const caret = el("span", "ai-search-caret"); 1131 textNode.appendChild(caret); 1132 1133 for (let i = 0; i <= copy.query.length; i++) { 1134 textNode.textContent = copy.query.slice(0, i); 1135 textNode.appendChild(caret); 1136 await sleep(34 + Math.random() * 22); 1137 } 1138 1139 caret.remove(); 1140 1141 await sleep(450); 1142 1143 await trackCursorToButton(cursor, button, 1100); 1144 setCursorPoint(cursor, getSearchButtonPoint(button)); 1145 1146 cursor.classList.add("click"); 1147 button.style.transform = "scale(.97)"; 1148 1149 await sleep(180); 1150 1151 button.style.transform = ""; 1152 cursor.classList.remove("click"); 1153 cursor.classList.remove("tracking"); 1154 1155 const root = loader.closest(".ai-search-demo"); 1156 const syncLoader = () => syncLoaderToDemoViewport(loader); 1157 1158 syncLoader(); 1159 if (root) { 1160 root.addEventListener("scroll", syncLoader, { passive: true }); 1161 } 1162 1163 loader.classList.add("show"); 1164 1165 await sleep(1350); 1166 1167 if (root) { 1168 root.removeEventListener("scroll", syncLoader); 1169 } 1170 1171 hero.style.display = "none"; 1172 loader.classList.remove("show"); 1173 results.classList.add("show"); 1174 restartButton.classList.add("show"); 1175 } 1176 1177 function makeProduct(product, index, copy) { 1178 const card = el("div", "ai-product"); 1179 1180 const image = el("div", "ai-product-image"); 1181 appendProductImage(image, product, false); 1182 1183 const swatches = el("div", "ai-swatches"); 1184 1185 const swatchOne = el("div", "ai-swatch"); 1186 appendProductImage(swatchOne, product, true); 1187 1188 const swatchTwo = el("div", "ai-swatch"); 1189 appendProductImage(swatchTwo, product, true); 1190 1191 swatches.appendChild(swatchOne); 1192 swatches.appendChild(swatchTwo); 1193 1194 card.appendChild(image); 1195 card.appendChild(swatches); 1196 card.appendChild(el("div", "ai-brand", product.brand)); 1197 card.appendChild(el("div", "ai-name", product.name)); 1198 1199 const bottom = el("div", "ai-product-bottom"); 1200 bottom.appendChild(el("span", "", copy.startingAt)); 1201 bottom.appendChild(el("strong", "", product.price)); 1202 card.appendChild(bottom); 1203 1204 card.addEventListener("click", () => { 1205 card.style.transform = "scale(.98)"; 1206 setTimeout(() => { 1207 card.style.transform = ""; 1208 }, 120); 1209 }); 1210 1211 return card; 1212 } 1213 1214 function buildResults(copy) { 1215 const page = el("div", "ai-results-page"); 1216 1217 page.appendChild(el("h1", "ai-results-title", copy.resultsTitle)); 1218 1219 const toolbar = el("div", "ai-results-toolbar"); 1220 1221 const tabs = el("div", "ai-result-tabs"); 1222
1223 copy.tabs.forEach(([name, count, active]) => { 1224 const tab = el("button", "ai-tab" + (active ? " active" : "")); 1225 tab.appendChild(el("span", "", name)); 1226 tab.appendChild(el("span", "ai-count", count)); 1227 tabs.appendChild(tab); 1228 tab.addEventListener("click", () => { 1229 document 1230 .querySelectorAll(".ai-tab") 1231 .forEach(t => t.classList.remove("active")); 1232 1233 tab.classList.add("active"); 1234 }); 1235 }); 1236 1237 const sortline = el("div", "ai-sortline"); 1238 sortline.appendChild(el("strong", "", copy.sortBy)); 1239 1240 const select = el("div", "ai-select"); 1241 select.appendChild(el("span", "", copy.relevance)); 1242 select.appendChild(el("span", "", "â")); 1243 sortline.appendChild(select); 1244 1245 const layout = el("div", "ai-layout-buttons"); 1246 1247 const grid = el("span", "active"); 1248 grid.innerHTML = iconGrid(); 1249 1250 const list = el("span"); 1251 list.innerHTML = iconList(); 1252 1253 grid.addEventListener("click", () => { 1254 productGrid.style.gridTemplateColumns = 1255 "repeat(3,minmax(0,1fr))"; 1256 grid.classList.add("active"); 1257 list.classList.remove("active"); 1258 }); 1259 1260 list.addEventListener("click", () => { 1261 productGrid.style.gridTemplateColumns = "1fr"; 1262 list.classList.add("active"); 1263 grid.classList.remove("active"); 1264 }); 1265 1266 1267 1268 layout.appendChild(grid); 1269 layout.appendChild(list); 1270 1271 sortline.appendChild(layout); 1272 sortline.appendChild(el("span", "", copy.resultsFound)); 1273 1274 toolbar.appendChild(tabs); 1275 toolbar.appendChild(sortline); 1276 1277 page.appendChild(toolbar); 1278 1279 const main = el("div", "ai-results-main"); 1280 1281 const sidebar = el("aside", "ai-sidebar"); 1282 sidebar.appendChild(el("h2", "ai-filter-title", copy.filters)); 1283 1284 const priceCard = el("div", "ai-filter-card"); 1285 1286 const priceHead = el("div", "ai-filter-head"); 1287 priceHead.appendChild(el("span", "", copy.price)); 1288 priceHead.appendChild(el("span", "", "-")); 1289 1290 priceCard.appendChild(priceHead); 1291 priceCard.appendChild(el("div", "ai-price-line")); 1292 1293 const inputs = el("div", "ai-price-inputs"); 1294 1295 const min = el("div"); 1296 min.appendChild(el("label", "", copy.min)); 1297 min.appendChild(el("div", "", "0")); 1298 1299 const max = el("div"); 1300 max.appendChild(el("label", "", copy.max)); 1301 max.appendChild(el("div", "", "1999")); 1302 1303 inputs.appendChild(min); 1304 inputs.appendChild(max); 1305 1306 priceCard.appendChild(inputs); 1307 priceCard.appendChild( 1308 el("div", "ai-range-note", copy.availableRange) 1309 ); 1310 1311 sidebar.appendChild(priceCard); 1312 1313 const collectionCard = el("div", "ai-filter-card"); 1314 1315 const collectionHead = el("div", "ai-filter-head"); 1316 collectionHead.appendChild(el("span", "", copy.collection)); 1317 collectionHead.appendChild(el("span", "", "-")); 1318 1319 collectionCard.appendChild(collectionHead); 1320 1321 copy.collections.forEach(([name, count]) => { 1322 const row = el("div", "ai-check"); 1323 row.appendChild(document.createTextNode(name + " ")); 1324 row.appendChild(el("b", "", String(count))); 1325 collectionCard.appendChild(row); 1326 }); 1327 1328 sidebar.appendChild(collectionCard); 1329 1330 const tagsCard = el("div", "ai-filter-card"); 1331 1332 const tagsHead = el("div", "ai-filter-head"); 1333 tagsHead.appendChild(el("span", "", copy.tags)); 1334 tagsHead.appendChild(el("span", "", "-")); 1335 1336 tagsCard.appendChild(tagsHead); 1337 1338 copy.tagItems.forEach((name) => { 1339 tagsCard.appendChild(el("div", "ai-check", name)); 1340 }); 1341 1342 sidebar.appendChild(tagsCard); 1343 1344 const summary = el("section", "ai-summary"); 1345 summary.appendChild(el("div", "ai-summary-label", copy.summaryLabel)); 1346 summary.appendChild(el("p", "ai-summary-text", copy.summaryText)); 1347 1348 const productGrid = el("div", "ai-product-grid"); 1349 1350 copy.searchResults.forEach((product, index) => { 1351 productGrid.appendChild( 1352 makeProduct( 1353 { 1354 brand: product[0], 1355 name: product[1], 1356 price: product[2], 1357 img: product[3], 1358 }, 1359 index, 1360 copy 1361 ) 1362 ); 1363 }); 1364 1365 main.appendChild(sidebar); 1366 main.appendChild(summary); 1367 main.appendChild(productGrid); 1368 1369 page.appendChild(main); 1370 1371 return page; 1372 } 1373 1374 function mount(target, options) { 1375 injectStyles(); 1376 1377 const container = 1378 typeof target === "string" ? document.querySelector(target) : target; 1379 1380 if (!container) { 1381 throw new Error("AISearchDemo: mount target not found."); 1382 } 1383 1384 const opts = options || {}; 1385 const copy = getCopy(opts); 1386 1387 const root = el("div", "ai-search-demo"); 1388 applyTheme(root, opts.theme || "blue-white"); 1389 1390 const restartButton = el("button", "ai-search-restart"); 1391 restartButton.type = "button"; 1392 restartButton.setAttribute("aria-label", "Replay demo"); 1393 const restartIcon = el("img"); 1394 restartIcon.src = "/images/replay.png"; 1395 restartIcon.alt = ""; 1396 restartIcon.draggable = false;
1397 restartButton.appendChild(restartIcon); 1398 restartButton.addEventListener("click", () => { 1399 container.innerHTML = ""; 1400 mount(container, opts); 1401 }); 1402 1403 const stage = el("div", "ai-search-stage"); 1404 const hero = el("div", "ai-search-hero"); 1405 1406 const panel = el("div", "ai-search-panel"); 1407 1408 panel.appendChild( 1409 el("div", "ai-search-kicker", copy.kicker) 1410 ); 1411 1412 panel.appendChild(el("h1", "ai-search-title", copy.title)); 1413 1414 panel.appendChild( 1415 el( 1416 "p", 1417 "ai-search-desc", 1418 copy.description 1419 ) 1420 ); 1421 1422 const searchBox = el("div", "ai-search-box"); 1423 searchBox.appendChild(el("div", "ai-search-icon", copy.searchIcon)); 1424 1425 const query = el("div", "ai-search-query"); 1426 searchBox.appendChild(query); 1427 1428 const button = el("button", "ai-search-button", copy.searchButton); 1429 searchBox.appendChild(button); 1430 1431 panel.appendChild(searchBox); 1432 const previewGrid = el("div", "ai-preview-grid"); 1433 1434 copy.heroProducts.forEach((product, index) => { 1435 previewGrid.appendChild( 1436 makeProduct( 1437 { 1438 brand: product[0], 1439 name: product[1], 1440 price: product[2], 1441 img: product[3], 1442 }, 1443 index, 1444 copy 1445 ) 1446 ); 1447 }); 1448 1449 panel.appendChild(previewGrid); 1450 hero.appendChild(panel); 1451 1452 const cursor = el("img", "ai-fake-cursor"); 1453 cursor.src = opts.cursorSrc || "/images/cursor.png"; 1454 cursor.alt = ""; 1455 cursor.draggable = false; 1456 1457 const loader = el("div", "ai-search-loader"); 1458 loader.appendChild(el("div", "ai-search-spinner")); 1459 1460 const results = buildResults(copy); 1461 1462 stage.appendChild(hero); 1463 stage.appendChild(cursor); 1464 stage.appendChild(loader); 1465 stage.appendChild(results); 1466 1467 root.appendChild(stage); 1468 root.appendChild(restartButton); 1469 1470 container.innerHTML = ""; 1471 container.appendChild(root); 1472 1473 const delay = typeof opts.delay === "number" ? opts.delay : 600; 1474 1475 setTimeout(() => { 1476 streamQuery(query, button, cursor, loader, hero, results, copy, restartButton); 1477 }, delay); 1478 1479 return { 1480 destroy() { 1481 container.innerHTML = ""; 1482 }, 1483 1484 restart() { 1485 container.innerHTML = ""; 1486 mount(container, opts); 1487 }, 1488 1489 setTheme(nextTheme) { 1490 applyTheme(root, nextTheme); 1491 }, 1492 }; 1493 } 1494 1495 window.AISearchDemo = { 1496 mount, 1497 }; 1498})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.