diff --git a/.changeset/mighty-wings-create.md b/.changeset/mighty-wings-create.md new file mode 100644 index 00000000000000..091fe4eebbcafe --- /dev/null +++ b/.changeset/mighty-wings-create.md @@ -0,0 +1,5 @@ +--- +"@calcom/atoms": minor +--- + +feat: style calendar settings and availability overrides diff --git a/docs/platform/atoms/availability-settings.mdx b/docs/platform/atoms/availability-settings.mdx index fbbe8b75f30a3a..6ff260f0db41b9 100644 --- a/docs/platform/atoms/availability-settings.mdx +++ b/docs/platform/atoms/availability-settings.mdx @@ -125,12 +125,45 @@ Along with the props, Availability settings atom accepts custom styles via the * | formClassName | Form which contains the days and toggles | | timezoneSelectClassName | Adds styling to the timezone select component | | subtitlesClassName | Styles the subtitle | -| scheduleContainer | Styles the entire schedule component | -| scheduleDay | Adds styling to just the day of a particular schedule | -| dayRanges | Adds styling to day ranges | -| timeRanges | Time ranges in the availability settings can be customized | -| labelAndSwitchContainer | Adds styling to label and switches | -| overridesModalClassNames | Adds styling to the date overrides modal | +| scheduleClassNames | An object for granular styling of schedule components (see detailed table below) | +| dateOverrideClassNames | An object for granular styling of date override components (see detailed table below) | + +### scheduleClassNames Object Structure + +The `scheduleClassNames` prop accepts an object with the following structure for granular styling of schedule components: + +| Property Path | Description | +|:--------------|:------------| +| `schedule` | Styles the entire schedule component | +| `scheduleContainer` | Styles the schedule container | +| `scheduleDay` | Adds styling to just the day of a particular schedule | +| `dayRanges` | Adds styling to day ranges | +| `timeRangeField` | Styles the time range input fields | +| `labelAndSwitchContainer` | Adds styling to label and switches | +| `timePicker` | An object for granular styling of time picker dropdown components (see nested table below) | + +#### timePicker Object Structure (nested within scheduleClassNames) + +The `timePicker` prop accepts an object with the following structure for granular styling of time picker dropdown components. This applies to the time selection dropdowns (e.g., [ 9:00 ]) that users can click to open a dropdown with available times or click to manually enter a time: + +| Property Path | Description | +|:--------------|:------------| +| `container` | Styles the main time picker container | +| `valueContainer` | Styles the container that holds the selected value | +| `value` | Styles the displayed selected time value | +| `input` | Styles the input field for manual time entry | +| `dropdown` | Styles the dropdown menu with time options | + +### dateOverrideClassNames Object Structure + +The `dateOverrideClassNames` prop accepts an object with the following structure for granular styling of date override components: + +| Property Path | Description | +|:--------------|:------------| +| `container` | Styles the main container | +| `title` | Styles the title| +| `description` | Styles the description | +| `button` | Styles the button to add date override | Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames. diff --git a/docs/platform/atoms/calendar-settings.mdx b/docs/platform/atoms/calendar-settings.mdx index 82c3cae1f727ae..c7ca09b1d9aecf 100644 --- a/docs/platform/atoms/calendar-settings.mdx +++ b/docs/platform/atoms/calendar-settings.mdx @@ -47,6 +47,37 @@ Along with the props, calendar settings atom accepts custom styles via the **cla | calendarSettingsCustomClassnames | Adds styling to the entire calendar settings atom | | destinationCalendarSettingsCustomClassnames | Adds styling only to the destination calendar container | | selectedCalendarSettingsCustomClassnames | Adds styling only to the selected calendar container | +| selectedCalendarSettingsClassNames | An object for granular styling of selected calendars component (see detailed table below) | +| destinationCalendarSettingsClassNames | An object for granular styling of destination calendar component (see detailed table below) | + +### selectedCalendarSettingsClassNames Object Structure + +The `selectedCalendarSettingsClassNames` prop accepts an object with the following nested structure for granular styling of the selected calendars component: + +| Property Path | Description | +|:--------------|:------------| +| `container` | Styles the main container of the selected calendars section | +| `header.container` | Styles the header container | +| `header.title` | Styles the header title | +| `header.description` | Styles the header description | +| `selectedCalendarsListClassNames.container` | Styles the container that holds all selected calendar items | +| `selectedCalendarsListClassNames.selectedCalendar.container` | Styles each individual calendar item container | +| `selectedCalendarsListClassNames.selectedCalendar.header.container` | Styles the header section of each calendar item | +| `selectedCalendarsListClassNames.selectedCalendar.header.title` | Styles the title of each calendar item | +| `selectedCalendarsListClassNames.selectedCalendar.header.description` | Styles the description of each calendar item | +| `selectedCalendarsListClassNames.selectedCalendar.body.container` | Styles the body section of each calendar item | +| `selectedCalendarsListClassNames.selectedCalendar.body.description` | Styles the body description of each calendar item | +| `noSelectedCalendarsMessage` | Styles the message shown when no calendars are connected | + +### destinationCalendarSettingsClassNames Object Structure + +The `destinationCalendarSettingsClassNames` prop accepts an object with the following nested structure for granular styling of the destination calendar component: + +| Property Path | Description | +|:--------------|:------------| +| `container` | Styles the main container of the destination calendar section | +| `header.title` | Styles the header title text | +| `header.description` | Styles the header description text |

Additionally, if you wish to select either the Destination Calendar or the Selected Calendar, we also provide atoms specifically designed for this purpose. diff --git a/packages/features/schedules/components/Schedule.tsx b/packages/features/schedules/components/Schedule.tsx index 06fa8994e10025..9a6ad2a8d0c0c7 100644 --- a/packages/features/schedules/components/Schedule.tsx +++ b/packages/features/schedules/components/Schedule.tsx @@ -36,6 +36,14 @@ export type ScheduleLabelsType = { deleteTime: string; }; +export type SelectInnerClassNames = { + control?: string; + singleValue?: string; + valueContainer?: string; + input?: string; + menu?: string; +}; + export type FieldPathByValue = { [Key in FieldPath]: FieldPathValue extends TValue ? Key : never; }[FieldPath]; @@ -105,6 +113,7 @@ export const ScheduleDay = ({ classNames={{ dayRanges: classNames?.dayRanges, timeRangeField: classNames?.timeRangeField, + timePicker: classNames?.timePicker, }} /> {!disabled &&
{CopyButton}
} @@ -237,7 +246,7 @@ export const DayRanges = ({ disabled?: boolean; labels?: ScheduleLabelsType; userTimeFormat: number | null; - classNames?: Pick; + classNames?: Pick; }) => { const { t } = useLocale(); const { getValues } = useFormContext(); @@ -260,6 +269,7 @@ export const DayRanges = ({ )} @@ -335,11 +345,27 @@ const TimeRangeField = ({ onChange, disabled, userTimeFormat, + timePickerClassNames, }: { className?: string; disabled?: boolean; userTimeFormat: number | null; + timePickerClassNames?: { + container?: string; + value?: string; + valueContainer?: string; + input?: string; + dropdown?: string; + }; } & ControllerRenderProps) => { + const innerClassNames: SelectInnerClassNames = { + control: timePickerClassNames?.container, + singleValue: timePickerClassNames?.value, + valueContainer: timePickerClassNames?.valueContainer, + input: timePickerClassNames?.input, + menu: timePickerClassNames?.dropdown, + }; + // this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic. return (
@@ -349,6 +375,7 @@ const TimeRangeField = ({ isDisabled={disabled} value={value.start} menuPlacement="bottom" + innerClassNames={innerClassNames} onChange={(option) => { const newStart = new Date(option?.value as number); if (newStart >= new Date(value.end)) { @@ -367,6 +394,7 @@ const TimeRangeField = ({ isDisabled={disabled} value={value.end} min={value.start} + innerClassNames={innerClassNames} menuPlacement="bottom" onChange={(option) => { onChange({ ...value, end: new Date(option?.value as number) }); @@ -388,6 +416,7 @@ const LazySelect = ({ min?: ConfigType; max?: ConfigType; userTimeFormat: number | null; + innerClassNames?: SelectInnerClassNames; }) => { // Lazy-loaded options, otherwise adding a field has a noticeable redraw delay. const { options, filter } = useOptions(userTimeFormat); diff --git a/packages/platform/atoms/availability/AvailabilitySettings.tsx b/packages/platform/atoms/availability/AvailabilitySettings.tsx index 2192a48b003f45..051c4d46194e15 100644 --- a/packages/platform/atoms/availability/AvailabilitySettings.tsx +++ b/packages/platform/atoms/availability/AvailabilitySettings.tsx @@ -68,6 +68,12 @@ export type CustomClassNames = { subtitlesClassName?: string; scheduleClassNames?: scheduleClassNames; overridesModalClassNames?: string; + dateOverrideClassNames?: { + container?: string; + title?: string; + description?: string; + button?: string; + }; hiddenSwitchClassname?: { container?: string; thumb?: string; @@ -180,6 +186,7 @@ const DateOverride = ({ travelSchedules, weekStart, overridesModalClassNames, + classNames, handleSubmit, }: { workingHours: WorkingHours[]; @@ -187,6 +194,12 @@ const DateOverride = ({ travelSchedules?: RouterOutputs["viewer"]["travelSchedules"]["get"]; weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6; overridesModalClassNames?: string; + classNames?: { + container?: string; + title?: string; + description?: string; + button?: string; + }; handleSubmit: (data: AvailabilityFormValues) => Promise; }) => { const { append, replace, fields } = useFieldArray({ @@ -202,8 +215,8 @@ const DateOverride = ({ }; return ( -
-

+
+

{t("date_overrides")}{" "} @@ -211,7 +224,9 @@ const DateOverride = ({

-

{t("date_overrides_subtitle")}

+

+ {t("date_overrides_subtitle")} +

+ } @@ -318,24 +337,27 @@ export const AvailabilitySettings = forwardRef void; onError?: (error: Error) => void }>({}); - const handleFormSubmit = useCallback((customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => { - if (customCallbacks) { - callbacksRef.current = customCallbacks; - } + const handleFormSubmit = useCallback( + (customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => { + if (customCallbacks) { + callbacksRef.current = customCallbacks; + } - if (saveButtonRef.current) { - saveButtonRef.current.click(); - } else { - form.handleSubmit(async (data) => { - try { - await handleSubmit(data); - callbacksRef.current?.onSuccess?.(); - } catch (error) { - callbacksRef.current?.onError?.(error as Error); - } - })(); - } - }, [form, handleSubmit]); + if (saveButtonRef.current) { + saveButtonRef.current.click(); + } else { + form.handleSubmit(async (data) => { + try { + await handleSubmit(data); + callbacksRef.current?.onSuccess?.(); + } catch (error) { + callbacksRef.current?.onError?.(error as Error); + } + })(); + } + }, + [form, handleSubmit] + ); const validateForm = useCallback(async () => { const isValid = await form.trigger(); @@ -661,6 +683,7 @@ export const AvailabilitySettings = forwardRef )}
diff --git a/packages/platform/atoms/availability/types.ts b/packages/platform/atoms/availability/types.ts index 8cdba22a344ba2..caf96f2f8e6b92 100644 --- a/packages/platform/atoms/availability/types.ts +++ b/packages/platform/atoms/availability/types.ts @@ -28,6 +28,13 @@ export type scheduleClassNames = { timeRangeField?: string; labelAndSwitchContainer?: string; scheduleContainer?: string; + timePicker?: { + container?: string; + valueContainer?: string; + value?: string; + input?: string; + dropdown?: string; + }; }; export type AvailabilityFormValidationResult = { diff --git a/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx b/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx index 917c25bdeae2d2..67008cc99c8d6c 100644 --- a/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx +++ b/packages/platform/atoms/calendar-settings/wrappers/CalendarSettingsPlatformWrapper.tsx @@ -1,12 +1,18 @@ +import type { DestinationCalendarClassNames } from "../../destination-calendar/DestinationCalendar"; import { DestinationCalendarSettingsPlatformWrapper } from "../../destination-calendar/index"; import { SelectedCalendarsSettingsPlatformWrapper } from "../../selected-calendars/index"; -import type { CalendarRedirectUrls } from "../../selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper"; +import type { + CalendarRedirectUrls, + SelectedCalendarsClassNames, +} from "../../selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper"; type CalendarSettingsPlatformWrapperProps = { classNames?: { calendarSettingsCustomClassnames?: string; destinationCalendarSettingsCustomClassnames?: string; selectedCalendarSettingsCustomClassnames?: string; + selectedCalendarSettingsClassNames?: SelectedCalendarsClassNames; + destinationCalendarSettingsClassNames?: DestinationCalendarClassNames; }; calendarRedirectUrls?: CalendarRedirectUrls; allowDelete?: boolean; @@ -24,6 +30,7 @@ export const CalendarSettingsPlatformWrapper = ({ } classNames={classNames?.destinationCalendarSettingsCustomClassnames} + classNamesObject={classNames?.destinationCalendarSettingsClassNames} isDryRun={isDryRun} />
); diff --git a/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx b/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx index a024956e6ca4d3..00907c8da34e8a 100644 --- a/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx +++ b/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx @@ -5,12 +5,29 @@ import { cn } from "../src/lib/utils"; import type { DestinationCalendarProps } from "./DestinationCalendarSelector"; import { DestinationCalendarSelector } from "./DestinationCalendarSelector"; -export const DestinationCalendarSettings = (props: DestinationCalendarProps & { classNames?: string }) => { +type DestinationHeaderClassnames = { + container?: string; + title?: string; + description?: string; +}; + +export type DestinationCalendarClassNames = { + container?: string; + header?: DestinationHeaderClassnames; +}; + +export const DestinationCalendarSettings = ( + props: DestinationCalendarProps & { classNames?: string; classNamesObject?: DestinationCalendarClassNames } +) => { const { t } = useLocale(); return ( -
- +
+
@@ -23,15 +40,17 @@ export const DestinationCalendarSettings = (props: DestinationCalendarProps & { ); }; -const DestinationCalendarSettingsHeading = () => { +const DestinationCalendarSettingsHeading = ({ classNames }: { classNames?: DestinationHeaderClassnames }) => { const { t } = useLocale(); return ( -
-

+
+

{t("add_to_calendar")}

-

{t("add_to_calendar_description")}

+

+ {t("add_to_calendar_description")} +

); }; diff --git a/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsPlatformWrapper.tsx b/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsPlatformWrapper.tsx index cc1db9571b21ab..402d69b80b7dad 100644 --- a/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsPlatformWrapper.tsx +++ b/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsPlatformWrapper.tsx @@ -1,15 +1,18 @@ import { useUpdateDestinationCalendars } from "../../hooks/calendars/useUpdateDestinationCalendars"; import { useConnectedCalendars } from "../../hooks/useConnectedCalendars"; import { AtomsWrapper } from "../../src/components/atoms-wrapper"; +import type { DestinationCalendarClassNames } from "../DestinationCalendar"; import { DestinationCalendarSettings } from "../DestinationCalendar"; export const DestinationCalendarSettingsPlatformWrapper = ({ statusLoader, classNames = "mx-5", + classNamesObject, isDryRun = false, }: { statusLoader?: JSX.Element; classNames?: string; + classNamesObject?: DestinationCalendarClassNames; isDryRun?: boolean; }) => { const calendars = useConnectedCalendars({}); @@ -35,6 +38,7 @@ export const DestinationCalendarSettingsPlatformWrapper = ({ { const { t } = useLocale(); const query = useConnectedCalendars({}); @@ -55,23 +83,33 @@ export const SelectedCalendarsSettingsPlatformWrapper = ({ if (!data.connectedCalendars.length) { return ( - + -

No connected calendars found.

+

+ No connected calendars found. +

); } return ( - + - + {data.connectedCalendars.map((connectedCalendar) => { if (!!connectedCalendar.calendars && connectedCalendar.calendars.length > 0) { return ( @@ -84,7 +122,18 @@ export const SelectedCalendarsSettingsPlatformWrapper = ({ description={ connectedCalendar.primary?.email ?? connectedCalendar.integration.description } - className="border-subtle mt-4 rounded-lg border" + classNameObject={{ + container: cn( + "border-subtle mt-4 rounded-lg border", + classNamesObject?.selectedCalendarsListClassNames?.selectedCalendar?.container + ), + title: + classNamesObject?.selectedCalendarsListClassNames?.selectedCalendar?.header + ?.title, + description: + classNamesObject?.selectedCalendarsListClassNames?.selectedCalendar?.header + ?.description, + }} actions={
{allowDelete && !connectedCalendar.delegationCredentialId && ( @@ -98,8 +147,20 @@ export const SelectedCalendarsSettingsPlatformWrapper = ({ )}
}> -
-

{t("toggle_calendars_conflict")}

+
+

+ {t("toggle_calendars_conflict")} +

    {connectedCalendar.calendars?.map((cal) => { return ( @@ -159,18 +220,28 @@ export const SelectedCalendarsSettingsPlatformWrapper = ({ const SelectedCalendarsSettingsHeading = ({ calendarRedirectUrls, isDryRun, + classNames, }: { calendarRedirectUrls?: CalendarRedirectUrls; isDryRun?: boolean; + classNames?: { + container?: string; + title?: string; + description?: string; + }; }) => { const { t } = useLocale(); return ( -
    +
    -

    {t("check_for_conflicts")}

    -

    {t("select_calendars")}

    +

    + {t("check_for_conflicts")} +

    +

    + {t("select_calendars")} +

    diff --git a/packages/platform/examples/base/src/pages/availability.tsx b/packages/platform/examples/base/src/pages/availability.tsx index 3477bd256f2bc8..876e2f916bb03b 100644 --- a/packages/platform/examples/base/src/pages/availability.tsx +++ b/packages/platform/examples/base/src/pages/availability.tsx @@ -1,6 +1,6 @@ import { Navbar } from "@/components/Navbar"; import { Inter } from "next/font/google"; -import { useRef, useCallback, useState } from "react"; +import { useRef, useCallback } from "react"; import type { AvailabilitySettingsFormRef } from "@calcom/atoms"; import { AvailabilitySettings } from "@calcom/atoms"; @@ -57,6 +57,27 @@ export default function Availability(props: { calUsername: string; calEmail: str ctaClassName: "border p-4 rounded-md", editableHeadingClassName: "underline font-semibold", hiddenSwitchClassname: { thumb: "bg-red-500" }, + scheduleClassNames: { + schedule: "bg-blue-50 border-2 border-blue-200 rounded-lg p-4", + scheduleDay: "bg-green-50 border border-green-300 rounded-md mb-2", + dayRanges: "bg-yellow-50 p-3 rounded border-l-4 border-yellow-400", + timeRangeField: "!text-2xl bg-red-50 border-2 border-red-300 rounded-xl px-4 py-2", + labelAndSwitchContainer: "bg-purple-50 border border-purple-200 rounded p-2", + scheduleContainer: "bg-gray-100 border-4 border-gray-400 rounded-2xl shadow-lg", + timePicker: { + container: "!bg-blue-900", + valueContainer: "!bg-pink-500", + value: "!bg-yellow-500", + input: "!bg-green-500", + dropdown: "!bg-cyan-500", + }, + }, + dateOverrideClassNames: { + container: "p-4 bg-gray-900 rounded-md", + title: "text-red-500 font-bold", + description: "text-white", + button: "text-black", + }, }} onFormStateChange={handleFormStateChange} onUpdateSuccess={() => { diff --git a/packages/platform/examples/base/src/pages/calendars.tsx b/packages/platform/examples/base/src/pages/calendars.tsx index 4aded1786b2930..2cd2e8d59a20cf 100644 --- a/packages/platform/examples/base/src/pages/calendars.tsx +++ b/packages/platform/examples/base/src/pages/calendars.tsx @@ -10,7 +10,43 @@ export default function Calendars(props: { calUsername: string; calEmail: string
    - +
    ); diff --git a/packages/ui/components/app-list-card/AppListCard.tsx b/packages/ui/components/app-list-card/AppListCard.tsx index 78df7ba8292a61..e770d394d06fd3 100644 --- a/packages/ui/components/app-list-card/AppListCard.tsx +++ b/packages/ui/components/app-list-card/AppListCard.tsx @@ -22,6 +22,12 @@ type ShouldHighlight = slug?: never; }; +export type AppCardClassNames = { + container: string; + title?: string; + description?: string; +}; + export type AppListCardProps = { logo?: string; title: string; @@ -33,6 +39,7 @@ export type AppListCardProps = { children?: ReactNode; credentialOwner?: CredentialOwner; className?: string; + classNameObject?: AppCardClassNames; } & ShouldHighlight; export const AppListCard = (props: AppListCardProps & { highlight?: boolean }) => { @@ -48,11 +55,16 @@ export const AppListCard = (props: AppListCardProps & { highlight?: boolean }) = children, credentialOwner, className, + classNameObject, highlight, } = props; return ( -
    +
    {logo ? (
    -

    {title}

    +

    + {title} +

    {isDefault && {t("default")}} {isTemplate && Template}
    - + {description} {invalidCredential && (