@luigisbox/luma 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE.md +90 -0
  2. package/README.md +615 -2
  3. package/a11y/audit.mjs +735 -0
  4. package/a11y/axe.mjs +73 -0
  5. package/a11y/report.mjs +347 -0
  6. package/a11y/run.mjs +256 -0
  7. package/a11y/wcag.mjs +233 -0
  8. package/bin/config.mjs +60 -0
  9. package/bin/luma.mjs +99 -0
  10. package/bin/paths.mjs +11 -0
  11. package/bin/tools.mjs +43 -0
  12. package/build/baseline.d.mts +8 -0
  13. package/build/baseline.mjs +167 -0
  14. package/build/tooling.mjs +102 -0
  15. package/build/vite.mjs +507 -0
  16. package/chrome/block.mjs +8 -0
  17. package/chrome/run.mjs +150 -0
  18. package/docs/ACCESSIBILITY.md +501 -0
  19. package/docs/INTEGRATION-PERFORMANCE.md +213 -0
  20. package/docs/PARITY.md +946 -0
  21. package/footprint/model.mjs +184 -0
  22. package/footprint/run.mjs +331 -0
  23. package/package.json +58 -4
  24. package/parity/alignment.mjs +109 -0
  25. package/parity/archive.mjs +223 -0
  26. package/parity/args.mjs +137 -0
  27. package/parity/behaviour.mjs +379 -0
  28. package/parity/compare.mjs +985 -0
  29. package/parity/engine.mjs +848 -0
  30. package/parity/hints.mjs +132 -0
  31. package/parity/hold.mjs +54 -0
  32. package/parity/init.mjs +703 -0
  33. package/parity/map.mjs +640 -0
  34. package/parity/owner.mjs +170 -0
  35. package/parity/pairing.mjs +110 -0
  36. package/parity/pixels.mjs +33 -0
  37. package/parity/probe.mjs +1239 -0
  38. package/parity/report.mjs +236 -0
  39. package/parity/run.mjs +267 -0
  40. package/snippet/early-hold.js +64 -0
  41. package/snippet/early-hold.min.js +1 -0
  42. package/src/analytics/chain.ts +137 -0
  43. package/src/analytics/collect.ts +64 -0
  44. package/src/analytics/datalayer.ts +67 -0
  45. package/src/analytics/from-ga4.ts +68 -0
  46. package/src/analytics/from-luma.ts +21 -0
  47. package/src/analytics/index.ts +24 -0
  48. package/src/analytics/outbox.ts +118 -0
  49. package/src/analytics/page.ts +46 -0
  50. package/src/analytics/pageview.ts +82 -0
  51. package/src/analytics/ref.ts +19 -0
  52. package/src/analytics/setup.ts +41 -0
  53. package/src/analytics/start.ts +53 -0
  54. package/src/analytics/triggers.ts +239 -0
  55. package/src/analytics/user.ts +12 -0
  56. package/src/analytics/wire.ts +67 -0
  57. package/src/api/ai.ts +98 -0
  58. package/src/api/index.ts +87 -0
  59. package/src/api/token-key.ts +42 -0
  60. package/src/api/token.ts +312 -0
  61. package/src/core/a11y-keys.ts +60 -0
  62. package/src/core/a11y.ts +160 -0
  63. package/src/core/boot-state.ts +6 -0
  64. package/src/core/color.ts +42 -0
  65. package/src/core/context.ts +108 -0
  66. package/src/core/early-rules.ts +96 -0
  67. package/src/core/entries.ts +25 -0
  68. package/src/core/fold.ts +7 -0
  69. package/src/core/health.ts +26 -0
  70. package/src/core/hold.ts +74 -0
  71. package/src/core/i18n.ts +66 -0
  72. package/src/core/image-load.ts +102 -0
  73. package/src/core/image-tags.ts +26 -0
  74. package/src/core/images.ts +170 -0
  75. package/src/core/integration.ts +126 -0
  76. package/src/core/kept.ts +25 -0
  77. package/src/core/listings.ts +100 -0
  78. package/src/core/locales.ts +14 -0
  79. package/src/core/own.ts +49 -0
  80. package/src/core/page.ts +14 -0
  81. package/src/core/prefetch.ts +88 -0
  82. package/src/core/recommenders.ts +166 -0
  83. package/src/core/reconciler.ts +397 -0
  84. package/src/core/result.ts +73 -0
  85. package/src/core/routes.ts +115 -0
  86. package/src/core/scheduler.ts +83 -0
  87. package/src/core/sense.ts +107 -0
  88. package/src/core/speculation.ts +59 -0
  89. package/src/core/stash.ts +38 -0
  90. package/src/core/storage.ts +45 -0
  91. package/src/core/styles.ts +106 -0
  92. package/src/core/support.ts +12 -0
  93. package/src/core/takeover.ts +220 -0
  94. package/src/core/templates.ts +74 -0
  95. package/src/core/tiles.ts +137 -0
  96. package/src/core/turbo.ts +64 -0
  97. package/src/core/url.ts +164 -0
  98. package/src/core/widget-inits.ts +40 -0
  99. package/src/env.d.ts +3 -0
  100. package/src/lit.ts +11 -0
  101. package/src/locales/cs.json +94 -0
  102. package/src/locales/de.json +88 -0
  103. package/src/locales/en.json +88 -0
  104. package/src/main.ts +493 -0
  105. package/src/types.ts +778 -0
  106. package/src/widgets/autocomplete/index.ts +1387 -0
  107. package/src/widgets/autocomplete/style-autocomplete.css +266 -0
  108. package/src/widgets/autocomplete/template-autocomplete-box.ts +61 -0
  109. package/src/widgets/autocomplete/template-autocomplete-empty.ts +53 -0
  110. package/src/widgets/autocomplete/template-autocomplete-result-default.ts +82 -0
  111. package/src/widgets/autocomplete/template-autocomplete-sheet.ts +74 -0
  112. package/src/widgets/listing/element.ts +1772 -0
  113. package/src/widgets/listing/flow.ts +30 -0
  114. package/src/widgets/listing/instant.ts +133 -0
  115. package/src/widgets/listing/intent.ts +193 -0
  116. package/src/widgets/listing/query.ts +298 -0
  117. package/src/widgets/listing/stash.ts +97 -0
  118. package/src/widgets/listing/state.ts +131 -0
  119. package/src/widgets/listing/view.ts +257 -0
  120. package/src/widgets/plp/index.ts +11 -0
  121. package/src/widgets/plp/route.ts +263 -0
  122. package/src/widgets/recommender/index.ts +261 -0
  123. package/src/widgets/recommender/style-recommender.css +26 -0
  124. package/src/widgets/recommender/template-recommender-box-default.ts +11 -0
  125. package/src/widgets/recommender/template-recommender-popup.ts +12 -0
  126. package/src/widgets/recommender/template-recommender-result-default.ts +10 -0
  127. package/src/widgets/search/assets.ts +7 -0
  128. package/src/widgets/search/first.ts +15 -0
  129. package/src/widgets/search/form-url.ts +59 -0
  130. package/src/widgets/search/in-place.ts +226 -0
  131. package/src/widgets/search/index.ts +16 -0
  132. package/src/widgets/search/range.ts +260 -0
  133. package/src/widgets/search/route.ts +32 -0
  134. package/src/widgets/search/speculate.ts +202 -0
  135. package/src/widgets/search/style-search.css +65 -0
  136. package/src/widgets/search/template-search-box.ts +17 -0
  137. package/src/widgets/search/template-search-facets-default.ts +73 -0
  138. package/src/widgets/search/template-search-header.ts +14 -0
  139. package/src/widgets/search/template-search-pagination.ts +12 -0
  140. package/src/widgets/search/template-search-range-default.ts +48 -0
  141. package/src/widgets/search/template-search-result-default.ts +13 -0
  142. package/src/widgets/search/template-search-results-default.ts +8 -0
  143. package/tasks/model.mjs +116 -0
  144. package/tasks/run.mjs +305 -0
  145. package/tsconfig.json +14 -0
@@ -0,0 +1,266 @@
1
+ /* The house design: lives in the dropdown's shadow
2
+ root, so the host's CSS cannot reach it and ours cannot leak.
3
+
4
+ Tokens. Five are the site's, sampled from the page by index.ts (only where
5
+ they read on our white panel) as --lbx-s-*: font (the input's family), fg,
6
+ bg, accent (accentFrom), radius (the search field's). The rest are derived.
7
+ Each is a custom property on :host; `autocomplete.tokens` in the config
8
+ (a :host rule after this one) or the integration's style-autocomplete.css
9
+ (`:host { --lbx-line: #d0d0d0; }`) replaces any of them:
10
+ --lbx-font, --lbx-fg, --lbx-bg, --lbx-accent, --lbx-radius the site's
11
+ --lbx-on-accent text on an accent fill: #fff, or #0a0f12 on a light accent (index.ts)
12
+ --lbx-muted fg 66% in bg (secondary text)
13
+ --lbx-line fg 11% in bg (rules, outlines)
14
+ --lbx-soft fg 4% in bg (the list column, picture tiles)
15
+ --lbx-tint accent 10% in bg (pills at rest, selected cards)
16
+ --lbx-tint-strong accent 28% in bg (the help box's dashed edge)
17
+ --lbx-ok #1f7a35 (stock)
18
+ --lbx-radius-row r x 0.6 (rows, cards, buttons)
19
+ --lbx-radius-tile r x 0.5 (picture tiles)
20
+ The --lbx-c-* properties are the high-contrast mode's palette (core/a11y.ts),
21
+ inherited from the document: unset, every rule keeps its normal value. */
22
+ :host {
23
+ all: initial;
24
+ --lbx-font: var(--lbx-s-font, system-ui, sans-serif);
25
+ --lbx-fg: var(--lbx-s-fg, #1a1a1a);
26
+ --lbx-bg: var(--lbx-s-bg, #fff);
27
+ --lbx-accent: var(--lbx-s-accent, #1a1a1a);
28
+ --lbx-radius: var(--lbx-s-radius, 4px);
29
+ --lbx-on-accent: var(--lbx-s-on-accent, #fff);
30
+ --lbx-muted: color-mix(in srgb, var(--lbx-fg) 66%, var(--lbx-bg));
31
+ --lbx-line: color-mix(in srgb, var(--lbx-fg) 11%, var(--lbx-bg));
32
+ --lbx-soft: color-mix(in srgb, var(--lbx-fg) 4%, var(--lbx-bg));
33
+ --lbx-tint: color-mix(in srgb, var(--lbx-accent) 10%, var(--lbx-bg));
34
+ --lbx-tint-strong: color-mix(in srgb, var(--lbx-accent) 28%, var(--lbx-bg));
35
+ --lbx-ok: #1f7a35;
36
+ --lbx-radius-row: calc(var(--lbx-radius) * .6);
37
+ --lbx-radius-tile: calc(var(--lbx-radius) * .5);
38
+ }
39
+ /* The render root: the sheet fills the host (sized to the visual viewport); the panel is as tall as it is. */
40
+ .lbx-root { height: 100%; }
41
+ /* What the rules read: the tokens, or the high-contrast palette while it is on. */
42
+ .box, .sheet {
43
+ --c-fg: var(--lbx-c-fg, var(--lbx-fg));
44
+ --c-bg: var(--lbx-c-bg, var(--lbx-bg));
45
+ --c-muted: var(--lbx-c-muted, var(--lbx-muted));
46
+ --c-line: var(--lbx-c-line, var(--lbx-line));
47
+ --c-accent: var(--lbx-c-accent, var(--lbx-accent));
48
+ --c-fill: var(--lbx-c-sel-bg, var(--lbx-accent));
49
+ --c-on-fill: var(--lbx-c-sel-fg, var(--lbx-on-accent));
50
+ --c-ok: var(--lbx-c-fg, var(--lbx-ok));
51
+ --c-tint: var(--lbx-tint);
52
+ box-sizing: border-box; font-family: var(--lbx-font); font-size: 14px; line-height: 1.35; color: var(--c-fg);
53
+ }
54
+ .box *, .sheet *, .box *::before, .sheet *::after { box-sizing: border-box; }
55
+ h3, p { margin: 0; font: inherit; }
56
+ a { color: inherit; text-decoration: none; }
57
+ button { font: inherit; color: inherit; background: none; border: 0; padding: 0; margin: 0; cursor: pointer; }
58
+ .sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
59
+
60
+ /* ---- the panel -------------------------------------------------------- */
61
+ .caret { display: block; position: relative; width: 14px; height: 14px; margin: 0 0 -7px var(--lbx-caret, 30px); transform: rotate(45deg); background: var(--c-fill); border-radius: 2px; }
62
+ .panel { position: relative; background: var(--c-bg); border-top: 3px solid var(--c-fill); border-radius: 0 0 var(--lbx-radius) var(--lbx-radius);
63
+ box-shadow: 0 30px 60px -20px rgb(30 42 51 / .28), 0 2px 8px rgb(30 42 51 / .06); overflow: hidden; }
64
+ .cols { display: flex; max-height: var(--lbx-max-h, 70vh); overflow: auto; }
65
+ .list { flex: none; width: 280px; padding: 20px 16px 16px; background: var(--lbx-soft); display: flex; flex-direction: column; gap: 18px; }
66
+ .only-list .list { width: 100%; }
67
+ .hero-col { flex: none; width: 310px; padding: 20px 24px 24px; border-right: 1px solid var(--c-line); display: flex; flex-direction: column; }
68
+ .prods { flex: 1; min-width: 0; padding: 20px 24px 86px; display: flex; flex-direction: column; }
69
+ .grp { display: flex; flex-direction: column; gap: 2px; }
70
+ .grp h3 { padding: 0 4px 6px; font-size: 15px; font-weight: 700; line-height: 1.3; color: var(--c-fg); }
71
+ .hero-col .grp, .prods .grp { gap: 12px; }
72
+ .hero-col .grp { flex: 1; }
73
+ .hero-col .grp h3, .prods .grp h3 { padding: 0; }
74
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
75
+ .pby { margin-top: auto; padding: 8px 4px 0; display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--c-muted); }
76
+ .logo { width: 14px; height: 14px; flex: none; border-radius: 3px; background: #35c2d6; display: flex; align-items: center; justify-content: center; }
77
+
78
+ /* Options: every one is the selection the arrow keys and the pointer move. */
79
+ .opt { display: flex; align-items: center; cursor: pointer; color: inherit; position: relative; }
80
+ /* On a selected option's tint the green and the accent are a step darker (towards the text colour): small
81
+ text such as the stock line can fall under 4.5:1 on an accent tint otherwise. */
82
+ .opt[aria-selected="true"] { --c-muted: var(--lbx-c-sel-fg, var(--lbx-muted)); --c-ok: var(--lbx-c-sel-fg, color-mix(in srgb, var(--lbx-ok) 75%, var(--lbx-fg)));
83
+ --c-accent: var(--lbx-c-sel-fg, color-mix(in srgb, var(--lbx-accent) 85%, var(--lbx-fg))); }
84
+ /* Query pills: selected, an accent fill. */
85
+ .pill { height: 32px; padding: 0 12px; border-radius: 999px; background: var(--c-tint); color: var(--c-fg); font-size: 14px; white-space: nowrap; max-width: 100%; }
86
+ .pill .t { overflow: hidden; text-overflow: ellipsis; }
87
+ .pill[aria-selected="true"] { background: var(--c-fill); color: var(--c-on-fill); }
88
+ /* List rows: one line; selected, the panel's background with an accent edge (WCAG 1.4.11: not a faint colour alone). */
89
+ .row { min-height: 32px; padding: 4px 8px 4px 4px; gap: 10px; border-radius: var(--lbx-radius-row); font-size: 14px; }
90
+ .row .t { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
91
+ .row[aria-selected="true"] { background: var(--lbx-c-sel-bg, var(--c-bg)); color: var(--lbx-c-sel-fg, inherit); box-shadow: inset 3px 0 0 var(--c-fill); }
92
+ /* Products: selected, the tint with an accent ring. */
93
+ .hero[aria-selected="true"], .card[aria-selected="true"], .line[aria-selected="true"], .mhero[aria-selected="true"], .mproduct[aria-selected="true"], .compact[aria-selected="true"] {
94
+ background: var(--lbx-c-sel-bg, var(--c-tint)); color: var(--lbx-c-sel-fg, inherit); box-shadow: inset 0 0 0 2px var(--c-fill); }
95
+ .tile { flex: none; display: flex; align-items: center; justify-content: center; border-radius: var(--lbx-radius-tile); background: var(--lbx-soft); overflow: hidden; }
96
+ .tile img, .pic img { width: 100%; height: 100%; object-fit: contain; }
97
+ .stock { display: flex; align-items: center; gap: 6px; font-size: 12px; min-width: 0; }
98
+ .dot { width: 7px; height: 7px; flex: none; border-radius: 999px; background: var(--c-ok); }
99
+ .avail { color: var(--c-ok); font-weight: 700; white-space: nowrap; }
100
+ .cat { color: var(--c-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
101
+ .prices { display: flex; gap: 6px; align-items: baseline; font-variant-numeric: tabular-nums; }
102
+ .now { font-size: 15px; font-weight: 800; }
103
+ .old { font-size: 12px; color: var(--c-muted); }
104
+ .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
105
+ .clamp, .card .t, .mproduct .t, .compact .t { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
106
+
107
+ /* The top product: the whole card is the option. */
108
+ .hero { flex: 1; flex-direction: column; align-items: stretch; gap: 12px; padding: 10px; margin: -10px; border-radius: var(--lbx-radius-row); }
109
+ .pic { position: relative; height: 230px; display: flex; align-items: center; justify-content: center; border-radius: var(--lbx-radius-tile); background: var(--lbx-soft); overflow: hidden; }
110
+ .v-spec .pic { height: 120px; }
111
+ .badge { position: absolute; top: 10px; left: 10px; height: 24px; padding: 0 10px; display: flex; align-items: center; border-radius: 999px; background: var(--c-fill); color: var(--c-on-fill); font-size: 12px; font-weight: 700; }
112
+ .hero .t { font-size: 17px; font-weight: 700; }
113
+ .hero .stock { font-size: 13px; gap: 8px; }
114
+ .hero .dot { width: 8px; height: 8px; }
115
+ .hero .prices { margin-top: auto; flex-direction: column-reverse; align-items: flex-start; gap: 0; }
116
+ .hero .now { font-size: 26px; letter-spacing: -.01em; }
117
+ .hero .old { font-size: 13px; }
118
+ .specs { display: flex; flex-direction: column; border-top: 1px solid var(--c-line); }
119
+ .sp { padding: 7px 0; display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--c-line); font-size: 13px; }
120
+ .specs .k { color: var(--c-muted); }
121
+ .specs .v { font-weight: 600; text-align: right; }
122
+
123
+ /* Product cards (grid, 2 columns) and product rows (list layout). */
124
+ .p-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 16px; }
125
+ .card { padding: 8px; gap: 14px; border-radius: var(--lbx-radius-row); }
126
+ .card .tile { width: 84px; height: 84px; }
127
+ .card .t { font-size: 14px; font-weight: 700; }
128
+ .p-list { display: flex; flex-direction: column; }
129
+ .line { display: grid; grid-template-columns: 48px minmax(0, 1fr) 110px 90px; gap: 14px; padding: 10px 8px; border-bottom: 1px solid var(--c-line); border-radius: var(--lbx-radius-row); }
130
+ .line .tile { width: 48px; height: 48px; }
131
+ .line .t { font-size: 14px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
132
+ .line .body { gap: 3px; }
133
+ .meta { font-size: 12px; color: var(--c-muted); font-variant-numeric: tabular-nums; }
134
+ .line .prices { justify-content: flex-end; flex-direction: column; align-items: flex-end; gap: 0; }
135
+
136
+ /* "Show all products": the PRODUCTS column's foot, outlined; filled when focused or hovered. Outside the listbox. */
137
+ .all { display: flex; align-items: center; justify-content: center; gap: 8px; height: 46px; border-radius: var(--lbx-radius-row); box-shadow: inset 0 0 0 1.5px var(--c-fill);
138
+ color: var(--c-accent); font-size: 15px; font-weight: 700; background: var(--c-bg); cursor: pointer; }
139
+ .all:hover, .all:focus-visible { background: var(--c-fill); color: var(--c-on-fill); }
140
+ .box .all { position: absolute; right: 24px; bottom: 20px; left: 24px; }
141
+ .box.has-list .all { left: 304px; }
142
+ .box.has-hero .all { left: 334px; }
143
+ .box.has-list.has-hero .all { left: 614px; }
144
+
145
+ /* @lbx-part autocomplete-empty: left out of a build whose integration replaces this template and uses none of its classes (build/vite.mjs) */
146
+ /* ---- no results: a layout of its own -------------------------- */
147
+ .msg-row { padding: 28px 32px 26px; display: flex; gap: 32px; align-items: stretch; }
148
+ .msg { flex: 1; min-width: 0; display: flex; gap: 20px; }
149
+ .ico { width: 56px; height: 56px; flex: none; display: flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--c-tint); color: var(--c-accent); }
150
+ .msg-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
151
+ .e-title { font-size: 22px; line-height: 1.3; font-weight: 800; letter-spacing: -.01em; color: var(--c-fg); }
152
+ .e-hint { font-size: 15px; line-height: 1.5; color: var(--c-muted); }
153
+ .e-opts { margin-top: 10px; display: flex; flex-direction: column; gap: 12px; }
154
+ .grp.inline { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; }
155
+ .grp.inline h3 { padding: 0; margin-right: 4px; font-size: 14px; }
156
+ .tries, .browse { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; }
157
+ .tries { gap: 8px; }
158
+ .try { height: 34px; padding: 0 14px; gap: 6px; border-radius: 999px; box-shadow: inset 0 0 0 1.5px var(--c-line); font-size: 14px; font-weight: 700; }
159
+ .try .glass { color: var(--c-accent); }
160
+ .try[aria-selected="true"] { background: var(--lbx-c-sel-bg, var(--c-tint)); color: var(--lbx-c-sel-fg, inherit); box-shadow: inset 0 0 0 1.5px var(--c-fill); }
161
+ .browse .chev { display: none; }
162
+ .opt.browse { padding: 2px 6px; margin: 0 -6px; border-radius: var(--lbx-radius-row); color: var(--c-accent); font-weight: 700; font-size: 14px; }
163
+ .opt.browse[aria-selected="true"] { background: var(--lbx-c-sel-bg, var(--c-tint)); text-decoration: underline; text-underline-offset: 3px; }
164
+ .help { width: 300px; flex: none; padding: 18px 20px; border-radius: var(--lbx-radius-row); border: 1.5px dashed var(--lbx-tint-strong); display: flex; gap: 14px; align-items: center; }
165
+ .phone-ico { width: 44px; height: 44px; flex: none; display: flex; align-items: center; justify-content: center; border-radius: 999px; border: 1.5px solid var(--c-accent); color: var(--c-accent); }
166
+ .help-text { display: flex; flex-direction: column; gap: 2px; }
167
+ .help-title { font-size: 14px; color: var(--c-accent); font-weight: 700; }
168
+ .phone { font-size: 22px; font-weight: 800; color: var(--c-accent); letter-spacing: .01em; }
169
+ .help-hours { font-size: 13px; color: var(--c-muted); }
170
+ .strip { padding: 18px 32px 22px; background: var(--lbx-soft); border-top: 1px dashed var(--c-line); display: flex; flex-direction: column; gap: 12px; }
171
+ .strip-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
172
+ .strip-head .pby { margin: 0; padding: 0; }
173
+ .note-wrap { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
174
+ .f-title { font-size: 14px; font-weight: 700; color: var(--c-muted); }
175
+ .f-note { font-size: 13px; color: var(--c-muted); }
176
+ .fb { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
177
+ .compact { padding: 8px; gap: 12px; border-radius: var(--lbx-radius-row); background: var(--c-bg); box-shadow: inset 0 0 0 1px var(--c-line); }
178
+ .compact .tile { width: 52px; height: 52px; }
179
+ .compact .body { gap: 3px; }
180
+ .compact .t, .compact .now { font-size: 13px; }
181
+ .compact .now { font-weight: 700; }
182
+ .e-foot { padding: 0 32px 16px; display: flex; justify-content: flex-end; }
183
+ .e-foot .pby { margin: 0; }
184
+
185
+ /* @lbx-part-end */
186
+ /* @lbx-part autocomplete-sheet: left out of a build whose integration replaces this template and uses none of its classes (build/vite.mjs) */
187
+ /* ---- the phone sheet --------------------------------------------------- */
188
+ .sheet { height: 100%; display: flex; flex-direction: column; overflow: hidden; background: var(--c-bg); border-top: 3px solid var(--c-fill); }
189
+ .sh-head { height: 60px; flex: none; padding: 0 8px 0 4px; display: flex; align-items: center; gap: 4px; border-bottom: 1px solid var(--c-line); touch-action: none; }
190
+ .icon { width: 44px; height: 44px; flex: none; display: flex; align-items: center; justify-content: center; border-radius: var(--lbx-radius-row); }
191
+ .x { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--lbx-soft); }
192
+ /* 17 px: iOS zooms into a field under 16 px. */
193
+ .sh-input { flex: 1; min-width: 0; height: 44px; border: 0; outline: 0; background: transparent; font: inherit; font-size: 17px; color: var(--c-fg); padding: 0 4px; }
194
+ .sh-phrases { flex: none; padding: 12px 16px; display: flex; gap: 8px; overflow-x: auto; background: var(--lbx-soft); touch-action: pan-x; }
195
+ .sheet .pill { height: 36px; padding: 0 14px; flex: none; font-size: 15px; }
196
+ .tabs { height: 48px; flex: none; padding: 0 8px; display: flex; overflow-x: auto; border-bottom: 1px solid var(--c-line); touch-action: pan-x; }
197
+ .tab { flex: none; padding: 0 14px; font-size: 15px; color: var(--c-muted); white-space: nowrap; }
198
+ .tab[aria-selected="true"] { color: var(--c-fg); font-weight: 700; box-shadow: inset 0 -3px 0 var(--c-fill); }
199
+ .sh-list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; touch-action: pan-y; }
200
+ .sh-list .pby { margin: 0; padding: 20px 16px 24px; justify-content: center; font-size: 13px; }
201
+ .m-hero { padding: 16px; gap: 12px; border-bottom: 1px solid var(--c-line); }
202
+ .m-hero h3 { padding: 0; font-size: 13px; color: var(--c-muted); }
203
+ .m-products { gap: 0; }
204
+ .m-products h3 { padding: 16px 16px 4px; font-size: 13px; color: var(--c-muted); }
205
+ .mhero { gap: 14px; align-items: flex-start; padding: 8px; margin: -8px; border-radius: var(--lbx-radius-row); }
206
+ .mhero .tile { width: 120px; height: 120px; }
207
+ .mhero .body { gap: 6px; }
208
+ .mhero .t { font-size: 16px; line-height: 1.3; font-weight: 700; }
209
+ .mhero .stock, .mproduct .stock { font-size: 13px; }
210
+ .mhero .now { font-size: 22px; }
211
+ .mproduct { padding: 12px 16px; gap: 14px; border-bottom: 1px solid var(--c-line); }
212
+ .mproduct .tile { width: 72px; height: 72px; }
213
+ .mproduct .t { font-size: 15px; font-weight: 700; }
214
+ .mproduct .now { font-size: 16px; }
215
+ .mrow { min-height: 56px; padding: 0 16px; gap: 12px; border-bottom: 1px solid var(--c-line); font-size: 16px; }
216
+ .mrow .t { flex: 1; min-width: 0; }
217
+ .mrow .chev, .opt.browse .chev { flex: none; color: var(--c-muted); }
218
+ .mrow[aria-selected="true"] { background: var(--lbx-c-sel-bg, var(--c-tint)); color: var(--lbx-c-sel-fg, inherit); box-shadow: inset 3px 0 0 var(--c-fill); }
219
+ .sh-foot { flex: none; padding: 10px 16px 14px; border-top: 1px solid var(--c-line); background: var(--c-bg); touch-action: none; }
220
+ .sheet .all { height: 48px; font-size: 16px; background: var(--c-fill); color: var(--c-on-fill); box-shadow: none; }
221
+ /* No results on the phone: one column, its own scroll. */
222
+ .sheet .empty { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; touch-action: pan-y; }
223
+ .sheet .msg-row { padding: 28px 20px 20px; flex-direction: column; gap: 20px; }
224
+ .sheet .msg { flex-direction: column; gap: 10px; }
225
+ .sheet .ico { width: 52px; height: 52px; }
226
+ .sheet .e-title { font-size: 21px; }
227
+ .sheet .grp.inline { flex-direction: column; align-items: stretch; }
228
+ .sheet .try { height: 40px; font-size: 15px; }
229
+ .sheet .browse { flex-direction: column; align-items: stretch; gap: 0; }
230
+ .sheet .opt.browse { min-height: 48px; margin: 0 -6px; padding: 0 6px; justify-content: space-between; font-size: 16px; }
231
+ .sheet .browse .chev { display: block; }
232
+ .sheet .help { width: auto; padding: 16px; }
233
+ .sheet .help-text { flex: 1; }
234
+ .sheet .help-title { font-size: 15px; }
235
+ .call { height: 44px; padding: 0 14px; display: flex; align-items: center; gap: 8px; border-radius: var(--lbx-radius-row); border: 1.5px solid var(--c-accent); color: var(--c-accent); font-size: 15px; font-weight: 700; }
236
+ .sheet .strip { padding: 16px 16px 20px; gap: 10px; }
237
+ .sheet .note-wrap { flex-direction: column; gap: 2px; }
238
+ .sheet .fb { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
239
+ .sheet .compact { flex-direction: column; align-items: stretch; gap: 6px; }
240
+ .sheet .compact .tile { width: auto; height: 72px; }
241
+ .sheet .compact .now { font-size: 14px; }
242
+ .sheet .strip .pby { margin: 0; padding-top: 8px; justify-content: center; font-size: 13px; }
243
+ .sheet .e-foot { padding: 0 16px 20px; justify-content: center; }
244
+
245
+ /* @lbx-part-end */
246
+ /* The sheet's and the panel's own scrolling never scrolls the page behind (where the browser has it). */
247
+ @supports (overscroll-behavior: contain) {
248
+ .cols, .sheet, .sh-list, .sheet .empty { overscroll-behavior: contain; }
249
+ }
250
+ @supports (scrollbar-width: none) {
251
+ .sh-phrases, .tabs { scrollbar-width: none; }
252
+ }
253
+
254
+ /* Windows contrast themes: backgrounds and shadows are the system's, so the
255
+ selection is drawn in system colours, and the panel keeps its edge. */
256
+ @media (forced-colors: active) {
257
+ .panel, .sheet { border: 1px solid CanvasText; border-top-width: 3px; }
258
+ .opt[aria-selected="true"], .tab[aria-selected="true"] { box-shadow: none; outline: 2px solid CanvasText; outline-offset: -2px; }
259
+ /* Filled in system colours where the browser lets us opt out of forcing
260
+ (forced-color-adjust is not Baseline): elsewhere the outline marks it. */
261
+ @supports (forced-color-adjust: none) {
262
+ .opt[aria-selected="true"] { forced-color-adjust: none; background: Highlight; color: HighlightText; }
263
+ .opt[aria-selected="true"] * { color: HighlightText; }
264
+ }
265
+ .all, .pill, .try, .compact { border: 1px solid CanvasText; }
266
+ }
@@ -0,0 +1,61 @@
1
+ import { html, nothing, repeat } from '../../lit';
2
+ import type { AutocompleteScope } from '../../types';
3
+
4
+ // The house design's panel: an accent
5
+ // bar and a caret under the search field; the list column (query pills, then
6
+ // one titled group per other type in config order, "Powered by Luigi's Box"),
7
+ // the top product, the products beside it, "Show all products". A listbox of
8
+ // options in labelled groups (WAI-ARIA APG combobox): the host's input is the
9
+ // combobox (index.ts), focus stays in it. "Show all products" (a link to the
10
+ // search page, the dropdown's one tab stop: data-lbx-all) is outside the
11
+ // listbox, as is the "No results" layout (autocomplete-empty). On a phone:
12
+ // autocomplete-sheet. No counts anywhere: the API returns none.
13
+
14
+ export const POWERED = (t: AutocompleteScope['t']) =>
15
+ html`<span class="pby" aria-hidden="true"><span class="logo"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span>${t('poweredBy')}</span>`;
16
+ export const ARROW = html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>`;
17
+
18
+ /** A titled group of options: its heading names it. */
19
+ export const group = (id: string, label: string, body: unknown, cls = '') =>
20
+ html`<div class=${`grp ${cls}`} role="group" data-lbx-part="ac-section" aria-labelledby=${`lbx-ac-g-${id}`}>
21
+ <h3 role="presentation" data-lbx-part="ac-title" id=${`lbx-ac-g-${id}`}>${label}</h3>${body}
22
+ </div>`;
23
+
24
+ export const showAll = (s: AutocompleteScope) =>
25
+ s.allHref
26
+ ? html`<a class="all" data-lbx-part="ac-all" data-lbx-all href=${s.allHref} @click=${(e: MouseEvent) => !(e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) && (e.preventDefault(), s.submitAll())}>${s.t('showAllProducts')}${ARROW}</a>`
27
+ : html`<button type="button" class="all" data-lbx-part="ac-all" data-lbx-all @click=${() => s.submitAll()}>${s.t('showAllProducts')}${ARROW}</button>`;
28
+
29
+ export default (s: AutocompleteScope) => {
30
+ if (s.sheet) return s.include('autocomplete-sheet');
31
+ const opt = (row: { r: any; i: number }, as: string) => s.include('autocomplete-result-default', { r: row.r, i: row.i, as });
32
+ const key = (row: { r: any; i: number }) => `${row.i}:${row.r.id}`;
33
+ const list = s.phrases.length > 0 || s.lists.length > 0;
34
+ const prods = s.products.length > 0 || !!s.hero;
35
+ const cls = `box${list ? ' has-list' : ''}${s.hero ? ' has-hero' : ''}${prods ? ' has-products' : ''}${list && !prods ? ' only-list' : ''}`;
36
+ return html`
37
+ <div class=${cls} data-lbx-part="ac-panel">
38
+ <span class="caret" aria-hidden="true"></span>
39
+ <div class="panel">
40
+ ${s.state !== 'results'
41
+ ? s.include('autocomplete-empty')
42
+ : html`<div class="cols" role="listbox" id="lbx-ac-listbox" aria-label=${s.t('label')}>
43
+ ${list
44
+ ? html`<div class="list">
45
+ ${s.phrases.length ? group('query', s.t('phrases'), html`<div class="pills">${repeat(s.phrases, key, (p) => opt(p, 'pill'))}</div>`) : nothing}
46
+ ${s.lists.map((g) => group(g.kind, g.label, repeat(g.rows, key, (row) => opt(row, 'row'))))}
47
+ ${POWERED(s.t)}
48
+ </div>`
49
+ : nothing}
50
+ ${s.hero ? html`<div class="hero-col">${group('hero', s.t('topProduct'), opt(s.hero, 'hero'), `v-${s.heroVariant}`)}</div>` : nothing}
51
+ ${s.products.length
52
+ ? html`<div class="prods">${group('products', s.t('products'), html`<div class=${s.productLayout === 'list' ? 'p-list' : 'p-grid'}>${repeat(s.products, key, (p) => opt(p, s.productLayout === 'list' ? 'line' : 'card'))}</div>`)}</div>`
53
+ : prods
54
+ ? html`<div class="prods"></div>`
55
+ : nothing}
56
+ </div>
57
+ ${prods ? showAll(s) : nothing}`}
58
+ </div>
59
+ </div>
60
+ `;
61
+ };
@@ -0,0 +1,53 @@
1
+ import { html, nothing } from '../../lit';
2
+ import type { AutocompleteScope } from '../../types';
3
+ import { POWERED, group } from './template-autocomplete-box';
4
+ import { GLASS } from './template-autocomplete-result-default';
5
+
6
+ // "No results" in the house design: a layout of its own, so it
7
+ // never passes for results (no pills, columns, top product or "Show all").
8
+ // The message, phrases to try (handed to the site's search), links to
9
+ // browse, the optional help box (`autocomplete.help.phone` set), then the
10
+ // fallback products apart, on a quiet strip that says they are not results
11
+ // for this search. The chips, links and cards are options (the arrow keys:
12
+ // chips, links, cards), in listboxes; the help box's phone link is not.
13
+
14
+ export default (s: AutocompleteScope) => {
15
+ const { tries, browse, fallback, help } = s.empty;
16
+ const opt = (row: { r: any; i: number }, as: string) => s.include('autocomplete-result-default', { r: row.r, i: row.i, as });
17
+ const helpBox = help
18
+ ? html`<div class="help">
19
+ ${s.sheet
20
+ ? html`<span class="help-text"><span class="help-title">${help.title}</span>${help.hours ? html`<span class="help-hours">${help.hours}</span>` : nothing}</span>
21
+ <a class="call" href=${`tel:${help.tel}`}><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="2.5"></rect><path d="M11 18h2"></path></svg>${s.t('call')}<span class="sr"> ${help.phone}</span></a>`
22
+ : html`<span class="phone-ico" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2" width="12" height="20" rx="2.5"></rect><path d="M11 18h2"></path></svg></span>
23
+ <span class="help-text"><span class="help-title">${help.title}</span><a class="phone" href=${`tel:${help.tel}`}>${help.phone}</a>${help.hours ? html`<span class="help-hours">${help.hours}</span>` : nothing}</span>`}
24
+ </div>`
25
+ : nothing;
26
+ return html`
27
+ <div class="empty" data-lbx-part="ac-empty">
28
+ <div class="msg-row">
29
+ <div class="msg">
30
+ <span class="ico" aria-hidden="true">${GLASS(26)}</span>
31
+ <div class="msg-body">
32
+ <p class="e-title">${s.t('empty', { query: s.query })}</p>
33
+ <p class="e-hint">${s.t('emptyHint')}</p>
34
+ ${tries.length || browse.length
35
+ ? html`<div class="e-opts" role="listbox" id="lbx-ac-listbox" aria-label=${s.t('label')}>
36
+ ${tries.length ? group('try', s.t('tryLabel'), html`<div class="tries">${tries.map((x) => opt(x, 'try'))}</div>`, 'inline') : nothing}
37
+ ${browse.length ? group('browse', s.t('browseLabel'), html`<div class="browse">${browse.map((x) => opt(x, 'browse'))}</div>`, 'inline') : nothing}
38
+ </div>`
39
+ : nothing}
40
+ </div>
41
+ </div>
42
+ ${helpBox}
43
+ </div>
44
+ ${fallback.length
45
+ ? html`<div class="strip">
46
+ <div class="strip-head"><span class="note-wrap"><span class="f-title" id="lbx-ac-g-fallback">${s.t('fallbackTitle')}</span><span class="f-note" id="lbx-ac-g-fallback-note">${s.t('fallbackNote')}</span></span>${s.sheet ? nothing : POWERED(s.t)}</div>
47
+ <div class="fb" role="listbox" id="lbx-ac-fallback" aria-labelledby="lbx-ac-g-fallback lbx-ac-g-fallback-note">${fallback.map((x) => opt(x, 'compact'))}</div>
48
+ ${s.sheet ? POWERED(s.t) : nothing}
49
+ </div>`
50
+ : html`<div class="e-foot">${POWERED(s.t)}</div>`}
51
+ </div>
52
+ `;
53
+ };
@@ -0,0 +1,82 @@
1
+ import { html, ifDefined, nothing } from '../../lit';
2
+ import type { AutocompleteResultScope, Result } from '../../types';
3
+
4
+ // One option of the house design, drawn
5
+ // as `as` says: a query pill, a list row, the top product, a product card
6
+ // or row, the phone sheet's rows, a "No results" chip, link or fallback card. Every option is
7
+ // one link (a query suggestion has no href: its text goes to the site's
8
+ // search) and never a tab stop; the pointer over it moves the selection.
9
+ // Every product field is optional: what a result lacks is not drawn. No add
10
+ // to cart, no counts.
11
+
12
+ export const CHEVRON = html`<svg class="chev" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg>`;
13
+ export const GLASS = (size = 14) =>
14
+ html`<svg class="glass" width=${size} height=${size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg>`;
15
+
16
+ export default (s: AutocompleteResultScope) => {
17
+ const { r, i, active, choose, hover, unhover, price, field, t, as = 'row' } = s;
18
+ // A square picture box (its size from the option's class: no inline style, which a strict CSP refuses).
19
+ const pic = (size: number) => html`<span class="tile">${r.c.image ? html`<img data-lbx-part="image" src=${r.c.image} alt="" width=${size} height=${size} decoding="async" />` : nothing}</span>`;
20
+ const stock = (rr: Result, cat = true) => {
21
+ const a = field(rr, 'availability');
22
+ const c = cat ? field(rr, 'category') : '';
23
+ return a || c
24
+ ? html`<span class="stock">${a ? html`<span class="dot"></span><span class="avail">${a}</span>` : nothing}${c ? html`<span class="cat">${a ? '· ' : ''}${c}</span>` : nothing}</span>`
25
+ : nothing;
26
+ };
27
+ // A number is formatted (`price`, the site's currency); a text (a computed
28
+ // attribute in the shop's own format, `productFields.price`) is shown as it is.
29
+ const money = (v: unknown) => (typeof v === 'number' || /^\s*\d+(?:\.\d+)?\s*$/.test(String(v)) ? price(v) : String(v));
30
+ const now = field(r, 'price') || r.c.price;
31
+ const old = field(r, 'oldPrice');
32
+ const prices = (cls: string) =>
33
+ now || old
34
+ ? html`<span class=${cls}>${old ? html`<s class="old" data-lbx-part="old-price"><span class="sr">${t('was')} </span>${money(old)}</s>` : nothing}${now ? html`<span class="now" data-lbx-part="price">${money(now)}</span>` : nothing}</span>`
35
+ : nothing;
36
+ const title = (cls: string) => html`<span class=${cls} data-lbx-part="title">${r.c.title}</span>`;
37
+ let body: unknown;
38
+ switch (as) {
39
+ case 'pill':
40
+ body = title('t');
41
+ break;
42
+ case 'try':
43
+ body = html`${GLASS()}${title('t')}`;
44
+ break;
45
+ case 'browse':
46
+ body = html`<span class="t" data-lbx-part="title">${r.c.title}</span>${CHEVRON}`;
47
+ break;
48
+ case 'hero': {
49
+ const badge = field(r, 'badge');
50
+ const specs = s.heroVariant === 'spec' ? s.specs(r) : [];
51
+ body = html`<span class="pic">${r.c.image ? html`<img data-lbx-part="image" src=${r.c.image} alt="" decoding="async" />` : nothing}${badge ? html`<span class="badge" data-lbx-part="badge">${badge}</span>` : nothing}</span>
52
+ ${title('t')}
53
+ ${specs.length ? html`<span class="specs">${specs.map((x) => html`<span class="sp"><span class="k">${x.label}</span><span class="v">${x.value}</span></span>`)}</span>` : nothing}
54
+ ${stock(r)} ${prices('prices')}`;
55
+ break;
56
+ }
57
+ case 'card':
58
+ body = html`${pic(84)}<span class="body">${title('t')}${stock(r)}${prices('prices')}</span>`;
59
+ break;
60
+ case 'line': {
61
+ const meta = field(r, 'meta');
62
+ body = html`${pic(48)}<span class="body">${title('t')}${meta ? html`<span class="meta">${meta}</span>` : nothing}</span>${stock(r, false)}${prices('prices')}`;
63
+ break;
64
+ }
65
+ case 'mhero':
66
+ body = html`${pic(120)}<span class="body">${title('t')}${stock(r)}${prices('prices')}</span>`;
67
+ break;
68
+ case 'mproduct':
69
+ body = html`${pic(72)}<span class="body">${title('t')}${stock(r)}${prices('prices')}</span>`;
70
+ break;
71
+ case 'mrow':
72
+ body = html`${title('t')}${CHEVRON}`;
73
+ break;
74
+ case 'compact':
75
+ body = html`${pic(52)}<span class="body"><span class="t" data-lbx-part="title">${r.c.title}</span>${prices('prices')}</span>`;
76
+ break;
77
+ default: // 'row'
78
+ body = title('t');
79
+ }
80
+ return html`<a class=${`opt ${as}`} data-lbx-part="ac-row" role="option" tabindex="-1" id=${`lbx-ac-${i}`} aria-selected=${i === active ? 'true' : 'false'}
81
+ href=${ifDefined(r.type === 'query' ? undefined : r.c.url || undefined)} @click=${(e: Event) => (e.preventDefault(), choose(i))} @pointerenter=${(e: PointerEvent) => hover(i, e)} @pointerleave=${(e: PointerEvent) => unhover(i, e)}>${body}</a>`;
82
+ };
@@ -0,0 +1,74 @@
1
+ import { html, nothing, repeat } from '../../lit';
2
+ import type { AutocompleteScope } from '../../types';
3
+ import { POWERED, group, showAll } from './template-autocomplete-box';
4
+
5
+ // The house design on a phone: a full-screen sheet. Its header
6
+ // (back, its own input, clear), the query pills on one sideways-scrolling
7
+ // band, a tab per type with results (products first; no counts), the open
8
+ // tab's options (the top product, then product rows; or 56 px rows), and
9
+ // "Show all products" in a bar that stays above the on-screen keyboard.
10
+ // Its input is the combobox (index.ts): it controls both listboxes and
11
+ // points at the selected option (aria-activedescendant, set by index.ts).
12
+
13
+ export default (s: AutocompleteScope) => {
14
+ const opt = (row: { r: any; i: number }, as: string) => s.include('autocomplete-result-default', { r: row.r, i: row.i, as });
15
+ const key = (row: { r: any; i: number }) => `${row.i}:${row.r.id}`;
16
+ const results = s.state === 'results';
17
+ const onProducts = !!s.hero && s.tabs.find((x) => x.selected)?.kind === s.hero.r.type;
18
+ const open = s.tabs.find((x) => x.selected);
19
+ const rows = open && !onProducts ? (s.lists.find((g) => g.kind === open.kind)?.rows ?? []) : [];
20
+ // What the input controls: the listboxes on screen.
21
+ const e = s.empty;
22
+ const controls = (
23
+ results
24
+ ? [s.phrases.length && 'lbx-ac-phrases', s.tabs.length && 'lbx-ac-listbox']
25
+ : s.state === 'empty'
26
+ ? [(e.tries.length || e.browse.length) && 'lbx-ac-listbox', e.fallback.length && 'lbx-ac-fallback']
27
+ : []
28
+ )
29
+ .filter(Boolean)
30
+ .join(' ');
31
+ return html`
32
+ <div class="sheet" data-lbx-part="ac-panel">
33
+ <div class="sh-head">
34
+ <button type="button" class="icon back" aria-label=${s.t('closeSearch')} @click=${() => s.back()}>
35
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6"></path></svg>
36
+ </button>
37
+ <input class="sh-input" data-lbx-sheet-input type="text" enterkeyhint="search" autocomplete="off" autocapitalize="off" spellcheck="false"
38
+ role="combobox" aria-autocomplete="list" aria-expanded=${controls ? 'true' : 'false'} aria-controls=${controls || nothing}
39
+ aria-label=${s.inputLabel} placeholder=${s.placeholder} .value=${s.value} />
40
+ ${s.value
41
+ ? html`<button type="button" class="icon clear" aria-label=${s.t('clear')} @click=${() => s.clear()}><span class="x"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"></path></svg></span></button>`
42
+ : nothing}
43
+ </div>
44
+ ${s.state === 'empty'
45
+ ? s.include('autocomplete-empty')
46
+ : results
47
+ ? html`
48
+ ${s.phrases.length
49
+ ? html`<div class="sh-phrases" role="listbox" id="lbx-ac-phrases" aria-label=${s.t('phrases')}>${repeat(s.phrases, key, (p) => opt(p, 'pill'))}</div>`
50
+ : nothing}
51
+ ${s.tabs.length
52
+ ? html`<div class="tabs" role="tablist" aria-label=${s.t('tabs')}>
53
+ ${s.tabs.map(
54
+ (x) =>
55
+ html`<button type="button" role="tab" class="tab" id=${`lbx-ac-tab-${x.kind}`} aria-selected=${x.selected ? 'true' : 'false'} aria-controls="lbx-ac-panel" tabindex=${x.selected ? '0' : '-1'} @click=${() => x.select()}>${x.label}</button>`,
56
+ )}
57
+ </div>`
58
+ : nothing}
59
+ <div class="sh-list" role="tabpanel" id="lbx-ac-panel" aria-labelledby=${open ? `lbx-ac-tab-${open.kind}` : nothing}>
60
+ ${s.tabs.length
61
+ ? html`<div role="listbox" id="lbx-ac-listbox" aria-label=${s.t('label')}>
62
+ ${onProducts
63
+ ? html`${group('hero', s.t('topProduct'), opt(s.hero!, 'mhero'), 'm-hero')}
64
+ ${s.products.length ? group('products', s.t('products'), repeat(s.products, key, (p) => opt(p, 'mproduct')), 'm-products') : nothing}`
65
+ : repeat(rows, key, (row) => opt(row, 'mrow'))}
66
+ </div>`
67
+ : nothing}
68
+ ${POWERED(s.t)}
69
+ </div>
70
+ ${s.hero ? html`<div class="sh-foot">${showAll(s)}</div>` : nothing}`
71
+ : nothing}
72
+ </div>
73
+ `;
74
+ };