diff --git a/.storybook/storybook.css b/.storybook/storybook.css index 9a141f91e16..fd3e3b1a84f 100644 --- a/.storybook/storybook.css +++ b/.storybook/storybook.css @@ -5,6 +5,7 @@ @import '../resources/css/ui.css'; @import '../resources/css/core/utilities.css'; @import '../resources/css/elements/tables.css'; +@import '../resources/css/components/forms/charts.css'; @import '../resources/css/components/stacks.css'; @custom-variant dark (&:where(.dark, .dark *)); @@ -332,4 +333,4 @@ .sbdocs-preview { border-radius: 17px; } -} \ No newline at end of file +} diff --git a/.storybook/theme.css b/.storybook/theme.css index 36e4eb449ea..6695be3418c 100644 --- a/.storybook/theme.css +++ b/.storybook/theme.css @@ -31,6 +31,18 @@ manually copy them here. --theme-color-ui-accent-bg: oklch(0.457 0.24 277.023); --theme-color-ui-accent-text: var(--theme-color-ui-accent-bg); --theme-color-switch-bg: var(--theme-color-ui-accent-bg); + --theme-color-chart-1: oklch(0.585 0.233 277.117); + --theme-color-chart-2: oklch(0.274 0.006 286.033); + --theme-color-chart-3: oklch(0.768 0.233 130.85); + --theme-color-chart-4: oklch(0.87 0.065 274.039); + --theme-color-chart-1-label-bg: oklch(0.511 0.262 276.966); + --theme-color-chart-2-label-bg: transparent; + --theme-color-chart-3-label-bg: oklch(0.648 0.2 131.684); + --theme-color-chart-4-label-bg: oklch(0.673 0.182 276.935); + --theme-color-chart-1-legend: oklch(0.585 0.233 277.117); + --theme-color-chart-2-legend: oklch(0.274 0.006 286.033); + --theme-color-chart-3-legend: oklch(0.768 0.233 130.85); + --theme-color-chart-4-legend: oklch(0.785 0.115 274.713); --z-index-below: -1; --z-index-above: 1; @@ -45,6 +57,18 @@ manually copy them here. --theme-color-content-bg: oklch(0.21 0.006 285.885); --theme-color-content-border: oklch(0.141 0.005 285.823); --theme-color-ui-accent-text: oklch(0.673 0.182 276.935); + --theme-color-chart-1: oklch(0.511 0.262 276.966); + --theme-color-chart-2: oklch(0.87 0.065 274.039); + --theme-color-chart-3: oklch(0.768 0.233 130.85); + --theme-color-chart-4: oklch(0.141 0.005 285.823); + --theme-color-chart-1-label-bg: oklch(0.457 0.24 277.023); + --theme-color-chart-2-label-bg: oklch(0.673 0.182 276.935); + --theme-color-chart-3-label-bg: oklch(0.648 0.2 131.684); + --theme-color-chart-4-label-bg: oklch(0.37 0.013 285.805); + --theme-color-chart-1-legend: oklch(0.511 0.262 276.966); + --theme-color-chart-2-legend: oklch(0.87 0.065 274.039); + --theme-color-chart-3-legend: oklch(0.768 0.233 130.85); + --theme-color-chart-4-legend: oklch(0.141 0.005 285.823); } } diff --git a/lang/en/messages.php b/lang/en/messages.php index d57a3e72ea8..7075b4cbfd0 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -164,6 +164,9 @@ '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_save_to_see_chart' => 'Save to see this chart.', '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.', 'getting_started_widget_header' => 'Getting Started with Statamic', diff --git a/lang/en/validation.php b/lang/en/validation.php index 9e46026ccab..f237d8e16ab 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -173,6 +173,9 @@ 'elevated_session_verification_code' => 'The verification code is incorrect.', 'email_available' => 'A user with this email already exists.', 'fieldset_imported_recursively' => 'Fieldset :handle is being imported recursively.', + 'form_chart_hidden_field' => 'Field :field is hidden and cannot be charted.', + 'form_chart_unknown_chart' => 'Chart :chart does not exist.', + 'form_chart_unknown_field' => 'Field :field does not exist on this form.', '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.', diff --git a/packages/cms/src/ui.js b/packages/cms/src/ui.js index 1cce409c7f4..a921bf82dec 100644 --- a/packages/cms/src/ui.js +++ b/packages/cms/src/ui.js @@ -48,6 +48,8 @@ export const { Field, Header, Heading, + HorizontalBarChart, + HorizontalLollipopChart, HoverCard, Icon, Input, @@ -78,6 +80,7 @@ export const { Panel, PanelFooter, PanelHeader, + PieChart, Popover, PublishComponents, PublishContainer, @@ -127,6 +130,7 @@ export const { Timezones, ToggleGroup, ToggleItem, + VerticalBarChart, Widget, registerIconSet, registerIconSetFromStrings, diff --git a/resources/css/components/forms/charts.css b/resources/css/components/forms/charts.css new file mode 100644 index 00000000000..0a85d32a19d --- /dev/null +++ b/resources/css/components/forms/charts.css @@ -0,0 +1,310 @@ +/* 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__link { + @apply contents cursor-pointer text-inherit; +} + +.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%; + 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; +} + +.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 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); } + + + +/* GROUP CHARTS / ANIMATIONS / PIE CHART +=================================================== */ +@media (prefers-reduced-motion: no-preference) { + /* These animations are triggered when switching chart types */ + @property --pie-reveal { + syntax: ''; + inherits: false; + initial-value: 0deg; + } + + @keyframes pie-chart-reveal { + from { --pie-reveal: 0deg; } + to { --pie-reveal: 360deg; } + } + + @keyframes bar-chart-reveal { + from { transform: scaleX(0); } + to { transform: scaleX(1); } + } + + .content-card:has([data-submission-summary]) { + [data-ui-card]:has([data-ui-toggle-group-interacted]) { + .pie-chart__disc, + .image-pie-chart__disc { + -webkit-mask-image: conic-gradient(from 0deg, #000 0deg, #000 var(--pie-reveal), transparent var(--pie-reveal)); + mask-image: conic-gradient(from 0deg, #000 0deg, #000 var(--pie-reveal), transparent var(--pie-reveal)); + --pie-reveal: 0deg; + animation: pie-chart-reveal 0.75s cubic-bezier(0.33, 1, 0.68, 1) forwards; + } + + .summary-bar-chart__fill { + transform-origin: left center; + animation: bar-chart-reveal 0.75s cubic-bezier(0.33, 1, 0.68, 1) forwards; + } + } + } +} diff --git a/resources/css/cp.css b/resources/css/cp.css index 9f8c9786f8f..b9ee26f8a15 100644 --- a/resources/css/cp.css +++ b/resources/css/cp.css @@ -19,6 +19,7 @@ @import './components/page-tree.css'; @import './components/forms.css'; @import './components/forms/logic-tree.css'; +@import './components/forms/charts.css'; @import './components/pagination.css'; @import './components/popover.css'; @import './components/preview.css'; diff --git a/resources/css/ui.css b/resources/css/ui.css index 16816ecbe18..3594899c94b 100644 --- a/resources/css/ui.css +++ b/resources/css/ui.css @@ -30,6 +30,18 @@ --color-ui-accent-bg: var(--theme-color-ui-accent-bg); --color-ui-accent-text: var(--theme-color-ui-accent-text); --color-switch-bg: var(--theme-color-switch-bg); + --color-chart-1: var(--theme-color-chart-1); + --color-chart-2: var(--theme-color-chart-2); + --color-chart-3: var(--theme-color-chart-3); + --color-chart-4: var(--theme-color-chart-4); + --color-chart-1-label-bg: var(--theme-color-chart-1-label-bg); + --color-chart-2-label-bg: var(--theme-color-chart-2-label-bg); + --color-chart-3-label-bg: var(--theme-color-chart-3-label-bg); + --color-chart-4-label-bg: var(--theme-color-chart-4-label-bg); + --color-chart-1-legend: var(--theme-color-chart-1-legend); + --color-chart-2-legend: var(--theme-color-chart-2-legend); + --color-chart-3-legend: var(--theme-color-chart-3-legend); + --color-chart-4-legend: var(--theme-color-chart-4-legend); /* Custom color shades for all Tailwind color families */ --color-slate-150: oklch(0.9485 0.01 251.702); diff --git a/resources/js/bootstrap/cms/ui.js b/resources/js/bootstrap/cms/ui.js index a38890b0bf4..82a032020da 100644 --- a/resources/js/bootstrap/cms/ui.js +++ b/resources/js/bootstrap/cms/ui.js @@ -46,6 +46,8 @@ export { Field, Header, Heading, + HorizontalBarChart, + HorizontalLollipopChart, HoverCard, Icon, Input, @@ -76,6 +78,7 @@ export { Panel, PanelFooter, PanelHeader, + PieChart, Popover, PublishComponents, PublishContainer, @@ -125,6 +128,7 @@ export { Timezones, ToggleGroup, ToggleItem, + VerticalBarChart, Widget, registerIconSet, registerIconSetFromStrings, diff --git a/resources/js/bootstrap/components.js b/resources/js/bootstrap/components.js index 47454014cc9..fb7fdabc901 100644 --- a/resources/js/bootstrap/components.js +++ b/resources/js/bootstrap/components.js @@ -8,6 +8,10 @@ import UserPublishForm from '../components/users/PublishForm.vue'; import EntryListing from '../components/entries/Listing.vue'; import CollectionWidget from '../components/entries/CollectionWidget.vue'; import FormWidget from '../components/forms/FormWidget.vue'; +import AverageInsight from '../components/forms/summary/insights/AverageInsight.vue'; +import CheckedInsight from '../components/forms/summary/insights/CheckedInsight.vue'; +import MinMaxInsight from '../components/forms/summary/insights/MinMaxInsight.vue'; +import StarRatingInsight from '../components/forms/summary/insights/StarRatingInsight.vue'; import FileIcon from '../components/FileIcon.vue'; import Slugify from '../components/slugs/Slugify.vue'; @@ -60,6 +64,12 @@ export default function registerGlobalComponents(app) { app.component('form-widget', FormWidget); app.component('updater-widget', UpdaterWidget); + // Form Summary Insights + app.component('average-insight', AverageInsight); + app.component('checked-insight', CheckedInsight); + app.component('min-max-insight', MinMaxInsight); + app.component('star-rating-insight', StarRatingInsight); + // Reusable app.component('file-icon', FileIcon); diff --git a/resources/js/components/forms/SubmissionListing.vue b/resources/js/components/forms/SubmissionListing.vue index db3004806d5..99d6a29150b 100644 --- a/resources/js/components/forms/SubmissionListing.vue +++ b/resources/js/components/forms/SubmissionListing.vue @@ -1,3 +1,53 @@ + + - - + + + + diff --git a/resources/js/components/forms/summary/ChartWidget.vue b/resources/js/components/forms/summary/ChartWidget.vue new file mode 100644 index 00000000000..83f482f2f20 --- /dev/null +++ b/resources/js/components/forms/summary/ChartWidget.vue @@ -0,0 +1,87 @@ + + + diff --git a/resources/js/components/forms/summary/EditChrome.vue b/resources/js/components/forms/summary/EditChrome.vue new file mode 100644 index 00000000000..abb651a7561 --- /dev/null +++ b/resources/js/components/forms/summary/EditChrome.vue @@ -0,0 +1,43 @@ + + + diff --git a/resources/js/components/forms/summary/FieldPicker.vue b/resources/js/components/forms/summary/FieldPicker.vue new file mode 100644 index 00000000000..4419003fd37 --- /dev/null +++ b/resources/js/components/forms/summary/FieldPicker.vue @@ -0,0 +1,29 @@ + + + diff --git a/resources/js/components/forms/summary/Summary.vue b/resources/js/components/forms/summary/Summary.vue new file mode 100644 index 00000000000..3e7e03626aa --- /dev/null +++ b/resources/js/components/forms/summary/Summary.vue @@ -0,0 +1,312 @@ + + + diff --git a/resources/js/components/forms/summary/insights/AverageInsight.vue b/resources/js/components/forms/summary/insights/AverageInsight.vue new file mode 100644 index 00000000000..97162e6eda7 --- /dev/null +++ b/resources/js/components/forms/summary/insights/AverageInsight.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/components/forms/summary/insights/CheckedInsight.vue b/resources/js/components/forms/summary/insights/CheckedInsight.vue new file mode 100644 index 00000000000..ffe91b20c40 --- /dev/null +++ b/resources/js/components/forms/summary/insights/CheckedInsight.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/components/forms/summary/insights/MinMaxInsight.vue b/resources/js/components/forms/summary/insights/MinMaxInsight.vue new file mode 100644 index 00000000000..b303a2b72b7 --- /dev/null +++ b/resources/js/components/forms/summary/insights/MinMaxInsight.vue @@ -0,0 +1,20 @@ + + + diff --git a/resources/js/components/forms/summary/insights/StarRatingInsight.vue b/resources/js/components/forms/summary/insights/StarRatingInsight.vue new file mode 100644 index 00000000000..bcef3be69d1 --- /dev/null +++ b/resources/js/components/forms/summary/insights/StarRatingInsight.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/components/forms/summary/types.ts b/resources/js/components/forms/summary/types.ts new file mode 100644 index 00000000000..01d8c36bdc4 --- /dev/null +++ b/resources/js/components/forms/summary/types.ts @@ -0,0 +1,74 @@ +export enum ChartMetric { + Percent = 'percent', + Count = 'count', +} + +export interface ChartItem { + key: string; + label: string; + count: number; + percent: number; + rank?: number; + icon?: string; + image?: string; + badge?: string; + other?: boolean; + clickable?: boolean; +} + +export interface ChartPayload { + handle: string; + component: string; + props: { + items: ChartItem[]; + drilldown?: { + items: ChartItem[]; + [prop: string]: unknown; + }; + }; +} + +export interface InsightPayload { + handle: string; + component: string; + props: Record; +} + +export interface SummaryField { + handle: string; + display: string; + icon: string; + fieldtype: string; + number: number | null; + responses: number; + chart: ChartPayload; + insights: InsightPayload[]; +} + +export interface ChartConfig { + field: string; + chart: string; +} + +export interface MetaChart { + handle: string; + title: string; + icon: string | null; + component: string; +} + +export interface MetaField { + handle: string; + display: string; + icon: string; + default_chart: string; +} + +export interface Summary { + total: number; + fields: SummaryField[]; + meta: { + charts?: MetaChart[]; + fields?: MetaField[]; + }; +} diff --git a/resources/js/components/themes/index.ts b/resources/js/components/themes/index.ts index 8659dcafa9f..69230413174 100644 --- a/resources/js/components/themes/index.ts +++ b/resources/js/components/themes/index.ts @@ -244,6 +244,18 @@ export const colors = [ { name: 'switch-bg', label: 'Switch Background', }, { name: 'success', label: 'Success', }, { name: 'danger', label: 'Danger', }, + { name: 'chart-1', label: 'Chart 1', }, + { name: 'chart-2', label: 'Chart 2', }, + { name: 'chart-3', label: 'Chart 3', }, + { name: 'chart-4', label: 'Chart 4', }, + { name: 'chart-1-label-bg', label: 'Chart 1 Label', }, + { name: 'chart-2-label-bg', label: 'Chart 2 Label', }, + { name: 'chart-3-label-bg', label: 'Chart 3 Label', }, + { name: 'chart-4-label-bg', label: 'Chart 4 Label', }, + { name: 'chart-1-legend', label: 'Chart 1 Legend', }, + { name: 'chart-2-legend', label: 'Chart 2 Legend', }, + { name: 'chart-3-legend', label: 'Chart 3 Legend', }, + { name: 'chart-4-legend', label: 'Chart 4 Legend', }, { name: 'gray-50', label: 'Gray 50', }, { name: 'gray-100', label: 'Gray 100', }, { name: 'gray-150', label: 'Gray 150', }, diff --git a/resources/js/components/themes/types.ts b/resources/js/components/themes/types.ts index 7899532a99b..193bf730885 100644 --- a/resources/js/components/themes/types.ts +++ b/resources/js/components/themes/types.ts @@ -16,6 +16,18 @@ export type ColorVariableName = | 'ui-accent-bg' | 'ui-accent-text' | 'switch-bg' + | 'chart-1' + | 'chart-2' + | 'chart-3' + | 'chart-4' + | 'chart-1-label-bg' + | 'chart-2-label-bg' + | 'chart-3-label-bg' + | 'chart-4-label-bg' + | 'chart-1-legend' + | 'chart-2-legend' + | 'chart-3-legend' + | 'chart-4-legend' | 'gray-50' | 'gray-100' | 'gray-150' diff --git a/resources/js/components/ui/Button/Button.vue b/resources/js/components/ui/Button/Button.vue index 64294008ba9..f209fa145a7 100644 --- a/resources/js/components/ui/Button/Button.vue +++ b/resources/js/components/ui/Button/Button.vue @@ -36,6 +36,8 @@ const props = defineProps({ type: { type: String, default: 'button' }, /** Controls the appearance of the button. Options: `default`, `primary`, `danger`, `filled`, `ghost`, `ghost-pressed`, `subtle`, `pressed` */ variant: { type: String, default: 'default' }, + /** Additional classes for the inner content wrapper around the default slot or text */ + contentClass: { type: String, default: null }, }); const attrs = useAttrs(); @@ -116,6 +118,12 @@ const buttonClasses = computed(() => { return twMerge(classes, attrs.class); }); +const contentClasses = computed(() => twMerge( + 'flex content-center items-center', + props.size !== 'xs' && props.size !== 'sm' ? 'st-text-trim-start' : null, + props.contentClass, +)); + const restAttrs = computed(() => { const { class: _, ...rest } = attrs; return rest; @@ -136,7 +144,7 @@ const restAttrs = computed(() => { -
+
diff --git a/resources/js/components/ui/Chart/HorizontalBarChart.vue b/resources/js/components/ui/Chart/HorizontalBarChart.vue new file mode 100644 index 00000000000..1bc3b3c2284 --- /dev/null +++ b/resources/js/components/ui/Chart/HorizontalBarChart.vue @@ -0,0 +1,102 @@ + + + diff --git a/resources/js/components/ui/Chart/HorizontalLollipopChart.vue b/resources/js/components/ui/Chart/HorizontalLollipopChart.vue new file mode 100644 index 00000000000..0302c657cc6 --- /dev/null +++ b/resources/js/components/ui/Chart/HorizontalLollipopChart.vue @@ -0,0 +1,67 @@ + + + diff --git a/resources/js/components/ui/Chart/Metric.vue b/resources/js/components/ui/Chart/Metric.vue new file mode 100644 index 00000000000..7014fb5a17d --- /dev/null +++ b/resources/js/components/ui/Chart/Metric.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/components/ui/Chart/PieChart.vue b/resources/js/components/ui/Chart/PieChart.vue new file mode 100644 index 00000000000..7039270c80d --- /dev/null +++ b/resources/js/components/ui/Chart/PieChart.vue @@ -0,0 +1,140 @@ + + + diff --git a/resources/js/components/ui/Chart/VerticalBarChart.vue b/resources/js/components/ui/Chart/VerticalBarChart.vue new file mode 100644 index 00000000000..5b836091abb --- /dev/null +++ b/resources/js/components/ui/Chart/VerticalBarChart.vue @@ -0,0 +1,55 @@ + + + diff --git a/resources/js/components/ui/Listing/Filters.vue b/resources/js/components/ui/Listing/Filters.vue index 5b6a15edc2a..a347c7cd448 100644 --- a/resources/js/components/ui/Listing/Filters.vue +++ b/resources/js/components/ui/Listing/Filters.vue @@ -6,7 +6,7 @@ import { PanelHeader, Card, Heading, - Stack, + Stack, } from '@ui'; import { injectListingContext } from '../Listing/Listing.vue'; import { dateFormatter } from '@/api'; diff --git a/resources/js/components/ui/Listing/Listing.vue b/resources/js/components/ui/Listing/Listing.vue index 6897d950d36..624fe033776 100644 --- a/resources/js/components/ui/Listing/Listing.vue +++ b/resources/js/components/ui/Listing/Listing.vue @@ -594,9 +594,9 @@ function autoApplyFilters() { } function reordered(order) { - if (! props.items) { - items.value = order; - } + if (! props.items) { + items.value = order; + } emit('reordered', order); } @@ -732,7 +732,17 @@ autoApplyState();
- +
diff --git a/resources/js/components/ui/Toggle/Group.vue b/resources/js/components/ui/Toggle/Group.vue index 2e217ced7e3..7eb62813b56 100644 --- a/resources/js/components/ui/Toggle/Group.vue +++ b/resources/js/components/ui/Toggle/Group.vue @@ -18,6 +18,7 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue']); const toggleState = ref(props.modelValue ?? (props.multiple ? [] : null)); +const hasInteracted = ref(false); watch( () => props.modelValue, @@ -58,6 +59,14 @@ function updateModelValue(value) { emit('update:modelValue', value); } } + +function markUserInteraction(event) { + if (event.type === 'keydown' && ! ['Enter', ' ', 'Spacebar'].includes(event.key)) { + return; + } + + hasInteracted.value = true; +}