@bison-lab/payload-blocks 3.0.0 → 3.2.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 +146 -1
- package/dist/index.d.mts +156 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +856 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +64 -7
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +182 -15
- package/dist/react.mjs.map +1 -1
- package/dist/rich-text.d.mts +1 -1
- package/dist/{types-BBumyFd-.d.mts → types-CJAvoZMk.d.mts} +97 -7
- package/dist/types-CJAvoZMk.d.mts.map +1 -0
- package/package.json +2 -2
- package/dist/types-BBumyFd-.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -641,6 +641,290 @@ const NapBlock = {
|
|
|
641
641
|
]
|
|
642
642
|
};
|
|
643
643
|
//#endregion
|
|
644
|
+
//#region src/blocks/mega-menu/rem-width.ts
|
|
645
|
+
/**
|
|
646
|
+
* The one rule for a typed panel width, shared by the config's validator and
|
|
647
|
+
* the renderer's reader so the two cannot drift: if the validator accepted a
|
|
648
|
+
* value the renderer dropped, the editor would be back to a width that
|
|
649
|
+
* silently does nothing. React-free, because `config.ts` ships from the Node
|
|
650
|
+
* entry.
|
|
651
|
+
*/
|
|
652
|
+
const REM_WIDTH = /^\s*(\d+(?:\.\d+)?)\s*(rem)?\s*$/;
|
|
653
|
+
/** "30" and "30rem" both mean 30rem; anything else is not a width. */
|
|
654
|
+
function remWidth(value) {
|
|
655
|
+
const match = REM_WIDTH.exec(value ?? "");
|
|
656
|
+
return match ? `${Number(match[1])}rem` : null;
|
|
657
|
+
}
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/blocks/mega-menu/config.ts
|
|
660
|
+
/** The percent tokens a column can take, as Payload stores them. */
|
|
661
|
+
const MEGA_MENU_WIDTHS = [
|
|
662
|
+
"25",
|
|
663
|
+
"33",
|
|
664
|
+
"50",
|
|
665
|
+
"66",
|
|
666
|
+
"75",
|
|
667
|
+
"100"
|
|
668
|
+
];
|
|
669
|
+
/** Twelfths, so 33 and 66 are real thirds and 33 + 33 + 33 is a full row. */
|
|
670
|
+
const TWELFTHS = {
|
|
671
|
+
"25": 3,
|
|
672
|
+
"33": 4,
|
|
673
|
+
"50": 6,
|
|
674
|
+
"66": 8,
|
|
675
|
+
"75": 9,
|
|
676
|
+
"100": 12
|
|
677
|
+
};
|
|
678
|
+
/**
|
|
679
|
+
* A custom `validate` replaces Payload's stock array check, so the row limits
|
|
680
|
+
* are re-applied here or five columns would pass. Same messages as the stock
|
|
681
|
+
* check, through the request's translator when the admin supplies one.
|
|
682
|
+
*/
|
|
683
|
+
function rowLimits(value, { minRows, maxRows, required, req }) {
|
|
684
|
+
const count = value?.length ?? 0;
|
|
685
|
+
const t = req?.t;
|
|
686
|
+
if (required && count === 0) return t ? t("validation:required") : "This field is required.";
|
|
687
|
+
if (minRows && count < minRows) return t ? t("validation:requiresAtLeast", {
|
|
688
|
+
count: minRows,
|
|
689
|
+
label: t("general:rows")
|
|
690
|
+
}) : `This field requires at least ${minRows} rows.`;
|
|
691
|
+
if (maxRows && count > maxRows) return t ? t("validation:requiresNoMoreThan", {
|
|
692
|
+
count: maxRows,
|
|
693
|
+
label: t("general:rows")
|
|
694
|
+
}) : `This field requires no more than ${maxRows} rows.`;
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* The rule on `columns`: read as fractions, the widths must total 100%. It is
|
|
699
|
+
* the array's own `validate`, so the editor sees the message as they build,
|
|
700
|
+
* and Payload runs field validation again on publish (only draft saves skip
|
|
701
|
+
* it), so nothing publishes with a short row. Skipped when Advanced has custom
|
|
702
|
+
* widths on.
|
|
703
|
+
*/
|
|
704
|
+
const validateColumnWidths = (value, options) => {
|
|
705
|
+
const limits = rowLimits(value, options);
|
|
706
|
+
if (limits !== true) return limits;
|
|
707
|
+
if (options.siblingData?.customWidths || !value?.length) return true;
|
|
708
|
+
const twelfths = value.reduce((sum, row) => sum + TWELFTHS[row?.width ?? "100"], 0);
|
|
709
|
+
if (twelfths === 12) return true;
|
|
710
|
+
return `The columns add up to ${Math.round(twelfths / 12 * 100)}%. They need to add up to 100%.`;
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* The row `levels` segments above the field at `path`, read off the whole
|
|
714
|
+
* document. A condition on a link needs its section; a condition on a column
|
|
715
|
+
* needs its panel. `data` carries the full form, so the path is enough.
|
|
716
|
+
*/
|
|
717
|
+
function ancestor(data, path, levels) {
|
|
718
|
+
let node = data;
|
|
719
|
+
for (const segment of path.slice(0, path.length - levels)) {
|
|
720
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
721
|
+
node = node[segment];
|
|
722
|
+
}
|
|
723
|
+
return node !== null && typeof node === "object" ? node : void 0;
|
|
724
|
+
}
|
|
725
|
+
/** `links.N.variant` → its section, three segments up. */
|
|
726
|
+
const whenSectionFeatured = (data, _siblingData, { path }) => ancestor(data, path, 3)?.display === "featured";
|
|
727
|
+
/** A featured link's glyph comes from its variant, so `icon` is a list link's field. */
|
|
728
|
+
const unlessSectionFeatured = (data, siblingData, ctx) => !whenSectionFeatured(data, siblingData, ctx);
|
|
729
|
+
/** The renderer would fall back to the preset silently; the editor should hear it here instead. */
|
|
730
|
+
const validateRemWidth = (value) => !value || remWidth(value) !== null ? true : "Type a width in rem, like 30 or 30rem.";
|
|
731
|
+
/** `columns.N.width` → its panel, three segments up. */
|
|
732
|
+
const whenCustomWidths = (data, _siblingData, { path }) => Boolean(ancestor(data, path, 3)?.customWidths);
|
|
733
|
+
const unlessCustomWidths = (data, siblingData, ctx) => !whenCustomWidths(data, siblingData, ctx);
|
|
734
|
+
function approvedSelect(name, options, admin) {
|
|
735
|
+
if (!options?.length) return [];
|
|
736
|
+
return [{
|
|
737
|
+
name,
|
|
738
|
+
type: "select",
|
|
739
|
+
options,
|
|
740
|
+
admin
|
|
741
|
+
}];
|
|
742
|
+
}
|
|
743
|
+
function megaMenuBlock({ variants, icons } = {}) {
|
|
744
|
+
const linkRowFields = [
|
|
745
|
+
...linkFields({ required: true }),
|
|
746
|
+
{
|
|
747
|
+
name: "description",
|
|
748
|
+
type: "textarea",
|
|
749
|
+
admin: { description: "Supporting line under the label. Optional." }
|
|
750
|
+
},
|
|
751
|
+
...approvedSelect("variant", variants, {
|
|
752
|
+
description: "The look of this featured link.",
|
|
753
|
+
condition: whenSectionFeatured
|
|
754
|
+
}),
|
|
755
|
+
...approvedSelect("icon", icons, {
|
|
756
|
+
description: "Glyph shown before a list link. Optional.",
|
|
757
|
+
condition: unlessSectionFeatured
|
|
758
|
+
})
|
|
759
|
+
];
|
|
760
|
+
return {
|
|
761
|
+
slug: "megaMenu",
|
|
762
|
+
interfaceName: "MegaMenuBlock",
|
|
763
|
+
labels: {
|
|
764
|
+
singular: "Mega menu",
|
|
765
|
+
plural: "Mega menus"
|
|
766
|
+
},
|
|
767
|
+
fields: [
|
|
768
|
+
{
|
|
769
|
+
name: "label",
|
|
770
|
+
type: "text",
|
|
771
|
+
required: true
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "href",
|
|
775
|
+
type: "text",
|
|
776
|
+
admin: { description: "The landing page this menu opens. Optional: a trigger with no landing page is allowed." }
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: "panel",
|
|
780
|
+
type: "group",
|
|
781
|
+
fields: [
|
|
782
|
+
{
|
|
783
|
+
name: "maxWidth",
|
|
784
|
+
type: "select",
|
|
785
|
+
defaultValue: "standard",
|
|
786
|
+
options: [
|
|
787
|
+
{
|
|
788
|
+
label: "Narrow",
|
|
789
|
+
value: "narrow"
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
label: "Standard",
|
|
793
|
+
value: "standard"
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
label: "Wide",
|
|
797
|
+
value: "wide"
|
|
798
|
+
}
|
|
799
|
+
]
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
name: "columns",
|
|
803
|
+
type: "array",
|
|
804
|
+
required: true,
|
|
805
|
+
minRows: 1,
|
|
806
|
+
maxRows: 4,
|
|
807
|
+
defaultValue: emptyRows(1),
|
|
808
|
+
labels: {
|
|
809
|
+
singular: "Column",
|
|
810
|
+
plural: "Columns"
|
|
811
|
+
},
|
|
812
|
+
validate: validateColumnWidths,
|
|
813
|
+
admin: { components: { Field: MIN_ROWS_ARRAY_FIELD } },
|
|
814
|
+
fields: [
|
|
815
|
+
{
|
|
816
|
+
name: "width",
|
|
817
|
+
type: "select",
|
|
818
|
+
defaultValue: "100",
|
|
819
|
+
options: MEGA_MENU_WIDTHS.map((value) => ({
|
|
820
|
+
label: `${value}%`,
|
|
821
|
+
value
|
|
822
|
+
})),
|
|
823
|
+
admin: { condition: unlessCustomWidths }
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: "customWidth",
|
|
827
|
+
type: "text",
|
|
828
|
+
admin: {
|
|
829
|
+
description: "A CSS width: 13rem, 1fr, minmax(0, 1fr)",
|
|
830
|
+
condition: whenCustomWidths
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
name: "divider",
|
|
835
|
+
type: "checkbox",
|
|
836
|
+
label: "Draw a line on the left of this column",
|
|
837
|
+
defaultValue: false
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: "sections",
|
|
841
|
+
type: "array",
|
|
842
|
+
labels: {
|
|
843
|
+
singular: "Section",
|
|
844
|
+
plural: "Sections"
|
|
845
|
+
},
|
|
846
|
+
fields: [
|
|
847
|
+
{
|
|
848
|
+
name: "eyebrow",
|
|
849
|
+
type: "text",
|
|
850
|
+
admin: { description: "Small heading above this group; leave empty for none" }
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: "display",
|
|
854
|
+
type: "select",
|
|
855
|
+
defaultValue: "list",
|
|
856
|
+
options: [{
|
|
857
|
+
label: "Featured",
|
|
858
|
+
value: "featured"
|
|
859
|
+
}, {
|
|
860
|
+
label: "List",
|
|
861
|
+
value: "list"
|
|
862
|
+
}]
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
name: "hideDescriptionsOnMobile",
|
|
866
|
+
type: "checkbox",
|
|
867
|
+
label: "Hide descriptions on mobile",
|
|
868
|
+
defaultValue: true
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: "links",
|
|
872
|
+
type: "array",
|
|
873
|
+
labels: {
|
|
874
|
+
singular: "Link",
|
|
875
|
+
plural: "Links"
|
|
876
|
+
},
|
|
877
|
+
fields: linkRowFields
|
|
878
|
+
}
|
|
879
|
+
]
|
|
880
|
+
}
|
|
881
|
+
]
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
name: "footer",
|
|
885
|
+
type: "group",
|
|
886
|
+
admin: { description: "The strip under the columns. Leave both empty for no footer." },
|
|
887
|
+
fields: [linkField({
|
|
888
|
+
name: "overview",
|
|
889
|
+
label: "Overview link",
|
|
890
|
+
admin: { description: "The section's own landing page, so it stays reachable from the bar." }
|
|
891
|
+
}), linkField({
|
|
892
|
+
name: "cta",
|
|
893
|
+
label: "Call to action"
|
|
894
|
+
})]
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
type: "collapsible",
|
|
898
|
+
label: "Advanced",
|
|
899
|
+
admin: { initCollapsed: true },
|
|
900
|
+
fields: [{
|
|
901
|
+
name: "customWidths",
|
|
902
|
+
type: "checkbox",
|
|
903
|
+
label: "Type a CSS width for each column instead of choosing a percentage",
|
|
904
|
+
defaultValue: false
|
|
905
|
+
}, {
|
|
906
|
+
name: "customMaxWidth",
|
|
907
|
+
type: "text",
|
|
908
|
+
validate: validateRemWidth,
|
|
909
|
+
admin: { description: "Panel width in rem, e.g. 30. Leave empty to use the preset above." }
|
|
910
|
+
}]
|
|
911
|
+
}
|
|
912
|
+
]
|
|
913
|
+
}
|
|
914
|
+
]
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/** A plain bar item: label and destination, nothing to open. */
|
|
918
|
+
const LinkBlock = {
|
|
919
|
+
slug: "navLink",
|
|
920
|
+
interfaceName: "NavLinkBlock",
|
|
921
|
+
labels: {
|
|
922
|
+
singular: "Link",
|
|
923
|
+
plural: "Links"
|
|
924
|
+
},
|
|
925
|
+
fields: linkFields({ required: true })
|
|
926
|
+
};
|
|
927
|
+
//#endregion
|
|
644
928
|
//#region src/media.ts
|
|
645
929
|
function isMediaDoc(value) {
|
|
646
930
|
return typeof value === "object" && value !== null;
|
|
@@ -669,6 +953,577 @@ function resolveMedia(value) {
|
|
|
669
953
|
};
|
|
670
954
|
}
|
|
671
955
|
//#endregion
|
|
672
|
-
|
|
956
|
+
//#region src/blocks/faq-columns/sample.ts
|
|
957
|
+
const faqColumnsSample = {
|
|
958
|
+
id: "sample-faq-columns",
|
|
959
|
+
blockType: "faqColumns",
|
|
960
|
+
eyebrow: "Good to know",
|
|
961
|
+
title: "Questions patients ask before their first visit",
|
|
962
|
+
description: "If yours is not here, the front desk answers the phone between eight and six on weekdays.",
|
|
963
|
+
items: [
|
|
964
|
+
{
|
|
965
|
+
id: "sample-faq-referral",
|
|
966
|
+
question: "Do I need a referral?",
|
|
967
|
+
answer: "No. You can book directly. Some insurers ask for one before they reimburse, so check your policy if you plan to claim."
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
id: "sample-faq-sessions",
|
|
971
|
+
question: "How many sessions will I need?",
|
|
972
|
+
answer: "Most plans finish in six to eight sessions. You will get a written estimate after your assessment, and we revise it with you as you progress."
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
id: "sample-faq-wear",
|
|
976
|
+
question: "What should I wear?",
|
|
977
|
+
answer: "Anything you can move in. Shorts for a knee or hip, a vest or loose top for a shoulder or neck.\n\nThere are changing rooms if you are coming from work."
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
id: "sample-faq-insurance",
|
|
981
|
+
question: "Is treatment covered by insurance?",
|
|
982
|
+
answer: "Usually, once conservative treatment has been recommended. We invoice you directly and give you everything the insurer needs to reimburse you."
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: "sample-faq-cancel",
|
|
986
|
+
question: "What is the cancellation policy?",
|
|
987
|
+
answer: "Twenty-four hours' notice, by phone or through the booking link in your confirmation email. Later than that and the session is charged."
|
|
988
|
+
}
|
|
989
|
+
],
|
|
990
|
+
cta: {
|
|
991
|
+
title: "Still have questions?",
|
|
992
|
+
linkText: "Call the front desk",
|
|
993
|
+
href: "tel:+13035550142",
|
|
994
|
+
newTab: false
|
|
995
|
+
},
|
|
996
|
+
sticky: true,
|
|
997
|
+
type: "single",
|
|
998
|
+
collapsible: true
|
|
999
|
+
};
|
|
1000
|
+
//#endregion
|
|
1001
|
+
//#region src/sample-image.ts
|
|
1002
|
+
function escapeXml(value) {
|
|
1003
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* A placeholder upload document for a block sample.
|
|
1007
|
+
*
|
|
1008
|
+
* A preview has no site media behind it, so the image has to travel with the
|
|
1009
|
+
* sample. This is an inline SVG as a `data:` URL, in the `MediaDoc` shape
|
|
1010
|
+
* `resolveMedia` already narrows, with `width` and `height` set so an image
|
|
1011
|
+
* adapter can reserve the box. The scheme is what tells an adapter which kind
|
|
1012
|
+
* of source it has: `next/image` treats a `data:` src as `unoptimized` on its
|
|
1013
|
+
* own, so a site's adapter needs no special case for samples.
|
|
1014
|
+
*
|
|
1015
|
+
* Neutral greys rather than theme tokens: the SVG is a standalone document
|
|
1016
|
+
* and cannot see the page's custom properties.
|
|
1017
|
+
*/
|
|
1018
|
+
function sampleImage({ label, width, height, alt }) {
|
|
1019
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#d4d4d8"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui, sans-serif" font-size="${Math.max(12, Math.round(Math.min(width, height) / 12))}" fill="#52525b">${escapeXml(label)}</text></svg>`;
|
|
1020
|
+
return {
|
|
1021
|
+
url: `data:image/svg+xml,${encodeURIComponent(svg)}`,
|
|
1022
|
+
alt: alt ?? label,
|
|
1023
|
+
width,
|
|
1024
|
+
height
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/blocks/hero/sample.ts
|
|
1029
|
+
/**
|
|
1030
|
+
* The placeholder hero's sample. The hero family (BIS-45) ships a sample per
|
|
1031
|
+
* hero as each one is added; this is the one for the plain renderer.
|
|
1032
|
+
*/
|
|
1033
|
+
const heroSample = {
|
|
1034
|
+
id: "sample-hero",
|
|
1035
|
+
blockType: "hero",
|
|
1036
|
+
eyebrow: "Now booking spring appointments",
|
|
1037
|
+
heading: "Move well again, without the long wait",
|
|
1038
|
+
body: "Same-week assessments with a physiotherapist who stays with you from the first visit to the last. Most treatment plans finish in six to eight sessions.",
|
|
1039
|
+
tone: "sweep",
|
|
1040
|
+
image: sampleImage({
|
|
1041
|
+
label: "Hero background · 2000 × 1000",
|
|
1042
|
+
width: 2e3,
|
|
1043
|
+
height: 1e3,
|
|
1044
|
+
alt: "A bright, open treatment room"
|
|
1045
|
+
}),
|
|
1046
|
+
links: [{
|
|
1047
|
+
label: "Book an assessment",
|
|
1048
|
+
href: "/book",
|
|
1049
|
+
newTab: false
|
|
1050
|
+
}, {
|
|
1051
|
+
label: "See our approach",
|
|
1052
|
+
href: "https://example.com/approach",
|
|
1053
|
+
newTab: true
|
|
1054
|
+
}]
|
|
1055
|
+
};
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/blocks/mega-menu/sample.ts
|
|
1058
|
+
/**
|
|
1059
|
+
* The options the mega menu sample was written against. A factory block's
|
|
1060
|
+
* sample only fits a config built with the same approved values, so a
|
|
1061
|
+
* preview builds its block with `megaMenuBlock(megaMenuSampleOptions)`.
|
|
1062
|
+
*/
|
|
1063
|
+
const megaMenuSampleOptions = {
|
|
1064
|
+
variants: [{
|
|
1065
|
+
label: "Lumbar",
|
|
1066
|
+
value: "lumbar"
|
|
1067
|
+
}, {
|
|
1068
|
+
label: "SI joint",
|
|
1069
|
+
value: "si"
|
|
1070
|
+
}],
|
|
1071
|
+
icons: [{
|
|
1072
|
+
label: "Help",
|
|
1073
|
+
value: "help"
|
|
1074
|
+
}, {
|
|
1075
|
+
label: "Mail",
|
|
1076
|
+
value: "mail"
|
|
1077
|
+
}]
|
|
1078
|
+
};
|
|
1079
|
+
/**
|
|
1080
|
+
* Spinal Simplicity's Patients panel as a saved row: two featured territories
|
|
1081
|
+
* in a 75% column, two rail sections in a divided 25% column, overview and
|
|
1082
|
+
* CTA. Every field is set at least once; the Advanced fields are set but off,
|
|
1083
|
+
* so the percent widths are what renders.
|
|
1084
|
+
*/
|
|
1085
|
+
const megaMenuSample = {
|
|
1086
|
+
id: "sample-mega-menu",
|
|
1087
|
+
blockType: "megaMenu",
|
|
1088
|
+
label: "Patients",
|
|
1089
|
+
href: "/patients",
|
|
1090
|
+
panel: {
|
|
1091
|
+
maxWidth: "standard",
|
|
1092
|
+
columns: [{
|
|
1093
|
+
id: "treatment",
|
|
1094
|
+
width: "75",
|
|
1095
|
+
customWidth: "minmax(0, 1fr)",
|
|
1096
|
+
divider: false,
|
|
1097
|
+
sections: [{
|
|
1098
|
+
id: "territories",
|
|
1099
|
+
eyebrow: "Treatment",
|
|
1100
|
+
display: "featured",
|
|
1101
|
+
hideDescriptionsOnMobile: true,
|
|
1102
|
+
links: [{
|
|
1103
|
+
id: "lumbar",
|
|
1104
|
+
label: "Low Back Pain",
|
|
1105
|
+
href: "/patients/low-back-pain",
|
|
1106
|
+
newTab: false,
|
|
1107
|
+
description: "Persistent low back pain from lumbar instability, often with leg pain that limits standing or walking.",
|
|
1108
|
+
variant: "lumbar"
|
|
1109
|
+
}, {
|
|
1110
|
+
id: "si",
|
|
1111
|
+
label: "Hip Pain",
|
|
1112
|
+
href: "/patients/hip-pain",
|
|
1113
|
+
newTab: false,
|
|
1114
|
+
description: "Pain centered on the sacroiliac joint, often worse with sitting or climbing stairs.",
|
|
1115
|
+
variant: "si"
|
|
1116
|
+
}]
|
|
1117
|
+
}]
|
|
1118
|
+
}, {
|
|
1119
|
+
id: "rail",
|
|
1120
|
+
width: "25",
|
|
1121
|
+
customWidth: "13rem",
|
|
1122
|
+
divider: true,
|
|
1123
|
+
sections: [{
|
|
1124
|
+
id: "outcomes",
|
|
1125
|
+
eyebrow: "Patient Outcomes",
|
|
1126
|
+
display: "list",
|
|
1127
|
+
hideDescriptionsOnMobile: true,
|
|
1128
|
+
links: [
|
|
1129
|
+
{
|
|
1130
|
+
id: "stories",
|
|
1131
|
+
label: "Testimonials",
|
|
1132
|
+
href: "/patients/stories",
|
|
1133
|
+
newTab: false
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: "research",
|
|
1137
|
+
label: "Research",
|
|
1138
|
+
href: "/patients/research",
|
|
1139
|
+
newTab: false
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
id: "path",
|
|
1143
|
+
label: "Path to Relief",
|
|
1144
|
+
href: "/patients/path-to-relief",
|
|
1145
|
+
newTab: false
|
|
1146
|
+
}
|
|
1147
|
+
]
|
|
1148
|
+
}, {
|
|
1149
|
+
id: "support",
|
|
1150
|
+
eyebrow: "Support",
|
|
1151
|
+
display: "list",
|
|
1152
|
+
hideDescriptionsOnMobile: false,
|
|
1153
|
+
links: [{
|
|
1154
|
+
id: "faqs",
|
|
1155
|
+
label: "FAQs",
|
|
1156
|
+
href: "/patients/faqs",
|
|
1157
|
+
newTab: false,
|
|
1158
|
+
description: "Short answers to the questions patients ask first.",
|
|
1159
|
+
icon: "help"
|
|
1160
|
+
}, {
|
|
1161
|
+
id: "contact",
|
|
1162
|
+
label: "Contact",
|
|
1163
|
+
href: "https://example.com/contact",
|
|
1164
|
+
newTab: true,
|
|
1165
|
+
icon: "mail"
|
|
1166
|
+
}]
|
|
1167
|
+
}]
|
|
1168
|
+
}],
|
|
1169
|
+
footer: {
|
|
1170
|
+
overview: {
|
|
1171
|
+
label: "All patient resources",
|
|
1172
|
+
href: "/patients",
|
|
1173
|
+
newTab: false
|
|
1174
|
+
},
|
|
1175
|
+
cta: {
|
|
1176
|
+
label: "Find a Doctor",
|
|
1177
|
+
href: "/find-a-doctor",
|
|
1178
|
+
newTab: false
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
customWidths: false,
|
|
1182
|
+
customMaxWidth: "42rem"
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
/** A plain bar item. */
|
|
1186
|
+
const navLinkSample = {
|
|
1187
|
+
id: "sample-nav-link",
|
|
1188
|
+
blockType: "navLink",
|
|
1189
|
+
label: "Contact",
|
|
1190
|
+
href: "/contact",
|
|
1191
|
+
newTab: false
|
|
1192
|
+
};
|
|
1193
|
+
//#endregion
|
|
1194
|
+
//#region src/blocks/nap/sample.ts
|
|
1195
|
+
/**
|
|
1196
|
+
* A fictional clinic. `emitJsonLd` is off: the block emits schema.org
|
|
1197
|
+
* `LocalBusiness` by default, and a preview must not put a business that does
|
|
1198
|
+
* not exist into a site's structured data. The phone numbers are in the
|
|
1199
|
+
* 555-01xx range reserved for fiction.
|
|
1200
|
+
*/
|
|
1201
|
+
const napSample = {
|
|
1202
|
+
id: "sample-nap",
|
|
1203
|
+
blockType: "nap",
|
|
1204
|
+
businessName: "Larkspur Physiotherapy",
|
|
1205
|
+
businessType: "Physiotherapy",
|
|
1206
|
+
address: {
|
|
1207
|
+
streetAddress: "400 Larkspur Lane, Suite 120",
|
|
1208
|
+
addressLocality: "Boulder",
|
|
1209
|
+
addressRegion: "CO",
|
|
1210
|
+
postalCode: "80302",
|
|
1211
|
+
addressCountry: "US"
|
|
1212
|
+
},
|
|
1213
|
+
departments: [{
|
|
1214
|
+
id: "sample-dept-front-desk",
|
|
1215
|
+
name: "Front desk",
|
|
1216
|
+
phoneE164: "+13035550142",
|
|
1217
|
+
phoneDisplay: "(303) 555-0142"
|
|
1218
|
+
}, {
|
|
1219
|
+
id: "sample-dept-billing",
|
|
1220
|
+
name: "Billing and insurance",
|
|
1221
|
+
departmentType: "AccountingService",
|
|
1222
|
+
phoneE164: "+13035550143"
|
|
1223
|
+
}],
|
|
1224
|
+
url: "https://example.com",
|
|
1225
|
+
showName: true,
|
|
1226
|
+
headingLevel: "h2",
|
|
1227
|
+
emitJsonLd: false
|
|
1228
|
+
};
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region src/blocks/process-steps/sample.ts
|
|
1231
|
+
/** Four steps: enough to show the rail advancing through a real sequence. */
|
|
1232
|
+
const processStepsSample = {
|
|
1233
|
+
id: "sample-process-steps",
|
|
1234
|
+
blockType: "processSteps",
|
|
1235
|
+
items: [
|
|
1236
|
+
{
|
|
1237
|
+
id: "sample-step-assess",
|
|
1238
|
+
title: "Assessment",
|
|
1239
|
+
description: "A fifty-minute first visit: your history, a movement screen, and a clear explanation of what we found.",
|
|
1240
|
+
image: sampleImage({
|
|
1241
|
+
label: "Step 1 · 1600 × 1000",
|
|
1242
|
+
width: 1600,
|
|
1243
|
+
height: 1e3,
|
|
1244
|
+
alt: "A physiotherapist taking notes during an assessment"
|
|
1245
|
+
})
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
id: "sample-step-plan",
|
|
1249
|
+
title: "Your plan",
|
|
1250
|
+
description: "A written plan with the number of sessions we expect, what each one is for, and the exercises between them.",
|
|
1251
|
+
image: sampleImage({
|
|
1252
|
+
label: "Step 2 · 1600 × 1000",
|
|
1253
|
+
width: 1600,
|
|
1254
|
+
height: 1e3,
|
|
1255
|
+
alt: "A printed treatment plan on a desk"
|
|
1256
|
+
})
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
id: "sample-step-treat",
|
|
1260
|
+
title: "Treatment",
|
|
1261
|
+
description: "Hands-on work where it helps, and progressive loading where it matters. You leave every session knowing what to do next.",
|
|
1262
|
+
image: sampleImage({
|
|
1263
|
+
label: "Step 3 · 1600 × 1000",
|
|
1264
|
+
width: 1600,
|
|
1265
|
+
height: 1e3,
|
|
1266
|
+
alt: "A patient lifting a light kettlebell under supervision"
|
|
1267
|
+
})
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
id: "sample-step-discharge",
|
|
1271
|
+
title: "Discharge and beyond",
|
|
1272
|
+
description: "A final review, a maintenance programme, and an open door if anything flares up later.",
|
|
1273
|
+
image: sampleImage({
|
|
1274
|
+
label: "Step 4 · 1600 × 1000",
|
|
1275
|
+
width: 1600,
|
|
1276
|
+
height: 1e3,
|
|
1277
|
+
alt: "A patient walking out of the clinic"
|
|
1278
|
+
})
|
|
1279
|
+
}
|
|
1280
|
+
],
|
|
1281
|
+
autoAdvance: true,
|
|
1282
|
+
autoAdvanceDuration: 5e3,
|
|
1283
|
+
pauseOnHover: true,
|
|
1284
|
+
defaultActiveIndex: 0
|
|
1285
|
+
};
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region src/blocks/rich-text/sample.ts
|
|
1288
|
+
/**
|
|
1289
|
+
* A serialized Lexical document, written by hand in the shapes the default
|
|
1290
|
+
* JSX converters read (`heading`, `paragraph`, `text`, `list`, `link`).
|
|
1291
|
+
* `version` is on every serialized Lexical node, and `direction`, `format` and
|
|
1292
|
+
* `indent` are what `RichTextContent` requires on the root; the converters
|
|
1293
|
+
* read none of them, and the sample carries them so it is shaped like a row
|
|
1294
|
+
* the editor saved.
|
|
1295
|
+
*/
|
|
1296
|
+
const block = {
|
|
1297
|
+
direction: "ltr",
|
|
1298
|
+
format: "",
|
|
1299
|
+
indent: 0,
|
|
1300
|
+
version: 1
|
|
1301
|
+
};
|
|
1302
|
+
function text(value, format = 0) {
|
|
1303
|
+
return {
|
|
1304
|
+
type: "text",
|
|
1305
|
+
text: value,
|
|
1306
|
+
format,
|
|
1307
|
+
detail: 0,
|
|
1308
|
+
mode: "normal",
|
|
1309
|
+
style: "",
|
|
1310
|
+
version: 1
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function paragraph(children) {
|
|
1314
|
+
return {
|
|
1315
|
+
type: "paragraph",
|
|
1316
|
+
children,
|
|
1317
|
+
textFormat: 0,
|
|
1318
|
+
textStyle: "",
|
|
1319
|
+
...block
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const BOLD = 1;
|
|
1323
|
+
const richTextSample = {
|
|
1324
|
+
id: "sample-rich-text",
|
|
1325
|
+
blockType: "richText",
|
|
1326
|
+
content: { root: {
|
|
1327
|
+
type: "root",
|
|
1328
|
+
...block,
|
|
1329
|
+
children: [
|
|
1330
|
+
{
|
|
1331
|
+
type: "heading",
|
|
1332
|
+
tag: "h2",
|
|
1333
|
+
children: [text("What to expect at your first visit")],
|
|
1334
|
+
...block
|
|
1335
|
+
},
|
|
1336
|
+
paragraph([
|
|
1337
|
+
text("Your first appointment runs about "),
|
|
1338
|
+
text("fifty minutes", BOLD),
|
|
1339
|
+
text(". We start with a conversation about what brought you in, then a movement assessment, and you leave with a written plan and the first two exercises.")
|
|
1340
|
+
]),
|
|
1341
|
+
paragraph([
|
|
1342
|
+
text("Bring comfortable clothes and any recent imaging. If you have questions before you arrive, "),
|
|
1343
|
+
{
|
|
1344
|
+
type: "link",
|
|
1345
|
+
fields: {
|
|
1346
|
+
url: "https://example.com/contact",
|
|
1347
|
+
newTab: true,
|
|
1348
|
+
linkType: "custom"
|
|
1349
|
+
},
|
|
1350
|
+
children: [text("get in touch")],
|
|
1351
|
+
...block,
|
|
1352
|
+
version: 3
|
|
1353
|
+
},
|
|
1354
|
+
text(".")
|
|
1355
|
+
]),
|
|
1356
|
+
{
|
|
1357
|
+
type: "list",
|
|
1358
|
+
listType: "bullet",
|
|
1359
|
+
tag: "ul",
|
|
1360
|
+
start: 1,
|
|
1361
|
+
children: [
|
|
1362
|
+
"Assessment and written plan",
|
|
1363
|
+
"Hands-on treatment where it helps",
|
|
1364
|
+
"Exercises you can do at home, with video"
|
|
1365
|
+
].map((item, i) => ({
|
|
1366
|
+
type: "listitem",
|
|
1367
|
+
value: i + 1,
|
|
1368
|
+
children: [text(item)],
|
|
1369
|
+
...block
|
|
1370
|
+
})),
|
|
1371
|
+
...block
|
|
1372
|
+
}
|
|
1373
|
+
]
|
|
1374
|
+
} }
|
|
1375
|
+
};
|
|
1376
|
+
//#endregion
|
|
1377
|
+
//#region src/blocks/showcase-panels/sample.ts
|
|
1378
|
+
/** Three panels: the minimum the layout is designed around. */
|
|
1379
|
+
const showcasePanelsSample = {
|
|
1380
|
+
id: "sample-showcase-panels",
|
|
1381
|
+
blockType: "showcasePanels",
|
|
1382
|
+
items: [
|
|
1383
|
+
{
|
|
1384
|
+
id: "sample-panel-back",
|
|
1385
|
+
title: "Back and neck pain",
|
|
1386
|
+
summary: "Most back pain settles with the right movement, not rest. We find what is driving yours and build a plan around your week, not ours.",
|
|
1387
|
+
image: sampleImage({
|
|
1388
|
+
label: "Panel 1 · 1400 × 1000",
|
|
1389
|
+
width: 1400,
|
|
1390
|
+
height: 1e3,
|
|
1391
|
+
alt: "A physiotherapist guiding a patient through a stretch"
|
|
1392
|
+
}),
|
|
1393
|
+
href: "/services/back-and-neck",
|
|
1394
|
+
numeral: "01"
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
id: "sample-panel-sport",
|
|
1398
|
+
title: "Sports injuries",
|
|
1399
|
+
summary: "From a rolled ankle to a post-surgical knee: a return-to-play plan with clear milestones, so you know when you are ready.",
|
|
1400
|
+
image: sampleImage({
|
|
1401
|
+
label: "Panel 2 · 1400 × 1000",
|
|
1402
|
+
width: 1400,
|
|
1403
|
+
height: 1e3,
|
|
1404
|
+
alt: "A runner mid-stride on a track"
|
|
1405
|
+
}),
|
|
1406
|
+
href: "/services/sports-injuries"
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
id: "sample-panel-post-op",
|
|
1410
|
+
title: "Post-operative rehab",
|
|
1411
|
+
summary: "We work from your surgeon's protocol and keep them in the loop, so every stage of recovery is signed off before the next begins.",
|
|
1412
|
+
image: sampleImage({
|
|
1413
|
+
label: "Panel 3 · 1400 × 1000",
|
|
1414
|
+
width: 1400,
|
|
1415
|
+
height: 1e3,
|
|
1416
|
+
alt: "A patient on a rehabilitation bike"
|
|
1417
|
+
})
|
|
1418
|
+
}
|
|
1419
|
+
],
|
|
1420
|
+
spineVariant: "numbered",
|
|
1421
|
+
watermark: "LARKSPUR",
|
|
1422
|
+
defaultActiveIndex: 0
|
|
1423
|
+
};
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/blocks/testimonial-masonry/sample.ts
|
|
1426
|
+
function avatar(name) {
|
|
1427
|
+
return sampleImage({
|
|
1428
|
+
label: name.split(" ").map((part) => part[0]).join(""),
|
|
1429
|
+
width: 160,
|
|
1430
|
+
height: 160,
|
|
1431
|
+
alt: `Portrait of ${name}`
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/samples.ts
|
|
1436
|
+
/**
|
|
1437
|
+
* Every block the package ships, rendered without a CMS row.
|
|
1438
|
+
*
|
|
1439
|
+
* A Block library page (BIS-52) renders these live so an admin can see each
|
|
1440
|
+
* block before enabling it. Each sample lives beside its block's `config.ts`
|
|
1441
|
+
* and `component.tsx` as `sample.ts`, and `src/__tests__/samples.test.tsx`
|
|
1442
|
+
* locks the contract: one entry per slug, `blockType` matching the key, every
|
|
1443
|
+
* config field exercised at least once, and every image self-contained.
|
|
1444
|
+
*
|
|
1445
|
+
* React-free on purpose: it is read from a Global's field config, which
|
|
1446
|
+
* Payload loads in plain Node like the rest of this entry, as well as from a
|
|
1447
|
+
* route handler. Only what this package ships is here; a site supplies samples
|
|
1448
|
+
* for its own blocks through the Block library factory, whose option BIS-52
|
|
1449
|
+
* names.
|
|
1450
|
+
*/
|
|
1451
|
+
const blockSamples = {
|
|
1452
|
+
hero: heroSample,
|
|
1453
|
+
richText: richTextSample,
|
|
1454
|
+
showcasePanels: showcasePanelsSample,
|
|
1455
|
+
processSteps: processStepsSample,
|
|
1456
|
+
faqColumns: faqColumnsSample,
|
|
1457
|
+
testimonialMasonry: {
|
|
1458
|
+
id: "sample-testimonial-masonry",
|
|
1459
|
+
blockType: "testimonialMasonry",
|
|
1460
|
+
eyebrow: "From our patients",
|
|
1461
|
+
title: "What people say after they finish",
|
|
1462
|
+
description: "Every review here was left by a patient we discharged. We do not edit them.",
|
|
1463
|
+
items: [
|
|
1464
|
+
{
|
|
1465
|
+
id: "sample-quote-1",
|
|
1466
|
+
content: "I had written off running. Eight weeks later I did a parkrun, and the plan I was given actually fitted around a job and two kids.",
|
|
1467
|
+
author: {
|
|
1468
|
+
name: "Priya Natarajan",
|
|
1469
|
+
title: "Recovered from a hamstring tear",
|
|
1470
|
+
avatar: avatar("Priya Natarajan")
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
id: "sample-quote-2",
|
|
1475
|
+
content: "The first physio who explained what was wrong in words I understood.",
|
|
1476
|
+
author: {
|
|
1477
|
+
name: "Tom Ferreira",
|
|
1478
|
+
title: "Lower back pain",
|
|
1479
|
+
avatar: avatar("Tom Ferreira")
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
id: "sample-quote-3",
|
|
1484
|
+
content: "My surgeon said my knee was ahead of schedule at every check-in. That was the rehab, not me.",
|
|
1485
|
+
author: {
|
|
1486
|
+
name: "Aisha Bello",
|
|
1487
|
+
title: "ACL reconstruction"
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
id: "sample-quote-4",
|
|
1492
|
+
content: "Booked on a Tuesday, seen on the Thursday. The shoulder I had put up with for a year was sorted in six visits.",
|
|
1493
|
+
author: {
|
|
1494
|
+
name: "Marcus Whitfield",
|
|
1495
|
+
title: "Frozen shoulder",
|
|
1496
|
+
avatar: avatar("Marcus Whitfield")
|
|
1497
|
+
}
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
id: "sample-quote-5",
|
|
1501
|
+
content: "They kept my consultant in the loop the whole way through, which nobody else had bothered to do.",
|
|
1502
|
+
author: { name: "Helen Ostrowski" }
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
id: "sample-quote-6",
|
|
1506
|
+
content: "Honest about what would take time and what would not. I would send my parents here.",
|
|
1507
|
+
author: {
|
|
1508
|
+
name: "Daniel Kim",
|
|
1509
|
+
title: "Ankle sprain",
|
|
1510
|
+
avatar: avatar("Daniel Kim")
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
],
|
|
1514
|
+
link: {
|
|
1515
|
+
label: "Read every review",
|
|
1516
|
+
href: "https://example.com/reviews",
|
|
1517
|
+
newTab: true
|
|
1518
|
+
},
|
|
1519
|
+
minItemsForFade: 6,
|
|
1520
|
+
maxVisibleRows: 2
|
|
1521
|
+
},
|
|
1522
|
+
nap: napSample,
|
|
1523
|
+
megaMenu: megaMenuSample,
|
|
1524
|
+
navLink: navLinkSample
|
|
1525
|
+
};
|
|
1526
|
+
//#endregion
|
|
1527
|
+
export { FaqColumnsBlock, HeroBlock, LinkBlock, MEGA_MENU_WIDTHS, MIN_ROWS_ARRAY_FIELD, NapBlock, ProcessStepsBlock, RichTextBlock, ShowcasePanelsBlock, TestimonialMasonryBlock, blockSamples, emptyRows, headingFields, imageField, linkField, linkFields, megaMenuBlock, megaMenuSampleOptions, resolveMedia, sampleImage, validateColumnWidths, validateRemWidth };
|
|
673
1528
|
|
|
674
1529
|
//# sourceMappingURL=index.mjs.map
|