@withpotter/lib 0.13.0-next.124 → 0.13.0-next.59

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 (79) hide show
  1. package/dist/admin-account-types.d.ts +0 -21
  2. package/dist/admin-api.d.ts +2 -224
  3. package/dist/admin-api.js +0 -41
  4. package/dist/admin-api.js.map +1 -1
  5. package/dist/admin-case-api.js +4 -1
  6. package/dist/admin-case-api.js.map +1 -1
  7. package/dist/admin-pos-api.d.ts +1 -25
  8. package/dist/admin-pos-api.js +0 -24
  9. package/dist/admin-pos-api.js.map +1 -1
  10. package/dist/admin-pos-types.d.ts +0 -43
  11. package/dist/api/accounts.d.ts +3 -5
  12. package/dist/api/accounts.js +1 -1
  13. package/dist/api/accounts.js.map +1 -1
  14. package/dist/api/analytics.js +3 -1
  15. package/dist/api/analytics.js.map +1 -1
  16. package/dist/api/core.d.ts +1 -0
  17. package/dist/api/core.js +7 -19
  18. package/dist/api/core.js.map +1 -1
  19. package/dist/api/potter.js +3 -2
  20. package/dist/api/potter.js.map +1 -1
  21. package/dist/api/session-scope.d.ts +2 -2
  22. package/dist/api/session-scope.js.map +1 -1
  23. package/dist/api/types/auth.d.ts +1 -7
  24. package/dist/api/types/auth.js.map +1 -1
  25. package/dist/api/types/crm.d.ts +0 -2
  26. package/dist/api/types/templates.d.ts +0 -6
  27. package/dist/auth-context.d.ts +0 -2
  28. package/dist/auth-context.js +37 -12
  29. package/dist/auth-context.js.map +1 -1
  30. package/dist/mobile-routes.js +2 -4
  31. package/dist/mobile-routes.js.map +1 -1
  32. package/dist/plan-price.d.ts +2 -16
  33. package/dist/plan-price.js +0 -9
  34. package/dist/plan-price.js.map +1 -1
  35. package/dist/resolve-enabled-behaviors.d.ts +0 -2
  36. package/dist/resolve-enabled-behaviors.js +1 -2
  37. package/dist/resolve-enabled-behaviors.js.map +1 -1
  38. package/dist/template-types.d.ts +0 -8
  39. package/dist/timeline-copy.d.ts +1 -1
  40. package/dist/timeline-copy.js +2 -6
  41. package/dist/timeline-copy.js.map +1 -1
  42. package/dist/use-nav-groups.js +5 -5
  43. package/dist/use-nav-groups.js.map +1 -1
  44. package/dist/utils.js +4 -4
  45. package/dist/utils.js.map +1 -1
  46. package/dist/wallet-format.js +0 -1
  47. package/dist/wallet-format.js.map +1 -1
  48. package/package.json +2 -2
  49. package/dist/__fixtures__/demo-content-server.json +0 -484
  50. package/dist/admin-earnings-api.d.ts +0 -90
  51. package/dist/admin-earnings-api.js +0 -15
  52. package/dist/admin-earnings-api.js.map +0 -1
  53. package/dist/admin-earnings-model.d.ts +0 -30
  54. package/dist/admin-earnings-model.js +0 -47
  55. package/dist/admin-earnings-model.js.map +0 -1
  56. package/dist/admin-pos-olive.d.ts +0 -44
  57. package/dist/admin-pos-olive.js +0 -93
  58. package/dist/admin-pos-olive.js.map +0 -1
  59. package/dist/api/demo-content.d.ts +0 -104
  60. package/dist/api/demo-content.js +0 -36
  61. package/dist/api/demo-content.js.map +0 -1
  62. package/dist/api/plan-refusal.d.ts +0 -21
  63. package/dist/api/plan-refusal.js +0 -28
  64. package/dist/api/plan-refusal.js.map +0 -1
  65. package/dist/api/session-refresh.d.ts +0 -18
  66. package/dist/api/session-refresh.js +0 -37
  67. package/dist/api/session-refresh.js.map +0 -1
  68. package/dist/demo-content-mock.d.ts +0 -22
  69. package/dist/demo-content-mock.js +0 -70
  70. package/dist/demo-content-mock.js.map +0 -1
  71. package/dist/demo-content-poller.d.ts +0 -33
  72. package/dist/demo-content-poller.js +0 -80
  73. package/dist/demo-content-poller.js.map +0 -1
  74. package/dist/demo-content.d.ts +0 -43
  75. package/dist/demo-content.js +0 -78
  76. package/dist/demo-content.js.map +0 -1
  77. package/dist/stored-token.d.ts +0 -1
  78. package/dist/stored-token.js +0 -11
  79. package/dist/stored-token.js.map +0 -1
@@ -128,7 +128,6 @@ export {
128
128
  readBehaviorOverrides,
129
129
  resolveBaseBehaviors,
130
130
  resolveBehaviorsFromIntents,
131
- resolveEnabledBehaviors,
132
- resolveStandaloneBehaviors
131
+ resolveEnabledBehaviors
133
132
  };
134
133
  //# sourceMappingURL=resolve-enabled-behaviors.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resolve-enabled-behaviors.ts"],"sourcesContent":["import type { ContentBehavior, TemplateConfig } from './template-types';\n\n/**\n * Behaviors no business model implies, so `resolveBehaviorsFromIntents` can\n * never produce them and the intent-derived path would otherwise wipe them.\n *\n * Whether a business writes (`publishable`) or shows its work (`showcase`) has\n * nothing to do with whether it sells, books or rents. Both stay deliberate\n * opt-ins, read off the tenant's own `enabledBehaviors` and unioned onto\n * whatever the intents imply.\n */\nconst STANDALONE_BEHAVIORS: ContentBehavior[] = ['publishable', 'showcase'];\n\n/** The standalone behaviors this tenant has explicitly turned on. */\nexport function resolveStandaloneBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n): ContentBehavior[] {\n const stored = tenantSettings?.enabledBehaviors;\n if (!Array.isArray(stored)) return [];\n return STANDALONE_BEHAVIORS.filter((b) => stored.includes(b));\n}\n\n/**\n * What the merchant's *operating model* turns on, over and above the archetype.\n *\n * `OperatingModelProfile.behaviorKeys` has existed since the operating model\n * did, is persisted through `normalizeProfiles`, and was read by nothing. It is\n * the more honest place for several of these: \"we deliver\" is a fact about how\n * a business operates, not about what it sells — which is why a takeaway that\n * only does pickup currently has to switch `shippable` off by hand after the\n * archetype grants it.\n *\n * Additive on purpose. Intents stay authoritative for what an archetype\n * implies; a profile can only ever turn something else on, so no tenant loses a\n * behaviour by this being read at last.\n */\nfunction resolveProfileBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n): ContentBehavior[] {\n const operatingModel = tenantSettings?.operatingModel as\n | { profiles?: Array<{ enabled?: boolean; behaviorKeys?: string[] }> }\n | undefined;\n\n if (!Array.isArray(operatingModel?.profiles)) return [];\n\n return operatingModel.profiles\n .filter((profile) => profile.enabled !== false)\n .flatMap((profile) => profile.behaviorKeys ?? []) as ContentBehavior[];\n}\n\nexport function resolveBehaviorsFromIntents(\n intents: string[],\n): ContentBehavior[] {\n const behaviors = new Set<ContentBehavior>();\n\n for (const intent of intents) {\n switch (intent) {\n case 'sell-products':\n behaviors.add('sellable');\n behaviors.add('shippable');\n break;\n case 'take-bookings':\n behaviors.add('bookable');\n break;\n case 'rent-property':\n behaviors.add('bookable');\n behaviors.add('rentable');\n break;\n case 'food-orders':\n behaviors.add('sellable');\n // The archetype for food that LEAVES the counter — its operating\n // profiles are dine-in, walk-in, QR, pickup and delivery, and the\n // delivery one already says `hideShipping: false`. The storefront\n // gates its whole delivery-address step on `shippable`, and Shipping\n // and Service areas are gated on it here, so without it a takeaway\n // could not say where it delivers, what delivery costs, or collect an\n // address on its own site. A kitchen that never leaves the building\n // switches it off in Settings → Features.\n //\n // Mirrored by INTENT_BEHAVIORS in potter-api; the two maps are\n // separate and must move together or provisioning writes one thing\n // while the dashboard resolves another.\n behaviors.add('shippable');\n break;\n case 'dine-in':\n // Reserve & Seat: sellable menu + bookable tables (as resources).\n behaviors.add('sellable');\n behaviors.add('bookable');\n break;\n case 'subscriptions':\n behaviors.add('subscribable');\n break;\n case 'digital-products':\n behaviors.add('sellable');\n behaviors.add('downloadable');\n break;\n case 'event-tickets':\n behaviors.add('sellable');\n behaviors.add('ticketed');\n break;\n case 'send-invoices':\n // Deliver & Quote: billed directly, never checked out of a storefront.\n behaviors.add('billable');\n break;\n case 'capture-leads':\n // Enquire & Qualify: nothing is bought. The thing that arrives is a\n // person asking, and the storefront's job is to let them ask well.\n behaviors.add('leadCapture');\n break;\n }\n }\n\n return [...behaviors];\n}\n\n/**\n * What a single model grants on its own — used by Settings → Features to show,\n * on hover, which capabilities a model is responsible for, and to preview what\n * adding an unselected one would turn on.\n */\nexport function behaviorsForIntent(intentKey: string): ContentBehavior[] {\n return resolveBehaviorsFromIntents([intentKey]);\n}\n\n/**\n * Per-behavior adjustments layered on top of the business model.\n *\n * A key is present ONLY when the merchant has deliberately overruled what their\n * business model implies — `{ shippable: false }` for a shop that sells over a\n * counter and never posts anything, `{ downloadable: true }` for a bookings\n * business that also hands out a workbook. Absent key means \"follow the model\",\n * which is why this is a sparse map and not a full behavior list: a stored list\n * silently freezes, so adding a business model later would grant no new\n * behaviors. The map only ever records the deltas.\n */\nexport type BehaviorOverrides = Partial<Record<ContentBehavior, boolean>>;\n\nexport function readBehaviorOverrides(\n tenantSettings: Record<string, unknown> | undefined,\n): BehaviorOverrides {\n const raw = tenantSettings?.behaviorOverrides;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};\n const out: BehaviorOverrides = {};\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value === 'boolean') out[key as ContentBehavior] = value;\n }\n return out;\n}\n\n/**\n * Applies the merchant's overrides to a base behavior set.\n *\n * The result can never be empty. A business with zero behaviors can't transact\n * at all — every behavior-gated nav item and storefront action disappears — so\n * the last remaining behavior is a floor, and an override that would clear the\n * set is ignored rather than honoured.\n */\nexport function applyBehaviorOverrides(\n base: ContentBehavior[],\n overrides: BehaviorOverrides,\n): ContentBehavior[] {\n const next = new Set(base);\n for (const [key, enabled] of Object.entries(overrides)) {\n if (enabled) next.add(key as ContentBehavior);\n else next.delete(key as ContentBehavior);\n }\n if (next.size === 0) return base.length > 0 ? base : ['sellable'];\n return [...next];\n}\n\n/**\n * Resolves which behaviors are enabled for a tenant.\n *\n * The business model (`selectedIntents`) is the control; it derives a base set.\n * `behaviorOverrides` then layers the merchant's deliberate adjustments on top.\n * Everything below that is fallback for tenants that predate intents.\n *\n * Priority:\n * 1. Intent-derived base + `behaviorOverrides`\n * 2. Legacy `enabledBehaviors` (tenants with no intents) + overrides\n * 3. Template-level `behaviors` + overrides\n * 4. Union of all content type behaviors + overrides\n * 5. `['sellable']` + overrides\n */\nexport function resolveEnabledBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n template: TemplateConfig | null,\n): ContentBehavior[] {\n const overrides = readBehaviorOverrides(tenantSettings);\n const withOverrides = (base: ContentBehavior[]) =>\n applyBehaviorOverrides(base, overrides);\n\n // 1. Intent selection is authoritative when the merchant has actually picked\n // something. An EMPTY array is treated as unset, not as \"this business\n // does nothing\": resolveTenantIntentConfigs already falls back to\n // default-commerce for the empty case, so honouring empty here produced a\n // split state — the commerce nav still rendered while zero behaviors\n // resolved, silently stripping every behavior-gated item and leaving the\n // storefront with no way to transact. A merchant can reach that state by\n // deselecting their last business model in Settings → Features.\n const selectedIntents = tenantSettings?.selectedIntents;\n if (Array.isArray(selectedIntents) && selectedIntents.length > 0) {\n // Overrides stay authoritative: they are applied last, so a switch turned\n // off in Business model still wins over anything the union puts in.\n return withOverrides([\n ...resolveBehaviorsFromIntents(selectedIntents),\n ...resolveProfileBehaviors(tenantSettings),\n ...resolveStandaloneBehaviors(tenantSettings),\n ]);\n }\n\n // 2. Legacy tenants that set behaviors directly, before intents existed.\n const tenantBehaviors = tenantSettings?.enabledBehaviors;\n if (Array.isArray(tenantBehaviors) && tenantBehaviors.length > 0) {\n return withOverrides(tenantBehaviors as ContentBehavior[]);\n }\n\n // 3. Template declares behaviors — use as defaults\n if (template?.behaviors && template.behaviors.length > 0) {\n return withOverrides(template.behaviors);\n }\n\n // 4. Collect from template content types\n if (template?.contentTypes) {\n const behaviors = new Set<ContentBehavior>();\n for (const ct of Object.values(template.contentTypes)) {\n ct.behaviors?.forEach((b) => behaviors.add(b));\n }\n if (behaviors.size > 0) return withOverrides(Array.from(behaviors));\n }\n\n // 5. Absolute fallback\n return withOverrides(['sellable']);\n}\n\n/**\n * The behaviors the business model implies, BEFORE the merchant's overrides.\n *\n * Settings → Features needs both halves to tell the truth: this is what your\n * model turns on, and this is where you've overruled it. Nothing else should\n * need the pre-override set — read {@link resolveEnabledBehaviors} instead.\n */\nexport function resolveBaseBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n template: TemplateConfig | null,\n): ContentBehavior[] {\n const selectedIntents = tenantSettings?.selectedIntents;\n if (Array.isArray(selectedIntents) && selectedIntents.length > 0) {\n return resolveBehaviorsFromIntents(selectedIntents);\n }\n const tenantBehaviors = tenantSettings?.enabledBehaviors;\n if (Array.isArray(tenantBehaviors) && tenantBehaviors.length > 0) {\n return tenantBehaviors as ContentBehavior[];\n }\n if (template?.behaviors && template.behaviors.length > 0) {\n return template.behaviors;\n }\n if (template?.contentTypes) {\n const behaviors = new Set<ContentBehavior>();\n for (const ct of Object.values(template.contentTypes)) {\n ct.behaviors?.forEach((b) => behaviors.add(b));\n }\n if (behaviors.size > 0) return Array.from(behaviors);\n }\n return ['sellable'];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/resolve-enabled-behaviors.ts"],"sourcesContent":["import type { ContentBehavior, TemplateConfig } from './template-types';\n\n/**\n * Behaviors no business model implies, so `resolveBehaviorsFromIntents` can\n * never produce them and the intent-derived path would otherwise wipe them.\n *\n * Whether a business writes (`publishable`) or shows its work (`showcase`) has\n * nothing to do with whether it sells, books or rents. Both stay deliberate\n * opt-ins, read off the tenant's own `enabledBehaviors` and unioned onto\n * whatever the intents imply.\n */\nconst STANDALONE_BEHAVIORS: ContentBehavior[] = ['publishable', 'showcase'];\n\n/** The standalone behaviors this tenant has explicitly turned on. */\nfunction resolveStandaloneBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n): ContentBehavior[] {\n const stored = tenantSettings?.enabledBehaviors;\n if (!Array.isArray(stored)) return [];\n return STANDALONE_BEHAVIORS.filter((b) => stored.includes(b));\n}\n\n/**\n * What the merchant's *operating model* turns on, over and above the archetype.\n *\n * `OperatingModelProfile.behaviorKeys` has existed since the operating model\n * did, is persisted through `normalizeProfiles`, and was read by nothing. It is\n * the more honest place for several of these: \"we deliver\" is a fact about how\n * a business operates, not about what it sells — which is why a takeaway that\n * only does pickup currently has to switch `shippable` off by hand after the\n * archetype grants it.\n *\n * Additive on purpose. Intents stay authoritative for what an archetype\n * implies; a profile can only ever turn something else on, so no tenant loses a\n * behaviour by this being read at last.\n */\nfunction resolveProfileBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n): ContentBehavior[] {\n const operatingModel = tenantSettings?.operatingModel as\n | { profiles?: Array<{ enabled?: boolean; behaviorKeys?: string[] }> }\n | undefined;\n\n if (!Array.isArray(operatingModel?.profiles)) return [];\n\n return operatingModel.profiles\n .filter((profile) => profile.enabled !== false)\n .flatMap((profile) => profile.behaviorKeys ?? []) as ContentBehavior[];\n}\n\nexport function resolveBehaviorsFromIntents(\n intents: string[],\n): ContentBehavior[] {\n const behaviors = new Set<ContentBehavior>();\n\n for (const intent of intents) {\n switch (intent) {\n case 'sell-products':\n behaviors.add('sellable');\n behaviors.add('shippable');\n break;\n case 'take-bookings':\n behaviors.add('bookable');\n break;\n case 'rent-property':\n behaviors.add('bookable');\n behaviors.add('rentable');\n break;\n case 'food-orders':\n behaviors.add('sellable');\n // The archetype for food that LEAVES the counter — its operating\n // profiles are dine-in, walk-in, QR, pickup and delivery, and the\n // delivery one already says `hideShipping: false`. The storefront\n // gates its whole delivery-address step on `shippable`, and Shipping\n // and Service areas are gated on it here, so without it a takeaway\n // could not say where it delivers, what delivery costs, or collect an\n // address on its own site. A kitchen that never leaves the building\n // switches it off in Settings → Features.\n //\n // Mirrored by INTENT_BEHAVIORS in potter-api; the two maps are\n // separate and must move together or provisioning writes one thing\n // while the dashboard resolves another.\n behaviors.add('shippable');\n break;\n case 'dine-in':\n // Reserve & Seat: sellable menu + bookable tables (as resources).\n behaviors.add('sellable');\n behaviors.add('bookable');\n break;\n case 'subscriptions':\n behaviors.add('subscribable');\n break;\n case 'digital-products':\n behaviors.add('sellable');\n behaviors.add('downloadable');\n break;\n case 'event-tickets':\n behaviors.add('sellable');\n behaviors.add('ticketed');\n break;\n case 'send-invoices':\n // Deliver & Quote: billed directly, never checked out of a storefront.\n behaviors.add('billable');\n break;\n case 'capture-leads':\n // Enquire & Qualify: nothing is bought. The thing that arrives is a\n // person asking, and the storefront's job is to let them ask well.\n behaviors.add('leadCapture');\n break;\n }\n }\n\n return [...behaviors];\n}\n\n/**\n * What a single model grants on its own — used by Settings → Features to show,\n * on hover, which capabilities a model is responsible for, and to preview what\n * adding an unselected one would turn on.\n */\nexport function behaviorsForIntent(intentKey: string): ContentBehavior[] {\n return resolveBehaviorsFromIntents([intentKey]);\n}\n\n/**\n * Per-behavior adjustments layered on top of the business model.\n *\n * A key is present ONLY when the merchant has deliberately overruled what their\n * business model implies — `{ shippable: false }` for a shop that sells over a\n * counter and never posts anything, `{ downloadable: true }` for a bookings\n * business that also hands out a workbook. Absent key means \"follow the model\",\n * which is why this is a sparse map and not a full behavior list: a stored list\n * silently freezes, so adding a business model later would grant no new\n * behaviors. The map only ever records the deltas.\n */\nexport type BehaviorOverrides = Partial<Record<ContentBehavior, boolean>>;\n\nexport function readBehaviorOverrides(\n tenantSettings: Record<string, unknown> | undefined,\n): BehaviorOverrides {\n const raw = tenantSettings?.behaviorOverrides;\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};\n const out: BehaviorOverrides = {};\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value === 'boolean') out[key as ContentBehavior] = value;\n }\n return out;\n}\n\n/**\n * Applies the merchant's overrides to a base behavior set.\n *\n * The result can never be empty. A business with zero behaviors can't transact\n * at all — every behavior-gated nav item and storefront action disappears — so\n * the last remaining behavior is a floor, and an override that would clear the\n * set is ignored rather than honoured.\n */\nexport function applyBehaviorOverrides(\n base: ContentBehavior[],\n overrides: BehaviorOverrides,\n): ContentBehavior[] {\n const next = new Set(base);\n for (const [key, enabled] of Object.entries(overrides)) {\n if (enabled) next.add(key as ContentBehavior);\n else next.delete(key as ContentBehavior);\n }\n if (next.size === 0) return base.length > 0 ? base : ['sellable'];\n return [...next];\n}\n\n/**\n * Resolves which behaviors are enabled for a tenant.\n *\n * The business model (`selectedIntents`) is the control; it derives a base set.\n * `behaviorOverrides` then layers the merchant's deliberate adjustments on top.\n * Everything below that is fallback for tenants that predate intents.\n *\n * Priority:\n * 1. Intent-derived base + `behaviorOverrides`\n * 2. Legacy `enabledBehaviors` (tenants with no intents) + overrides\n * 3. Template-level `behaviors` + overrides\n * 4. Union of all content type behaviors + overrides\n * 5. `['sellable']` + overrides\n */\nexport function resolveEnabledBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n template: TemplateConfig | null,\n): ContentBehavior[] {\n const overrides = readBehaviorOverrides(tenantSettings);\n const withOverrides = (base: ContentBehavior[]) =>\n applyBehaviorOverrides(base, overrides);\n\n // 1. Intent selection is authoritative when the merchant has actually picked\n // something. An EMPTY array is treated as unset, not as \"this business\n // does nothing\": resolveTenantIntentConfigs already falls back to\n // default-commerce for the empty case, so honouring empty here produced a\n // split state — the commerce nav still rendered while zero behaviors\n // resolved, silently stripping every behavior-gated item and leaving the\n // storefront with no way to transact. A merchant can reach that state by\n // deselecting their last business model in Settings → Features.\n const selectedIntents = tenantSettings?.selectedIntents;\n if (Array.isArray(selectedIntents) && selectedIntents.length > 0) {\n // Overrides stay authoritative: they are applied last, so a switch turned\n // off in Business model still wins over anything the union puts in.\n return withOverrides([\n ...resolveBehaviorsFromIntents(selectedIntents),\n ...resolveProfileBehaviors(tenantSettings),\n ...resolveStandaloneBehaviors(tenantSettings),\n ]);\n }\n\n // 2. Legacy tenants that set behaviors directly, before intents existed.\n const tenantBehaviors = tenantSettings?.enabledBehaviors;\n if (Array.isArray(tenantBehaviors) && tenantBehaviors.length > 0) {\n return withOverrides(tenantBehaviors as ContentBehavior[]);\n }\n\n // 3. Template declares behaviors — use as defaults\n if (template?.behaviors && template.behaviors.length > 0) {\n return withOverrides(template.behaviors);\n }\n\n // 4. Collect from template content types\n if (template?.contentTypes) {\n const behaviors = new Set<ContentBehavior>();\n for (const ct of Object.values(template.contentTypes)) {\n ct.behaviors?.forEach((b) => behaviors.add(b));\n }\n if (behaviors.size > 0) return withOverrides(Array.from(behaviors));\n }\n\n // 5. Absolute fallback\n return withOverrides(['sellable']);\n}\n\n/**\n * The behaviors the business model implies, BEFORE the merchant's overrides.\n *\n * Settings → Features needs both halves to tell the truth: this is what your\n * model turns on, and this is where you've overruled it. Nothing else should\n * need the pre-override set — read {@link resolveEnabledBehaviors} instead.\n */\nexport function resolveBaseBehaviors(\n tenantSettings: Record<string, unknown> | undefined,\n template: TemplateConfig | null,\n): ContentBehavior[] {\n const selectedIntents = tenantSettings?.selectedIntents;\n if (Array.isArray(selectedIntents) && selectedIntents.length > 0) {\n return resolveBehaviorsFromIntents(selectedIntents);\n }\n const tenantBehaviors = tenantSettings?.enabledBehaviors;\n if (Array.isArray(tenantBehaviors) && tenantBehaviors.length > 0) {\n return tenantBehaviors as ContentBehavior[];\n }\n if (template?.behaviors && template.behaviors.length > 0) {\n return template.behaviors;\n }\n if (template?.contentTypes) {\n const behaviors = new Set<ContentBehavior>();\n for (const ct of Object.values(template.contentTypes)) {\n ct.behaviors?.forEach((b) => behaviors.add(b));\n }\n if (behaviors.size > 0) return Array.from(behaviors);\n }\n return ['sellable'];\n}\n"],"mappings":"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","names":[]}
@@ -290,14 +290,6 @@ export interface TemplateConfigAPI {
290
290
  siteSettings?: SiteSettingsSchema;
291
291
  pageContent?: PageContentSchemas;
292
292
  appearance?: AppearanceSchema;
293
- /**
294
- * `false` when the template keeps its own type and ignores the owner's font
295
- * choices (top-level `customFonts` in template.json). Absent means the
296
- * owner can choose fonts.
297
- */
298
- customFonts?: boolean;
299
- /** Why the template keeps its own type, from `customFontsReason`. */
300
- customFontsReason?: string;
301
293
  contentTypes?: Record<string, ContentTypeConfigAPI>;
302
294
  dashboardModules?: DashboardModuleAPI[];
303
295
  behaviors?: ContentBehavior[];
@@ -17,7 +17,7 @@ export declare function productUpdated(itemTitle: string, contentType: string):
17
17
  description: string;
18
18
  emoji: string;
19
19
  };
20
- export declare function orderPlaced(customerName: string, orderNumber: string, formattedTotal: string, industryKey?: string | null, isPaid?: boolean): {
20
+ export declare function orderPlaced(customerName: string, orderNumber: string, formattedTotal: string, industryKey?: string | null): {
21
21
  title: string;
22
22
  description: string;
23
23
  emoji: string;
@@ -17,12 +17,8 @@ const ORDER_VERB = {
17
17
  hospitality: "booked",
18
18
  events: "purchased tickets for"
19
19
  };
20
- const UNPAID_ORDER_VERB = {
21
- events: "started a ticket order for"
22
- };
23
- function orderPlaced(customerName, orderNumber, formattedTotal, industryKey, isPaid = true) {
24
- const verbs = isPaid ? ORDER_VERB : { ...ORDER_VERB, ...UNPAID_ORDER_VERB };
25
- const verb = industryKey && verbs[industryKey] || "placed an order";
20
+ function orderPlaced(customerName, orderNumber, formattedTotal, industryKey) {
21
+ const verb = industryKey && ORDER_VERB[industryKey] || "placed an order";
26
22
  return {
27
23
  title: `${customerName} ${verb}`,
28
24
  description: `${orderNumber} \u2022 ${formattedTotal}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/timeline-copy.ts"],"sourcesContent":["/**\n * Timeline copy/messaging for the dashboard activity feed.\n *\n * All user-facing strings for the \"What's happening\" timeline live here\n * so we can iterate on tone, A/B test, or add i18n without touching\n * component logic.\n *\n * Each generator receives contextual data and returns { title, description }.\n */\n\n// ─── Product / Content ──────────────────────────────────────────────\n\nexport function productAdded(itemTitle: string, contentType: string) {\n return {\n title: `${itemTitle} is now live`,\n description: `New ${contentType.toLowerCase()} added to your store`,\n emoji: '📦',\n };\n}\n\nexport function productUpdated(itemTitle: string, contentType: string) {\n return {\n title: `${itemTitle} was updated`,\n description: `You updated your ${contentType.toLowerCase()}`,\n emoji: '🔄',\n };\n}\n\n// ─── Orders ─────────────────────────────────────────────────────────\n\nconst ORDER_VERB: Record<string, string> = {\n food: 'ordered',\n hospitality: 'booked',\n events: 'purchased tickets for',\n};\n\n// A ticket is only bought once it is paid. An unpaid ticket order is a checkout\n// that was started, and calling it a purchase puts money on the feed that is\n// not in the wallet.\nconst UNPAID_ORDER_VERB: Record<string, string> = {\n events: 'started a ticket order for',\n};\n\nexport function orderPlaced(\n customerName: string,\n orderNumber: string,\n formattedTotal: string,\n industryKey?: string | null,\n isPaid = true,\n) {\n const verbs = isPaid ? ORDER_VERB : { ...ORDER_VERB, ...UNPAID_ORDER_VERB };\n const verb = (industryKey && verbs[industryKey]) || 'placed an order';\n return {\n title: `${customerName} ${verb}`,\n description: `${orderNumber} • ${formattedTotal}`,\n emoji: '🛍️',\n };\n}\n\nexport function orderCompleted(customerName: string, orderNumber: string) {\n return {\n title: `${orderNumber} is fulfilled`,\n description: `${customerName}'s order is complete`,\n emoji: '✅',\n };\n}\n\n// ─── Customers / Members ────────────────────────────────────────────\n\nconst BOOKABLE_INDUSTRIES = new Set([\n 'beauty',\n 'wellness',\n 'professional',\n 'fitness',\n]);\n\nexport function memberSignup(\n name: string,\n email: string,\n industryKey?: string | null,\n) {\n const verb =\n industryKey && BOOKABLE_INDUSTRIES.has(industryKey)\n ? 'became a client'\n : 'joined your store';\n return {\n title: `${name} ${verb}`,\n description: email,\n emoji: '👤',\n };\n}\n\nexport function memberConverted(name: string, email: string) {\n return {\n title: `${name} just became a customer`,\n description: `Converted from guest · ${email}`,\n emoji: '🎉',\n };\n}\n\n// ─── Subscriptions ──────────────────────────────────────────────────\n\nexport function subscriptionCreated(\n name: string,\n planName: string,\n formattedAmount: string,\n) {\n return {\n title: `${name} subscribed to ${planName}`,\n description: `Recurring revenue: ${formattedAmount}/month`,\n emoji: '💳',\n };\n}\n\n// ─── Invoices ───────────────────────────────────────────────────────\n\nexport function invoicePaid(\n customerName: string,\n invoiceNumber: string,\n formattedAmount: string,\n) {\n return {\n title: `${customerName} paid invoice ${invoiceNumber}`,\n description: `Payment received: ${formattedAmount}`,\n emoji: '💰',\n };\n}\n\nexport function invoiceSent(\n customerName: string,\n invoiceNumber: string,\n formattedAmount: string,\n) {\n return {\n title: `Invoice ${invoiceNumber} sent to ${customerName}`,\n description: `Amount due: ${formattedAmount}`,\n emoji: '📨',\n };\n}\n\n// ─── Empty state ────────────────────────────────────────────────────\n\nconst INDUSTRY_EMPTY_TIMELINE: Record<\n string,\n { heading: string; subtext: string }\n> = {\n retail: {\n heading: 'Nothing here yet',\n subtext: 'Orders, signups, and updates will show up here as they happen.',\n },\n beauty: {\n heading: 'Nothing here yet',\n subtext: 'Appointments, bookings, and client activity will show up here.',\n },\n wellness: {\n heading: 'Nothing here yet',\n subtext: 'Appointments, bookings, and client activity will show up here.',\n },\n hospitality: {\n heading: 'Nothing here yet',\n subtext: 'Reservations, guest check-ins, and updates will show up here.',\n },\n food: {\n heading: 'Nothing here yet',\n subtext: 'Orders and customer activity will show up here as they happen.',\n },\n fitness: {\n heading: 'Nothing here yet',\n subtext: 'Class sign-ups, member activity, and updates will show up here.',\n },\n professional: {\n heading: 'Nothing here yet',\n subtext: 'Appointments, bookings, and client activity will show up here.',\n },\n creative: {\n heading: 'Nothing here yet',\n subtext: 'Bookings, orders, and client activity will show up here.',\n },\n events: {\n heading: 'Nothing here yet',\n subtext:\n 'Ticket sales, attendee activity, and event updates will show up here.',\n },\n};\n\nexport const emptyTimeline = {\n heading: 'Nothing here yet',\n subtext: 'Orders, signups, and updates will show up here as they happen.',\n};\n\nexport function getEmptyTimeline(industryKey: string | null) {\n if (industryKey && INDUSTRY_EMPTY_TIMELINE[industryKey]) {\n return INDUSTRY_EMPTY_TIMELINE[industryKey];\n }\n return emptyTimeline;\n}\n\n// ─── Feed chrome ────────────────────────────────────────────────────\n\n// Curly apostrophe, matching every other apostrophe in the product's copy.\nexport const feedHeader = 'What’s happening';\n\nexport function showMoreLabel(remaining: number) {\n return `Show ${remaining} more`;\n}\n\nexport const showLessLabel = 'Show less';\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/timeline-copy.ts"],"sourcesContent":["/**\n * Timeline copy/messaging for the dashboard activity feed.\n *\n * All user-facing strings for the \"What's happening\" timeline live here\n * so we can iterate on tone, A/B test, or add i18n without touching\n * component logic.\n *\n * Each generator receives contextual data and returns { title, description }.\n */\n\n// ─── Product / Content ──────────────────────────────────────────────\n\nexport function productAdded(itemTitle: string, contentType: string) {\n return {\n title: `${itemTitle} is now live`,\n description: `New ${contentType.toLowerCase()} added to your store`,\n emoji: '📦',\n };\n}\n\nexport function productUpdated(itemTitle: string, contentType: string) {\n return {\n title: `${itemTitle} was updated`,\n description: `You updated your ${contentType.toLowerCase()}`,\n emoji: '🔄',\n };\n}\n\n// ─── Orders ─────────────────────────────────────────────────────────\n\nconst ORDER_VERB: Record<string, string> = {\n food: 'ordered',\n hospitality: 'booked',\n events: 'purchased tickets for',\n};\n\nexport function orderPlaced(\n customerName: string,\n orderNumber: string,\n formattedTotal: string,\n industryKey?: string | null,\n) {\n const verb = (industryKey && ORDER_VERB[industryKey]) || 'placed an order';\n return {\n title: `${customerName} ${verb}`,\n description: `${orderNumber} • ${formattedTotal}`,\n emoji: '🛍️',\n };\n}\n\nexport function orderCompleted(customerName: string, orderNumber: string) {\n return {\n title: `${orderNumber} is fulfilled`,\n description: `${customerName}'s order is complete`,\n emoji: '✅',\n };\n}\n\n// ─── Customers / Members ────────────────────────────────────────────\n\nconst BOOKABLE_INDUSTRIES = new Set([\n 'beauty',\n 'wellness',\n 'professional',\n 'fitness',\n]);\n\nexport function memberSignup(\n name: string,\n email: string,\n industryKey?: string | null,\n) {\n const verb =\n industryKey && BOOKABLE_INDUSTRIES.has(industryKey)\n ? 'became a client'\n : 'joined your store';\n return {\n title: `${name} ${verb}`,\n description: email,\n emoji: '👤',\n };\n}\n\nexport function memberConverted(name: string, email: string) {\n return {\n title: `${name} just became a customer`,\n description: `Converted from guest · ${email}`,\n emoji: '🎉',\n };\n}\n\n// ─── Subscriptions ──────────────────────────────────────────────────\n\nexport function subscriptionCreated(\n name: string,\n planName: string,\n formattedAmount: string,\n) {\n return {\n title: `${name} subscribed to ${planName}`,\n description: `Recurring revenue: ${formattedAmount}/month`,\n emoji: '💳',\n };\n}\n\n// ─── Invoices ───────────────────────────────────────────────────────\n\nexport function invoicePaid(\n customerName: string,\n invoiceNumber: string,\n formattedAmount: string,\n) {\n return {\n title: `${customerName} paid invoice ${invoiceNumber}`,\n description: `Payment received: ${formattedAmount}`,\n emoji: '💰',\n };\n}\n\nexport function invoiceSent(\n customerName: string,\n invoiceNumber: string,\n formattedAmount: string,\n) {\n return {\n title: `Invoice ${invoiceNumber} sent to ${customerName}`,\n description: `Amount due: ${formattedAmount}`,\n emoji: '📨',\n };\n}\n\n// ─── Empty state ────────────────────────────────────────────────────\n\nconst INDUSTRY_EMPTY_TIMELINE: Record<\n string,\n { heading: string; subtext: string }\n> = {\n retail: {\n heading: 'Nothing here yet',\n subtext: 'Orders, signups, and updates will show up here as they happen.',\n },\n beauty: {\n heading: 'Nothing here yet',\n subtext: 'Appointments, bookings, and client activity will show up here.',\n },\n wellness: {\n heading: 'Nothing here yet',\n subtext: 'Appointments, bookings, and client activity will show up here.',\n },\n hospitality: {\n heading: 'Nothing here yet',\n subtext: 'Reservations, guest check-ins, and updates will show up here.',\n },\n food: {\n heading: 'Nothing here yet',\n subtext: 'Orders and customer activity will show up here as they happen.',\n },\n fitness: {\n heading: 'Nothing here yet',\n subtext: 'Class sign-ups, member activity, and updates will show up here.',\n },\n professional: {\n heading: 'Nothing here yet',\n subtext: 'Appointments, bookings, and client activity will show up here.',\n },\n creative: {\n heading: 'Nothing here yet',\n subtext: 'Bookings, orders, and client activity will show up here.',\n },\n events: {\n heading: 'Nothing here yet',\n subtext:\n 'Ticket sales, attendee activity, and event updates will show up here.',\n },\n};\n\nexport const emptyTimeline = {\n heading: 'Nothing here yet',\n subtext: 'Orders, signups, and updates will show up here as they happen.',\n};\n\nexport function getEmptyTimeline(industryKey: string | null) {\n if (industryKey && INDUSTRY_EMPTY_TIMELINE[industryKey]) {\n return INDUSTRY_EMPTY_TIMELINE[industryKey];\n }\n return emptyTimeline;\n}\n\n// ─── Feed chrome ────────────────────────────────────────────────────\n\n// Curly apostrophe, matching every other apostrophe in the product's copy.\nexport const feedHeader = 'What’s happening';\n\nexport function showMoreLabel(remaining: number) {\n return `Show ${remaining} more`;\n}\n\nexport const showLessLabel = 'Show less';\n"],"mappings":"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","names":[]}
@@ -22,18 +22,18 @@ function useNavDeps() {
22
22
  const moduleLabels = effectiveModuleLabels(settings);
23
23
  const hiddenModules = settings?.hiddenModules;
24
24
  const salesChannel = isSalesChannel(settings?.salesChannel) ? settings.salesChannel : null;
25
- const enabledBehaviors = useMemo(
26
- () => resolveEnabledBehaviors(settings, template),
27
- [settings, template]
28
- );
29
25
  const { coreNavigation } = usePlatformModules({
30
26
  template,
31
27
  dashboardState,
32
- enabledBehaviors,
28
+ enabledBehaviors: settings?.enabledBehaviors,
33
29
  hiddenModules,
34
30
  moduleLabels,
35
31
  moduleOrder: settings?.moduleOrder
36
32
  });
33
+ const enabledBehaviors = useMemo(
34
+ () => resolveEnabledBehaviors(settings, template),
35
+ [settings, template]
36
+ );
37
37
  const intentConfig = useIntentConfig();
38
38
  return useMemo(
39
39
  () => ({
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/use-nav-groups.ts"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { effectiveModuleLabels } from './effective-module-labels';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { usePlatformModules } from '@withpotter/lib/use-platform-modules';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { resolveEnabledBehaviors } from '@withpotter/lib/resolve-enabled-behaviors';\nimport { isSalesChannel } from '@withpotter/hooks/use-sales-channel';\nimport { buildNavGroups } from '@withpotter/components/sidebar/build-nav-groups';\nimport {\n ACCOUNT_GROUPS,\n BUSINESS_GROUPS,\n type NavGroup,\n type NavItem,\n} from '@withpotter/components/sidebar/config';\nimport type { RouteRule } from '@withpotter/lib/route-permission';\nimport { businessItems, customizationItems } from '@withpotter/lib/navigation-groups';\n\n/**\n * The sidebar's own groups, for callers that need to know what the nav shows\n * without being the sidebar.\n *\n * The deps this builder wants are a fixed dozen, and assembling them was\n * already copied out once into the editor's jump targets. This is that\n * assembly, named — permissions, hidden modules, archetype collapsing and the\n * merchant's own module labels all arrive already applied, because they are\n * applied inside `buildNavGroups`.\n *\n * Calls `useIntentConfig`, which calls `useSearchParams`. A caller in a layout\n * needs a Suspense boundary above it or the whole route bails out of rendering.\n */\nfunction useNavDeps() {\n const { tenant } = useAuth();\n const { template, dashboardState } = useCurrentTemplate(tenant?.templateId);\n\n const settings = tenant?.settings as Record<string, unknown> | undefined;\n const moduleLabels = effectiveModuleLabels(settings);\n const hiddenModules = settings?.hiddenModules as string[] | undefined;\n const salesChannel = isSalesChannel(settings?.salesChannel)\n ? settings.salesChannel\n : null;\n\n // Resolved first: the core list gates its modules on these, and the raw\n // stored field knows nothing of the merchant's own switches.\n const enabledBehaviors = useMemo(\n () => resolveEnabledBehaviors(settings, template),\n [settings, template],\n );\n\n const { coreNavigation } = usePlatformModules({\n template,\n dashboardState,\n enabledBehaviors,\n hiddenModules,\n moduleLabels,\n moduleOrder: settings?.moduleOrder as string[] | undefined,\n });\n\n const intentConfig = useIntentConfig();\n\n return useMemo(\n () => ({\n hiddenModules,\n moduleLabels,\n enabledBehaviors,\n inPersonOnly: salesChannel === 'in_person',\n intentConfig,\n coreNavigation,\n template,\n }),\n [\n hiddenModules,\n moduleLabels,\n enabledBehaviors,\n salesChannel,\n intentConfig,\n coreNavigation,\n template,\n ],\n );\n}\n\nexport function useNavGroups(): NavGroup[] {\n const deps = useNavDeps();\n const { can, isOwner, loading: permissionsLoading } = usePermissions();\n\n return useMemo(\n () => buildNavGroups({ ...deps, isOwner, can, permissionsLoading }),\n [deps, isOwner, can, permissionsLoading],\n );\n}\n\n/**\n * Every destination this business has, with the grant each one asks for —\n * before anybody's permissions are applied to it.\n *\n * `useNavGroups` returns what THIS person may see, which cannot answer \"may\n * this person be here\": a row missing from their nav is missing for four\n * different reasons, and only one of them is permission. Built from the same\n * table with the filter switched off, so the sidebar and the page gate can\n * never drift into disagreeing about which grant a screen needs.\n */\nexport function useAllNavRoutes(): RouteRule[] {\n const deps = useNavDeps();\n\n return useMemo(() => {\n const groups = buildNavGroups({\n ...deps,\n isOwner: true,\n can: () => true,\n permissionsLoading: false,\n });\n\n const rules: RouteRule[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n if (item.href) {\n rules.push({\n href: item.href,\n requiredPermission: item.requiredPermission,\n });\n }\n if (item.children?.length) walk(item.children);\n }\n };\n\n for (const group of groups) {\n if (group.href) {\n rules.push({\n href: group.href,\n requiredPermission: group.requiredPermission,\n });\n }\n for (const section of group.sections ?? []) walk(section.items);\n walk(group.items ?? []);\n }\n return rules;\n }, [deps]);\n}\n\n/**\n * What the nav calls the row that owns this route.\n *\n * `resolveScreenTitle` answers this for the archetype rows, and answers nothing\n * for the rest — Analytics, Payments, Templates, Help and the settings tree are\n * platform rows that live in the spine rather than in an intent's nav. Those\n * are most of the 94 screens that sit on no page frame, so without this they\n * reach the tab with no name of their own.\n *\n * Deepest href wins, so /dashboard/settings/team is Team rather than the\n * Settings row above it.\n */\nexport function useNavScreenName(): string | undefined {\n const groups = useNavGroups();\n const pathname = usePathname();\n\n return useMemo(() => {\n const path = (pathname ?? '').replace(/\\/+$/, '');\n let best: { name: string; depth: number } | undefined;\n\n const consider = (name: string | undefined, href: string | undefined) => {\n if (!name || !href) return;\n const target = href.split('?')[0].replace(/\\/+$/, '');\n if (path !== target && !path.startsWith(`${target}/`)) return;\n if (!best || target.length > best.depth) {\n best = { name, depth: target.length };\n }\n };\n\n const walk = (items: NavItem[] | undefined) =>\n items?.forEach((item) => {\n consider(item.name, item.href);\n walk(item.children);\n });\n\n groups.forEach((group) => {\n consider(group.label, group.href);\n walk(group.items);\n group.sections?.forEach((section) => walk(section.items));\n });\n\n // The account and settings rail is a third list, and a static one — it\n // takes no deps and never varies by archetype, which is why it is a plain\n // constant rather than something the builder assembles. Workspaces, Account\n // and the settings sections are only reachable through it.\n [...ACCOUNT_GROUPS, ...BUSINESS_GROUPS].forEach((group) =>\n group.items.forEach((item) => {\n consider(item.name, item.href);\n // A collapsed row can own routes it does not link to — Team & permissions\n // owns /settings/roles, Shipping owns /settings/service-areas. The rail\n // already says so in order to keep itself highlighted, and that is the\n // same question the tab is asking.\n item.match?.forEach((href) => consider(item.name, href));\n }),\n );\n\n // And a fourth list, the one the Settings and Website hubs are built from.\n // Several screens are reachable only through a hub tile — the editor,\n // Templates, SEO, Payouts — so the sidebar never names them and they were\n // arriving at the tab anonymous. The tile that opens a screen is as good a\n // name for it as a sidebar row is, and it is already written down.\n [...customizationItems, ...businessItems].forEach((item) =>\n consider(item.name, item.href),\n );\n\n return best?.name;\n }, [groups, pathname]);\n}\n"],"mappings":";AAEA,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAC5B,SAAS,6BAA6B;AACtC,SAAS,eAAe;AACxB,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAEP,SAAS,eAAe,0BAA0B;AAelD,SAAS,aAAa;AACpB,QAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAM,EAAE,UAAU,eAAe,IAAI,mBAAmB,QAAQ,UAAU;AAE1E,QAAM,WAAW,QAAQ;AACzB,QAAM,eAAe,sBAAsB,QAAQ;AACnD,QAAM,gBAAgB,UAAU;AAChC,QAAM,eAAe,eAAe,UAAU,YAAY,IACtD,SAAS,eACT;AAIJ,QAAM,mBAAmB;AAAA,IACvB,MAAM,wBAAwB,UAAU,QAAQ;AAAA,IAChD,CAAC,UAAU,QAAQ;AAAA,EACrB;AAEA,QAAM,EAAE,eAAe,IAAI,mBAAmB;AAAA,IAC5C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,UAAU;AAAA,EACzB,CAAC;AAED,QAAM,eAAe,gBAAgB;AAErC,SAAO;AAAA,IACL,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,cAAc,iBAAiB;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,eAA2B;AACzC,QAAM,OAAO,WAAW;AACxB,QAAM,EAAE,KAAK,SAAS,SAAS,mBAAmB,IAAI,eAAe;AAErE,SAAO;AAAA,IACL,MAAM,eAAe,EAAE,GAAG,MAAM,SAAS,KAAK,mBAAmB,CAAC;AAAA,IAClE,CAAC,MAAM,SAAS,KAAK,kBAAkB;AAAA,EACzC;AACF;AAYO,SAAS,kBAA+B;AAC7C,QAAM,OAAO,WAAW;AAExB,SAAO,QAAQ,MAAM;AACnB,UAAM,SAAS,eAAe;AAAA,MAC5B,GAAG;AAAA,MACH,SAAS;AAAA,MACT,KAAK,MAAM;AAAA,MACX,oBAAoB;AAAA,IACtB,CAAC;AAED,UAAM,QAAqB,CAAC;AAC5B,UAAM,OAAO,CAAC,UAAqB;AACjC,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,MAAM;AACb,gBAAM,KAAK;AAAA,YACT,MAAM,KAAK;AAAA,YACX,oBAAoB,KAAK;AAAA,UAC3B,CAAC;AAAA,QACH;AACA,YAAI,KAAK,UAAU,OAAQ,MAAK,KAAK,QAAQ;AAAA,MAC/C;AAAA,IACF;AAEA,eAAW,SAAS,QAAQ;AAC1B,UAAI,MAAM,MAAM;AACd,cAAM,KAAK;AAAA,UACT,MAAM,MAAM;AAAA,UACZ,oBAAoB,MAAM;AAAA,QAC5B,CAAC;AAAA,MACH;AACA,iBAAW,WAAW,MAAM,YAAY,CAAC,EAAG,MAAK,QAAQ,KAAK;AAC9D,WAAK,MAAM,SAAS,CAAC,CAAC;AAAA,IACxB;AACA,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,CAAC;AACX;AAcO,SAAS,mBAAuC;AACrD,QAAM,SAAS,aAAa;AAC5B,QAAM,WAAW,YAAY;AAE7B,SAAO,QAAQ,MAAM;AACnB,UAAM,QAAQ,YAAY,IAAI,QAAQ,QAAQ,EAAE;AAChD,QAAI;AAEJ,UAAM,WAAW,CAAC,MAA0B,SAA6B;AACvE,UAAI,CAAC,QAAQ,CAAC,KAAM;AACpB,YAAM,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,QAAQ,QAAQ,EAAE;AACpD,UAAI,SAAS,UAAU,CAAC,KAAK,WAAW,GAAG,MAAM,GAAG,EAAG;AACvD,UAAI,CAAC,QAAQ,OAAO,SAAS,KAAK,OAAO;AACvC,eAAO,EAAE,MAAM,OAAO,OAAO,OAAO;AAAA,MACtC;AAAA,IACF;AAEA,UAAM,OAAO,CAAC,UACZ,OAAO,QAAQ,CAAC,SAAS;AACvB,eAAS,KAAK,MAAM,KAAK,IAAI;AAC7B,WAAK,KAAK,QAAQ;AAAA,IACpB,CAAC;AAEH,WAAO,QAAQ,CAAC,UAAU;AACxB,eAAS,MAAM,OAAO,MAAM,IAAI;AAChC,WAAK,MAAM,KAAK;AAChB,YAAM,UAAU,QAAQ,CAAC,YAAY,KAAK,QAAQ,KAAK,CAAC;AAAA,IAC1D,CAAC;AAMD,KAAC,GAAG,gBAAgB,GAAG,eAAe,EAAE;AAAA,MAAQ,CAAC,UAC/C,MAAM,MAAM,QAAQ,CAAC,SAAS;AAC5B,iBAAS,KAAK,MAAM,KAAK,IAAI;AAK7B,aAAK,OAAO,QAAQ,CAAC,SAAS,SAAS,KAAK,MAAM,IAAI,CAAC;AAAA,MACzD,CAAC;AAAA,IACH;AAOA,KAAC,GAAG,oBAAoB,GAAG,aAAa,EAAE;AAAA,MAAQ,CAAC,SACjD,SAAS,KAAK,MAAM,KAAK,IAAI;AAAA,IAC/B;AAEA,WAAO,MAAM;AAAA,EACf,GAAG,CAAC,QAAQ,QAAQ,CAAC;AACvB;","names":[]}
1
+ {"version":3,"sources":["../src/use-nav-groups.ts"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { usePathname } from '@withpotter/lib/use-pathname';\nimport { effectiveModuleLabels } from './effective-module-labels';\nimport { useAuth } from '@withpotter/lib/auth-context';\nimport { useCurrentTemplate } from '@withpotter/lib/template-context';\nimport { usePlatformModules } from '@withpotter/lib/use-platform-modules';\nimport { useIntentConfig } from '@withpotter/lib/intents/use-intent-config';\nimport { usePermissions } from '@withpotter/lib/use-permissions';\nimport { resolveEnabledBehaviors } from '@withpotter/lib/resolve-enabled-behaviors';\nimport { isSalesChannel } from '@withpotter/hooks/use-sales-channel';\nimport { buildNavGroups } from '@withpotter/components/sidebar/build-nav-groups';\nimport {\n ACCOUNT_GROUPS,\n BUSINESS_GROUPS,\n type NavGroup,\n type NavItem,\n} from '@withpotter/components/sidebar/config';\nimport type { RouteRule } from '@withpotter/lib/route-permission';\nimport { businessItems, customizationItems } from '@withpotter/lib/navigation-groups';\n\n/**\n * The sidebar's own groups, for callers that need to know what the nav shows\n * without being the sidebar.\n *\n * The deps this builder wants are a fixed dozen, and assembling them was\n * already copied out once into the editor's jump targets. This is that\n * assembly, named — permissions, hidden modules, archetype collapsing and the\n * merchant's own module labels all arrive already applied, because they are\n * applied inside `buildNavGroups`.\n *\n * Calls `useIntentConfig`, which calls `useSearchParams`. A caller in a layout\n * needs a Suspense boundary above it or the whole route bails out of rendering.\n */\nfunction useNavDeps() {\n const { tenant } = useAuth();\n const { template, dashboardState } = useCurrentTemplate(tenant?.templateId);\n\n const settings = tenant?.settings as Record<string, unknown> | undefined;\n const moduleLabels = effectiveModuleLabels(settings);\n const hiddenModules = settings?.hiddenModules as string[] | undefined;\n const salesChannel = isSalesChannel(settings?.salesChannel)\n ? settings.salesChannel\n : null;\n\n const { coreNavigation } = usePlatformModules({\n template,\n dashboardState,\n enabledBehaviors: settings?.enabledBehaviors as string[] | undefined,\n hiddenModules,\n moduleLabels,\n moduleOrder: settings?.moduleOrder as string[] | undefined,\n });\n\n const enabledBehaviors = useMemo(\n () => resolveEnabledBehaviors(settings, template),\n [settings, template],\n );\n const intentConfig = useIntentConfig();\n\n return useMemo(\n () => ({\n hiddenModules,\n moduleLabels,\n enabledBehaviors,\n inPersonOnly: salesChannel === 'in_person',\n intentConfig,\n coreNavigation,\n template,\n }),\n [\n hiddenModules,\n moduleLabels,\n enabledBehaviors,\n salesChannel,\n intentConfig,\n coreNavigation,\n template,\n ],\n );\n}\n\nexport function useNavGroups(): NavGroup[] {\n const deps = useNavDeps();\n const { can, isOwner, loading: permissionsLoading } = usePermissions();\n\n return useMemo(\n () => buildNavGroups({ ...deps, isOwner, can, permissionsLoading }),\n [deps, isOwner, can, permissionsLoading],\n );\n}\n\n/**\n * Every destination this business has, with the grant each one asks for —\n * before anybody's permissions are applied to it.\n *\n * `useNavGroups` returns what THIS person may see, which cannot answer \"may\n * this person be here\": a row missing from their nav is missing for four\n * different reasons, and only one of them is permission. Built from the same\n * table with the filter switched off, so the sidebar and the page gate can\n * never drift into disagreeing about which grant a screen needs.\n */\nexport function useAllNavRoutes(): RouteRule[] {\n const deps = useNavDeps();\n\n return useMemo(() => {\n const groups = buildNavGroups({\n ...deps,\n isOwner: true,\n can: () => true,\n permissionsLoading: false,\n });\n\n const rules: RouteRule[] = [];\n const walk = (items: NavItem[]) => {\n for (const item of items) {\n if (item.href) {\n rules.push({\n href: item.href,\n requiredPermission: item.requiredPermission,\n });\n }\n if (item.children?.length) walk(item.children);\n }\n };\n\n for (const group of groups) {\n if (group.href) {\n rules.push({\n href: group.href,\n requiredPermission: group.requiredPermission,\n });\n }\n for (const section of group.sections ?? []) walk(section.items);\n walk(group.items ?? []);\n }\n return rules;\n }, [deps]);\n}\n\n/**\n * What the nav calls the row that owns this route.\n *\n * `resolveScreenTitle` answers this for the archetype rows, and answers nothing\n * for the rest — Analytics, Payments, Templates, Help and the settings tree are\n * platform rows that live in the spine rather than in an intent's nav. Those\n * are most of the 94 screens that sit on no page frame, so without this they\n * reach the tab with no name of their own.\n *\n * Deepest href wins, so /dashboard/settings/team is Team rather than the\n * Settings row above it.\n */\nexport function useNavScreenName(): string | undefined {\n const groups = useNavGroups();\n const pathname = usePathname();\n\n return useMemo(() => {\n const path = (pathname ?? '').replace(/\\/+$/, '');\n let best: { name: string; depth: number } | undefined;\n\n const consider = (name: string | undefined, href: string | undefined) => {\n if (!name || !href) return;\n const target = href.split('?')[0].replace(/\\/+$/, '');\n if (path !== target && !path.startsWith(`${target}/`)) return;\n if (!best || target.length > best.depth) {\n best = { name, depth: target.length };\n }\n };\n\n const walk = (items: NavItem[] | undefined) =>\n items?.forEach((item) => {\n consider(item.name, item.href);\n walk(item.children);\n });\n\n groups.forEach((group) => {\n consider(group.label, group.href);\n walk(group.items);\n group.sections?.forEach((section) => walk(section.items));\n });\n\n // The account and settings rail is a third list, and a static one — it\n // takes no deps and never varies by archetype, which is why it is a plain\n // constant rather than something the builder assembles. Workspaces, Account\n // and the settings sections are only reachable through it.\n [...ACCOUNT_GROUPS, ...BUSINESS_GROUPS].forEach((group) =>\n group.items.forEach((item) => {\n consider(item.name, item.href);\n // A collapsed row can own routes it does not link to — Team & permissions\n // owns /settings/roles, Shipping owns /settings/service-areas. The rail\n // already says so in order to keep itself highlighted, and that is the\n // same question the tab is asking.\n item.match?.forEach((href) => consider(item.name, href));\n }),\n );\n\n // And a fourth list, the one the Settings and Website hubs are built from.\n // Several screens are reachable only through a hub tile — the editor,\n // Templates, SEO, Payouts — so the sidebar never names them and they were\n // arriving at the tab anonymous. The tile that opens a screen is as good a\n // name for it as a sidebar row is, and it is already written down.\n [...customizationItems, ...businessItems].forEach((item) =>\n consider(item.name, item.href),\n );\n\n return best?.name;\n }, [groups, pathname]);\n}\n"],"mappings":"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","names":[]}
package/dist/utils.js CHANGED
@@ -23,7 +23,7 @@ const CURRENCY_SYMBOLS = {
23
23
  MXN: "$",
24
24
  AED: "\u062F.\u0625"
25
25
  };
26
- function normalizeCurrencyCode(currency, fallback = "NGN") {
26
+ function normalizeCurrencyCode(currency, fallback = "USD") {
27
27
  const normalized = typeof currency === "string" ? currency.trim().toUpperCase() : "";
28
28
  const supportedCurrencies = Intl.supportedValuesOf?.("currency") ?? [];
29
29
  return supportedCurrencies.includes(normalized) ? normalized : fallback;
@@ -53,19 +53,19 @@ function setActiveCurrency(currency) {
53
53
  }
54
54
  }
55
55
  function getActiveCurrency() {
56
- if (typeof window === "undefined") return override ?? "NGN";
56
+ if (typeof window === "undefined") return override ?? "USD";
57
57
  let slug = null;
58
58
  try {
59
59
  slug = localStorage.getItem(WORKSPACE_KEY);
60
60
  } catch {
61
- return override ?? cachedCurrency ?? "NGN";
61
+ return override ?? cachedCurrency ?? "USD";
62
62
  }
63
63
  if (slug !== cachedSlug) {
64
64
  cachedSlug = slug;
65
65
  cachedCurrency = readStoredCurrency();
66
66
  override = null;
67
67
  }
68
- return override ?? cachedCurrency ?? "NGN";
68
+ return override ?? cachedCurrency ?? "USD";
69
69
  }
70
70
  function formatCurrency(amount, currency) {
71
71
  const code = currency || getActiveCurrency();
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * The value as a list, whatever actually arrived.\n *\n * `apiFetch<Thing[]>` is a promise of a type, not a guarantee of one: the\n * generic tells TypeScript what to expect and nothing checks it at runtime. A\n * 200 carrying an error object, a `{ data: [...] }` envelope from a changed\n * endpoint, or `null` all satisfy the compiler and then meet `.filter`.\n *\n * A `= []` default does not cover this — that only fires when the value is\n * `undefined`, and neither does `.catch(() => [])`, which only fires when the\n * promise rejects. This is for the case in between, where the request\n * succeeded and returned the wrong shape.\n *\n * Worth using anywhere the list drives render in the shell, where a throw\n * takes down every page rather than one screen.\n */\nexport function asList<T>(value: T[] | null | undefined | unknown): T[] {\n return Array.isArray(value) ? (value as T[]) : [];\n}\n\n// Currency symbol map – avoids Intl locale inconsistencies (en-US renders NGN\n// as \"NGN\" instead of \"₦\"). Keep in sync with dynamic-form.tsx CURRENCY_SYMBOLS.\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n NGN: '₦',\n USD: '$',\n EUR: '€',\n GBP: '£',\n CAD: 'C$',\n AUD: 'A$',\n GHS: '₵',\n KES: 'KSh',\n ZAR: 'R',\n INR: '₹',\n JPY: '¥',\n CNY: '¥',\n BRL: 'R$',\n MXN: '$',\n AED: 'د.إ',\n};\n\nexport function normalizeCurrencyCode(\n currency: unknown,\n fallback = 'NGN',\n): string {\n const normalized =\n typeof currency === 'string' ? currency.trim().toUpperCase() : '';\n const supportedCurrencies = Intl.supportedValuesOf?.('currency') ?? [];\n return supportedCurrencies.includes(normalized) ? normalized : fallback;\n}\n\n/**\n * The workspace's own currency, for the 250-odd places that format money\n * without naming one.\n *\n * `formatCurrency` defaulted to USD, so a Lagos merchant's ₦500,000 quote read\n * \"$500,000\" on every screen that did not pass a code — Orders, Payments,\n * Customers, invoice totals, the enquiry stats. The ones that DO pass a code\n * are passing a product's or an invoice's own currency, which answers a\n * different question: what this row is priced in, not what this business\n * trades in. Those still win.\n *\n * Read from the persisted workspace rather than set by a provider. Auth already\n * writes the whole tenant to `localStorage.tenant` every time one becomes\n * current — login, reload and `switchWorkspace` all land there — so the value\n * is available to plain functions with no hook and no context, which is what\n * `toAnswers` and the table cell formatters are. It also keeps this out of\n * `auth-context.tsx`, which sits exactly on its length ledger.\n *\n * The parse is cached against `currentWorkspace`, a one-key string read: the\n * cheap read happens per call, the JSON parse only when the workspace changes.\n */\nconst WORKSPACE_KEY = 'currentWorkspace';\nconst TENANT_KEY = 'tenant';\n\nlet cachedSlug: string | null | undefined;\nlet cachedCurrency: string | null = null;\n/** Set from a live tenant, so a fresh fetch beats what was last persisted. */\nlet override: string | null = null;\n\nfunction readStoredCurrency(): string | null {\n try {\n const raw = localStorage.getItem(TENANT_KEY);\n if (!raw) return null;\n const code = (JSON.parse(raw) as { settings?: { currency?: unknown } })\n ?.settings?.currency;\n return typeof code === 'string' && code ? code.toUpperCase() : null;\n } catch {\n // Unparseable, or storage blocked. The default is naira; throwing is wrong\n // for every merchant.\n return null;\n }\n}\n\n/**\n * Point the formatter at a currency directly.\n *\n * `useCurrency` calls this when a live tenant differs from the stored one. An\n * empty value is ignored rather than resetting a correct currency to dollars —\n * workspaces load asynchronously and some have none set.\n */\nexport function setActiveCurrency(currency: string | undefined | null): void {\n if (!currency) return;\n override = currency.toUpperCase();\n try {\n cachedSlug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n cachedSlug = undefined;\n }\n}\n\nexport function getActiveCurrency(): string {\n if (typeof window === 'undefined') return override ?? 'NGN';\n\n let slug: string | null = null;\n try {\n slug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n return override ?? cachedCurrency ?? 'NGN';\n }\n\n if (slug !== cachedSlug) {\n cachedSlug = slug;\n cachedCurrency = readStoredCurrency();\n // The override belonged to the workspace that just went away.\n override = null;\n }\n\n return override ?? cachedCurrency ?? 'NGN';\n}\n\nexport function formatCurrency(amount: number, currency?: string): string {\n const code = currency || getActiveCurrency();\n const symbol = CURRENCY_SYMBOLS[code] || code;\n const formatted = new Intl.NumberFormat('en-US', {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n }).format(amount);\n return `${symbol}${formatted}`;\n}\n\nexport function formatDate(\n date: string | Date,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const defaultOptions: Intl.DateTimeFormatOptions = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n ...options,\n };\n return new Intl.DateTimeFormat('en-NG', defaultOptions).format(\n new Date(date),\n );\n}\n\n/**\n * How long ago something happened: \"just now\", \"4m\", \"3h\", \"2d\", then a date.\n *\n * The counterpart to `formatRelativeTime`, which rounds to whole days and\n * leans forward (\"Tomorrow\", \"In 3 days\") because it was written for things\n * that are due. Anything that has already happened lands in its `Today` bucket\n * and stops being a time at all, which is no use in a feed where every row is\n * minutes old and the order is the whole point.\n *\n * Bare units, no \"ago\": in a list where every row is a past event, the word is\n * on every line and carries nothing.\n */\nexport function formatTimeAgo(date: string | Date): string {\n const then = new Date(date).getTime();\n if (Number.isNaN(then)) return '';\n const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));\n\n if (seconds < 45) return 'just now';\n if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;\n if (seconds < 86_400) return `${Math.floor(seconds / 3600)}h`;\n if (seconds < 604_800) return `${Math.floor(seconds / 86_400)}d`;\n return formatDate(date, { year: undefined, month: 'short', day: 'numeric' });\n}\n\nexport function formatRelativeTime(date: string | Date): string {\n const now = new Date();\n const target = new Date(date);\n const diffInDays = Math.ceil(\n (target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n if (diffInDays < 0) {\n return `${Math.abs(diffInDays)} days ago`;\n } else if (diffInDays === 0) {\n return 'Today';\n } else if (diffInDays === 1) {\n return 'Tomorrow';\n } else if (diffInDays < 7) {\n return `In ${diffInDays} days`;\n } else if (diffInDays < 30) {\n const weeks = Math.ceil(diffInDays / 7);\n return `In ${weeks} week${weeks > 1 ? 's' : ''}`;\n } else {\n return formatDate(date);\n }\n}\n\nexport function capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n if (i === 0) return `${bytes} B`;\n const value = bytes / Math.pow(1024, i);\n return `${parseFloat(value.toFixed(1))} ${units[i]}`;\n}\n\n/**\n * \"3d ago\". Lived in `reviews/_components` and was copied by hand into the\n * abandoned-cart page, so two surfaces rounded time differently. It is a date\n * helper, so it belongs beside the other date helpers.\n */\nexport function timeAgo(dateStr: string): string {\n const now = Date.now();\n const d = new Date(dateStr).getTime();\n const diff = now - d;\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return 'just now';\n if (mins < 60) return `${mins}m ago`;\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return `${hrs}h ago`;\n const days = Math.floor(hrs / 24);\n if (days < 30) return `${days}d ago`;\n const months = Math.floor(days / 30);\n if (months < 12) return `${months}mo ago`;\n return `${Math.floor(months / 12)}y ago`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/utils.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * The value as a list, whatever actually arrived.\n *\n * `apiFetch<Thing[]>` is a promise of a type, not a guarantee of one: the\n * generic tells TypeScript what to expect and nothing checks it at runtime. A\n * 200 carrying an error object, a `{ data: [...] }` envelope from a changed\n * endpoint, or `null` all satisfy the compiler and then meet `.filter`.\n *\n * A `= []` default does not cover this — that only fires when the value is\n * `undefined`, and neither does `.catch(() => [])`, which only fires when the\n * promise rejects. This is for the case in between, where the request\n * succeeded and returned the wrong shape.\n *\n * Worth using anywhere the list drives render in the shell, where a throw\n * takes down every page rather than one screen.\n */\nexport function asList<T>(value: T[] | null | undefined | unknown): T[] {\n return Array.isArray(value) ? (value as T[]) : [];\n}\n\n// Currency symbol map – avoids Intl locale inconsistencies (en-US renders NGN\n// as \"NGN\" instead of \"₦\"). Keep in sync with dynamic-form.tsx CURRENCY_SYMBOLS.\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n NGN: '₦',\n USD: '$',\n EUR: '€',\n GBP: '£',\n CAD: 'C$',\n AUD: 'A$',\n GHS: '₵',\n KES: 'KSh',\n ZAR: 'R',\n INR: '₹',\n JPY: '¥',\n CNY: '¥',\n BRL: 'R$',\n MXN: '$',\n AED: 'د.إ',\n};\n\nexport function normalizeCurrencyCode(\n currency: unknown,\n fallback = 'USD',\n): string {\n const normalized =\n typeof currency === 'string' ? currency.trim().toUpperCase() : '';\n const supportedCurrencies = Intl.supportedValuesOf?.('currency') ?? [];\n return supportedCurrencies.includes(normalized) ? normalized : fallback;\n}\n\n/**\n * The workspace's own currency, for the 250-odd places that format money\n * without naming one.\n *\n * `formatCurrency` defaulted to USD, so a Lagos merchant's ₦500,000 quote read\n * \"$500,000\" on every screen that did not pass a code — Orders, Payments,\n * Customers, invoice totals, the enquiry stats. The ones that DO pass a code\n * are passing a product's or an invoice's own currency, which answers a\n * different question: what this row is priced in, not what this business\n * trades in. Those still win.\n *\n * Read from the persisted workspace rather than set by a provider. Auth already\n * writes the whole tenant to `localStorage.tenant` every time one becomes\n * current — login, reload and `switchWorkspace` all land there — so the value\n * is available to plain functions with no hook and no context, which is what\n * `toAnswers` and the table cell formatters are. It also keeps this out of\n * `auth-context.tsx`, which sits exactly on its length ledger.\n *\n * The parse is cached against `currentWorkspace`, a one-key string read: the\n * cheap read happens per call, the JSON parse only when the workspace changes.\n */\nconst WORKSPACE_KEY = 'currentWorkspace';\nconst TENANT_KEY = 'tenant';\n\nlet cachedSlug: string | null | undefined;\nlet cachedCurrency: string | null = null;\n/** Set from a live tenant, so a fresh fetch beats what was last persisted. */\nlet override: string | null = null;\n\nfunction readStoredCurrency(): string | null {\n try {\n const raw = localStorage.getItem(TENANT_KEY);\n if (!raw) return null;\n const code = (JSON.parse(raw) as { settings?: { currency?: unknown } })\n ?.settings?.currency;\n return typeof code === 'string' && code ? code.toUpperCase() : null;\n } catch {\n // Unparseable, or storage blocked. Dollars is wrong for some merchants and\n // right for others; throwing is wrong for all of them.\n return null;\n }\n}\n\n/**\n * Point the formatter at a currency directly.\n *\n * `useCurrency` calls this when a live tenant differs from the stored one. An\n * empty value is ignored rather than resetting a correct currency to dollars —\n * workspaces load asynchronously and some have none set.\n */\nexport function setActiveCurrency(currency: string | undefined | null): void {\n if (!currency) return;\n override = currency.toUpperCase();\n try {\n cachedSlug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n cachedSlug = undefined;\n }\n}\n\nexport function getActiveCurrency(): string {\n if (typeof window === 'undefined') return override ?? 'USD';\n\n let slug: string | null = null;\n try {\n slug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n return override ?? cachedCurrency ?? 'USD';\n }\n\n if (slug !== cachedSlug) {\n cachedSlug = slug;\n cachedCurrency = readStoredCurrency();\n // The override belonged to the workspace that just went away.\n override = null;\n }\n\n return override ?? cachedCurrency ?? 'USD';\n}\n\nexport function formatCurrency(amount: number, currency?: string): string {\n const code = currency || getActiveCurrency();\n const symbol = CURRENCY_SYMBOLS[code] || code;\n const formatted = new Intl.NumberFormat('en-US', {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n }).format(amount);\n return `${symbol}${formatted}`;\n}\n\nexport function formatDate(\n date: string | Date,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const defaultOptions: Intl.DateTimeFormatOptions = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n ...options,\n };\n return new Intl.DateTimeFormat('en-NG', defaultOptions).format(\n new Date(date),\n );\n}\n\n/**\n * How long ago something happened: \"just now\", \"4m\", \"3h\", \"2d\", then a date.\n *\n * The counterpart to `formatRelativeTime`, which rounds to whole days and\n * leans forward (\"Tomorrow\", \"In 3 days\") because it was written for things\n * that are due. Anything that has already happened lands in its `Today` bucket\n * and stops being a time at all, which is no use in a feed where every row is\n * minutes old and the order is the whole point.\n *\n * Bare units, no \"ago\": in a list where every row is a past event, the word is\n * on every line and carries nothing.\n */\nexport function formatTimeAgo(date: string | Date): string {\n const then = new Date(date).getTime();\n if (Number.isNaN(then)) return '';\n const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));\n\n if (seconds < 45) return 'just now';\n if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;\n if (seconds < 86_400) return `${Math.floor(seconds / 3600)}h`;\n if (seconds < 604_800) return `${Math.floor(seconds / 86_400)}d`;\n return formatDate(date, { year: undefined, month: 'short', day: 'numeric' });\n}\n\nexport function formatRelativeTime(date: string | Date): string {\n const now = new Date();\n const target = new Date(date);\n const diffInDays = Math.ceil(\n (target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n if (diffInDays < 0) {\n return `${Math.abs(diffInDays)} days ago`;\n } else if (diffInDays === 0) {\n return 'Today';\n } else if (diffInDays === 1) {\n return 'Tomorrow';\n } else if (diffInDays < 7) {\n return `In ${diffInDays} days`;\n } else if (diffInDays < 30) {\n const weeks = Math.ceil(diffInDays / 7);\n return `In ${weeks} week${weeks > 1 ? 's' : ''}`;\n } else {\n return formatDate(date);\n }\n}\n\nexport function capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n if (i === 0) return `${bytes} B`;\n const value = bytes / Math.pow(1024, i);\n return `${parseFloat(value.toFixed(1))} ${units[i]}`;\n}\n\n/**\n * \"3d ago\". Lived in `reviews/_components` and was copied by hand into the\n * abandoned-cart page, so two surfaces rounded time differently. It is a date\n * helper, so it belongs beside the other date helpers.\n */\nexport function timeAgo(dateStr: string): string {\n const now = Date.now();\n const d = new Date(dateStr).getTime();\n const diff = now - d;\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return 'just now';\n if (mins < 60) return `${mins}m ago`;\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return `${hrs}h ago`;\n const days = Math.floor(hrs / 24);\n if (days < 30) return `${days}d ago`;\n const months = Math.floor(days / 30);\n if (months < 12) return `${months}mo ago`;\n return `${Math.floor(months / 12)}y ago`;\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,6 @@
1
1
  function formatMinor(minor, currency) {
2
2
  const format = new Intl.NumberFormat(void 0, {
3
3
  style: "currency",
4
- currencyDisplay: "narrowSymbol",
5
4
  currency
6
5
  });
7
6
  const digits = format.resolvedOptions().maximumFractionDigits ?? 2;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/wallet-format.ts"],"sourcesContent":["/**\n * Wallet amounts arrive as minor units in strings (\"150000\" = ₦1,500.00).\n * Divide by the currency's own number of decimals, in BigInt so large\n * balances stay exact.\n */\nexport function formatMinor(minor: string, currency: string): string {\n const format = new Intl.NumberFormat(undefined, {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n });\n const digits = format.resolvedOptions().maximumFractionDigits ?? 2;\n\n let value: bigint;\n\n try {\n value = BigInt(minor);\n } catch {\n return `${minor} ${currency}`;\n }\n\n const negative = value < BigInt(0);\n const abs = negative ? -value : value;\n const scale = BigInt(10) ** BigInt(digits);\n const whole = abs / scale;\n const fraction = (abs % scale).toString().padStart(digits, '0');\n const parts = format.formatToParts(0);\n const decimal = parts.find((p) => p.type === 'decimal')?.value ?? '.';\n const grouped = format\n .formatToParts(Number(whole) <= Number.MAX_SAFE_INTEGER ? Number(whole) : 0)\n .map((p) => {\n if (p.type === 'integer' && whole > BigInt(Number.MAX_SAFE_INTEGER)) {\n return whole.toString();\n }\n if (p.type === 'fraction') return fraction;\n if (p.type === 'decimal') return digits ? decimal : '';\n return p.value;\n })\n .join('');\n\n return negative ? `-${grouped}` : grouped;\n}\n\n/** Statuses come back in either case (\"ACTIVE\" from money, \"active\" elsewhere). */\nexport const sameStatus = (status: string) => status.toLowerCase();\n"],"mappings":"AAKO,SAAS,YAAY,OAAe,UAA0B;AACnE,QAAM,SAAS,IAAI,KAAK,aAAa,QAAW;AAAA,IAC9C,OAAO;AAAA,IACP,iBAAiB;AAAA,IACjB;AAAA,EACF,CAAC;AACD,QAAM,SAAS,OAAO,gBAAgB,EAAE,yBAAyB;AAEjE,MAAI;AAEJ,MAAI;AACF,YAAQ,OAAO,KAAK;AAAA,EACtB,QAAQ;AACN,WAAO,GAAG,KAAK,IAAI,QAAQ;AAAA,EAC7B;AAEA,QAAM,WAAW,QAAQ,OAAO,CAAC;AACjC,QAAM,MAAM,WAAW,CAAC,QAAQ;AAChC,QAAM,QAAQ,OAAO,EAAE,KAAK,OAAO,MAAM;AACzC,QAAM,QAAQ,MAAM;AACpB,QAAM,YAAY,MAAM,OAAO,SAAS,EAAE,SAAS,QAAQ,GAAG;AAC9D,QAAM,QAAQ,OAAO,cAAc,CAAC;AACpC,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,GAAG,SAAS;AAClE,QAAM,UAAU,OACb,cAAc,OAAO,KAAK,KAAK,OAAO,mBAAmB,OAAO,KAAK,IAAI,CAAC,EAC1E,IAAI,CAAC,MAAM;AACV,QAAI,EAAE,SAAS,aAAa,QAAQ,OAAO,OAAO,gBAAgB,GAAG;AACnE,aAAO,MAAM,SAAS;AAAA,IACxB;AACA,QAAI,EAAE,SAAS,WAAY,QAAO;AAClC,QAAI,EAAE,SAAS,UAAW,QAAO,SAAS,UAAU;AACpD,WAAO,EAAE;AAAA,EACX,CAAC,EACA,KAAK,EAAE;AAEV,SAAO,WAAW,IAAI,OAAO,KAAK;AACpC;AAGO,MAAM,aAAa,CAAC,WAAmB,OAAO,YAAY;","names":[]}
1
+ {"version":3,"sources":["../src/wallet-format.ts"],"sourcesContent":["/**\n * Wallet amounts arrive as minor units in strings (\"150000\" = ₦1,500.00).\n * Divide by the currency's own number of decimals, in BigInt so large\n * balances stay exact.\n */\nexport function formatMinor(minor: string, currency: string): string {\n const format = new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n });\n const digits = format.resolvedOptions().maximumFractionDigits ?? 2;\n\n let value: bigint;\n\n try {\n value = BigInt(minor);\n } catch {\n return `${minor} ${currency}`;\n }\n\n const negative = value < BigInt(0);\n const abs = negative ? -value : value;\n const scale = BigInt(10) ** BigInt(digits);\n const whole = abs / scale;\n const fraction = (abs % scale).toString().padStart(digits, '0');\n const parts = format.formatToParts(0);\n const decimal = parts.find((p) => p.type === 'decimal')?.value ?? '.';\n const grouped = format\n .formatToParts(Number(whole) <= Number.MAX_SAFE_INTEGER ? Number(whole) : 0)\n .map((p) => {\n if (p.type === 'integer' && whole > BigInt(Number.MAX_SAFE_INTEGER)) {\n return whole.toString();\n }\n if (p.type === 'fraction') return fraction;\n if (p.type === 'decimal') return digits ? decimal : '';\n return p.value;\n })\n .join('');\n\n return negative ? `-${grouped}` : grouped;\n}\n\n/** Statuses come back in either case (\"ACTIVE\" from money, \"active\" elsewhere). */\nexport const sameStatus = (status: string) => status.toLowerCase();\n"],"mappings":"AAKO,SAAS,YAAY,OAAe,UAA0B;AACnE,QAAM,SAAS,IAAI,KAAK,aAAa,QAAW;AAAA,IAC9C,OAAO;AAAA,IACP;AAAA,EACF,CAAC;AACD,QAAM,SAAS,OAAO,gBAAgB,EAAE,yBAAyB;AAEjE,MAAI;AAEJ,MAAI;AACF,YAAQ,OAAO,KAAK;AAAA,EACtB,QAAQ;AACN,WAAO,GAAG,KAAK,IAAI,QAAQ;AAAA,EAC7B;AAEA,QAAM,WAAW,QAAQ,OAAO,CAAC;AACjC,QAAM,MAAM,WAAW,CAAC,QAAQ;AAChC,QAAM,QAAQ,OAAO,EAAE,KAAK,OAAO,MAAM;AACzC,QAAM,QAAQ,MAAM;AACpB,QAAM,YAAY,MAAM,OAAO,SAAS,EAAE,SAAS,QAAQ,GAAG;AAC9D,QAAM,QAAQ,OAAO,cAAc,CAAC;AACpC,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,GAAG,SAAS;AAClE,QAAM,UAAU,OACb,cAAc,OAAO,KAAK,KAAK,OAAO,mBAAmB,OAAO,KAAK,IAAI,CAAC,EAC1E,IAAI,CAAC,MAAM;AACV,QAAI,EAAE,SAAS,aAAa,QAAQ,OAAO,OAAO,gBAAgB,GAAG;AACnE,aAAO,MAAM,SAAS;AAAA,IACxB;AACA,QAAI,EAAE,SAAS,WAAY,QAAO;AAClC,QAAI,EAAE,SAAS,UAAW,QAAO,SAAS,UAAU;AACpD,WAAO,EAAE;AAAA,EACX,CAAC,EACA,KAAK,EAAE;AAEV,SAAO,WAAW,IAAI,OAAO,KAAK;AACpC;AAGO,MAAM,aAAa,CAAC,WAAmB,OAAO,YAAY;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withpotter/lib",
3
- "version": "0.13.0-next.124",
3
+ "version": "0.13.0-next.59",
4
4
  "description": "Box dashboard foundation: API client, contexts, types, intent config, and utilities.",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": false,
@@ -51,7 +51,7 @@
51
51
  "typescript": "^5"
52
52
  },
53
53
  "publishConfig": {
54
- "registry": "https://registry.npmjs.org",
54
+ "registry": "https://registry.npmjs.org/",
55
55
  "access": "restricted"
56
56
  }
57
57
  }