:path',
'email_utility_description' => 'Check email configuration settings and send test emails.',
'entry_count' => ':count entry|:count entries',
'entry_origin_instructions' => 'The new localization will inherit values from the entry in the selected site.',
'expect_root_instructions' => 'Consider the first page in the tree a "root" or "home" page.',
- 'field_conditions_always_save_instructions' => 'Always save field value, even if the field is hidden.',
+ 'field_conditions_always_save_instructions' => 'Always save the field value, even if the field is hidden.',
+ 'field_conditions_reserve_space_when_hidden_instructions' => 'When hidden, reserve the field\'s space in the layout.',
'field_conditions_field_instructions' => 'You may enter any field handle. You are not limited to the options in the dropdown.',
'field_conditions_instructions' => 'When to show or hide this field.',
'field_conditions_reserve_space_when_hidden_instructions' => 'When hidden, reserve the field\'s space in the layout.',
@@ -123,25 +130,47 @@
'forgot_password_enter_email' => 'Enter your email address to receive a password reset link.',
'forgot_password_sent' => 'If an account with that email address exists, a password reset link has been sent.',
'form_configure_blueprint_instructions' => 'Choose from existing blueprints or create a new one.',
- 'form_configure_email_attachments_instructions' => 'Attach uploaded assets to this email.',
- 'form_configure_email_bcc_instructions' => 'Email address of the BCC recipient(s) - comma separated.',
- 'form_configure_email_cc_instructions' => 'Email address of the CC recipient(s) - comma separated.',
+ 'form_configure_email_attachments_instructions' => 'Attach files uploaded with the submission.',
+ 'form_configure_email_body_instructions' => 'Type @ to include submitted values. Leave blank to list all submitted fields instead.',
'form_configure_email_from_instructions' => 'Leave blank to fall back to the site default.',
- 'form_configure_email_html_instructions' => 'The view for the HTML version of this email.',
- 'form_configure_email_instructions' => 'Configure emails to be sent when new form submissions are received.',
+ 'form_configure_email_views_instructions' => 'Leave both blank to use the default layout.',
'form_configure_email_description' => 'Be sure your email settings are configured to send and receive notifications when forms are submitted.',
'form_configure_email_markdown_instructions' => 'Render the HTML version of this email using Markdown.',
- 'form_configure_email_reply_to_instructions' => 'Leave blank to fall back to sender.',
- 'form_configure_email_subject_instructions' => 'Email subject line.',
- 'form_configure_email_text_instructions' => 'The view for the text version of this email.',
- 'form_configure_email_to_instructions' => 'Email address of the recipient(s) - comma separated.',
+ 'form_configure_email_reply_to_instructions' => 'Leave blank to fall back to the sender.',
+ 'form_configure_email_to_instructions' => 'Choose a form field or enter an email address.',
'form_configure_handle_instructions' => 'Used to reference this form on the frontend. This cannot be easily changed later.',
+ 'form_configure_honeypot_behavior_instructions' => 'What happens to submissions caught by the honeypot. They can be silently discarded, or stored and marked as spam for review.',
'form_configure_honeypot_instructions' => 'Field name to use as a honeypot. Honeypots are special fields used to reduce bot spam.',
'form_configure_intro' => 'Forms collect information from visitors and can trigger events and send notifications when submissions are received.',
- 'form_configure_mailer_instructions' => 'Choose the mailer for sending this email. Leave blank to fall back to the default mailer.',
+ 'form_configure_mailer_instructions' => 'Leave blank to fall back to the default mailer.',
+ 'form_configure_generate_fake_submissions_instructions' => 'Allow generating fake submissions and workflow testing from the submissions screen.',
'form_configure_store_instructions' => 'Disable to stop storing submissions. Events and email notifications will still be sent.',
'form_configure_title_instructions' => 'Use a call to action, such as \'Contact Us\'.',
+ 'form_configure_close_date_instructions' => 'The form will stop accepting submissions after this date. Leave blank to never close.',
+ 'form_configure_submission_limit_instructions' => 'The maximum number of submissions to accept. Leave blank for no limit.',
+ 'form_configure_submission_limit_period_instructions' => 'How often the submission limit resets.',
+ 'form_configure_closed_message_instructions' => 'Shown when the form is closed or the submission limit has been reached. Leave blank to use the default message.',
+ 'form_configure_require_login_instructions' => 'Only allow logged in users to submit this form.',
+ 'form_configure_require_login_message_instructions' => 'Shown when a logged out visitor tries to submit the form. Leave blank to use the default message.',
+ 'form_closed_message' => 'This form is no longer accepting submissions.',
+ 'form_require_login_message' => 'You must be logged in to submit this form.',
'form_create_description' => 'Get started by creating your first form.',
+ 'form_builder' => 'Form Builder',
+ 'form_builder_empty_inspector' => 'Drag fields onto the form to add them. Click a field, section, or page to edit its settings here.',
+ 'form_fake_submissions_deleted' => 'Deleted :count fake submission.|Deleted :count fake submissions.',
+ 'form_fake_submissions_delete_failed' => 'Fake submissions could not be deleted.',
+ 'form_fake_submissions_form_not_found' => 'Form could not be found for deleting fake submissions.',
+ 'form_fake_submissions_none_to_delete' => 'No fake submissions to delete.',
+ 'form_fake_submissions_partial_delete' => ':success/:total fake submissions were deleted.',
+ 'form_fake_submission_generation_disabled' => 'Generating fake submissions is disabled for this form.',
+ 'form_fake_submission_cancelled' => 'Fake submission was cancelled by a form event listener.',
+ 'form_fake_submission_generated' => 'Fake submission generated.',
+ 'form_fields_handle_instructions' => 'Auto-generated from the label. It can\'t be changed after saving the form.',
+ 'form_fields_instructions_instructions' => 'Additional field instructions like this.',
+ 'form_navigation' => 'Form navigation',
+ 'form_summary_add_chart_instructions' => 'Add a chart to start summarizing responses.',
+ 'form_summary_checked_insight' => 'of users checked the toggle',
+ 'form_summary_other_breakdown' => ':field: Other breakdown: :values',
'form_export_filtered_description' => 'Exports only the submissions matching the current search and filters.',
'getting_started_widget_collections' => 'Collections hold the different content types that make up your site, helping you stay organized.',
'getting_started_widget_docs' => 'Discover everything Statamic can do, and learn how to use its powerful features the right way.',
@@ -184,6 +213,7 @@
'licensing_trial_mode_alert_addons' => 'This site is using commercial addons in trial mode. Valid licenses will be required when you\'re ready to launch.',
'licensing_trial_mode_alert_statamic' => 'Thanks for trying Statamic Pro! This site is currently in trial mode — please enter a license before your site goes live.',
'licensing_utility_description' => 'View and resolve licensing details.',
+ 'mark_as_not_spam_action_confirmation' => 'Submissions that were caught before being finalized will be finalized, triggering any configured connections.',
'max_depth_instructions' => 'Set the max page nesting level.',
'max_items_instructions' => 'Set a maximum number of selectable items.',
'navigation_configure_blueprint_instructions' => 'Choose from existing blueprints or create a new one.',
@@ -313,5 +343,10 @@
'user_wizard_roles_groups_intro' => 'Users can be assigned to roles that customize their permissions, access, and abilities throughout the Control Panel.',
'user_wizard_super_admin_instructions' => 'Super admins have complete control and access to everything in the control panel. Grant this role wisely.',
'video_fieldtype_cloudflare_manifest' => 'Cloudflare Stream manifest URLs can\'t be embedded. Use the URL from the "Video Link" button in Cloudflare instead.',
+ 'webhook_connection_delete_confirmation' => 'Are you sure you want to delete this webhook?',
+ 'webhook_connection_instructions' => 'Each webhook is sent when this form is submitted. Notify other services or trigger automations with the submission data.',
+ 'webhook_connection_payload_instructions' => 'Each submission is sent to the webhook URL as a JSON POST request.',
+ 'webhook_connection_verify_ssl_instructions' => 'Only disable this if the receiving server uses a self-signed certificate.',
+ 'webhook_connector_description' => 'Send submissions to an external URL.',
'width_x_height' => ':width × :height',
];
diff --git a/lang/en/permissions.php b/lang/en/permissions.php
index dae704fcd3e..4c8f42fd931 100644
--- a/lang/en/permissions.php
+++ b/lang/en/permissions.php
@@ -72,10 +72,18 @@
'group_forms' => 'Forms',
'configure_forms' => 'Configure forms',
'configure_forms_desc' => 'Grants access to all form related permissions',
- 'configure_form_fields' => 'Configure Form Fields',
- 'configure_form_fields_desc' => 'Ability to edit form blueprints, fieldsets, and their fields.',
+ 'edit_forms' => 'Edit forms',
+ 'edit_forms_desc' => 'Grants access to all forms, except their submissions',
+ 'create_forms' => 'Create forms',
+ 'delete_forms' => 'Delete forms',
+ 'view_form_submissions' => 'View submissions',
+ 'view_form_submissions_desc' => 'Grants access to submissions across all forms',
+ 'delete_form_submissions' => 'Delete submissions',
+ 'manage_form_spam' => 'Manage spam',
+ 'edit_{form}_form' => 'Edit :form form',
'view_{form}_form_submissions' => 'View :form submissions',
'delete_{form}_form_submissions' => 'Delete :form submissions',
+ 'manage_{form}_form_spam' => 'Manage :form spam',
'group_users' => 'Users',
'view_users' => 'View users',
diff --git a/lang/en/validation.php b/lang/en/validation.php
index 02a87bf05ca..d6806db6662 100644
--- a/lang/en/validation.php
+++ b/lang/en/validation.php
@@ -196,7 +196,15 @@
'elevated_session_resend_code_unavailable' => 'Resend code is only available for verification code method.',
'elevated_session_verification_code' => 'The verification code is incorrect.',
'email_available' => 'A user with this email already exists.',
+ 'email_field_reference' => "References a field that doesn't exist on this form.",
'fieldset_imported_recursively' => 'Fieldset :handle is being imported recursively.',
+ 'form_chart_hidden_field' => 'Field :field is hidden and cannot be charted.',
+ 'form_chart_not_applicable' => 'Chart :chart cannot be used for field :field.',
+ 'form_chart_unknown_chart' => 'Chart :chart does not exist.',
+ 'form_chart_unknown_field' => 'Field :field does not exist on this form.',
+ 'form_insight_duplicate' => 'Insight :insight is used more than once for field :field.',
+ 'form_insight_not_applicable' => 'Insight :insight cannot be used for field :field.',
+ 'form_insight_unknown' => 'Insight :insight does not exist.',
'one_site_without_origin' => 'At least one site must not have an origin.',
'options_require_keys' => 'All options must have keys.',
'origin_cannot_be_disabled' => 'Cannot select a disabled origin.',
@@ -211,6 +219,7 @@
'unique_user_value' => 'This value has already been taken.',
'unique_uri' => 'This URI has already been taken.',
'time' => 'Not a valid time.',
+ 'webhook_url_not_public' => 'Webhooks must use a public address.',
'asset_current_filename' => 'This is the current filename.',
'asset_file_exists' => 'A file already exists with this name.',
'asset_file_exists_same_content' => 'A file already exists with this name and has the same content. You may want to delete this rather than rename it.',
diff --git a/packages/cms/src/index.js b/packages/cms/src/index.js
index 8b30e9f288b..cbee0e5568c 100644
--- a/packages/cms/src/index.js
+++ b/packages/cms/src/index.js
@@ -16,6 +16,10 @@ export const {
RevisionPreview,
SaveButtonOptions,
SortableList,
+ ConnectionFields,
+ ConnectionList,
+ ConnectionRules,
+ ConnectionSummary,
requireElevatedSession,
requireElevatedSessionIf,
clone,
diff --git a/packages/cms/src/ui.js b/packages/cms/src/ui.js
index d8c18b5057e..38068c62e78 100644
--- a/packages/cms/src/ui.js
+++ b/packages/cms/src/ui.js
@@ -14,6 +14,7 @@ export const {
CharacterCounter,
Checkbox,
CheckboxGroup,
+ ChoiceGrid,
CodeEditor,
Combobox,
CommandPaletteItem,
@@ -48,6 +49,8 @@ export const {
Field,
Header,
Heading,
+ HorizontalBarChart,
+ HorizontalLollipopChart,
HoverCard,
Icon,
Input,
@@ -78,6 +81,7 @@ export const {
Panel,
PanelFooter,
PanelHeader,
+ PieChart,
Popover,
PublishComponents,
PublishContainer,
@@ -92,6 +96,7 @@ export const {
PublishTabs,
Radio,
RadioGroup,
+ RankList,
Select,
Separator,
Slider,
@@ -99,6 +104,7 @@ export const {
SplitterGroup,
SplitterPanel,
SplitterResizeHandle,
+ StarRating,
StatusIndicator,
Subheading,
Switch,
@@ -125,6 +131,7 @@ export const {
Timezones,
ToggleGroup,
ToggleItem,
+ VerticalBarChart,
Widget,
registerIconSet,
registerIconSetFromStrings,
diff --git a/resources/css/components/fieldtypes/assets.css b/resources/css/components/fieldtypes/assets.css
index f07290edd8f..7fee0ddf4d6 100644
--- a/resources/css/components/fieldtypes/assets.css
+++ b/resources/css/components/fieldtypes/assets.css
@@ -30,6 +30,21 @@
}
}
+/* Single asset picker (e.g. Image Choice option image)
+ ========================================================================== */
+
+/* Remove the double border effect */
+.single_asset-fieldtype .asset-tile {
+ outline: none;
+ img {
+ @apply rounded-[calc(var(--radius-sm)-1px)]!;
+ }
+ :has(> &) {
+ padding: 0;
+ padding-block-end: 3px;
+ }
+}
+
/* In a Bard fieldtype
========================================================================== */
diff --git a/resources/css/components/fieldtypes/table.css b/resources/css/components/fieldtypes/table.css
index 785f31b1ef7..a9dff5e26f4 100644
--- a/resources/css/components/fieldtypes/table.css
+++ b/resources/css/components/fieldtypes/table.css
@@ -3,7 +3,7 @@ Table Fieldtype
========================================================================== */
.table-contained {
- @apply relative mb-4 w-full rounded-lg border shadow-ui-sm outline-hidden dark:border-gray-700 with-contrast:border-gray-500 text-start;
+ @apply relative mb-4 w-full rounded-lg bg-content-bg border shadow-ui-sm outline-hidden dark:border-gray-700 with-contrast:border-gray-500 text-start;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
@@ -107,6 +107,10 @@ Table Fieldtype
@apply border-e-0;
}
+ &:has(+ .visibility-controls) {
+ @apply border-e-0;
+ }
+
&.table-drag-handle {
@apply w-3 p-2 bg-white dark:bg-gray-850;
&:hover {
@@ -129,9 +133,13 @@ Table Fieldtype
@apply not-dark:bg-gray-100 text-gray-600 dark:text-gray-500;
}
- .row-controls {
+ .row-controls,
+ .visibility-controls {
@apply w-8 text-center ps-0;
vertical-align: middle;
+ }
+
+ .row-controls {
&:focus-within {
@apply bg-gray-100 dark:bg-gray-700;
button {
diff --git a/resources/css/components/forms.css b/resources/css/components/forms.css
new file mode 100644
index 00000000000..269f65d46ab
--- /dev/null
+++ b/resources/css/components/forms.css
@@ -0,0 +1,417 @@
+[data-ui-input-group] {
+ /* Lock in logic icons */
+ position: relative;
+}
+
+/* GROUP MAIN COLUMN / FIELD BACKGROUND DECORATION
+=================================================== */
+[data-editing-field] {
+ position: relative;
+ scroll-margin-top: 7rem;
+ > * {
+ /* Pull everything inside above */
+ position: relative;
+ }
+ /* Field Editing */
+ &:not(button) {
+ &::before {
+ content: '';
+ position: absolute;
+ /* Bit of optical alignment here */
+ inset: -0.5rem calc(-1rem - 2px) -0.75rem;
+ border: 1px dashed var(--color-blue-400);
+ border-radius: 10px;
+ @apply bg-blue-50;
+ }
+ &:has([data-form-banner]) {
+ &::before {
+ /* Exception for the banner since it's more uniform */
+ inset-block-end: -0.5rem;
+ }
+ &::before,
+ [data-editing-field-actions] {
+ background: transparent;
+ }
+ }
+ &[data-first-row]::before {
+ inset-block-start: -1.25rem;
+ }
+ &[data-last-row]::before {
+ inset-block-end: -1.25rem;
+ }
+ :where(.dark) &::before {
+ @apply bg-blue-950 border-blue-500/75;
+ }
+ }
+ /* Pull the field actions up a bit for the first row */
+ &[data-first-row] {
+ [data-editing-field-actions] {
+ top: -0.85rem;
+ }
+ }
+ /* Editing a button like "Next page" and "Submit" */
+ :has(> button&) {
+ position: relative;
+ &::before {
+ content: '';
+ position: absolute;
+ inset: -1rem calc(-1rem - 2px) -1.25rem;
+ border: 1px dashed var(--color-blue-400);
+ border-radius: 10px;
+ @apply bg-blue-50;
+ }
+ :where(.dark) &::before {
+ @apply bg-blue-950 border-blue-500/75;
+ }
+ }
+}
+
+
+/* GROUP MAIN COLUMN / FIELDSETS
+=================================================== */
+[data-fieldset-group] {
+ anchor-scope: --fieldset-start, --fieldset-end;
+}
+
+[data-fieldset-label] {
+ @apply bg-content-bg text-indigo-800 dark:text-indigo-400/85;
+ position: absolute;
+ display: inline-flex;
+ top: calc(anchor(--fieldset-start top) + 2rem);
+ left: calc(anchor(--fieldset-start left) - 2.5rem);
+ width: 0.75rem;
+ svg {
+ rotate: 90deg;
+ }
+}
+
+#fieldset-start {
+ anchor-name: --fieldset-start;
+
+ /* The connector */
+ &::before {
+ @apply border border-dashed border-indigo-500;
+ content: '';
+ position: absolute;
+ width: 3rem;
+ top: calc(anchor(--fieldset-start top) + 0.5rem);
+ left: calc(anchor(--fieldset-start left) - 2rem);
+ bottom: anchor(--fieldset-end bottom);
+ border-radius: 10px;
+ clip-path: polygon(0 0, 30% 0, 30% 100%, 0 100%);
+ }
+}
+
+:where(.dark) #fieldset-start::before {
+ @apply border-indigo-400/50;
+}
+
+#fieldset-end {
+ anchor-name: --fieldset-end;
+}
+
+/* Hide anchors and absolutely positioned logic icons unless the panel is open. When the panel collapses, elements like anchors are slow to hide because of the way they're rendered. */
+[data-panel-collapsed="true"] {
+ [data-fieldset-label],
+ [data-logic-attached],
+ #fieldset-start::before {
+ opacity: 0;
+ transition: opacity 0.05s ease-in;
+ }
+}
+
+
+/* GROUP MAIN COLUMN / COLLAPSIBLE FIELDS
+=================================================== */
+/* Field Icons (hide by default) */
+[data-collapsed-field-icon] {
+ display: none;
+}
+
+/* GROUP MAIN COLUMN / COLLAPSIBLE FIELDS / COLLAPSED STATE
+=================================================== */
+[data-fields-collapsed="true"] {
+ /* Hide the inputs */
+ [data-ui-input-group] {
+ [data-ui-description],
+ > :not(:first-child, :has([data-logic-attached])) {
+ display: none;
+ }
+
+ * {
+ margin-bottom: 0;
+ }
+ }
+
+ [data-editing-item] {
+ /* Tweak the actions position since the surrounding space is different */
+ [data-editing-field-actions] {
+ top: 50%;
+ transform: translateY(calc(-50% - 0.5px));
+ @apply pe-3;
+ }
+
+ &[data-first-row]::before,
+ &[data-last-row]::before,
+ &::before {
+ inset: calc(0% - 1px);
+ }
+ }
+
+ /* Field Icons */
+ [data-collapsed-field-icon] {
+ display: inline-flex;
+ }
+
+ /* Compress spacing for fieldsets when the fields are collapsed */
+ [data-fieldset-group] {
+ @apply space-y-5;
+ }
+
+ /* Wrapper/white background */
+ [data-section-drop-zone]:has(&) {
+ background: unset;
+ box-shadow: unset;
+ padding: 0;
+ }
+
+ [data-field-item] {
+ @apply px-3 sm:px-5 py-2.75 bg-white dark:bg-gray-850 rounded-xl ring ring-gray-200 dark:ring-gray-700/80 shadow-ui-md;
+ }
+
+ ~ [data-pagination] {
+ @apply mt-4;
+ /* Editing a button like "Next page" and "Submit" */
+ :has(> button)::before {
+ inset: calc(0% - 1px);
+ }
+ }
+}
+
+
+/* GROUP SIDEBAR / EDITING ITEM DIRECTION
+=================================================== */
+/* No decoration of the editing item itself, just a hook to handle the editing item direction indicators. */
+#main-content {
+ timeline-scope: --editing-field-scroll;
+}
+[data-editing-item] {
+ view-timeline-name: --editing-field-scroll;
+}
+
+/* Page 1 of 2, etc. */
+[data-form-page-label],
+/* Next/previous page buttons */
+[data-pagination] {
+ scroll-margin-top: 7rem;
+}
+
+[data-field-direction-down],
+[data-field-direction-up] {
+ @apply p-1 size-4.75! ms-1 text-blue-500 dark:text-blue-400 bg-blue-50 dark:bg-blue-950 border-1 border-dotted border-blue-400 dark:border-blue-700 rounded-full;
+}
+
+@keyframes field-direction-fade-in {
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+[data-field-direction-up] {
+ opacity: 0;
+ animation: field-direction-fade-in linear both;
+ animation-timeline: --editing-field-scroll;
+ animation-range: exit 0%, exit 100%;
+}
+
+@keyframes field-direction-fade-out {
+ to {
+ opacity: 0;
+ }
+}
+
+[data-field-direction-down] {
+ animation: field-direction-fade-out linear both;
+ animation-timeline: --editing-field-scroll;
+ animation-range: entry 50%, entry 100%;
+}
+
+
+/* GROUP SIDEBAR / LOGIC TEXT / VERTICAL / LAYOUT
+=================================================== */
+.logic-text {
+ --inner-indent: 1.75rem;
+ :where(.dark) & {
+ --logic-line: var(--color-gray-600);
+ }
+
+ /* Outer structure */
+ > ol {
+ /* Optically align */
+ margin-inline-start: -0.1rem;
+ /* Inner structure */
+ ol {
+ display: grid;
+ grid-template-columns: 100%;
+ @apply gap-2 mb-2.5;
+ &:not(.indented-condition *) {
+ margin-inline-start: var(--inner-indent);
+ }
+ li {
+ /* Prevent full-width comboboxes */
+ > .w-full {
+ display: inline-flex;
+ width: unset;
+ min-width: unset;
+ max-width: 100%;
+ }
+ }
+ }
+ }
+ /* Make the input field automatically adjust to the content size */
+ [data-ui-input] input {
+ field-sizing: content;
+ }
+}
+
+.logic-text-badge {
+ @apply
+ py-1.5 px-3
+ bg-content-bg
+ text-gray-800 dark:text-gray-300
+ rounded-full
+ text-sm
+ shadow-ui-sm
+ border-1
+ border-gray-300 dark:border-gray-600
+ ;
+ display: inline-block;
+ user-select: none;
+ text-align: center;
+}
+
+.logic-text__pill {
+ display: flex;
+ max-width: fit-content;
+ align-items: center;
+ @apply
+ gap-1
+ rounded-full
+ bg-content-bg
+ px-3 py-1
+ shadow-ui-sm
+ text-gray-900 dark:text-gray-100
+ border-1
+ border-gray-300
+ dark:border-1 dark:border-gray-600
+ ;
+}
+
+.logic-text__pill-icon {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+}
+
+.logic-text__pill-text {
+ @apply
+ text-sm
+ text-gray-900 dark:text-gray-100
+ ;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ user-select: none;
+}
+
+/* GROUP SIDEBAR / LOGIC TEXT / VERTICAL / CONNECTORS
+=================================================== */
+.logic-text {
+ --logic-line: 1px dashed var(--color-gray-400);
+ --outer-indent-connector: calc(1rem - 2px);
+ @apply mb-0;
+ /* Inner structure */
+ ol ol {
+ position: relative;
+ &::before {
+ content: '';
+ position: absolute;
+ z-index: var(--z-index-below);
+ inset-inline-start: 1.15rem;
+ border-inline-start: var(--logic-line);
+ height: 100%;
+ }
+ }
+ /* Exit "If" */
+ > ol > li {
+ &:not(:last-child) {
+ + li {
+ position: relative;
+ &::before {
+ content: '';
+ position: absolute;
+ z-index: var(--z-index-below);
+ top: -1.5rem;
+ left: calc(0% + var(--inner-indent) / 2);
+ width: calc(var(--spacing) * 4);
+ height: 3.5rem;
+ border: var(--logic-line);
+ border-inline-end: 0;
+ border-block-end: 0;
+ border-start-start-radius: 100vw;
+ }
+ }
+ }
+ }
+}
+.logic-text__condition {
+ position: relative;
+ /* e.g. not a "to" badge inside a calculation condition */
+ &:not(ol ol *) {
+ @apply mb-2.5;
+ &::after {
+ content: '';
+ position: absolute;
+ left: var(--outer-indent-connector);
+ border: var(--logic-line);
+ border-inline-end: 0;
+ border-block-start: 0;
+ border-end-start-radius: 100vw;
+ top: 100%;
+ width: calc(var(--spacing) * 4);
+ /* Don't overlap/clash with subsequent connectors */
+ height: 85%;
+ }
+ }
+}
+
+/* GROUP SIDEBAR / LOGIC TEXT / VERTICAL / HORIZONTAL ORIENTATION
+=================================================== */
+@container (width >= 500px) {
+ .logic-text {
+ /* Outer structure */
+ > ol {
+ > li {
+ &:not(:last-child) {
+ li:last-child {
+ /* Pull logic connectors back to the left */
+ position: unset;
+ }
+ }
+ }
+ /* Inner structure */
+ ol {
+ display: flex;
+ flex-wrap: wrap;
+ &::before {
+ /* Hide connectors for now-horizontal logic */
+ content: unset;
+ }
+ }
+ }
+ }
+}
diff --git a/resources/css/components/forms/charts.css b/resources/css/components/forms/charts.css
new file mode 100644
index 00000000000..63dbfc77a42
--- /dev/null
+++ b/resources/css/components/forms/charts.css
@@ -0,0 +1,323 @@
+/* GROUP CHARTS / PIE CHART
+=================================================== */
+.pie-chart-figure {
+ display: grid;
+ grid-template-columns: 0.6fr 1fr;
+ @apply gap-4 p-6;
+}
+
+.pie-chart-legend {
+ @apply pt-4;
+}
+
+.pie-chart-legend__list {
+ @apply grid gap-2.25 text-xs text-gray-700 dark:text-gray-50;
+}
+
+.pie-chart-legend__item {
+ @apply flex items-center gap-2.25;
+}
+
+.pie-chart-legend__value {
+ @apply min-w-7 text-end text-[0.785rem] font-medium;
+}
+
+.pie-chart-legend__swatch {
+ @apply size-2.5 shrink-0 rounded-full;
+}
+
+.pie-chart-legend__swatch--1 { @apply bg-chart-1-legend; }
+.pie-chart-legend__swatch--2 { @apply bg-chart-2-legend; }
+.pie-chart-legend__swatch--3 { @apply bg-chart-3-legend; }
+.pie-chart-legend__swatch--4 { @apply bg-chart-4-legend; }
+.pie-chart-legend__swatch--5 { @apply bg-chart-5-legend; }
+
+.pie-chart-legend__link,
+.summary-bar-chart__link {
+ @apply contents cursor-pointer text-inherit text-start;
+}
+
+.pie-chart {
+ --label-radius: 30%;
+
+ position: relative;
+ width: 9rem;
+ aspect-ratio: 1 / 1;
+
+ /* Slice sizes as unitless numbers (percent of circle). */
+ --end1: calc(var(--1) * 1%);
+ --end2: calc((var(--1) + var(--2)) * 1%);
+ --end3: calc((var(--1) + var(--2) + var(--3)) * 1%);
+ --end4: 100%;
+
+ /* Midpoint angle for each slice (100% = 360deg). */
+ --angle1: calc(var(--1) * 3.6deg / 2);
+ --angle2: calc((var(--1) + var(--2) / 2) * 3.6deg);
+ --angle3: calc((var(--1) + var(--2) + var(--3) / 2) * 3.6deg);
+ --angle4: calc((var(--1) + var(--2) + var(--3) + var(--4) / 2) * 3.6deg);
+
+ &:hover {
+ .pie-chart__label {
+ opacity: unset;
+ }
+ }
+}
+
+.pie-chart__disc {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background-image: conic-gradient(
+ var(--slice-1-color, var(--color-chart-1)) 0% var(--end1),
+ var(--slice-2-color, var(--color-chart-2)) var(--end1) var(--end2),
+ var(--slice-3-color, var(--color-chart-3)) var(--end2) var(--end3),
+ var(--slice-4-color, var(--color-chart-4)) var(--end3) var(--end4)
+ );
+}
+
+.pie-chart__label {
+ --angle: 0deg;
+ opacity: 0;
+ transition: opacity 0.2s ease-in 0s;
+
+ padding: 0.075rem 0.15rem;
+ border-radius: 0.25rem;
+
+ position: absolute;
+ left: calc(50% + var(--label-radius) * sin(var(--angle)));
+ top: calc(50% - var(--label-radius) * cos(var(--angle)));
+ transform: translate(-50%, -50%);
+
+ @apply text-xs font-semibold tabular-nums text-white pointer-events-none;
+}
+
+.pie-chart__label--1 { --angle: var(--angle1); background-color: var(--color-chart-1-label-bg); }
+.pie-chart__label--2 { --angle: var(--angle2); background-color: var(--color-chart-2-label-bg); }
+.pie-chart__label--3 { --angle: var(--angle3); background-color: var(--color-chart-3-label-bg); }
+/* Kick the radius out a bit because the label segment is smaller, and it might overlap with the preceding segment. */
+.pie-chart__label--4 { --angle: var(--angle4); background-color: var(--color-chart-4-label-bg); --label-radius: 39%; }
+
+
+/* GROUP CHARTS / PIE CHART / MODIFIERS
+=================================================== */
+.pie-chart--focused {
+ .pie-chart__label,
+ &.pie-chart:hover .pie-chart__label {
+ opacity: 0;
+ }
+
+ .pie-chart__label[data-focused],
+ &.pie-chart:hover .pie-chart__label[data-focused] {
+ opacity: 1;
+ border: 2px solid black;
+ }
+}
+
+
+
+/* GROUP CHARTS / IMAGE PIE CHART
+=================================================== */
+.image-pie-chart {
+ --label-radius: 30%;
+
+ position: relative;
+ width: 9rem;
+ aspect-ratio: 1 / 1;
+
+ /* Midpoint angle for each slice (100% = 360deg). */
+ --angle1: calc(var(--1) * 3.6deg / 2);
+ --angle2: calc((var(--1) + var(--2) / 2) * 3.6deg);
+
+ &:hover {
+ .image-pie-chart__label {
+ opacity: unset;
+ }
+ }
+}
+
+.image-pie-chart__disc {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ overflow: hidden;
+
+ @apply bg-gray-200 dark:bg-gray-700;
+}
+
+.image-pie-chart__slice {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background-image: var(--image);
+ background-size: cover;
+ background-position: center;
+}
+
+.image-pie-chart__slice--1 {
+ --slice-size: calc(var(--1) * 3.6deg);
+
+ -webkit-mask-image: conic-gradient(from 0deg, #000 0deg var(--slice-size), transparent var(--slice-size));
+ mask-image: conic-gradient(from 0deg, #000 0deg var(--slice-size), transparent var(--slice-size));
+}
+
+.image-pie-chart__slice--2 {
+ --slice-start: calc(var(--1) * 3.6deg);
+ --slice-size: calc(var(--2) * 3.6deg);
+
+ -webkit-mask-image: conic-gradient(from var(--slice-start), #000 0deg var(--slice-size), transparent var(--slice-size));
+ mask-image: conic-gradient(from var(--slice-start), #000 0deg var(--slice-size), transparent var(--slice-size));
+}
+
+.image-pie-chart__label {
+ --angle: 0deg;
+ opacity: 0;
+ transition: opacity 0.2s ease-in 0s;
+
+ padding: 0.075rem 0.15rem;
+ border-radius: 0.25rem;
+
+ position: absolute;
+ left: calc(50% + var(--label-radius) * sin(var(--angle)));
+ top: calc(50% - var(--label-radius) * cos(var(--angle)));
+ transform: translate(-50%, -50%);
+
+ @apply text-xs font-medium tabular-nums text-white pointer-events-none;
+}
+
+.image-pie-chart__label--1 { --angle: var(--angle1); background-color: hsl(0 0% 0% / 0.55); }
+.image-pie-chart__label--2 { --angle: var(--angle2); background-color: hsl(0 0% 0% / 0.55); }
+
+
+
+/* GROUP CHARTS / VERTICAL BAR CHART
+=================================================== */
+.vertical-bar-chart-figure {
+ display: grid;
+ height: 100%;
+ @apply py-3 px-3;
+}
+
+.vertical-bar-chart {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 0.1rem;
+ height: 100%;
+ min-height: 10rem;
+ list-style: none;
+ padding: 0;
+}
+
+.vertical-bar-chart__bar {
+ display: grid;
+ flex: 1;
+ grid-template-rows: 1fr auto;
+ align-items: end;
+ justify-items: center;
+ gap: 0.5rem;
+ min-width: 0;
+ height: 100%;
+}
+
+.vertical-bar-chart__plot {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ padding-top: calc(0.785rem * 1.5 + 0.375rem);
+}
+
+.vertical-bar-chart__value {
+ @apply mb-1.5 text-[0.785rem] font-medium tabular-nums text-gray-800 dark:text-gray-100;
+}
+
+.vertical-bar-chart__fill {
+ flex-grow: 0;
+ flex-shrink: 0;
+ width: 100%;
+ max-width: 2rem;
+ border-radius: 0.25rem 0.25rem 0 0;
+ background-color: var(--color-chart-1);
+}
+
+.vertical-bar-chart__scale-label {
+ @apply min-w-8 rounded-md border border-gray-200 shadow-ui-xs px-1.5 py-0.5 text-center text-[0.785rem] font-medium tabular-nums text-gray-700 dark:border-gray-700 dark:text-gray-300;
+}
+
+
+
+/* GROUP CHARTS / SUMMARY / EDITING
+=================================================== */
+.summary-chart-editing header {
+ @apply border-dashed border-gray-400 dark:border-gray-700;
+}
+
+
+
+/* GROUP CHARTS / SUMMARY BAR CHART / ICON STROKES
+=================================================== */
+/* Note: This is used to style the checkbox icons in the summary bar chart depending on the background contrast. */
+.summary-bar-chart__icon-stroke {
+ color: var(--chart-tick-color);
+}
+
+.summary-bar-chart__icon-stroke path:not(:first-child) {
+ stroke: white;
+}
+
+@supports (color: contrast-color(red)) {
+ .dark .summary-bar-chart__icon-stroke path:not(:first-child) {
+ stroke: contrast-color(var(--chart-tick-color));
+ }
+}
+
+.summary-bar-chart__icon-stroke--1 { --chart-tick-color: var(--color-chart-1-legend); }
+.summary-bar-chart__icon-stroke--2 { --chart-tick-color: var(--color-chart-2-legend); }
+.summary-bar-chart__icon-stroke--3 { --chart-tick-color: var(--color-chart-3-legend); }
+.summary-bar-chart__icon-stroke--4 { --chart-tick-color: var(--color-chart-4-legend); }
+.summary-bar-chart__icon-stroke--5 { --chart-tick-color: var(--color-chart-5-legend); }
+
+
+
+/* GROUP CHARTS / ANIMATIONS / PIE CHART
+=================================================== */
+@media (prefers-reduced-motion: no-preference) {
+ /* These animations are triggered when switching chart types */
+ @property --pie-reveal {
+ syntax: '|
+
+
+
+ {{ file.filename }}
+
+
+ |
+ + {{ file.size }} + | +
+ |
+
|
@@ -23,7 +23,7 @@
|
- + |
- |
@@ -55,9 +63,9 @@
|---|
{{ __('statamic::messages.form_builder_empty_inspector') }}
+{{ __(fieldtype.description) }}
+ +{{ preview.body }}
+ {{ examplePayload }}
+ + {{ heading }} +
+ ++ {{ description }} +
+{{ showingDrilldown ? accessibleLabel : '' }}
++ {{ __(':count responses', { count: $number.format(summary.total) }) }} +
+