✨(react) add DatePicker component
Based on the Figma DS design by Alex, a mono date-picker component has been added. It uses react-aria headless ui component capabilities, with downshift headless ui component. React-aria was not supporting by default dropdown menus to select months and years. We could not reuse Popover component from react-aria because we are not using their headless ui component for the button one. Clicking on the toggle calendar button triggers both the button and the popover click outside events. React-aria button uses a custom onPress props that is disabled by their popover. Instead, I have implemented a simple custom hook. This is the first acceptable version of the component. Some minor user interaction are missing. This first component doesn't support time selection.
This commit is contained in:
committed by
aleb_the_flash
parent
1d1cf81cf6
commit
10fa71e2a7
5
.changeset/new-buckets-accept.md
Normal file
5
.changeset/new-buckets-accept.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@openfun/cunningham-react": minor
|
||||
---
|
||||
|
||||
add datepicker component
|
||||
323
packages/react/src/components/Forms/DatePicker/Calendar.tsx
Normal file
323
packages/react/src/components/Forms/DatePicker/Calendar.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import {
|
||||
CalendarDate,
|
||||
createCalendar,
|
||||
DateValue,
|
||||
GregorianCalendar,
|
||||
toCalendar,
|
||||
} from "@internationalized/date";
|
||||
import { useDateFormatter, useLocale } from "@react-aria/i18n";
|
||||
import { useCalendarState } from "@react-stately/calendar";
|
||||
import { useCalendar } from "@react-aria/calendar";
|
||||
import {
|
||||
useSelect,
|
||||
UseSelectReturnValue,
|
||||
UseSelectStateChange,
|
||||
} from "downshift";
|
||||
import classNames from "classnames";
|
||||
import { CalendarProps } from "react-aria";
|
||||
import { range } from ":/utils";
|
||||
import { Button } from ":/components/Button";
|
||||
import { CalendarGrid } from ":/components/Forms/DatePicker/CalendarGrid";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
|
||||
// todo to be factorized with the select component
|
||||
interface Option {
|
||||
value: number;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// todo to be factorized with the select component
|
||||
const optionToString = (option: Option | null) => {
|
||||
return option ? option.label : "";
|
||||
};
|
||||
|
||||
type DropdownValuesProps = {
|
||||
options: Array<Option>;
|
||||
downShift: UseSelectReturnValue<Option>;
|
||||
};
|
||||
|
||||
// todo to be factorized with the select component
|
||||
const DropdownValues = ({ options, downShift }: DropdownValuesProps) => (
|
||||
<div
|
||||
className={classNames("c__calendar__menu", {
|
||||
"c__calendar__menu--opened": downShift.isOpen,
|
||||
})}
|
||||
{...downShift.getMenuProps()}
|
||||
>
|
||||
<ul>
|
||||
{downShift.isOpen &&
|
||||
options.map((item, index) => (
|
||||
<li
|
||||
key={`${item.value}${index}`}
|
||||
{...downShift.getItemProps({
|
||||
item,
|
||||
index,
|
||||
disabled: item.disabled,
|
||||
})}
|
||||
className={classNames("c__calendar__menu__item", {
|
||||
"c__calendar__menu__item--highlight":
|
||||
downShift.highlightedIndex === index,
|
||||
"c__calendar__menu__item--selected":
|
||||
downShift.selectedItem?.label === item.label,
|
||||
"c__calendar__menu__item--disabled": item.disabled,
|
||||
})}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface CalendarSubProps extends CalendarProps<DateValue> {
|
||||
minYear?: number;
|
||||
maxYear?: number;
|
||||
}
|
||||
|
||||
export const Calendar = ({
|
||||
minYear = 1900, // in gregorian calendar.
|
||||
maxYear = 2050, // in gregorian calendar.
|
||||
...props
|
||||
}: CalendarSubProps) => {
|
||||
const { locale } = useLocale();
|
||||
|
||||
const state = useCalendarState({
|
||||
...props,
|
||||
locale,
|
||||
createCalendar,
|
||||
});
|
||||
|
||||
const ref = useRef(null);
|
||||
const { calendarProps, prevButtonProps, nextButtonProps } = useCalendar(
|
||||
props,
|
||||
state
|
||||
);
|
||||
|
||||
const monthItemsFormatter = useDateFormatter({
|
||||
month: "long",
|
||||
timeZone: state.timeZone,
|
||||
});
|
||||
|
||||
const selectedMonthItemFormatter = useDateFormatter({
|
||||
month: "short",
|
||||
timeZone: state.timeZone,
|
||||
});
|
||||
|
||||
const yearItemsFormatter = useDateFormatter({
|
||||
year: "numeric",
|
||||
timeZone: state.timeZone,
|
||||
});
|
||||
|
||||
const monthItems: Array<Option> = useMemo(() => {
|
||||
// Note that in some calendar systems, such as the Hebrew, the number of months may differ between years.
|
||||
const numberOfMonths = state.focusedDate.calendar.getMonthsInYear(
|
||||
state.focusedDate
|
||||
);
|
||||
return range(1, numberOfMonths).map((monthNumber) => {
|
||||
const date = state.focusedDate.set({ month: monthNumber });
|
||||
return {
|
||||
value: monthNumber,
|
||||
label: monthItemsFormatter.format(date.toDate(state.timeZone)),
|
||||
disabled:
|
||||
(!!state.minValue && state.minValue.month > monthNumber) ||
|
||||
(!!state.maxValue && state.maxValue.month < monthNumber),
|
||||
};
|
||||
});
|
||||
}, [state.maxValue, state.minValue, state.focusedDate.year]);
|
||||
|
||||
const downshiftMonth = useSelect({
|
||||
items: monthItems,
|
||||
itemToString: optionToString,
|
||||
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
||||
const updatedFocusedDate = state.focusedDate.set({
|
||||
month: e?.selectedItem?.value,
|
||||
});
|
||||
state.setFocusedDate(updatedFocusedDate);
|
||||
},
|
||||
});
|
||||
|
||||
const yearItems: Array<Option> = useMemo(() => {
|
||||
const calendarCurrentUser = createCalendar(
|
||||
new Intl.DateTimeFormat().resolvedOptions().calendar
|
||||
);
|
||||
const minDate = toCalendar(
|
||||
new CalendarDate(new GregorianCalendar(), minYear, 1, 1),
|
||||
calendarCurrentUser
|
||||
);
|
||||
const maxDate = toCalendar(
|
||||
new CalendarDate(new GregorianCalendar(), maxYear, 12, 31),
|
||||
calendarCurrentUser
|
||||
);
|
||||
return range(minDate.year, maxDate.year).map((yearNumber) => {
|
||||
const date = state.focusedDate.set({ year: yearNumber });
|
||||
return {
|
||||
value: yearNumber,
|
||||
label: yearItemsFormatter.format(date.toDate(state.timeZone)),
|
||||
disabled:
|
||||
(!!state.minValue && state.minValue.year > yearNumber) ||
|
||||
(!!state.maxValue && state.maxValue.year < yearNumber),
|
||||
};
|
||||
});
|
||||
}, [state.focusedDate, state.timeZone, state.maxValue, state.minValue]);
|
||||
|
||||
const downshiftYear = useSelect({
|
||||
items: yearItems,
|
||||
itemToString: optionToString,
|
||||
onSelectedItemChange: (e: UseSelectStateChange<Option>) => {
|
||||
const updatedFocusedDate = state.focusedDate.set({
|
||||
year: e?.selectedItem?.value,
|
||||
});
|
||||
state.setFocusedDate(updatedFocusedDate);
|
||||
},
|
||||
initialSelectedItem: yearItems.find(
|
||||
(item) => item.value === state.focusedDate.year
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (downshiftMonth.selectedItem?.value === state.focusedDate.month) {
|
||||
return;
|
||||
}
|
||||
const focusedMonth = monthItems.find(
|
||||
(item) => item.value === state.focusedDate.month
|
||||
);
|
||||
if (focusedMonth) {
|
||||
downshiftMonth.selectItem(focusedMonth);
|
||||
}
|
||||
}, [state.focusedDate.month]);
|
||||
|
||||
useEffect(() => {
|
||||
if (downshiftYear.selectedItem?.value === state.focusedDate.year) {
|
||||
return;
|
||||
}
|
||||
const focusedYear = yearItems.find(
|
||||
(item) => item.value === state.focusedDate.year
|
||||
);
|
||||
if (focusedYear) {
|
||||
downshiftYear.selectItem(focusedYear);
|
||||
}
|
||||
}, [state.focusedDate.year]);
|
||||
const { t } = useCunningham();
|
||||
|
||||
// isDisabled and onPress props don't exist on the <Button /> component.
|
||||
// remove them to avoid any warning.
|
||||
const {
|
||||
isDisabled: isPrevButtonDisabled,
|
||||
onPress: onPressPrev,
|
||||
...prevButtonOtherProps
|
||||
} = prevButtonProps;
|
||||
const {
|
||||
isDisabled: isNextButtonDisabled,
|
||||
onPress: onPressNext,
|
||||
...nextButtonOtherProps
|
||||
} = nextButtonProps;
|
||||
|
||||
return (
|
||||
<div className="c__calendar">
|
||||
<div
|
||||
ref={ref}
|
||||
{...calendarProps}
|
||||
className={classNames("c__calendar__wrapper", {
|
||||
"c__calendar__wrapper--opened":
|
||||
!downshiftMonth.isOpen && !downshiftYear.isOpen,
|
||||
})}
|
||||
>
|
||||
<div className="c__calendar__wrapper__header">
|
||||
<div className="c__calendar__wrapper__header__actions">
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="small"
|
||||
icon={<span className="material-icons">navigate_before</span>}
|
||||
{...{
|
||||
...prevButtonOtherProps,
|
||||
"aria-label": t(
|
||||
"components.forms.date_picker.previous_month_button_aria_label"
|
||||
),
|
||||
}}
|
||||
disabled={isPrevButtonDisabled}
|
||||
onClick={() => state.focusPreviousSection()}
|
||||
/>
|
||||
<Button
|
||||
className="c__calendar__wrapper__header__actions__dropdown"
|
||||
color="tertiary"
|
||||
size="small"
|
||||
iconPosition="right"
|
||||
icon={<span className="material-icons">arrow_drop_down</span>}
|
||||
{...downshiftMonth.getToggleButtonProps()}
|
||||
aria-label={t(
|
||||
"components.forms.date_picker.month_select_button_aria_label"
|
||||
)}
|
||||
>
|
||||
{selectedMonthItemFormatter.format(
|
||||
state.focusedDate.toDate(state.timeZone)
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="small"
|
||||
icon={<span className="material-icons">navigate_next</span>}
|
||||
{...{
|
||||
...nextButtonOtherProps,
|
||||
"aria-label": t(
|
||||
"components.forms.date_picker.next_month_button_aria_label"
|
||||
),
|
||||
}}
|
||||
disabled={isNextButtonDisabled}
|
||||
onClick={() => state.focusNextSection()}
|
||||
/>
|
||||
</div>
|
||||
<div className="c__calendar__wrapper__header__actions">
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="small"
|
||||
icon={<span className="material-icons">navigate_before</span>}
|
||||
onClick={() => state.focusPreviousSection(true)}
|
||||
disabled={
|
||||
!!state.minValue &&
|
||||
state.minValue.year > state.focusedDate.add({ years: -1 }).year
|
||||
}
|
||||
aria-label={t(
|
||||
"components.forms.date_picker.previous_year_button_aria_label"
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="c__calendar__wrapper__header__actions__dropdown"
|
||||
color="tertiary"
|
||||
size="small"
|
||||
iconPosition="right"
|
||||
icon={<span className="material-icons">arrow_drop_down</span>}
|
||||
{...downshiftYear.getToggleButtonProps()}
|
||||
aria-label={t(
|
||||
"components.forms.date_picker.year_select_button_aria_label"
|
||||
)}
|
||||
>
|
||||
{yearItemsFormatter.format(
|
||||
state.focusedDate.toDate(state.timeZone)
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
color="tertiary"
|
||||
size="small"
|
||||
icon={<span className="material-icons">navigate_next</span>}
|
||||
onClick={() => state.focusNextSection(true)}
|
||||
disabled={
|
||||
!!state.maxValue &&
|
||||
state.maxValue.year < state.focusedDate.add({ years: 1 }).year
|
||||
}
|
||||
aria-label={t(
|
||||
"components.forms.date_picker.next_year_button_aria_label"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!downshiftMonth.isOpen && !downshiftYear.isOpen && (
|
||||
<CalendarGrid state={state} />
|
||||
)}
|
||||
</div>
|
||||
<DropdownValues options={monthItems} downShift={downshiftMonth} />
|
||||
<DropdownValues options={yearItems} downShift={downshiftYear} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useCalendarCell } from "@react-aria/calendar";
|
||||
import {
|
||||
CalendarDate,
|
||||
getLocalTimeZone,
|
||||
isToday,
|
||||
} from "@internationalized/date";
|
||||
import { CalendarState } from "@react-stately/calendar";
|
||||
import { Button } from ":/components/Button";
|
||||
|
||||
interface CalendarCellProps {
|
||||
state: CalendarState;
|
||||
date: CalendarDate;
|
||||
}
|
||||
|
||||
export const CalendarCell = ({ state, date }: CalendarCellProps) => {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const { cellProps, buttonProps, isSelected, formattedDate } = useCalendarCell(
|
||||
{ date },
|
||||
state,
|
||||
ref
|
||||
);
|
||||
return (
|
||||
<td {...cellProps}>
|
||||
<Button
|
||||
size="small"
|
||||
color={isSelected ? "primary" : "tertiary"}
|
||||
className={classNames("c__calendar__wrapper__grid__week-row__button", {
|
||||
"c__calendar__wrapper__grid__week-row__button--selected": isSelected,
|
||||
"c__calendar__wrapper__grid__week-row__button--today": isToday(
|
||||
date,
|
||||
getLocalTimeZone()
|
||||
),
|
||||
})}
|
||||
disabled={!!cellProps["aria-disabled"]}
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
>
|
||||
{formattedDate}
|
||||
</Button>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useDateFormatter, useLocale } from "@react-aria/i18n";
|
||||
import {
|
||||
endOfMonth,
|
||||
getWeeksInMonth,
|
||||
startOfWeek,
|
||||
today,
|
||||
} from "@internationalized/date";
|
||||
import { useCalendarGrid } from "react-aria";
|
||||
import { CalendarState } from "@react-stately/calendar";
|
||||
import { CalendarCell } from ":/components/Forms/DatePicker/CalendarCell";
|
||||
import { range } from ":/utils";
|
||||
|
||||
interface CalendarGridProps {
|
||||
state: CalendarState;
|
||||
defaultDaysInWeek?: number;
|
||||
}
|
||||
|
||||
export const CalendarGrid = ({
|
||||
state,
|
||||
defaultDaysInWeek = 7,
|
||||
}: CalendarGridProps) => {
|
||||
const { locale } = useLocale();
|
||||
|
||||
const weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale);
|
||||
|
||||
const { gridProps, headerProps } = useCalendarGrid(
|
||||
{
|
||||
startDate: state.visibleRange.start,
|
||||
endDate: endOfMonth(state.visibleRange.start),
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
const shortDayFormatter = useDateFormatter({
|
||||
weekday: "short",
|
||||
timeZone: state.timeZone,
|
||||
});
|
||||
|
||||
const weekDays = useMemo(() => {
|
||||
const weekStart = startOfWeek(today(state.timeZone), locale);
|
||||
return range(0, defaultDaysInWeek - 1).map((index) => {
|
||||
const dateDay = weekStart.add({ days: index }).toDate(state.timeZone);
|
||||
return shortDayFormatter.format(dateDay);
|
||||
});
|
||||
}, [locale, state.timeZone, shortDayFormatter]);
|
||||
|
||||
return (
|
||||
<table {...gridProps} className="c__calendar__wrapper__grid">
|
||||
<thead {...headerProps}>
|
||||
<tr className="c__calendar__wrapper__grid__header-row">
|
||||
{weekDays.map((day, index) => (
|
||||
<th key={`${day}${index}`}>{day}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{range(0, weeksInMonth - 1).map((weekIndex) => (
|
||||
<tr key={weekIndex} className="c__calendar__wrapper__grid__week-row">
|
||||
{state
|
||||
.getDatesInWeek(weekIndex)
|
||||
.map(
|
||||
(date, i) =>
|
||||
date && <CalendarCell key={i} state={state} date={date} />
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
88
packages/react/src/components/Forms/DatePicker/DateField.tsx
Normal file
88
packages/react/src/components/Forms/DatePicker/DateField.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
AriaDatePickerProps,
|
||||
useDateField,
|
||||
useDateSegment,
|
||||
} from "@react-aria/datepicker";
|
||||
import { useLocale } from "@react-aria/i18n";
|
||||
import {
|
||||
DateFieldState,
|
||||
useDateFieldState,
|
||||
DateSegment,
|
||||
} from "@react-stately/datepicker";
|
||||
import { createCalendar, DateValue } from "@internationalized/date";
|
||||
import classNames from "classnames";
|
||||
import { Button } from ":/components/Button";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
|
||||
interface DateSegmentProps {
|
||||
currentSegment: DateSegment;
|
||||
previousSegment: DateSegment;
|
||||
state: DateFieldState;
|
||||
}
|
||||
|
||||
export const DateSegmentInput = ({
|
||||
currentSegment,
|
||||
previousSegment,
|
||||
state,
|
||||
}: DateSegmentProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { segmentProps } = useDateSegment(currentSegment, state, ref);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...segmentProps}
|
||||
className={classNames("c__date-picker__inner__value__segment", {
|
||||
"c__date-picker__inner__value__segment--empty":
|
||||
currentSegment.isPlaceholder ||
|
||||
(currentSegment.type === "literal" && previousSegment?.isPlaceholder),
|
||||
})}
|
||||
>
|
||||
{currentSegment.text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DateField = (props: AriaDatePickerProps<DateValue>) => {
|
||||
const { locale } = useLocale();
|
||||
const state = useDateFieldState({
|
||||
...props,
|
||||
locale,
|
||||
createCalendar,
|
||||
});
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { fieldProps } = useDateField(props, state, ref);
|
||||
const { t } = useCunningham();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="c__date-picker__inner__value" {...fieldProps} ref={ref}>
|
||||
{state.segments.map((segment, i, segments) => (
|
||||
<DateSegmentInput
|
||||
key={i}
|
||||
currentSegment={segment}
|
||||
previousSegment={segments[i - 1]}
|
||||
state={state}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!props.isDisabled && (
|
||||
<Button
|
||||
className={classNames("c__date-picker__inner__action", {
|
||||
"c__date-picker__inner__action--empty": !state.value,
|
||||
})}
|
||||
color="tertiary"
|
||||
size="small"
|
||||
icon={<span className="material-icons">cancel</span>}
|
||||
onClick={() => {
|
||||
// "era" option doesn't clear partially filled dataField.
|
||||
state.clearSegment("day");
|
||||
state.clearSegment("month");
|
||||
state.clearSegment("year");
|
||||
}}
|
||||
aria-label={t("components.forms.date_picker.clear_button_aria_label")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
114
packages/react/src/components/Forms/DatePicker/index.mdx
Normal file
114
packages/react/src/components/Forms/DatePicker/index.mdx
Normal file
@@ -0,0 +1,114 @@
|
||||
|
||||
import { Canvas, Meta, Story, Source, ArgsTable } from '@storybook/addon-docs';
|
||||
import { DatePicker } from "./index";
|
||||
|
||||
<Meta title="Components/Forms/DatePicker/Doc" component={DatePicker}/>
|
||||
|
||||
|
||||
# DatePicker
|
||||
|
||||
|
||||
Cunningham provides a versatile DatePicker component to select or input a date in your form. It uses the headless
|
||||
UI components provided by [React-Spectrum](https://react-spectrum.adobe.com/react-aria/useDatePicker.html) from Adobe.
|
||||
|
||||
> For now it is only available for single date selection, a range date picker and time features will be available soon.
|
||||
|
||||
|
||||
## Basic
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-forms-datepicker--default"/>
|
||||
</Canvas>
|
||||
|
||||
## Default value
|
||||
You can use the following props to change the default value of the DatePicker component by using the `state` props.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--default-value"/>
|
||||
</Canvas>
|
||||
|
||||
|
||||
## States
|
||||
|
||||
You can use the following props to change the state of the DatePicker component by using the `state` props.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--error"/>
|
||||
<Story id="components-forms-datepicker--success"/>
|
||||
</Canvas>
|
||||
|
||||
## Disabled
|
||||
|
||||
As a regular input, you can disable it by using the `disabled` props.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--disabled"/>
|
||||
<Story id="components-forms-datepicker--disabled-value"/>
|
||||
</Canvas>
|
||||
|
||||
## Texts
|
||||
|
||||
As the component uses [Field](?path=/story/components-forms-field-doc--page), you can use the `text` props to provide a description of the checkbox.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--with-text"/>
|
||||
</Canvas>
|
||||
|
||||
## Controlled / Non Controlled
|
||||
|
||||
Like a native input, you can use the DatePicker component in a controlled or non controlled way. You can see the example below
|
||||
using the component in a controlled way.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--controlled"/>
|
||||
</Canvas>
|
||||
|
||||
## MinValue / MaxValue
|
||||
|
||||
You can pass a date range that are valid using the `minValue` and `maxValue` props.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--min-max-value"/>
|
||||
</Canvas>
|
||||
|
||||
## Invalid date
|
||||
|
||||
When passing an invalid date, for example outside of the valid range, the DatePicker component would render invalid.
|
||||
|
||||
<Canvas sourceState="shown">
|
||||
<Story id="components-forms-datepicker--invalid-value"/>
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
You can see the list of props below.
|
||||
|
||||
<ArgsTable of={DatePicker} />
|
||||
|
||||
|
||||
## Design tokens
|
||||
|
||||
Here are the custom design tokens defined by the datepicker.
|
||||
|
||||
| Token | Description |
|
||||
|--------------- |----------------------------- |
|
||||
| background-color | Background color of the datepicker |
|
||||
| border-color | Border color of the datepicker |
|
||||
| border-color--hover | Border color of the datepicker on mouse hover |
|
||||
| border-color--focus | Border color of the datepicker when focus |
|
||||
| border-radius | Border radius of the datepicker |
|
||||
| border-width | Border width of the datepicker |
|
||||
| border-radius--hover | Border radius of the datepicker on mouse hover |
|
||||
| border-radius--focus | Border radius of the datepicker when focused |
|
||||
| color | Value color |
|
||||
| font-size | Value font size |
|
||||
| height | Height of the combo box |
|
||||
| item-background-color--hover | Background color of the item on mouse hover (months/years menus) |
|
||||
| item-background-color--selected | Background color of the selected item (months/years menus) |
|
||||
| item-color | Color of the item (months/years menus) |
|
||||
| item-font-size | Font size of the item (months/years menus) |
|
||||
| menu-background-color | Background color of the menu (months/years menus) |
|
||||
| grid-cell--border-color--today | Border color of the today grid-cell |
|
||||
| grid-cell--color--today | Value color of the today grid-cell |
|
||||
|
||||
See also [Field](?path=/story/components-forms-field-doc--page)
|
||||
291
packages/react/src/components/Forms/DatePicker/index.scss
Normal file
291
packages/react/src/components/Forms/DatePicker/index.scss
Normal file
@@ -0,0 +1,291 @@
|
||||
@use 'src/utils';
|
||||
|
||||
|
||||
.c__date-picker {
|
||||
position: relative;
|
||||
|
||||
|
||||
&__wrapper {
|
||||
border-radius: var(--c--components--forms-datepicker--border-radius);
|
||||
border-width: var(--c--components--forms-datepicker--border-width);
|
||||
border-color: var(--c--components--forms-datepicker--border-color);
|
||||
border-style: var(--c--components--forms-datepicker--border-style);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: border var(--c--theme--transitions--duration) var(--c--theme--transitions--ease-out);
|
||||
padding: 0 0.75rem;
|
||||
gap: 1rem;
|
||||
color: var(--c--components--forms-datepicker--color);
|
||||
box-sizing: border-box;
|
||||
height: var(--c--components--forms-datepicker--height);
|
||||
background-color: var(--c--components--forms-datepicker--background-color);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
min-width: 0;
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: flex;
|
||||
font-size: var(--c--components--forms-datepicker--font-size);
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
|
||||
&__segment {
|
||||
&--empty {
|
||||
color: var(--c--components--forms-field--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
|
||||
&.c__button--small.c__button--icon-only {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
&--empty {
|
||||
color: var(--c--components--forms-field--color);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Modifiers */
|
||||
|
||||
&--disabled {
|
||||
.c__date-picker__wrapper {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
border-color: var(--c--theme--colors--greyscale-300);
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
|
||||
label {
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
}
|
||||
|
||||
.c__date-picker__inner {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
&__action {
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--invalid {
|
||||
.c__date-picker__wrapper {
|
||||
border-color: var(--c--theme--colors--danger-600);
|
||||
border-radius: var(--c--components--forms-datepicker--border-radius);
|
||||
|
||||
&__toggle {
|
||||
color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.c__date-picker--disabled) {
|
||||
&:hover {
|
||||
.c__date-picker__wrapper {
|
||||
border-color: var(--c--theme--colors--danger-200);
|
||||
&__toggle {
|
||||
color: var(--c--theme--colors--danger-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
.c__date-picker__wrapper {
|
||||
border-color: var(--c--theme--colors--success-600);
|
||||
border-radius: var(--c--components--forms-datepicker--border-radius);
|
||||
|
||||
&__toggle {
|
||||
color: var(--c--theme--colors--success-600);
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--c--theme--colors--success-600);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.c__date-picker--disabled) {
|
||||
&:hover {
|
||||
.c__date-picker__wrapper {
|
||||
border-color: var(--c--theme--colors--success-200);
|
||||
&__toggle {
|
||||
color: var(--c--theme--colors--success-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--focused {
|
||||
.c__date-picker__wrapper {
|
||||
border-radius: var(--c--components--forms-datepicker--border-radius--focus);
|
||||
border-color: var(--c--components--forms-datepicker--border-color--focus);
|
||||
|
||||
&__toggle {
|
||||
color: var(--c--components--forms-datepicker--border-color--focus);
|
||||
}
|
||||
}}
|
||||
|
||||
&:not(&--focused):not(&--invalid):not(&--disabled):not(&--success) {
|
||||
&:hover {
|
||||
.c__date-picker__wrapper {
|
||||
border-radius: var(--c--components--forms-datepicker--border-radius--hover);
|
||||
border-color: var(--c--components--forms-datepicker--border-color--hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.c__calendar {
|
||||
display: block;
|
||||
transform: translate(2px, 0);
|
||||
|
||||
&__wrapper {
|
||||
display: block;
|
||||
padding: 0.75rem;
|
||||
|
||||
&--opened {
|
||||
@extend %shadow;
|
||||
background-color: var(--c--components--forms-datepicker--menu-background-color);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
min-width: 8.5rem;
|
||||
|
||||
&__dropdown {
|
||||
padding: 0 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__grid {
|
||||
width: 100%;
|
||||
padding-top: 0.5rem;
|
||||
table-layout: fixed;
|
||||
|
||||
&__header-row {
|
||||
line-height: 3rem;
|
||||
font-size: var(--c--components--button--small-font-size);
|
||||
|
||||
th {
|
||||
font-weight: var(--c--components--button--font-weight);
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
}
|
||||
}
|
||||
|
||||
&__week-row {
|
||||
&__button {
|
||||
margin: 0.2rem auto;
|
||||
&--today {
|
||||
border: 1px solid var(--c--components--forms-datepicker--grid-cell--border-color--today);
|
||||
}
|
||||
&--today:not(&--selected) {
|
||||
color: var(--c--components--forms-datepicker--grid-cell--color--today);
|
||||
}
|
||||
}
|
||||
.c__button--small {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
@extend %shadow;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -2px;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
max-height: 25rem;
|
||||
background-color: var(--c--components--forms-datepicker--menu-background-color);
|
||||
transform: translate(2px, 0);
|
||||
display: none;
|
||||
z-index: 2;
|
||||
|
||||
&--opened {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-top: px-to-rem(3px);
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: 0.75rem;
|
||||
font-size: var(--c--components--forms-datepicker--item-font-size);
|
||||
color: var(--c--components--forms-datepicker--item-color);
|
||||
cursor: pointer;
|
||||
|
||||
&--highlight {
|
||||
background-color: var(--c--components--forms-datepicker--item-background-color--hover);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background-color: var(--c--components--forms-datepicker--item-background-color--selected);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: var(--c--theme--colors--greyscale-400);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1087
packages/react/src/components/Forms/DatePicker/index.spec.tsx
Normal file
1087
packages/react/src/components/Forms/DatePicker/index.spec.tsx
Normal file
File diff suppressed because it is too large
Load Diff
104
packages/react/src/components/Forms/DatePicker/index.stories.tsx
Normal file
104
packages/react/src/components/Forms/DatePicker/index.stories.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { CunninghamProvider } from ":/components/Provider";
|
||||
import { Button } from ":/components/Button";
|
||||
import { DatePicker } from "./index";
|
||||
|
||||
export default {
|
||||
title: "Components/Forms/DatePicker",
|
||||
component: DatePicker,
|
||||
} as Meta<typeof DatePicker>;
|
||||
|
||||
const Template: StoryFn<typeof DatePicker> = (args) => (
|
||||
<CunninghamProvider>
|
||||
<DatePicker {...args} label="Pick a date" />
|
||||
</CunninghamProvider>
|
||||
);
|
||||
|
||||
export const Default = () => (
|
||||
<div style={{ minHeight: "400px" }}>
|
||||
<CunninghamProvider>
|
||||
<DatePicker label="Pick a date" />
|
||||
</CunninghamProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Disabled = {
|
||||
render: Template,
|
||||
args: { disabled: true },
|
||||
};
|
||||
|
||||
export const DefaultValue = {
|
||||
render: Template,
|
||||
args: { defaultValue: "2023-05-24" },
|
||||
};
|
||||
|
||||
export const DisabledValue = {
|
||||
render: Template,
|
||||
args: { disabled: true, defaultValue: "2023-05-24" },
|
||||
};
|
||||
|
||||
export const Error = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
state: "error",
|
||||
text: "Something went wrong",
|
||||
},
|
||||
};
|
||||
|
||||
export const Success = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
state: "success",
|
||||
text: "Well done",
|
||||
},
|
||||
};
|
||||
|
||||
export const MinMaxValue = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
minValue: "2023-04-23",
|
||||
maxValue: "2023-06-23",
|
||||
},
|
||||
};
|
||||
|
||||
export const InvalidValue = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-02-24",
|
||||
minValue: "2023-04-23",
|
||||
maxValue: "2023-06-23",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithText = {
|
||||
render: Template,
|
||||
args: {
|
||||
defaultValue: "2023-05-24",
|
||||
text: "This is a text, you can display anything you want here like warnings, information or errors.",
|
||||
},
|
||||
};
|
||||
|
||||
export const Controlled = () => {
|
||||
const [value, setValue] = useState<string | Date>("2023-05-26");
|
||||
return (
|
||||
<CunninghamProvider>
|
||||
<div>
|
||||
<div>
|
||||
Value: <span>{value?.toString()}</span>
|
||||
</div>
|
||||
<DatePicker
|
||||
label="Pick a date"
|
||||
value={value}
|
||||
onChange={(e: string) => {
|
||||
setValue(e);
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => setValue("")}>Reset</Button>
|
||||
</div>
|
||||
</CunninghamProvider>
|
||||
);
|
||||
};
|
||||
174
packages/react/src/components/Forms/DatePicker/index.tsx
Normal file
174
packages/react/src/components/Forms/DatePicker/index.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React, { PropsWithChildren, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
CalendarDate,
|
||||
DateValue,
|
||||
parseAbsoluteToLocal,
|
||||
toCalendarDate,
|
||||
} from "@internationalized/date";
|
||||
import {
|
||||
DatePickerStateOptions,
|
||||
useDatePickerState,
|
||||
} from "@react-stately/datepicker";
|
||||
import { useDatePicker } from "@react-aria/datepicker";
|
||||
import classNames from "classnames";
|
||||
import { Button } from ":/components/Button";
|
||||
import { Field, FieldProps } from ":/components/Forms/Field";
|
||||
import { LabelledBox } from ":/components/Forms/LabelledBox";
|
||||
import { DateField } from ":/components/Forms/DatePicker/DateField";
|
||||
import { Calendar } from ":/components/Forms/DatePicker/Calendar";
|
||||
import { Popover } from ":/components/Popover";
|
||||
import { useCunningham } from ":/components/Provider";
|
||||
|
||||
type DatePickerProps = PropsWithChildren &
|
||||
FieldProps & {
|
||||
label: string;
|
||||
name?: string;
|
||||
value?: null | Date | string;
|
||||
defaultValue?: Date | string;
|
||||
minValue?: Date | string;
|
||||
maxValue?: Date | string;
|
||||
onChange?: (value: string) => void | undefined;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const DatePicker = ({
|
||||
label,
|
||||
name,
|
||||
disabled = false,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
if (props.defaultValue && props.value) {
|
||||
throw new Error(
|
||||
"You cannot use both defaultValue and value props on DatePicker component"
|
||||
);
|
||||
}
|
||||
|
||||
const parseCalendarDate = (
|
||||
rawDate: Date | string | undefined
|
||||
): undefined | CalendarDate => {
|
||||
if (!rawDate) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const ISODateString = new Date(rawDate).toISOString();
|
||||
return toCalendarDate(parseAbsoluteToLocal(ISODateString));
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
"Invalid date format when initializing props on DatePicker component"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const datePickerOptions: DatePickerStateOptions<DateValue> = {
|
||||
value:
|
||||
// Force clear the component's value when passing null or an empty string.
|
||||
props.value === "" || props.value === null
|
||||
? null
|
||||
: parseCalendarDate(props.value),
|
||||
defaultValue: parseCalendarDate(props.defaultValue),
|
||||
minValue: parseCalendarDate(props.minValue),
|
||||
maxValue: parseCalendarDate(props.maxValue),
|
||||
onChange: (value: DateValue | null) => {
|
||||
props.onChange?.(value?.toString() || "");
|
||||
},
|
||||
shouldCloseOnSelect: true,
|
||||
granularity: "day",
|
||||
isDisabled: disabled,
|
||||
label,
|
||||
};
|
||||
|
||||
const state = useDatePickerState(datePickerOptions);
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useCunningham();
|
||||
|
||||
const { fieldProps, buttonProps, groupProps, calendarProps } = useDatePicker(
|
||||
datePickerOptions,
|
||||
state,
|
||||
wrapperRef
|
||||
);
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const labelAsPlaceholder = useMemo(
|
||||
() => !state.isOpen && !state.value,
|
||||
[state.value, state.isOpen]
|
||||
);
|
||||
|
||||
const isDateInvalid = useMemo(
|
||||
() => state.validationState === "invalid" || props.state === "error",
|
||||
[state.validationState, props.state]
|
||||
);
|
||||
|
||||
// onPress props don't exist on the <Button /> component.
|
||||
// Remove it to avoid any warning.
|
||||
const { onPress: onPressToggleButton, ...otherButtonProps } = buttonProps;
|
||||
|
||||
return (
|
||||
<Field {...props}>
|
||||
<div
|
||||
ref={pickerRef}
|
||||
className={classNames("c__date-picker", {
|
||||
"c__date-picker--disabled": disabled,
|
||||
"c__date-picker--invalid": isDateInvalid,
|
||||
"c__date-picker--success": props.state === "success",
|
||||
"c__date-picker--focused":
|
||||
!isDateInvalid && !disabled && (state.isOpen || isFocused),
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames("c__date-picker__wrapper", {
|
||||
"c__date-picker__wrapper--clickable": labelAsPlaceholder,
|
||||
})}
|
||||
ref={wrapperRef}
|
||||
{...groupProps}
|
||||
aria-label={label}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => !state.isOpen && state.open()}
|
||||
>
|
||||
{state.value && (
|
||||
<input type="hidden" name={name} value={state.value?.toString()} />
|
||||
)}
|
||||
<Button
|
||||
{...{
|
||||
...otherButtonProps,
|
||||
"aria-label": t(
|
||||
state.isOpen
|
||||
? "components.forms.date_picker.toggle_button_aria_label_close"
|
||||
: "components.forms.date_picker.toggle_button_aria_label_open"
|
||||
),
|
||||
}}
|
||||
color="tertiary"
|
||||
size="small"
|
||||
className="c__date-picker__wrapper__toggle"
|
||||
onClick={state.toggle}
|
||||
icon={<span className="material-icons">calendar_today</span>}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<LabelledBox label={label} labelAsPlaceholder={labelAsPlaceholder}>
|
||||
<div className="c__date-picker__inner">
|
||||
{!labelAsPlaceholder && (
|
||||
<DateField
|
||||
{...{
|
||||
...fieldProps,
|
||||
onFocusChange: (focus) => setIsFocused(focus),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LabelledBox>
|
||||
</div>
|
||||
{state.isOpen && (
|
||||
<Popover
|
||||
parentRef={pickerRef}
|
||||
onClickOutside={state.close}
|
||||
borderless
|
||||
>
|
||||
<Calendar {...calendarProps} />
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
23
packages/react/src/components/Forms/DatePicker/tokens.ts
Normal file
23
packages/react/src/components/Forms/DatePicker/tokens.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { DefaultTokens } from "@openfun/cunningham-tokens";
|
||||
|
||||
export const tokens = (defaults: DefaultTokens) => ({
|
||||
"border-color": defaults.theme.colors["greyscale-300"],
|
||||
"border-color--focus": defaults.theme.colors["primary-600"],
|
||||
"border-color--hover": defaults.theme.colors["greyscale-500"],
|
||||
"border-radius": "8px",
|
||||
"border-radius--focus": "2px",
|
||||
"border-radius--hover": "2px",
|
||||
"border-style": "solid",
|
||||
"border-width": "2px",
|
||||
color: defaults.theme.colors["greyscale-800"],
|
||||
"font-size": defaults.theme.font.sizes.l,
|
||||
height: "3.5rem",
|
||||
"item-background-color--hover": defaults.theme.colors["greyscale-200"],
|
||||
"item-background-color--selected": defaults.theme.colors["primary-100"],
|
||||
"item-color": defaults.theme.colors["greyscale-800"],
|
||||
"item-font-size": defaults.theme.font.sizes.l,
|
||||
"background-color": "white",
|
||||
"menu-background-color": "white",
|
||||
"grid-cell--border-color--today": defaults.theme.colors["primary-600"],
|
||||
"grid-cell--color--today": defaults.theme.colors["primary-600"],
|
||||
});
|
||||
@@ -102,11 +102,11 @@
|
||||
}
|
||||
|
||||
&__menu {
|
||||
@extend %shadow;
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
width: calc(100% - 4px);
|
||||
max-height: 10rem;
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--c--components--forms-select--menu-background-color);
|
||||
transform: translate(2px, 0);
|
||||
display: none;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@use 'src/utils';
|
||||
|
||||
|
||||
.c__popover {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@@ -5,7 +8,7 @@
|
||||
z-index: 1;
|
||||
|
||||
&:not(&--borderless) {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
@extend %shadow;
|
||||
background-color: var(--c--components--forms-datepicker--menu-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,25 @@
|
||||
--c--components--forms-field--width: 292px;
|
||||
--c--components--forms-field--font-size: 0.6875rem;
|
||||
--c--components--forms-field--color: #79818A;
|
||||
--c--components--forms-datepicker--border-color: #E7E8EA;
|
||||
--c--components--forms-datepicker--border-color--focus: #0556BF;
|
||||
--c--components--forms-datepicker--border-color--hover: #9EA3AA;
|
||||
--c--components--forms-datepicker--border-radius: 8px;
|
||||
--c--components--forms-datepicker--border-radius--focus: 2px;
|
||||
--c--components--forms-datepicker--border-radius--hover: 2px;
|
||||
--c--components--forms-datepicker--border-style: solid;
|
||||
--c--components--forms-datepicker--border-width: 2px;
|
||||
--c--components--forms-datepicker--color: #303C4B;
|
||||
--c--components--forms-datepicker--font-size: 1rem;
|
||||
--c--components--forms-datepicker--height: 3.5rem;
|
||||
--c--components--forms-datepicker--item-background-color--hover: #F3F4F4;
|
||||
--c--components--forms-datepicker--item-background-color--selected: #EBF2FC;
|
||||
--c--components--forms-datepicker--item-color: #303C4B;
|
||||
--c--components--forms-datepicker--item-font-size: 1rem;
|
||||
--c--components--forms-datepicker--background-color: white;
|
||||
--c--components--forms-datepicker--menu-background-color: white;
|
||||
--c--components--forms-datepicker--grid-cell--border-color--today: #0556BF;
|
||||
--c--components--forms-datepicker--grid-cell--color--today: #0556BF;
|
||||
--c--components--forms-checkbox--background-color--hover: #F3F4F4;
|
||||
--c--components--forms-checkbox--background-color: white;
|
||||
--c--components--forms-checkbox--font-size: 0.8125rem;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const tokens = {"theme":{"colors":{"primary-text":"#FFFFFF","primary-100":"#EBF2FC","primary-200":"#8CB5EA","primary-300":"#5894E1","primary-400":"#377FDB","primary-500":"#055FD2","primary-600":"#0556BF","primary-700":"#044395","primary-800":"#033474","primary-900":"#022858","secondary-text":"#555F6B","secondary-100":"#F2F7FC","secondary-200":"#EBF3FA","secondary-300":"#E2EEF8","secondary-400":"#DDEAF7","secondary-500":"#D4E5F5","secondary-600":"#C1D0DF","secondary-700":"#97A3AE","secondary-800":"#757E87","secondary-900":"#596067","greyscale-000":"#FFFFFF","greyscale-100":"#FAFAFB","greyscale-200":"#F3F4F4","greyscale-300":"#E7E8EA","greyscale-400":"#C2C6CA","greyscale-500":"#9EA3AA","greyscale-600":"#79818A","greyscale-700":"#555F6B","greyscale-800":"#303C4B","greyscale-900":"#0C1A2B","success-text":"#FFFFFF","success-100":"#EFFCD3","success-200":"#DBFAA9","success-300":"#BEF27C","success-400":"#A0E659","success-500":"#76D628","success-600":"#5AB81D","success-700":"#419A14","success-800":"#2C7C0C","success-900":"#1D6607","info-text":"#FFFFFF","info-100":"#EBF2FC","info-200":"#8CB5EA","info-300":"#5894E1","info-400":"#377FDB","info-500":"#055FD2","info-600":"#0556BF","info-700":"#044395","info-800":"#033474","info-900":"#022858","warning-text":"#FFFFFF","warning-100":"#FFF8CD","warning-200":"#FFEF9B","warning-300":"#FFE469","warning-400":"#FFDA43","warning-500":"#FFC805","warning-600":"#DBA603","warning-700":"#B78702","warning-800":"#936901","warning-900":"#7A5400","danger-text":"#FFFFFF","danger-100":"#F4B0B0","danger-200":"#EE8A8A","danger-300":"#E65454","danger-400":"#E13333","danger-500":"#DA0000","danger-600":"#C60000","danger-700":"#9B0000","danger-800":"#780000","danger-900":"#5C0000"},"font":{"sizes":{"h1":"1.75rem","h2":"1.375rem","h3":"1.125rem","h4":"0.8125rem","h5":"0.625rem","h6":"0.5rem","l":"1rem","m":"0.8125rem","s":"0.6875rem"},"weights":{"thin":200,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":700,"black":800},"families":{"base":"\"Roboto Flex Variable\", sans-serif","accent":"\"Roboto Flex Variable\", sans-serif"}},"spacings":{"xl":"4rem","l":"3rem","b":"1.625rem","s":"1rem","t":"0.5rem","st":"0.25rem"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"}},"components":{"forms-switch":{"accent-color":"#419A14","rail-background-color":"#9EA3AA","rail-background-color--disabled":"#C2C6CA","rail-border-radius":"50vw","handle-background-color":"white","handle-background-color--disabled":"#F3F4F4","handle-border-radius":"50%"},"forms-select":{"border-color":"#E7E8EA","border-color--focus":"#0556BF","border-color--hover":"#9EA3AA","border-radius":"8px","border-radius--focus":"2px","border-radius--hover":"2px","border-style":"solid","border-width":"1px","color":"#303C4B","font-size":"1rem","height":"3.5rem","item-background-color--hover":"#F3F4F4","item-background-color--selected":"#EBF2FC","item-color":"#303C4B","item-color--disabled":"#9EA3AA","item-font-size":"1rem","background-color":"white","menu-background-color":"white","label-color--focus":"#0556BF"},"forms-radio":{"border-color":"#E7E8EA","accent-color":"#419A14","background-color":"white"},"forms-input":{"font-weight":400,"font-size":"1rem","border-radius":"8px","border-radius--hover":"2px","border-radius--focus":"2px","border-width":"1px","border-color":"#E7E8EA","border-color--hover":"#9EA3AA","border-color--focus":"#0556BF","border-style":"solid","color":"#303C4B","label-color--focus":"#0556BF","background-color":"white"},"forms-field":{"width":"292px","font-size":"0.6875rem","color":"#79818A"},"forms-checkbox":{"background-color--hover":"#F3F4F4","background-color":"white","font-size":"0.8125rem","font-weight":500,"color":"#0C1A2B","border-color":"#E7E8EA","border-radius":"2px","accent-color":"#419A14","size":"1.5rem"},"button":{"border-radius":"8px","border-radius--active":"2px","medium-height":"48px","small-height":"32px","medium-font-size":"1rem","small-font-size":"0.8125rem","font-weight":400}}};
|
||||
export const tokens = {"theme":{"colors":{"primary-text":"#FFFFFF","primary-100":"#EBF2FC","primary-200":"#8CB5EA","primary-300":"#5894E1","primary-400":"#377FDB","primary-500":"#055FD2","primary-600":"#0556BF","primary-700":"#044395","primary-800":"#033474","primary-900":"#022858","secondary-text":"#555F6B","secondary-100":"#F2F7FC","secondary-200":"#EBF3FA","secondary-300":"#E2EEF8","secondary-400":"#DDEAF7","secondary-500":"#D4E5F5","secondary-600":"#C1D0DF","secondary-700":"#97A3AE","secondary-800":"#757E87","secondary-900":"#596067","greyscale-000":"#FFFFFF","greyscale-100":"#FAFAFB","greyscale-200":"#F3F4F4","greyscale-300":"#E7E8EA","greyscale-400":"#C2C6CA","greyscale-500":"#9EA3AA","greyscale-600":"#79818A","greyscale-700":"#555F6B","greyscale-800":"#303C4B","greyscale-900":"#0C1A2B","success-text":"#FFFFFF","success-100":"#EFFCD3","success-200":"#DBFAA9","success-300":"#BEF27C","success-400":"#A0E659","success-500":"#76D628","success-600":"#5AB81D","success-700":"#419A14","success-800":"#2C7C0C","success-900":"#1D6607","info-text":"#FFFFFF","info-100":"#EBF2FC","info-200":"#8CB5EA","info-300":"#5894E1","info-400":"#377FDB","info-500":"#055FD2","info-600":"#0556BF","info-700":"#044395","info-800":"#033474","info-900":"#022858","warning-text":"#FFFFFF","warning-100":"#FFF8CD","warning-200":"#FFEF9B","warning-300":"#FFE469","warning-400":"#FFDA43","warning-500":"#FFC805","warning-600":"#DBA603","warning-700":"#B78702","warning-800":"#936901","warning-900":"#7A5400","danger-text":"#FFFFFF","danger-100":"#F4B0B0","danger-200":"#EE8A8A","danger-300":"#E65454","danger-400":"#E13333","danger-500":"#DA0000","danger-600":"#C60000","danger-700":"#9B0000","danger-800":"#780000","danger-900":"#5C0000"},"font":{"sizes":{"h1":"1.75rem","h2":"1.375rem","h3":"1.125rem","h4":"0.8125rem","h5":"0.625rem","h6":"0.5rem","l":"1rem","m":"0.8125rem","s":"0.6875rem"},"weights":{"thin":200,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":700,"black":800},"families":{"base":"\"Roboto Flex Variable\", sans-serif","accent":"\"Roboto Flex Variable\", sans-serif"}},"spacings":{"xl":"4rem","l":"3rem","b":"1.625rem","s":"1rem","t":"0.5rem","st":"0.25rem"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"}},"components":{"forms-switch":{"accent-color":"#419A14","rail-background-color":"#9EA3AA","rail-background-color--disabled":"#C2C6CA","rail-border-radius":"50vw","handle-background-color":"white","handle-background-color--disabled":"#F3F4F4","handle-border-radius":"50%"},"forms-select":{"border-color":"#E7E8EA","border-color--focus":"#0556BF","border-color--hover":"#9EA3AA","border-radius":"8px","border-radius--focus":"2px","border-radius--hover":"2px","border-style":"solid","border-width":"1px","color":"#303C4B","font-size":"1rem","height":"3.5rem","item-background-color--hover":"#F3F4F4","item-background-color--selected":"#EBF2FC","item-color":"#303C4B","item-color--disabled":"#9EA3AA","item-font-size":"1rem","background-color":"white","menu-background-color":"white","label-color--focus":"#0556BF"},"forms-radio":{"border-color":"#E7E8EA","accent-color":"#419A14","background-color":"white"},"forms-input":{"font-weight":400,"font-size":"1rem","border-radius":"8px","border-radius--hover":"2px","border-radius--focus":"2px","border-width":"1px","border-color":"#E7E8EA","border-color--hover":"#9EA3AA","border-color--focus":"#0556BF","border-style":"solid","color":"#303C4B","label-color--focus":"#0556BF","background-color":"white"},"forms-field":{"width":"292px","font-size":"0.6875rem","color":"#79818A"},"forms-datepicker":{"border-color":"#E7E8EA","border-color--focus":"#0556BF","border-color--hover":"#9EA3AA","border-radius":"8px","border-radius--focus":"2px","border-radius--hover":"2px","border-style":"solid","border-width":"2px","color":"#303C4B","font-size":"1rem","height":"3.5rem","item-background-color--hover":"#F3F4F4","item-background-color--selected":"#EBF2FC","item-color":"#303C4B","item-font-size":"1rem","background-color":"white","menu-background-color":"white","grid-cell--border-color--today":"#0556BF","grid-cell--color--today":"#0556BF"},"forms-checkbox":{"background-color--hover":"#F3F4F4","background-color":"white","font-size":"0.8125rem","font-weight":500,"color":"#0C1A2B","border-color":"#E7E8EA","border-radius":"2px","accent-color":"#419A14","size":"1.5rem"},"button":{"border-radius":"8px","border-radius--active":"2px","medium-height":"48px","small-height":"32px","medium-font-size":"1rem","small-font-size":"0.8125rem","font-weight":400}}};
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const tokens = {"theme":{"colors":{"primary-text":"#FFFFFF","primary-100":"#EBF2FC","primary-200":"#8CB5EA","primary-300":"#5894E1","primary-400":"#377FDB","primary-500":"#055FD2","primary-600":"#0556BF","primary-700":"#044395","primary-800":"#033474","primary-900":"#022858","secondary-text":"#555F6B","secondary-100":"#F2F7FC","secondary-200":"#EBF3FA","secondary-300":"#E2EEF8","secondary-400":"#DDEAF7","secondary-500":"#D4E5F5","secondary-600":"#C1D0DF","secondary-700":"#97A3AE","secondary-800":"#757E87","secondary-900":"#596067","greyscale-000":"#FFFFFF","greyscale-100":"#FAFAFB","greyscale-200":"#F3F4F4","greyscale-300":"#E7E8EA","greyscale-400":"#C2C6CA","greyscale-500":"#9EA3AA","greyscale-600":"#79818A","greyscale-700":"#555F6B","greyscale-800":"#303C4B","greyscale-900":"#0C1A2B","success-text":"#FFFFFF","success-100":"#EFFCD3","success-200":"#DBFAA9","success-300":"#BEF27C","success-400":"#A0E659","success-500":"#76D628","success-600":"#5AB81D","success-700":"#419A14","success-800":"#2C7C0C","success-900":"#1D6607","info-text":"#FFFFFF","info-100":"#EBF2FC","info-200":"#8CB5EA","info-300":"#5894E1","info-400":"#377FDB","info-500":"#055FD2","info-600":"#0556BF","info-700":"#044395","info-800":"#033474","info-900":"#022858","warning-text":"#FFFFFF","warning-100":"#FFF8CD","warning-200":"#FFEF9B","warning-300":"#FFE469","warning-400":"#FFDA43","warning-500":"#FFC805","warning-600":"#DBA603","warning-700":"#B78702","warning-800":"#936901","warning-900":"#7A5400","danger-text":"#FFFFFF","danger-100":"#F4B0B0","danger-200":"#EE8A8A","danger-300":"#E65454","danger-400":"#E13333","danger-500":"#DA0000","danger-600":"#C60000","danger-700":"#9B0000","danger-800":"#780000","danger-900":"#5C0000"},"font":{"sizes":{"h1":"1.75rem","h2":"1.375rem","h3":"1.125rem","h4":"0.8125rem","h5":"0.625rem","h6":"0.5rem","l":"1rem","m":"0.8125rem","s":"0.6875rem"},"weights":{"thin":200,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":700,"black":800},"families":{"base":"\"Roboto Flex Variable\", sans-serif","accent":"\"Roboto Flex Variable\", sans-serif"}},"spacings":{"xl":"4rem","l":"3rem","b":"1.625rem","s":"1rem","t":"0.5rem","st":"0.25rem"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"}},"components":{"forms-switch":{"accent-color":"#419A14","rail-background-color":"#9EA3AA","rail-background-color--disabled":"#C2C6CA","rail-border-radius":"50vw","handle-background-color":"white","handle-background-color--disabled":"#F3F4F4","handle-border-radius":"50%"},"forms-select":{"border-color":"#E7E8EA","border-color--focus":"#0556BF","border-color--hover":"#9EA3AA","border-radius":"8px","border-radius--focus":"2px","border-radius--hover":"2px","border-style":"solid","border-width":"1px","color":"#303C4B","font-size":"1rem","height":"3.5rem","item-background-color--hover":"#F3F4F4","item-background-color--selected":"#EBF2FC","item-color":"#303C4B","item-color--disabled":"#9EA3AA","item-font-size":"1rem","background-color":"white","menu-background-color":"white","label-color--focus":"#0556BF"},"forms-radio":{"border-color":"#E7E8EA","accent-color":"#419A14","background-color":"white"},"forms-input":{"font-weight":400,"font-size":"1rem","border-radius":"8px","border-radius--hover":"2px","border-radius--focus":"2px","border-width":"1px","border-color":"#E7E8EA","border-color--hover":"#9EA3AA","border-color--focus":"#0556BF","border-style":"solid","color":"#303C4B","label-color--focus":"#0556BF","background-color":"white"},"forms-field":{"width":"292px","font-size":"0.6875rem","color":"#79818A"},"forms-checkbox":{"background-color--hover":"#F3F4F4","background-color":"white","font-size":"0.8125rem","font-weight":500,"color":"#0C1A2B","border-color":"#E7E8EA","border-radius":"2px","accent-color":"#419A14","size":"1.5rem"},"button":{"border-radius":"8px","border-radius--active":"2px","medium-height":"48px","small-height":"32px","medium-font-size":"1rem","small-font-size":"0.8125rem","font-weight":400}}};
|
||||
export const tokens = {"theme":{"colors":{"primary-text":"#FFFFFF","primary-100":"#EBF2FC","primary-200":"#8CB5EA","primary-300":"#5894E1","primary-400":"#377FDB","primary-500":"#055FD2","primary-600":"#0556BF","primary-700":"#044395","primary-800":"#033474","primary-900":"#022858","secondary-text":"#555F6B","secondary-100":"#F2F7FC","secondary-200":"#EBF3FA","secondary-300":"#E2EEF8","secondary-400":"#DDEAF7","secondary-500":"#D4E5F5","secondary-600":"#C1D0DF","secondary-700":"#97A3AE","secondary-800":"#757E87","secondary-900":"#596067","greyscale-000":"#FFFFFF","greyscale-100":"#FAFAFB","greyscale-200":"#F3F4F4","greyscale-300":"#E7E8EA","greyscale-400":"#C2C6CA","greyscale-500":"#9EA3AA","greyscale-600":"#79818A","greyscale-700":"#555F6B","greyscale-800":"#303C4B","greyscale-900":"#0C1A2B","success-text":"#FFFFFF","success-100":"#EFFCD3","success-200":"#DBFAA9","success-300":"#BEF27C","success-400":"#A0E659","success-500":"#76D628","success-600":"#5AB81D","success-700":"#419A14","success-800":"#2C7C0C","success-900":"#1D6607","info-text":"#FFFFFF","info-100":"#EBF2FC","info-200":"#8CB5EA","info-300":"#5894E1","info-400":"#377FDB","info-500":"#055FD2","info-600":"#0556BF","info-700":"#044395","info-800":"#033474","info-900":"#022858","warning-text":"#FFFFFF","warning-100":"#FFF8CD","warning-200":"#FFEF9B","warning-300":"#FFE469","warning-400":"#FFDA43","warning-500":"#FFC805","warning-600":"#DBA603","warning-700":"#B78702","warning-800":"#936901","warning-900":"#7A5400","danger-text":"#FFFFFF","danger-100":"#F4B0B0","danger-200":"#EE8A8A","danger-300":"#E65454","danger-400":"#E13333","danger-500":"#DA0000","danger-600":"#C60000","danger-700":"#9B0000","danger-800":"#780000","danger-900":"#5C0000"},"font":{"sizes":{"h1":"1.75rem","h2":"1.375rem","h3":"1.125rem","h4":"0.8125rem","h5":"0.625rem","h6":"0.5rem","l":"1rem","m":"0.8125rem","s":"0.6875rem"},"weights":{"thin":200,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":700,"black":800},"families":{"base":"\"Roboto Flex Variable\", sans-serif","accent":"\"Roboto Flex Variable\", sans-serif"}},"spacings":{"xl":"4rem","l":"3rem","b":"1.625rem","s":"1rem","t":"0.5rem","st":"0.25rem"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"}},"components":{"forms-switch":{"accent-color":"#419A14","rail-background-color":"#9EA3AA","rail-background-color--disabled":"#C2C6CA","rail-border-radius":"50vw","handle-background-color":"white","handle-background-color--disabled":"#F3F4F4","handle-border-radius":"50%"},"forms-select":{"border-color":"#E7E8EA","border-color--focus":"#0556BF","border-color--hover":"#9EA3AA","border-radius":"8px","border-radius--focus":"2px","border-radius--hover":"2px","border-style":"solid","border-width":"1px","color":"#303C4B","font-size":"1rem","height":"3.5rem","item-background-color--hover":"#F3F4F4","item-background-color--selected":"#EBF2FC","item-color":"#303C4B","item-color--disabled":"#9EA3AA","item-font-size":"1rem","background-color":"white","menu-background-color":"white","label-color--focus":"#0556BF"},"forms-radio":{"border-color":"#E7E8EA","accent-color":"#419A14","background-color":"white"},"forms-input":{"font-weight":400,"font-size":"1rem","border-radius":"8px","border-radius--hover":"2px","border-radius--focus":"2px","border-width":"1px","border-color":"#E7E8EA","border-color--hover":"#9EA3AA","border-color--focus":"#0556BF","border-style":"solid","color":"#303C4B","label-color--focus":"#0556BF","background-color":"white"},"forms-field":{"width":"292px","font-size":"0.6875rem","color":"#79818A"},"forms-datepicker":{"border-color":"#E7E8EA","border-color--focus":"#0556BF","border-color--hover":"#9EA3AA","border-radius":"8px","border-radius--focus":"2px","border-radius--hover":"2px","border-style":"solid","border-width":"2px","color":"#303C4B","font-size":"1rem","height":"3.5rem","item-background-color--hover":"#F3F4F4","item-background-color--selected":"#EBF2FC","item-color":"#303C4B","item-font-size":"1rem","background-color":"white","menu-background-color":"white","grid-cell--border-color--today":"#0556BF","grid-cell--color--today":"#0556BF"},"forms-checkbox":{"background-color--hover":"#F3F4F4","background-color":"white","font-size":"0.8125rem","font-weight":500,"color":"#0C1A2B","border-color":"#E7E8EA","border-radius":"2px","accent-color":"#419A14","size":"1.5rem"},"button":{"border-radius":"8px","border-radius--active":"2px","medium-height":"48px","small-height":"32px","medium-font-size":"1rem","small-font-size":"0.8125rem","font-weight":400}}};
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
@import './components/Forms/LabelledBox';
|
||||
@import './components/Forms/Select';
|
||||
@import './components/Forms/Switch';
|
||||
@import './components/Forms/DatePicker';
|
||||
@import './components/Loader';
|
||||
@import './components/Pagination';
|
||||
@import './components/Popover';
|
||||
|
||||
@@ -22,6 +22,17 @@
|
||||
"select": {
|
||||
"toggle_button_aria_label": "Toggle dropdown",
|
||||
"clear_button_aria_label": "Clear selection"
|
||||
},
|
||||
"date_picker": {
|
||||
"toggle_button_aria_label_open": "Open calendar",
|
||||
"toggle_button_aria_label_close": "Close calendar",
|
||||
"clear_button_aria_label": "Clear date",
|
||||
"next_month_button_aria_label": "Next month",
|
||||
"next_year_button_aria_label": "Next year",
|
||||
"previous_month_button_aria_label": "Previous month",
|
||||
"previous_year_button_aria_label": "Previous year",
|
||||
"year_select_button_aria_label": "Select a year",
|
||||
"month_select_button_aria_label": "Select a month"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,17 @@
|
||||
"select": {
|
||||
"toggle_button_aria_label": "Ouvrir le menu",
|
||||
"clear_button_aria_label": "Effacer la sélection"
|
||||
},
|
||||
"date_picker": {
|
||||
"toggle_button_aria_label_open": "Ouvrir le calendrier",
|
||||
"toggle_button_aria_label_close": "Fermer le calendrier",
|
||||
"clear_button_aria_label": "Effacer la date",
|
||||
"next_month_button_aria_label": "Mois suivant",
|
||||
"next_year_button_aria_label": "Année suivante",
|
||||
"previous_month_button_aria_label": "Mois précédent",
|
||||
"previous_year_button_aria_label": "Année précédente",
|
||||
"year_select_button_aria_label": "Sélectionner une année",
|
||||
"month_select_button_aria_label": "Sélectionner un mois"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,7 @@
|
||||
@function px-to-rem($size, $base-font-size:16px) {
|
||||
@return math.div(strip-unit($size), strip-unit($base-font-size)) * 1rem;
|
||||
}
|
||||
|
||||
%shadow {
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user