@aglyn/aglyn 1.0.0-beta.230 → 1.0.0-beta.232

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 (150) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +15 -2
  3. package/src/lib/app-utils/account-acquisition.js +20 -1
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/artifact-list-queries.d.ts +35 -0
  6. package/src/lib/app-utils/artifact-list-queries.js +203 -0
  7. package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
  8. package/src/lib/app-utils/business-profile.d.ts +171 -0
  9. package/src/lib/app-utils/business-profile.js +277 -0
  10. package/src/lib/app-utils/business-profile.js.map +1 -0
  11. package/src/lib/app-utils/child-contract.d.ts +4 -1
  12. package/src/lib/app-utils/child-contract.js +5 -2
  13. package/src/lib/app-utils/child-contract.js.map +1 -1
  14. package/src/lib/app-utils/console-routes.d.ts +5 -0
  15. package/src/lib/app-utils/console-routes.js +1 -0
  16. package/src/lib/app-utils/console-routes.js.map +1 -1
  17. package/src/lib/app-utils/content-schema-type.d.ts +9 -0
  18. package/src/lib/app-utils/content-schema-type.js +4 -0
  19. package/src/lib/app-utils/content-schema-type.js.map +1 -1
  20. package/src/lib/app-utils/crm.d.ts +14 -1
  21. package/src/lib/app-utils/crm.js +19 -2
  22. package/src/lib/app-utils/crm.js.map +1 -1
  23. package/src/lib/app-utils/docs-help.generated.d.ts +119 -5
  24. package/src/lib/app-utils/docs-help.generated.js +290 -2
  25. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  26. package/src/lib/app-utils/docs-index.generated.js +697 -32
  27. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  28. package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
  29. package/src/lib/app-utils/entry-list-declaration.js +172 -0
  30. package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
  31. package/src/lib/app-utils/first-version-seed.d.ts +19 -0
  32. package/src/lib/app-utils/first-version-seed.js +53 -0
  33. package/src/lib/app-utils/first-version-seed.js.map +1 -0
  34. package/src/lib/app-utils/host-status.d.ts +85 -0
  35. package/src/lib/app-utils/host-status.js +115 -0
  36. package/src/lib/app-utils/host-status.js.map +1 -0
  37. package/src/lib/app-utils/local-business.d.ts +8 -0
  38. package/src/lib/app-utils/local-business.js +4 -0
  39. package/src/lib/app-utils/local-business.js.map +1 -1
  40. package/src/lib/app-utils/lockdown.js +1 -1
  41. package/src/lib/app-utils/lockdown.js.map +1 -1
  42. package/src/lib/app-utils/media-filter.d.ts +136 -0
  43. package/src/lib/app-utils/media-filter.js +400 -0
  44. package/src/lib/app-utils/media-filter.js.map +1 -0
  45. package/src/lib/app-utils/mobile-push.d.ts +98 -0
  46. package/src/lib/app-utils/mobile-push.js +97 -0
  47. package/src/lib/app-utils/mobile-push.js.map +1 -0
  48. package/src/lib/app-utils/notification-push.d.ts +38 -0
  49. package/src/lib/app-utils/notification-push.js +54 -0
  50. package/src/lib/app-utils/notification-push.js.map +1 -0
  51. package/src/lib/app-utils/notifications.d.ts +7 -0
  52. package/src/lib/app-utils/notifications.js.map +1 -1
  53. package/src/lib/app-utils/organizations.js +5 -2
  54. package/src/lib/app-utils/organizations.js.map +1 -1
  55. package/src/lib/app-utils/page-markdown.js +3 -0
  56. package/src/lib/app-utils/page-markdown.js.map +1 -1
  57. package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
  58. package/src/lib/app-utils/plugin-host-events.generated.js +149 -0
  59. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
  60. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  61. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -0
  62. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  63. package/src/lib/app-utils/realm-host-surface.generated.js +3 -0
  64. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  65. package/src/lib/app-utils/release-flags.js +6 -2
  66. package/src/lib/app-utils/release-flags.js.map +1 -1
  67. package/src/lib/app-utils/scope-tokens.d.ts +16 -1
  68. package/src/lib/app-utils/scope-tokens.js +15 -1
  69. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  70. package/src/lib/app-utils/site-list-query.d.ts +47 -0
  71. package/src/lib/app-utils/site-list-query.js +142 -0
  72. package/src/lib/app-utils/site-list-query.js.map +1 -0
  73. package/src/lib/app-utils/site-wide-outbox.d.ts +95 -0
  74. package/src/lib/app-utils/site-wide-outbox.js +117 -0
  75. package/src/lib/app-utils/site-wide-outbox.js.map +1 -0
  76. package/src/lib/app-utils/transfer-launcher-context.d.ts +6 -0
  77. package/src/lib/app-utils/transfer-launcher-context.js.map +1 -1
  78. package/src/lib/app-utils/upload-inspection.js +7 -0
  79. package/src/lib/app-utils/upload-inspection.js.map +1 -1
  80. package/src/lib/app-utils/webhook-delivery.js +4 -1
  81. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  82. package/src/lib/foundation/definitions/org-billing.types.d.ts +8 -0
  83. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  84. package/src/lib/foundation/definitions/organization.types.d.ts +18 -7
  85. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  86. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +4 -1
  87. package/src/lib/foundation/definitions/write-deny-coverage.util.js +12 -2
  88. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  89. package/src/lib/plugin-manager/enabled-plugins.js +4 -2
  90. package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
  91. package/src/lib/plugin-manager/feature-plugins.d.ts +107 -0
  92. package/src/lib/plugin-manager/feature-plugins.js +52 -0
  93. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  94. package/src/lib/plugin-manager/first-party-plugins.generated.js +326 -2
  95. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  96. package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
  97. package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
  98. package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
  99. package/src/lib/plugin-manager/plugin-checkout-extras.d.ts +168 -0
  100. package/src/lib/plugin-manager/plugin-checkout-extras.js +172 -0
  101. package/src/lib/plugin-manager/plugin-checkout-extras.js.map +1 -0
  102. package/src/lib/plugin-manager/plugin-contributions.d.ts +7 -0
  103. package/src/lib/plugin-manager/plugin-contributions.js +1 -1
  104. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  105. package/src/lib/plugin-manager/plugin-domain-events.d.ts +138 -0
  106. package/src/lib/plugin-manager/plugin-domain-events.js +148 -0
  107. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -0
  108. package/src/lib/plugin-manager/plugin-events.d.ts +30 -0
  109. package/src/lib/plugin-manager/plugin-events.js +4 -0
  110. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  111. package/src/lib/plugin-manager/plugin-fulfillment-providers.d.ts +101 -0
  112. package/src/lib/plugin-manager/plugin-fulfillment-providers.js +83 -0
  113. package/src/lib/plugin-manager/plugin-fulfillment-providers.js.map +1 -0
  114. package/src/lib/plugin-manager/plugin-permissions.js +21 -5
  115. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  116. package/src/lib/plugin-manager/plugin-person-records.d.ts +90 -0
  117. package/src/lib/plugin-manager/plugin-person-records.js +26 -0
  118. package/src/lib/plugin-manager/plugin-person-records.js.map +1 -1
  119. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +209 -0
  120. package/src/lib/plugin-manager/plugin-product-catalog.js +43 -0
  121. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -0
  122. package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
  123. package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
  124. package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
  125. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +210 -0
  126. package/src/lib/plugin-manager/plugin-shipment-records.js +63 -0
  127. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -0
  128. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +151 -0
  129. package/src/lib/plugin-manager/plugin-shipping-rates.js +62 -0
  130. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -0
  131. package/src/lib/plugin-manager/plugin-sms-messaging.d.ts +103 -0
  132. package/src/lib/plugin-manager/plugin-sms-messaging.js +39 -0
  133. package/src/lib/plugin-manager/plugin-sms-messaging.js.map +1 -0
  134. package/src/lib/plugin-manager/plugin-stock-levels.d.ts +81 -0
  135. package/src/lib/plugin-manager/plugin-stock-levels.js +32 -0
  136. package/src/lib/plugin-manager/plugin-stock-levels.js.map +1 -0
  137. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +154 -0
  138. package/src/lib/plugin-manager/plugin-tax-profile.js +56 -0
  139. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  140. package/src/lib/plugin-manager/plugin-theme-font-catalog.d.ts +59 -0
  141. package/src/lib/plugin-manager/plugin-theme-font-catalog.js +40 -0
  142. package/src/lib/plugin-manager/plugin-theme-font-catalog.js.map +1 -0
  143. package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
  144. package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
  145. package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
  146. package/src/lib/plugin-manager/plugin-tracking-pages.d.ts +55 -0
  147. package/src/lib/plugin-manager/plugin-tracking-pages.js +76 -0
  148. package/src/lib/plugin-manager/plugin-tracking-pages.js.map +1 -0
  149. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +3 -0
  150. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter';
7
+ import type { ListQueryDeclaration } from '@aglyn/shared-util-tools/list-query/list-query-plan';
8
+ export declare const ENTRY_LIST_FILTER_FIELDS: readonly ListFilterField[];
9
+ /** How each entry field reads — as a hidden column's header, and on a chip. */
10
+ export declare const ENTRY_LIST_FILTER_HEADERS: Readonly<Record<string, string>>;
11
+ /**
12
+ * Everything a FILTERED or searched entries list can ask of Firestore
13
+ * (AGL-3321): the clauses (`ENTRY_LIST_FILTER_FIELDS`), the quick search's
14
+ * token array, and the orders those predicates may be combined with.
15
+ *
16
+ * Three orders, each justified by the four composites it costs (one per
17
+ * predicate field — status, category, author, title tokens):
18
+ *
19
+ * Published, newest first the list's default; mandatory.
20
+ * Published, oldest first the same column reversed — the first posts in
21
+ * a category, the earliest of an author's.
22
+ * Updated, newest first what was just edited, which is the question an
23
+ * editor brings to a filtered list.
24
+ *
25
+ * Title and Status, and Updated oldest first, would cost four more each and
26
+ * are offered on the UNFILTERED list only, where they cost none.
27
+ */
28
+ export declare const ENTRY_LIST_QUERY: ListQueryDeclaration;
29
+ /** The stored statuses the Status filter offers, in the order it lists them. */
30
+ export declare const ENTRY_STATUS_OPTIONS: ReadonlyArray<{
31
+ value: string;
32
+ label: string;
33
+ }>;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */ import { ENTRY_PUBLISH_SORT_FIELD } from "./collection-entry-date.js";
6
+ /**
7
+ * The array the quick search and the title filter read: the same key
8
+ * `ENTRY_TITLE_TOKENS_FIELD` names in `content-query-fields.ts`, written by
9
+ * `entryTitleSearchFields`. Named here rather than imported because that
10
+ * module reaches the author model, which the native apps never read.
11
+ */ const ENTRY_TITLE_TOKENS_FIELD = 'titleTokens';
12
+ /*
13
+ * WHAT A COLLECTION'S ENTRY LIST ASKS FIRESTORE (AGL-3321), as one pure
14
+ * module: the fields its Filters panel accepts, their headers, the orders a
15
+ * filtered list may take, and the statuses. The console's Content page plans
16
+ * its queries from here (`entry-list-query.ts`, `list-filters.ts`), and so do
17
+ * the native apps, through the contracts codegen (AGL-3668).
18
+ */ /*
19
+ * Content entries (`hosts/{hostId}/collections/{collectionId}/entries`).
20
+ *
21
+ * EVERY clause below is on the Firestore query together (AGL-3321), composed
22
+ * by `planListQuery` through `ENTRY_LIST_QUERY` in
23
+ * this module, which also holds the orders a filtered list may take. Nothing is matched over the rows a page loaded.
24
+ *
25
+ * The equalities — status, category, author — and the title search each need
26
+ * one composite per filtered order (index merging serves every combination of
27
+ * them), which is what keeps this list's filtered orders to three. The two
28
+ * dates are ranges over fields those orders already walk, so they cost no
29
+ * index of their own; a range leads the order, and so reads with ONE ordered
30
+ * query rather than the two-segment walk: every entry it matches carries the
31
+ * field it ranges over.
32
+ *
33
+ * `status` matches the stored word. An entry with no `status` at all — only a
34
+ * hand-written import bundle produces one — matches none of the three, and is
35
+ * still on the unfiltered list.
36
+ *
37
+ * `categoryId` and `authorId` match the stable ids the entry editor writes.
38
+ * An entry that still carries only the legacy free-typed `category` or
39
+ * `authorName` matches no id until one is picked for it in the editor.
40
+ *
41
+ * `title` is WORD-level, over `titleTokens` (`entryTitleSearchFields`): the
42
+ * quick search reads the same array, and a query holds one array clause, so
43
+ * the two do not combine.
44
+ *
45
+ * `publishedAt` is the Published column, and ranges over the date that column
46
+ * shows — `publishSortAt`, the scheduled date while an entry waits and the
47
+ * published date after (AGL-3323). A draft has neither and matches no range.
48
+ */ export const ENTRY_LIST_FILTER_FIELDS = [
49
+ {
50
+ column: 'title',
51
+ kind: 'text',
52
+ path: 'title',
53
+ tokensPath: 'titleTokens',
54
+ operators: [
55
+ 'contains'
56
+ ]
57
+ },
58
+ {
59
+ column: 'status',
60
+ kind: 'exact',
61
+ path: 'status',
62
+ operators: [
63
+ 'equals',
64
+ 'isAnyOf'
65
+ ]
66
+ },
67
+ {
68
+ column: 'categoryId',
69
+ kind: 'exact',
70
+ path: 'categoryId',
71
+ operators: [
72
+ 'equals',
73
+ 'isAnyOf'
74
+ ]
75
+ },
76
+ {
77
+ column: 'authorId',
78
+ kind: 'exact',
79
+ path: 'authorId',
80
+ operators: [
81
+ 'equals',
82
+ 'isAnyOf'
83
+ ]
84
+ },
85
+ {
86
+ column: 'publishedAt',
87
+ kind: 'date',
88
+ path: 'publishSortAt',
89
+ operators: [
90
+ 'is',
91
+ 'after',
92
+ 'onOrAfter',
93
+ 'before',
94
+ 'onOrBefore'
95
+ ]
96
+ },
97
+ {
98
+ column: 'updatedAt',
99
+ kind: 'date',
100
+ path: 'updatedAt',
101
+ operators: [
102
+ 'is',
103
+ 'after',
104
+ 'onOrAfter',
105
+ 'before',
106
+ 'onOrBefore'
107
+ ]
108
+ }
109
+ ];
110
+ /** How each entry field reads — as a hidden column's header, and on a chip. */ export const ENTRY_LIST_FILTER_HEADERS = {
111
+ title: 'Title',
112
+ status: 'Status',
113
+ categoryId: 'Category',
114
+ authorId: 'Author',
115
+ publishedAt: 'Published',
116
+ updatedAt: 'Updated'
117
+ };
118
+ /**
119
+ * Everything a FILTERED or searched entries list can ask of Firestore
120
+ * (AGL-3321): the clauses (`ENTRY_LIST_FILTER_FIELDS`), the quick search's
121
+ * token array, and the orders those predicates may be combined with.
122
+ *
123
+ * Three orders, each justified by the four composites it costs (one per
124
+ * predicate field — status, category, author, title tokens):
125
+ *
126
+ * Published, newest first the list's default; mandatory.
127
+ * Published, oldest first the same column reversed — the first posts in
128
+ * a category, the earliest of an author's.
129
+ * Updated, newest first what was just edited, which is the question an
130
+ * editor brings to a filtered list.
131
+ *
132
+ * Title and Status, and Updated oldest first, would cost four more each and
133
+ * are offered on the UNFILTERED list only, where they cost none.
134
+ */ export const ENTRY_LIST_QUERY = {
135
+ fields: ENTRY_LIST_FILTER_FIELDS,
136
+ sorts: [
137
+ {
138
+ path: ENTRY_PUBLISH_SORT_FIELD,
139
+ direction: 'desc',
140
+ column: 'publishedAt'
141
+ },
142
+ {
143
+ path: ENTRY_PUBLISH_SORT_FIELD,
144
+ direction: 'asc',
145
+ column: 'publishedAt'
146
+ },
147
+ {
148
+ path: 'updatedAt',
149
+ direction: 'desc',
150
+ column: 'updatedAt'
151
+ }
152
+ ],
153
+ search: {
154
+ tokensPath: ENTRY_TITLE_TOKENS_FIELD
155
+ }
156
+ };
157
+ /** The stored statuses the Status filter offers, in the order it lists them. */ export const ENTRY_STATUS_OPTIONS = [
158
+ {
159
+ value: 'draft',
160
+ label: 'Draft'
161
+ },
162
+ {
163
+ value: 'published',
164
+ label: 'Published'
165
+ },
166
+ {
167
+ value: 'scheduled',
168
+ label: 'Scheduled'
169
+ }
170
+ ];
171
+
172
+ //# sourceMappingURL=entry-list-declaration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/entry-list-declaration.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { ENTRY_PUBLISH_SORT_FIELD } from './collection-entry-date'\n\n/**\n * The array the quick search and the title filter read: the same key\n * `ENTRY_TITLE_TOKENS_FIELD` names in `content-query-fields.ts`, written by\n * `entryTitleSearchFields`. Named here rather than imported because that\n * module reaches the author model, which the native apps never read.\n */\nconst ENTRY_TITLE_TOKENS_FIELD = 'titleTokens'\n\n/*\n * WHAT A COLLECTION'S ENTRY LIST ASKS FIRESTORE (AGL-3321), as one pure\n * module: the fields its Filters panel accepts, their headers, the orders a\n * filtered list may take, and the statuses. The console's Content page plans\n * its queries from here (`entry-list-query.ts`, `list-filters.ts`), and so do\n * the native apps, through the contracts codegen (AGL-3668).\n */\n\n/*\n * Content entries (`hosts/{hostId}/collections/{collectionId}/entries`).\n *\n * EVERY clause below is on the Firestore query together (AGL-3321), composed\n * by `planListQuery` through `ENTRY_LIST_QUERY` in\n * this module, which also holds the orders a filtered list may take. Nothing is matched over the rows a page loaded.\n *\n * The equalities — status, category, author — and the title search each need\n * one composite per filtered order (index merging serves every combination of\n * them), which is what keeps this list's filtered orders to three. The two\n * dates are ranges over fields those orders already walk, so they cost no\n * index of their own; a range leads the order, and so reads with ONE ordered\n * query rather than the two-segment walk: every entry it matches carries the\n * field it ranges over.\n *\n * `status` matches the stored word. An entry with no `status` at all — only a\n * hand-written import bundle produces one — matches none of the three, and is\n * still on the unfiltered list.\n *\n * `categoryId` and `authorId` match the stable ids the entry editor writes.\n * An entry that still carries only the legacy free-typed `category` or\n * `authorName` matches no id until one is picked for it in the editor.\n *\n * `title` is WORD-level, over `titleTokens` (`entryTitleSearchFields`): the\n * quick search reads the same array, and a query holds one array clause, so\n * the two do not combine.\n *\n * `publishedAt` is the Published column, and ranges over the date that column\n * shows — `publishSortAt`, the scheduled date while an entry waits and the\n * published date after (AGL-3323). A draft has neither and matches no range.\n */\nexport const ENTRY_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'title',\n kind: 'text',\n path: 'title',\n tokensPath: 'titleTokens',\n operators: ['contains'],\n },\n {\n column: 'status',\n kind: 'exact',\n path: 'status',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'categoryId',\n kind: 'exact',\n path: 'categoryId',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'authorId',\n kind: 'exact',\n path: 'authorId',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'publishedAt',\n kind: 'date',\n path: 'publishSortAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n]\n\n/** How each entry field reads — as a hidden column's header, and on a chip. */\nexport const ENTRY_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n title: 'Title',\n status: 'Status',\n categoryId: 'Category',\n authorId: 'Author',\n publishedAt: 'Published',\n updatedAt: 'Updated',\n}\n\n/**\n * Everything a FILTERED or searched entries list can ask of Firestore\n * (AGL-3321): the clauses (`ENTRY_LIST_FILTER_FIELDS`), the quick search's\n * token array, and the orders those predicates may be combined with.\n *\n * Three orders, each justified by the four composites it costs (one per\n * predicate field — status, category, author, title tokens):\n *\n * Published, newest first the list's default; mandatory.\n * Published, oldest first the same column reversed — the first posts in\n * a category, the earliest of an author's.\n * Updated, newest first what was just edited, which is the question an\n * editor brings to a filtered list.\n *\n * Title and Status, and Updated oldest first, would cost four more each and\n * are offered on the UNFILTERED list only, where they cost none.\n */\nexport const ENTRY_LIST_QUERY: ListQueryDeclaration = {\n fields: ENTRY_LIST_FILTER_FIELDS,\n sorts: [\n { path: ENTRY_PUBLISH_SORT_FIELD, direction: 'desc', column: 'publishedAt' },\n { path: ENTRY_PUBLISH_SORT_FIELD, direction: 'asc', column: 'publishedAt' },\n { path: 'updatedAt', direction: 'desc', column: 'updatedAt' },\n ],\n search: { tokensPath: ENTRY_TITLE_TOKENS_FIELD },\n}\n\n/** The stored statuses the Status filter offers, in the order it lists them. */\nexport const ENTRY_STATUS_OPTIONS: ReadonlyArray<{\n value: string\n label: string\n}> = [\n { value: 'draft', label: 'Draft' },\n { value: 'published', label: 'Published' },\n { value: 'scheduled', label: 'Scheduled' },\n]\n"],"names":["ENTRY_PUBLISH_SORT_FIELD","ENTRY_TITLE_TOKENS_FIELD","ENTRY_LIST_FILTER_FIELDS","column","kind","path","tokensPath","operators","ENTRY_LIST_FILTER_HEADERS","title","status","categoryId","authorId","publishedAt","updatedAt","ENTRY_LIST_QUERY","fields","sorts","direction","search","ENTRY_STATUS_OPTIONS","value","label"],"mappings":"AAAA;;;;CAIC,GAID,SAASA,wBAAwB,QAAQ,6BAAyB;AAElE;;;;;CAKC,GACD,MAAMC,2BAA2B;AAEjC;;;;;;CAMC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,MAAMC,2BAAuD;IAClE;QACEC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZC,WAAW;YAAC;SAAW;IACzB;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;SAAa;IACjE;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;SAAa;IACjE;CACD,CAAA;AAED,6EAA6E,GAC7E,OAAO,MAAMC,4BAA8D;IACzEC,OAAO;IACPC,QAAQ;IACRC,YAAY;IACZC,UAAU;IACVC,aAAa;IACbC,WAAW;AACb,EAAC;AAED;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,mBAAyC;IACpDC,QAAQd;IACRe,OAAO;QACL;YAAEZ,MAAML;YAA0BkB,WAAW;YAAQf,QAAQ;QAAc;QAC3E;YAAEE,MAAML;YAA0BkB,WAAW;YAAOf,QAAQ;QAAc;QAC1E;YAAEE,MAAM;YAAaa,WAAW;YAAQf,QAAQ;QAAY;KAC7D;IACDgB,QAAQ;QAAEb,YAAYL;IAAyB;AACjD,EAAC;AAED,8EAA8E,GAC9E,OAAO,MAAMmB,uBAGR;IACH;QAAEC,OAAO;QAASC,OAAO;IAAQ;IACjC;QAAED,OAAO;QAAaC,OAAO;IAAY;IACzC;QAAED,OAAO;QAAaC,OAAO;IAAY;CAC1C,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /** The back-pointer key a version of each parent kind carries. */
7
+ export declare const VERSION_PARENT_KEYS: Readonly<Record<string, string>>;
8
+ /** A canvas with nothing on it but its root. */
9
+ export declare function rootOnlyCanvas(): Record<string, unknown>;
10
+ /**
11
+ * The first version's document for [parentId] of [kind], from the parent's
12
+ * stored fields with its node map already DECODED. Null for a kind with no
13
+ * versions.
14
+ */
15
+ export declare function firstVersionSeed(kind: string, parentId: string, hostId: string, parent: {
16
+ nodes?: Record<string, unknown> | null;
17
+ rootId?: unknown;
18
+ props?: unknown;
19
+ }): Record<string, unknown> | null;
@@ -0,0 +1,53 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ * SPDX-License-Identifier: Apache-2.0
6
+ */ /*
7
+ * A resource's FIRST version, seeded from the resource itself (AGL-3668).
8
+ *
9
+ * A reusable component is created with its design on its own document and no
10
+ * version, and the console's "Open Besigner" mints the first version from
11
+ * that document before it opens it: the decoded node map (or a root-only
12
+ * canvas when it is empty, never `{}`, which renders as "Invalid node"), the
13
+ * canvas root, and the declared properties. The native apps cannot decode a
14
+ * compressed node map, so `/api/hosts/versions` builds the same seed on the
15
+ * server from the stored document (`seedFromParent`), and the node map never
16
+ * crosses the wire. This is that seed, as one pure rule both sides share.
17
+ */ const CANVAS_ROOT_ID = '_@_';
18
+ /** The back-pointer key a version of each parent kind carries. */ export const VERSION_PARENT_KEYS = {
19
+ screen: 'screenId',
20
+ layout: 'layoutId',
21
+ component: 'componentId',
22
+ form: 'formId'
23
+ };
24
+ /** A canvas with nothing on it but its root. */ export function rootOnlyCanvas() {
25
+ return {
26
+ [CANVAS_ROOT_ID]: {
27
+ $id: CANVAS_ROOT_ID,
28
+ componentId: 'div',
29
+ nodes: []
30
+ }
31
+ };
32
+ }
33
+ /**
34
+ * The first version's document for [parentId] of [kind], from the parent's
35
+ * stored fields with its node map already DECODED. Null for a kind with no
36
+ * versions.
37
+ */ export function firstVersionSeed(kind, parentId, hostId, parent) {
38
+ const backPointer = VERSION_PARENT_KEYS[kind];
39
+ if (!backPointer) return null;
40
+ const nodes = parent.nodes && Object.keys(parent.nodes).length ? parent.nodes : rootOnlyCanvas();
41
+ return _extends({
42
+ [backPointer]: parentId,
43
+ hostId,
44
+ displayName: 'Initial version',
45
+ nodes
46
+ }, typeof parent.rootId === 'string' && parent.rootId ? {
47
+ rootId: parent.rootId
48
+ } : {}, Array.isArray(parent.props) && parent.props.length ? {
49
+ props: parent.props
50
+ } : {});
51
+ }
52
+
53
+ //# sourceMappingURL=first-version-seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/first-version-seed.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/*\n * A resource's FIRST version, seeded from the resource itself (AGL-3668).\n *\n * A reusable component is created with its design on its own document and no\n * version, and the console's \"Open Besigner\" mints the first version from\n * that document before it opens it: the decoded node map (or a root-only\n * canvas when it is empty, never `{}`, which renders as \"Invalid node\"), the\n * canvas root, and the declared properties. The native apps cannot decode a\n * compressed node map, so `/api/hosts/versions` builds the same seed on the\n * server from the stored document (`seedFromParent`), and the node map never\n * crosses the wire. This is that seed, as one pure rule both sides share.\n */\n\nconst CANVAS_ROOT_ID = '_@_'\n\n/** The back-pointer key a version of each parent kind carries. */\nexport const VERSION_PARENT_KEYS: Readonly<Record<string, string>> = {\n screen: 'screenId',\n layout: 'layoutId',\n component: 'componentId',\n form: 'formId',\n}\n\n/** A canvas with nothing on it but its root. */\nexport function rootOnlyCanvas(): Record<string, unknown> {\n return { [CANVAS_ROOT_ID]: { $id: CANVAS_ROOT_ID, componentId: 'div', nodes: [] } }\n}\n\n/**\n * The first version's document for [parentId] of [kind], from the parent's\n * stored fields with its node map already DECODED. Null for a kind with no\n * versions.\n */\nexport function firstVersionSeed(\n kind: string,\n parentId: string,\n hostId: string,\n parent: {\n nodes?: Record<string, unknown> | null\n rootId?: unknown\n props?: unknown\n },\n): Record<string, unknown> | null {\n const backPointer = VERSION_PARENT_KEYS[kind]\n if (!backPointer) return null\n const nodes = parent.nodes && Object.keys(parent.nodes).length ? parent.nodes : rootOnlyCanvas()\n return {\n [backPointer]: parentId,\n hostId,\n displayName: 'Initial version',\n nodes,\n ...(typeof parent.rootId === 'string' && parent.rootId ? { rootId: parent.rootId } : {}),\n ...(Array.isArray(parent.props) && parent.props.length ? { props: parent.props } : {}),\n }\n}\n"],"names":["CANVAS_ROOT_ID","VERSION_PARENT_KEYS","screen","layout","component","form","rootOnlyCanvas","$id","componentId","nodes","firstVersionSeed","kind","parentId","hostId","parent","backPointer","Object","keys","length","displayName","rootId","Array","isArray","props"],"mappings":";AAAA;;;;CAIC,GAED;;;;;;;;;;;CAWC,GAED,MAAMA,iBAAiB;AAEvB,gEAAgE,GAChE,OAAO,MAAMC,sBAAwD;IACnEC,QAAQ;IACRC,QAAQ;IACRC,WAAW;IACXC,MAAM;AACR,EAAC;AAED,8CAA8C,GAC9C,OAAO,SAASC;IACd,OAAO;QAAE,CAACN,eAAe,EAAE;YAAEO,KAAKP;YAAgBQ,aAAa;YAAOC,OAAO,EAAE;QAAC;IAAE;AACpF;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBACdC,IAAY,EACZC,QAAgB,EAChBC,MAAc,EACdC,MAIC;IAED,MAAMC,cAAcd,mBAAmB,CAACU,KAAK;IAC7C,IAAI,CAACI,aAAa,OAAO;IACzB,MAAMN,QAAQK,OAAOL,KAAK,IAAIO,OAAOC,IAAI,CAACH,OAAOL,KAAK,EAAES,MAAM,GAAGJ,OAAOL,KAAK,GAAGH;IAChF,OAAO;QACL,CAACS,YAAY,EAAEH;QACfC;QACAM,aAAa;QACbV;OACI,OAAOK,OAAOM,MAAM,KAAK,YAAYN,OAAOM,MAAM,GAAG;QAAEA,QAAQN,OAAOM,MAAM;IAAC,IAAI,CAAC,GAClFC,MAAMC,OAAO,CAACR,OAAOS,KAAK,KAAKT,OAAOS,KAAK,CAACL,MAAM,GAAG;QAAEK,OAAOT,OAAOS,KAAK;IAAC,IAAI,CAAC;AAExF"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ export interface HostStatus {
18
+ /** `Live`, `Draft`, `Maintenance`, `Suspended`. */
19
+ label: string;
20
+ color: 'success' | 'default' | 'warning' | 'error';
21
+ /** Why, in one sentence — the pill's tooltip. */
22
+ detail: string;
23
+ }
24
+ /**
25
+ * How many pages this site publishes.
26
+ *
27
+ * Read off `host.screens` — the map publishing writes into the host document
28
+ * (`hosts/{hostId}.screens[screenId] = slug`, see `template-screens.ts`) and
29
+ * the tenant reads to route a request. So a site with an entry has at least
30
+ * one page a visitor can reach, and one without has none. That makes this a
31
+ * fact already sitting in a document its readers hold: **no extra read**, on
32
+ * a page that can list a hundred sites.
33
+ *
34
+ * ⚠️ It counts PUBLISHED ROUTES, not authored work. A site with five
35
+ * unpublished drafts counts 0 — the map is written by publishing and by
36
+ * nothing else. Every caller is asking "what can a visitor reach?"; a caller
37
+ * that means "has anyone worked on this?" is asking a different question and
38
+ * this is the wrong number for it.
39
+ *
40
+ * The placeholder home page a new site is created with (AGL-3408) COUNTS
41
+ * here: it is a page a visitor reaches, so a new site is Live. The first-run
42
+ * offer asks a different question — has the OWNER published anything? — and
43
+ * reads `hostIsBlankSite`, which discounts it.
44
+ */
45
+ export declare function publishedScreenCount(host: {
46
+ screens?: Record<string, unknown>;
47
+ } | null | undefined): number;
48
+ /**
49
+ * The `Live` / `Draft` pill the console Sites mockup puts on every card
50
+ * (AGL-2166).
51
+ *
52
+ * Derived from {@link publishedScreenCount}: the pill reports what a visitor
53
+ * can reach, placeholder home page included.
54
+ *
55
+ * The staff admin host page has rendered a `published` / `draft` chip since
56
+ * AGL-390 off `host.published`, which is not a field on `AglynHost` and is
57
+ * written by nothing — so that chip has always said `draft`. Deriving from
58
+ * `screens` is what it should have done.
59
+ *
60
+ * Order matters. A suspended site is not "live" whatever it has published,
61
+ * and a site in maintenance is serving the 503 screen rather than its
62
+ * pages — reporting either as `Live` would be the console agreeing with a
63
+ * customer who thinks their site is up.
64
+ */
65
+ export declare function describeHostStatus(host: {
66
+ screens?: Record<string, unknown>;
67
+ maintenance?: boolean;
68
+ suspendedAt?: number;
69
+ suspendedUntilMs?: number;
70
+ }): HostStatus;
71
+ /**
72
+ * `6 of 10 sites · Business plan` — the meta line the Sites mockup puts
73
+ * opposite the section heading.
74
+ *
75
+ * Returns `undefined` rather than a partial line while the org or its plan
76
+ * is still resolving. A count against the FREE limit is what an unresolved
77
+ * org produces, and telling a Business customer they are at "1 of 1 sites"
78
+ * is how a correct page delivers a false upgrade prompt.
79
+ */
80
+ export declare function describeSiteAllowance(options: {
81
+ used: number;
82
+ limit: number | undefined;
83
+ planLabel: string | undefined;
84
+ ready: boolean;
85
+ }): string | undefined;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * How many pages this site publishes.
18
+ *
19
+ * Read off `host.screens` — the map publishing writes into the host document
20
+ * (`hosts/{hostId}.screens[screenId] = slug`, see `template-screens.ts`) and
21
+ * the tenant reads to route a request. So a site with an entry has at least
22
+ * one page a visitor can reach, and one without has none. That makes this a
23
+ * fact already sitting in a document its readers hold: **no extra read**, on
24
+ * a page that can list a hundred sites.
25
+ *
26
+ * ⚠️ It counts PUBLISHED ROUTES, not authored work. A site with five
27
+ * unpublished drafts counts 0 — the map is written by publishing and by
28
+ * nothing else. Every caller is asking "what can a visitor reach?"; a caller
29
+ * that means "has anyone worked on this?" is asking a different question and
30
+ * this is the wrong number for it.
31
+ *
32
+ * The placeholder home page a new site is created with (AGL-3408) COUNTS
33
+ * here: it is a page a visitor reaches, so a new site is Live. The first-run
34
+ * offer asks a different question — has the OWNER published anything? — and
35
+ * reads `hostIsBlankSite`, which discounts it.
36
+ */ export function publishedScreenCount(host) {
37
+ var _ref;
38
+ return Object.keys((_ref = host == null ? void 0 : host.screens) != null ? _ref : {}).length;
39
+ }
40
+ /**
41
+ * The `Live` / `Draft` pill the console Sites mockup puts on every card
42
+ * (AGL-2166).
43
+ *
44
+ * Derived from {@link publishedScreenCount}: the pill reports what a visitor
45
+ * can reach, placeholder home page included.
46
+ *
47
+ * The staff admin host page has rendered a `published` / `draft` chip since
48
+ * AGL-390 off `host.published`, which is not a field on `AglynHost` and is
49
+ * written by nothing — so that chip has always said `draft`. Deriving from
50
+ * `screens` is what it should have done.
51
+ *
52
+ * Order matters. A suspended site is not "live" whatever it has published,
53
+ * and a site in maintenance is serving the 503 screen rather than its
54
+ * pages — reporting either as `Live` would be the console agreeing with a
55
+ * customer who thinks their site is up.
56
+ */ export function describeHostStatus(host) {
57
+ if (host == null ? void 0 : host.suspendedAt) {
58
+ // A timed suspension that has elapsed is over, even though the fields
59
+ // are still on the document — the tenant makes the same check.
60
+ const until = host.suspendedUntilMs;
61
+ if (!until || until > Date.now()) {
62
+ return {
63
+ label: 'Suspended',
64
+ color: 'error',
65
+ detail: 'This site is serving a lockdown notice instead of content.'
66
+ };
67
+ }
68
+ }
69
+ if (host == null ? void 0 : host.maintenance) {
70
+ return {
71
+ label: 'Maintenance',
72
+ color: 'warning',
73
+ detail: 'Every path serves the maintenance page.'
74
+ };
75
+ }
76
+ const published = publishedScreenCount(host);
77
+ if (published > 0) {
78
+ return {
79
+ label: 'Live',
80
+ color: 'success',
81
+ detail: `${published} published page${published === 1 ? '' : 's'}.`
82
+ };
83
+ }
84
+ return {
85
+ label: 'Draft',
86
+ color: 'default',
87
+ detail: 'Nothing published yet — visitors see the placeholder.'
88
+ };
89
+ }
90
+ /**
91
+ * `6 of 10 sites · Business plan` — the meta line the Sites mockup puts
92
+ * opposite the section heading.
93
+ *
94
+ * Returns `undefined` rather than a partial line while the org or its plan
95
+ * is still resolving. A count against the FREE limit is what an unresolved
96
+ * org produces, and telling a Business customer they are at "1 of 1 sites"
97
+ * is how a correct page delivers a false upgrade prompt.
98
+ */ export function describeSiteAllowance(options) {
99
+ const { used, limit, planLabel, ready } = options;
100
+ if (!ready || !planLabel) return undefined;
101
+ /*
102
+ * `Number.isFinite`, not `limit < 0` (AGL-2223).
103
+ *
104
+ * `UNLIMITED` is `Number.POSITIVE_INFINITY`, and Enterprise's `hostLimit`
105
+ * is exactly that — so the `< 0` test was never true for the one plan the
106
+ * Unlimited branch existed for, and an Enterprise organization read
107
+ * `4 of Infinity sites · Enterprise plan`. The negative case is kept: an
108
+ * entitlement override can carry a negative sentinel, and reading that as a
109
+ * cap of "-1 sites" would be the same class of nonsense.
110
+ */ const cap = limit === undefined || limit === null ? undefined : !Number.isFinite(limit) || limit < 0 ? 'Unlimited' : String(limit);
111
+ const sites = cap === undefined ? `${used} site${used === 1 ? '' : 's'}` : `${used} of ${cap} site${cap === '1' ? '' : 's'}`;
112
+ return `${sites} · ${planLabel} plan`;
113
+ }
114
+
115
+ //# sourceMappingURL=host-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport interface HostStatus {\n /** `Live`, `Draft`, `Maintenance`, `Suspended`. */\n label: string\n color: 'success' | 'default' | 'warning' | 'error'\n /** Why, in one sentence — the pill's tooltip. */\n detail: string\n}\n\n/**\n * How many pages this site publishes.\n *\n * Read off `host.screens` — the map publishing writes into the host document\n * (`hosts/{hostId}.screens[screenId] = slug`, see `template-screens.ts`) and\n * the tenant reads to route a request. So a site with an entry has at least\n * one page a visitor can reach, and one without has none. That makes this a\n * fact already sitting in a document its readers hold: **no extra read**, on\n * a page that can list a hundred sites.\n *\n * ⚠️ It counts PUBLISHED ROUTES, not authored work. A site with five\n * unpublished drafts counts 0 — the map is written by publishing and by\n * nothing else. Every caller is asking \"what can a visitor reach?\"; a caller\n * that means \"has anyone worked on this?\" is asking a different question and\n * this is the wrong number for it.\n *\n * The placeholder home page a new site is created with (AGL-3408) COUNTS\n * here: it is a page a visitor reaches, so a new site is Live. The first-run\n * offer asks a different question — has the OWNER published anything? — and\n * reads `hostIsBlankSite`, which discounts it.\n */\nexport function publishedScreenCount(\n host: { screens?: Record<string, unknown> } | null | undefined,\n): number {\n return Object.keys(host?.screens ?? {}).length\n}\n\n/**\n * The `Live` / `Draft` pill the console Sites mockup puts on every card\n * (AGL-2166).\n *\n * Derived from {@link publishedScreenCount}: the pill reports what a visitor\n * can reach, placeholder home page included.\n *\n * The staff admin host page has rendered a `published` / `draft` chip since\n * AGL-390 off `host.published`, which is not a field on `AglynHost` and is\n * written by nothing — so that chip has always said `draft`. Deriving from\n * `screens` is what it should have done.\n *\n * Order matters. A suspended site is not \"live\" whatever it has published,\n * and a site in maintenance is serving the 503 screen rather than its\n * pages — reporting either as `Live` would be the console agreeing with a\n * customer who thinks their site is up.\n */\nexport function describeHostStatus(host: {\n screens?: Record<string, unknown>\n maintenance?: boolean\n suspendedAt?: number\n suspendedUntilMs?: number\n}): HostStatus {\n if (host?.suspendedAt) {\n // A timed suspension that has elapsed is over, even though the fields\n // are still on the document — the tenant makes the same check.\n const until = host.suspendedUntilMs\n if (!until || until > Date.now()) {\n return {\n label: 'Suspended',\n color: 'error',\n detail: 'This site is serving a lockdown notice instead of content.',\n }\n }\n }\n if (host?.maintenance) {\n return {\n label: 'Maintenance',\n color: 'warning',\n detail: 'Every path serves the maintenance page.',\n }\n }\n const published = publishedScreenCount(host)\n if (published > 0) {\n return {\n label: 'Live',\n color: 'success',\n detail: `${published} published page${published === 1 ? '' : 's'}.`,\n }\n }\n return {\n label: 'Draft',\n color: 'default',\n detail: 'Nothing published yet — visitors see the placeholder.',\n }\n}\n\n/**\n * `6 of 10 sites · Business plan` — the meta line the Sites mockup puts\n * opposite the section heading.\n *\n * Returns `undefined` rather than a partial line while the org or its plan\n * is still resolving. A count against the FREE limit is what an unresolved\n * org produces, and telling a Business customer they are at \"1 of 1 sites\"\n * is how a correct page delivers a false upgrade prompt.\n */\nexport function describeSiteAllowance(options: {\n used: number\n limit: number | undefined\n planLabel: string | undefined\n ready: boolean\n}): string | undefined {\n const { used, limit, planLabel, ready } = options\n if (!ready || !planLabel) return undefined\n /*\n * `Number.isFinite`, not `limit < 0` (AGL-2223).\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, and Enterprise's `hostLimit`\n * is exactly that — so the `< 0` test was never true for the one plan the\n * Unlimited branch existed for, and an Enterprise organization read\n * `4 of Infinity sites · Enterprise plan`. The negative case is kept: an\n * entitlement override can carry a negative sentinel, and reading that as a\n * cap of \"-1 sites\" would be the same class of nonsense.\n */\n const cap =\n limit === undefined || limit === null\n ? undefined\n : !Number.isFinite(limit) || limit < 0\n ? 'Unlimited'\n : String(limit)\n const sites =\n cap === undefined\n ? `${used} site${used === 1 ? '' : 's'}`\n : `${used} of ${cap} site${cap === '1' ? '' : 's'}`\n return `${sites} · ${planLabel} plan`\n}\n"],"names":["publishedScreenCount","host","Object","keys","screens","length","describeHostStatus","suspendedAt","until","suspendedUntilMs","Date","now","label","color","detail","maintenance","published","describeSiteAllowance","options","used","limit","planLabel","ready","undefined","cap","Number","isFinite","String","sites"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAUD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASA,qBACdC,IAA8D;;IAE9D,OAAOC,OAAOC,IAAI,SAACF,wBAAAA,KAAMG,OAAO,mBAAI,CAAC,GAAGC,MAAM;AAChD;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,mBAAmBL,IAKlC;IACC,IAAIA,wBAAAA,KAAMM,WAAW,EAAE;QACrB,sEAAsE;QACtE,+DAA+D;QAC/D,MAAMC,QAAQP,KAAKQ,gBAAgB;QACnC,IAAI,CAACD,SAASA,QAAQE,KAAKC,GAAG,IAAI;YAChC,OAAO;gBACLC,OAAO;gBACPC,OAAO;gBACPC,QAAQ;YACV;QACF;IACF;IACA,IAAIb,wBAAAA,KAAMc,WAAW,EAAE;QACrB,OAAO;YACLH,OAAO;YACPC,OAAO;YACPC,QAAQ;QACV;IACF;IACA,MAAME,YAAYhB,qBAAqBC;IACvC,IAAIe,YAAY,GAAG;QACjB,OAAO;YACLJ,OAAO;YACPC,OAAO;YACPC,QAAQ,GAAGE,UAAU,eAAe,EAAEA,cAAc,IAAI,KAAK,IAAI,CAAC,CAAC;QACrE;IACF;IACA,OAAO;QACLJ,OAAO;QACPC,OAAO;QACPC,QAAQ;IACV;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,sBAAsBC,OAKrC;IACC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,SAAS,EAAEC,KAAK,EAAE,GAAGJ;IAC1C,IAAI,CAACI,SAAS,CAACD,WAAW,OAAOE;IACjC;;;;;;;;;GASC,GACD,MAAMC,MACJJ,UAAUG,aAAaH,UAAU,OAC7BG,YACA,CAACE,OAAOC,QAAQ,CAACN,UAAUA,QAAQ,IACjC,cACAO,OAAOP;IACf,MAAMQ,QACJJ,QAAQD,YACJ,GAAGJ,KAAK,KAAK,EAAEA,SAAS,IAAI,KAAK,KAAK,GACtC,GAAGA,KAAK,IAAI,EAAEK,IAAI,KAAK,EAAEA,QAAQ,MAAM,KAAK,KAAK;IACvD,OAAO,GAAGI,MAAM,GAAG,EAAEP,UAAU,KAAK,CAAC;AACvC"}
@@ -105,6 +105,14 @@ export declare const LOCAL_BUSINESS_TYPES: readonly [{
105
105
  readonly label: "Animal shelter";
106
106
  }];
107
107
  export type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value'];
108
+ /**
109
+ * {@link LOCAL_BUSINESS_TYPES} as a plain list, for readers that cannot read
110
+ * a literal tuple: the native apps' contracts (AGL-3668).
111
+ */
112
+ export declare const LOCAL_BUSINESS_TYPE_OPTIONS: ReadonlyArray<{
113
+ value: string;
114
+ label: string;
115
+ }>;
108
116
  /** At most this many areas are published; a service area is a list, not a gazetteer. */
109
117
  export declare const AREA_SERVED_MAX = 20;
110
118
  export declare const AREA_SERVED_NAME_MAX_LENGTH = 120;
@@ -126,6 +126,10 @@
126
126
  label: 'Animal shelter'
127
127
  }
128
128
  ];
129
+ /**
130
+ * {@link LOCAL_BUSINESS_TYPES} as a plain list, for readers that cannot read
131
+ * a literal tuple: the native apps' contracts (AGL-3668).
132
+ */ export const LOCAL_BUSINESS_TYPE_OPTIONS = LOCAL_BUSINESS_TYPES;
129
133
  /** At most this many areas are published; a service area is a list, not a gazetteer. */ export const AREA_SERVED_MAX = 20;
130
134
  export const AREA_SERVED_NAME_MAX_LENGTH = 120;
131
135
  /** Seven days, twice over — room for split shifts without inviting a timetable. */ export const OPENING_HOURS_MAX_ROWS = 14;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/local-business.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A site entity that is a LOCAL BUSINESS (AGL-3383).\n *\n * `host.seo.entity.type` answers Organization or Person and keeps answering\n * it: it is a numeric enum stored as `\"1\"`/`\"2\"`, read by the author editor,\n * the logo card and the AI audit, and a third value there would be a third\n * value each of them has to learn. A plumber is still an Organization — the\n * business type REFINES that answer rather than replacing it — so it is a\n * field of its own, `businessType`, holding the `schema.org` subtype name.\n *\n * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the\n * console's Select is built from too. A stored value outside it (a typo, a\n * type removed from the list) publishes the plain Organization it would have\n * published before the field existed, rather than an `@type` no consumer\n * recognizes.\n */\n\n/** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */\nexport const LOCAL_BUSINESS_TYPES = [\n { value: 'LocalBusiness', label: 'Local business (general)' },\n { value: 'HomeAndConstructionBusiness', label: 'Home and construction' },\n { value: 'GeneralContractor', label: 'General contractor' },\n { value: 'HousePainter', label: 'House painter' },\n { value: 'Plumber', label: 'Plumber' },\n { value: 'Electrician', label: 'Electrician' },\n { value: 'HVACBusiness', label: 'Heating and air (HVAC)' },\n { value: 'Locksmith', label: 'Locksmith' },\n { value: 'RoofingContractor', label: 'Roofing contractor' },\n { value: 'MovingCompany', label: 'Moving company' },\n { value: 'ProfessionalService', label: 'Professional service' },\n { value: 'Store', label: 'Store' },\n { value: 'FoodEstablishment', label: 'Food and drink' },\n { value: 'Restaurant', label: 'Restaurant' },\n { value: 'HealthAndBeautyBusiness', label: 'Health and beauty' },\n { value: 'MedicalBusiness', label: 'Medical practice' },\n { value: 'Dentist', label: 'Dentist' },\n { value: 'AutomotiveBusiness', label: 'Automotive' },\n { value: 'LegalService', label: 'Legal service' },\n { value: 'AccountingService', label: 'Accounting' },\n { value: 'RealEstateAgent', label: 'Real estate agent' },\n { value: 'SportsActivityLocation', label: 'Gym, sports or fitness' },\n { value: 'ChildCare', label: 'Child care' },\n { value: 'AnimalShelter', label: 'Animal shelter' },\n] as const\n\nexport type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value']\n\n/** At most this many areas are published; a service area is a list, not a gazetteer. */\nexport const AREA_SERVED_MAX = 20\nexport const AREA_SERVED_NAME_MAX_LENGTH = 120\n/** Seven days, twice over — room for split shifts without inviting a timetable. */\nexport const OPENING_HOURS_MAX_ROWS = 14\n/** Google reads a price range past 100 characters as not being one. */\nexport const PRICE_RANGE_MAX_LENGTH = 100\nexport const PAYMENT_ACCEPTED_MAX_LENGTH = 200\n\n/**\n * The stored business type, as the `schema.org` name it publishes under, or\n * `undefined` when there is none or it is not on the list.\n *\n * Case-insensitive, returning the canonical spelling, because `@type` is\n * case-sensitive to every consumer and `plumber` would name nothing.\n */\nexport function localBusinessType(value: unknown): LocalBusinessType | undefined {\n if (typeof value !== 'string') return undefined\n const wanted = value.trim().toLowerCase()\n if (!wanted) return undefined\n return LOCAL_BUSINESS_TYPES.find((entry) => entry.value.toLowerCase() === wanted)\n ?.value\n}\n\n/**\n * The areas a business serves: trimmed, blanks dropped, de-duplicated without\n * regard to case, and capped.\n *\n * Accepts the stored array or a newline-separated string, so a value typed\n * into the console's text box reads the same whichever shape reached the\n * document.\n */\nexport function normalizeAreaServed(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(/\\r?\\n/)\n : []\n const seen = new Set<string>()\n const areas: string[] = []\n for (const item of items) {\n if (typeof item !== 'string') continue\n const name = item.trim().replace(/\\s+/g, ' ').slice(0, AREA_SERVED_NAME_MAX_LENGTH)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n areas.push(name)\n if (areas.length >= AREA_SERVED_MAX) break\n }\n return areas\n}\n\nconst DAYS = [\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n 'Sunday',\n] as const\n\nexport type OpeningHoursDay = (typeof DAYS)[number]\n\n/** One line of opening hours: these days, from `opens` to `closes`. */\nexport interface OpeningHoursRow {\n days: OpeningHoursDay[]\n /** `HH:MM`, 24-hour. */\n opens: string\n /** `HH:MM`, 24-hour. Earlier than `opens` means the next morning. */\n closes: string\n}\n\n/**\n * A day token, by the shortest prefix that is unambiguous — `Mo`, `Mon` and\n * `Monday` all work, which is every way a person abbreviates a weekday. A\n * single letter is refused: `T` and `S` are each two days.\n */\nconst dayIndex = (token: string): number => {\n const wanted = token.trim().toLowerCase()\n if (wanted.length < 2) return -1\n return DAYS.findIndex((day) => day.toLowerCase().startsWith(wanted))\n}\n\n/** `9:00` → `09:00`; anything that is not a real time of day → `''`. */\nconst clockTime = (hours: string, minutes: string): string => {\n const h = Number(hours)\n const m = Number(minutes)\n if (h > 23 || m > 59) return ''\n return `${String(h).padStart(2, '0')}:${minutes}`\n}\n\nconst DASH = /\\s*[-–—]\\s*/\nconst LINE = /^(.+?)\\s+(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})$/\n\n/** One line, or `null` when any part of it does not read. */\nfunction parseOpeningHoursLine(line: string): OpeningHoursRow | null {\n const match = LINE.exec(line.trim())\n if (!match) return null\n const opens = clockTime(match[2], match[3])\n const closes = clockTime(match[4], match[5])\n if (!opens || !closes) return null\n const days: OpeningHoursDay[] = []\n for (const part of match[1].split(',')) {\n const ends = part.trim().split(DASH)\n if (ends.length > 2) return null\n const from = dayIndex(ends[0])\n const to = dayIndex(ends[ends.length - 1])\n if (from < 0 || to < 0) return null\n // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.\n for (let step = 0; step <= (to - from + 7) % 7; step++) {\n const day = DAYS[(from + step) % 7]\n if (!days.includes(day)) days.push(day)\n }\n }\n return days.length ? { days, opens, closes } : null\n}\n\nconst hoursLines = (value: unknown): string[] =>\n typeof value === 'string' ? value.split(/\\r?\\n/) : []\n\n/**\n * Opening hours, one line per set of days, in the syntax `schema.org`'s own\n * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,\n * `Mo,We,Fr 08:00-12:00`.\n *\n * A text box rather than a repeating group of pickers because it is what the\n * settings forms can hold without a new control, and because it is the form\n * a business already writes its hours in. A line that does not read — a\n * mistyped day, `25:00` — is DROPPED rather than guessed at: a published\n * opening time is a promise a customer drives across town on.\n */\nexport function parseOpeningHours(value: unknown): OpeningHoursRow[] {\n const rows: OpeningHoursRow[] = []\n for (const line of hoursLines(value)) {\n if (!line.trim()) continue\n const row = parseOpeningHoursLine(line)\n if (row) rows.push(row)\n if (rows.length >= OPENING_HOURS_MAX_ROWS) break\n }\n return rows\n}\n\n/**\n * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so\n * the console can say which one before it is saved rather than after it\n * silently fails to publish. Blank lines are not problems.\n */\nexport function invalidOpeningHoursLines(value: unknown): number[] {\n const invalid: number[] = []\n hoursLines(value).forEach((line, index) => {\n if (line.trim() && !parseOpeningHoursLine(line)) invalid.push(index + 1)\n })\n return invalid\n}\n"],"names":["LOCAL_BUSINESS_TYPES","value","label","AREA_SERVED_MAX","AREA_SERVED_NAME_MAX_LENGTH","OPENING_HOURS_MAX_ROWS","PRICE_RANGE_MAX_LENGTH","PAYMENT_ACCEPTED_MAX_LENGTH","localBusinessType","undefined","wanted","trim","toLowerCase","find","entry","normalizeAreaServed","items","Array","isArray","split","seen","Set","areas","item","name","replace","slice","key","has","add","push","length","DAYS","dayIndex","token","findIndex","day","startsWith","clockTime","hours","minutes","h","Number","m","String","padStart","DASH","LINE","parseOpeningHoursLine","line","match","exec","opens","closes","days","part","ends","from","to","step","includes","hoursLines","parseOpeningHours","rows","row","invalidOpeningHoursLines","invalid","forEach","index"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/local-business.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A site entity that is a LOCAL BUSINESS (AGL-3383).\n *\n * `host.seo.entity.type` answers Organization or Person and keeps answering\n * it: it is a numeric enum stored as `\"1\"`/`\"2\"`, read by the author editor,\n * the logo card and the AI audit, and a third value there would be a third\n * value each of them has to learn. A plumber is still an Organization — the\n * business type REFINES that answer rather than replacing it — so it is a\n * field of its own, `businessType`, holding the `schema.org` subtype name.\n *\n * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the\n * console's Select is built from too. A stored value outside it (a typo, a\n * type removed from the list) publishes the plain Organization it would have\n * published before the field existed, rather than an `@type` no consumer\n * recognizes.\n */\n\n/** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */\nexport const LOCAL_BUSINESS_TYPES = [\n { value: 'LocalBusiness', label: 'Local business (general)' },\n { value: 'HomeAndConstructionBusiness', label: 'Home and construction' },\n { value: 'GeneralContractor', label: 'General contractor' },\n { value: 'HousePainter', label: 'House painter' },\n { value: 'Plumber', label: 'Plumber' },\n { value: 'Electrician', label: 'Electrician' },\n { value: 'HVACBusiness', label: 'Heating and air (HVAC)' },\n { value: 'Locksmith', label: 'Locksmith' },\n { value: 'RoofingContractor', label: 'Roofing contractor' },\n { value: 'MovingCompany', label: 'Moving company' },\n { value: 'ProfessionalService', label: 'Professional service' },\n { value: 'Store', label: 'Store' },\n { value: 'FoodEstablishment', label: 'Food and drink' },\n { value: 'Restaurant', label: 'Restaurant' },\n { value: 'HealthAndBeautyBusiness', label: 'Health and beauty' },\n { value: 'MedicalBusiness', label: 'Medical practice' },\n { value: 'Dentist', label: 'Dentist' },\n { value: 'AutomotiveBusiness', label: 'Automotive' },\n { value: 'LegalService', label: 'Legal service' },\n { value: 'AccountingService', label: 'Accounting' },\n { value: 'RealEstateAgent', label: 'Real estate agent' },\n { value: 'SportsActivityLocation', label: 'Gym, sports or fitness' },\n { value: 'ChildCare', label: 'Child care' },\n { value: 'AnimalShelter', label: 'Animal shelter' },\n] as const\n\nexport type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value']\n\n/**\n * {@link LOCAL_BUSINESS_TYPES} as a plain list, for readers that cannot read\n * a literal tuple: the native apps' contracts (AGL-3668).\n */\nexport const LOCAL_BUSINESS_TYPE_OPTIONS: ReadonlyArray<{ value: string; label: string }> =\n LOCAL_BUSINESS_TYPES\n\n/** At most this many areas are published; a service area is a list, not a gazetteer. */\nexport const AREA_SERVED_MAX = 20\nexport const AREA_SERVED_NAME_MAX_LENGTH = 120\n/** Seven days, twice over — room for split shifts without inviting a timetable. */\nexport const OPENING_HOURS_MAX_ROWS = 14\n/** Google reads a price range past 100 characters as not being one. */\nexport const PRICE_RANGE_MAX_LENGTH = 100\nexport const PAYMENT_ACCEPTED_MAX_LENGTH = 200\n\n/**\n * The stored business type, as the `schema.org` name it publishes under, or\n * `undefined` when there is none or it is not on the list.\n *\n * Case-insensitive, returning the canonical spelling, because `@type` is\n * case-sensitive to every consumer and `plumber` would name nothing.\n */\nexport function localBusinessType(value: unknown): LocalBusinessType | undefined {\n if (typeof value !== 'string') return undefined\n const wanted = value.trim().toLowerCase()\n if (!wanted) return undefined\n return LOCAL_BUSINESS_TYPES.find((entry) => entry.value.toLowerCase() === wanted)\n ?.value\n}\n\n/**\n * The areas a business serves: trimmed, blanks dropped, de-duplicated without\n * regard to case, and capped.\n *\n * Accepts the stored array or a newline-separated string, so a value typed\n * into the console's text box reads the same whichever shape reached the\n * document.\n */\nexport function normalizeAreaServed(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(/\\r?\\n/)\n : []\n const seen = new Set<string>()\n const areas: string[] = []\n for (const item of items) {\n if (typeof item !== 'string') continue\n const name = item.trim().replace(/\\s+/g, ' ').slice(0, AREA_SERVED_NAME_MAX_LENGTH)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n areas.push(name)\n if (areas.length >= AREA_SERVED_MAX) break\n }\n return areas\n}\n\nconst DAYS = [\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n 'Sunday',\n] as const\n\nexport type OpeningHoursDay = (typeof DAYS)[number]\n\n/** One line of opening hours: these days, from `opens` to `closes`. */\nexport interface OpeningHoursRow {\n days: OpeningHoursDay[]\n /** `HH:MM`, 24-hour. */\n opens: string\n /** `HH:MM`, 24-hour. Earlier than `opens` means the next morning. */\n closes: string\n}\n\n/**\n * A day token, by the shortest prefix that is unambiguous — `Mo`, `Mon` and\n * `Monday` all work, which is every way a person abbreviates a weekday. A\n * single letter is refused: `T` and `S` are each two days.\n */\nconst dayIndex = (token: string): number => {\n const wanted = token.trim().toLowerCase()\n if (wanted.length < 2) return -1\n return DAYS.findIndex((day) => day.toLowerCase().startsWith(wanted))\n}\n\n/** `9:00` → `09:00`; anything that is not a real time of day → `''`. */\nconst clockTime = (hours: string, minutes: string): string => {\n const h = Number(hours)\n const m = Number(minutes)\n if (h > 23 || m > 59) return ''\n return `${String(h).padStart(2, '0')}:${minutes}`\n}\n\nconst DASH = /\\s*[-–—]\\s*/\nconst LINE = /^(.+?)\\s+(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})$/\n\n/** One line, or `null` when any part of it does not read. */\nfunction parseOpeningHoursLine(line: string): OpeningHoursRow | null {\n const match = LINE.exec(line.trim())\n if (!match) return null\n const opens = clockTime(match[2], match[3])\n const closes = clockTime(match[4], match[5])\n if (!opens || !closes) return null\n const days: OpeningHoursDay[] = []\n for (const part of match[1].split(',')) {\n const ends = part.trim().split(DASH)\n if (ends.length > 2) return null\n const from = dayIndex(ends[0])\n const to = dayIndex(ends[ends.length - 1])\n if (from < 0 || to < 0) return null\n // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.\n for (let step = 0; step <= (to - from + 7) % 7; step++) {\n const day = DAYS[(from + step) % 7]\n if (!days.includes(day)) days.push(day)\n }\n }\n return days.length ? { days, opens, closes } : null\n}\n\nconst hoursLines = (value: unknown): string[] =>\n typeof value === 'string' ? value.split(/\\r?\\n/) : []\n\n/**\n * Opening hours, one line per set of days, in the syntax `schema.org`'s own\n * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,\n * `Mo,We,Fr 08:00-12:00`.\n *\n * A text box rather than a repeating group of pickers because it is what the\n * settings forms can hold without a new control, and because it is the form\n * a business already writes its hours in. A line that does not read — a\n * mistyped day, `25:00` — is DROPPED rather than guessed at: a published\n * opening time is a promise a customer drives across town on.\n */\nexport function parseOpeningHours(value: unknown): OpeningHoursRow[] {\n const rows: OpeningHoursRow[] = []\n for (const line of hoursLines(value)) {\n if (!line.trim()) continue\n const row = parseOpeningHoursLine(line)\n if (row) rows.push(row)\n if (rows.length >= OPENING_HOURS_MAX_ROWS) break\n }\n return rows\n}\n\n/**\n * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so\n * the console can say which one before it is saved rather than after it\n * silently fails to publish. Blank lines are not problems.\n */\nexport function invalidOpeningHoursLines(value: unknown): number[] {\n const invalid: number[] = []\n hoursLines(value).forEach((line, index) => {\n if (line.trim() && !parseOpeningHoursLine(line)) invalid.push(index + 1)\n })\n return invalid\n}\n"],"names":["LOCAL_BUSINESS_TYPES","value","label","LOCAL_BUSINESS_TYPE_OPTIONS","AREA_SERVED_MAX","AREA_SERVED_NAME_MAX_LENGTH","OPENING_HOURS_MAX_ROWS","PRICE_RANGE_MAX_LENGTH","PAYMENT_ACCEPTED_MAX_LENGTH","localBusinessType","undefined","wanted","trim","toLowerCase","find","entry","normalizeAreaServed","items","Array","isArray","split","seen","Set","areas","item","name","replace","slice","key","has","add","push","length","DAYS","dayIndex","token","findIndex","day","startsWith","clockTime","hours","minutes","h","Number","m","String","padStart","DASH","LINE","parseOpeningHoursLine","line","match","exec","opens","closes","days","part","ends","from","to","step","includes","hoursLines","parseOpeningHours","rows","row","invalidOpeningHoursLines","invalid","forEach","index"],"mappings":"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"}
@@ -979,7 +979,7 @@ export const LOCKDOWN_REFUSAL_FALLBACK_MESSAGE = 'This is temporarily paused whi
979
979
  * generic pause rather than a guess; being vague at a stranger is cheap,
980
980
  * being wrong about their money is not.
981
981
  */ export function lockdownPausedSurfaceForPluginApiPath(path) {
982
- if (path === 'commerce/checkout' || path === 'commerce/cart-checkout' || path === 'commerce/pos-order' || path === 'commerce/draft-order') {
982
+ if (path === 'commerce/checkout' || path === 'commerce/cart-checkout' || path === 'commerce/pos-order' || path === 'commerce/pos-payment' || path === 'commerce/draft-order') {
983
983
  return 'checkout';
984
984
  }
985
985
  if (path === 'commerce/cart' || path.startsWith('commerce/cart/')) return 'cart';