@aparte/plugin-cards 0.0.0-stage → 0.29.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/dist/carousel-CPy-0JGN.js +63 -0
  4. package/dist/carousel-CPy-0JGN.js.map +1 -0
  5. package/dist/controls-BVX2u7sK.js +62 -0
  6. package/dist/controls-BVX2u7sK.js.map +1 -0
  7. package/dist/format-CchcJRum.js +720 -0
  8. package/dist/format-CchcJRum.js.map +1 -0
  9. package/dist/index.d.ts +28 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +35 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/items/icons.d.ts +12 -0
  14. package/dist/items/icons.d.ts.map +1 -0
  15. package/dist/items/index.d.ts +100 -0
  16. package/dist/items/index.d.ts.map +1 -0
  17. package/dist/items.js +698 -0
  18. package/dist/items.js.map +1 -0
  19. package/dist/kit/carousel.d.ts +17 -0
  20. package/dist/kit/carousel.d.ts.map +1 -0
  21. package/dist/kit/controls.d.ts +51 -0
  22. package/dist/kit/controls.d.ts.map +1 -0
  23. package/dist/kit/define.d.ts +194 -0
  24. package/dist/kit/define.d.ts.map +1 -0
  25. package/dist/kit/dom.d.ts +40 -0
  26. package/dist/kit/dom.d.ts.map +1 -0
  27. package/dist/kit/format.d.ts +49 -0
  28. package/dist/kit/format.d.ts.map +1 -0
  29. package/dist/kit/rating.d.ts +19 -0
  30. package/dist/kit/rating.d.ts.map +1 -0
  31. package/dist/kit/sparkline.d.ts +19 -0
  32. package/dist/kit/sparkline.d.ts.map +1 -0
  33. package/dist/kit/strings.d.ts +139 -0
  34. package/dist/kit/strings.d.ts.map +1 -0
  35. package/dist/link/icons.d.ts +16 -0
  36. package/dist/link/icons.d.ts.map +1 -0
  37. package/dist/link/index.d.ts +98 -0
  38. package/dist/link/index.d.ts.map +1 -0
  39. package/dist/link.js +387 -0
  40. package/dist/link.js.map +1 -0
  41. package/dist/media/icons.d.ts +28 -0
  42. package/dist/media/icons.d.ts.map +1 -0
  43. package/dist/media/index.d.ts +107 -0
  44. package/dist/media/index.d.ts.map +1 -0
  45. package/dist/media.js +610 -0
  46. package/dist/media.js.map +1 -0
  47. package/dist/order/icons.d.ts +49 -0
  48. package/dist/order/icons.d.ts.map +1 -0
  49. package/dist/order/index.d.ts +204 -0
  50. package/dist/order/index.d.ts.map +1 -0
  51. package/dist/order.js +1843 -0
  52. package/dist/order.js.map +1 -0
  53. package/dist/place/icons.d.ts +18 -0
  54. package/dist/place/icons.d.ts.map +1 -0
  55. package/dist/place/index.d.ts +207 -0
  56. package/dist/place/index.d.ts.map +1 -0
  57. package/dist/place.js +1456 -0
  58. package/dist/place.js.map +1 -0
  59. package/dist/plan/icons.d.ts +25 -0
  60. package/dist/plan/icons.d.ts.map +1 -0
  61. package/dist/plan/index.d.ts +153 -0
  62. package/dist/plan/index.d.ts.map +1 -0
  63. package/dist/plan.js +1191 -0
  64. package/dist/plan.js.map +1 -0
  65. package/dist/product/icons.d.ts +16 -0
  66. package/dist/product/icons.d.ts.map +1 -0
  67. package/dist/product/index.d.ts +133 -0
  68. package/dist/product/index.d.ts.map +1 -0
  69. package/dist/product.js +1245 -0
  70. package/dist/product.js.map +1 -0
  71. package/dist/rating-B8Mdx_b_.js +35 -0
  72. package/dist/rating-B8Mdx_b_.js.map +1 -0
  73. package/dist/score/icons.d.ts +35 -0
  74. package/dist/score/icons.d.ts.map +1 -0
  75. package/dist/score/index.d.ts +185 -0
  76. package/dist/score/index.d.ts.map +1 -0
  77. package/dist/score.js +1843 -0
  78. package/dist/score.js.map +1 -0
  79. package/dist/sparkline-C-cA9obz.js +56 -0
  80. package/dist/sparkline-C-cA9obz.js.map +1 -0
  81. package/dist/stats/icons.d.ts +19 -0
  82. package/dist/stats/icons.d.ts.map +1 -0
  83. package/dist/stats/index.d.ts +137 -0
  84. package/dist/stats/index.d.ts.map +1 -0
  85. package/dist/stats.js +1085 -0
  86. package/dist/stats.js.map +1 -0
  87. package/dist/stock/chart.d.ts +135 -0
  88. package/dist/stock/chart.d.ts.map +1 -0
  89. package/dist/stock/icons.d.ts +15 -0
  90. package/dist/stock/icons.d.ts.map +1 -0
  91. package/dist/stock/index.d.ts +195 -0
  92. package/dist/stock/index.d.ts.map +1 -0
  93. package/dist/stock.js +1013 -0
  94. package/dist/stock.js.map +1 -0
  95. package/dist/weather/conditions.d.ts +25 -0
  96. package/dist/weather/conditions.d.ts.map +1 -0
  97. package/dist/weather/icons.d.ts +12 -0
  98. package/dist/weather/icons.d.ts.map +1 -0
  99. package/dist/weather/index.d.ts +146 -0
  100. package/dist/weather/index.d.ts.map +1 -0
  101. package/dist/weather.js +1162 -0
  102. package/dist/weather.js.map +1 -0
  103. package/package.json +137 -3
package/dist/place.js ADDED
@@ -0,0 +1,1456 @@
1
+ import { c as carousel } from "./carousel-CPy-0JGN.js";
2
+ import { d as defineCard, o as str, b as formatNumber, p as word, e as el, n as num, k as glyph, s as safeUrl, i as formatUnit, l as intlTag } from "./format-CchcJRum.js";
3
+ import { r as rating } from "./rating-B8Mdx_b_.js";
4
+ const svg = (body) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">${body}</svg>`;
5
+ const PIN = svg('<path d="M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0c0 5.4-6.5 11-6.5 11z"/><circle cx="12" cy="10" r="2.4"/>');
6
+ const PHONE = svg('<path d="M5.2 4h3l1.6 4-2 1.3a10.5 10.5 0 0 0 5 5l1.3-2 4 1.6v3a2 2 0 0 1-2.1 2A15.2 15.2 0 0 1 3.2 6.1 2 2 0 0 1 5.2 4z"/>');
7
+ const GLOBE = svg('<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.4 2.4 3.5 5.3 3.5 8.5s-1.1 6.1-3.5 8.5c-2.4-2.4-3.5-5.3-3.5-8.5s1.1-6.1 3.5-8.5z"/>');
8
+ const DIRECTIONS = svg('<path d="M12 2.8 21.2 12 12 21.2 2.8 12z"/><path d="M9.2 14.6v-2.4a1.4 1.4 0 0 1 1.4-1.4h4.6M13.4 8.8l1.9 2-1.9 2"/>');
9
+ const CLOCK = svg('<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/>');
10
+ const KIND_GLYPHS = {
11
+ restaurant: svg('<path d="M7 3v18M4.6 3v4.8A2.4 2.4 0 0 0 7 10.2a2.4 2.4 0 0 0 2.4-2.4V3M17 21V3c-2.3 1.4-3.5 4-3.5 7.2V13H17"/>'),
12
+ cafe: svg('<path d="M4.5 9h11v5.5a4.5 4.5 0 0 1-4.5 4.5H9a4.5 4.5 0 0 1-4.5-4.5z"/><path d="M15.5 10.5h1.3a2.6 2.6 0 0 1 0 5.2h-1.6M8 3.5c-.6.8-.6 1.7 0 2.5M11.5 3.5c-.6.8-.6 1.7 0 2.5"/>'),
13
+ bar: svg('<path d="M5 4h14l-7 8z"/><path d="M12 12v8M8.5 20h7M8.6 8h6.8"/>'),
14
+ hotel: svg('<path d="M3 18.5V6M3 14h18v4.5M21 14v-2.5a3 3 0 0 0-3-3h-7V14"/><circle cx="7" cy="10.5" r="1.8"/>'),
15
+ shop: svg('<path d="M5.5 8h13l-1 12.5h-11z"/><path d="M9 10V7a3 3 0 0 1 6 0v3"/>'),
16
+ museum: svg('<path d="M3.5 9.5 12 4l8.5 5.5zM5 20.5h14M6.5 11.5v6.5M10 11.5v6.5M14 11.5v6.5M17.5 11.5v6.5"/>'),
17
+ park: svg('<path d="M12 21v-5M12 3 6.5 11h3L6 16h12l-3.5-5h3z"/>'),
18
+ gym: svg('<path d="M6.5 7v10M17.5 7v10M3.5 9.5v5M20.5 9.5v5M6.5 12h11"/>'),
19
+ pharmacy: svg('<rect x="4" y="4" width="16" height="16" rx="4"/><path d="M12 8.5v7M8.5 12h7"/>'),
20
+ place: PIN
21
+ };
22
+ function kindGlyph(kind) {
23
+ return KIND_GLYPHS[kind];
24
+ }
25
+ const placeStyles = `/* =========================================
26
+ The place card — @aparte/plugin-cards/place
27
+ =========================================
28
+ Three variants of one card, chosen by \`data-variant\` on its root:
29
+ card a tile: the photo with its category, the name, the rating, whether it is
30
+ open, the address, three buttons. Several scroll sideways as tiles.
31
+ compact one row: a thumbnail, the name and the distance, the rating, the status.
32
+ Several are stacked rows in one card.
33
+ detail one place in full: the tile's head, the buttons, the ways in (address,
34
+ phone, site), the week's hours with today marked, the tags.
35
+ Every colour comes from core's tokens, or from a \`--aparte-place-*\` token here: the
36
+ three status inks and one hue per kind of place, which tint the placeholder and the
37
+ marker. A theme repaints the card by setting them.
38
+ */
39
+
40
+ .aparte-place-card {
41
+ /* What "open", "closes soon" and "closed" are written in: the derived inks, which are
42
+ readable as text on either ground. */
43
+ --aparte-place-open: var(--aparte-success-ink);
44
+ --aparte-place-soon: var(--aparte-warning-ink);
45
+ --aparte-place-closed: var(--aparte-error-ink);
46
+ --aparte-place-media-ratio: 16 / 9;
47
+ --aparte-place-detail-ratio: 2 / 1;
48
+ --aparte-place-thumb-size: 3.5rem;
49
+ --aparte-place-on-hue: #ffffff;
50
+
51
+ /* One hue per kind of place: the marker on the placeholder, the badge's glyph. */
52
+ --aparte-place-kind-restaurant: #e8710a;
53
+ --aparte-place-kind-cafe: #a0612b;
54
+ --aparte-place-kind-bar: #9c3fd1;
55
+ --aparte-place-kind-hotel: #2f6fdf;
56
+ --aparte-place-kind-shop: #d6336c;
57
+ --aparte-place-kind-museum: #5b5bd6;
58
+ --aparte-place-kind-park: #2f9e44;
59
+ --aparte-place-kind-gym: #e03131;
60
+ --aparte-place-kind-pharmacy: #0c9488;
61
+ --aparte-place-kind-place: var(--aparte-primary);
62
+ }
63
+
64
+ /* A tile reads its kind's hue once; everything inside it says \`--aparte-place-hue\`. */
65
+ .aparte-place { --aparte-place-hue: var(--aparte-place-kind-place); }
66
+ .aparte-place[data-kind="restaurant"] { --aparte-place-hue: var(--aparte-place-kind-restaurant); }
67
+ .aparte-place[data-kind="cafe"] { --aparte-place-hue: var(--aparte-place-kind-cafe); }
68
+ .aparte-place[data-kind="bar"] { --aparte-place-hue: var(--aparte-place-kind-bar); }
69
+ .aparte-place[data-kind="hotel"] { --aparte-place-hue: var(--aparte-place-kind-hotel); }
70
+ .aparte-place[data-kind="shop"] { --aparte-place-hue: var(--aparte-place-kind-shop); }
71
+ .aparte-place[data-kind="museum"] { --aparte-place-hue: var(--aparte-place-kind-museum); }
72
+ .aparte-place[data-kind="park"] { --aparte-place-hue: var(--aparte-place-kind-park); }
73
+ .aparte-place[data-kind="gym"] { --aparte-place-hue: var(--aparte-place-kind-gym); }
74
+ .aparte-place[data-kind="pharmacy"] { --aparte-place-hue: var(--aparte-place-kind-pharmacy); }
75
+
76
+ /* A single tile is narrower than the card's default: it is a photo and five lines. */
77
+ .aparte-place-card[data-variant="card"] { --aparte-rich-card-max-inline-size: 22rem; }
78
+
79
+ /* ── The place ───────────────────────────────────────────────────────── */
80
+
81
+ .aparte-place {
82
+ position: relative;
83
+ display: flex;
84
+ flex-direction: column;
85
+ min-inline-size: 0;
86
+ color: var(--aparte-text);
87
+ }
88
+
89
+ .aparte-place__body {
90
+ display: flex;
91
+ flex: 1;
92
+ flex-direction: column;
93
+ gap: var(--aparte-space-2);
94
+ min-inline-size: 0;
95
+ padding: var(--aparte-space-6) var(--aparte-space-7) var(--aparte-space-7);
96
+ }
97
+
98
+ .aparte-place__name {
99
+ overflow: hidden;
100
+ font-size: var(--aparte-font-size-lg);
101
+ font-weight: var(--aparte-font-weight-semibold);
102
+ letter-spacing: -0.01em;
103
+ line-height: 1.25;
104
+ text-overflow: ellipsis;
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .aparte-place__link {
109
+ color: inherit;
110
+ text-decoration: none;
111
+ border-radius: var(--aparte-radius-xs);
112
+ }
113
+
114
+ .aparte-place__link:hover {
115
+ text-decoration: underline;
116
+ text-decoration-thickness: 1px;
117
+ text-underline-offset: 0.18em;
118
+ }
119
+
120
+ .aparte-place__link:focus-visible {
121
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
122
+ outline-offset: var(--aparte-focus-outline-offset);
123
+ }
124
+
125
+ /* A tile, a row and a lone card are one target: the name's link covers the whole of it,
126
+ and its buttons sit above that cover. The ring is then drawn around the target, inset,
127
+ because the card clips what passes its edge. */
128
+ .aparte-place--tile .aparte-place__name .aparte-place__link::after,
129
+ .aparte-place__row .aparte-place__name .aparte-place__link::after,
130
+ .aparte-place-card[data-variant="card"] > .aparte-place .aparte-place__name .aparte-place__link::after {
131
+ content: "";
132
+ position: absolute;
133
+ inset: 0;
134
+ border-radius: inherit;
135
+ }
136
+
137
+ .aparte-place--tile .aparte-place__name .aparte-place__link:focus-visible,
138
+ .aparte-place__row .aparte-place__name .aparte-place__link:focus-visible,
139
+ .aparte-place-card[data-variant="card"] > .aparte-place .aparte-place__name .aparte-place__link:focus-visible {
140
+ outline: none;
141
+ }
142
+
143
+ .aparte-place--tile .aparte-place__name .aparte-place__link:focus-visible::after,
144
+ .aparte-place__row .aparte-place__name .aparte-place__link:focus-visible::after,
145
+ .aparte-place-card[data-variant="card"] > .aparte-place .aparte-place__name .aparte-place__link:focus-visible::after {
146
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
147
+ outline-offset: calc(var(--aparte-focus-outline-width) * -1);
148
+ }
149
+
150
+ .aparte-place--tile:hover .aparte-place__name .aparte-place__link,
151
+ .aparte-place__row:hover .aparte-place__name .aparte-place__link {
152
+ text-decoration: underline;
153
+ text-decoration-thickness: 1px;
154
+ text-underline-offset: 0.18em;
155
+ }
156
+
157
+ .aparte-place__dot {
158
+ flex: none;
159
+ color: var(--aparte-text-muted);
160
+ opacity: 0.7;
161
+ }
162
+
163
+ .aparte-place__score {
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ align-items: center;
167
+ column-gap: var(--aparte-space-2);
168
+ min-inline-size: 0;
169
+ color: var(--aparte-text-muted);
170
+ font-size: var(--aparte-font-size-sm);
171
+ }
172
+
173
+ .aparte-place__price {
174
+ white-space: nowrap;
175
+ letter-spacing: 0.04em;
176
+ }
177
+
178
+ .aparte-place__price-on {
179
+ color: var(--aparte-text);
180
+ font-weight: var(--aparte-font-weight-medium);
181
+ }
182
+
183
+ .aparte-place__price-off { opacity: 0.35; }
184
+
185
+ .aparte-place__category {
186
+ overflow: hidden;
187
+ text-overflow: ellipsis;
188
+ white-space: nowrap;
189
+ }
190
+
191
+ /* ── Open, closes soon, closed ───────────────────────────────────────── */
192
+
193
+ .aparte-place__status {
194
+ --aparte-place-status: var(--aparte-place-open);
195
+ display: flex;
196
+ flex-wrap: wrap;
197
+ align-items: center;
198
+ column-gap: var(--aparte-space-2);
199
+ margin: 0;
200
+ color: var(--aparte-text-muted);
201
+ font-size: var(--aparte-font-size-sm);
202
+ }
203
+
204
+ .aparte-place__status[data-status="soon"] { --aparte-place-status: var(--aparte-place-soon); }
205
+ .aparte-place__status[data-status="closed"] { --aparte-place-status: var(--aparte-place-closed); }
206
+
207
+ .aparte-place__status-dot {
208
+ position: relative;
209
+ flex: none;
210
+ inline-size: 0.5rem;
211
+ block-size: 0.5rem;
212
+ margin-inline-end: var(--aparte-space-1);
213
+ border-radius: var(--aparte-radius-full);
214
+ background: var(--aparte-place-status);
215
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--aparte-place-status) 20%, transparent);
216
+ }
217
+
218
+ /* An open place breathes: a ring leaves the dot every few seconds. Still under reduced motion. */
219
+ .aparte-place__status[data-status="open"] .aparte-place__status-dot::after {
220
+ content: "";
221
+ position: absolute;
222
+ inset: 0;
223
+ border-radius: inherit;
224
+ background: var(--aparte-place-status);
225
+ animation: aparte-place-pulse 2.6s ease-out infinite;
226
+ }
227
+
228
+ .aparte-place__state {
229
+ color: var(--aparte-place-status);
230
+ font-weight: var(--aparte-font-weight-semibold);
231
+ }
232
+
233
+ @keyframes aparte-place-pulse {
234
+ 0% { opacity: 0.55; transform: scale(1); }
235
+ 70%, 100% { opacity: 0; transform: scale(2.6); }
236
+ }
237
+
238
+ /* ── Address and the lines with a glyph ──────────────────────────────── */
239
+
240
+ .aparte-place__address {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: var(--aparte-space-2);
244
+ min-inline-size: 0;
245
+ margin: 0;
246
+ color: var(--aparte-text-muted);
247
+ font-size: var(--aparte-font-size-sm);
248
+ white-space: nowrap;
249
+ }
250
+
251
+ .aparte-place__address-text {
252
+ min-inline-size: 0;
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ }
256
+
257
+ .aparte-place__distance {
258
+ flex: none;
259
+ color: var(--aparte-text);
260
+ font-weight: var(--aparte-font-weight-medium);
261
+ white-space: nowrap;
262
+ }
263
+
264
+ .aparte-place__line-glyph {
265
+ --aparte-rich-card-glyph-size: 1rem;
266
+ color: var(--aparte-text-muted);
267
+ }
268
+
269
+ /* ── The photo, or its placeholder ───────────────────────────────────── */
270
+
271
+ .aparte-place__media {
272
+ position: relative;
273
+ flex: none;
274
+ overflow: hidden;
275
+ isolation: isolate;
276
+ background: var(--aparte-surface-2);
277
+ }
278
+
279
+ .aparte-place__media--wide { aspect-ratio: var(--aparte-place-media-ratio); }
280
+
281
+ .aparte-place__img {
282
+ position: absolute;
283
+ inset: 0;
284
+ z-index: 1;
285
+ display: block;
286
+ inline-size: 100%;
287
+ block-size: 100%;
288
+ object-fit: cover;
289
+ transition: transform var(--aparte-duration-slower, 0.3s) ease;
290
+ }
291
+
292
+ .aparte-place--tile:hover .aparte-place__img,
293
+ .aparte-place-card[data-variant="card"] > .aparte-place:hover .aparte-place__img {
294
+ transform: scale(1.04);
295
+ }
296
+
297
+ /* No photo: a soft map in the kind's hue — two avenues, a grid of streets, a glow — and
298
+ the kind's marker standing on it. Decoration, drawn by the sheet: no map tile is fetched. */
299
+ .aparte-place__placeholder {
300
+ --aparte-place-street: color-mix(in oklab, var(--aparte-surface-1) 72%, transparent);
301
+ --aparte-place-avenue: color-mix(in oklab, var(--aparte-surface-1) 92%, transparent);
302
+ position: absolute;
303
+ inset: 0;
304
+ display: grid;
305
+ place-items: center;
306
+ background:
307
+ radial-gradient(circle at 50% 48%, color-mix(in oklab, var(--aparte-place-hue) 24%, transparent) 0 14%, transparent 48%),
308
+ linear-gradient(118deg, transparent 0 36%, var(--aparte-place-avenue) 36% 39.5%, transparent 39.5%),
309
+ linear-gradient(22deg, transparent 0 63%, var(--aparte-place-avenue) 63% 65.5%, transparent 65.5%),
310
+ repeating-linear-gradient(118deg, transparent 0 22px, var(--aparte-place-street) 22px 24px),
311
+ repeating-linear-gradient(28deg, transparent 0 31px, var(--aparte-place-street) 31px 33px),
312
+ color-mix(in oklab, var(--aparte-place-hue) 13%, var(--aparte-surface-2));
313
+ }
314
+
315
+ /* The marker's shadow on the ground. */
316
+ .aparte-place__placeholder::after {
317
+ content: "";
318
+ position: absolute;
319
+ inset-block-start: calc(50% + 1.45rem);
320
+ inset-inline-start: 50%;
321
+ inline-size: 1.4rem;
322
+ block-size: 0.4rem;
323
+ margin-inline-start: -0.7rem;
324
+ border-radius: 50%;
325
+ background: rgb(0 0 0 / 0.22);
326
+ filter: blur(2px);
327
+ }
328
+
329
+ /* A drop pin: a square with three round corners, turned so the sharp one points down. */
330
+ .aparte-place__marker {
331
+ display: grid;
332
+ place-items: center;
333
+ inline-size: 2.75rem;
334
+ block-size: 2.75rem;
335
+ margin-block-end: 0.9rem;
336
+ border-radius: 50% 50% 50% 0;
337
+ background: var(--aparte-place-hue);
338
+ color: var(--aparte-place-on-hue);
339
+ box-shadow:
340
+ 0 0 0 3px color-mix(in oklab, var(--aparte-surface-1) 85%, transparent),
341
+ 0 8px 16px -6px color-mix(in oklab, var(--aparte-place-hue) 70%, black);
342
+ transform: rotate(-45deg);
343
+ }
344
+
345
+ .aparte-place__marker-glyph {
346
+ --aparte-rich-card-glyph-size: 1.3rem;
347
+ transform: rotate(45deg);
348
+ }
349
+
350
+ .aparte-place__media--thumb {
351
+ inline-size: var(--aparte-place-thumb-size);
352
+ block-size: var(--aparte-place-thumb-size);
353
+ border-radius: var(--aparte-radius-lg);
354
+ }
355
+
356
+ /* In a thumbnail the placeholder is only the kind's glyph on its tint. */
357
+ .aparte-place__media--thumb .aparte-place__placeholder {
358
+ background: color-mix(in oklab, var(--aparte-place-hue) 15%, var(--aparte-surface-1));
359
+ }
360
+
361
+ .aparte-place__media--thumb .aparte-place__placeholder::after { content: none; }
362
+
363
+ .aparte-place__media--thumb .aparte-place__marker {
364
+ inline-size: auto;
365
+ block-size: auto;
366
+ margin: 0;
367
+ border-radius: 0;
368
+ background: none;
369
+ box-shadow: none;
370
+ color: var(--aparte-place-hue);
371
+ transform: none;
372
+ }
373
+
374
+ .aparte-place__media--thumb .aparte-place__marker-glyph {
375
+ --aparte-rich-card-glyph-size: 1.5rem;
376
+ transform: none;
377
+ }
378
+
379
+ /* The category, over the photo: a frosted chip, which reads on any picture. */
380
+ .aparte-place__badge {
381
+ position: absolute;
382
+ inset-block-start: var(--aparte-space-5);
383
+ inset-inline-start: var(--aparte-space-5);
384
+ z-index: 2;
385
+ display: inline-flex;
386
+ align-items: center;
387
+ gap: var(--aparte-space-2);
388
+ max-inline-size: calc(100% - var(--aparte-space-5) * 2);
389
+ padding-block: var(--aparte-space-2);
390
+ padding-inline: var(--aparte-space-3) var(--aparte-space-4);
391
+ border-radius: var(--aparte-radius-full);
392
+ background: color-mix(in oklab, var(--aparte-surface-1) 86%, transparent);
393
+ backdrop-filter: blur(8px) saturate(1.4);
394
+ box-shadow: 0 1px 4px rgb(0 0 0 / 0.14);
395
+ color: var(--aparte-text);
396
+ font-size: var(--aparte-font-size-sm);
397
+ font-weight: var(--aparte-font-weight-medium);
398
+ line-height: 1.2;
399
+ }
400
+
401
+ .aparte-place__badge-glyph {
402
+ --aparte-rich-card-glyph-size: 0.95rem;
403
+ color: var(--aparte-place-hue);
404
+ }
405
+
406
+ /* ── Buttons ─────────────────────────────────────────────────────────── */
407
+
408
+ .aparte-place__actions {
409
+ position: relative;
410
+ z-index: 3;
411
+ display: flex;
412
+ flex-wrap: wrap;
413
+ gap: var(--aparte-space-3);
414
+ margin-block-start: var(--aparte-space-5);
415
+ }
416
+
417
+ .aparte-place__action {
418
+ gap: var(--aparte-space-2);
419
+ }
420
+
421
+ /* ── Several, side by side ───────────────────────────────────────────── */
422
+
423
+ /* Tiles in a row carry their own frame, so the card around them steps back. */
424
+ .aparte-place-card:has(> .aparte-place__collection) {
425
+ --aparte-rich-card-max-inline-size: 40rem;
426
+ overflow: visible;
427
+ border-color: transparent;
428
+ background: transparent;
429
+ }
430
+
431
+ .aparte-place__collection {
432
+ display: grid;
433
+ gap: var(--aparte-space-4);
434
+ min-inline-size: 0;
435
+ }
436
+
437
+ .aparte-place__collection .aparte-rich-card__carousel {
438
+ --aparte-rich-card-slide-size: min(15rem, 80%);
439
+ }
440
+
441
+ .aparte-place__heading {
442
+ display: flex;
443
+ align-items: center;
444
+ gap: var(--aparte-space-3);
445
+ min-inline-size: 0;
446
+ font-size: var(--aparte-font-size-md);
447
+ font-weight: var(--aparte-font-weight-semibold);
448
+ }
449
+
450
+ .aparte-place__heading-text {
451
+ overflow: hidden;
452
+ text-overflow: ellipsis;
453
+ white-space: nowrap;
454
+ }
455
+
456
+ .aparte-place__collection > .aparte-place__heading { padding-inline: var(--aparte-space-2); }
457
+
458
+ .aparte-place--tile {
459
+ block-size: 100%;
460
+ overflow: hidden;
461
+ border: var(--aparte-border-width) solid var(--aparte-border);
462
+ border-radius: var(--aparte-radius-xl);
463
+ background: var(--aparte-surface-1);
464
+ transition:
465
+ border-color var(--aparte-duration-base, 0.15s) ease,
466
+ box-shadow var(--aparte-duration-base, 0.15s) ease;
467
+ }
468
+
469
+ .aparte-place--tile:hover {
470
+ border-color: color-mix(in oklab, var(--aparte-text) 20%, var(--aparte-border));
471
+ box-shadow: 0 10px 24px -16px rgb(0 0 0 / 0.5);
472
+ }
473
+
474
+ .aparte-place--tile .aparte-place__body {
475
+ padding: var(--aparte-space-5) var(--aparte-space-6) var(--aparte-space-6);
476
+ }
477
+
478
+ .aparte-place--tile .aparte-place__name { font-size: var(--aparte-font-size-base); }
479
+
480
+ /* The buttons sit on the tile's floor, so a row of tiles lines them up. */
481
+ .aparte-place--tile .aparte-place__actions { margin-block-start: auto; padding-block-start: var(--aparte-space-5); }
482
+
483
+ /* ── Compact: rows ───────────────────────────────────────────────────── */
484
+
485
+ .aparte-place__row {
486
+ flex-direction: row;
487
+ align-items: center;
488
+ gap: var(--aparte-space-5);
489
+ padding: var(--aparte-space-5) var(--aparte-space-6);
490
+ transition: background-color var(--aparte-duration-fast, 0.12s) ease;
491
+ }
492
+
493
+ .aparte-place__text {
494
+ display: flex;
495
+ flex: 1;
496
+ flex-direction: column;
497
+ gap: var(--aparte-space-1);
498
+ min-inline-size: 0;
499
+ }
500
+
501
+ .aparte-place__row-head {
502
+ display: flex;
503
+ align-items: baseline;
504
+ gap: var(--aparte-space-3);
505
+ min-inline-size: 0;
506
+ }
507
+
508
+ .aparte-place__row .aparte-place__name {
509
+ flex: 1;
510
+ min-inline-size: 0;
511
+ font-size: var(--aparte-font-size-base);
512
+ }
513
+
514
+ .aparte-place__row .aparte-place__distance {
515
+ color: var(--aparte-text-muted);
516
+ font-size: var(--aparte-font-size-sm);
517
+ }
518
+
519
+ .aparte-place__row .aparte-place__score,
520
+ .aparte-place__row .aparte-place__status { font-size: var(--aparte-font-size-xs); }
521
+
522
+ .aparte-place__row .aparte-place__score { flex-wrap: nowrap; overflow: hidden; }
523
+
524
+ .aparte-place__list {
525
+ margin: 0;
526
+ padding: 0;
527
+ list-style: none;
528
+ }
529
+
530
+ .aparte-place__list > li + li { border-block-start: var(--aparte-border-width) solid var(--aparte-border); }
531
+
532
+ .aparte-place__list > .aparte-place__row:hover,
533
+ .aparte-place-card[data-variant="compact"] > .aparte-place__row:hover {
534
+ background: color-mix(in oklab, var(--aparte-text) 4%, transparent);
535
+ }
536
+
537
+ .aparte-place-card[data-variant="compact"] > .aparte-place__heading {
538
+ padding: var(--aparte-space-5) var(--aparte-space-6) var(--aparte-space-3);
539
+ border-block-end: var(--aparte-border-width) solid var(--aparte-border);
540
+ }
541
+
542
+ /* ── Detail ──────────────────────────────────────────────────────────── */
543
+
544
+ .aparte-place--detail .aparte-place__media--wide { aspect-ratio: var(--aparte-place-detail-ratio); }
545
+
546
+ .aparte-place--detail .aparte-place__body {
547
+ gap: var(--aparte-space-7);
548
+ padding: var(--aparte-space-7) var(--aparte-space-8) var(--aparte-space-8);
549
+ }
550
+
551
+ .aparte-place__head {
552
+ display: flex;
553
+ flex-direction: column;
554
+ gap: var(--aparte-space-2);
555
+ min-inline-size: 0;
556
+ }
557
+
558
+ .aparte-place--detail .aparte-place__name {
559
+ font-size: var(--aparte-font-size-xl);
560
+ white-space: normal;
561
+ }
562
+
563
+ .aparte-place--detail .aparte-place__score,
564
+ .aparte-place--detail .aparte-place__status { font-size: var(--aparte-font-size-md); }
565
+
566
+ .aparte-place--detail .aparte-place__actions { margin: 0; }
567
+
568
+ .aparte-place__info {
569
+ display: grid;
570
+ gap: var(--aparte-space-5);
571
+ margin: 0;
572
+ padding: var(--aparte-space-6) 0 0;
573
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
574
+ list-style: none;
575
+ font-size: var(--aparte-font-size-md);
576
+ }
577
+
578
+ .aparte-place__info > li {
579
+ display: flex;
580
+ align-items: flex-start;
581
+ gap: var(--aparte-space-4);
582
+ min-inline-size: 0;
583
+ overflow-wrap: anywhere;
584
+ }
585
+
586
+ .aparte-place__info .aparte-place__line-glyph { margin-block-start: 0.1em; }
587
+ .aparte-place__info .aparte-place__address-text { white-space: normal; }
588
+ .aparte-place__info .aparte-place__link { color: var(--aparte-primary-ink, var(--aparte-text)); }
589
+
590
+ .aparte-place__hours {
591
+ inline-size: 100%;
592
+ border-collapse: collapse;
593
+ font-size: var(--aparte-font-size-md);
594
+ }
595
+
596
+ .aparte-place__hours-title {
597
+ padding-block: var(--aparte-space-6) var(--aparte-space-3);
598
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
599
+ font-weight: var(--aparte-font-weight-semibold);
600
+ text-align: start;
601
+ }
602
+
603
+ .aparte-place__hours-title .aparte-place__line-glyph {
604
+ margin-inline-end: var(--aparte-space-3);
605
+ vertical-align: -0.15em;
606
+ }
607
+
608
+ .aparte-place__hours th,
609
+ .aparte-place__hours td {
610
+ padding-block: var(--aparte-space-2);
611
+ vertical-align: top;
612
+ }
613
+
614
+ .aparte-place__hours th {
615
+ padding-inline: var(--aparte-space-3) var(--aparte-space-6);
616
+ color: var(--aparte-text-muted);
617
+ font-weight: var(--aparte-font-weight-normal);
618
+ text-align: start;
619
+ white-space: nowrap;
620
+ }
621
+
622
+ /* \`Intl\` writes "lundi" for the middle of a sentence; a row's label starts with a capital. */
623
+ .aparte-place__hours th::first-letter { text-transform: uppercase; }
624
+
625
+ .aparte-place__hours td {
626
+ padding-inline-end: var(--aparte-space-3);
627
+ text-align: end;
628
+ }
629
+
630
+ .aparte-place__hours tr[data-closed] td { color: var(--aparte-place-closed); }
631
+
632
+ /* Today: the first row, on a band of the accent, in full ink. */
633
+ .aparte-place__hours tr[aria-current] > * {
634
+ background: color-mix(in oklab, var(--aparte-primary) 10%, transparent);
635
+ color: var(--aparte-text);
636
+ font-weight: var(--aparte-font-weight-semibold);
637
+ }
638
+
639
+ .aparte-place__hours tr[aria-current] > :first-child {
640
+ border-start-start-radius: var(--aparte-radius-md);
641
+ border-end-start-radius: var(--aparte-radius-md);
642
+ }
643
+
644
+ .aparte-place__hours tr[aria-current] > :last-child {
645
+ border-start-end-radius: var(--aparte-radius-md);
646
+ border-end-end-radius: var(--aparte-radius-md);
647
+ }
648
+
649
+ .aparte-place__tags {
650
+ display: flex;
651
+ flex-wrap: wrap;
652
+ gap: var(--aparte-space-2);
653
+ margin: 0;
654
+ padding: 0;
655
+ list-style: none;
656
+ }
657
+
658
+ /* ── Loading ─────────────────────────────────────────────────────────── */
659
+
660
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-media {
661
+ block-size: auto;
662
+ border-radius: 0;
663
+ }
664
+
665
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-thumb {
666
+ flex: none;
667
+ block-size: var(--aparte-place-thumb-size);
668
+ border-radius: var(--aparte-radius-lg);
669
+ }
670
+
671
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-name { inline-size: 60%; block-size: 1.1rem; }
672
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-line { inline-size: 80%; }
673
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-line--short { inline-size: 50%; }
674
+
675
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-actions {
676
+ display: flex;
677
+ gap: var(--aparte-space-3);
678
+ margin-block-start: var(--aparte-space-5);
679
+ }
680
+
681
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-btn {
682
+ inline-size: 6rem;
683
+ block-size: 2rem;
684
+ border-radius: var(--aparte-radius-full);
685
+ }
686
+
687
+ .aparte-place-card[data-state="loading"] .aparte-place__sk-hours {
688
+ display: grid;
689
+ gap: var(--aparte-space-3);
690
+ margin-block-start: var(--aparte-space-5);
691
+ }
692
+
693
+ /* ── Small screens, and stillness ────────────────────────────────────── */
694
+
695
+ @media (max-width: 380px) {
696
+ .aparte-place__body { padding: var(--aparte-space-5) var(--aparte-space-6) var(--aparte-space-6); }
697
+ .aparte-place--detail .aparte-place__body { padding: var(--aparte-space-6); }
698
+ .aparte-place__actions > .aparte-place__action:not(.aparte-btn--icon) { flex: 1 1 auto; justify-content: center; }
699
+ .aparte-place__collection .aparte-rich-card__carousel { --aparte-rich-card-slide-size: 84%; }
700
+ .aparte-place__hours th { padding-inline-end: var(--aparte-space-4); }
701
+ .aparte-place-card { --aparte-place-thumb-size: 3rem; }
702
+ }
703
+
704
+ @media (prefers-reduced-motion: reduce) {
705
+ .aparte-place--tile:hover .aparte-place__img,
706
+ .aparte-place-card[data-variant="card"] > .aparte-place:hover .aparte-place__img { transform: none; }
707
+ .aparte-place__status-dot::after { content: none; }
708
+ }
709
+ `;
710
+ const P = {
711
+ cardsPlaceTitle: { en: "Places", fr: "Lieux", de: "Orte", es: "Lugares", it: "Luoghi", ja: "場所", ko: "장소", pt: "Lugares", ru: "Места", zh: "地点" },
712
+ cardsPlaceCount: { en: "{n} places", fr: "{n} lieux", de: "{n} Orte", es: "{n} lugares", it: "{n} luoghi", ja: "{n}件の場所", ko: "장소 {n}곳", pt: "{n} lugares", ru: "Мест: {n}", zh: "{n} 个地点" },
713
+ cardsPlaceOpen: { en: "Open", fr: "Ouvert", de: "Geöffnet", es: "Abierto", it: "Aperto", ja: "営業中", ko: "영업 중", pt: "Aberto", ru: "Открыто", zh: "营业中" },
714
+ cardsPlaceClosed: { en: "Closed", fr: "Fermé", de: "Geschlossen", es: "Cerrado", it: "Chiuso", ja: "営業時間外", ko: "영업 종료", pt: "Fechado", ru: "Закрыто", zh: "已打烊" },
715
+ cardsPlaceClosesSoon: { en: "Closes soon", fr: "Ferme bientôt", de: "Schließt bald", es: "Cierra pronto", it: "Chiude a breve", ja: "まもなく営業終了", ko: "곧 영업 종료", pt: "Fecha em breve", ru: "Скоро закроется", zh: "即将打烊" },
716
+ cardsPlaceClosesAt: { en: "Closes at {t}", fr: "Ferme à {t}", de: "Schließt um {t}", es: "Cierra a las {t}", it: "Chiude alle {t}", ja: "{t}に営業終了", ko: "{t}에 영업 종료", pt: "Fecha às {t}", ru: "Закроется в {t}", zh: "{t}打烊" },
717
+ cardsPlaceClosesOn: { en: "Closes {day} at {t}", fr: "Ferme {day} à {t}", de: "Schließt {day} um {t}", es: "Cierra el {day} a las {t}", it: "Chiude {day} alle {t}", ja: "{day}の{t}に営業終了", ko: "{day} {t}에 영업 종료", pt: "Fecha {day} às {t}", ru: "Закроется: {day}, {t}", zh: "{day}{t}打烊" },
718
+ cardsPlaceOpensAt: { en: "Opens at {t}", fr: "Ouvre à {t}", de: "Öffnet um {t}", es: "Abre a las {t}", it: "Apre alle {t}", ja: "{t}に営業開始", ko: "{t}에 영업 시작", pt: "Abre às {t}", ru: "Откроется в {t}", zh: "{t}开始营业" },
719
+ cardsPlaceOpensTomorrow: { en: "Opens tomorrow at {t}", fr: "Ouvre demain à {t}", de: "Öffnet morgen um {t}", es: "Abre mañana a las {t}", it: "Apre domani alle {t}", ja: "明日{t}に営業開始", ko: "내일 {t}에 영업 시작", pt: "Abre amanhã às {t}", ru: "Откроется завтра в {t}", zh: "明天{t}开始营业" },
720
+ cardsPlaceOpensOn: { en: "Opens {day} at {t}", fr: "Ouvre {day} à {t}", de: "Öffnet {day} um {t}", es: "Abre el {day} a las {t}", it: "Apre {day} alle {t}", ja: "{day}の{t}に営業開始", ko: "{day} {t}에 영업 시작", pt: "Abre {day} às {t}", ru: "Откроется: {day}, {t}", zh: "{day}{t}开始营业" },
721
+ cardsPlaceAlwaysOpen: { en: "Open 24 hours", fr: "Ouvert 24 h/24", de: "Rund um die Uhr geöffnet", es: "Abierto 24 horas", it: "Aperto 24 ore su 24", ja: "24時間営業", ko: "24시간 영업", pt: "Aberto 24 horas", ru: "Круглосуточно", zh: "24小时营业" },
722
+ cardsPlaceDirections: { en: "Directions", fr: "Itinéraire", de: "Route", es: "Cómo llegar", it: "Indicazioni", ja: "ルート", ko: "길찾기", pt: "Rotas", ru: "Маршрут", zh: "路线" },
723
+ cardsPlaceCall: { en: "Call", fr: "Appeler", de: "Anrufen", es: "Llamar", it: "Chiama", ja: "電話", ko: "전화", pt: "Ligar", ru: "Позвонить", zh: "致电" },
724
+ cardsPlaceWebsite: { en: "Website", fr: "Site web", de: "Website", es: "Sitio web", it: "Sito web", ja: "ウェブサイト", ko: "웹사이트", pt: "Site", ru: "Сайт", zh: "网站" },
725
+ cardsPlaceHours: { en: "Hours", fr: "Horaires", de: "Öffnungszeiten", es: "Horario", it: "Orari", ja: "営業時間", ko: "영업시간", pt: "Horário", ru: "Часы работы", zh: "营业时间" },
726
+ cardsPlacePrice: { en: "Price level {level} of {max}", fr: "Gamme de prix {level} sur {max}", de: "Preisniveau {level} von {max}", es: "Nivel de precio {level} de {max}", it: "Fascia di prezzo {level} su {max}", ja: "価格帯 {max}段階中{level}", ko: "가격대 {max}단계 중 {level}", pt: "Faixa de preço {level} de {max}", ru: "Уровень цен: {level} из {max}", zh: "价格等级 {level}/{max}" },
727
+ cardsPlacePhoto: { en: "Photo of {name}", fr: "Photo de {name}", de: "Foto von {name}", es: "Foto de {name}", it: "Foto di {name}", ja: "{name}の写真", ko: "{name} 사진", pt: "Foto de {name}", ru: "Фото: {name}", zh: "{name}的照片" }
728
+ };
729
+ const KIND_WORDS = {
730
+ restaurant: { en: "Restaurant", fr: "Restaurant", de: "Restaurant", es: "Restaurante", it: "Ristorante", ja: "レストラン", ko: "음식점", pt: "Restaurante", ru: "Ресторан", zh: "餐厅" },
731
+ cafe: { en: "Café", fr: "Café", de: "Café", es: "Cafetería", it: "Caffè", ja: "カフェ", ko: "카페", pt: "Café", ru: "Кафе", zh: "咖啡馆" },
732
+ bar: { en: "Bar", fr: "Bar", de: "Bar", es: "Bar", it: "Bar", ja: "バー", ko: "바", pt: "Bar", ru: "Бар", zh: "酒吧" },
733
+ hotel: { en: "Hotel", fr: "Hôtel", de: "Hotel", es: "Hotel", it: "Hotel", ja: "ホテル", ko: "호텔", pt: "Hotel", ru: "Отель", zh: "酒店" },
734
+ shop: { en: "Shop", fr: "Boutique", de: "Geschäft", es: "Tienda", it: "Negozio", ja: "ショップ", ko: "상점", pt: "Loja", ru: "Магазин", zh: "商店" },
735
+ museum: { en: "Museum", fr: "Musée", de: "Museum", es: "Museo", it: "Museo", ja: "博物館", ko: "박물관", pt: "Museu", ru: "Музей", zh: "博物馆" },
736
+ park: { en: "Park", fr: "Parc", de: "Park", es: "Parque", it: "Parco", ja: "公園", ko: "공원", pt: "Parque", ru: "Парк", zh: "公园" },
737
+ gym: { en: "Gym", fr: "Salle de sport", de: "Fitnessstudio", es: "Gimnasio", it: "Palestra", ja: "ジム", ko: "헬스장", pt: "Academia", ru: "Спортзал", zh: "健身房" },
738
+ pharmacy: { en: "Pharmacy", fr: "Pharmacie", de: "Apotheke", es: "Farmacia", it: "Farmacia", ja: "薬局", ko: "약국", pt: "Farmácia", ru: "Аптека", zh: "药店" }
739
+ };
740
+ const MAX_PLACES = 10;
741
+ const MAX_TAGS = 4;
742
+ const PRICE_MAX = 4;
743
+ const SOON = 60;
744
+ const DAY = 1440;
745
+ const WEEK = 7 * DAY;
746
+ const DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
747
+ function readDay(value) {
748
+ const n = num(value);
749
+ if (n !== void 0) return Number.isInteger(n) && n >= 0 && n <= 7 ? n % 7 : void 0;
750
+ if (typeof value !== "string") return void 0;
751
+ const i = DAY_NAMES.indexOf(value.trim().slice(0, 3).toLowerCase());
752
+ return i >= 0 ? i : void 0;
753
+ }
754
+ function readTime(value) {
755
+ if (value && typeof value === "object") {
756
+ const o = value;
757
+ const h2 = num(o["hour"]);
758
+ const m2 = num(o["minute"]) ?? 0;
759
+ return h2 !== void 0 && h2 >= 0 && h2 <= 24 && m2 >= 0 && m2 < 60 ? h2 * 60 + m2 : void 0;
760
+ }
761
+ const s = str(value);
762
+ if (!s) return void 0;
763
+ const match = /^(\d{1,2})(?:\s*[:h.]\s*)?(\d{2})?\s*(?:([ap])\.?\s*m\.?)?$/i.exec(s);
764
+ if (!match) return void 0;
765
+ let h = Number(match[1]);
766
+ const m = match[2] ? Number(match[2]) : 0;
767
+ const half = match[3]?.toLowerCase();
768
+ if (m > 59) return void 0;
769
+ if (half) {
770
+ if (h < 1 || h > 12) return void 0;
771
+ h = h % 12 + (half === "p" ? 12 : 0);
772
+ }
773
+ if (h > 24 || h === 24 && m > 0) return void 0;
774
+ return h * 60 + m;
775
+ }
776
+ function rangesOfText(day, value) {
777
+ const parts = Array.isArray(value) ? value : typeof value === "string" ? value.split(/\s*[,;]\s*/) : [];
778
+ const out = [];
779
+ for (const part of parts) {
780
+ if (typeof part !== "string") continue;
781
+ const [from, to] = part.split(/\s*[-–—]\s*|\s+to\s+/i);
782
+ if (to === void 0 && /24\s*(h|hours|heures|\/\s*24)/i.test(part)) {
783
+ out.push({ day, start: 0, end: DAY });
784
+ continue;
785
+ }
786
+ let end = readTime(to);
787
+ const start = sharedMeridiem(from, to, end) ?? readTime(from);
788
+ if (start === void 0 || end === void 0) continue;
789
+ if (end <= start) end += DAY;
790
+ out.push({ day, start, end });
791
+ }
792
+ return out;
793
+ }
794
+ function sharedMeridiem(from, to, end) {
795
+ const MERIDIEM = /([ap])\.?\s*m\.?\s*$/i;
796
+ const half = to !== void 0 ? MERIDIEM.exec(to) : null;
797
+ if (!from || !half || end === void 0 || /[ap]\.?\s*m\.?\s*$/i.test(from)) return void 0;
798
+ const start = readTime(`${from} ${half[1]}M`);
799
+ return start !== void 0 && start < end ? start : void 0;
800
+ }
801
+ function splitLong(range) {
802
+ if (range.end - range.start <= DAY) return [range];
803
+ const out = [];
804
+ let at = range.day * DAY + range.start;
805
+ const stop = Math.min(range.day * DAY + range.end, range.day * DAY + range.start + WEEK);
806
+ while (at < stop) {
807
+ const midnight = (Math.floor(at / DAY) + 1) * DAY;
808
+ const end = Math.min(stop, midnight);
809
+ out.push({ day: Math.floor(at / DAY) % 7, start: at % DAY, end: end - Math.floor(at / DAY) * DAY });
810
+ at = end;
811
+ }
812
+ return out;
813
+ }
814
+ function readHours(raw) {
815
+ if (!raw || typeof raw !== "object") return [];
816
+ if (!Array.isArray(raw)) {
817
+ const o = raw;
818
+ for (const key of ["periods", "weekday_text", "weekdayDescriptions"]) {
819
+ if (Array.isArray(o[key])) return readHours(o[key]);
820
+ }
821
+ const out2 = [];
822
+ for (const [key, value] of Object.entries(o)) {
823
+ if (!/^[a-z]+$/i.test(key)) continue;
824
+ const day = readDay(key);
825
+ if (day !== void 0) out2.push(...rangesOfText(day, value));
826
+ }
827
+ return tidy(out2);
828
+ }
829
+ const yelp = raw.filter((item) => !!item && typeof item === "object" && Array.isArray(item["open"]));
830
+ if (yelp.length) return tidy(yelpRanges(yelp));
831
+ const out = [];
832
+ for (const item of raw) {
833
+ if (typeof item === "string") {
834
+ const line = /^\s*([A-Za-z]+)\s*:\s*(.+)$/.exec(item);
835
+ const day2 = line ? readDay(line[1]) : void 0;
836
+ if (line && day2 !== void 0) out.push(...rangesOfText(day2, line[2]));
837
+ continue;
838
+ }
839
+ if (!item || typeof item !== "object") continue;
840
+ const r = item;
841
+ if (typeof r["start"] === "number" && typeof r["end"] === "number") {
842
+ const day2 = readDay(r["day"]);
843
+ if (day2 !== void 0 && r["end"] > r["start"]) out.push({ day: day2, start: r["start"], end: r["end"] });
844
+ continue;
845
+ }
846
+ if (r["open"] && typeof r["open"] === "object") {
847
+ const o = r["open"];
848
+ const day2 = readDay(o["day"]);
849
+ const start2 = readTime(o["time"] ?? o);
850
+ if (day2 === void 0 || start2 === void 0) continue;
851
+ const c = r["close"];
852
+ if (!c || typeof c !== "object") {
853
+ out.push({ day: day2, start: start2, end: day2 === 0 && start2 === 0 ? WEEK : DAY });
854
+ continue;
855
+ }
856
+ const cDay = readDay(c["day"]);
857
+ const cTime = readTime(c["time"] ?? c);
858
+ if (cDay === void 0 || cTime === void 0) continue;
859
+ let end2 = (cDay - day2 + 7) % 7 * DAY + cTime;
860
+ if (end2 <= start2) end2 += WEEK;
861
+ out.push({ day: day2, start: start2, end: end2 });
862
+ continue;
863
+ }
864
+ const day = readDay(r["day"] ?? r["weekday"]);
865
+ const start = readTime(r["open"] ?? r["opens"] ?? r["from"]);
866
+ if (day === void 0 || start === void 0) continue;
867
+ const close = r["close"] ?? r["closes"] ?? r["to"];
868
+ let end = close === void 0 || close === null || close === "" ? DAY : readTime(close);
869
+ if (end === void 0) continue;
870
+ if (end <= start) end += DAY;
871
+ out.push({ day, start, end });
872
+ }
873
+ return tidy(out);
874
+ }
875
+ function yelpRanges(entries) {
876
+ const regular = entries.filter((e) => !str(e["hour_type"]) || /regular/i.test(String(e["hour_type"])));
877
+ const out = [];
878
+ for (const entry of regular.length ? regular : entries.slice(0, 1)) {
879
+ for (const range of entry["open"]) {
880
+ const r = range ?? {};
881
+ const n = num(r["day"]);
882
+ const hhmm = (v) => readTime(str(v)?.padStart(4, "0"));
883
+ const start = hhmm(r["start"]);
884
+ let end = hhmm(r["end"]);
885
+ if (n === void 0 || !Number.isInteger(n) || n < 0 || n > 6 || start === void 0 || end === void 0) continue;
886
+ if (end <= start) end += DAY;
887
+ out.push({ day: (n + 1) % 7, start, end });
888
+ }
889
+ }
890
+ return out;
891
+ }
892
+ function tidy(ranges) {
893
+ return ranges.flatMap(splitLong).sort((a, b) => a.day - b.day || a.start - b.start);
894
+ }
895
+ function spans(ranges) {
896
+ const list = [];
897
+ for (const r of ranges) {
898
+ for (const shift of [-WEEK, 0, WEEK]) list.push([r.day * DAY + r.start + shift, r.day * DAY + r.end + shift]);
899
+ }
900
+ list.sort((a, b) => a[0] - b[0]);
901
+ const merged = [];
902
+ for (const span of list) {
903
+ const last = merged[merged.length - 1];
904
+ if (last && span[0] <= last[1]) last[1] = Math.max(last[1], span[1]);
905
+ else merged.push([span[0], span[1]]);
906
+ }
907
+ return merged;
908
+ }
909
+ function placeStatus(hours, date) {
910
+ const ranges = readHours(hours);
911
+ if (!ranges.length) return null;
912
+ const t = date.getDay() * DAY + date.getHours() * 60 + date.getMinutes();
913
+ const at = (minute) => new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay() + Math.floor(minute / DAY), 0, minute % DAY);
914
+ const list = spans(ranges);
915
+ const inside = list.find(([s, e]) => s <= t && t < e);
916
+ if (inside) return inside[1] - inside[0] >= WEEK ? { open: true } : { open: true, changesAt: at(inside[1]) };
917
+ const next = list.find(([s]) => s > t);
918
+ return next ? { open: false, changesAt: at(next[0]) } : { open: false };
919
+ }
920
+ function isOpenAt(hours, date) {
921
+ return placeStatus(hours, date)?.open ?? false;
922
+ }
923
+ function placeTime(place, now) {
924
+ if (place.utcOffset === void 0) return now;
925
+ const t = new Date(now.getTime() + place.utcOffset * 6e4);
926
+ return new Date(t.getUTCFullYear(), t.getUTCMonth(), t.getUTCDate(), t.getUTCHours(), t.getUTCMinutes(), t.getUTCSeconds(), t.getUTCMilliseconds());
927
+ }
928
+ const KIND_RULES = [
929
+ ["cafe", /\bcaf[eé]s?\b|coffee|tea ?room|bakery|boulangerie|p[aâ]tisserie|salon de th[eé]/],
930
+ ["bar", /\bbars?\b|\bpubs?\b|brewery|\bwine\b|cocktail|night ?club/],
931
+ ["restaurant", /restaurant|\bfood\b|meal|pizz|sushi|burger|bistro|brasserie|diner|eatery|trattoria|ramen|steak|cr[eê]perie/],
932
+ ["hotel", /h[oô]tel|lodging|hostel|motel|\binn\b|resort|guest ?house|bed and breakfast|\bb ?& ?b\b/],
933
+ ["museum", /museum|mus[eé]e|galler|monument|landmark|tourist attraction|\bart\b/],
934
+ ["park", /\bparks?\b|\bparcs?\b|garden|jardin|beach|plage|\bzoo\b|campground/],
935
+ ["gym", /\bgyms?\b|fitness|stadium|swimming|piscine/],
936
+ ["pharmacy", /pharmac|drugstore|chemist/],
937
+ ["shop", /shop|store|boutique|market|\bmall\b|grocery|supermarch|magasin|[eé]picerie/]
938
+ ];
939
+ function readKind(candidates) {
940
+ for (const candidate of candidates) {
941
+ const text = str(candidate)?.toLowerCase().replace(/[_-]+/g, " ");
942
+ if (!text) continue;
943
+ for (const [kind, rule] of KIND_RULES) if (rule.test(text)) return kind;
944
+ }
945
+ return "place";
946
+ }
947
+ const PRICE_WORDS = {
948
+ inexpensive: 1,
949
+ cheap: 1,
950
+ moderate: 2,
951
+ expensive: 3,
952
+ very_expensive: 4,
953
+ "very expensive": 4
954
+ };
955
+ function readPrice(value) {
956
+ const n = num(value);
957
+ if (n !== void 0) return n >= 1 && n <= PRICE_MAX ? { price: { level: Math.round(n) } } : {};
958
+ const s = str(value);
959
+ if (!s) return {};
960
+ const repeated = /^(\p{Sc})\1{0,3}$/u.exec(s);
961
+ if (repeated) return { price: { level: s.length / repeated[1].length, symbol: repeated[1] } };
962
+ const named = PRICE_WORDS[s.toLowerCase().replace(/^price_level_/, "")];
963
+ if (named) return { price: { level: named } };
964
+ if (/^price_level_/i.test(s)) return {};
965
+ return s.length <= 24 ? { priceText: s } : {};
966
+ }
967
+ function readCoordinates(r) {
968
+ const candidates = [r["coordinates"], r["geometry"]?.["location"], r["location"], r["position"], r["latlng"], r];
969
+ for (const c of candidates) {
970
+ if (!c || typeof c !== "object") continue;
971
+ const lat = num(c["lat"] ?? c["latitude"]);
972
+ const lng = num(c["lng"] ?? c["lon"] ?? c["long"] ?? c["longitude"]);
973
+ if (lat !== void 0 && lng !== void 0 && Math.abs(lat) <= 90 && Math.abs(lng) <= 180) return { lat, lng };
974
+ }
975
+ return void 0;
976
+ }
977
+ function webUrl(value, guessScheme = false) {
978
+ let s = str(value);
979
+ if (!s) return void 0;
980
+ if (s.startsWith("//")) {
981
+ s = `https:${s}`;
982
+ } else if (!/^https?:\/\//i.test(s)) {
983
+ if (!guessScheme || !/^[\w-]+(\.[\w-]+)+(\/|$)/.test(s)) return void 0;
984
+ s = `https://${s}`;
985
+ }
986
+ const url = safeUrl(s);
987
+ return url && /^https?:/i.test(url) ? url : void 0;
988
+ }
989
+ function textOf(value) {
990
+ if (value && typeof value === "object") {
991
+ const o = value;
992
+ return str(o["text"] ?? o["title"] ?? o["name"] ?? o["url"]);
993
+ }
994
+ return str(value);
995
+ }
996
+ function linkOf(value) {
997
+ if (value && typeof value === "object") {
998
+ const o = value;
999
+ return str(o["url"] ?? o["src"] ?? o["href"] ?? o["uri"]);
1000
+ }
1001
+ return str(value);
1002
+ }
1003
+ function firstOf(value) {
1004
+ return Array.isArray(value) ? value[0] : value;
1005
+ }
1006
+ function readPlace(raw) {
1007
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
1008
+ const r = raw;
1009
+ const name = textOf(r["name"] ?? r["displayName"] ?? r["title"]);
1010
+ if (!name) return null;
1011
+ const category = textOf(r["category"] ?? r["primaryTypeDisplayName"] ?? firstOf(r["categories"]));
1012
+ const types = Array.isArray(r["types"]) ? r["types"] : [];
1013
+ let ratingValue = num(r["rating"] ?? r["stars"]);
1014
+ if (ratingValue !== void 0 && ratingValue > 5 && ratingValue <= 10) ratingValue /= 2;
1015
+ if (ratingValue !== void 0 && (ratingValue < 0 || ratingValue > 5)) ratingValue = void 0;
1016
+ const reviewCount = num(r["reviewCount"] ?? r["review_count"] ?? r["user_ratings_total"] ?? r["userRatingCount"] ?? r["ratingCount"] ?? r["rating_count"] ?? r["reviews"]);
1017
+ const rawDistance = r["distance"] ?? r["distanceMeters"] ?? r["distance_meters"] ?? r["distance_m"];
1018
+ const distance = num(rawDistance);
1019
+ const hourSources = [r["hours"], r["currentOpeningHours"], r["current_opening_hours"], r["regularOpeningHours"], r["opening_hours"], r["openingHours"]].filter((h) => h && typeof h === "object");
1020
+ let hours = [];
1021
+ for (const source of hourSources) {
1022
+ hours = readHours(source);
1023
+ if (hours.length) break;
1024
+ }
1025
+ const openSource = r["openNow"] ?? r["open_now"] ?? r["isOpen"] ?? r["is_open"] ?? r["is_open_now"] ?? hourSources.map((h) => Array.isArray(h) ? h[0] : h).map((h) => h?.["openNow"] ?? h?.["open_now"] ?? h?.["is_open_now"]).find((v) => typeof v === "boolean");
1026
+ const tags = (Array.isArray(r["tags"]) ? r["tags"] : Array.isArray(r["features"]) ? r["features"] : Array.isArray(r["amenities"]) ? r["amenities"] : []).map(textOf).filter((t) => !!t && t.length <= 32).slice(0, MAX_TAGS);
1027
+ const utcOffset = num(r["utcOffset"] ?? r["utc_offset_minutes"] ?? r["utcOffsetMinutes"] ?? r["utc_offset"]);
1028
+ return {
1029
+ name,
1030
+ category,
1031
+ kind: readKind([category, r["primaryType"], ...types]),
1032
+ image: webUrl(linkOf(r["image"] ?? r["photo"] ?? r["imageUrl"] ?? r["image_url"] ?? r["photoUrl"] ?? r["thumbnail"] ?? r["picture"] ?? firstOf(r["photos"]))),
1033
+ rating: ratingValue,
1034
+ reviewCount: reviewCount !== void 0 && reviewCount >= 0 ? Math.round(reviewCount) : void 0,
1035
+ ...readPrice(r["priceLevel"] ?? r["price_level"] ?? r["price"]),
1036
+ address: str(r["address"] ?? r["formatted_address"] ?? r["formattedAddress"] ?? r["shortFormattedAddress"] ?? r["vicinity"] ?? (typeof r["location"] === "string" ? r["location"] : void 0)) ?? (Array.isArray(r["location"]?.["display_address"]) ? str(r["location"]["display_address"].map(str).filter(Boolean).join(", ")) : void 0),
1037
+ distance: distance !== void 0 && distance >= 0 ? distance : void 0,
1038
+ distanceText: distance === void 0 && typeof rawDistance === "string" ? str(rawDistance) : void 0,
1039
+ phone: str(r["phone"] ?? r["phoneNumber"] ?? r["phone_number"] ?? r["formatted_phone_number"] ?? r["nationalPhoneNumber"] ?? r["international_phone_number"] ?? r["internationalPhoneNumber"] ?? r["tel"]),
1040
+ website: webUrl(r["website"] ?? r["websiteUri"] ?? r["websiteUrl"] ?? r["site"], true),
1041
+ url: webUrl(r["url"] ?? r["googleMapsUri"] ?? r["mapsUrl"] ?? r["maps_url"] ?? r["link"]),
1042
+ coordinates: readCoordinates(r),
1043
+ placeId: str(r["placeId"] ?? r["place_id"] ?? r["googlePlaceId"]),
1044
+ openNow: typeof openSource === "boolean" ? openSource : void 0,
1045
+ hours,
1046
+ utcOffset: utcOffset !== void 0 && Math.abs(utcOffset) <= 14 * 60 ? utcOffset : void 0,
1047
+ tags
1048
+ };
1049
+ }
1050
+ function readPlaces(raw) {
1051
+ if (!raw || typeof raw !== "object") return null;
1052
+ let title;
1053
+ let list;
1054
+ if (Array.isArray(raw)) {
1055
+ list = raw;
1056
+ } else {
1057
+ const r = raw;
1058
+ const nested = r["places"] ?? r["results"] ?? r["candidates"];
1059
+ if (Array.isArray(nested)) {
1060
+ list = nested;
1061
+ title = str(r["title"]);
1062
+ } else {
1063
+ list = [raw];
1064
+ }
1065
+ }
1066
+ const places = list.map(readPlace).filter((p) => p !== null).slice(0, MAX_PLACES);
1067
+ return places.length ? { ...title ? { title } : {}, places } : null;
1068
+ }
1069
+ function directionsUrl(place) {
1070
+ const destination = place.coordinates ? `${place.coordinates.lat},${place.coordinates.lng}` : place.address ? `${place.name}, ${place.address}` : null;
1071
+ if (!destination) return null;
1072
+ const id = place.placeId ? `&destination_place_id=${encodeURIComponent(place.placeId)}` : "";
1073
+ return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(destination)}${id}`;
1074
+ }
1075
+ function phoneUrl(phone) {
1076
+ if (!phone) return null;
1077
+ const number = phone.replace(/\s*(?:\b(?:ext(?:ension)?\.?|x)\s*\d|#).*$/i, "");
1078
+ const digits = number.replace(/\D/g, "");
1079
+ const plus = /^[^\d]*\+/.test(number) ? "+" : "";
1080
+ return /\d{3,}/.test(digits) ? safeUrl(`tel:${plus}${digits}`) : null;
1081
+ }
1082
+ function clock(config, date) {
1083
+ const tag = intlTag(config);
1084
+ const twelve = /h1[12]/.test(new Intl.DateTimeFormat(tag, { hour: "numeric" }).resolvedOptions().hourCycle ?? "");
1085
+ return twelve ? new Intl.DateTimeFormat(tag, { hour: "numeric", minute: date.getMinutes() ? "2-digit" : void 0 }).format(date) : new Intl.DateTimeFormat(tag, { hour: "2-digit", minute: "2-digit" }).format(date);
1086
+ }
1087
+ const weekday = (config, date) => new Intl.DateTimeFormat(intlTag(config), { weekday: "long" }).format(date);
1088
+ const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
1089
+ const daysBetween = (from, to) => Math.round((startOfDay(to) - startOfDay(from)) / 864e5);
1090
+ function statusView(place, now) {
1091
+ const local = placeTime(place, now);
1092
+ const status = place.hours.length ? placeStatus(place.hours, local) : null;
1093
+ if (!status) {
1094
+ if (place.openNow === void 0) return null;
1095
+ return { tone: place.openNow ? "open" : "closed", state: place.openNow ? "cardsPlaceOpen" : "cardsPlaceClosed" };
1096
+ }
1097
+ const at = status.changesAt;
1098
+ if (status.open) {
1099
+ if (!at) return { tone: "open", state: "cardsPlaceAlwaysOpen" };
1100
+ const minutes = (at.getTime() - local.getTime()) / 6e4;
1101
+ if (minutes < SOON) return { tone: "soon", state: "cardsPlaceClosesSoon", next: (c) => clock(c, at) };
1102
+ return {
1103
+ tone: "open",
1104
+ state: "cardsPlaceOpen",
1105
+ // Past midnight is still "closes at 02:00"; only a close more than a day away names its day.
1106
+ next: minutes <= DAY ? (c) => word(c, "cardsPlaceClosesAt", P.cardsPlaceClosesAt, { t: clock(c, at) }) : (c) => word(c, "cardsPlaceClosesOn", P.cardsPlaceClosesOn, { day: weekday(c, at), t: clock(c, at) })
1107
+ };
1108
+ }
1109
+ if (!at) return { tone: "closed", state: "cardsPlaceClosed" };
1110
+ const days = daysBetween(local, at);
1111
+ return {
1112
+ tone: "closed",
1113
+ state: "cardsPlaceClosed",
1114
+ next: days === 0 ? (c) => word(c, "cardsPlaceOpensAt", P.cardsPlaceOpensAt, { t: clock(c, at) }) : days === 1 ? (c) => word(c, "cardsPlaceOpensTomorrow", P.cardsPlaceOpensTomorrow, { t: clock(c, at) }) : (c) => word(c, "cardsPlaceOpensOn", P.cardsPlaceOpensOn, { day: weekday(c, at), t: clock(c, at) })
1115
+ };
1116
+ }
1117
+ function distanceText(config, metres) {
1118
+ if (metres < 1e3) return formatUnit(config, metres < 100 ? Math.round(metres) : Math.round(metres / 10) * 10, "meter");
1119
+ const km = metres / 1e3;
1120
+ return formatUnit(config, km, "kilometer", km < 10 ? 1 : 0);
1121
+ }
1122
+ const dot = () => el("span", { class: "aparte-place__dot", attrs: { "aria-hidden": "true" }, text: "·" });
1123
+ function joined(className, parts, tag = "div") {
1124
+ const present = parts.filter((p) => p !== null);
1125
+ if (!present.length) return null;
1126
+ const line = el(tag, { class: className });
1127
+ present.forEach((part, i) => {
1128
+ if (i) line.append(dot());
1129
+ line.append(part);
1130
+ });
1131
+ return line;
1132
+ }
1133
+ function nameOf(place, className = "aparte-place__name") {
1134
+ const name = el("div", { class: className });
1135
+ if (place.url) {
1136
+ name.append(el("a", { class: "aparte-place__link", attrs: { href: place.url, target: "_blank", rel: "noopener noreferrer" }, text: place.name }));
1137
+ } else {
1138
+ name.textContent = place.name;
1139
+ }
1140
+ return name;
1141
+ }
1142
+ function categoryOf(ctx, place) {
1143
+ if (place.category) return el("span", { class: "aparte-place__category", text: place.category });
1144
+ if (place.kind === "place") return null;
1145
+ const node = el("span", { class: "aparte-place__category" });
1146
+ ctx.word(node, `cardsPlace_${place.kind}`, KIND_WORDS[place.kind]);
1147
+ return node;
1148
+ }
1149
+ function priceOf(ctx, place, settings) {
1150
+ if (place.priceText) return el("span", { class: "aparte-place__price", text: place.priceText });
1151
+ if (!place.price) return null;
1152
+ const { level } = place.price;
1153
+ const sign = place.price.symbol ?? settings.currencySymbol;
1154
+ const node = el(
1155
+ "span",
1156
+ { class: "aparte-place__price", attrs: { role: "img" } },
1157
+ el("span", { class: "aparte-place__price-on", attrs: { "aria-hidden": "true" }, text: sign.repeat(level) }),
1158
+ level < PRICE_MAX ? el("span", { class: "aparte-place__price-off", attrs: { "aria-hidden": "true" }, text: sign.repeat(PRICE_MAX - level) }) : null
1159
+ );
1160
+ ctx.bind(node, () => word(ctx.config, "cardsPlacePrice", P.cardsPlacePrice, {
1161
+ level: formatNumber(ctx.config, level),
1162
+ max: formatNumber(ctx.config, PRICE_MAX)
1163
+ }), "aria-label");
1164
+ return node;
1165
+ }
1166
+ function scoreOf(ctx, place, settings) {
1167
+ return joined("aparte-place__score", [rating(ctx, place.rating, { count: place.reviewCount }), priceOf(ctx, place, settings)]);
1168
+ }
1169
+ function distanceOf(ctx, place) {
1170
+ if (place.distance !== void 0) {
1171
+ const node = el("span", { class: "aparte-place__distance" });
1172
+ ctx.bind(node, () => distanceText(ctx.config, place.distance));
1173
+ return node;
1174
+ }
1175
+ return place.distanceText ? el("span", { class: "aparte-place__distance", text: place.distanceText }) : null;
1176
+ }
1177
+ function statusOf(ctx, place, now) {
1178
+ const view = statusView(place, now);
1179
+ if (!view) return null;
1180
+ const line = el("p", { class: "aparte-place__status", attrs: { "data-status": view.tone } });
1181
+ const state = el("span", { class: "aparte-place__state" });
1182
+ ctx.word(state, view.state, P[view.state]);
1183
+ line.append(el("span", { class: "aparte-place__status-dot", attrs: { "aria-hidden": "true" } }), state);
1184
+ if (view.next) {
1185
+ const next = el("span", { class: "aparte-place__next" });
1186
+ ctx.bind(next, () => view.next(ctx.config));
1187
+ line.append(dot(), next);
1188
+ }
1189
+ return line;
1190
+ }
1191
+ function addressOf(ctx, place) {
1192
+ const distance = distanceOf(ctx, place);
1193
+ const address = place.address ? el("span", { class: "aparte-place__address-text", text: place.address }) : null;
1194
+ const line = joined("aparte-place__address", [distance, address], "p");
1195
+ if (line) line.prepend(glyph(PIN, "aparte-rich-card__glyph aparte-place__line-glyph"));
1196
+ return line;
1197
+ }
1198
+ function mediaOf(ctx, place, size) {
1199
+ const box = el("div", { class: `aparte-place__media aparte-place__media--${size}` });
1200
+ const mark = glyph(kindGlyph(place.kind), "aparte-rich-card__glyph aparte-place__marker-glyph");
1201
+ box.append(el(
1202
+ "span",
1203
+ { class: "aparte-place__placeholder", attrs: { "aria-hidden": "true" } },
1204
+ el("span", { class: "aparte-place__marker" }, mark)
1205
+ ));
1206
+ if (place.image) {
1207
+ const img = el("img", { class: "aparte-place__img", attrs: { src: place.image, loading: "lazy", decoding: "async", referrerpolicy: "no-referrer" } });
1208
+ ctx.bind(img, () => word(ctx.config, "cardsPlacePhoto", P.cardsPlacePhoto, { name: place.name }), "alt");
1209
+ img.addEventListener("error", () => {
1210
+ img.remove();
1211
+ box.setAttribute("data-empty", "");
1212
+ }, { once: true });
1213
+ box.append(img);
1214
+ } else {
1215
+ box.setAttribute("data-empty", "");
1216
+ }
1217
+ return box;
1218
+ }
1219
+ const ACTION_WORD = {
1220
+ directions: "cardsPlaceDirections",
1221
+ call: "cardsPlaceCall",
1222
+ website: "cardsPlaceWebsite"
1223
+ };
1224
+ const ACTION_GLYPH = { directions: DIRECTIONS, call: PHONE, website: GLOBE };
1225
+ function actionsOf(ctx, place, settings, iconOnly) {
1226
+ if (!settings.builtIns) return null;
1227
+ const urls = [
1228
+ ["directions", directionsUrl(place)],
1229
+ ["call", phoneUrl(place.phone)],
1230
+ ["website", place.website ?? place.url]
1231
+ ];
1232
+ const buttons = [];
1233
+ for (const [kind, url] of urls) {
1234
+ if (!url) continue;
1235
+ const label2 = () => word(ctx.config, ACTION_WORD[kind], P[ACTION_WORD[kind]]);
1236
+ const primary = kind === "directions" && !iconOnly;
1237
+ const button = ctx.action(
1238
+ { id: kind, label: label2(), url, primary },
1239
+ place,
1240
+ `aparte-place__action${iconOnly ? " aparte-btn--icon aparte-btn--circle" : ""}`
1241
+ );
1242
+ button.textContent = "";
1243
+ button.setAttribute("data-action", kind);
1244
+ button.append(glyph(ACTION_GLYPH[kind], "aparte-rich-card__glyph aparte-btn__icon"));
1245
+ if (iconOnly) {
1246
+ ctx.bind(button, label2, "aria-label");
1247
+ ctx.bind(button, label2, "title");
1248
+ } else {
1249
+ const text = el("span");
1250
+ ctx.bind(text, label2);
1251
+ button.append(text);
1252
+ }
1253
+ buttons.push(button);
1254
+ }
1255
+ return buttons.length ? el("div", { class: "aparte-place__actions" }, ...buttons) : null;
1256
+ }
1257
+ function tileOf(ctx, place, settings, now, inRow) {
1258
+ const media = mediaOf(ctx, place, "wide");
1259
+ const category = categoryOf(ctx, place);
1260
+ if (category) {
1261
+ media.append(el(
1262
+ "span",
1263
+ { class: "aparte-place__badge" },
1264
+ glyph(kindGlyph(place.kind), "aparte-rich-card__glyph aparte-place__badge-glyph"),
1265
+ category
1266
+ ));
1267
+ }
1268
+ const body = el("div", { class: "aparte-place__body" }, nameOf(place), scoreOf(ctx, place, settings), statusOf(ctx, place, now), addressOf(ctx, place));
1269
+ const actions = actionsOf(ctx, place, settings, inRow);
1270
+ if (actions) body.append(actions);
1271
+ return el("article", { class: `aparte-place${inRow ? " aparte-place--tile" : ""}`, attrs: { "data-kind": place.kind } }, media, body);
1272
+ }
1273
+ function rowOf(ctx, place, settings, now, tag) {
1274
+ const head = el("div", { class: "aparte-place__row-head" }, nameOf(place), distanceOf(ctx, place));
1275
+ const facts = joined("aparte-place__score", [
1276
+ rating(ctx, place.rating, { count: place.reviewCount }),
1277
+ priceOf(ctx, place, settings),
1278
+ categoryOf(ctx, place)
1279
+ ]);
1280
+ const text = el("div", { class: "aparte-place__text" }, head, facts, statusOf(ctx, place, now));
1281
+ return el(tag, { class: "aparte-place aparte-place__row", attrs: { "data-kind": place.kind } }, mediaOf(ctx, place, "thumb"), text);
1282
+ }
1283
+ function rangeText(config, range) {
1284
+ if (range.start === 0 && range.end >= DAY) return word(config, "cardsPlaceAlwaysOpen", P.cardsPlaceAlwaysOpen);
1285
+ const at = (minute) => clock(config, new Date(2e3, 0, 1, Math.floor(minute / 60) % 24, minute % 60));
1286
+ return `${at(range.start)}–${at(range.end)}`;
1287
+ }
1288
+ function hoursOf(ctx, place, now) {
1289
+ if (!place.hours.length) return null;
1290
+ const local = placeTime(place, now);
1291
+ const caption = el("caption", { class: "aparte-place__hours-title" });
1292
+ const title = el("span");
1293
+ ctx.word(title, "cardsPlaceHours", P.cardsPlaceHours);
1294
+ caption.append(glyph(CLOCK, "aparte-rich-card__glyph aparte-place__line-glyph"), title);
1295
+ const body = el("tbody");
1296
+ for (let i = 0; i < 7; i++) {
1297
+ const date = new Date(local.getFullYear(), local.getMonth(), local.getDate() + i, 12);
1298
+ const ranges = place.hours.filter((r) => r.day === date.getDay());
1299
+ const day = el("th", { attrs: { scope: "row" } });
1300
+ ctx.bind(day, () => weekday(ctx.config, date));
1301
+ const hours = el("td");
1302
+ ctx.bind(hours, () => ranges.length ? ranges.map((r) => rangeText(ctx.config, r)).join(", ") : word(ctx.config, "cardsPlaceClosed", P.cardsPlaceClosed));
1303
+ body.append(el("tr", { attrs: { "aria-current": i === 0 ? "date" : void 0, "data-closed": !ranges.length } }, day, hours));
1304
+ }
1305
+ return el("table", { class: "aparte-place__hours" }, caption, body);
1306
+ }
1307
+ function infoOf(ctx, place) {
1308
+ const items = [];
1309
+ const address = addressOf(ctx, place);
1310
+ if (address) items.push(el("li", {}, ...address.childNodes));
1311
+ const tel = phoneUrl(place.phone);
1312
+ if (place.phone) {
1313
+ items.push(el(
1314
+ "li",
1315
+ {},
1316
+ glyph(PHONE, "aparte-rich-card__glyph aparte-place__line-glyph"),
1317
+ tel ? el("a", { class: "aparte-place__link", attrs: { href: tel }, text: place.phone }) : place.phone
1318
+ ));
1319
+ }
1320
+ if (place.website) {
1321
+ const host = new URL(place.website).hostname.replace(/^www\./, "");
1322
+ items.push(el(
1323
+ "li",
1324
+ {},
1325
+ glyph(GLOBE, "aparte-rich-card__glyph aparte-place__line-glyph"),
1326
+ el("a", { class: "aparte-place__link", attrs: { href: place.website, target: "_blank", rel: "noopener noreferrer" }, text: host })
1327
+ ));
1328
+ }
1329
+ return items.length ? el("ul", { class: "aparte-place__info" }, ...items) : null;
1330
+ }
1331
+ function tagsOf(place) {
1332
+ if (!place.tags.length) return null;
1333
+ return el("ul", { class: "aparte-place__tags" }, ...place.tags.map((t) => el("li", { class: "aparte-tag aparte-tag--sm aparte-place__tag" }, el("span", { class: "aparte-tag__label", text: t }))));
1334
+ }
1335
+ function renderDetail(ctx, place, settings, now) {
1336
+ const media = mediaOf(ctx, place, "wide");
1337
+ const category = categoryOf(ctx, place);
1338
+ if (category) {
1339
+ media.append(el(
1340
+ "span",
1341
+ { class: "aparte-place__badge" },
1342
+ glyph(kindGlyph(place.kind), "aparte-rich-card__glyph aparte-place__badge-glyph"),
1343
+ category
1344
+ ));
1345
+ }
1346
+ const head = el("div", { class: "aparte-place__head" }, nameOf(place), scoreOf(ctx, place, settings), statusOf(ctx, place, now));
1347
+ const body = el("div", { class: "aparte-place__body" }, head);
1348
+ for (const part of [actionsOf(ctx, place, settings, false), infoOf(ctx, place), hoursOf(ctx, place, now), tagsOf(place)]) {
1349
+ if (part) body.append(part);
1350
+ }
1351
+ return [el("article", { class: "aparte-place aparte-place--detail", attrs: { "data-kind": place.kind } }, media, body)];
1352
+ }
1353
+ function countLabel(ctx, data) {
1354
+ return data.title ?? word(ctx.config, "cardsPlaceCount", P.cardsPlaceCount, { n: formatNumber(ctx.config, data.places?.length ?? 0) });
1355
+ }
1356
+ function headingOf(ctx, data) {
1357
+ const text = el("span", { class: "aparte-place__heading-text" });
1358
+ ctx.bind(text, () => countLabel(ctx, data));
1359
+ return el("div", { class: "aparte-place__heading" }, glyph(PIN, "aparte-rich-card__glyph aparte-place__line-glyph"), text);
1360
+ }
1361
+ function render(data, ctx, settings) {
1362
+ const now = settings.now();
1363
+ const [first] = data.places;
1364
+ const many = data.places.length > 1;
1365
+ if (ctx.variant === "compact") {
1366
+ if (!many) return [rowOf(ctx, first, settings, now, "div")];
1367
+ const list = el("ul", { class: "aparte-place__list" }, ...data.places.map((p) => rowOf(ctx, p, settings, now, "li")));
1368
+ ctx.bind(list, () => countLabel(ctx, data), "aria-label");
1369
+ return [headingOf(ctx, data), list];
1370
+ }
1371
+ if (many) {
1372
+ const slides = data.places.map((p) => tileOf(ctx, p, settings, now, true));
1373
+ return [el("div", { class: "aparte-place__collection" }, headingOf(ctx, data), carousel(ctx, slides, { label: () => countLabel(ctx, data) }))];
1374
+ }
1375
+ return ctx.variant === "detail" ? renderDetail(ctx, first, settings, now) : [tileOf(ctx, first, settings, now, false)];
1376
+ }
1377
+ function skeleton(partial2, ctx) {
1378
+ const block = (cls, kind = "rect") => el("span", { class: `aparte-skeleton aparte-skeleton--${kind} ${cls}`, attrs: { "aria-hidden": "true" } });
1379
+ const known = partial2?.places?.[0]?.name ?? partial2?.title;
1380
+ const name = known ? el("div", { class: "aparte-place__name", text: known }) : block("aparte-place__sk-name", "text");
1381
+ if (ctx.variant === "compact") {
1382
+ return [el(
1383
+ "div",
1384
+ { class: "aparte-place aparte-place__row" },
1385
+ block("aparte-place__media--thumb aparte-place__sk-thumb"),
1386
+ el("div", { class: "aparte-place__text" }, name, block("aparte-place__sk-line", "text"))
1387
+ )];
1388
+ }
1389
+ const body = el(
1390
+ "div",
1391
+ { class: "aparte-place__body" },
1392
+ name,
1393
+ block("aparte-place__sk-line", "text"),
1394
+ block("aparte-place__sk-line aparte-place__sk-line--short", "text"),
1395
+ el("div", { class: "aparte-place__sk-actions" }, block("aparte-place__sk-btn"), block("aparte-place__sk-btn"))
1396
+ );
1397
+ if (ctx.variant === "detail") body.append(el("div", { class: "aparte-place__sk-hours" }, ...Array.from({ length: 4 }, () => block("aparte-place__sk-line", "text"))));
1398
+ return [el("article", { class: "aparte-place" }, block("aparte-place__media--wide aparte-place__sk-media"), body)];
1399
+ }
1400
+ function label(data, ctx) {
1401
+ const places = data?.places ?? [];
1402
+ if (places.length === 1) return places[0].name;
1403
+ if (places.length > 1 || data?.title) return countLabel(ctx, data);
1404
+ return word(ctx.config, "cardsPlaceTitle", P.cardsPlaceTitle);
1405
+ }
1406
+ function fallback(data, ctx) {
1407
+ const lines = data.places.map((p) => {
1408
+ const score = p.rating !== void 0 ? `${formatNumber(ctx.config, p.rating, { maximumFractionDigits: 1 })}/5${p.reviewCount !== void 0 ? ` (${formatNumber(ctx.config, p.reviewCount)})` : ""}` : void 0;
1409
+ const rest = [score, p.address].filter(Boolean).join(", ");
1410
+ return rest ? `${p.name} — ${rest}` : p.name;
1411
+ });
1412
+ return data.title ? `${data.title}:
1413
+ ${lines.join("\n")}` : lines.join("\n");
1414
+ }
1415
+ function partial(input) {
1416
+ const name = str(input["name"] ?? input["place"] ?? input["placeName"]);
1417
+ if (name) return { places: [readPlace({ name })] };
1418
+ const query = str(input["query"] ?? input["q"] ?? input["keyword"] ?? input["search"] ?? input["textQuery"]);
1419
+ return query ? { title: query } : null;
1420
+ }
1421
+ function createPlaceCard(settings) {
1422
+ return defineCard({
1423
+ name: "place",
1424
+ variants: ["card", "compact", "detail"],
1425
+ styles: placeStyles,
1426
+ icon: PIN,
1427
+ normalize: readPlaces,
1428
+ render: (data, ctx) => render(data, ctx, settings),
1429
+ skeleton,
1430
+ label,
1431
+ fallback,
1432
+ partial
1433
+ });
1434
+ }
1435
+ const placeCard = createPlaceCard({ builtIns: true, currencySymbol: "$", now: () => /* @__PURE__ */ new Date() });
1436
+ function setupPlaceCard(options = {}) {
1437
+ const { currencySymbol, now, builtIns, ...rest } = options;
1438
+ const card = createPlaceCard({
1439
+ builtIns: builtIns !== false,
1440
+ currencySymbol: str(currencySymbol) ?? "$",
1441
+ now: now ?? (() => /* @__PURE__ */ new Date())
1442
+ });
1443
+ return card.setup(rest);
1444
+ }
1445
+ export {
1446
+ directionsUrl,
1447
+ isOpenAt,
1448
+ phoneUrl,
1449
+ placeCard,
1450
+ placeStatus,
1451
+ readHours,
1452
+ readPlaces,
1453
+ readTime,
1454
+ setupPlaceCard
1455
+ };
1456
+ //# sourceMappingURL=place.js.map