simple-module-pagebuilder 0.0.1__py3-none-any.whl
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.
- pagebuilder/__init__.py +0 -0
- pagebuilder/components/DiffSummary.tsx +107 -0
- pagebuilder/components/ScheduledBadge.tsx +34 -0
- pagebuilder/components/StatusBadge.tsx +21 -0
- pagebuilder/components/blockRegistry.test.ts +56 -0
- pagebuilder/components/blockRegistry.ts +80 -0
- pagebuilder/components/blocks/Button.tsx +142 -0
- pagebuilder/components/blocks/Columns.tsx +64 -0
- pagebuilder/components/blocks/Heading.tsx +92 -0
- pagebuilder/components/blocks/Image.tsx +125 -0
- pagebuilder/components/blocks/MediaPicker.tsx +178 -0
- pagebuilder/components/blocks/Spacer.tsx +31 -0
- pagebuilder/components/blocks/Text.tsx +80 -0
- pagebuilder/components/editor/PageEditorToolbar.tsx +169 -0
- pagebuilder/components/editor/RevisionHistoryPanel.tsx +95 -0
- pagebuilder/components/editor/SchedulePanel.tsx +85 -0
- pagebuilder/components/editor/SeoSettingsPanel.tsx +119 -0
- pagebuilder/components/layoutPuckConfig.tsx +109 -0
- pagebuilder/components/media/MediaFilters.tsx +83 -0
- pagebuilder/components/media/MediaFolderSidebar.tsx +97 -0
- pagebuilder/components/media/MediaGrid.tsx +76 -0
- pagebuilder/components/media/MediaHeader.tsx +37 -0
- pagebuilder/components/media/MediaUploadQueue.tsx +103 -0
- pagebuilder/components/media/types.ts +20 -0
- pagebuilder/components/migrateContent.test.ts +146 -0
- pagebuilder/components/migrateContent.ts +135 -0
- pagebuilder/components/puckConfig.test.tsx +118 -0
- pagebuilder/components/puckConfig.tsx +121 -0
- pagebuilder/components/widgetCatalog.test.tsx +123 -0
- pagebuilder/components/widgetCatalog.ts +208 -0
- pagebuilder/components/widgets/_internal/accent-text.tsx +60 -0
- pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +14 -0
- pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +71 -0
- pagebuilder/components/widgets/_internal/link-arrow.tsx +23 -0
- pagebuilder/components/widgets/_internal/rich-text.tsx +243 -0
- pagebuilder/components/widgets/_internal/solid-icons.tsx +73 -0
- pagebuilder/components/widgets/_shared/background-media.tsx +74 -0
- pagebuilder/components/widgets/_shared/content-media.tsx +43 -0
- pagebuilder/components/widgets/_shared/cta-button.tsx +69 -0
- pagebuilder/components/widgets/_shared/empty-placeholder.tsx +15 -0
- pagebuilder/components/widgets/_shared/eyebrow-text.tsx +27 -0
- pagebuilder/components/widgets/_shared/heading.tsx +88 -0
- pagebuilder/components/widgets/_shared/index.ts +39 -0
- pagebuilder/components/widgets/_shared/mask.ts +54 -0
- pagebuilder/components/widgets/_shared/section.tsx +60 -0
- pagebuilder/components/widgets/_shared/social-icon.tsx +71 -0
- pagebuilder/components/widgets/_shared/text-link.tsx +47 -0
- pagebuilder/components/widgets/accordion-widget.tsx +64 -0
- pagebuilder/components/widgets/alert-widget.tsx +47 -0
- pagebuilder/components/widgets/app-store-badges-widget.tsx +91 -0
- pagebuilder/components/widgets/article-cards-render.tsx +132 -0
- pagebuilder/components/widgets/article-cards-widget.tsx +72 -0
- pagebuilder/components/widgets/call-to-action-render.tsx +285 -0
- pagebuilder/components/widgets/call-to-action-styles.ts +28 -0
- pagebuilder/components/widgets/call-to-action-widget.tsx +83 -0
- pagebuilder/components/widgets/carousel-widget.tsx +97 -0
- pagebuilder/components/widgets/centered-hero-widget.tsx +54 -0
- pagebuilder/components/widgets/colored-action-list-widget.tsx +117 -0
- pagebuilder/components/widgets/column-widget.tsx +63 -0
- pagebuilder/components/widgets/contact-cards-widget.tsx +172 -0
- pagebuilder/components/widgets/contact-form-render.tsx +293 -0
- pagebuilder/components/widgets/contact-form-widget.tsx +69 -0
- pagebuilder/components/widgets/container-widget.tsx +93 -0
- pagebuilder/components/widgets/definition-list-widget.tsx +144 -0
- pagebuilder/components/widgets/divider-widget.tsx +56 -0
- pagebuilder/components/widgets/eyebrow-section-widget.tsx +56 -0
- pagebuilder/components/widgets/faq-render.tsx +268 -0
- pagebuilder/components/widgets/faq-widget.tsx +64 -0
- pagebuilder/components/widgets/feature-card-widget.tsx +98 -0
- pagebuilder/components/widgets/feature-cards-render.tsx +258 -0
- pagebuilder/components/widgets/feature-cards-styles.ts +107 -0
- pagebuilder/components/widgets/feature-cards-widget.tsx +133 -0
- pagebuilder/components/widgets/feature-grid-widget.tsx +118 -0
- pagebuilder/components/widgets/feature-showcase-widget.tsx +118 -0
- pagebuilder/components/widgets/gallery-widget.tsx +139 -0
- pagebuilder/components/widgets/grid-widget.tsx +59 -0
- pagebuilder/components/widgets/hero-widget.tsx +292 -0
- pagebuilder/components/widgets/html-widget.tsx +42 -0
- pagebuilder/components/widgets/iframe-widget.tsx +44 -0
- pagebuilder/components/widgets/leaderboard-widget.tsx +203 -0
- pagebuilder/components/widgets/list-widget.tsx +56 -0
- pagebuilder/components/widgets/logo-cloud-widget.tsx +127 -0
- pagebuilder/components/widgets/markdown-widget.tsx +148 -0
- pagebuilder/components/widgets/media-object-layout.ts +156 -0
- pagebuilder/components/widgets/media-object-render.tsx +284 -0
- pagebuilder/components/widgets/media-object-text.tsx +202 -0
- pagebuilder/components/widgets/media-object-widget.tsx +176 -0
- pagebuilder/components/widgets/newsletter-widget.tsx +217 -0
- pagebuilder/components/widgets/page-header-widget.tsx +118 -0
- pagebuilder/components/widgets/prose-section-widget.tsx +99 -0
- pagebuilder/components/widgets/quote-widget.tsx +51 -0
- pagebuilder/components/widgets/row-widget.tsx +74 -0
- pagebuilder/components/widgets/section-intro-widget.tsx +143 -0
- pagebuilder/components/widgets/signup-banner-widget.tsx +50 -0
- pagebuilder/components/widgets/simple-action-list-widget.tsx +91 -0
- pagebuilder/components/widgets/site-footer-widget.tsx +97 -0
- pagebuilder/components/widgets/site-header-nav.tsx +153 -0
- pagebuilder/components/widgets/site-header-widget.tsx +182 -0
- pagebuilder/components/widgets/social-banner-widget.tsx +83 -0
- pagebuilder/components/widgets/stats-widget.tsx +200 -0
- pagebuilder/components/widgets/steps-widget.tsx +211 -0
- pagebuilder/components/widgets/story-cards-widget.tsx +182 -0
- pagebuilder/components/widgets/table-widget.tsx +86 -0
- pagebuilder/components/widgets/tabs-widget.tsx +95 -0
- pagebuilder/components/widgets/tags-widget.tsx +85 -0
- pagebuilder/components/widgets/testimonial-widget.tsx +102 -0
- pagebuilder/components/widgets/timeline-layout.ts +73 -0
- pagebuilder/components/widgets/timeline-render.tsx +294 -0
- pagebuilder/components/widgets/timeline-widget.tsx +106 -0
- pagebuilder/components/widgets/under-construction-widget.tsx +40 -0
- pagebuilder/components/widgets/video-widget.tsx +103 -0
- pagebuilder/components/widgets/welcome-widget.tsx +38 -0
- pagebuilder/contracts/__init__.py +57 -0
- pagebuilder/contracts/events.py +25 -0
- pagebuilder/contracts/schemas.py +239 -0
- pagebuilder/conversion/blocks-to-html.test.ts +105 -0
- pagebuilder/conversion/blocks-to-html.ts +102 -0
- pagebuilder/conversion/index.ts +2 -0
- pagebuilder/conversion/types.ts +5 -0
- pagebuilder/deps.py +26 -0
- pagebuilder/diff.py +129 -0
- pagebuilder/endpoints/__init__.py +0 -0
- pagebuilder/endpoints/api/__init__.py +21 -0
- pagebuilder/endpoints/api/_deps.py +21 -0
- pagebuilder/endpoints/api/layout.py +78 -0
- pagebuilder/endpoints/api/pages.py +85 -0
- pagebuilder/endpoints/api/revisions.py +79 -0
- pagebuilder/endpoints/api/uploads.py +92 -0
- pagebuilder/endpoints/api/workflow.py +124 -0
- pagebuilder/endpoints/seo.py +95 -0
- pagebuilder/endpoints/views.py +228 -0
- pagebuilder/fields/checkbox-field.tsx +22 -0
- pagebuilder/fields/file-picker/file-gallery.tsx +152 -0
- pagebuilder/fields/file-picker/image-picker-field.tsx +214 -0
- pagebuilder/fields/file-picker/index.ts +6 -0
- pagebuilder/fields/file-picker/types.ts +15 -0
- pagebuilder/fields/index.ts +9 -0
- pagebuilder/fields/media-adapter.ts +47 -0
- pagebuilder/hooks/useAutosave.ts +134 -0
- pagebuilder/hooks/useEditorForm.ts +193 -0
- pagebuilder/hooks/useMediaUploads.ts +96 -0
- pagebuilder/hooks/usePageRevisions.ts +59 -0
- pagebuilder/hooks/usePageSchedule.ts +72 -0
- pagebuilder/hooks/usePageWorkflow.ts +173 -0
- pagebuilder/layout_service.py +101 -0
- pagebuilder/media_images.py +185 -0
- pagebuilder/media_queries.py +92 -0
- pagebuilder/media_service.py +222 -0
- pagebuilder/models.py +209 -0
- pagebuilder/module.py +279 -0
- pagebuilder/package.json +18 -0
- pagebuilder/pages/LayoutEditor.tsx +183 -0
- pagebuilder/pages/MediaLibrary.tsx +249 -0
- pagebuilder/pages/PageEditor.tsx +181 -0
- pagebuilder/pages/PageList.tsx +129 -0
- pagebuilder/pages/PendingReview.tsx +128 -0
- pagebuilder/pages/PublicPage.tsx +126 -0
- pagebuilder/permissions.py +44 -0
- pagebuilder/py.typed +0 -0
- pagebuilder/security.py +230 -0
- pagebuilder/service/__init__.py +141 -0
- pagebuilder/service/_common.py +24 -0
- pagebuilder/service/_revisions.py +92 -0
- pagebuilder/service/_workflow.py +207 -0
- pagebuilder/services.py +19 -0
- pagebuilder/settings.py +161 -0
- pagebuilder/static/widgets-base.css +57 -0
- pagebuilder/translation/apply.ts +54 -0
- pagebuilder/translation/extract.ts +141 -0
- pagebuilder/translation/index.ts +20 -0
- pagebuilder/translation/translatable-fields.ts +65 -0
- pagebuilder/translation/translation.test.ts +167 -0
- pagebuilder/utils/api.ts +12 -0
- pagebuilder/utils/datetime.ts +25 -0
- pagebuilder/utils/decorative-image.ts +15 -0
- pagebuilder/utils/editorSnapshot.ts +39 -0
- pagebuilder/utils/markdown.ts +160 -0
- pagebuilder/utils/mediaApi.ts +112 -0
- pagebuilder/utils/mediaFormat.ts +19 -0
- pagebuilder/utils/pagesApi.ts +111 -0
- pagebuilder/utils/parse-list.ts +7 -0
- pagebuilder/utils/request.ts +52 -0
- pagebuilder/utils/slugify.ts +9 -0
- pagebuilder/utils/types.ts +171 -0
- pagebuilder/utils/widgetUtils.ts +11 -0
- simple_module_pagebuilder-0.0.1.dist-info/METADATA +152 -0
- simple_module_pagebuilder-0.0.1.dist-info/RECORD +189 -0
- simple_module_pagebuilder-0.0.1.dist-info/WHEEL +4 -0
- simple_module_pagebuilder-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
BackgroundMediaFallback,
|
|
3
|
+
BackgroundMediaOverlay,
|
|
4
|
+
BackgroundMediaProps,
|
|
5
|
+
} from './background-media';
|
|
6
|
+
export { BackgroundMedia } from './background-media';
|
|
7
|
+
export type {
|
|
8
|
+
ContentMediaAspect,
|
|
9
|
+
ContentMediaProps,
|
|
10
|
+
} from './content-media';
|
|
11
|
+
export { ContentMedia } from './content-media';
|
|
12
|
+
export type {
|
|
13
|
+
CTAButtonProps,
|
|
14
|
+
CTAButtonSize,
|
|
15
|
+
CTAButtonVariant,
|
|
16
|
+
} from './cta-button';
|
|
17
|
+
export { CTAButton } from './cta-button';
|
|
18
|
+
export type { EmptyPlaceholderProps } from './empty-placeholder';
|
|
19
|
+
export { EmptyPlaceholder } from './empty-placeholder';
|
|
20
|
+
export type { EyebrowTextProps, EyebrowTone } from './eyebrow-text';
|
|
21
|
+
export { EyebrowText } from './eyebrow-text';
|
|
22
|
+
export type {
|
|
23
|
+
HeadingLevel,
|
|
24
|
+
HeadingProps,
|
|
25
|
+
HeadingSize,
|
|
26
|
+
HeadingTone,
|
|
27
|
+
} from './heading';
|
|
28
|
+
export { Heading } from './heading';
|
|
29
|
+
export {
|
|
30
|
+
EYEBROW_CASE_STYLE,
|
|
31
|
+
eyebrowTextStyle,
|
|
32
|
+
maskStyle,
|
|
33
|
+
SURFACE_CONTRAST,
|
|
34
|
+
} from './mask';
|
|
35
|
+
export type { SectionProps, SectionSpacing, SectionVariant } from './section';
|
|
36
|
+
export { Section } from './section';
|
|
37
|
+
export type { SocialIconName } from './social-icon';
|
|
38
|
+
export { SOCIAL_ICON_NAMES, SOCIAL_ICON_OPTIONS, SocialIcon } from './social-icon';
|
|
39
|
+
export { TextLink } from './text-link';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An organic mask clips an image to a content-supplied SVG shape (e.g. the
|
|
5
|
+
* BioGarden blob/egg shapes). The -webkit- prefixed keys keep Safari/iOS in
|
|
6
|
+
* sync with the standard properties. Call once per widget render (not per
|
|
7
|
+
* item) and share the returned object across mapped items.
|
|
8
|
+
*/
|
|
9
|
+
export function maskStyle(url: string): CSSProperties {
|
|
10
|
+
return {
|
|
11
|
+
maskImage: `url(${url})`,
|
|
12
|
+
maskSize: '100% 100%',
|
|
13
|
+
maskRepeat: 'no-repeat',
|
|
14
|
+
WebkitMaskImage: `url(${url})`,
|
|
15
|
+
WebkitMaskSize: '100% 100%',
|
|
16
|
+
WebkitMaskRepeat: 'no-repeat',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Text color for content sitting on a colored band/card surface
|
|
22
|
+
* (`surfaceColor`/`cardBg`). Tenants override `--pb-surface-contrast` when
|
|
23
|
+
* their brand colors need dark-on-light contrast instead of white.
|
|
24
|
+
*/
|
|
25
|
+
export const SURFACE_CONTRAST = 'var(--pb-surface-contrast, #ffffff)';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Theme-driven eyebrow case/tracking (`--pb-eyebrow-transform` /
|
|
29
|
+
* `--pb-eyebrow-tracking`). Defaults keep eyebrows sentence-case; a tenant
|
|
30
|
+
* sets `uppercase` + a tracking value for the small-caps eyebrow look.
|
|
31
|
+
*/
|
|
32
|
+
export const EYEBROW_CASE_STYLE: CSSProperties = {
|
|
33
|
+
textTransform: 'var(--pb-eyebrow-transform, none)' as CSSProperties['textTransform'],
|
|
34
|
+
letterSpacing: 'var(--pb-eyebrow-tracking, normal)',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The ONE eyebrow ("subtitle") type spec, shared by every widget that renders
|
|
39
|
+
* one — MediaObject (inline and banner), EyebrowSplitSection, Timeline (both
|
|
40
|
+
* variants) and FeatureCards. Each of those used to hardcode its own size and
|
|
41
|
+
* weight, so a single tenant showed four different eyebrows across one page.
|
|
42
|
+
*
|
|
43
|
+
* `size`/`weight` are the caller's PREVIOUS literal values and are used only
|
|
44
|
+
* as the `var()` fallback, so a tenant that sets neither token renders exactly
|
|
45
|
+
* as before; a tenant that sets `--pb-eyebrow-size`/`--pb-eyebrow-weight`
|
|
46
|
+
* (BioGarden: 16px Medium) gets one consistent eyebrow everywhere.
|
|
47
|
+
*/
|
|
48
|
+
export function eyebrowTextStyle(size = '0.875rem', weight = '500'): CSSProperties {
|
|
49
|
+
return {
|
|
50
|
+
...EYEBROW_CASE_STYLE,
|
|
51
|
+
fontSize: `var(--pb-eyebrow-size, ${size})`,
|
|
52
|
+
fontWeight: `var(--pb-eyebrow-weight, ${weight})` as CSSProperties['fontWeight'],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../../../utils/widgetUtils';
|
|
3
|
+
|
|
4
|
+
export type SectionVariant = 'default' | 'muted' | 'accent' | 'image-overlay';
|
|
5
|
+
export type SectionSpacing = 'compact' | 'default' | 'loose';
|
|
6
|
+
|
|
7
|
+
export type SectionProps = {
|
|
8
|
+
variant: SectionVariant;
|
|
9
|
+
spacing: SectionSpacing;
|
|
10
|
+
align?: 'left' | 'center';
|
|
11
|
+
as?: 'section' | 'header';
|
|
12
|
+
className?: string;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
} & Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'children'>;
|
|
15
|
+
|
|
16
|
+
const VARIANT_CLASSES: Record<SectionVariant, string> = {
|
|
17
|
+
default: '',
|
|
18
|
+
muted: 'bg-gray-50 dark:bg-gray-900/40',
|
|
19
|
+
accent: 'bg-primary-800 text-white rounded-xl',
|
|
20
|
+
'image-overlay': 'relative isolate overflow-hidden rounded-xl bg-slate-900 text-white',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Vertical section rhythm is a component-level, tenant-tunable token. A tenant
|
|
24
|
+
// (e.g. Mowing) sets `--pb-section-py` to one value so every section shares a
|
|
25
|
+
// consistent rhythm; tenants that don't set it keep these per-variant fallbacks
|
|
26
|
+
// (the original py-8/12 · py-12/16 · py-16/24 scale), so GCA/Recodo are unchanged.
|
|
27
|
+
const SPACING_CLASSES: Record<SectionSpacing, string> = {
|
|
28
|
+
compact: 'py-[var(--pb-section-py,2rem)] lg:py-[var(--pb-section-py,3rem)]',
|
|
29
|
+
default: 'py-[var(--pb-section-py,3rem)] lg:py-[var(--pb-section-py,4rem)]',
|
|
30
|
+
loose: 'py-[var(--pb-section-py,4rem)] lg:py-[var(--pb-section-py,6rem)]',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function Section({
|
|
34
|
+
variant,
|
|
35
|
+
spacing,
|
|
36
|
+
align,
|
|
37
|
+
as = 'section',
|
|
38
|
+
className,
|
|
39
|
+
children,
|
|
40
|
+
...rest
|
|
41
|
+
}: SectionProps) {
|
|
42
|
+
const Tag = as;
|
|
43
|
+
return (
|
|
44
|
+
<Tag
|
|
45
|
+
{...rest}
|
|
46
|
+
className={cn(
|
|
47
|
+
// Same responsive gutters as every other widget container so all
|
|
48
|
+
// sections share one grid edge (px-4 alone left Section content
|
|
49
|
+
// 16px wider than its siblings at lg).
|
|
50
|
+
'container mx-auto px-4 sm:px-6 lg:px-8',
|
|
51
|
+
VARIANT_CLASSES[variant],
|
|
52
|
+
SPACING_CLASSES[spacing],
|
|
53
|
+
align === 'center' && 'text-center',
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</Tag>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Mail } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Icons the site header's utility bar can show in place of a link label.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately small: lucide 1.x removed its brand glyphs, so anything here
|
|
7
|
+
* has to be vendored below with its licence. Adding one means finding a
|
|
8
|
+
* correctly-licensed path, not approximating it.
|
|
9
|
+
*/
|
|
10
|
+
export const SOCIAL_ICON_NAMES = ['bluesky', 'linkedin', 'email'] as const;
|
|
11
|
+
|
|
12
|
+
export type SocialIconName = (typeof SOCIAL_ICON_NAMES)[number];
|
|
13
|
+
|
|
14
|
+
export const SOCIAL_ICON_OPTIONS = [
|
|
15
|
+
{ label: 'BlueSky', value: 'bluesky' },
|
|
16
|
+
{ label: 'LinkedIn', value: 'linkedin' },
|
|
17
|
+
{ label: 'Email', value: 'email' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* BlueSky butterfly. Glyph: Font Awesome Free 6.7.2 `brands/bluesky`
|
|
22
|
+
* (https://fontawesome.com — CC BY 4.0), path data unmodified.
|
|
23
|
+
*/
|
|
24
|
+
function BlueskyIcon({ className }: { className?: string }) {
|
|
25
|
+
return (
|
|
26
|
+
<svg
|
|
27
|
+
viewBox="0 0 512 512"
|
|
28
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
29
|
+
fill="currentColor"
|
|
30
|
+
className={className}
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
{/* No <title>: the svg is aria-hidden (the link carries the label), and a
|
|
34
|
+
title would pop a stray native tooltip on hover. */}
|
|
35
|
+
<path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z" />
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* LinkedIn mark. Geometry: lucide `linkedin` (ISC), which lucide 1.x dropped
|
|
42
|
+
* along with the rest of its brand set — vendored rather than pinned back, so
|
|
43
|
+
* the module keeps up with lucide. Filled rather than stroked, matching the
|
|
44
|
+
* design system's solid social strip.
|
|
45
|
+
*/
|
|
46
|
+
function LinkedinIcon({ className }: { className?: string }) {
|
|
47
|
+
return (
|
|
48
|
+
<svg
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
51
|
+
fill="currentColor"
|
|
52
|
+
className={className}
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
>
|
|
55
|
+
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
|
|
56
|
+
<rect width="4" height="12" x="2" y="9" />
|
|
57
|
+
<circle cx="4" cy="4" r="2" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function SocialIcon({ name, className }: { name: SocialIconName; className?: string }) {
|
|
63
|
+
switch (name) {
|
|
64
|
+
case 'bluesky':
|
|
65
|
+
return <BlueskyIcon className={className} />;
|
|
66
|
+
case 'linkedin':
|
|
67
|
+
return <LinkedinIcon className={className} />;
|
|
68
|
+
default:
|
|
69
|
+
return <Mail className={className} aria-hidden="true" />;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import { cn } from '../../../utils/widgetUtils';
|
|
3
|
+
import { LinkArrow } from '../_internal/link-arrow';
|
|
4
|
+
import { renderRichText } from '../_internal/rich-text';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inline text link with a trailing arrow ("View all", "Learn more", …), themed
|
|
8
|
+
* via the tenant link tokens: `--pb-link-color` / `--pb-link-hover-color` /
|
|
9
|
+
* `--pb-link-hover-decoration` / `--pb-link-size` / `--pb-link-weight`
|
|
10
|
+
* (GCA: ink 18px SemiBold, teal hover, no underline). The fallbacks reproduce
|
|
11
|
+
* the prior widget styling (accent-coloured 14px medium, underline on hover)
|
|
12
|
+
* so tenants that don't set the tokens render unchanged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Hoisted: a fresh style object per render would defeat React's prop-equality
|
|
16
|
+
// fast-path (see the same pattern on MESH_STYLE in _shared/background-media).
|
|
17
|
+
const LINK_TEXT_STYLE: CSSProperties = {
|
|
18
|
+
fontSize: 'var(--pb-link-size, 0.875rem)',
|
|
19
|
+
fontWeight: 'var(--pb-link-weight, 500)' as CSSProperties['fontWeight'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function TextLink({
|
|
23
|
+
label,
|
|
24
|
+
href,
|
|
25
|
+
className,
|
|
26
|
+
}: {
|
|
27
|
+
label: string;
|
|
28
|
+
href: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
}) {
|
|
31
|
+
if (!label || !href) return null;
|
|
32
|
+
return (
|
|
33
|
+
<a
|
|
34
|
+
href={href}
|
|
35
|
+
className={cn(
|
|
36
|
+
'inline-flex shrink-0 items-center gap-2 whitespace-nowrap text-[color:var(--pb-link-color,var(--pb-accent))] underline-offset-4 hover:text-[color:var(--pb-link-hover-color,var(--pb-link-color,var(--pb-accent)))] hover:[text-decoration-line:var(--pb-link-hover-decoration,underline)]',
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
style={LINK_TEXT_STYLE}
|
|
40
|
+
>
|
|
41
|
+
{/* Emphasis only: the label already sits inside this <a>, and a nested
|
|
42
|
+
anchor is invalid HTML that browsers split into two links. */}
|
|
43
|
+
{renderRichText(label, { allowLinks: false })}
|
|
44
|
+
<LinkArrow className="size-5" />
|
|
45
|
+
</a>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { DisclosureChevron } from './_internal/disclosure-chevron';
|
|
3
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
4
|
+
|
|
5
|
+
export type AccordionItem = {
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type AccordionWidgetProps = {
|
|
11
|
+
items: AccordionItem[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const AccordionWidget: ComponentConfig<AccordionWidgetProps> = {
|
|
15
|
+
label: 'Accordion',
|
|
16
|
+
fields: {
|
|
17
|
+
items: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
label: 'Items',
|
|
20
|
+
arrayFields: {
|
|
21
|
+
title: { type: 'text', label: 'Title' },
|
|
22
|
+
content: { type: 'textarea', label: 'Content' },
|
|
23
|
+
},
|
|
24
|
+
defaultItemProps: {
|
|
25
|
+
title: 'Section title',
|
|
26
|
+
content: 'Section content...',
|
|
27
|
+
},
|
|
28
|
+
min: 1,
|
|
29
|
+
max: 20,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultProps: {
|
|
33
|
+
items: [
|
|
34
|
+
{ title: 'Section 1', content: 'Content for section 1...' },
|
|
35
|
+
{ title: 'Section 2', content: 'Content for section 2...' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
render: ({ items }) => {
|
|
39
|
+
if (!items || items.length === 0) {
|
|
40
|
+
return (
|
|
41
|
+
<div className="text-gray-500 text-center py-4">Add accordion items in the editor.</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<div className="container mx-auto max-w-3xl py-8 space-y-2">
|
|
46
|
+
{items.map((item, idx) => (
|
|
47
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; content repeats, so a content key collides
|
|
48
|
+
<details key={idx} className="group rounded-lg border bg-white dark:bg-gray-900 p-4">
|
|
49
|
+
<summary className="cursor-pointer flex items-center justify-between font-semibold list-none">
|
|
50
|
+
{renderRichText(item.title || `Item ${idx + 1}`, {
|
|
51
|
+
allowLinks: false,
|
|
52
|
+
})}
|
|
53
|
+
<DisclosureChevron />
|
|
54
|
+
</summary>
|
|
55
|
+
<RichTextBlock
|
|
56
|
+
text={item.content}
|
|
57
|
+
className="mt-3 text-gray-700 dark:text-gray-300 leading-relaxed"
|
|
58
|
+
/>
|
|
59
|
+
</details>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
3
|
+
|
|
4
|
+
export type AlertWidgetProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
variant: 'info' | 'success' | 'warning' | 'error';
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const VARIANT_CLASS: Record<AlertWidgetProps['variant'], string> = {
|
|
11
|
+
info: 'bg-primary-50 border-primary-200 text-primary-900',
|
|
12
|
+
success: 'bg-green-50 border-green-200 text-green-900',
|
|
13
|
+
warning: 'bg-yellow-50 border-yellow-200 text-yellow-900',
|
|
14
|
+
error: 'bg-red-50 border-red-200 text-red-900',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const AlertWidget: ComponentConfig<AlertWidgetProps> = {
|
|
18
|
+
label: 'Alert',
|
|
19
|
+
fields: {
|
|
20
|
+
title: { type: 'text', label: 'Title' },
|
|
21
|
+
message: { type: 'textarea', label: 'Message' },
|
|
22
|
+
variant: {
|
|
23
|
+
type: 'select',
|
|
24
|
+
label: 'Variant',
|
|
25
|
+
options: [
|
|
26
|
+
{ label: 'Info', value: 'info' },
|
|
27
|
+
{ label: 'Success', value: 'success' },
|
|
28
|
+
{ label: 'Warning', value: 'warning' },
|
|
29
|
+
{ label: 'Error', value: 'error' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultProps: {
|
|
34
|
+
title: 'Heads up!',
|
|
35
|
+
message: 'Something important to share.',
|
|
36
|
+
variant: 'info',
|
|
37
|
+
},
|
|
38
|
+
render: ({ title, message, variant }) => (
|
|
39
|
+
<div
|
|
40
|
+
className={`max-w-3xl mx-auto rounded-lg border px-4 py-3 ${VARIANT_CLASS[variant]}`}
|
|
41
|
+
role="alert"
|
|
42
|
+
>
|
|
43
|
+
<div className="font-semibold">{renderRichText(title || '')}</div>
|
|
44
|
+
{message && <RichTextBlock text={message} className="mt-1 text-sm" />}
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
|
+
import { cn } from '../../utils/widgetUtils';
|
|
3
|
+
|
|
4
|
+
export type AppStoreBadgesWidgetProps = {
|
|
5
|
+
googlePlayHref: string;
|
|
6
|
+
appStoreHref: string;
|
|
7
|
+
googlePlaySrc: string;
|
|
8
|
+
appStoreSrc: string;
|
|
9
|
+
align: 'left' | 'center';
|
|
10
|
+
/**
|
|
11
|
+
* Start the badges at a page-grid column so they line up with an indented
|
|
12
|
+
* page header above (Mowing landing hero: 2nd column). "none" keeps the
|
|
13
|
+
* legacy container-edge alignment.
|
|
14
|
+
*/
|
|
15
|
+
indent?: 'none' | '2' | '4';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 12-col grid placement (gap 24px) matching the Figma page grid.
|
|
19
|
+
const INDENT_CLASSES: Record<'2' | '4', string> = {
|
|
20
|
+
'2': 'lg:col-start-2 lg:col-span-10',
|
|
21
|
+
'4': 'lg:col-start-4 lg:col-span-9',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const AppStoreBadgesWidget: ComponentConfig<AppStoreBadgesWidgetProps> = {
|
|
25
|
+
label: 'App store badges',
|
|
26
|
+
fields: {
|
|
27
|
+
googlePlayHref: { type: 'text', label: 'Google Play link' },
|
|
28
|
+
appStoreHref: { type: 'text', label: 'App Store link' },
|
|
29
|
+
googlePlaySrc: { type: 'text', label: 'Google Play badge image' },
|
|
30
|
+
appStoreSrc: { type: 'text', label: 'App Store badge image' },
|
|
31
|
+
align: {
|
|
32
|
+
type: 'select',
|
|
33
|
+
label: 'Align',
|
|
34
|
+
options: [
|
|
35
|
+
{ label: 'Left', value: 'left' },
|
|
36
|
+
{ label: 'Center', value: 'center' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
indent: {
|
|
40
|
+
type: 'select',
|
|
41
|
+
label: 'Grid indent',
|
|
42
|
+
options: [
|
|
43
|
+
{ label: 'None', value: 'none' },
|
|
44
|
+
{ label: '2nd column', value: '2' },
|
|
45
|
+
{ label: '4th column', value: '4' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultProps: {
|
|
50
|
+
googlePlayHref: '#',
|
|
51
|
+
appStoreHref: '#',
|
|
52
|
+
googlePlaySrc: '',
|
|
53
|
+
appStoreSrc: '',
|
|
54
|
+
align: 'left',
|
|
55
|
+
indent: 'none',
|
|
56
|
+
},
|
|
57
|
+
render: ({
|
|
58
|
+
googlePlayHref,
|
|
59
|
+
appStoreHref,
|
|
60
|
+
googlePlaySrc,
|
|
61
|
+
appStoreSrc,
|
|
62
|
+
align,
|
|
63
|
+
indent = 'none',
|
|
64
|
+
}) => (
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
'container mx-auto px-4 py-[var(--pb-section-py,3rem)] sm:px-6 lg:px-8',
|
|
68
|
+
indent !== 'none' && 'lg:grid lg:grid-cols-12 lg:gap-6',
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex flex-wrap items-center gap-4',
|
|
74
|
+
align === 'center' ? 'justify-center' : 'justify-start',
|
|
75
|
+
indent !== 'none' && INDENT_CLASSES[indent],
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{googlePlaySrc && (
|
|
79
|
+
<a href={googlePlayHref} target="_blank" rel="noopener noreferrer">
|
|
80
|
+
<img src={googlePlaySrc} alt="Get it on Google Play" className="h-16 w-auto" />
|
|
81
|
+
</a>
|
|
82
|
+
)}
|
|
83
|
+
{appStoreSrc && (
|
|
84
|
+
<a href={appStoreHref} target="_blank" rel="noopener noreferrer">
|
|
85
|
+
<img src={appStoreSrc} alt="Download on the App Store" className="h-16 w-auto" />
|
|
86
|
+
</a>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** ArticleCards presentation, split from its Puck config so another module
|
|
2
|
+
* can render the same grid from live data — the news feed does.
|
|
3
|
+
* The config lives in article-cards-widget.tsx. */
|
|
4
|
+
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
6
|
+
import { cn } from '../../utils/widgetUtils';
|
|
7
|
+
import { renderRichText } from './_internal/rich-text';
|
|
8
|
+
import { TextLink } from './_shared/text-link';
|
|
9
|
+
|
|
10
|
+
export type ArticleCardItem = {
|
|
11
|
+
imageUrl: string;
|
|
12
|
+
imageAlt: string;
|
|
13
|
+
eyebrow: string;
|
|
14
|
+
date: string;
|
|
15
|
+
title: string;
|
|
16
|
+
body: string;
|
|
17
|
+
href: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ArticleCardsWidgetProps = {
|
|
21
|
+
title: string;
|
|
22
|
+
viewAllLabel: string;
|
|
23
|
+
viewAllHref: string;
|
|
24
|
+
columns: '2' | '3' | '4';
|
|
25
|
+
items: ArticleCardItem[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const COLS_CLASS: Record<ArticleCardsWidgetProps['columns'], string> = {
|
|
29
|
+
'2': 'sm:grid-cols-2',
|
|
30
|
+
'3': 'sm:grid-cols-2 lg:grid-cols-3',
|
|
31
|
+
'4': 'sm:grid-cols-2 lg:grid-cols-4',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Display tokens come from the per-tenant page-builder theme (see styles.css
|
|
35
|
+
// `:root` for Recodo defaults and `.gca-root` for the GCA overrides). The
|
|
36
|
+
// fallback clamp reproduces the prior `text-2xl sm:text-3xl lg:text-4xl`
|
|
37
|
+
// steps: 24px on phones, 30px at 640px, 36px at 1024px. All style objects are
|
|
38
|
+
// hoisted — constant tokens only, no per-render allocation.
|
|
39
|
+
const HEADING_STYLE: CSSProperties = {
|
|
40
|
+
fontWeight: 'var(--pb-display-weight)' as CSSProperties['fontWeight'],
|
|
41
|
+
letterSpacing: 'var(--pb-display-tracking)',
|
|
42
|
+
fontFamily: 'var(--pb-display-font)',
|
|
43
|
+
color: 'var(--pb-heading-color)',
|
|
44
|
+
fontSize: 'var(--pb-heading-lg, clamp(1.5rem, 1.25rem + 1.5625vw, 2.25rem))',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CARD_META_STYLE: CSSProperties = {
|
|
48
|
+
color: 'var(--pb-body-color)',
|
|
49
|
+
fontSize: 'var(--pb-card-meta-size, 0.75rem)',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const CARD_TITLE_STYLE: CSSProperties = {
|
|
53
|
+
fontFamily: 'var(--pb-display-font)',
|
|
54
|
+
fontWeight:
|
|
55
|
+
'var(--pb-card-title-weight, var(--pb-display-weight))' as CSSProperties['fontWeight'],
|
|
56
|
+
fontSize: 'var(--pb-card-title-size, 1.125rem)',
|
|
57
|
+
color: 'var(--pb-heading-color)',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const CARD_BODY_STYLE: CSSProperties = {
|
|
61
|
+
color: 'var(--pb-body-color)',
|
|
62
|
+
fontSize: 'var(--pb-card-body-size, 0.875rem)',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function ArticleCardsGrid({
|
|
66
|
+
title,
|
|
67
|
+
viewAllLabel,
|
|
68
|
+
viewAllHref,
|
|
69
|
+
columns,
|
|
70
|
+
items,
|
|
71
|
+
}: ArticleCardsWidgetProps) {
|
|
72
|
+
return (
|
|
73
|
+
<section className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
|
74
|
+
{(title || (viewAllLabel && viewAllHref)) && (
|
|
75
|
+
<div className="mb-8 flex items-end justify-between gap-4">
|
|
76
|
+
{title && (
|
|
77
|
+
<h2 className="leading-[var(--pb-heading-leading,1.15)]" style={HEADING_STYLE}>
|
|
78
|
+
{renderRichText(title)}
|
|
79
|
+
</h2>
|
|
80
|
+
)}
|
|
81
|
+
{viewAllLabel && viewAllHref && <TextLink label={viewAllLabel} href={viewAllHref} />}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
<div className={cn('grid gap-6 grid-cols-1', COLS_CLASS[columns])}>
|
|
85
|
+
{items?.map((item, idx) => (
|
|
86
|
+
// The whole card is one anchor, so every string inside it renders
|
|
87
|
+
// with `allowLinks: false` — a nested <a> is invalid HTML and
|
|
88
|
+
// browsers split the card link in two.
|
|
89
|
+
<a
|
|
90
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: author-ordered array with no stable id; card content repeats, so a content key collides
|
|
91
|
+
key={idx}
|
|
92
|
+
href={item.href || '#'}
|
|
93
|
+
className="group block"
|
|
94
|
+
>
|
|
95
|
+
<div className="relative aspect-[16/10] overflow-hidden rounded-xl bg-[var(--pb-surface-muted)]">
|
|
96
|
+
{item.imageUrl && (
|
|
97
|
+
<img
|
|
98
|
+
src={item.imageUrl}
|
|
99
|
+
alt={item.imageAlt || item.title}
|
|
100
|
+
loading="lazy"
|
|
101
|
+
className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
{item.eyebrow && (
|
|
105
|
+
<span className="absolute right-3 top-3 inline-flex w-fit items-center rounded-full bg-[var(--pb-card-tag-bg,rgb(255_255_255/0.9))] px-2.5 py-0.5 text-xs font-medium text-[var(--pb-card-tag-color,#1a353e)] backdrop-blur-sm">
|
|
106
|
+
{renderRichText(item.eyebrow, { allowLinks: false })}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
<div className="mt-4">
|
|
111
|
+
{item.date && (
|
|
112
|
+
<span style={CARD_META_STYLE}>
|
|
113
|
+
{renderRichText(item.date, { allowLinks: false })}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
{item.title && (
|
|
117
|
+
<h3 className="mt-3 leading-snug" style={CARD_TITLE_STYLE}>
|
|
118
|
+
{renderRichText(item.title, { allowLinks: false })}
|
|
119
|
+
</h3>
|
|
120
|
+
)}
|
|
121
|
+
{item.body && (
|
|
122
|
+
<p className="mt-2 leading-relaxed" style={CARD_BODY_STYLE}>
|
|
123
|
+
{renderRichText(item.body, { allowLinks: false })}
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</a>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** ArticleCards widget: field definitions and config assembly.
|
|
2
|
+
* Types and the grid itself live in article-cards-render.tsx. */
|
|
3
|
+
|
|
4
|
+
import type { ComponentConfig } from '@puckeditor/core';
|
|
5
|
+
|
|
6
|
+
import { createImageField, mediaLibraryAdapter } from '../../fields';
|
|
7
|
+
import { ArticleCardsGrid, type ArticleCardsWidgetProps } from './article-cards-render';
|
|
8
|
+
|
|
9
|
+
export type { ArticleCardItem, ArticleCardsWidgetProps } from './article-cards-render';
|
|
10
|
+
export { ArticleCardsGrid } from './article-cards-render';
|
|
11
|
+
|
|
12
|
+
export const ArticleCardsWidget: ComponentConfig<ArticleCardsWidgetProps> = {
|
|
13
|
+
label: 'Article cards (image-top, with category & date)',
|
|
14
|
+
fields: {
|
|
15
|
+
title: { type: 'text', label: 'Title' },
|
|
16
|
+
viewAllLabel: { type: 'text', label: 'View-all link label' },
|
|
17
|
+
viewAllHref: { type: 'text', label: 'View-all link URL' },
|
|
18
|
+
columns: {
|
|
19
|
+
type: 'select',
|
|
20
|
+
label: 'Columns (desktop)',
|
|
21
|
+
options: [
|
|
22
|
+
{ label: '2', value: '2' },
|
|
23
|
+
{ label: '3', value: '3' },
|
|
24
|
+
{ label: '4', value: '4' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
items: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
label: 'Articles',
|
|
30
|
+
arrayFields: {
|
|
31
|
+
imageUrl: createImageField(mediaLibraryAdapter, 'Image'),
|
|
32
|
+
imageAlt: { type: 'text', label: 'Image alt text' },
|
|
33
|
+
eyebrow: { type: 'text', label: 'Category tag' },
|
|
34
|
+
date: { type: 'text', label: 'Date' },
|
|
35
|
+
title: { type: 'text', label: 'Title' },
|
|
36
|
+
// The whole card is a link, so markdown links in this copy render
|
|
37
|
+
// as their label text only — say so where the editor can see it.
|
|
38
|
+
body: { type: 'textarea', label: 'Body (links not supported here)' },
|
|
39
|
+
href: { type: 'text', label: 'Link' },
|
|
40
|
+
},
|
|
41
|
+
defaultItemProps: {
|
|
42
|
+
imageUrl: '',
|
|
43
|
+
imageAlt: '',
|
|
44
|
+
eyebrow: 'Category tag',
|
|
45
|
+
date: 'MMM DD, YYYY',
|
|
46
|
+
title: 'Article title goes here and may be on two lines',
|
|
47
|
+
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
|
48
|
+
href: '#',
|
|
49
|
+
},
|
|
50
|
+
min: 1,
|
|
51
|
+
max: 12,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultProps: {
|
|
55
|
+
title: 'Latest news, updates & events',
|
|
56
|
+
viewAllLabel: 'View all',
|
|
57
|
+
viewAllHref: '#',
|
|
58
|
+
columns: '4',
|
|
59
|
+
items: [
|
|
60
|
+
{
|
|
61
|
+
imageUrl: '',
|
|
62
|
+
imageAlt: '',
|
|
63
|
+
eyebrow: 'Category tag',
|
|
64
|
+
date: 'MMM DD, YYYY',
|
|
65
|
+
title: 'Article title goes here and may be on two lines',
|
|
66
|
+
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
|
67
|
+
href: '#',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
render: ArticleCardsGrid,
|
|
72
|
+
};
|