@odla-ai/chapter 0.31.3 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -2
- package/dist/{chunk-QWMP6TSM.js → chunk-BBCT7U3D.js} +282 -2
- package/dist/chunk-BBCT7U3D.js.map +1 -0
- package/dist/{copy-context-HAhNTZdl.d.ts → copy-context-BXBEdp4p.d.ts} +3 -3
- package/dist/index.cjs +363 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -5
- package/dist/index.d.ts +79 -5
- package/dist/index.js +363 -142
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -1
- package/dist/ui/member/index.d.ts +74 -3
- package/dist/ui/member/index.js +5 -1
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +3 -3
- package/dist/worker/index.d.ts +3 -3
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-QWMP6TSM.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ A foundation for **membership sites**. One `defineChapter({...})` config resolve
|
|
|
4
4
|
the shared application engine — join/apply → Stripe membership → Google booking
|
|
5
5
|
→ member area — plus an admin console and CRM, or an **admin-only hub**, on
|
|
6
6
|
odla-db + Clerk + [@odla-ai/crm](https://odla.ai/docs/packages/crm) + calendar +
|
|
7
|
-
email. The host still builds the public pages
|
|
8
|
-
|
|
7
|
+
email. The host still builds the public pages and routing. Chapter supplies the
|
|
8
|
+
application mechanics plus an optional, versioned membership-card renderer so
|
|
9
|
+
a hub preview and follower site can render the same reviewed content exactly.
|
|
9
10
|
|
|
10
11
|
```sh
|
|
11
12
|
npm install @odla-ai/chapter
|
|
@@ -255,6 +256,95 @@ CRM, payment, booking, or account logic to achieve a different brand.
|
|
|
255
256
|
|
|
256
257
|
### Join and member composition
|
|
257
258
|
|
|
259
|
+
For a centrally managed membership page, validate a bounded presentation
|
|
260
|
+
document against the live, server-authoritative tiers, then render it with
|
|
261
|
+
`MembershipCards`:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
import {
|
|
265
|
+
MEMBERSHIP_PAGE_RENDERER,
|
|
266
|
+
membershipPageDocument,
|
|
267
|
+
} from "@odla-ai/chapter";
|
|
268
|
+
import { MembershipCards } from "@odla-ai/chapter/ui/member";
|
|
269
|
+
import { ThemeScope } from "@odla-ai/ui/components";
|
|
270
|
+
|
|
271
|
+
const membershipPage = membershipPageDocument({
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
274
|
+
tiers: [
|
|
275
|
+
{
|
|
276
|
+
tierId: "steward",
|
|
277
|
+
variant: "supporting",
|
|
278
|
+
badge: "Sustains the collective",
|
|
279
|
+
benefits: ["Everything in **Founding Member**", "The annual retreat"],
|
|
280
|
+
callout: {
|
|
281
|
+
label: "A word of thanks",
|
|
282
|
+
bodyMarkdown: "*You are the reason this collective can exist.*",
|
|
283
|
+
},
|
|
284
|
+
ctaLabel: "Fund the movement",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
tierId: "founding",
|
|
288
|
+
variant: "featured",
|
|
289
|
+
badge: "Most members start here",
|
|
290
|
+
compareAtPriceCents: 100_000,
|
|
291
|
+
billingLabel: "a year",
|
|
292
|
+
capacity: { total: 100, label: "only" },
|
|
293
|
+
priceNoteMarkdown: "Your founding rate is **held for as long as you stay**.",
|
|
294
|
+
benefits: ["Live pitch nights", "A seat in diligence"],
|
|
295
|
+
ctaLabel: "Join the collective",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
tierId: "associate",
|
|
299
|
+
summaryMarkdown: "Open to everyone, anywhere",
|
|
300
|
+
benefits: ["Founder pitch recordings", "The monthly member letter"],
|
|
301
|
+
ctaLabel: "Join for free",
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
}, { tiers: joinConfig.tiers });
|
|
305
|
+
|
|
306
|
+
<ThemeScope tokens={approvedChapterTokens}>
|
|
307
|
+
<MembershipCards
|
|
308
|
+
tiers={joinConfig.tiers}
|
|
309
|
+
document={membershipPage}
|
|
310
|
+
applicationPath="/apply"
|
|
311
|
+
/>
|
|
312
|
+
</ThemeScope>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The document controls tier order and bounded copy slots. It cannot author
|
|
316
|
+
prices, free/paid state, tier identity, links, CSS, or HTML; those remain live
|
|
317
|
+
Chapter authority. Unknown fields, missing/duplicate tiers, oversized content,
|
|
318
|
+
and compare-at prices that do not exceed the charged price fail validation.
|
|
319
|
+
Markdown is rendered by the safe renderer from `@odla-ai/ui`.
|
|
320
|
+
|
|
321
|
+
`MembershipCards` is token-native, including `--ui-font-sans`,
|
|
322
|
+
`--ui-font-display`, and `--ui-font-numeral` as well as the semantic color,
|
|
323
|
+
surface, border, radius, spacing, and shadow tokens. Its responsive layout and
|
|
324
|
+
component CSS are versioned with the renderer. A CMS preview uses
|
|
325
|
+
`mode="preview"`; the public page uses the default link mode. Both therefore
|
|
326
|
+
share the same component, document, tier authority, and token scope.
|
|
327
|
+
|
|
328
|
+
The same renderer can also replace the placeholder chooser inside
|
|
329
|
+
`JoinIsland` without reimplementing selection:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
<JoinIsland
|
|
333
|
+
config={joinConfig}
|
|
334
|
+
renderTiers={({ tiers, selectedTierId, selectTier }) => (
|
|
335
|
+
<MembershipCards
|
|
336
|
+
tiers={tiers}
|
|
337
|
+
document={membershipPage}
|
|
338
|
+
mode="select"
|
|
339
|
+
selectedTierId={selectedTierId}
|
|
340
|
+
onSelectTier={selectTier}
|
|
341
|
+
/>
|
|
342
|
+
)}
|
|
343
|
+
>
|
|
344
|
+
<ApplicationFields />
|
|
345
|
+
</JoinIsland>
|
|
346
|
+
```
|
|
347
|
+
|
|
258
348
|
`JoinIsland` keeps application mechanics while exposing presentation seams:
|
|
259
349
|
|
|
260
350
|
```tsx
|
|
@@ -841,6 +931,14 @@ worker to 6 lines and deleted ~2,500 lines. The order that worked:
|
|
|
841
931
|
|
|
842
932
|
These bite silently — a smoke test won't catch them:
|
|
843
933
|
|
|
934
|
+
- **Application fields generate the application schema.** The resolved
|
|
935
|
+
`application.required` and `application.optional` lists now control both
|
|
936
|
+
submit validation and the `applications` entity emitted by
|
|
937
|
+
`defineChapter()`. Built-in fields retain their types and indexes;
|
|
938
|
+
site-defined fields become string attrs. A field removed from the reference
|
|
939
|
+
form no longer remains accidentally required at `db.transact`, and a field
|
|
940
|
+
listed in both arrays is rejected at definition time. Keep package-owned
|
|
941
|
+
operational attrs such as `status` out of both lists.
|
|
844
942
|
- **Per-field caps.** `application.defaultMaxLen` is 2000. If your form accepts
|
|
845
943
|
longer input, pass `maxLen` explicitly or the default starts rejecting it.
|
|
846
944
|
- **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
|
|
@@ -720,6 +720,284 @@ function JoinIsland(props) {
|
|
|
720
720
|
] });
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
+
// src/ui/membership-cards.tsx
|
|
724
|
+
import { Markdown } from "@odla-ai/ui/components";
|
|
725
|
+
|
|
726
|
+
// src/membership-page.ts
|
|
727
|
+
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
728
|
+
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
729
|
+
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
730
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
731
|
+
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
732
|
+
"tierId",
|
|
733
|
+
"variant",
|
|
734
|
+
"badge",
|
|
735
|
+
"summaryMarkdown",
|
|
736
|
+
"priceEyebrow",
|
|
737
|
+
"compareAtPriceCents",
|
|
738
|
+
"billingLabel",
|
|
739
|
+
"priceNoteMarkdown",
|
|
740
|
+
"capacity",
|
|
741
|
+
"benefits",
|
|
742
|
+
"callout",
|
|
743
|
+
"ctaLabel"
|
|
744
|
+
]);
|
|
745
|
+
var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
746
|
+
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
747
|
+
var MAX_TIERS = 20;
|
|
748
|
+
var MAX_BENEFITS = 24;
|
|
749
|
+
var MAX_MARKDOWN = 65536;
|
|
750
|
+
var MAX_LABEL = 160;
|
|
751
|
+
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
752
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
753
|
+
for (const key of Object.keys(value)) {
|
|
754
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function boundedString(value, path, errors, max, required = false) {
|
|
758
|
+
if (value === void 0) {
|
|
759
|
+
if (required) errors.push(`${path} is required`);
|
|
760
|
+
return void 0;
|
|
761
|
+
}
|
|
762
|
+
if (typeof value !== "string") {
|
|
763
|
+
errors.push(`${path} must be a string`);
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
const text = value.trim();
|
|
767
|
+
if (required && !text) errors.push(`${path} is required`);
|
|
768
|
+
if (text.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
769
|
+
return text;
|
|
770
|
+
}
|
|
771
|
+
function validateMembershipPageDocument(input, options = {}) {
|
|
772
|
+
const errors = [];
|
|
773
|
+
const root = objectRecord(input);
|
|
774
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
775
|
+
rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
|
|
776
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
777
|
+
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
778
|
+
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
779
|
+
}
|
|
780
|
+
if (!Array.isArray(root.tiers)) errors.push("document.tiers must be an array");
|
|
781
|
+
const rawTiers = Array.isArray(root.tiers) ? root.tiers : [];
|
|
782
|
+
if (Array.isArray(root.tiers) && rawTiers.length === 0) errors.push("document.tiers must include at least one tier");
|
|
783
|
+
if (rawTiers.length > MAX_TIERS) errors.push(`document.tiers supports at most ${MAX_TIERS} entries`);
|
|
784
|
+
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
785
|
+
const seen = /* @__PURE__ */ new Set();
|
|
786
|
+
const tiers = [];
|
|
787
|
+
for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
788
|
+
const path = `document.tiers[${index}]`;
|
|
789
|
+
const item = objectRecord(raw);
|
|
790
|
+
if (!item) {
|
|
791
|
+
errors.push(`${path} must be an object`);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
rejectUnknownKeys(item, TIER_KEYS, path, errors);
|
|
795
|
+
const tierId = boundedString(item.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
796
|
+
if (tierId && !TIER_ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
797
|
+
if (seen.has(tierId)) errors.push(`${path}.tierId duplicates "${tierId}"`);
|
|
798
|
+
seen.add(tierId);
|
|
799
|
+
if (authority.size && !authority.has(tierId)) errors.push(`${path}.tierId "${tierId}" is not an offered tier`);
|
|
800
|
+
const variant = item.variant;
|
|
801
|
+
if (variant !== void 0 && (typeof variant !== "string" || !VARIANTS.has(variant))) {
|
|
802
|
+
errors.push(`${path}.variant must be standard, featured, or supporting`);
|
|
803
|
+
}
|
|
804
|
+
const compareAt = item.compareAtPriceCents;
|
|
805
|
+
if (compareAt !== void 0 && (!Number.isInteger(compareAt) || Number(compareAt) < 0 || Number(compareAt) > 1e8)) {
|
|
806
|
+
errors.push(`${path}.compareAtPriceCents must be an integer from 0 to 100,000,000`);
|
|
807
|
+
}
|
|
808
|
+
const actual = authority.get(tierId)?.priceCents;
|
|
809
|
+
if (Number.isInteger(compareAt) && actual !== void 0 && Number(compareAt) <= actual) {
|
|
810
|
+
errors.push(`${path}.compareAtPriceCents must be greater than the authoritative tier price`);
|
|
811
|
+
}
|
|
812
|
+
let benefits;
|
|
813
|
+
if (item.benefits !== void 0) {
|
|
814
|
+
if (!Array.isArray(item.benefits)) errors.push(`${path}.benefits must be an array`);
|
|
815
|
+
else {
|
|
816
|
+
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
817
|
+
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
818
|
+
const text = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
819
|
+
return text ? [text] : [];
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
let callout;
|
|
824
|
+
if (item.callout !== void 0) {
|
|
825
|
+
const rawCallout = objectRecord(item.callout);
|
|
826
|
+
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
827
|
+
else {
|
|
828
|
+
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
829
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
|
|
830
|
+
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
831
|
+
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
let capacity;
|
|
835
|
+
if (item.capacity !== void 0) {
|
|
836
|
+
const rawCapacity = objectRecord(item.capacity);
|
|
837
|
+
if (!rawCapacity) errors.push(`${path}.capacity must be an object`);
|
|
838
|
+
else {
|
|
839
|
+
rejectUnknownKeys(rawCapacity, CAPACITY_KEYS, `${path}.capacity`, errors);
|
|
840
|
+
const total = rawCapacity.total;
|
|
841
|
+
if (!Number.isInteger(total) || Number(total) < 1 || Number(total) > 1e6) {
|
|
842
|
+
errors.push(`${path}.capacity.total must be an integer from 1 to 1,000,000`);
|
|
843
|
+
}
|
|
844
|
+
const label = boundedString(rawCapacity.label, `${path}.capacity.label`, errors, MAX_LABEL);
|
|
845
|
+
if (Number.isInteger(total) && Number(total) >= 1 && Number(total) <= 1e6) {
|
|
846
|
+
capacity = { total: Number(total), ...label ? { label } : {} };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
851
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
|
|
852
|
+
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
853
|
+
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
854
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
|
|
855
|
+
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
856
|
+
tiers.push({
|
|
857
|
+
tierId,
|
|
858
|
+
...typeof variant === "string" && VARIANTS.has(variant) ? { variant } : {},
|
|
859
|
+
...badge ? { badge } : {},
|
|
860
|
+
...summaryMarkdown ? { summaryMarkdown } : {},
|
|
861
|
+
...priceEyebrow ? { priceEyebrow } : {},
|
|
862
|
+
...Number.isInteger(compareAt) ? { compareAtPriceCents: Number(compareAt) } : {},
|
|
863
|
+
...billingLabel ? { billingLabel } : {},
|
|
864
|
+
...priceNoteMarkdown ? { priceNoteMarkdown } : {},
|
|
865
|
+
...capacity ? { capacity } : {},
|
|
866
|
+
...benefits ? { benefits } : {},
|
|
867
|
+
...callout ? { callout } : {},
|
|
868
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (authority.size) {
|
|
872
|
+
for (const tierId of authority.keys()) {
|
|
873
|
+
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
|
|
877
|
+
if (errors.length) return { ok: false, errors };
|
|
878
|
+
return {
|
|
879
|
+
ok: true,
|
|
880
|
+
value: {
|
|
881
|
+
schemaVersion: 1,
|
|
882
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
883
|
+
tiers,
|
|
884
|
+
...footnoteMarkdown ? { footnoteMarkdown } : {}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function membershipPageDocument(input, options = {}) {
|
|
889
|
+
const result = validateMembershipPageDocument(input, options);
|
|
890
|
+
if (!result.ok) throw new TypeError(`Invalid membership page document: ${result.errors.join("; ")}`);
|
|
891
|
+
return result.value;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/ui/membership-cards.tsx
|
|
895
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
|
|
896
|
+
var MEMBERSHIP_CARDS_CSS = `
|
|
897
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}]{font-family:var(--ui-font-sans,ui-sans-serif,system-ui,sans-serif);color:var(--ui-text,#17252d)}
|
|
898
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-grid{display:grid;grid-template-columns:1fr;gap:var(--ui-space-6,1.5rem);align-items:stretch}
|
|
899
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-card{position:relative;display:flex;min-width:0;flex-direction:column;padding:clamp(1.35rem,3vw,2.25rem);border:1px solid var(--ui-border,#d9e1e1);border-radius:var(--ui-radius-lg,1.25rem);background:var(--ui-surface,#fff);box-shadow:var(--ui-shadow,0 10px 30px rgb(8 31 44 / .05))}
|
|
900
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-card[data-variant=featured]{border-color:var(--ui-accent,#168c84);box-shadow:var(--ui-shadow-strong,var(--ui-shadow,0 18px 48px rgb(8 31 44 / .1)))}
|
|
901
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-badge{align-self:flex-start;margin:-2.85rem 0 1.35rem;padding:.55rem 1rem;border-radius:999px;background:var(--ui-accent,#168c84);color:var(--ui-on-accent,#fff);font-family:var(--ui-font-mono,ui-monospace,monospace);font-size:var(--ui-text-xs,.75rem);font-weight:700;letter-spacing:.12em;text-transform:uppercase}
|
|
902
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] [data-variant=supporting] .chapter-membership-badge{background:var(--ui-accent-strong,#0d3b52)}
|
|
903
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-name{margin:0;font-family:var(--ui-font-display,ui-serif,Georgia,serif);font-size:clamp(1.65rem,3vw,2.25rem);font-weight:var(--ui-font-weight-display,400);line-height:1.08;color:var(--ui-text,#17252d)}
|
|
904
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-price-row{display:flex;flex-wrap:wrap;align-items:end;gap:.45rem;margin:1.1rem 0 .7rem}
|
|
905
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-price{font-family:var(--ui-font-numeral,var(--ui-font-display,ui-serif,Georgia,serif));font-size:clamp(2rem,4vw,3rem);line-height:1;color:var(--ui-text,#17252d)}
|
|
906
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-compare{font-size:var(--ui-text-lg,1.125rem);color:var(--ui-text-muted,#66777f);text-decoration:line-through}
|
|
907
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-billing{padding-bottom:.18rem;color:var(--ui-text-muted,#66777f)}
|
|
908
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-price-eyebrow{margin:1rem 0 -.75rem;font-family:var(--ui-font-mono,ui-monospace,monospace);font-size:var(--ui-text-xs,.75rem);font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ui-accent-strong,#0d3b52)}
|
|
909
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-summary{color:var(--ui-text-muted,#66777f)}
|
|
910
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-summary.ui-markdown,[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-price-note.ui-markdown,[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-benefit.ui-markdown,[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-callout .ui-markdown{font:inherit;color:inherit}
|
|
911
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-capacity{display:grid;place-items:center;align-self:flex-start;width:4.75rem;height:4.75rem;margin:.35rem 0;border:2px solid var(--ui-accent,#168c84);border-radius:50%;color:var(--ui-accent-strong,#0d3b52)}
|
|
912
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-capacity strong{font-family:var(--ui-font-numeral,var(--ui-font-display,ui-serif,Georgia,serif));font-size:1.7rem;font-weight:400;line-height:1}
|
|
913
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-capacity small{font-family:var(--ui-font-mono,ui-monospace,monospace);font-size:.62rem;letter-spacing:.1em;text-transform:uppercase}
|
|
914
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-benefits{display:grid;gap:.8rem;margin:1.3rem 0;padding:0;list-style:none}
|
|
915
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-benefits li{display:grid;grid-template-columns:1.2rem minmax(0,1fr);gap:.55rem;align-items:start}
|
|
916
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-check{display:grid;place-items:center;width:1.15rem;height:1.15rem;margin-top:.12rem;border-radius:50%;background:var(--ui-accent-soft,#edf7fa);color:var(--ui-accent-strong,#0d3b52);font-size:.72rem;font-weight:800}
|
|
917
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-callout{margin:1rem 0;padding:1.15rem;border-radius:var(--ui-radius-md,.9rem);background:var(--ui-surface-2,#f1f6fa);color:var(--ui-text,#17252d)}
|
|
918
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-callout-label{margin:0 0 .6rem;font-family:var(--ui-font-mono,ui-monospace,monospace);font-size:var(--ui-text-xs,.75rem);font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ui-accent-strong,#0d3b52)}
|
|
919
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-action{display:inline-flex;align-items:center;justify-content:flex-start;gap:.4rem;margin-top:auto;padding:1.25rem 0 0;border:0;background:transparent;color:var(--ui-accent-strong,#0d3b52);font:inherit;font-weight:700;text-decoration:none;cursor:pointer}
|
|
920
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-action[aria-current=true]{color:var(--ui-accent,#168c84)}
|
|
921
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-action:focus-visible{outline:none;box-shadow:var(--ui-focus,0 0 0 3px var(--ui-accent-soft,#dff5f1));border-radius:var(--ui-radius-sm,.2rem)}
|
|
922
|
+
[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-footnote{margin:1.5rem auto 0;max-width:70ch;color:var(--ui-text-muted,#66777f);text-align:center}
|
|
923
|
+
@media (min-width:56rem){[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-grid{grid-template-columns:repeat(var(--chapter-membership-columns,3),minmax(0,1fr))}}
|
|
924
|
+
@media (prefers-reduced-motion:no-preference){[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-action{transition:color .16s ease,transform .16s ease}[data-chapter-membership=${MEMBERSHIP_PAGE_RENDERER}] .chapter-membership-action:hover{transform:translateX(.2rem)}}
|
|
925
|
+
`;
|
|
926
|
+
var safeApplicationPath = (value) => {
|
|
927
|
+
if (typeof value !== "string" || !/^\/(?!\/)[^\s#\\]*$/.test(value)) return "/apply";
|
|
928
|
+
try {
|
|
929
|
+
const resolved = new URL(value, "https://chapter.invalid");
|
|
930
|
+
return resolved.origin === "https://chapter.invalid" ? `${resolved.pathname}${resolved.search}` : "/apply";
|
|
931
|
+
} catch {
|
|
932
|
+
return "/apply";
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
function membershipApplicationHref(applicationPath, tierId) {
|
|
936
|
+
const path = safeApplicationPath(applicationPath);
|
|
937
|
+
return `${path}${path.includes("?") ? "&" : "?"}tier=${encodeURIComponent(tierId)}`;
|
|
938
|
+
}
|
|
939
|
+
function TierAction(props) {
|
|
940
|
+
if (props.mode === "select") {
|
|
941
|
+
return /* @__PURE__ */ jsxs8("button", { type: "button", className: "chapter-membership-action", "aria-current": props.selected ? "true" : void 0, onClick: () => props.onSelectTier?.(props.tierId), children: [
|
|
942
|
+
props.label,
|
|
943
|
+
/* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\u2192" })
|
|
944
|
+
] });
|
|
945
|
+
}
|
|
946
|
+
if (props.mode === "preview") {
|
|
947
|
+
return /* @__PURE__ */ jsxs8("span", { className: "chapter-membership-action", "aria-disabled": "true", children: [
|
|
948
|
+
props.label,
|
|
949
|
+
/* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\u2192" })
|
|
950
|
+
] });
|
|
951
|
+
}
|
|
952
|
+
return /* @__PURE__ */ jsxs8("a", { className: "chapter-membership-action", href: membershipApplicationHref(props.applicationPath, props.tierId), children: [
|
|
953
|
+
props.label,
|
|
954
|
+
/* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\u2192" })
|
|
955
|
+
] });
|
|
956
|
+
}
|
|
957
|
+
function TierCard(props) {
|
|
958
|
+
const { tier, presentation } = props;
|
|
959
|
+
const summary = presentation.summaryMarkdown ?? tier.blurb;
|
|
960
|
+
const action = presentation.ctaLabel ?? (tier.free ? "Join for free" : `Choose ${tier.name}`);
|
|
961
|
+
return /* @__PURE__ */ jsxs8("article", { className: "chapter-membership-card", "data-tier-id": tier.id, "data-variant": presentation.variant ?? "standard", children: [
|
|
962
|
+
presentation.badge ? /* @__PURE__ */ jsx8("div", { className: "chapter-membership-badge", children: presentation.badge }) : null,
|
|
963
|
+
/* @__PURE__ */ jsx8("h3", { className: "chapter-membership-name", children: tier.name }),
|
|
964
|
+
presentation.priceEyebrow ? /* @__PURE__ */ jsx8("p", { className: "chapter-membership-price-eyebrow", children: presentation.priceEyebrow }) : null,
|
|
965
|
+
presentation.capacity ? /* @__PURE__ */ jsxs8("div", { className: "chapter-membership-capacity", "aria-label": `${presentation.capacity.total} ${presentation.capacity.label ?? "available"}`, children: [
|
|
966
|
+
/* @__PURE__ */ jsx8("strong", { children: presentation.capacity.total.toLocaleString("en-US") }),
|
|
967
|
+
/* @__PURE__ */ jsx8("small", { children: presentation.capacity.label ?? "available" })
|
|
968
|
+
] }) : null,
|
|
969
|
+
/* @__PURE__ */ jsxs8("div", { className: "chapter-membership-price-row", children: [
|
|
970
|
+
presentation.compareAtPriceCents !== void 0 ? /* @__PURE__ */ jsx8("span", { className: "chapter-membership-compare", children: fmtMoney(presentation.compareAtPriceCents) }) : null,
|
|
971
|
+
/* @__PURE__ */ jsx8("span", { className: "chapter-membership-price", children: tier.free ? "Free" : fmtMoney(tier.priceCents) }),
|
|
972
|
+
!tier.free && presentation.billingLabel ? /* @__PURE__ */ jsx8("span", { className: "chapter-membership-billing", children: presentation.billingLabel }) : null
|
|
973
|
+
] }),
|
|
974
|
+
presentation.priceNoteMarkdown ? /* @__PURE__ */ jsx8(Markdown, { className: "chapter-membership-price-note", source: presentation.priceNoteMarkdown }) : null,
|
|
975
|
+
summary ? /* @__PURE__ */ jsx8(Markdown, { className: "chapter-membership-summary", source: summary }) : null,
|
|
976
|
+
presentation.benefits?.length ? /* @__PURE__ */ jsx8("ul", { className: "chapter-membership-benefits", children: presentation.benefits.map((benefit, index) => /* @__PURE__ */ jsxs8("li", { children: [
|
|
977
|
+
/* @__PURE__ */ jsx8("span", { className: "chapter-membership-check", "aria-hidden": "true", children: "\u2713" }),
|
|
978
|
+
/* @__PURE__ */ jsx8(Markdown, { className: "chapter-membership-benefit", source: benefit })
|
|
979
|
+
] }, `${tier.id}-benefit-${index}`)) }) : null,
|
|
980
|
+
presentation.callout ? /* @__PURE__ */ jsxs8("aside", { className: "chapter-membership-callout", children: [
|
|
981
|
+
presentation.callout.label ? /* @__PURE__ */ jsx8("p", { className: "chapter-membership-callout-label", children: presentation.callout.label }) : null,
|
|
982
|
+
/* @__PURE__ */ jsx8(Markdown, { source: presentation.callout.bodyMarkdown })
|
|
983
|
+
] }) : null,
|
|
984
|
+
/* @__PURE__ */ jsx8(TierAction, { tierId: tier.id, label: action, mode: props.mode, selected: props.selected, applicationPath: props.applicationPath, onSelectTier: props.onSelectTier })
|
|
985
|
+
] });
|
|
986
|
+
}
|
|
987
|
+
function MembershipCards(props) {
|
|
988
|
+
const document2 = membershipPageDocument(props.document, { tiers: props.tiers });
|
|
989
|
+
const byId = new Map(props.tiers.map((tier) => [tier.id, tier]));
|
|
990
|
+
const mode = props.mode ?? "link";
|
|
991
|
+
return /* @__PURE__ */ jsxs8("section", { className: ["chapter-membership", props.className].filter(Boolean).join(" "), "data-chapter-membership": MEMBERSHIP_PAGE_RENDERER, "aria-label": props.ariaLabel ?? "Membership options", children: [
|
|
992
|
+
/* @__PURE__ */ jsx8("style", { children: MEMBERSHIP_CARDS_CSS }),
|
|
993
|
+
/* @__PURE__ */ jsx8("div", { className: "chapter-membership-grid", style: { "--chapter-membership-columns": String(Math.min(document2.tiers.length, 3)) }, children: document2.tiers.map((presentation) => {
|
|
994
|
+
const tier = byId.get(presentation.tierId);
|
|
995
|
+
return /* @__PURE__ */ jsx8(TierCard, { tier, presentation, mode, selected: props.selectedTierId === tier.id, applicationPath: props.applicationPath, onSelectTier: props.onSelectTier }, tier.id);
|
|
996
|
+
}) }),
|
|
997
|
+
document2.footnoteMarkdown ? /* @__PURE__ */ jsx8(Markdown, { className: "chapter-membership-footnote", source: document2.footnoteMarkdown }) : null
|
|
998
|
+
] });
|
|
999
|
+
}
|
|
1000
|
+
|
|
723
1001
|
export {
|
|
724
1002
|
tzShort,
|
|
725
1003
|
dayKey,
|
|
@@ -737,6 +1015,8 @@ export {
|
|
|
737
1015
|
TierPlaceholder,
|
|
738
1016
|
tierNeedsPayment,
|
|
739
1017
|
loadJoinResume,
|
|
740
|
-
JoinIsland
|
|
1018
|
+
JoinIsland,
|
|
1019
|
+
membershipApplicationHref,
|
|
1020
|
+
MembershipCards
|
|
741
1021
|
};
|
|
742
|
-
//# sourceMappingURL=chunk-
|
|
1022
|
+
//# sourceMappingURL=chunk-BBCT7U3D.js.map
|