@wix/himalaya-cli 0.1158.0 → 0.1160.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 (63) hide show
  1. package/dist/cli.mjs +2 -0
  2. package/package.json +2 -2
  3. package/templates/beauty-salon/tier5-src/_shared/himi-copy.ts +1 -1
  4. package/templates/beauty-salon-owner/dev/index.ts +382 -194
  5. package/templates/beauty-salon-owner/tier5-src/_shared/himi-copy.ts +1 -1
  6. package/templates/beauty-salon-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  7. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  8. package/templates/branded-lite/tier5-src/_shared/himi-copy.ts +1 -1
  9. package/templates/brandy/config.ts +14 -3
  10. package/templates/brandy/dev/index.ts +339 -2
  11. package/templates/brandy/tier5-src/_shared/himi-copy.ts +56 -0
  12. package/templates/brandy/tier5-src/himi-notification-settings/index.ts +130 -0
  13. package/templates/brandy/tokens.json +10 -0
  14. package/templates/community-club/tier5-src/_shared/himi-copy.ts +1 -1
  15. package/templates/community-club-owner/dev/index.ts +382 -194
  16. package/templates/community-club-owner/tier5-src/_shared/himi-copy.ts +1 -1
  17. package/templates/community-club-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  18. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  19. package/templates/events-meetups/tier5-src/_shared/himi-copy.ts +1 -1
  20. package/templates/events-meetups-owner/dev/index.ts +382 -194
  21. package/templates/events-meetups-owner/tier5-src/_shared/himi-copy.ts +1 -1
  22. package/templates/events-meetups-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  23. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  24. package/templates/gym-fitness/tier5-src/_shared/himi-copy.ts +1 -1
  25. package/templates/gym-fitness-owner/dev/index.ts +382 -194
  26. package/templates/gym-fitness-owner/tier5-src/_shared/himi-copy.ts +1 -1
  27. package/templates/gym-fitness-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  28. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  29. package/templates/himi-chat/tier5-src/_shared/himi-copy.ts +1 -1
  30. package/templates/nonprofit-cause/tier5-src/_shared/himi-copy.ts +1 -1
  31. package/templates/nonprofit-cause-owner/dev/index.ts +382 -194
  32. package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-copy.ts +1 -1
  33. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  34. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  35. package/templates/owner-lite/dev/index.ts +382 -194
  36. package/templates/owner-lite/tier5-src/_shared/himi-copy.ts +1 -1
  37. package/templates/owner-lite/tier5-src/_shared/owner-editor-value.ts +56 -0
  38. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  39. package/templates/podcast-creator/tier5-src/_shared/himi-copy.ts +1 -1
  40. package/templates/podcast-creator-owner/dev/index.ts +382 -194
  41. package/templates/podcast-creator-owner/tier5-src/_shared/himi-copy.ts +1 -1
  42. package/templates/podcast-creator-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  43. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  44. package/templates/portfolio-creative/tier5-src/_shared/himi-copy.ts +1 -1
  45. package/templates/portfolio-creative-owner/dev/index.ts +382 -194
  46. package/templates/portfolio-creative-owner/tier5-src/_shared/himi-copy.ts +1 -1
  47. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  48. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  49. package/templates/restaurant/tier5-src/_shared/himi-copy.ts +1 -1
  50. package/templates/restaurant-owner/dev/index.ts +382 -194
  51. package/templates/restaurant-owner/tier5-src/_shared/himi-copy.ts +1 -1
  52. package/templates/restaurant-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  53. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  54. package/templates/retail-store/tier5-src/_shared/himi-copy.ts +1 -1
  55. package/templates/retail-store-owner/dev/index.ts +382 -194
  56. package/templates/retail-store-owner/tier5-src/_shared/himi-copy.ts +1 -1
  57. package/templates/retail-store-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  58. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
  59. package/templates/yoga-studio/tier5-src/_shared/himi-copy.ts +1 -1
  60. package/templates/yoga-studio-owner/dev/index.ts +382 -194
  61. package/templates/yoga-studio-owner/tier5-src/_shared/himi-copy.ts +1 -1
  62. package/templates/yoga-studio-owner/tier5-src/_shared/owner-editor-value.ts +56 -0
  63. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +132 -46
@@ -5,6 +5,340 @@ import { siteScreens } from "@himalaya/core";
5
5
  import config, { site } from "../config.js";
6
6
 
7
7
  const SCREENS = siteScreens(site);
8
+
9
+ // The native Notifications screen (a drawer row): whether this app may notify, and the one action
10
+ // that changes it — Turn on (the system prompt when it can still show, otherwise the device
11
+ // settings) or Open settings. It runs on the himi-notification-settings worker in tier5-src/.
12
+ const NOTIFICATIONS = {
13
+ "screenId": "himi_notification_settings",
14
+ "renderer": "sdui_intent",
15
+ "title": "",
16
+ "schemaVersion": 1,
17
+ "cacheControl": {
18
+ "maxAge": 0,
19
+ "staleWhileRevalidate": 0
20
+ },
21
+ "payload": {
22
+ "id": "himi_notification_settings.root",
23
+ "body": {
24
+ "package": "himalaya",
25
+ "type": "VStack",
26
+ "props": {
27
+ "spacing": 16,
28
+ "alignment": "leading",
29
+ "children": [
30
+ {
31
+ "id": "himi_notification_settings.title",
32
+ "body": {
33
+ "package": "himalaya",
34
+ "type": "Text",
35
+ "props": {
36
+ "text": "Notifications",
37
+ "style": "title1"
38
+ }
39
+ }
40
+ },
41
+ {
42
+ "id": "himi_notification_settings.status",
43
+ "body": {
44
+ "package": "himalaya",
45
+ "type": "HStack",
46
+ "props": {
47
+ "spacing": 12,
48
+ "children": [
49
+ {
50
+ "id": "",
51
+ "body": {
52
+ "package": "himalaya",
53
+ "type": "Icon",
54
+ "props": {
55
+ "name": "bell",
56
+ "size": 24,
57
+ "tint": "accentPrimary"
58
+ }
59
+ },
60
+ "modifiers": {
61
+ "accessibilityRole": "decorative"
62
+ }
63
+ },
64
+ {
65
+ "id": "",
66
+ "body": {
67
+ "package": "himalaya",
68
+ "type": "VStack",
69
+ "props": {
70
+ "spacing": 4,
71
+ "alignment": "leading",
72
+ "children": [
73
+ {
74
+ "id": "himi_notification_settings.status_title",
75
+ "body": {
76
+ "package": "himalaya",
77
+ "type": "Text",
78
+ "props": {
79
+ "text": "$service:himiNotificationSettings.statusTitle",
80
+ "style": "title2"
81
+ }
82
+ }
83
+ },
84
+ {
85
+ "id": "himi_notification_settings.status_body",
86
+ "body": {
87
+ "package": "himalaya",
88
+ "type": "Text",
89
+ "props": {
90
+ "text": "$service:himiNotificationSettings.statusBody",
91
+ "style": "body",
92
+ "color": "textSecondary",
93
+ "collapseWhenEmpty": true
94
+ }
95
+ }
96
+ }
97
+ ]
98
+ }
99
+ }
100
+ }
101
+ ]
102
+ }
103
+ },
104
+ "modifiers": {
105
+ "background": {
106
+ "token": "surfaceElevated"
107
+ },
108
+ "border": {
109
+ "width": 1,
110
+ "color": {
111
+ "token": "border"
112
+ }
113
+ },
114
+ "cornerRadius": 16,
115
+ "padding": {
116
+ "leading": 16,
117
+ "trailing": 16,
118
+ "top": 16,
119
+ "bottom": 16
120
+ }
121
+ }
122
+ },
123
+ {
124
+ "id": "himi_notification_settings.turn_on",
125
+ "body": {
126
+ "package": "himalaya",
127
+ "type": "Button",
128
+ "props": {
129
+ "title": "Turn on notifications",
130
+ "style": "primary",
131
+ "fullWidth": true,
132
+ "action": {
133
+ "actionId": "tier5.himiNotificationSettings.enable"
134
+ }
135
+ }
136
+ },
137
+ "modifiers": {
138
+ "hiddenPath": "$service:himiNotificationSettings.hideTurnOn"
139
+ }
140
+ },
141
+ {
142
+ "id": "himi_notification_settings.open_settings",
143
+ "body": {
144
+ "package": "himalaya",
145
+ "type": "Button",
146
+ "props": {
147
+ "title": "Open settings",
148
+ "style": "secondary",
149
+ "fullWidth": true,
150
+ "action": {
151
+ "actionId": "tier5.himiNotificationSettings.openSettings"
152
+ }
153
+ }
154
+ },
155
+ "modifiers": {
156
+ "hiddenPath": "$service:himiNotificationSettings.hideOpenSettings"
157
+ }
158
+ },
159
+ {
160
+ "id": "himi_notification_settings.message",
161
+ "body": {
162
+ "package": "himalaya",
163
+ "type": "Text",
164
+ "props": {
165
+ "text": "$service:himiNotificationSettings.message",
166
+ "style": "footnote",
167
+ "color": "textSecondary",
168
+ "collapseWhenEmpty": true
169
+ }
170
+ }
171
+ }
172
+ ]
173
+ }
174
+ },
175
+ "modifiers": {
176
+ "background": {
177
+ "token": "surfacePrimary"
178
+ },
179
+ "padding": {
180
+ "leading": 16,
181
+ "trailing": 16,
182
+ "top": 18,
183
+ "bottom": 28
184
+ },
185
+ "onAppear": {
186
+ "actionId": "tier5.himiNotificationSettings.refresh"
187
+ }
188
+ }
189
+ },
190
+ "logicBundle": {
191
+ "url": "http://localhost:8190/v1/bundles/himi-notification-settings?integrity=",
192
+ "integrity": "",
193
+ "contentType": "application/javascript+ts-bundle",
194
+ "declaredCapabilities": [
195
+ "push"
196
+ ],
197
+ "minBinaryVersion": "0.2.0",
198
+ "bundleVersion": "0.0.1"
199
+ }
200
+ };
201
+ // The offline seed copy has no running worker, so it carries no buttons and no live bindings.
202
+ const NOTIFICATIONS_SEED = {
203
+ "screenId": "himi_notification_settings",
204
+ "renderer": "sdui_intent",
205
+ "title": "",
206
+ "schemaVersion": 1,
207
+ "cacheControl": {
208
+ "maxAge": 0,
209
+ "staleWhileRevalidate": 0
210
+ },
211
+ "payload": {
212
+ "id": "himi_notification_settings.root",
213
+ "body": {
214
+ "package": "himalaya",
215
+ "type": "VStack",
216
+ "props": {
217
+ "spacing": 16,
218
+ "alignment": "leading",
219
+ "children": [
220
+ {
221
+ "id": "himi_notification_settings.title",
222
+ "body": {
223
+ "package": "himalaya",
224
+ "type": "Text",
225
+ "props": {
226
+ "text": "Notifications",
227
+ "style": "title1"
228
+ }
229
+ }
230
+ },
231
+ {
232
+ "id": "himi_notification_settings.status",
233
+ "body": {
234
+ "package": "himalaya",
235
+ "type": "HStack",
236
+ "props": {
237
+ "spacing": 12,
238
+ "children": [
239
+ {
240
+ "id": "",
241
+ "body": {
242
+ "package": "himalaya",
243
+ "type": "Icon",
244
+ "props": {
245
+ "name": "bell",
246
+ "size": 24,
247
+ "tint": "accentPrimary"
248
+ }
249
+ },
250
+ "modifiers": {
251
+ "accessibilityRole": "decorative"
252
+ }
253
+ },
254
+ {
255
+ "id": "",
256
+ "body": {
257
+ "package": "himalaya",
258
+ "type": "VStack",
259
+ "props": {
260
+ "spacing": 4,
261
+ "alignment": "leading",
262
+ "children": [
263
+ {
264
+ "id": "himi_notification_settings.status_title",
265
+ "body": {
266
+ "package": "himalaya",
267
+ "type": "Text",
268
+ "props": {
269
+ "text": "Notifications",
270
+ "style": "title2"
271
+ }
272
+ }
273
+ },
274
+ {
275
+ "id": "himi_notification_settings.status_body",
276
+ "body": {
277
+ "package": "himalaya",
278
+ "type": "Text",
279
+ "props": {
280
+ "text": "Connect to the internet to change notification settings.",
281
+ "style": "body",
282
+ "color": "textSecondary",
283
+ "collapseWhenEmpty": true
284
+ }
285
+ }
286
+ }
287
+ ]
288
+ }
289
+ }
290
+ }
291
+ ]
292
+ }
293
+ },
294
+ "modifiers": {
295
+ "background": {
296
+ "token": "surfaceElevated"
297
+ },
298
+ "border": {
299
+ "width": 1,
300
+ "color": {
301
+ "token": "border"
302
+ }
303
+ },
304
+ "cornerRadius": 16,
305
+ "padding": {
306
+ "leading": 16,
307
+ "trailing": 16,
308
+ "top": 16,
309
+ "bottom": 16
310
+ }
311
+ }
312
+ }
313
+ ]
314
+ }
315
+ },
316
+ "modifiers": {
317
+ "background": {
318
+ "token": "surfacePrimary"
319
+ },
320
+ "padding": {
321
+ "leading": 16,
322
+ "trailing": 16,
323
+ "top": 18,
324
+ "bottom": 28
325
+ },
326
+ "onAppear": {
327
+ "actionId": "tier5.himiNotificationSettings.refresh"
328
+ }
329
+ }
330
+ },
331
+ "logicBundle": {
332
+ "url": "http://localhost:8190/v1/bundles/himi-notification-settings?integrity=",
333
+ "integrity": "",
334
+ "contentType": "application/javascript+ts-bundle",
335
+ "declaredCapabilities": [
336
+ "push"
337
+ ],
338
+ "minBinaryVersion": "0.2.0",
339
+ "bundleVersion": "0.0.1"
340
+ }
341
+ };
8
342
  // The home screen is always the `home` page's: `site_home`. A literal on purpose — `himi init`
9
343
  // and the agent tooling read `homeScreenId` from this file as written; config.ts refuses a map
10
344
  // whose first tab is anything else, so the literal and the tab bar cannot disagree.
@@ -14,12 +348,15 @@ export default {
14
348
  homeScreenId: "site_home",
15
349
  screens: {
16
350
  // A fresh copy per build: descriptors are pure functions of the site block.
17
- exact: Object.fromEntries(Object.keys(SCREENS).map((id) => [id, () => structuredClone(SCREENS[id])])),
351
+ exact: {
352
+ ...Object.fromEntries(Object.keys(SCREENS).map((id) => [id, () => structuredClone(SCREENS[id])])),
353
+ himi_notification_settings: () => structuredClone(NOTIFICATIONS),
354
+ },
18
355
  templated: [],
19
356
  },
20
357
  actions: [],
21
358
  createStore: () => ({}),
22
359
  // Every page is seeded, so the whole tab bar and drawer work on a cold offline start (each
23
360
  // WebView then shows its own offline state until the network returns).
24
- seed: { screens: SCREENS, data: {} },
361
+ seed: { screens: { ...SCREENS, himi_notification_settings: NOTIFICATIONS_SEED }, data: {} },
25
362
  };
@@ -0,0 +1,56 @@
1
+ // Copy-as-configuration for WORKER-emitted strings (`himi docs designing-himi-screens`,
2
+ // §"Copy is configuration"): a worker never hardcodes user-facing copy — the
3
+ // descriptor bakes the provisioned copy-pack overrides relevant to this worker
4
+ // into its onAppear action params as a `copy` JSON object (key → override
5
+ // string, computed from the app contract at descriptor-build time), and the
6
+ // worker reads every user-facing string through `copyParam(key, fallback)`.
7
+ //
8
+ // STASHED (the stashed appear-params idiom, himi-branded-lite-home): the
9
+ // param-carrying onAppear dispatch updates the stash; later arg-less calls
10
+ // (self-kick refreshes, retry buttons, pull-to-refresh) fall back to it, so
11
+ // resolution order can never drop the pack. With no pack the param is absent
12
+ // and every read returns `fallback` verbatim — routing a worker string through
13
+ // copyParam is byte-for-byte behavior-preserving.
14
+ //
15
+ // Pure + Hermes-safe (no host globals, no Node APIs, no imports): bundled into
16
+ // every worker that emits user-facing copy, and copied verbatim into the
17
+ // generated templates' tier5-src/_shared/.
18
+
19
+ let stashedCopy: Record<string, string> = {};
20
+
21
+ /** Absorb the `copy` onAppear param (a JSON object of key → override string)
22
+ * from an action arg. Call first in every action that can receive onAppear
23
+ * params (load/refresh). Non-copy args and malformed packs are ignored — the
24
+ * stock fallbacks keep rendering. */
25
+ export function stashCopy(arg: unknown): void {
26
+ if (!arg || typeof arg !== "object") return;
27
+ const raw = (arg as Record<string, unknown>).copy;
28
+ if (typeof raw !== "string" || !raw) return;
29
+ try {
30
+ const parsed = JSON.parse(raw) as unknown;
31
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
32
+ const next: Record<string, string> = {};
33
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
34
+ if (typeof value === "string") next[key] = value;
35
+ }
36
+ stashedCopy = next;
37
+ } catch {
38
+ // Malformed pack param — keep the stock copy rather than dying mid-action.
39
+ }
40
+ }
41
+
42
+ /** The provisioned override for `key`, or `fallback` (the stock copy) when no
43
+ * pack / no such key reached this worker. */
44
+ export function copyParam(key: string, fallback: string): string {
45
+ const value = stashedCopy[key];
46
+ return typeof value === "string" && value !== "" ? value : fallback;
47
+ }
48
+
49
+ /** Interpolate `{name}` placeholders in a (possibly overridden) template:
50
+ * `copyTemplate("plans.joinOnSite", "…join {name}.", { name })`. Unknown
51
+ * placeholders resolve to "" so a typo'd override degrades, never crashes. */
52
+ export function copyTemplate(key: string, fallback: string, values: Record<string, string | number>): string {
53
+ return copyParam(key, fallback).replace(/\{([A-Za-z0-9_]+)\}/g, (_all, name: string) =>
54
+ name in values ? String(values[name]) : "",
55
+ );
56
+ }
@@ -0,0 +1,130 @@
1
+ // himi-notification-settings — the shared Tier-5 worker behind an app's Notifications
2
+ // settings screen: a native feature an app built on its website needs (App Store guideline 4.2).
3
+ //
4
+ // It shows whether this app may send notifications and offers ONE action:
5
+ // - off → "Turn on notifications": the OS prompt when the OS can still show it, else the OS
6
+ // settings. Whether a prompt is still possible is not knowable up front on every platform
7
+ // (Android reports "denied" both before the first ask and after a second refusal), so the
8
+ // action asks first and falls through to the settings when the answer is not a grant.
9
+ // - on → "Open settings", to turn them off there.
10
+ // While the status is loading — or cannot be read at all — neither action shows.
11
+ //
12
+ // The screen must refresh when the user comes BACK from the OS settings, and a worker has
13
+ // no clock. `refresh` (the screen's onAppear) therefore arms a `_poll`: the host suspends it
14
+ // in the background and FIRES IT IMMEDIATELY on foreground, which is exactly the return
15
+ // from Settings; the slow cadence in between is only a backstop. Re-armed on every entry,
16
+ // because restored state arms no timer. A tick that sees notifications newly turned on calls
17
+ // requestPermission(), which (with no prompt, the user has already decided) registers the
18
+ // push token for this session.
19
+ //
20
+ // Copy is overridable through an optional `copy` param on the onAppear action (stashCopy /
21
+ // copyParam), like the account worker; a descriptor that passes none gets the English
22
+ // defaults. Declares the "push" capability.
23
+
24
+ import { defineService, type PollDirective } from "@himalaya/state";
25
+ import { getPermissionStatus, openSettings, requestPermission, type PushPermission } from "@himalaya/push";
26
+ import { stashCopy, copyParam } from "../_shared/himi-copy.js";
27
+
28
+ type Status = PushPermission | "loading" | "error";
29
+
30
+ interface S {
31
+ status: Status;
32
+ statusTitle: string;
33
+ statusBody: string;
34
+ /** Hide flags, true until the status is known (and in the error state), so the live
35
+ * screen shows no action while loading. NOTE an unresolved `$service:` hidden path fails
36
+ * OPEN on every renderer — a descriptor with no running worker would show both — which is
37
+ * why a seeded, worker-less copy of the screen must not carry the buttons at all. */
38
+ hideTurnOn: boolean;
39
+ hideOpenSettings: boolean;
40
+ /** A one-line note after an action (e.g. settings could not be opened here). */
41
+ message: string;
42
+ }
43
+
44
+ const POLL: PollDirective = {
45
+ id: "himi-notification-settings",
46
+ intervalMs: 60_000,
47
+ action: "himiNotificationSettings.check",
48
+ immediate: false,
49
+ };
50
+
51
+ /** The whole view for a status. Pure, so the test pins every branch. */
52
+ export function viewFor(status: Status): Omit<S, "message"> {
53
+ const on = status === "granted";
54
+ const off = status === "denied" || status === "undetermined";
55
+ const statusTitle = on
56
+ ? copyParam("notificationSettings.onTitle", "Notifications are on")
57
+ : off
58
+ ? copyParam("notificationSettings.offTitle", "Notifications are off")
59
+ : copyParam("notificationSettings.loadingTitle", "Notifications");
60
+ const statusBody = on
61
+ ? copyParam("notificationSettings.onBody", "You'll get updates from this app. You can turn them off in your device settings.")
62
+ : off
63
+ ? copyParam("notificationSettings.offBody", "Turn them on to get updates from this app.")
64
+ : status === "error"
65
+ ? copyParam("notificationSettings.unavailableBody", "Notification settings aren't available here.")
66
+ : "";
67
+ return { status, statusTitle, statusBody, hideTurnOn: !off, hideOpenSettings: !on };
68
+ }
69
+
70
+ async function readStatus(): Promise<Status> {
71
+ try {
72
+ return await getPermissionStatus();
73
+ } catch {
74
+ // A missing or failing push capability is not "undetermined": offering Turn on would
75
+ // lead nowhere. Say it is unavailable and offer nothing.
76
+ return "error";
77
+ }
78
+ }
79
+
80
+ async function open(): Promise<boolean> {
81
+ try {
82
+ return await openSettings();
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ const OPEN_FAILED = () => copyParam("notificationSettings.openFailed", "Change notifications for this app in your device or browser settings.");
89
+
90
+ const svc = defineService<S, {
91
+ refresh: (s: S, arg?: unknown) => Promise<S & { _poll: PollDirective }>;
92
+ check: (s: S) => Promise<S>;
93
+ enable: (s: S) => Promise<S>;
94
+ openSettings: (s: S) => Promise<S>;
95
+ }>("himiNotificationSettings", {
96
+ state: { ...viewFor("loading"), message: "" },
97
+ actions: {
98
+ // onAppear: read the status and (re-)arm the foreground-refresh poll.
99
+ refresh: async (s, arg) => {
100
+ stashCopy(arg);
101
+ return { ...s, ...viewFor(await readStatus()), message: "", _poll: POLL };
102
+ },
103
+ // One poll tick: idempotent — the status is just re-read.
104
+ check: async (s) => {
105
+ const status = await readStatus();
106
+ if (status === s.status) return s;
107
+ // Turned on in the OS settings while away: register the token for this session.
108
+ if (status === "granted") await requestPermission().catch(() => "granted");
109
+ return { ...s, ...viewFor(status), message: "" };
110
+ },
111
+ // Ask if the OS can still prompt; anything but a grant goes to the OS settings.
112
+ enable: async (s) => {
113
+ let status: Status;
114
+ try {
115
+ status = await requestPermission();
116
+ } catch {
117
+ status = await readStatus();
118
+ }
119
+ if (status === "granted") return { ...s, ...viewFor(status), message: "" };
120
+ const opened = await open();
121
+ return { ...s, ...viewFor(status), message: opened ? "" : OPEN_FAILED() };
122
+ },
123
+ openSettings: async (s) => {
124
+ // Coming back fires the poll, which repaints the status; nothing to do on success.
125
+ return { ...s, message: (await open()) ? "" : OPEN_FAILED() };
126
+ },
127
+ },
128
+ });
129
+
130
+ export { svc };
@@ -109,11 +109,21 @@
109
109
  "weight": 700,
110
110
  "line": 1.2
111
111
  },
112
+ "title2": {
113
+ "size": 22,
114
+ "weight": 700,
115
+ "line": 1.22
116
+ },
112
117
  "body": {
113
118
  "size": 16,
114
119
  "weight": 400,
115
120
  "line": 1.5
116
121
  },
122
+ "footnote": {
123
+ "size": 14,
124
+ "weight": 500,
125
+ "line": 1.35
126
+ },
117
127
  "caption": {
118
128
  "size": 13,
119
129
  "weight": 400,
@@ -1,4 +1,4 @@
1
- // Copy-as-configuration for WORKER-emitted strings (docs/designing-himi-screens.md
1
+ // Copy-as-configuration for WORKER-emitted strings (`himi docs designing-himi-screens`,
2
2
  // §"Copy is configuration"): a worker never hardcodes user-facing copy — the
3
3
  // descriptor bakes the provisioned copy-pack overrides relevant to this worker
4
4
  // into its onAppear action params as a `copy` JSON object (key → override