@odla-ai/chapter 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
3
  import { CrmClient, Crm, CrmRecord } from '@odla-ai/crm';
4
- import { a as Chapter, b as ChapterCopy } from '../../copy-context-efEMF_aD.js';
5
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-efEMF_aD.js';
4
+ import { a as Chapter, b as ChapterCopy } from '../../copy-context-HAhNTZdl.js';
5
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-HAhNTZdl.js';
6
6
  import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
7
7
 
8
8
  /** Normalized public auth configuration consumed by the Clerk gate. */
@@ -0,0 +1,29 @@
1
+ import { JSX } from 'preact';
2
+ import { FieldState, FieldConditions } from '@odla-ai/crm';
3
+
4
+ /** What {@link useFieldConditions} hands back. */
5
+ interface FieldConditionsHandle {
6
+ /** Live form answers, refreshed on every input. */
7
+ values: Record<string, unknown>;
8
+ /** Per-field verdict. A field with no declared condition is visible. */
9
+ states: Record<string, FieldState>;
10
+ /** Whether to render a field. Unconditional fields are always visible. */
11
+ isVisible: (field: string) => boolean;
12
+ /** Whether a field must be answered given the current values. */
13
+ isRequired: (field: string) => boolean;
14
+ /** Attach to the form (or any ancestor of the inputs) to track answers. */
15
+ onInput: (event: JSX.TargetedEvent<HTMLElement>) => void;
16
+ }
17
+ /**
18
+ * Track a join form's answers and resolve which fields apply.
19
+ *
20
+ * Values are read straight off the DOM `FormData` rather than mirrored into
21
+ * component state, so a site keeps uncontrolled inputs and this stays a
22
+ * read-only observer of a form it does not own.
23
+ *
24
+ * @param conditions Per-field conditions, as `GET /api/join-config` returns them.
25
+ * @param staticRequired Fields required unconditionally, for the fallback.
26
+ */
27
+ declare function useFieldConditions(conditions?: Readonly<Record<string, FieldConditions>>, staticRequired?: readonly string[]): FieldConditionsHandle;
28
+
29
+ export { type FieldConditionsHandle, useFieldConditions };
@@ -0,0 +1,32 @@
1
+ import {
2
+ collectFormFields
3
+ } from "../../chunk-IVXVECKR.js";
4
+
5
+ // src/ui/field-conditions.tsx
6
+ import { useMemo, useState } from "preact/hooks";
7
+ import { resolveFieldStates } from "@odla-ai/crm";
8
+ function useFieldConditions(conditions = {}, staticRequired = []) {
9
+ const [values, setValues] = useState({});
10
+ const states = useMemo(
11
+ () => resolveFieldStates(conditions, values, staticRequired),
12
+ [conditions, values, staticRequired]
13
+ );
14
+ return {
15
+ values,
16
+ states,
17
+ // Absent from `states` means undeclared and unconditional: visible, and
18
+ // required only if the caller said so. Never hide a field we know nothing
19
+ // about — the same fail-open rule the evaluator uses.
20
+ isVisible: (field) => states[field]?.visible ?? true,
21
+ isRequired: (field) => states[field]?.required ?? staticRequired.includes(field),
22
+ onInput: (event) => {
23
+ const target = event.currentTarget;
24
+ const form = target instanceof HTMLFormElement ? target : target.closest?.("form") ?? null;
25
+ if (form) setValues(collectFormFields(new FormData(form)));
26
+ }
27
+ };
28
+ }
29
+ export {
30
+ useFieldConditions
31
+ };
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/field-conditions.tsx"],"sourcesContent":["// The conditional-field seam for a join form. The site owns every input; this\n// only answers \"should that one be shown, and must it be answered right now?\"\n//\n// The verdict comes from @odla-ai/crm's resolver — the same function\n// submitApplication calls server-side — so a form and its validator cannot\n// disagree about a field. That is the whole reason the rule is data rather than\n// a render-time `&&`.\nimport { useMemo, useState } from \"preact/hooks\";\nimport type { JSX } from \"preact\";\nimport { resolveFieldStates, type FieldConditions, type FieldState } from \"@odla-ai/crm\";\nimport { collectFormFields } from \"./form-fields.js\";\n\n/** What {@link useFieldConditions} hands back. */\nexport interface FieldConditionsHandle {\n /** Live form answers, refreshed on every input. */\n values: Record<string, unknown>;\n /** Per-field verdict. A field with no declared condition is visible. */\n states: Record<string, FieldState>;\n /** Whether to render a field. Unconditional fields are always visible. */\n isVisible: (field: string) => boolean;\n /** Whether a field must be answered given the current values. */\n isRequired: (field: string) => boolean;\n /** Attach to the form (or any ancestor of the inputs) to track answers. */\n onInput: (event: JSX.TargetedEvent<HTMLElement>) => void;\n}\n\n/**\n * Track a join form's answers and resolve which fields apply.\n *\n * Values are read straight off the DOM `FormData` rather than mirrored into\n * component state, so a site keeps uncontrolled inputs and this stays a\n * read-only observer of a form it does not own.\n *\n * @param conditions Per-field conditions, as `GET /api/join-config` returns them.\n * @param staticRequired Fields required unconditionally, for the fallback.\n */\nexport function useFieldConditions(\n conditions: Readonly<Record<string, FieldConditions>> = {},\n staticRequired: readonly string[] = [],\n): FieldConditionsHandle {\n const [values, setValues] = useState<Record<string, unknown>>({});\n const states = useMemo(\n () => resolveFieldStates(conditions, values, staticRequired),\n [conditions, values, staticRequired],\n );\n return {\n values,\n states,\n // Absent from `states` means undeclared and unconditional: visible, and\n // required only if the caller said so. Never hide a field we know nothing\n // about — the same fail-open rule the evaluator uses.\n isVisible: (field) => states[field]?.visible ?? true,\n isRequired: (field) => states[field]?.required ?? staticRequired.includes(field),\n onInput: (event) => {\n const target = event.currentTarget;\n const form = target instanceof HTMLFormElement\n ? target\n : target.closest?.(\"form\") ?? null;\n if (form) setValues(collectFormFields(new FormData(form)));\n },\n };\n}\n"],"mappings":";;;;;AAOA,SAAS,SAAS,gBAAgB;AAElC,SAAS,0BAAiE;AA2BnE,SAAS,mBACd,aAAwD,CAAC,GACzD,iBAAoC,CAAC,GACd;AACvB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAkC,CAAC,CAAC;AAChE,QAAM,SAAS;AAAA,IACb,MAAM,mBAAmB,YAAY,QAAQ,cAAc;AAAA,IAC3D,CAAC,YAAY,QAAQ,cAAc;AAAA,EACrC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA,WAAW,CAAC,UAAU,OAAO,KAAK,GAAG,WAAW;AAAA,IAChD,YAAY,CAAC,UAAU,OAAO,KAAK,GAAG,YAAY,eAAe,SAAS,KAAK;AAAA,IAC/E,SAAS,CAAC,UAAU;AAClB,YAAM,SAAS,MAAM;AACrB,YAAM,OAAO,kBAAkB,kBAC3B,SACA,OAAO,UAAU,MAAM,KAAK;AAChC,UAAI,KAAM,WAAU,kBAAkB,IAAI,SAAS,IAAI,CAAC,CAAC;AAAA,IAC3D;AAAA,EACF;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  export { AdminAccountMenuProps, AdminChrome, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceNavProps, AvailabilitySectionOptions, BillingSectionOptions, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderNetworkOverview, MeetingsSectionOptions, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, applicationLifecycleAdapter, availabilitySection, billingSection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
2
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-efEMF_aD.js';
3
- export { ApiFn, BrandStyle, BrandStyleProps, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinStepRenderContext, JoinSubmitRenderContext, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, PaymentPriceLines, PaymentStep, PaymentStepProps, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, timeLabel, tzShort } from './member/index.js';
2
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-HAhNTZdl.js';
3
+ export { ApiFn, BrandStyle, BrandStyleProps, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, PaymentPriceLines, PaymentStep, PaymentStepProps, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
4
4
  import 'preact';
5
5
  import '@odla-ai/crm';
6
6
  import '@odla-ai/crm/ui';
package/dist/ui/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  PaymentStep,
5
5
  Rescheduler,
6
6
  SlotPicker,
7
+ TierPlaceholder,
7
8
  dayKey,
8
9
  dayLabel,
9
10
  fmtDate,
@@ -11,9 +12,11 @@ import {
11
12
  fullLabel,
12
13
  groupSlotsByDay,
13
14
  loadJoinResume,
15
+ tierNeedsPayment,
14
16
  timeLabel,
15
- tzShort
16
- } from "../chunk-IOHVFBXH.js";
17
+ tzShort,
18
+ useTierSelection
19
+ } from "../chunk-QWMP6TSM.js";
17
20
  import {
18
21
  AdminNote,
19
22
  AdminPage,
@@ -49,6 +52,7 @@ import {
49
52
  ChapterCopyProvider,
50
53
  useChapterCopy
51
54
  } from "../chunk-JAWTIROV.js";
55
+ import "../chunk-IVXVECKR.js";
52
56
  export {
53
57
  AdminNote,
54
58
  AdminPage,
@@ -66,6 +70,7 @@ export {
66
70
  Rescheduler,
67
71
  SlotPicker,
68
72
  ThemeWarning,
73
+ TierPlaceholder,
69
74
  adminFetch,
70
75
  adminRouteFromUrl,
71
76
  adminRouteHref,
@@ -91,9 +96,11 @@ export {
91
96
  meetingsSection,
92
97
  networkRecordsWorkspace,
93
98
  peopleSection,
99
+ tierNeedsPayment,
94
100
  timeLabel,
95
101
  tzShort,
96
102
  useAdminResource,
97
- useChapterCopy
103
+ useChapterCopy,
104
+ useTierSelection
98
105
  };
99
106
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  import * as preact from 'preact';
2
2
  import { ComponentChildren, JSX } from 'preact';
3
- import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-efEMF_aD.js';
4
- export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-efEMF_aD.js';
3
+ import { b as ChapterCopy, c as ChapterBrand } from '../../copy-context-HAhNTZdl.js';
4
+ export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-HAhNTZdl.js';
5
5
  import '@odla-ai/crm';
6
6
 
7
7
  /** A bookable slot: a start instant in epoch milliseconds. */
@@ -127,12 +127,65 @@ interface RescheduleProps {
127
127
  * rebooks on pick. Degrades to a message when scheduling is unavailable. */
128
128
  declare function Rescheduler(props: RescheduleProps): preact.JSX.Element;
129
129
 
130
+ /** One membership tier as the public join config exposes it. */
131
+ interface JoinTier {
132
+ id: string;
133
+ name: string;
134
+ priceCents: number;
135
+ blurb: string;
136
+ /** True when the tier costs nothing, so the flow skips the payment step. */
137
+ free: boolean;
138
+ }
139
+ /**
140
+ * What a site needs to render its own tier chooser.
141
+ *
142
+ * Pass `renderTiers` to take over presentation entirely — the built-in output
143
+ * is an unstyled placeholder, not a design.
144
+ */
145
+ interface JoinTierRenderContext {
146
+ /** Offered tiers, in display order. */
147
+ tiers: readonly JoinTier[];
148
+ /** The chosen tier, or null while nothing is chosen. */
149
+ selected: JoinTier | null;
150
+ selectedTierId: string | null;
151
+ /** Choose a tier. Unknown ids are ignored. */
152
+ selectTier: (tierId: string) => void;
153
+ }
154
+ /**
155
+ * Track the selected tier.
156
+ *
157
+ * A single offered tier is selected implicitly: the choice is unambiguous, so
158
+ * a chooser would be furniture. Nothing is preselected when several are on
159
+ * offer, because defaulting silently picks a price on the applicant's behalf.
160
+ */
161
+ declare function useTierSelection(tiers: readonly JoinTier[], initialTierId?: string): JoinTierRenderContext;
162
+ /**
163
+ * The unstyled fallback chooser.
164
+ *
165
+ * Deliberately plain and self-labelling: the flow stays usable before a site
166
+ * has designed anything, and it is obvious on sight that this was never meant
167
+ * to ship. Sites replace it wholesale via `renderTiers`; the `data-*` hook is
168
+ * there so a build can assert no placeholder survives into production.
169
+ */
170
+ declare function TierPlaceholder(context: JoinTierRenderContext): ComponentChildren;
171
+ /**
172
+ * Whether the flow should charge for this submission.
173
+ *
174
+ * A free tier skips payment even on a chapter otherwise wired for it. With no
175
+ * tier in play the chapter-wide readiness decides, which is how every chapter
176
+ * behaved before tiers existed.
177
+ */
178
+ declare function tierNeedsPayment(selected: JoinTier | null, paymentsReady: boolean): boolean;
179
+
130
180
  /** The public join config (the shape `GET /api/join-config` returns). */
131
181
  interface JoinConfig {
132
182
  id: string;
133
183
  name: string;
134
184
  paymentsReady: boolean;
135
185
  refundPolicyText?: string;
186
+ /** Offered membership tiers, in display order. Empty on a chapter that
187
+ * declares none, in which case no tier is posted and nothing renders. */
188
+ tiers?: readonly JoinTier[];
136
189
  /** Resolved copy for the packaged join flow. */
137
190
  copy?: ChapterCopy["join"];
138
191
  }
@@ -192,6 +245,12 @@ interface JoinIslandProps {
192
245
  }) => ComponentChildren;
193
246
  /** Replace the submit control without replacing the application form. */
194
247
  renderSubmit?: (context: JoinSubmitRenderContext) => ComponentChildren;
248
+ /** Render the site's own tier chooser. Omitted, the flow still works and
249
+ * emits an unstyled placeholder meant to be replaced, never shipped. */
250
+ renderTiers?: (context: JoinTierRenderContext) => ComponentChildren;
251
+ /** Preselect a tier, e.g. from a pricing page link. Ignored if not offered.
252
+ * With exactly one offered tier the selection is implicit and needs no UI. */
253
+ initialTierId?: string;
195
254
  /** Additional host validation gate for the submit action. */
196
255
  submitDisabled?: boolean;
197
256
  /** Stripe Elements presentation and site-owned payment content. */
@@ -250,4 +309,4 @@ interface BrandStyleProps {
250
309
  * there is no brand to theme). */
251
310
  declare function BrandStyle(props: BrandStyleProps): preact.JSX.Element | null;
252
311
 
253
- export { type ApiFn, BrandStyle, type BrandStyleProps, type JoinConfig, type JoinFlowState, JoinIsland, type JoinIslandProps, type JoinStepRenderContext, type JoinSubmitRenderContext, type MemberProvisionalRenderContext, MembersArea, type MembersAreaProps, type PaymentPriceLines, PaymentStep, type PaymentStepProps, type RescheduleProps, Rescheduler, type Slot, SlotPicker, type SlotPickerClasses, type SlotPickerProps, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, timeLabel, tzShort };
312
+ export { type ApiFn, BrandStyle, type BrandStyleProps, type JoinConfig, type JoinFlowState, JoinIsland, type JoinIslandProps, type JoinStepRenderContext, type JoinSubmitRenderContext, type JoinTier, type JoinTierRenderContext, type MemberProvisionalRenderContext, MembersArea, type MembersAreaProps, type PaymentPriceLines, PaymentStep, type PaymentStepProps, type RescheduleProps, Rescheduler, type Slot, SlotPicker, type SlotPickerClasses, type SlotPickerProps, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, tierNeedsPayment, timeLabel, tzShort, useTierSelection };
@@ -4,6 +4,7 @@ import {
4
4
  PaymentStep,
5
5
  Rescheduler,
6
6
  SlotPicker,
7
+ TierPlaceholder,
7
8
  dayKey,
8
9
  dayLabel,
9
10
  fmtDate,
@@ -11,14 +12,17 @@ import {
11
12
  fullLabel,
12
13
  groupSlotsByDay,
13
14
  loadJoinResume,
15
+ tierNeedsPayment,
14
16
  timeLabel,
15
- tzShort
16
- } from "../../chunk-IOHVFBXH.js";
17
+ tzShort,
18
+ useTierSelection
19
+ } from "../../chunk-QWMP6TSM.js";
17
20
  import {
18
21
  BrandStyle,
19
22
  ChapterCopyProvider,
20
23
  useChapterCopy
21
24
  } from "../../chunk-JAWTIROV.js";
25
+ import "../../chunk-IVXVECKR.js";
22
26
  export {
23
27
  BrandStyle,
24
28
  ChapterCopyProvider,
@@ -27,6 +31,7 @@ export {
27
31
  PaymentStep,
28
32
  Rescheduler,
29
33
  SlotPicker,
34
+ TierPlaceholder,
30
35
  dayKey,
31
36
  dayLabel,
32
37
  fmtDate,
@@ -34,8 +39,10 @@ export {
34
39
  fullLabel,
35
40
  groupSlotsByDay,
36
41
  loadJoinResume,
42
+ tierNeedsPayment,
37
43
  timeLabel,
38
44
  tzShort,
39
- useChapterCopy
45
+ useChapterCopy,
46
+ useTierSelection
40
47
  };
41
48
  //# sourceMappingURL=index.js.map