simple-module-pagebuilder 0.0.2__tar.gz → 0.0.4__tar.gz
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.
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/PKG-INFO +33 -1
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/README.md +32 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/package.json +1 -1
- simple_module_pagebuilder-0.0.4/pagebuilder/components/blocks/Image.test.tsx +94 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Image.tsx +37 -3
- simple_module_pagebuilder-0.0.4/pagebuilder/components/lazyBlock.tsx +58 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/background-media.tsx +5 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/content-media.tsx +13 -1
- simple_module_pagebuilder-0.0.4/pagebuilder/components/widgets/_shared/image-srcset.ts +55 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/index.ts +7 -0
- simple_module_pagebuilder-0.0.4/pagebuilder/components/widgets/hero-widget-props.ts +33 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/hero-widget.tsx +21 -29
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-layout.ts +2 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-render.tsx +5 -1
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-widget.tsx +4 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/blocks-to-html.test.ts +24 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/blocks-to-html.ts +10 -2
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/seo.py +4 -1
- simple_module_pagebuilder-0.0.4/pagebuilder/media_files.py +128 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_service.py +4 -1
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/module.py +8 -3
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/__init__.py +32 -2
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/settings.py +5 -2
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/static/widgets-base.css +7 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/markdown.ts +8 -1
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pyproject.toml +1 -1
- simple_module_pagebuilder-0.0.4/tests/test_media_files.py +137 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/.gitignore +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/DiffSummary.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/ScheduledBadge.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/StatusBadge.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blockRegistry.test.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blockRegistry.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Button.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Columns.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Heading.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/MediaPicker.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Spacer.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/blocks/Text.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/PageEditorToolbar.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/RevisionHistoryPanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/SchedulePanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/editor/SeoSettingsPanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/layoutPuckConfig.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaFilters.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaFolderSidebar.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaGrid.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaHeader.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/MediaUploadQueue.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/media/types.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/migrateContent.test.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/migrateContent.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/puckConfig.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/puckConfig.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgetCatalog.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgetCatalog.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/accent-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/link-arrow.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/rich-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_internal/solid-icons.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/cta-button.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/heading.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/mask.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/section.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/social-icon.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/_shared/text-link.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/accordion-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/alert-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/app-store-badges-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/article-cards-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/article-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-styles.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/call-to-action-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/carousel-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/centered-hero-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/colored-action-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/column-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-form-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/contact-form-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/container-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/definition-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/divider-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/eyebrow-section-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/faq-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/faq-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-card-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-styles.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-grid-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/feature-showcase-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/gallery-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/grid-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/html-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/iframe-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/leaderboard-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/logo-cloud-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/markdown-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/media-object-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/newsletter-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/page-header-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/prose-section-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/quote-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/row-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/section-intro-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/signup-banner-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/simple-action-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-footer-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-header-nav.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/site-header-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/social-banner-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/stats-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/steps-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/story-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/table-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/tabs-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/tags-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/testimonial-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-layout.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/timeline-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/under-construction-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/video-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/components/widgets/welcome-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/events.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/contracts/schemas.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/index.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/conversion/types.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/deps.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/diff.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/_deps.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/layout.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/pages.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/revisions.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/uploads.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/api/workflow.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/endpoints/views.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/checkbox-field.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/file-gallery.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/image-picker-field.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/index.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/file-picker/types.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/index.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/fields/media-adapter.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useAutosave.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useEditorForm.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/useMediaUploads.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageRevisions.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageSchedule.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/hooks/usePageWorkflow.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/layout_service.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_images.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/media_queries.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/models.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/LayoutEditor.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/MediaLibrary.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PageEditor.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PageList.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PendingReview.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/pages/PublicPage.tsx +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/permissions.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/py.typed +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/security.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_common.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_revisions.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/service/_workflow.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/services.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/apply.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/extract.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/index.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/translatable-fields.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/translation/translation.test.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/api.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/datetime.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/decorative-image.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/editorSnapshot.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/mediaApi.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/mediaFormat.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/pagesApi.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/parse-list.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/request.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/slugify.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/types.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/pagebuilder/utils/widgetUtils.ts +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/conftest.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_approval_workflow.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_auth_gating.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_csrf.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_image_processing.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_media_security.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_module.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_pages_api.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_public_routes.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_public_view_headers.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_public_viewer.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_revision_notes_and_diff.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_revisions_api.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_scheduled_publish.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_seo_meta.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_site_layout.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_sitemap_robots.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tests/test_uploads_api.py +0 -0
- {simple_module_pagebuilder-0.0.2 → simple_module_pagebuilder-0.0.4}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_pagebuilder
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Drag-and-drop visual page builder for simple_module apps
|
|
5
5
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python_modules
|
|
6
6
|
Author-email: Anto Subash <antosubash@live.com>
|
|
@@ -81,6 +81,38 @@ Once installed and migrated, sign in and open `/pagebuilder`:
|
|
|
81
81
|
Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
|
|
82
82
|
two revisions and restore either one as a draft.
|
|
83
83
|
|
|
84
|
+
## Contributing blocks from another module
|
|
85
|
+
|
|
86
|
+
A module ships Puck blocks by calling `registerPuckBlocks` from its
|
|
87
|
+
`puck-blocks.ts`; the host imports every module's registration eagerly at app
|
|
88
|
+
start, so the registry is populated before the first render.
|
|
89
|
+
|
|
90
|
+
Because registration is eager, a block that statically imports a heavy render
|
|
91
|
+
component puts that component's whole dependency graph in the entry chunk —
|
|
92
|
+
on every page of the site, for every visitor. Measured on a consuming site, a
|
|
93
|
+
map block's static import put maplibre-gl (1 MB minified, 62% of the bundle)
|
|
94
|
+
in front of visitors who never opened the map. Register heavy blocks with
|
|
95
|
+
`lazyBlock` instead, which keeps fields and defaults eager but loads the
|
|
96
|
+
component the first time a page actually renders the block:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { lazyBlock } from '@simple-module-py/pagebuilder/pagebuilder/components/lazyBlock';
|
|
100
|
+
import { registerPuckBlocks } from '@simple-module-py/pagebuilder/pagebuilder/components/blockRegistry';
|
|
101
|
+
|
|
102
|
+
registerPuckBlocks({
|
|
103
|
+
blocks: {
|
|
104
|
+
AtlasMap: lazyBlock(
|
|
105
|
+
() => import('./components/AtlasMap').then((m) => m.AtlasMapEmbed),
|
|
106
|
+
{ label: 'Atlas map', fields: { /* … */ }, defaultProps: { /* … */ } },
|
|
107
|
+
(props) => <div className={heightClass[props.height]} aria-busy="true" />,
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Light blocks (text, cards, lists) can keep their static imports — the split
|
|
114
|
+
only pays for itself when the component drags in something big.
|
|
115
|
+
|
|
84
116
|
## Migrations
|
|
85
117
|
|
|
86
118
|
This module ships **no** migrations — that is the framework convention. Its
|
|
@@ -59,6 +59,38 @@ Once installed and migrated, sign in and open `/pagebuilder`:
|
|
|
59
59
|
Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
|
|
60
60
|
two revisions and restore either one as a draft.
|
|
61
61
|
|
|
62
|
+
## Contributing blocks from another module
|
|
63
|
+
|
|
64
|
+
A module ships Puck blocks by calling `registerPuckBlocks` from its
|
|
65
|
+
`puck-blocks.ts`; the host imports every module's registration eagerly at app
|
|
66
|
+
start, so the registry is populated before the first render.
|
|
67
|
+
|
|
68
|
+
Because registration is eager, a block that statically imports a heavy render
|
|
69
|
+
component puts that component's whole dependency graph in the entry chunk —
|
|
70
|
+
on every page of the site, for every visitor. Measured on a consuming site, a
|
|
71
|
+
map block's static import put maplibre-gl (1 MB minified, 62% of the bundle)
|
|
72
|
+
in front of visitors who never opened the map. Register heavy blocks with
|
|
73
|
+
`lazyBlock` instead, which keeps fields and defaults eager but loads the
|
|
74
|
+
component the first time a page actually renders the block:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { lazyBlock } from '@simple-module-py/pagebuilder/pagebuilder/components/lazyBlock';
|
|
78
|
+
import { registerPuckBlocks } from '@simple-module-py/pagebuilder/pagebuilder/components/blockRegistry';
|
|
79
|
+
|
|
80
|
+
registerPuckBlocks({
|
|
81
|
+
blocks: {
|
|
82
|
+
AtlasMap: lazyBlock(
|
|
83
|
+
() => import('./components/AtlasMap').then((m) => m.AtlasMapEmbed),
|
|
84
|
+
{ label: 'Atlas map', fields: { /* … */ }, defaultProps: { /* … */ } },
|
|
85
|
+
(props) => <div className={heightClass[props.height]} aria-busy="true" />,
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Light blocks (text, cards, lists) can keep their static imports — the split
|
|
92
|
+
only pays for itself when the component drags in something big.
|
|
93
|
+
|
|
62
94
|
## Migrations
|
|
63
95
|
|
|
64
96
|
This module ships **no** migrations — that is the framework convention. Its
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { ImageBlock } from './Image';
|
|
6
|
+
|
|
7
|
+
// Puck passes its own `id`/`puck` props alongside the authored ones; the block
|
|
8
|
+
// reads none of them, so these tests supply the authored props alone.
|
|
9
|
+
const Image = ImageBlock.render as unknown as (props: Record<string, unknown>) => ReactElement;
|
|
10
|
+
|
|
11
|
+
const BASE = {
|
|
12
|
+
...ImageBlock.defaultProps,
|
|
13
|
+
src: '/media/pagebuilder/atlas-map.jpg',
|
|
14
|
+
alt: 'The Global Canopy Atlas interactive map',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function html(props: Record<string, unknown> = {}): string {
|
|
18
|
+
return renderToStaticMarkup(<Image {...BASE} {...props} />);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('ImageBlock caption', () => {
|
|
22
|
+
it('offers a Caption field in the inspector', () => {
|
|
23
|
+
expect(ImageBlock.fields?.caption).toEqual({ type: 'text', label: 'Caption' });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('defaults to no caption', () => {
|
|
27
|
+
expect(ImageBlock.defaultProps?.caption).toBe('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders the caption in a figcaption inside a figure', () => {
|
|
31
|
+
const markup = html({ caption: 'Image caption goes here, if CMS field is populated' });
|
|
32
|
+
expect(markup).toContain('<figure');
|
|
33
|
+
expect(markup).toContain('<figcaption');
|
|
34
|
+
expect(markup).toContain('Image caption goes here, if CMS field is populated');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('takes the caption’s colour and metrics from the design pack', () => {
|
|
38
|
+
// A tenant restyles captions by overriding tokens, not by patching this
|
|
39
|
+
// block — same contract the widget set gives the rest of the page.
|
|
40
|
+
const markup = html({ caption: 'A caption' });
|
|
41
|
+
expect(markup).toContain('--pb-caption-color');
|
|
42
|
+
expect(markup).toContain('--pb-body-color');
|
|
43
|
+
expect(markup).toContain('--pb-caption-size');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps alt and caption separate', () => {
|
|
47
|
+
// alt is the accessible description, caption is visible prose; the two are
|
|
48
|
+
// rarely the same sentence, so neither may stand in for the other.
|
|
49
|
+
const markup = html({ alt: 'A map of the world', caption: 'Figure 1' });
|
|
50
|
+
expect(markup).toContain('alt="A map of the world"');
|
|
51
|
+
expect(markup).toContain('>Figure 1</figcaption>');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders the caption as light markdown, like every other copy field', () => {
|
|
55
|
+
// Table, Carousel and CallToAction captions all go through renderRichText,
|
|
56
|
+
// so `**bold**` cannot mean one thing in one caption and another here.
|
|
57
|
+
expect(html({ caption: '**Figure 1** — the map' })).toContain('<strong');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('escapes caption text rather than emitting it as markup', () => {
|
|
61
|
+
expect(html({ caption: '<b>bold</b>' })).not.toContain('<b>bold</b>');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('emits a bare <img> when no caption is set', () => {
|
|
65
|
+
// Existing pages carry no caption prop at all — `undefined`, not `''`, is
|
|
66
|
+
// what Puck hands the block for them — and must render exactly as they did
|
|
67
|
+
// before the field existed.
|
|
68
|
+
const markup = html({ caption: undefined });
|
|
69
|
+
expect(markup).not.toContain('figure');
|
|
70
|
+
expect(markup.startsWith('<img')).toBe(true);
|
|
71
|
+
expect(markup).toContain('class="my-3"');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('treats an empty or blank caption as no caption', () => {
|
|
75
|
+
// Whitespace-only would otherwise be truthy and render an empty caption
|
|
76
|
+
// box under the image.
|
|
77
|
+
const bare = html({ caption: undefined });
|
|
78
|
+
expect(html({ caption: '' })).toBe(bare);
|
|
79
|
+
expect(html({ caption: ' ' })).toBe(bare);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('leaves the empty-src placeholder alone', () => {
|
|
83
|
+
const markup = html({ src: '', caption: 'A caption' });
|
|
84
|
+
expect(markup).toContain('No image selected');
|
|
85
|
+
expect(markup).not.toContain('figcaption');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('still hides a decorative image from screen readers', () => {
|
|
89
|
+
const markup = html({ altKind: 'decorative', caption: 'A caption' });
|
|
90
|
+
expect(markup).toContain('alt=""');
|
|
91
|
+
expect(markup).toContain('aria-hidden="true"');
|
|
92
|
+
expect(markup).toContain('<figcaption');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
lookupAsset,
|
|
7
7
|
type MediaAssetRead,
|
|
8
8
|
} from '../../utils/api';
|
|
9
|
+
import { renderRichText } from '../widgets/_internal/rich-text';
|
|
9
10
|
import { MediaPicker } from './MediaPicker';
|
|
10
11
|
|
|
11
12
|
type AltKind = 'meaningful' | 'decorative';
|
|
@@ -14,6 +15,10 @@ interface ImageProps {
|
|
|
14
15
|
src: string;
|
|
15
16
|
alt: string;
|
|
16
17
|
altKind: AltKind;
|
|
18
|
+
/** Visible prose under the image, as light markdown. Distinct from `alt`,
|
|
19
|
+
* which describes the image to a screen reader — the two are rarely the
|
|
20
|
+
* same sentence. */
|
|
21
|
+
caption?: string;
|
|
17
22
|
width: number | null;
|
|
18
23
|
height: number | null;
|
|
19
24
|
objectFit: 'cover' | 'contain' | 'fill';
|
|
@@ -21,6 +26,14 @@ interface ImageProps {
|
|
|
21
26
|
sizes: string;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
// Captions read their look from the design pack the way the widget set does,
|
|
30
|
+
// so a tenant restyles them by overriding tokens instead of patching this
|
|
31
|
+
// block. `--pb-caption-color` falls through to the pack's body colour, which
|
|
32
|
+
// is what an unstyled pack (and widgets-base) already defines.
|
|
33
|
+
const CAPTION_CLASS =
|
|
34
|
+
'mt-2 text-[length:var(--pb-caption-size,0.875rem)] leading-[var(--pb-body-leading,1.625)]';
|
|
35
|
+
const CAPTION_STYLE = { color: 'var(--pb-caption-color, var(--pb-body-color))' };
|
|
36
|
+
|
|
24
37
|
function resolveSizes(srcset: string, sizes: string): string | undefined {
|
|
25
38
|
if (sizes) return sizes;
|
|
26
39
|
// Browsers assume 100vw when sizes is omitted but srcset is present;
|
|
@@ -50,6 +63,7 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
|
|
|
50
63
|
],
|
|
51
64
|
},
|
|
52
65
|
alt: { type: 'text' },
|
|
66
|
+
caption: { type: 'text', label: 'Caption' },
|
|
53
67
|
width: { type: 'number' },
|
|
54
68
|
height: { type: 'number' },
|
|
55
69
|
objectFit: {
|
|
@@ -67,6 +81,7 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
|
|
|
67
81
|
src: '',
|
|
68
82
|
alt: '',
|
|
69
83
|
altKind: 'meaningful',
|
|
84
|
+
caption: '',
|
|
70
85
|
width: null,
|
|
71
86
|
height: null,
|
|
72
87
|
objectFit: 'cover',
|
|
@@ -96,7 +111,7 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
|
|
|
96
111
|
},
|
|
97
112
|
};
|
|
98
113
|
},
|
|
99
|
-
render: ({ src, alt, altKind, width, height, objectFit, srcset, sizes }) => {
|
|
114
|
+
render: ({ src, alt, altKind, caption, width, height, objectFit, srcset, sizes }) => {
|
|
100
115
|
if (!src) {
|
|
101
116
|
return (
|
|
102
117
|
<div className="my-3 p-6 border-2 border-dashed border-gray-300 rounded text-center text-gray-500 text-sm">
|
|
@@ -105,7 +120,11 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
|
|
|
105
120
|
);
|
|
106
121
|
}
|
|
107
122
|
const decorative = altKind === 'decorative';
|
|
108
|
-
|
|
123
|
+
// Trimmed, so a caption of nothing but spaces is no caption: it would
|
|
124
|
+
// otherwise render an empty <figcaption> box under the image. Same rule
|
|
125
|
+
// the translation extractor applies to every other copy field.
|
|
126
|
+
const hasCaption = Boolean(caption?.trim());
|
|
127
|
+
const image = (
|
|
109
128
|
<img
|
|
110
129
|
src={src}
|
|
111
130
|
srcSet={srcset || undefined}
|
|
@@ -118,8 +137,23 @@ export const ImageBlock: ComponentConfig<ImageProps> = {
|
|
|
118
137
|
loading="lazy"
|
|
119
138
|
decoding="async"
|
|
120
139
|
style={{ objectFit, maxWidth: '100%' }}
|
|
121
|
-
|
|
140
|
+
// Inside a <figure> the margin belongs to the figure, and `block`
|
|
141
|
+
// drops the inline descender gap above the caption. Uncaptioned
|
|
142
|
+
// images keep the exact markup they had before the field existed.
|
|
143
|
+
className={hasCaption ? 'block' : 'my-3'}
|
|
122
144
|
/>
|
|
123
145
|
);
|
|
146
|
+
if (!hasCaption) return image;
|
|
147
|
+
return (
|
|
148
|
+
<figure className="my-3">
|
|
149
|
+
{image}
|
|
150
|
+
<figcaption className={CAPTION_CLASS} style={CAPTION_STYLE}>
|
|
151
|
+
{/* Light markdown, like every other copy field in the module — the
|
|
152
|
+
Table/Carousel/CallToAction captions all render this way, so
|
|
153
|
+
`**Figure 1**` can't work in one caption and not another. */}
|
|
154
|
+
{renderRichText(caption)}
|
|
155
|
+
</figcaption>
|
|
156
|
+
</figure>
|
|
157
|
+
);
|
|
124
158
|
},
|
|
125
159
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register a Puck block whose render component loads on demand (issue #16).
|
|
3
|
+
*
|
|
4
|
+
* Block registration is eager by design — the host imports every module's
|
|
5
|
+
* `puck-blocks.ts` before first render so the registry is populated when a
|
|
6
|
+
* page reads the Puck config. But a registration that statically imports its
|
|
7
|
+
* render component drags the component's whole dependency graph into the
|
|
8
|
+
* entry chunk, on every page of the site. Measured on a consuming site, a
|
|
9
|
+
* map block put maplibre-gl — 1 MB minified, 62% of the bundle — in front
|
|
10
|
+
* of every visitor.
|
|
11
|
+
*
|
|
12
|
+
* `lazyBlock` keeps the cheap parts eager (fields, defaults, labels) and
|
|
13
|
+
* loads the component the first time a page actually renders the block:
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* registerPuckBlocks({
|
|
17
|
+
* blocks: {
|
|
18
|
+
* CanopyAtlasMap: lazyBlock(
|
|
19
|
+
* () => import('./components/CanopyAtlasMap').then((m) => m.CanopyAtlasMapEmbed),
|
|
20
|
+
* {
|
|
21
|
+
* label: 'Canopy Atlas map',
|
|
22
|
+
* fields: { height: { type: 'select', options: HEIGHTS } },
|
|
23
|
+
* defaultProps: { height: 'screen' },
|
|
24
|
+
* },
|
|
25
|
+
* (props) => <div className={heightClass[props.height]} aria-busy="true" />,
|
|
26
|
+
* ),
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { type ComponentType, lazy, type ReactNode, Suspense } from 'react';
|
|
33
|
+
|
|
34
|
+
import type { AnyBlock } from './blockRegistry';
|
|
35
|
+
|
|
36
|
+
// Typed via AnyBlock rather than a precise ComponentConfig<P>: Puck's config
|
|
37
|
+
// generic carries constraints a pass-through helper cannot restate, and the
|
|
38
|
+
// registry these blocks land in erases the props type anyway.
|
|
39
|
+
// biome-ignore lint/suspicious/noExplicitAny: heterogeneous block props, same as AnyBlock
|
|
40
|
+
type AnyProps = Record<string, any>;
|
|
41
|
+
|
|
42
|
+
export function lazyBlock<P extends AnyProps>(
|
|
43
|
+
load: () => Promise<ComponentType<P>>,
|
|
44
|
+
config: Omit<AnyBlock, 'render'>,
|
|
45
|
+
/** Rendered while the chunk downloads — size it like the block to avoid
|
|
46
|
+
* layout shift. Defaults to nothing. */
|
|
47
|
+
fallback?: (props: P) => ReactNode,
|
|
48
|
+
): AnyBlock {
|
|
49
|
+
const Lazy = lazy(async () => ({ default: await load() }));
|
|
50
|
+
return {
|
|
51
|
+
...config,
|
|
52
|
+
render: (props: AnyProps) => (
|
|
53
|
+
<Suspense fallback={fallback ? fallback(props as P) : null}>
|
|
54
|
+
<Lazy {...(props as P)} />
|
|
55
|
+
</Suspense>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn, decorativeAriaProps } from '../../../utils/widgetUtils';
|
|
2
|
+
import { SIZES_FULL, srcsetAttrs } from './image-srcset';
|
|
2
3
|
|
|
3
4
|
export type BackgroundMediaOverlay = 'none' | 'subtle' | 'dark';
|
|
4
5
|
export type BackgroundMediaFallback = 'gradient-mesh' | 'solid';
|
|
@@ -15,6 +16,8 @@ export type BackgroundMediaProps = {
|
|
|
15
16
|
* anchors to the top; centered, the sky is cut away). Omitted = centered.
|
|
16
17
|
*/
|
|
17
18
|
objectPosition?: string;
|
|
19
|
+
/** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
|
|
20
|
+
imageSrcset?: string;
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
// The "dark" scrim strength is tenant-tunable: GCA's Figma hero is nearly
|
|
@@ -41,6 +44,7 @@ export function BackgroundMedia({
|
|
|
41
44
|
overlay = 'none',
|
|
42
45
|
fallback = 'gradient-mesh',
|
|
43
46
|
objectPosition,
|
|
47
|
+
imageSrcset,
|
|
44
48
|
}: BackgroundMediaProps) {
|
|
45
49
|
// data-pb-* attributes are stable selectors for tests and CMS-side analytics.
|
|
46
50
|
return (
|
|
@@ -48,6 +52,7 @@ export function BackgroundMedia({
|
|
|
48
52
|
{imageUrl ? (
|
|
49
53
|
<img
|
|
50
54
|
src={imageUrl}
|
|
55
|
+
{...srcsetAttrs(imageSrcset, SIZES_FULL)}
|
|
51
56
|
alt={imageAlt ?? ''}
|
|
52
57
|
{...decorativeAriaProps(imageAlt)}
|
|
53
58
|
loading="lazy"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn, decorativeAriaProps } from '../../../utils/widgetUtils';
|
|
2
|
+
import { srcsetAttrs } from './image-srcset';
|
|
2
3
|
|
|
3
4
|
export type ContentMediaAspect = 'square' | 'video' | 'portrait';
|
|
4
5
|
|
|
@@ -6,6 +7,10 @@ export type ContentMediaProps = {
|
|
|
6
7
|
imageUrl: string;
|
|
7
8
|
imageAlt: string;
|
|
8
9
|
aspect?: ContentMediaAspect;
|
|
10
|
+
/** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
|
|
11
|
+
imageSrcset?: string;
|
|
12
|
+
/** `sizes` for the srcset; callers in split layouts pass SIZES_HALF. */
|
|
13
|
+
sizes?: string;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
const ASPECT_CLASSES: Record<ContentMediaAspect, string> = {
|
|
@@ -23,13 +28,20 @@ const DOTTED_STYLE = {
|
|
|
23
28
|
backgroundSize: '16px 16px, 100% 100%',
|
|
24
29
|
} as const;
|
|
25
30
|
|
|
26
|
-
export function ContentMedia({
|
|
31
|
+
export function ContentMedia({
|
|
32
|
+
imageUrl,
|
|
33
|
+
imageAlt,
|
|
34
|
+
aspect = 'video',
|
|
35
|
+
imageSrcset,
|
|
36
|
+
sizes = '100vw',
|
|
37
|
+
}: ContentMediaProps) {
|
|
27
38
|
const wrapperCls = cn('w-full rounded-xl overflow-hidden', ASPECT_CLASSES[aspect], 'shadow-xl');
|
|
28
39
|
if (imageUrl) {
|
|
29
40
|
return (
|
|
30
41
|
<div className={wrapperCls}>
|
|
31
42
|
<img
|
|
32
43
|
src={imageUrl}
|
|
44
|
+
{...srcsetAttrs(imageSrcset, sizes)}
|
|
33
45
|
alt={imageAlt ?? ''}
|
|
34
46
|
{...decorativeAriaProps(imageAlt)}
|
|
35
47
|
loading="lazy"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Responsive-image support for picker-backed widgets (issue #15).
|
|
2
|
+
*
|
|
3
|
+
* The media library generates WebP variants at upload, but widgets that
|
|
4
|
+
* store a bare `imageUrl` string shipped the full-size original to every
|
|
5
|
+
* viewport. These helpers let a widget persist the variant srcset next to
|
|
6
|
+
* the URL the same way the Image block does: `resolveImageSrcset` runs as
|
|
7
|
+
* Puck's `resolveData` when the picked URL changes, and the value lands in
|
|
8
|
+
* block data so the public render needs no lookup at all. Old content
|
|
9
|
+
* without the prop renders exactly as before.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { buildSrcset, lookupAsset } from '../../../utils/mediaApi';
|
|
13
|
+
|
|
14
|
+
/** Full-bleed imagery (background heroes, banners). */
|
|
15
|
+
export const SIZES_FULL = '100vw';
|
|
16
|
+
|
|
17
|
+
/** Half-width split layouts (side hero, media object) — full width stacked. */
|
|
18
|
+
export const SIZES_HALF = '(min-width: 1024px) 50vw, 100vw';
|
|
19
|
+
|
|
20
|
+
/** Field for the persisted srcset — visible so an author can override it. */
|
|
21
|
+
export const imageSrcsetField = {
|
|
22
|
+
type: 'textarea',
|
|
23
|
+
label: 'Image srcset (auto-filled from the media library)',
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/** Spreadable `<img>` attributes; nothing when no srcset is recorded. */
|
|
27
|
+
export function srcsetAttrs(
|
|
28
|
+
srcset: string | undefined,
|
|
29
|
+
sizes: string,
|
|
30
|
+
): { srcSet: string; sizes: string } | Record<string, never> {
|
|
31
|
+
return srcset ? { srcSet: srcset, sizes } : {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type SrcsetProps = { imageUrl: string; imageSrcset?: string };
|
|
35
|
+
|
|
36
|
+
/** `resolveData` for any widget with `imageUrl` + `imageSrcset` props.
|
|
37
|
+
* `changed` is typed to just the key read here — Puck's own param type
|
|
38
|
+
* (which also carries `id: string`) stays assignable. */
|
|
39
|
+
export async function resolveImageSrcset<P extends SrcsetProps>(
|
|
40
|
+
{ props }: { props: P },
|
|
41
|
+
{ changed }: { changed: { imageUrl?: unknown } },
|
|
42
|
+
): Promise<{ props: P }> {
|
|
43
|
+
if (!changed.imageUrl) return { props };
|
|
44
|
+
if (!props.imageUrl) return { props: { ...props, imageSrcset: '' } };
|
|
45
|
+
let asset: Awaited<ReturnType<typeof lookupAsset>>;
|
|
46
|
+
try {
|
|
47
|
+
asset = await lookupAsset(props.imageUrl);
|
|
48
|
+
} catch {
|
|
49
|
+
// Network failure mid-edit shouldn't break the editor — keep the picked
|
|
50
|
+
// URL and let the image serve without variants.
|
|
51
|
+
return { props };
|
|
52
|
+
}
|
|
53
|
+
if (!asset) return { props };
|
|
54
|
+
return { props: { ...props, imageSrcset: buildSrcset(asset) } };
|
|
55
|
+
}
|
|
@@ -26,6 +26,13 @@ export type {
|
|
|
26
26
|
HeadingTone,
|
|
27
27
|
} from './heading';
|
|
28
28
|
export { Heading } from './heading';
|
|
29
|
+
export {
|
|
30
|
+
imageSrcsetField,
|
|
31
|
+
resolveImageSrcset,
|
|
32
|
+
SIZES_FULL,
|
|
33
|
+
SIZES_HALF,
|
|
34
|
+
srcsetAttrs,
|
|
35
|
+
} from './image-srcset';
|
|
29
36
|
export {
|
|
30
37
|
EYEBROW_CASE_STYLE,
|
|
31
38
|
eyebrowTextStyle,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Prop shape for HeroWidget — extracted so hero-widget.tsx stays under the
|
|
2
|
+
* repo's 300-line cap. Re-exported from there, so importers are unaffected. */
|
|
3
|
+
|
|
4
|
+
export type HeroWidgetProps = {
|
|
5
|
+
eyebrow: string;
|
|
6
|
+
title: string;
|
|
7
|
+
subtitle: string;
|
|
8
|
+
logoUrl: string;
|
|
9
|
+
logoAlt: string;
|
|
10
|
+
imageUrl: string;
|
|
11
|
+
imageAlt: string;
|
|
12
|
+
/** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
|
|
13
|
+
imageSrcset?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Organic mask (SVG url, from the tenant's content) applied to the side
|
|
16
|
+
* image in the right/left layouts. Empty = the standard rounded media.
|
|
17
|
+
*/
|
|
18
|
+
imageMaskUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* "native" renders supplied transparent artwork exactly as-is — no mask, no
|
|
21
|
+
* crop — for a photo already cut to its final organic shape. Preferred over
|
|
22
|
+
* `imageMaskUrl`, which re-derives the silhouette from a hand-kept SVG.
|
|
23
|
+
*/
|
|
24
|
+
imageShape?: 'rounded' | 'native';
|
|
25
|
+
imagePosition: 'background' | 'right' | 'left';
|
|
26
|
+
primaryLabel: string;
|
|
27
|
+
primaryHref: string;
|
|
28
|
+
secondaryLabel: string;
|
|
29
|
+
secondaryHref: string;
|
|
30
|
+
overlay: boolean;
|
|
31
|
+
align: 'left' | 'center';
|
|
32
|
+
surface: 'plain' | 'card';
|
|
33
|
+
};
|
|
@@ -8,38 +8,17 @@ import {
|
|
|
8
8
|
CTAButton,
|
|
9
9
|
EyebrowText,
|
|
10
10
|
Heading,
|
|
11
|
+
imageSrcsetField,
|
|
11
12
|
maskStyle,
|
|
13
|
+
resolveImageSrcset,
|
|
12
14
|
Section,
|
|
15
|
+
SIZES_HALF,
|
|
16
|
+
srcsetAttrs,
|
|
13
17
|
} from './_shared';
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
subtitle: string;
|
|
19
|
-
logoUrl: string;
|
|
20
|
-
logoAlt: string;
|
|
21
|
-
imageUrl: string;
|
|
22
|
-
imageAlt: string;
|
|
23
|
-
/**
|
|
24
|
-
* Organic mask (SVG url, from the tenant's content) applied to the side
|
|
25
|
-
* image in the right/left layouts. Empty = the standard rounded media.
|
|
26
|
-
*/
|
|
27
|
-
imageMaskUrl?: string;
|
|
28
|
-
/**
|
|
29
|
-
* "native" renders supplied transparent artwork exactly as-is — no mask, no
|
|
30
|
-
* crop — for a photo already cut to its final organic shape. Preferred over
|
|
31
|
-
* `imageMaskUrl`, which re-derives the silhouette from a hand-kept SVG.
|
|
32
|
-
*/
|
|
33
|
-
imageShape?: 'rounded' | 'native';
|
|
34
|
-
imagePosition: 'background' | 'right' | 'left';
|
|
35
|
-
primaryLabel: string;
|
|
36
|
-
primaryHref: string;
|
|
37
|
-
secondaryLabel: string;
|
|
38
|
-
secondaryHref: string;
|
|
39
|
-
overlay: boolean;
|
|
40
|
-
align: 'left' | 'center';
|
|
41
|
-
surface: 'plain' | 'card';
|
|
42
|
-
};
|
|
19
|
+
import type { HeroWidgetProps } from './hero-widget-props';
|
|
20
|
+
|
|
21
|
+
export type { HeroWidgetProps } from './hero-widget-props';
|
|
43
22
|
|
|
44
23
|
// Exported so CenteredHeroWidget can render through it without smuggling
|
|
45
24
|
// Puck's ComponentConfig.render signature past the type system.
|
|
@@ -51,6 +30,7 @@ export function renderHero({
|
|
|
51
30
|
logoAlt,
|
|
52
31
|
imageUrl,
|
|
53
32
|
imageAlt,
|
|
33
|
+
imageSrcset = '',
|
|
54
34
|
imageMaskUrl = '',
|
|
55
35
|
imageShape = 'rounded',
|
|
56
36
|
imagePosition,
|
|
@@ -120,6 +100,7 @@ export function renderHero({
|
|
|
120
100
|
<BackgroundMedia
|
|
121
101
|
imageUrl={imageUrl}
|
|
122
102
|
imageAlt={imageAlt}
|
|
103
|
+
imageSrcset={imageSrcset}
|
|
123
104
|
overlay={overlay ? 'dark' : 'none'}
|
|
124
105
|
fallback="gradient-mesh"
|
|
125
106
|
objectPosition="var(--pb-hero-image-position, 50% 50%)"
|
|
@@ -189,6 +170,7 @@ export function renderHero({
|
|
|
189
170
|
// Figma's box width. No mask, no crop — the file IS the shape.
|
|
190
171
|
<img
|
|
191
172
|
src={imageUrl}
|
|
173
|
+
{...srcsetAttrs(imageSrcset, SIZES_HALF)}
|
|
192
174
|
alt={imageAlt || title}
|
|
193
175
|
loading="lazy"
|
|
194
176
|
className="h-auto w-full max-w-[var(--pb-hero-media-max-w,none)]"
|
|
@@ -199,6 +181,7 @@ export function renderHero({
|
|
|
199
181
|
// unset, the image keeps its previous free-flowing size.
|
|
200
182
|
<img
|
|
201
183
|
src={imageUrl}
|
|
184
|
+
{...srcsetAttrs(imageSrcset, SIZES_HALF)}
|
|
202
185
|
alt={imageAlt || title}
|
|
203
186
|
loading="lazy"
|
|
204
187
|
className="h-auto w-full max-w-[var(--pb-hero-media-max-w,none)] object-cover"
|
|
@@ -208,7 +191,13 @@ export function renderHero({
|
|
|
208
191
|
}}
|
|
209
192
|
/>
|
|
210
193
|
) : (
|
|
211
|
-
<ContentMedia
|
|
194
|
+
<ContentMedia
|
|
195
|
+
imageUrl={imageUrl}
|
|
196
|
+
imageAlt={imageAlt}
|
|
197
|
+
imageSrcset={imageSrcset}
|
|
198
|
+
sizes={SIZES_HALF}
|
|
199
|
+
aspect="square"
|
|
200
|
+
/>
|
|
212
201
|
)}
|
|
213
202
|
</div>
|
|
214
203
|
</div>
|
|
@@ -226,6 +215,7 @@ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
|
|
|
226
215
|
logoAlt: { type: 'text', label: 'Logo alt text' },
|
|
227
216
|
imageUrl: createImageField(mediaLibraryAdapter, 'Image'),
|
|
228
217
|
imageAlt: { type: 'text', label: 'Image alt text' },
|
|
218
|
+
imageSrcset: imageSrcsetField,
|
|
229
219
|
imageMaskUrl: {
|
|
230
220
|
type: 'text',
|
|
231
221
|
label: 'Image mask URL (organic shape, side layouts — optional)',
|
|
@@ -277,6 +267,7 @@ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
|
|
|
277
267
|
logoAlt: '',
|
|
278
268
|
imageUrl: '',
|
|
279
269
|
imageAlt: '',
|
|
270
|
+
imageSrcset: '',
|
|
280
271
|
imageMaskUrl: '',
|
|
281
272
|
imageShape: 'rounded',
|
|
282
273
|
imagePosition: 'background',
|
|
@@ -288,5 +279,6 @@ export const HeroWidget: ComponentConfig<HeroWidgetProps> = {
|
|
|
288
279
|
align: 'center',
|
|
289
280
|
surface: 'plain',
|
|
290
281
|
},
|
|
282
|
+
resolveData: resolveImageSrcset,
|
|
291
283
|
render: renderHero,
|
|
292
284
|
};
|
|
@@ -5,6 +5,8 @@ import type { CSSProperties } from 'react';
|
|
|
5
5
|
export type MediaObjectWidgetProps = {
|
|
6
6
|
imageUrl: string;
|
|
7
7
|
imageAlt: string;
|
|
8
|
+
/** Variant srcset recorded at pick time (see _shared/image-srcset.ts). */
|
|
9
|
+
imageSrcset?: string;
|
|
8
10
|
imagePosition: 'left' | 'right';
|
|
9
11
|
heading: string;
|
|
10
12
|
body: string;
|
|
@@ -9,7 +9,7 @@ import { cn } from '../../utils/widgetUtils';
|
|
|
9
9
|
import { AccentText } from './_internal/accent-text';
|
|
10
10
|
import { LinkArrow } from './_internal/link-arrow';
|
|
11
11
|
import { RichTextBlock, renderRichText } from './_internal/rich-text';
|
|
12
|
-
import { CTAButton, eyebrowTextStyle, maskStyle } from './_shared';
|
|
12
|
+
import { CTAButton, eyebrowTextStyle, maskStyle, SIZES_HALF, srcsetAttrs } from './_shared';
|
|
13
13
|
|
|
14
14
|
import { GRID_IMAGE, GRID_TEXT, type MediaObjectWidgetProps } from './media-object-layout';
|
|
15
15
|
import { MediaObjectText } from './media-object-text';
|
|
@@ -19,6 +19,7 @@ export type { MediaObjectWidgetProps } from './media-object-layout';
|
|
|
19
19
|
export const MediaObjectWidgetRender = ({
|
|
20
20
|
imageUrl,
|
|
21
21
|
imageAlt,
|
|
22
|
+
imageSrcset = '',
|
|
22
23
|
imageMaskUrl = '',
|
|
23
24
|
imageShape = 'rounded',
|
|
24
25
|
imageTag = '',
|
|
@@ -86,6 +87,7 @@ export const MediaObjectWidgetRender = ({
|
|
|
86
87
|
const imageEl = imageUrl ? (
|
|
87
88
|
<img
|
|
88
89
|
src={imageUrl}
|
|
90
|
+
{...srcsetAttrs(imageSrcset, SIZES_HALF)}
|
|
89
91
|
alt={imageAlt || heading}
|
|
90
92
|
loading="lazy"
|
|
91
93
|
className={cn('w-full h-auto', !masked && !native && 'rounded-lg')}
|
|
@@ -196,6 +198,8 @@ export const MediaObjectWidgetRender = ({
|
|
|
196
198
|
{imageUrl && (
|
|
197
199
|
<img
|
|
198
200
|
src={imageUrl}
|
|
201
|
+
// Rendered 68px tall — the smallest variant is plenty.
|
|
202
|
+
{...srcsetAttrs(imageSrcset, '320px')}
|
|
199
203
|
alt={imageAlt || heading}
|
|
200
204
|
loading="lazy"
|
|
201
205
|
className="mt-8 h-[68px] w-auto max-w-full object-contain object-left"
|