@wix/himalaya-cli 0.1614.0 → 0.1615.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.
- package/dist/cli.mjs +1 -0
- package/package.json +2 -2
- package/template-icons/salon-spa.png +0 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/branded-lite/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/branded-lite/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/branded-lite/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-galleries/index.ts +104 -12
- package/templates/community-club/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/community-club/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/retail-store/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/salon-spa/README.md +53 -0
- package/templates/salon-spa/config.ts +272 -0
- package/templates/salon-spa/dev/index.ts +19893 -0
- package/templates/salon-spa/himalaya.content.json +49 -0
- package/templates/salon-spa/himalaya.template.json +14 -0
- package/templates/salon-spa/icon.png +0 -0
- package/templates/salon-spa/platforms.json +1 -0
- package/templates/salon-spa/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/salon-spa/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/salon-spa/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/salon-spa/tier5-src/_shared/branded-lite-hosted.ts +278 -0
- package/templates/salon-spa/tier5-src/_shared/branded-load-state.ts +109 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-copy.ts +40 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-history.ts +48 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-message-model.ts +177 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-model.ts +56 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-presenter.ts +581 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-transport.ts +148 -0
- package/templates/salon-spa/tier5-src/_shared/collection-contract.ts +230 -0
- package/templates/salon-spa/tier5-src/_shared/collection-query.ts +193 -0
- package/templates/salon-spa/tier5-src/_shared/collection-service.ts +400 -0
- package/templates/salon-spa/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/salon-spa/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/salon-spa/tier5-src/_shared/engine-intl.ts +508 -0
- package/templates/salon-spa/tier5-src/_shared/himi-account.ts +164 -0
- package/templates/salon-spa/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/salon-spa/tier5-src/_shared/himi-bookings.ts +318 -0
- package/templates/salon-spa/tier5-src/_shared/himi-copy.ts +89 -0
- package/templates/salon-spa/tier5-src/_shared/himi-store.ts +7 -0
- package/templates/salon-spa/tier5-src/_shared/himi-text.ts +24 -0
- package/templates/salon-spa/tier5-src/_shared/host-events.ts +66 -0
- package/templates/salon-spa/tier5-src/_shared/hosted-checkout.ts +70 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/salon-spa/tier5-src/_shared/iap-worker.ts +125 -0
- package/templates/salon-spa/tier5-src/_shared/local-preferences.ts +38 -0
- package/templates/salon-spa/tier5-src/_shared/member-access.ts +52 -0
- package/templates/salon-spa/tier5-src/_shared/member-account-routes.ts +52 -0
- package/templates/salon-spa/tier5-src/_shared/member-bookings.ts +94 -0
- package/templates/salon-spa/tier5-src/_shared/offset-page.ts +10 -0
- package/templates/salon-spa/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/salon-spa/tier5-src/_shared/owner-load-error.ts +348 -0
- package/templates/salon-spa/tier5-src/_shared/payment-setup.ts +6 -0
- package/templates/salon-spa/tier5-src/_shared/rich-body.ts +78 -0
- package/templates/salon-spa/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/salon-spa/tier5-src/_shared/weekly-goal.ts +107 -0
- package/templates/salon-spa/tier5-src/_shared/wix-service-price.ts +146 -0
- package/templates/salon-spa/tier5-src/business-chat/index.ts +669 -0
- package/templates/salon-spa/tier5-src/himi-booking-account/index.ts +241 -0
- package/templates/salon-spa/tier5-src/himi-booking-bookings/index.ts +466 -0
- package/templates/salon-spa/tier5-src/himi-booking-saved/index.ts +103 -0
- package/templates/salon-spa/tier5-src/himi-booking-service/index.ts +1170 -0
- package/templates/salon-spa/tier5-src/himi-booking-services/index.ts +791 -0
- package/templates/salon-spa/tier5-src/himi-booking-time/index.ts +1712 -0
- package/templates/salon-spa/tier5-src/himi-branded-lite-agenda/index.ts +950 -0
- package/templates/salon-spa/tier5-src/himi-branded-lite-galleries/index.ts +282 -0
- package/templates/salon-spa/tier5-src/himi-salon-home/index.ts +544 -0
- package/templates/salon-spa/tier5-src/himi-store-account/index.ts +925 -0
- package/templates/salon-spa/tokens.json +341 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/yoga-studio/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
{
|
|
2
|
+
"colors": {
|
|
3
|
+
"surfacePrimary": {
|
|
4
|
+
"light": "#FAF6EF",
|
|
5
|
+
"dark": "#141712",
|
|
6
|
+
"highContrast": "#FFFFFF"
|
|
7
|
+
},
|
|
8
|
+
"surfaceSecondary": {
|
|
9
|
+
"light": "#F2ECE2",
|
|
10
|
+
"dark": "#1B1F19",
|
|
11
|
+
"highContrast": "#F2F2F2"
|
|
12
|
+
},
|
|
13
|
+
"surfaceElevated": {
|
|
14
|
+
"light": "#FFFFFF",
|
|
15
|
+
"dark": "#222720",
|
|
16
|
+
"highContrast": "#FFFFFF"
|
|
17
|
+
},
|
|
18
|
+
"surfaceSubtle": {
|
|
19
|
+
"light": "#F6F1E8",
|
|
20
|
+
"dark": "#181B16",
|
|
21
|
+
"highContrast": "#FFFFFF"
|
|
22
|
+
},
|
|
23
|
+
"surfaceTertiary": {
|
|
24
|
+
"light": "#E9E1D4",
|
|
25
|
+
"dark": "#2A3027",
|
|
26
|
+
"highContrast": "#E6E6E6"
|
|
27
|
+
},
|
|
28
|
+
"textPrimary": {
|
|
29
|
+
"light": "#252A22",
|
|
30
|
+
"dark": "#F2EFE7",
|
|
31
|
+
"highContrast": "#000000"
|
|
32
|
+
},
|
|
33
|
+
"textSecondary": {
|
|
34
|
+
"light": "#585E53",
|
|
35
|
+
"dark": "#C2BDB1",
|
|
36
|
+
"highContrast": "#1F1F1F"
|
|
37
|
+
},
|
|
38
|
+
"textTertiary": {
|
|
39
|
+
"light": "#767A6E",
|
|
40
|
+
"dark": "#97938A",
|
|
41
|
+
"highContrast": "#333333"
|
|
42
|
+
},
|
|
43
|
+
"textOnAccent": {
|
|
44
|
+
"light": "#FFFFFF",
|
|
45
|
+
"dark": "#10130E",
|
|
46
|
+
"highContrast": "#FFFFFF"
|
|
47
|
+
},
|
|
48
|
+
"accentPrimary": {
|
|
49
|
+
"light": "#46594A",
|
|
50
|
+
"dark": "#A7C0A9",
|
|
51
|
+
"highContrast": "#1E2D21"
|
|
52
|
+
},
|
|
53
|
+
"accentSecondary": {
|
|
54
|
+
"light": "#A9834C",
|
|
55
|
+
"dark": "#D4B47F",
|
|
56
|
+
"highContrast": "#5A4220"
|
|
57
|
+
},
|
|
58
|
+
"accentSurface": {
|
|
59
|
+
"light": "#F3E0D8",
|
|
60
|
+
"dark": "#2C3830",
|
|
61
|
+
"highContrast": "#F7E9E3"
|
|
62
|
+
},
|
|
63
|
+
"accentText": {
|
|
64
|
+
"light": "#3E5042",
|
|
65
|
+
"dark": "#B4CCB6",
|
|
66
|
+
"highContrast": "#1E2D21"
|
|
67
|
+
},
|
|
68
|
+
"success": {
|
|
69
|
+
"light": "#2F6A47",
|
|
70
|
+
"dark": "#8CC9A2",
|
|
71
|
+
"highContrast": "#0F4526"
|
|
72
|
+
},
|
|
73
|
+
"successSurface": {
|
|
74
|
+
"light": "#E7F0E9",
|
|
75
|
+
"dark": "#15211A",
|
|
76
|
+
"highContrast": "#E7F0E9"
|
|
77
|
+
},
|
|
78
|
+
"warning": {
|
|
79
|
+
"light": "#8A6118",
|
|
80
|
+
"dark": "#D6B061",
|
|
81
|
+
"highContrast": "#5C3E07"
|
|
82
|
+
},
|
|
83
|
+
"warningSurface": {
|
|
84
|
+
"light": "#F6EEDC",
|
|
85
|
+
"dark": "#231D10",
|
|
86
|
+
"highContrast": "#F6EEDC"
|
|
87
|
+
},
|
|
88
|
+
"danger": {
|
|
89
|
+
"light": "#A63D33",
|
|
90
|
+
"dark": "#E0928A",
|
|
91
|
+
"highContrast": "#7A1F17"
|
|
92
|
+
},
|
|
93
|
+
"dangerSurface": {
|
|
94
|
+
"light": "#F7E8E5",
|
|
95
|
+
"dark": "#261513",
|
|
96
|
+
"highContrast": "#F7E8E5"
|
|
97
|
+
},
|
|
98
|
+
"border": {
|
|
99
|
+
"light": "#E5DCCD",
|
|
100
|
+
"dark": "#30362D",
|
|
101
|
+
"highContrast": "#5C5C5C"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"spacing": {
|
|
105
|
+
"xs": 4,
|
|
106
|
+
"sm": 8,
|
|
107
|
+
"md": 12,
|
|
108
|
+
"lg": 16,
|
|
109
|
+
"xl": 24,
|
|
110
|
+
"xxl": 32
|
|
111
|
+
},
|
|
112
|
+
"typography": {
|
|
113
|
+
"display": {
|
|
114
|
+
"size": 38,
|
|
115
|
+
"weight": 400,
|
|
116
|
+
"line": 1.12,
|
|
117
|
+
"letterSpacing": -0.4,
|
|
118
|
+
"family": "Georgia"
|
|
119
|
+
},
|
|
120
|
+
"heroTitle": {
|
|
121
|
+
"size": 34,
|
|
122
|
+
"weight": 400,
|
|
123
|
+
"line": 1.12,
|
|
124
|
+
"letterSpacing": -0.3,
|
|
125
|
+
"family": "Georgia"
|
|
126
|
+
},
|
|
127
|
+
"pageTitle": {
|
|
128
|
+
"size": 28,
|
|
129
|
+
"weight": 400,
|
|
130
|
+
"line": 1.2,
|
|
131
|
+
"letterSpacing": -0.2,
|
|
132
|
+
"family": "Georgia"
|
|
133
|
+
},
|
|
134
|
+
"title1": {
|
|
135
|
+
"size": 30,
|
|
136
|
+
"weight": 400,
|
|
137
|
+
"line": 1.2,
|
|
138
|
+
"letterSpacing": -0.3,
|
|
139
|
+
"family": "Georgia"
|
|
140
|
+
},
|
|
141
|
+
"title2": {
|
|
142
|
+
"size": 24,
|
|
143
|
+
"weight": 400,
|
|
144
|
+
"line": 1.25,
|
|
145
|
+
"letterSpacing": -0.2,
|
|
146
|
+
"family": "Georgia"
|
|
147
|
+
},
|
|
148
|
+
"title3": {
|
|
149
|
+
"size": 18,
|
|
150
|
+
"weight": 600,
|
|
151
|
+
"line": 1.3,
|
|
152
|
+
"letterSpacing": 0,
|
|
153
|
+
"family": "Wix Madefor Text"
|
|
154
|
+
},
|
|
155
|
+
"headline": {
|
|
156
|
+
"size": 16,
|
|
157
|
+
"weight": 600,
|
|
158
|
+
"line": 1.35,
|
|
159
|
+
"letterSpacing": 0,
|
|
160
|
+
"family": "Wix Madefor Text"
|
|
161
|
+
},
|
|
162
|
+
"body": {
|
|
163
|
+
"size": 16,
|
|
164
|
+
"weight": 400,
|
|
165
|
+
"line": 1.5,
|
|
166
|
+
"letterSpacing": 0,
|
|
167
|
+
"family": "Wix Madefor Text"
|
|
168
|
+
},
|
|
169
|
+
"bodyEmphasis": {
|
|
170
|
+
"size": 16,
|
|
171
|
+
"weight": 600,
|
|
172
|
+
"line": 1.4,
|
|
173
|
+
"letterSpacing": 0,
|
|
174
|
+
"family": "Wix Madefor Text"
|
|
175
|
+
},
|
|
176
|
+
"callout": {
|
|
177
|
+
"size": 15,
|
|
178
|
+
"weight": 500,
|
|
179
|
+
"line": 1.4,
|
|
180
|
+
"letterSpacing": 0,
|
|
181
|
+
"family": "Wix Madefor Text"
|
|
182
|
+
},
|
|
183
|
+
"label": {
|
|
184
|
+
"size": 14,
|
|
185
|
+
"weight": 600,
|
|
186
|
+
"line": 1.3,
|
|
187
|
+
"letterSpacing": 0,
|
|
188
|
+
"family": "Wix Madefor Text"
|
|
189
|
+
},
|
|
190
|
+
"footnote": {
|
|
191
|
+
"size": 14,
|
|
192
|
+
"weight": 400,
|
|
193
|
+
"line": 1.45,
|
|
194
|
+
"letterSpacing": 0,
|
|
195
|
+
"family": "Wix Madefor Text"
|
|
196
|
+
},
|
|
197
|
+
"caption": {
|
|
198
|
+
"size": 12,
|
|
199
|
+
"weight": 500,
|
|
200
|
+
"line": 1.4,
|
|
201
|
+
"letterSpacing": 0.2,
|
|
202
|
+
"family": "Wix Madefor Text"
|
|
203
|
+
},
|
|
204
|
+
"greetingScript": {
|
|
205
|
+
"size": 22,
|
|
206
|
+
"weight": 400,
|
|
207
|
+
"line": 1.2,
|
|
208
|
+
"letterSpacing": 0,
|
|
209
|
+
"family": "Georgia",
|
|
210
|
+
"style": "italic"
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"radius": {
|
|
214
|
+
"sm": 8,
|
|
215
|
+
"md": 14,
|
|
216
|
+
"lg": 20,
|
|
217
|
+
"pill": 999
|
|
218
|
+
},
|
|
219
|
+
"kit": {
|
|
220
|
+
"material": "flat",
|
|
221
|
+
"variant": "default",
|
|
222
|
+
"density": "spacious",
|
|
223
|
+
"motion": "smooth",
|
|
224
|
+
"motionDefaults": "subtle"
|
|
225
|
+
},
|
|
226
|
+
"shadows": {
|
|
227
|
+
"sm": {
|
|
228
|
+
"color": "#2B2418",
|
|
229
|
+
"opacity": 0.06,
|
|
230
|
+
"radius": 8,
|
|
231
|
+
"x": 0,
|
|
232
|
+
"y": 2
|
|
233
|
+
},
|
|
234
|
+
"md": {
|
|
235
|
+
"color": "#2B2418",
|
|
236
|
+
"opacity": 0.09,
|
|
237
|
+
"radius": 18,
|
|
238
|
+
"x": 0,
|
|
239
|
+
"y": 6
|
|
240
|
+
},
|
|
241
|
+
"lg": {
|
|
242
|
+
"color": "#2B2418",
|
|
243
|
+
"opacity": 0.12,
|
|
244
|
+
"radius": 30,
|
|
245
|
+
"x": 0,
|
|
246
|
+
"y": 12
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
"gradients": {
|
|
250
|
+
"brand": {
|
|
251
|
+
"colors": [
|
|
252
|
+
"accentPrimary",
|
|
253
|
+
"accentText"
|
|
254
|
+
],
|
|
255
|
+
"angle": 135
|
|
256
|
+
},
|
|
257
|
+
"subtle": {
|
|
258
|
+
"colors": [
|
|
259
|
+
"surfaceSecondary",
|
|
260
|
+
"surfacePrimary"
|
|
261
|
+
],
|
|
262
|
+
"angle": 180
|
|
263
|
+
},
|
|
264
|
+
"hero": {
|
|
265
|
+
"colors": [
|
|
266
|
+
"accentSurface",
|
|
267
|
+
"surfacePrimary"
|
|
268
|
+
],
|
|
269
|
+
"angle": 160
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"materials": {
|
|
273
|
+
"flat": {
|
|
274
|
+
"style": "flat",
|
|
275
|
+
"tint": "surfaceSecondary",
|
|
276
|
+
"opacity": 1
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"motions": {
|
|
280
|
+
"smooth": {
|
|
281
|
+
"duration": 260,
|
|
282
|
+
"easing": "easeInOut"
|
|
283
|
+
},
|
|
284
|
+
"quick": {
|
|
285
|
+
"duration": 140,
|
|
286
|
+
"easing": "easeOut"
|
|
287
|
+
},
|
|
288
|
+
"appearSoft": {
|
|
289
|
+
"duration": 480,
|
|
290
|
+
"easing": "easeOut"
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
"tabBar": {
|
|
294
|
+
"background": {
|
|
295
|
+
"color": "surfaceElevated"
|
|
296
|
+
},
|
|
297
|
+
"shape": {
|
|
298
|
+
"kind": "full"
|
|
299
|
+
},
|
|
300
|
+
"border": {
|
|
301
|
+
"topHairline": true,
|
|
302
|
+
"color": "border"
|
|
303
|
+
},
|
|
304
|
+
"selectedIndicator": {
|
|
305
|
+
"kind": "colorOnly",
|
|
306
|
+
"color": "accentPrimary"
|
|
307
|
+
},
|
|
308
|
+
"activeTint": "accentPrimary",
|
|
309
|
+
"inactiveTint": "textTertiary",
|
|
310
|
+
"labels": {
|
|
311
|
+
"show": true,
|
|
312
|
+
"typography": "caption"
|
|
313
|
+
},
|
|
314
|
+
"iconSize": 22,
|
|
315
|
+
"motion": "smooth"
|
|
316
|
+
},
|
|
317
|
+
"drawer": {
|
|
318
|
+
"background": {
|
|
319
|
+
"color": "surfaceSecondary"
|
|
320
|
+
},
|
|
321
|
+
"width": 300,
|
|
322
|
+
"activeTint": "accentPrimary",
|
|
323
|
+
"inactiveTint": "textSecondary",
|
|
324
|
+
"selectedIndicator": {
|
|
325
|
+
"kind": "pill",
|
|
326
|
+
"color": "accentPrimary"
|
|
327
|
+
},
|
|
328
|
+
"labels": {
|
|
329
|
+
"show": true,
|
|
330
|
+
"typography": "body"
|
|
331
|
+
},
|
|
332
|
+
"iconSize": 22,
|
|
333
|
+
"edgeInsets": 12,
|
|
334
|
+
"motion": "smooth"
|
|
335
|
+
},
|
|
336
|
+
"contentMaxWidth": {
|
|
337
|
+
"base": 480,
|
|
338
|
+
"medium": 600,
|
|
339
|
+
"expanded": 760
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -35,11 +35,14 @@ export interface LoadFlags {
|
|
|
35
35
|
notMock: boolean;
|
|
36
36
|
notMockEmpty: boolean;
|
|
37
37
|
notMockUnavailable: boolean;
|
|
38
|
+
/** hiddenWhen gate for the empty state: false ONLY when the site answered with no rows
|
|
39
|
+
* (`notMockEmpty` is true then — it gates the sample banner, not the empty state). */
|
|
40
|
+
hideEmpty: boolean;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export const LOADING: LoadFlags = {
|
|
41
44
|
status: "loading", error: "", signInRequired: false, notErrored: true, notLoading: false, busy: true, noSignIn: true,
|
|
42
|
-
isMock: false, notMock: true, notMockEmpty: true, notMockUnavailable: true,
|
|
45
|
+
isMock: false, notMock: true, notMockEmpty: true, notMockUnavailable: true, hideEmpty: true,
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
const READY: LoadFlags = { ...LOADING, status: "ready", notLoading: true, busy: false };
|
|
@@ -78,7 +81,7 @@ type Rows<K extends string, T> = { [P in K]: T[] } & { emptyText: string } & Loa
|
|
|
78
81
|
export function loaded<K extends string, T>(key: K, rows: T[], copy: LoadCopy<T>): Rows<K, T> {
|
|
79
82
|
if (rows.length) return { ...READY, [key]: rows, emptyText: "" } as Rows<K, T>;
|
|
80
83
|
if (samplesRequested()) return sampleRows(key, copy, "empty");
|
|
81
|
-
return { ...READY, [key]: [], emptyText: copyParam(`${copy.prefix}.emptyText`, copy.empty) } as Rows<K, T>;
|
|
84
|
+
return { ...READY, [key]: [], emptyText: copyParam(`${copy.prefix}.emptyText`, copy.empty), hideEmpty: false } as Rows<K, T>;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/** A query threw. Never rows the customer did not create. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { dispatch } from "@himalaya/dispatch";
|
|
2
2
|
// Relative for node resolution (see himi-store.ts note); rewritten off-repo.
|
|
3
3
|
import { displayName, getMyData, initials, type WixMyData } from "@himalaya/wix-users";
|
|
4
|
+
import { getMyMember } from "@himalaya/wix-members";
|
|
5
|
+
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
4
6
|
import { formatDateMedium, formatWeekdayDateTime } from "../_shared/engine-intl.js";
|
|
5
7
|
|
|
6
8
|
export interface AccountProfile {
|
|
@@ -82,6 +84,31 @@ function profileFromData(data: WixMyData, fallbackEmail = ""): AccountProfile {
|
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/** The signed-in site member's name and photo from their member record, or undefined when it
|
|
88
|
+
* cannot be read or names nobody (the caller then shows the email). */
|
|
89
|
+
async function siteMemberProfile(email: string): Promise<AccountProfile | undefined> {
|
|
90
|
+
try {
|
|
91
|
+
const member = (await getMyMember({ fieldsets: ["FULL"] }))?.member;
|
|
92
|
+
const name = [member?.contact?.firstName, member?.contact?.lastName].map((part) => (part ?? "").trim()).filter(Boolean).join(" ")
|
|
93
|
+
|| (member?.profile?.nickname ?? "").trim();
|
|
94
|
+
if (!name) return undefined;
|
|
95
|
+
const image = wixMediaUrl(member?.profile?.photo?.url) || wixMediaUrl(member?.profile?.photo?.id) || "";
|
|
96
|
+
return {
|
|
97
|
+
isMember: true,
|
|
98
|
+
identityLine: "Signed in",
|
|
99
|
+
detailLine: email || name,
|
|
100
|
+
memberName: name,
|
|
101
|
+
memberEmail: email || member?.loginEmail || "",
|
|
102
|
+
memberImage: image,
|
|
103
|
+
memberInitials: initialsFromName(name) || "M",
|
|
104
|
+
hasMemberImage: !!image,
|
|
105
|
+
noMemberImage: !image,
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
export function guestProfile(detailLine: string): AccountProfile {
|
|
86
113
|
return {
|
|
87
114
|
isMember: false,
|
|
@@ -118,6 +145,10 @@ export async function loadMemberProfile(guestDetailLine: string): Promise<Accoun
|
|
|
118
145
|
const data = await getMyData();
|
|
119
146
|
return profileFromData(data, storedEmail);
|
|
120
147
|
} catch {
|
|
148
|
+
// The account profile is an owner endpoint: the web host refuses it, and a site member need
|
|
149
|
+
// not have one. The member's own record (the Members API) still names them.
|
|
150
|
+
const member = await siteMemberProfile(storedEmail);
|
|
151
|
+
if (member) return member;
|
|
121
152
|
return {
|
|
122
153
|
isMember: true,
|
|
123
154
|
identityLine: "Signed in",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Saved (favorite) booking services use the host's durable, app-scoped KV, shared by every
|
|
2
|
+
// screen worker — the service detail toggles a row (himi-booking-service `toggleSaved`), the
|
|
3
|
+
// Favorites list reads them (himi-booking-saved). Same model as _shared/himi-store-saved.ts: a
|
|
4
|
+
// failed write must never claim that a save succeeded.
|
|
5
|
+
export interface SavedService {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
/** The price line as the detail showed it ("$250"), kept so the list paints offline. */
|
|
9
|
+
price: string;
|
|
10
|
+
/** The duration line ("1 hr"). */
|
|
11
|
+
duration: string;
|
|
12
|
+
image: string;
|
|
13
|
+
category: string;
|
|
14
|
+
/** A course: the list's Book passes `course` to the time screen, which then refuses a course
|
|
15
|
+
* whose record failed to load instead of offering its sessions one by one. */
|
|
16
|
+
course?: boolean;
|
|
17
|
+
}
|
|
18
|
+
type StorageLike = { getItem(key: string): string | null; setItem(key: string, value: string): void };
|
|
19
|
+
type Host = { himalaya?: { storage?: StorageLike; input?: Record<string, unknown> }; localStorage?: StorageLike };
|
|
20
|
+
const BASE = "himi-booking:saved-services:v1";
|
|
21
|
+
function storage(): StorageLike {
|
|
22
|
+
const host = globalThis as Host;
|
|
23
|
+
const kv = host.himalaya?.storage ?? host.localStorage;
|
|
24
|
+
if (!kv) throw new Error("Saved-service storage is unavailable");
|
|
25
|
+
return kv;
|
|
26
|
+
}
|
|
27
|
+
const validPart = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(value) && value !== "default";
|
|
28
|
+
/** Saves belong to whoever made them. A signed-in member's live under the host's authenticated
|
|
29
|
+
* account + business — the partition collection preferences use, so a second member on the same
|
|
30
|
+
* device never sees them; a visitor's live on the device. A member's partition, read for the
|
|
31
|
+
* first time, takes the visitor's saves on this device (signing in carries them, like a cart),
|
|
32
|
+
* once — the device list is then emptied. */
|
|
33
|
+
function storeKey(kv: StorageLike): string {
|
|
34
|
+
const input = (globalThis as Host).himalaya?.input;
|
|
35
|
+
const account = input?.__himiPreferenceAccount, business = input?.__himiPreferenceBusiness;
|
|
36
|
+
if (!validPart(account) || !validPart(business)) return BASE;
|
|
37
|
+
const key = `${BASE}.${account}.${encodeURIComponent(business)}`;
|
|
38
|
+
if (kv.getItem(key) === null) {
|
|
39
|
+
const visitor = kv.getItem(BASE);
|
|
40
|
+
if (visitor !== null) {
|
|
41
|
+
kv.setItem(key, visitor);
|
|
42
|
+
// Empty the device list only once the member's copy is confirmed: a web host may swallow a
|
|
43
|
+
// failed write, and clearing then would lose the visitor's saves.
|
|
44
|
+
if (kv.getItem(key) === visitor) kv.setItem(BASE, "[]");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
const str = (value: unknown): string => (typeof value === "string" ? value : "");
|
|
50
|
+
export function readSavedServices(): SavedService[] {
|
|
51
|
+
const kv = storage();
|
|
52
|
+
const raw = kv.getItem(storeKey(kv));
|
|
53
|
+
if (!raw) return [];
|
|
54
|
+
const parsed: unknown = JSON.parse(raw);
|
|
55
|
+
if (!Array.isArray(parsed)) throw new Error("Invalid saved-service data");
|
|
56
|
+
const rows = new Map<string, SavedService>();
|
|
57
|
+
for (const value of parsed) {
|
|
58
|
+
if (!value || typeof value !== "object") continue;
|
|
59
|
+
const row = value as Record<string, unknown>;
|
|
60
|
+
if (typeof row.id !== "string" || !row.id.trim() || typeof row.name !== "string") continue;
|
|
61
|
+
rows.set(row.id, { id: row.id, name: row.name, price: str(row.price), duration: str(row.duration), image: str(row.image), category: str(row.category), ...(row.course === true ? { course: true } : {}) });
|
|
62
|
+
}
|
|
63
|
+
return [...rows.values()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isServiceSaved(id: string): boolean {
|
|
67
|
+
return readSavedServices().some(row => row.id === id);
|
|
68
|
+
}
|
|
69
|
+
export function writeSavedServices(rows: SavedService[]): void {
|
|
70
|
+
const kv = storage();
|
|
71
|
+
const key = storeKey(kv);
|
|
72
|
+
const value = JSON.stringify(rows);
|
|
73
|
+
kv.setItem(key, value);
|
|
74
|
+
// Web hosts may swallow quota/security errors. Confirm the synchronous write
|
|
75
|
+
// before displaying success; a missing/mismatched read is a failed save.
|
|
76
|
+
if (kv.getItem(key) !== value) throw new Error("Saved-service write did not persist");
|
|
77
|
+
}
|
|
78
|
+
/** Save or unsave; returns the new saved state. The newest save is listed first. */
|
|
79
|
+
export function toggleSavedService(service: SavedService): boolean {
|
|
80
|
+
if (!service.id.trim() || !service.name.trim()) throw new Error("No service to save");
|
|
81
|
+
const rows = readSavedServices();
|
|
82
|
+
const saved = !rows.some(row => row.id === service.id);
|
|
83
|
+
writeSavedServices(saved ? [service, ...rows] : rows.filter(row => row.id !== service.id));
|
|
84
|
+
return saved;
|
|
85
|
+
}
|
|
86
|
+
export function removeSavedService(id: string): void {
|
|
87
|
+
writeSavedServices(readSavedServices().filter(row => row.id !== id));
|
|
88
|
+
}
|
|
@@ -22,6 +22,7 @@ import { wixMediaUrl, WIX_DETAIL_IMAGE, WIX_LIST_IMAGE } from "@himalaya/wix-med
|
|
|
22
22
|
import { formatClock, formatDateMedium, formatMonthDay } from "../_shared/engine-intl.js";
|
|
23
23
|
import { descriptionBody } from "../_shared/rich-body.js";
|
|
24
24
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
25
|
+
import { isServiceSaved, toggleSavedService } from "../_shared/himi-booking-saved.js";
|
|
25
26
|
|
|
26
27
|
// Offline-first (data-offline): the service detail is a public read-through. On a
|
|
27
28
|
// fetch failure we fall back to the last-synced row for this service written by
|
|
@@ -182,6 +183,13 @@ interface S {
|
|
|
182
183
|
sessionMessageOk: boolean;
|
|
183
184
|
sessionMessageFailed: boolean;
|
|
184
185
|
noSessionMessage: boolean;
|
|
186
|
+
// ---- Favorites (the heart an app adds with withServiceSaveToggle; _shared/himi-booking-saved.ts) ----
|
|
187
|
+
saved: boolean;
|
|
188
|
+
notSaved: boolean;
|
|
189
|
+
/** The heart's spoken label: what a tap does now. */
|
|
190
|
+
saveLabel: string;
|
|
191
|
+
/** "" unless the last save or unsave failed. */
|
|
192
|
+
saveMessage: string;
|
|
185
193
|
}
|
|
186
194
|
|
|
187
195
|
function inputId(): string {
|
|
@@ -790,6 +798,10 @@ function derive(s: S): S {
|
|
|
790
798
|
// Change Time opens the time screen, which cannot book a service closed to online booking.
|
|
791
799
|
noChangeTime: (control !== "book" && control !== "waitlist" && control !== "closed") || !s.bookableOnline,
|
|
792
800
|
noSessionMessage: !s.sessionMessage,
|
|
801
|
+
notSaved: !s.saved,
|
|
802
|
+
saveLabel: s.saved
|
|
803
|
+
? copyParam("service.unsaveLabel", "Remove from favorites")
|
|
804
|
+
: copyParam("service.saveLabel", "Save to favorites"),
|
|
793
805
|
notBookableOnline: !s.bookableOnline,
|
|
794
806
|
noAbout: !s.about && !s.aboutJson,
|
|
795
807
|
noPolicy: s.policyLines.length === 0,
|
|
@@ -866,9 +878,16 @@ function withService(s: S, service: WixService): S {
|
|
|
866
878
|
// WS-C — canonical param-less target; the descriptor's choose-time CTA
|
|
867
879
|
// binds the separate `id` state field as a first-class nav param.
|
|
868
880
|
timeScreenId: "himi_time",
|
|
881
|
+
saved: savedNow(serviceId),
|
|
869
882
|
});
|
|
870
883
|
}
|
|
871
884
|
|
|
885
|
+
/** Whether the service is a favorite. Unreadable storage reads as not saved: the heart is a
|
|
886
|
+
* convenience and never holds the screen back. */
|
|
887
|
+
function savedNow(id: string): boolean {
|
|
888
|
+
try { return !!id && isServiceSaved(id); } catch { return false; }
|
|
889
|
+
}
|
|
890
|
+
|
|
872
891
|
/** The service record: with staff details when this token may read them, else without. */
|
|
873
892
|
async function fetchService(id: string): Promise<WixService | undefined> {
|
|
874
893
|
try {
|
|
@@ -897,9 +916,10 @@ const svc = defineService<S, {
|
|
|
897
916
|
book: (s: S) => S;
|
|
898
917
|
changeTime: (s: S) => S;
|
|
899
918
|
joinWaitlist: (s: S, arg?: unknown) => Promise<S>;
|
|
919
|
+
toggleSaved: (s: S) => S;
|
|
900
920
|
}>("himiBookingService", {
|
|
901
921
|
// A join's outcome belongs to that visit: a cold start must not replay it from saved state.
|
|
902
|
-
notHydrated: ["sessionMessage", "sessionMessageOk", "sessionMessageFailed", "noSessionMessage"],
|
|
922
|
+
notHydrated: ["sessionMessage", "sessionMessageOk", "sessionMessageFailed", "noSessionMessage", "saveMessage"],
|
|
903
923
|
state: derive({
|
|
904
924
|
status: "loading",
|
|
905
925
|
id,
|
|
@@ -1005,6 +1025,10 @@ const svc = defineService<S, {
|
|
|
1005
1025
|
noChangeTime: true,
|
|
1006
1026
|
...NO_SESSION_MESSAGE,
|
|
1007
1027
|
noSessionMessage: true,
|
|
1028
|
+
saved: false,
|
|
1029
|
+
notSaved: true,
|
|
1030
|
+
saveLabel: "",
|
|
1031
|
+
saveMessage: "",
|
|
1008
1032
|
}),
|
|
1009
1033
|
actions: {
|
|
1010
1034
|
load: async (s, arg) => {
|
|
@@ -1012,14 +1036,16 @@ const svc = defineService<S, {
|
|
|
1012
1036
|
rememberSignIn(arg);
|
|
1013
1037
|
if (!s.id) return derive({ ...s, status: "ready", errored: true });
|
|
1014
1038
|
// A new view: a join's outcome from before is not news when the screen shows again.
|
|
1015
|
-
const start = { ...s, policyExpanded: false, scheduleExpanded: false, ...NO_SESSION_MESSAGE };
|
|
1039
|
+
const start = { ...s, policyExpanded: false, scheduleExpanded: false, saveMessage: "", ...NO_SESSION_MESSAGE };
|
|
1016
1040
|
try {
|
|
1017
1041
|
const service = await fetchService(s.id);
|
|
1018
1042
|
if (!service) return withSession(derive({ ...start, status: "ready" }), undefined);
|
|
1019
1043
|
loadedService = service;
|
|
1020
1044
|
// The session the Agenda row opened on, read beside the service-wide availability.
|
|
1021
1045
|
const [shown, sel] = await Promise.all([withGroupAvailability(withService(start, service), service), readSessionBounded(service)]);
|
|
1022
|
-
|
|
1046
|
+
// The heart is read again after the waits (the session read takes up to 10 s): a toggle
|
|
1047
|
+
// tapped meanwhile wrote storage, and the snapshot taken before would undo it.
|
|
1048
|
+
return withSession({ ...shown, saved: savedNow(s.id) }, sel);
|
|
1023
1049
|
} catch {
|
|
1024
1050
|
// Offline: paint from the last-synced services cache (written by the list
|
|
1025
1051
|
// worker) before falling back to the error state. The cache holds the name,
|
|
@@ -1038,6 +1064,7 @@ const svc = defineService<S, {
|
|
|
1038
1064
|
category: String(cached.category ?? ""),
|
|
1039
1065
|
images: image ? [{ url: image, alt: name }] : [],
|
|
1040
1066
|
timeScreenId: `himi_time_${s.id}`,
|
|
1067
|
+
saved: savedNow(s.id),
|
|
1041
1068
|
}, undefined);
|
|
1042
1069
|
}
|
|
1043
1070
|
return withSession({ ...start, status: "ready", errored: true, name: "" }, undefined);
|
|
@@ -1056,6 +1083,26 @@ const svc = defineService<S, {
|
|
|
1056
1083
|
},
|
|
1057
1084
|
togglePolicy: (s) => derive({ ...s, policyExpanded: !s.policyExpanded }),
|
|
1058
1085
|
toggleSchedule: (s) => derive({ ...s, scheduleExpanded: !s.scheduleExpanded }),
|
|
1086
|
+
// The heart: save or unsave this service on the device (the member's own list once signed
|
|
1087
|
+
// in). The row keeps what the Favorites list shows offline; a failed write says so and never
|
|
1088
|
+
// shows the heart as saved.
|
|
1089
|
+
toggleSaved: (s) => {
|
|
1090
|
+
if (!s.id || !s.hasService || s.errored) return s;
|
|
1091
|
+
try {
|
|
1092
|
+
const saved = toggleSavedService({
|
|
1093
|
+
id: s.id,
|
|
1094
|
+
name: s.name,
|
|
1095
|
+
price: s.priceIsText ? "" : s.priceText,
|
|
1096
|
+
duration: s.durationText,
|
|
1097
|
+
image: s.image,
|
|
1098
|
+
category: s.category,
|
|
1099
|
+
...(s.isCourse ? { course: true } : {}),
|
|
1100
|
+
});
|
|
1101
|
+
return derive({ ...s, saved, saveMessage: "" });
|
|
1102
|
+
} catch {
|
|
1103
|
+
return derive({ ...s, saved: savedNow(s.id), saveMessage: copyParam("service.saveError", "Could not update your favorites. Try again.") });
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1059
1106
|
// The footer button opens the time screen for the loaded service. A worker directive,
|
|
1060
1107
|
// not a `himi.open` with a `$service:` id param: the iOS and Android sticky footers
|
|
1061
1108
|
// dispatch their taps without resolving `$service:` params, so the id never arrived.
|