Compare commits

...
Author SHA1 Message Date
Wahyu Kurniawan 83508cd81d Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4837-badge 2024-02-20 23:38:35 +07:00
Wahyu Kurniawan 76c59e2c58 Merge pull request #80 from snowball-tools/ayungavis/T-4838-checkbox
[T-4838: feat] Checkbox component
2024-02-20 23:37:30 +07:00
Wahyu Kurniawan a5bce0cda2 🎨 style: adjust disabled hover text color 2024-02-20 23:34:20 +07:00
Wahyu Kurniawan 3a085a79ae Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4838-checkbox 2024-02-20 23:29:18 +07:00
Wahyu Kurniawan abfe6a523c Merge pull request #79 from snowball-tools/ayungavis/T-4842-datepicker
[T-4842: feat] Calendar component
2024-02-20 23:26:38 +07:00
Wahyu Kurniawan f764bea6d1 📝 docs: add badge to the example page 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan cc9c24cca1 🎨 style: add new font sizes 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan e482f998a1 ️ feat: create badge comopnent 2024-02-20 23:26:04 +07:00
Wahyu Kurniawan b255078431 📝 docs: add checkbox to example component page 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 26260976fb ️ feat: create check icon for checkbox component 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 5e3a6ad2b5 ️ feat: create checkbox component 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan f31ee7349f 🔧 chore: install @radix-ui/react-checkbox 2024-02-20 23:25:19 +07:00
Wahyu Kurniawan 5c5d759c10 ♻️ refactor: fix typo and change to readable name 2024-02-20 23:24:28 +07:00
Wahyu Kurniawan dc6bf3794a ️ feat: add interaction on the example page 2024-02-20 23:24:28 +07:00
Wahyu Kurniawan af0d28e654 🎨 style: add new shadows 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 4f7f9cf914 📝 docs: add calendar component to the example page 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan ad7dd1920a ️ feat: create calendar component 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan cc97ddff9d ️ feat: create chevron left, right, and grabbed horizontal component 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 0a5a1b8c55 🔧 chore: add vscode settings to enable tailwind variant intellisense 2024-02-20 23:24:27 +07:00
Wahyu Kurniawan 4f2944a974 🔧 chore: install date-fns and react-calendar 2024-02-20 23:24:26 +07:00
Wahyu Kurniawan 69198307fb Merge pull request #77 from snowball-tools/ayungavis/T-4834-button
[T-4834: feat] Button component
2024-02-20 23:23:38 +07:00
Wahyu Kurniawan 0f7c6c73c9 ️ feat: make the button component to forward ref 2024-02-20 22:59:31 +07:00
Wahyu Kurniawan 2ac657c32e 🎨 style: add new spacing 2024-02-19 21:16:55 +07:00
Wahyu Kurniawan 943d427db1 📝 docs: add button icon only on component example 2024-02-19 21:16:39 +07:00
Wahyu Kurniawan 7e6b74208d ️ feat: create cloneIcon util function 2024-02-19 21:16:17 +07:00
Wahyu Kurniawan 2b78cab849 ️ feat: create custom icon and plus icon component 2024-02-19 21:15:51 +07:00
Wahyu Kurniawan 862862a9c5 🎨 style: update button and button icon only theme 2024-02-19 21:15:29 +07:00
Wahyu Kurniawan 6a3af491ca Merge branch 'andrehadianto/design-system-components' of https://github.com/snowball-tools/snowballtools-base into ayungavis/T-4834-button 2024-02-19 20:20:31 +07:00
Wahyu Kurniawan 92f7d07f3f 🔧 chore: install tailwind-variants and @fontsource/inter 2024-02-19 20:16:25 +07:00
Wahyu Kurniawan 6fb94aeb11 🔧 chore: add absolute path 2024-02-19 20:15:56 +07:00
Wahyu Kurniawan 1c848c5a89 🎨 style: add inter font family 2024-02-19 20:15:32 +07:00
Wahyu Kurniawan 17b5ff9a74 🎨 style: add colors, font family, and shadow 2024-02-19 20:14:49 +07:00
Wahyu Kurniawan 56adfc4806 🐛 fix: typescript error 2024-02-19 20:14:23 +07:00
Wahyu Kurniawan b51d050ee6 📝 docs: add button to the example comopnent pag 2024-02-19 20:13:56 +07:00
Wahyu Kurniawan 8021ff7ff9 🔧 chore: add vscode settings on frontend package 2024-02-19 20:13:36 +07:00
Wahyu Kurniawan e850435c2d ️ feat: create button component 2024-02-19 20:13:11 +07:00
Zachery 8a43698c43 Merge pull request #75 from snowball-tools/zachery/T-4843-focus-ring-class
feat: focus ring class
2024-02-19 20:55:52 +08:00
Zachery Ng 499dba2c5e chore: add full palette colors 2024-02-19 20:12:25 +08:00
Zachery Ng d498ba7e9a feat: focus ring class 2024-02-19 20:05:29 +08:00
Andre H 082353e9bc ️ feat: implement components page to act as storybook 2024-02-16 12:31:13 +07:00
32 changed files with 1768 additions and 13 deletions
+6
View File
@@ -0,0 +1,6 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+39
View File
@@ -0,0 +1,39 @@
{
// eslint extension options
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"css.customData": [".vscode/tailwind.json"],
// prettier extension setting
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [80],
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll",
"source.organizeImports"
],
// Show in vscode "Problems" tab when there are errors
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// Use absolute import for typescript files
"typescript.preferences.importModuleSpecifier": "non-relative",
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
+5 -1
View File
@@ -3,7 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-checkbox": "^1.0.4",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -12,13 +14,14 @@
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"assert": "^2.1.0",
"date-fns": "^3.0.1",
"date-fns": "^3.3.1",
"downshift": "^8.2.3",
"eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0",
"luxon": "^3.4.4",
"octokit": "^3.1.2",
"react": "^18.2.0",
"react-calendar": "^4.8.0",
"react-code-blocks": "^0.1.6",
"react-day-picker": "^8.9.1",
"react-dom": "^18.2.0",
@@ -29,6 +32,7 @@
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"react-timer-hook": "^3.0.7",
"tailwind-variants": "^0.2.0",
"typescript": "^4.9.5",
"usehooks-ts": "^2.10.0",
"vertical-stepper-nav": "^1.0.2",
@@ -1,9 +1,8 @@
import React, { useState } from 'react';
import { Card, Collapse, Typography } from '@material-tailwind/react';
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
import { Environment, EnvironmentVariable } from 'gql-client';
interface DisplayEnvironmentVariablesProps {
environment: Environment;
@@ -0,0 +1,43 @@
import { VariantProps, tv } from 'tailwind-variants';
export const badgeTheme = tv({
slots: {
wrapper: ['rounded-full', 'grid', 'place-content-center'],
},
variants: {
variant: {
primary: {
wrapper: ['bg-controls-primary', 'text-elements-on-primary'],
},
secondary: {
wrapper: ['bg-controls-secondary', 'text-elements-on-secondary'],
},
tertiary: {
wrapper: [
'bg-controls-tertiary',
'border',
'border-border-interactive/10',
'text-elements-high-em',
'shadow-button',
],
},
inset: {
wrapper: ['bg-controls-inset', 'text-elements-high-em'],
},
},
size: {
sm: {
wrapper: ['h-5', 'w-5', 'text-xs'],
},
xs: {
wrapper: ['h-4', 'w-4', 'text-2xs', 'font-medium'],
},
},
},
defaultVariants: {
variant: 'primary',
size: 'sm',
},
});
export type BadgeTheme = VariantProps<typeof badgeTheme>;
@@ -0,0 +1,33 @@
import React from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { BadgeTheme, badgeTheme } from './Badge.theme';
export interface BadgeProps
extends ComponentPropsWithoutRef<'div'>,
BadgeTheme {}
/**
* A badge is a small status descriptor for UI elements.
* It can be used to indicate a status, a count, or a category.
* It is typically used in lists, tables, or navigation elements.
*
* @example
* ```tsx
* <Badge variant="primary" size="sm">1</Badge
* ```
*/
export const Badge = ({
className,
children,
variant,
size,
...props
}: BadgeProps) => {
const { wrapper } = badgeTheme();
return (
<div {...props} className={wrapper({ className, variant, size })}>
{children}
</div>
);
};
@@ -0,0 +1 @@
export * from './Badge';
@@ -0,0 +1,162 @@
import { tv } from 'tailwind-variants';
import type { VariantProps } from 'tailwind-variants';
/**
* Defines the theme for a button component.
*/
export const buttonTheme = tv(
{
base: [
'h-fit',
'inline-flex',
'items-center',
'justify-center',
'whitespace-nowrap',
'focus-ring',
'disabled:cursor-not-allowed',
],
variants: {
size: {
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
},
fullWidth: {
true: 'w-full',
},
shape: {
default: '',
rounded: 'rounded-full',
},
iconOnly: {
true: '',
},
variant: {
primary: [
'text-elements-on-primary',
'border',
'border-transparent',
'bg-controls-primary',
'shadow-button',
'hover:bg-controls-primary-hovered',
'focus-visible:bg-controls-primary-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
secondary: [
'text-elements-on-secondary',
'border',
'border-transparent',
'bg-controls-secondary',
'hover:bg-controls-secondary-hovered',
'focus-visible:bg-controls-secondary-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
tertiary: [
'text-elements-on-tertiary',
'border',
'border-border-interactive/10',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
ghost: [
'text-elements-on-tertiary',
'border',
'border-transparent',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
danger: [
'text-elements-on-danger',
'border',
'border-transparent',
'bg-border-danger',
'hover:bg-controls-danger-hovered',
'focus-visible:bg-controls-danger-hovered',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
'danger-ghost': [
'text-elements-danger',
'border',
'border-transparent',
'bg-transparent',
'hover:bg-controls-tertiary-hovered',
'hover:border-border-interactive-hovered',
'hover:border-border-interactive-hovered/[0.14]',
'focus-visible:bg-controls-tertiary-hovered',
'focus-visible:border-border-interactive-hovered',
'focus-visible:border-border-interactive-hovered/[0.14]',
'disabled:text-elements-on-disabled',
'disabled:bg-controls-disabled',
'disabled:border-transparent',
'disabled:shadow-none',
],
unstyled: [],
},
},
compoundVariants: [
{
size: 'lg',
iconOnly: true,
class: ['py-3.5', 'px-3.5'],
},
{
size: 'md',
iconOnly: true,
class: ['py-3.25', 'px-3.25'],
},
{
size: 'sm',
iconOnly: true,
class: ['py-2', 'px-2'],
},
{
size: 'xs',
iconOnly: true,
class: ['py-1', 'px-1'],
},
],
defaultVariants: {
size: 'md',
variant: 'primary',
fullWidth: false,
iconOnly: false,
shape: 'rounded',
},
},
{
responsiveVariants: true,
},
);
/**
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
*/
export type ButtonTheme = VariantProps<typeof buttonTheme>;
@@ -0,0 +1,178 @@
import React, { forwardRef, useCallback } from 'react';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { buttonTheme } from './Button.theme';
import type { ButtonTheme } from './Button.theme';
import { Link } from 'react-router-dom';
import { cloneIcon } from 'utils/cloneIcon';
/**
* Represents the properties of a base button component.
*/
interface ButtonBaseProps {
/**
* The optional left icon element for a component.
* @type {ReactNode}
*/
leftIcon?: ReactNode;
/**
* The optional right icon element to display.
* @type {ReactNode}
*/
rightIcon?: ReactNode;
}
/**
* Interface for the props of a button link component.
*/
export interface ButtonLinkProps
extends Omit<ComponentPropsWithoutRef<'a'>, 'color'> {
/**
* Specifies the optional property `as` with a value of `'a'`.
* @type {'a'}
*/
as?: 'a';
/**
* Indicates whether the item is external or not.
* @type {boolean}
*/
external?: boolean;
/**
* The URL of a web page or resource.
* @type {string}
*/
href: string;
}
export interface ButtonProps
extends Omit<ComponentPropsWithoutRef<'button'>, 'color'> {
/**
* Specifies the optional property `as` with a value of `'button'`.
* @type {'button'}
*/
as?: 'button';
}
/**
* Interface representing the props for a button component.
* Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces.
*/
export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
ButtonBaseProps &
ButtonTheme;
/**
* A custom button component that can be used in React applications.
*/
const Button = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ButtonOrLinkProps
>(
(
{
children,
className,
leftIcon,
rightIcon,
fullWidth,
iconOnly,
shape,
variant,
...props
},
ref,
) => {
// Conditionally render between <NextLink>, <a> or <button> depending on props
// useCallback to prevent unnecessary re-rendering
const Component = useCallback(
({ children: _children, ..._props }: ButtonOrLinkProps) => {
if (_props.as === 'a') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { external, href, as, ...baseLinkProps } = _props;
// External link
if (external) {
const externalLinkProps = {
target: '_blank',
rel: 'noopener',
href,
...baseLinkProps,
};
return <a {...externalLinkProps}>{_children}</a>;
}
// Internal link
return (
<Link {...baseLinkProps} to={href}>
{_children}
</Link>
);
} else {
const { ...buttonProps } = _props;
// @ts-expect-error - as prop is not a valid prop for button elements
return <button {...buttonProps}>{_children}</button>;
}
},
[],
);
/**
* Extracts specific style properties from the given props object and returns them as a new object.
*/
const styleProps = (({
variant = 'primary',
size = 'md',
fullWidth = false,
iconOnly = false,
shape = 'rounded',
as,
}) => ({
variant,
size,
fullWidth,
iconOnly,
shape,
as,
}))({ ...props, fullWidth, iconOnly, shape, variant });
/**
* Validates that a button component has either children or an aria-label prop.
*/
if (typeof children === 'undefined' && !props['aria-label']) {
throw new Error(
'Button components must have either children or an aria-label prop',
);
}
const iconSize = useCallback(() => {
switch (styleProps.size) {
case 'lg':
return { width: 20, height: 20 };
case 'sm':
case 'xs':
return { width: 16, height: 16 };
case 'md':
default: {
return { width: 18, height: 18 };
}
}
}, [styleProps.size])();
return (
<Component
{...props}
// @ts-expect-error - ref is not a valid prop for button elements
ref={ref}
className={buttonTheme({ ...styleProps, class: className })}
>
{cloneIcon(leftIcon, { ...iconSize })}
{children}
{cloneIcon(rightIcon, { ...iconSize })}
</Component>
);
},
);
Button.displayName = 'Button';
export { Button };
@@ -0,0 +1,2 @@
export * from './Button';
export * from './Button.theme';
@@ -0,0 +1,128 @@
/* React Calendar */
.react-calendar {
@apply border-none font-sans;
}
/* Weekdays -- START */
.react-calendar__month-view__weekdays {
@apply p-0 flex items-center justify-center;
}
.react-calendar__month-view__weekdays__weekday {
@apply h-8 w-12 flex items-center justify-center p-0 font-medium text-xs text-elements-disabled mb-2;
}
abbr[title] {
text-decoration: none;
}
/* Weekdays -- END */
/* Days -- START */
.react-calendar__month-view__days {
@apply p-0 gap-0;
}
.react-calendar__month-view__days__day--neighboringMonth {
@apply !text-elements-disabled;
}
.react-calendar__month-view__days__day--neighboringMonth:hover {
@apply !text-elements-disabled !bg-transparent;
}
.react-calendar__month-view__days__day--neighboringMonth {
@apply !text-elements-disabled !bg-transparent;
}
/* For weekend days */
.react-calendar__month-view__days__day--weekend {
/* color: ${colors.grey[950]} !important; */
}
.react-calendar__tile {
@apply h-12 w-12 text-elements-high-em;
}
.react-calendar__tile:hover {
@apply bg-base-bg-emphasized rounded-lg;
}
.react-calendar__tile:focus-visible {
@apply bg-base-bg-emphasized rounded-lg focus-ring z-10;
}
.react-calendar__tile--now {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
}
.react-calendar__tile--now:hover {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
}
.react-calendar__tile--now:focus-visible {
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg focus-ring;
}
.react-calendar__tile--active {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--active:hover {
@apply bg-controls-primary-hovered;
}
.react-calendar__tile--active:focus-visible {
@apply bg-controls-primary-hovered focus-ring;
}
/* Range -- START */
.react-calendar__tile--range {
@apply bg-controls-secondary text-elements-on-secondary rounded-none;
}
.react-calendar__tile--range:hover {
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-none;
}
.react-calendar__tile--range:focus-visible {
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-lg;
}
.react-calendar__tile--rangeStart {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeStart:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeStart:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
}
.react-calendar__tile--rangeEnd {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeEnd:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--rangeEnd:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
}
/* Range -- END */
/* Days -- END */
/* Months -- START */
.react-calendar__tile--hasActive {
@apply bg-controls-primary text-elements-on-primary rounded-lg;
}
.react-calendar__tile--hasActive:hover {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
}
.react-calendar__tile--hasActive:focus-visible {
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
}
@@ -0,0 +1,49 @@
import { VariantProps, tv } from 'tailwind-variants';
export const calendarTheme = tv({
slots: {
wrapper: [
'max-w-[352px]',
'bg-surface-floating',
'shadow-calendar',
'rounded-xl',
],
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
navigation: [
'flex',
'items-center',
'justify-between',
'gap-3',
'py-2.5',
'px-1',
],
actions: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'],
button: [
'flex',
'items-center',
'gap-2',
'px-2',
'py-2',
'rounded-lg',
'border',
'border-border-interactive',
'text-elements-high-em',
'shadow-field',
'bg-white',
'hover:bg-base-bg-alternate',
'focus-visible:bg-base-bg-alternate',
],
footer: [
'flex',
'items-center',
'justify-end',
'py-3',
'px-2',
'gap-3',
'border-t',
'border-border-separator',
],
},
});
export type CalendarTheme = VariantProps<typeof calendarTheme>;
@@ -0,0 +1,298 @@
import React, {
ComponentPropsWithRef,
MouseEvent,
ReactNode,
useCallback,
useState,
} from 'react';
import {
Calendar as ReactCalendar,
CalendarProps as ReactCalendarProps,
} from 'react-calendar';
import { Value } from 'react-calendar/dist/cjs/shared/types';
import { CalendarTheme, calendarTheme } from './Calendar.theme';
import { Button } from 'components/shared/Button';
import {
ChevronGrabberHorizontal,
ChevronLeft,
ChevronRight,
} from 'components/shared/CustomIcon';
import './Calendar.css';
import { format } from 'date-fns';
const CALENDAR_VIEW = ['month', 'year', 'decade'] as const;
export type CalendarView = (typeof CALENDAR_VIEW)[number];
/**
* Defines a custom set of props for a React calendar component by excluding specific props
* from the original ReactCalendarProps type.
* @type {CustomReactCalendarProps}
*/
type CustomReactCalendarProps = Omit<
ReactCalendarProps,
'view' | 'showNavigation' | 'onClickMonth' | 'onClickYear'
>;
export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
/**
* Optional props for wrapping a component with a div element.
*/
wrapperProps?: ComponentPropsWithRef<'div'>;
/**
* Props for the calendar wrapper component.
*/
calendarWrapperProps?: ComponentPropsWithRef<'div'>;
/**
* Optional props for the footer component.
*/
footerProps?: ComponentPropsWithRef<'div'>;
/**
* Optional custom actions to be rendered.
*/
actions?: ReactNode;
/**
* Optional callback function that is called when a value is selected.
* @param {Value} value - The selected value
* @returns None
*/
onSelect?: (value: Value) => void;
/**
* Optional callback function that is called when a cancel action is triggered.
* @returns None
*/
onCancel?: () => void;
}
/**
* Calendar component that allows users to select dates and navigate through months and years.
* @param {Object} CalendarProps - Props for the Calendar component.
* @returns {JSX.Element} A calendar component with navigation, date selection, and actions.
*/
export const Calendar = ({
selectRange,
activeStartDate: activeStartDateProp,
value: valueProp,
wrapperProps,
calendarWrapperProps,
footerProps,
actions,
onSelect,
onCancel,
onChange: onChangeProp,
...props
}: CalendarProps): JSX.Element => {
const {
wrapper,
calendar,
navigation,
actions: actionsClass,
button,
footer,
} = calendarTheme();
const today = new Date();
const currentMonth = format(today, 'MMM');
const currentYear = format(today, 'yyyy');
const [view, setView] = useState<CalendarView>('month');
const [activeDate, setActiveDate] = useState<Date>(
activeStartDateProp ?? today,
);
const [value, setValue] = useState<Value>(valueProp as Value);
const [month, setMonth] = useState(currentMonth);
const [year, setYear] = useState(currentYear);
/**
* Update the navigation label based on the active date
*/
const changeNavigationLabel = useCallback(
(date: Date) => {
setMonth(format(date, 'MMM'));
setYear(format(date, 'yyyy'));
},
[setMonth, setYear],
);
/**
* Change the active date base on the action and range
*/
const handleNavigate = useCallback(
(action: 'previous' | 'next', view: CalendarView) => {
setActiveDate((date) => {
const newDate = new Date(date);
switch (view) {
case 'month':
newDate.setMonth(
action === 'previous' ? date.getMonth() - 1 : date.getMonth() + 1,
);
break;
case 'year':
newDate.setFullYear(
action === 'previous'
? date.getFullYear() - 1
: date.getFullYear() + 1,
);
break;
case 'decade':
newDate.setFullYear(
action === 'previous'
? date.getFullYear() - 10
: date.getFullYear() + 10,
);
break;
}
changeNavigationLabel(newDate);
return newDate;
});
},
[setActiveDate, changeNavigationLabel],
);
/**
* Change the view of the calendar
*/
const handleChangeView = useCallback(
(view: CalendarView) => {
setView(view);
},
[setView],
);
/**
* Change the active date and set the view to the selected type
* and also update the navigation label
*/
const handleChangeNavigation = useCallback(
(view: 'month' | 'year', date: Date) => {
setActiveDate(date);
changeNavigationLabel(date);
setView(view);
},
[setActiveDate, changeNavigationLabel, setView],
);
const handlePrevious = useCallback(() => {
switch (view) {
case 'month':
return handleNavigate('previous', 'month');
case 'year':
return handleNavigate('previous', 'year');
case 'decade':
return handleNavigate('previous', 'decade');
}
}, [view]);
const handleNext = useCallback(() => {
switch (view) {
case 'month':
return handleNavigate('next', 'month');
case 'year':
return handleNavigate('next', 'year');
case 'decade':
return handleNavigate('next', 'decade');
}
}, [view]);
const handleChange = useCallback(
(newValue: Value, event: MouseEvent<HTMLButtonElement>) => {
setValue(newValue);
// Call the onChange prop if it exists
onChangeProp?.(newValue, event);
/**
* Update the active date and navigation label
*
* NOTE:
* For range selection, the active date is not updated
* The user only can select multiple dates within the same month
*/
if (!selectRange) {
setActiveDate(newValue as Date);
changeNavigationLabel(newValue as Date);
}
},
[setValue, setActiveDate, changeNavigationLabel, selectRange],
);
return (
<div
{...wrapperProps}
className={wrapper({ className: wrapperProps?.className })}
>
{/* Calendar wrapper */}
<div
{...calendarWrapperProps}
className={calendar({ className: calendarWrapperProps?.className })}
>
{/* Navigation */}
<div className={navigation()}>
<Button iconOnly size="sm" variant="ghost" onClick={handlePrevious}>
<ChevronLeft />
</Button>
<div className={actionsClass()}>
<Button
variant="unstyled"
className={button()}
rightIcon={
<ChevronGrabberHorizontal className="text-elements-low-em" />
}
onClick={() => handleChangeView('year')}
>
{month}
</Button>
<Button
variant="unstyled"
className={button()}
rightIcon={
<ChevronGrabberHorizontal className="text-elements-low-em" />
}
onClick={() => handleChangeView('decade')}
>
{year}
</Button>
</div>
<Button iconOnly size="sm" variant="ghost" onClick={handleNext}>
<ChevronRight />
</Button>
</div>
{/* Calendar */}
<ReactCalendar
{...props}
activeStartDate={activeDate}
view={view}
value={value}
showNavigation={false}
selectRange={selectRange}
onChange={handleChange}
onClickMonth={(date) => handleChangeNavigation('month', date)}
onClickYear={(date) => handleChangeNavigation('year', date)}
/>
</div>
{/* Footer or CTA */}
<div
{...footerProps}
className={footer({ className: footerProps?.className })}
>
{actions ? (
actions
) : (
<>
<Button variant="tertiary" onClick={onCancel}>
Cancel
</Button>
<Button
disabled={!value}
onClick={() => (value ? onSelect?.(value) : null)}
>
Select
</Button>
</>
)}
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from './Calendar';
@@ -0,0 +1,68 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const getCheckboxVariant = tv({
slots: {
wrapper: ['group', 'flex', 'gap-3'],
indicator: [
'grid',
'place-content-center',
'text-transparent',
'group-hover:text-controls-disabled',
'focus-visible:text-controls-disabled',
'group-focus-visible:text-controls-disabled',
'data-[state=checked]:text-elements-on-primary',
'data-[state=checked]:group-focus-visible:text-elements-on-primary',
'data-[state=indeterminate]:text-elements-on-primary',
'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active',
],
icon: ['w-3', 'h-3', 'stroke-current', 'text-current'],
input: [
'h-5',
'w-5',
'group',
'border',
'border-border-interactive/10',
'bg-controls-tertiary',
'rounded-md',
'transition-all',
'duration-150',
'focus-ring',
'shadow-button',
'group-hover:border-border-interactive/[0.14]',
'group-hover:bg-controls-tertiary',
'data-[state=checked]:bg-controls-primary',
'data-[state=checked]:hover:bg-controls-primary-hovered',
'data-[state=checked]:focus-visible:bg-controls-primary-hovered',
'data-[disabled]:bg-controls-disabled',
'data-[disabled]:shadow-none',
'data-[disabled]:hover:border-border-interactive/10',
'data-[disabled]:cursor-not-allowed',
'data-[state=checked]:data-[disabled]:bg-controls-disabled-active',
],
label: [
'text-sm',
'tracking-[-0.006em]',
'text-elements-high-em',
'flex',
'flex-col',
'gap-1',
'px-1',
],
description: ['text-xs', 'text-elements-low-em'],
},
variants: {
disabled: {
true: {
wrapper: ['cursor-not-allowed'],
indicator: ['group-hover:text-transparent'],
input: [
'group-hover:border-border-interactive/[0.14]',
'group-hover:bg-controls-disabled',
],
label: ['cursor-not-allowed'],
},
},
},
});
export type CheckboxVariants = VariantProps<typeof getCheckboxVariant>;
@@ -0,0 +1,76 @@
import React from 'react';
import * as CheckboxRadix from '@radix-ui/react-checkbox';
import { type CheckboxProps as CheckboxRadixProps } from '@radix-ui/react-checkbox';
import { getCheckboxVariant, type CheckboxVariants } from './Checkbox.theme';
import { CheckIcon } from 'components/shared/CustomIcon';
interface CheckBoxProps extends CheckboxRadixProps, CheckboxVariants {
/**
* The label of the checkbox.
*/
label?: string;
/**
* The description of the checkbox.
*/
description?: string;
}
/**
* Checkbox component is used to allow users to select one or more items from a set.
* It is a wrapper around the `@radix-ui/react-checkbox` component.
*
* It accepts all the props from `@radix-ui/react-checkbox` component and the variants from the theme.
*
* It also accepts `label` and `description` props to display the label and description of the checkbox.
*
* @example
* ```tsx
* <Checkbox
* id="checkbox"
* label="Checkbox"
* description="This is a checkbox"
* />
* ```
*/
export const Checkbox = ({
id,
className,
label,
description,
onCheckedChange,
...props
}: CheckBoxProps) => {
const {
wrapper: wrapperStyle,
indicator: indicatorStyle,
icon: iconStyle,
input: inputStyle,
label: labelStyle,
description: descriptionStyle,
} = getCheckboxVariant({
disabled: props?.disabled,
});
return (
<div className={wrapperStyle()}>
<CheckboxRadix.Root
{...props}
className={inputStyle({ className })}
id={id}
onCheckedChange={onCheckedChange}
>
<CheckboxRadix.Indicator forceMount className={indicatorStyle()}>
<CheckIcon className={iconStyle()} />
</CheckboxRadix.Indicator>
</CheckboxRadix.Root>
{label && (
<label className={labelStyle()} htmlFor={id}>
{label}
{description && (
<span className={descriptionStyle()}>{description}</span>
)}
</label>
)}
</div>
);
};
@@ -0,0 +1 @@
export * from './Checkbox';
@@ -0,0 +1,22 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CheckIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
{...props}
>
<path
d="M1.5 7.5L4.64706 10L10.5 2"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};
@@ -0,0 +1,20 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronGrabberHorizontal = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M6.66666 12.5L9.99999 15.8333L13.3333 12.5M6.66666 7.5L9.99999 4.16666L13.3333 7.5"
stroke="currentColor"
strokeLinecap="square"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronLeft = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.7071 2.66666L5.37378 8.00001L10.7071 13.3333L10 14.0404L3.95956 8.00001L10 1.95956L10.7071 2.66666Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ChevronRight = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.00001 1.95956L12.0405 7.99998L6.00001 14.0404L5.29291 13.3333L10.6262 7.99999L5.29291 2.66666L6.00001 1.95956Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,30 @@
import React, { ComponentPropsWithoutRef } from 'react';
export interface CustomIconProps extends ComponentPropsWithoutRef<'svg'> {
size?: number | string; // width and height will both be set as the same value
name?: string;
}
export const CustomIcon = ({
children,
width = 24,
height = 24,
size,
viewBox = '0 0 24 24',
name,
...rest
}: CustomIconProps) => {
return (
<svg
aria-labelledby={name}
height={size || height}
role="presentation"
viewBox={viewBox}
width={size || width}
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
{children}
</svg>
);
};
@@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const PlusIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.66666 9.99999C1.66666 5.39762 5.39762 1.66666 9.99999 1.66666C14.6024 1.66666 18.3333 5.39762 18.3333 9.99999C18.3333 14.6024 14.6024 18.3333 9.99999 18.3333C5.39762 18.3333 1.66666 14.6024 1.66666 9.99999ZM10.625 6.46483C10.625 6.11966 10.3452 5.83983 9.99999 5.83983C9.65481 5.83983 9.37499 6.11966 9.37499 6.46483V9.37537H6.46446C6.11928 9.37537 5.83946 9.65519 5.83946 10.0004C5.83946 10.3455 6.11928 10.6254 6.46446 10.6254H9.37499V13.5359C9.37499 13.8811 9.65481 14.1609 9.99999 14.1609C10.3452 14.1609 10.625 13.8811 10.625 13.5359V10.6254H13.5355C13.8807 10.6254 14.1605 10.3455 14.1605 10.0004C14.1605 9.65519 13.8807 9.37537 13.5355 9.37537H10.625V6.46483Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -0,0 +1,24 @@
# 1. What icons are compatible with this component?
- Viewbox "0 0 24 24": From where you're exporting from, please make sure the icon is using viewBox="0 0 24 24" before downloading/exporting. Not doing so will result in incorrect icon scaling
# 2. How to add a new icon?
**2.1 Sanitising the icon**
1. Duplicate a current icon e.g. CrossIcon and rename it accordingly.
2. Rename the function inside the new file you duplicated too
3. Replace the markup with your SVG markup (make sure it complies with the above section's rule)
4. Depending on the svg you pasted...
A. If the `<svg>` has only 1 child, remove the `<svg>` parent entirely so you only have the path left
B. If your component has more than 1 paths, rename `<svg>` tag with the `<g>` tag. Then, remove all attributes of this `<g>` tag so that it's just `<g>`
5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. <path d="..." fill="currentColor">. Leave the other attributes without removing them.
6. If your icon has more than one colour, then it's up to you to decide whether we want to use tailwind to help set the fill and stroke colors
7. Lastly, export your icon in `index.ts` by following what was done for CrossIcon
8. Make sure to provide a name to the `<CustomIcon>` component for accessibility sake
9. Done!
**2.3 Use your newly imported icon**
1. You can change simply use `<BellIcon size="32" />` to quickly change both width and height with the same value (square). For custom viewBox, width and height, simply provide all three props.
2. Coloring the icon: Simply add a className with text color. E.g. `<BellIcon className="text-gray-500" />`
@@ -0,0 +1,6 @@
export * from './PlusIcon';
export * from './CustomIcon';
export * from './CheckIcon';
export * from './ChevronGrabberHorizontal';
export * from './ChevronLeft';
export * from './ChevronRight';
+6
View File
@@ -1,3 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.focus-ring {
@apply focus-visible:ring-[3px] focus-visible:ring-snowball-200 focus-visible:ring-offset-1 focus-visible:ring-offset-snowball-500 focus-visible:outline-none;
}
}
+1
View File
@@ -7,6 +7,7 @@ import { GQLClient } from 'gql-client';
import { ThemeProvider } from '@material-tailwind/react';
import './index.css';
import '@fontsource/inter';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { GQLClientProvider } from './context/GQLClientContext';
@@ -0,0 +1,163 @@
import { Badge, BadgeProps } from 'components/shared/Badge';
import { Button, ButtonOrLinkProps } from 'components/shared/Button';
import { Calendar } from 'components/shared/Calendar';
import { Checkbox } from 'components/shared/Checkbox';
import { PlusIcon } from 'components/shared/CustomIcon';
import React, { useState } from 'react';
import { Value } from 'react-calendar/dist/cjs/shared/types';
const Page = () => {
const [singleDate, setSingleDate] = useState<Value>();
const [dateRange, setDateRange] = useState<Value>();
return (
<div className="relative h-full min-h-full">
<div className="flex flex-col items-center justify-center max-w-7xl mx-auto px-20 py-20">
<h1 className="text-4xl font-bold">Manual Storybook</h1>
<p className="mt-4 text-lg text-center text-gray-500">
Get started by editing{' '}
<code className="p-2 font-mono text-sm bg-gray-100 rounded-md">
packages/frontend/src/pages/components/index.tsx
</code>
</p>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Insert Components here */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Button</h1>
<div className="flex flex-col gap-10">
{['primary', 'secondary', 'tertiary', 'danger'].map(
(variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}>
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
<Button
leftIcon={<PlusIcon />}
rightIcon={<PlusIcon />}
variant={variant as ButtonOrLinkProps['variant']}
size={
size !== 'disabled'
? (size as ButtonOrLinkProps['size'])
: 'md'
}
key={`${variant}-${size}`}
disabled={size === 'disabled'}
>
Button
</Button>
))}
</div>
),
)}
{[
'primary',
'secondary',
'tertiary',
'ghost',
'danger',
'danger-ghost',
].map((variant, index) => (
<div className="flex gap-5 flex-wrap" key={index}>
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
<Button
iconOnly
variant={variant as ButtonOrLinkProps['variant']}
size={
size !== 'disabled'
? (size as ButtonOrLinkProps['size'])
: 'md'
}
key={`${variant}-${size}`}
disabled={size === 'disabled'}
>
<PlusIcon />
</Button>
))}
</div>
))}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Badge</h1>
<div className="space-y-5">
{['primary', 'secondary', 'tertiary', 'inset'].map(
(variant, index) => (
<div className="flex gap-5" key={index}>
{['sm', 'xs'].map((size) => (
<Badge
key={size}
variant={variant as BadgeProps['variant']}
size={size as BadgeProps['size']}
>
1
</Badge>
))}
</div>
),
)}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Checkbox</h1>
<div className="flex gap-10 flex-wrap">
{Array.from({ length: 5 }).map((_, index) => (
<Checkbox
id={`checkbox-${index + 1}`}
key={index}
label={`Label ${index + 1}`}
disabled={index === 2 || index === 4 ? true : false}
checked={index === 4 ? true : undefined}
value={`value-${index + 1}`}
/>
))}
</div>
<div className="flex gap-10 flex-wrap">
{Array.from({ length: 2 }).map((_, index) => (
<Checkbox
id={`checkbox-description-${index + 1}`}
key={index}
label={`Label ${index + 1}`}
description={`Description of the checkbox ${index + 1}`}
value={`value-with-description-${index + 1}`}
/>
))}
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Calendar</h1>
<div className="flex flex-col gap-10">
<div className="space-y-5 flex flex-col items-center">
<p>Selected date: {singleDate?.toString()}</p>
<Calendar
value={singleDate}
onChange={setSingleDate}
onSelect={setSingleDate}
/>
</div>
<div className="space-y-5 flex flex-col items-center">
<p>
Start date:{' '}
{dateRange instanceof Array ? dateRange[0]?.toString() : ''}{' '}
<br />
End date:{' '}
{dateRange instanceof Array ? dateRange[1]?.toString() : ''}
</p>
<Calendar selectRange value={dateRange} onChange={setDateRange} />
</div>
</div>
</div>
</div>
</div>
);
};
export default Page;
+17
View File
@@ -0,0 +1,17 @@
import { Children, cloneElement, isValidElement } from 'react';
import type { Attributes, ReactElement, ReactNode } from 'react';
/**
* Clones an icon element with optional additional props.
* @param {ReactNode} icon - The icon element to clone.
* @param {Attributes & P} [props] - Additional props to apply to the cloned icon.
* @returns {ReactNode} - The cloned icon element with the applied props.
*/
export function cloneIcon<P extends object>(
icon: ReactNode,
props?: Attributes & P,
): ReactNode {
return Children.map(icon, (child) =>
isValidElement(child) ? cloneElement(child as ReactElement, props) : child,
);
}
+150 -1
View File
@@ -8,7 +8,156 @@ export default withMT({
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
extend: {
fontSize: {
'2xs': '0.625rem',
'3xs': '0.5rem',
},
colors: {
emerald: {
100: '#d1fae5',
200: '#a9f1d0',
300: '#6ee7b7',
400: '#34d399',
50: '#ecfdf5',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
},
gray: {
0: '#ffffff',
100: '#f1f5f9',
200: '#e2e9f0',
300: '#cbd6e1',
400: '#94a7b8',
50: '#f8fafc',
500: '#60788f',
600: '#475969',
700: '#334555',
800: '#1b2d3e',
900: '#0b1d2e',
},
orange: {
100: '#ffedd5',
200: '#fed7aa',
300: '#fdba74',
400: '#fb923c',
50: '#fff7ed',
500: '#f97316',
600: '#ea580c',
700: '#c2410c',
800: '#9a3412',
900: '#7c2d12',
},
rose: {
100: '#ffe4e6',
200: '#fecdd3',
300: '#fda4af',
400: '#fb7185',
50: '#fff1f2',
500: '#f43f5e',
600: '#e11d48',
700: '#be123c',
800: '#9f1239',
900: '#881337',
},
snowball: {
100: '#e1f1fe',
200: '#ddeefd',
300: '#cfe6fc',
400: '#74bafb',
50: '#ecf6fe',
500: '#47a4fa',
600: '#0f86f5',
700: '#0977dc',
800: '#075185',
900: '#0a3a5c',
},
base: {
bg: '#ffffff',
'bg-alternate': '#f8fafc',
'bg-emphasized': '#f1f5f9',
'bg-emphasized-danger': '#fff1f2',
'bg-emphasized-info': '#ecf6fe',
'bg-emphasized-success': '#ecfdf5',
'bg-emphasized-warning': '#fff7ed',
},
border: {
active: '#0f86f5',
danger: '#e11d48',
'danger-light': '#ffe4e6',
'info-light': '#ddeefd',
interactive: '#082f561a',
'interactive-hovered': '#082f5624',
separator: '#082f560f',
'success-light': '#d1fae5',
'warning-light': '#ffedd5',
},
controls: {
danger: '#e11d48',
'danger-hovered': '#be123c',
disabled: '#e2e9f0',
'disabled-active': '#74bafb',
elevated: '#ffffff',
inset: '#e2e9f0',
'inset-hovered': '#cbd6e1',
primary: '#0f86f5',
'primary-hovered': '#0977dc',
secondary: '#ddeefd',
'secondary-hovered': '#cfe6fc',
tertiary: '#ffffff',
'tertiary-hovered': '#f8fafc',
},
elements: {
danger: '#e11d48',
disabled: '#94a7b8',
'high-em': '#0b1d2e',
info: '#0f86f5',
link: '#0f86f5',
'link-hovered': '#0977dc',
'low-em': '#60788f',
'mid-em': '#475969',
'on-danger': '#ffffff',
'on-disabled': '#60788f',
'on-disabled-active': '#0a3a5c',
'on-emphasized-danger': '#9f1239',
'on-emphasized-info': '#0a3a5c',
'on-emphasized-success': '#065f46',
'on-emphasized-warning': '#9a3412',
'on-high-contrast': '#ffffff',
'on-primary': '#ffffff',
'on-secondary': '#0977dc',
'on-secondary-tinted': '#075185',
'on-tertiary': '#1b2d3e',
success: '#059669',
warning: '#ea580c',
},
surface: {
card: '#ffffff',
'card-hovered': '#f8fafc',
floating: '#ffffff',
'floating-hovered': '#f1f5f9',
'high-contrast': '#0b1d2e',
},
},
boxShadow: {
button:
'inset 0px 0px 4px rgba(255, 255, 255, 0.25), inset 0px -2px 0px rgba(0, 0, 0, 0.04)',
calendar:
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
},
spacing: {
2.5: '0.625rem',
3.25: '0.8125rem',
3.5: '0.875rem',
},
},
},
plugins: [],
});
+2 -1
View File
@@ -14,7 +14,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"baseUrl": "src"
},
"include": ["src"]
}
+173 -8
View File
@@ -1280,7 +1280,7 @@
resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.10.4", "@babel/runtime@^7.3.1":
"@babel/runtime@^7.10.4", "@babel/runtime@^7.13.10", "@babel/runtime@^7.23.7", "@babel/runtime@^7.3.1":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
@@ -2068,6 +2068,11 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
"@fontsource/inter@^5.0.16":
version "5.0.16"
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50"
integrity sha512-qF0aH5UiZvCmneX5orJbVRoc2VTyLTV3X/7laMp03Qt28L+B9tFlZODOGUL64wDWc69YVdi1LeJB0cIgd51lvw==
"@gar/promisify@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
@@ -3272,6 +3277,104 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@radix-ui/primitive@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-checkbox@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
integrity sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/react-compose-refs@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-presence@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
integrity sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-primitive@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-slot@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-controllable-state@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399"
integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66"
integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-size@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@remix-run/router@1.13.1":
version "1.13.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e"
@@ -3905,6 +4008,18 @@
dependencies:
"@types/node" "*"
"@types/lodash.memoize@^4.1.7":
version "4.1.9"
resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.9.tgz#9f8912d39b6e450c0d342a2b74c99d331bf2016b"
integrity sha512-glY1nQuoqX4Ft8Uk+KfJudOD7DQbbEDF6k9XpGncaohW3RW4eSWBlx6AA0fZCrh40tZcQNH4jS/Oc59J6Eq+aw==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.202"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8"
integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==
"@types/long@^4.0.0", "@types/long@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
@@ -4682,6 +4797,11 @@
"@webassemblyjs/ast" "1.11.6"
"@xtuc/long" "4.2.2"
"@wojtekmaj/date-utils@^1.1.3":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz#c3cd67177ac781cfa5736219d702a55a2aea5f2b"
integrity sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==
"@wry/caches@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52"
@@ -6139,6 +6259,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
cmd-shim@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d"
@@ -6816,10 +6941,10 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.0.1.tgz#a95b3e8296e72cf57c99819f37679aa27ca65ec4"
integrity sha512-cr9igCUa0QSqgAMj7JOrYTY6Nh1rmyGrFDko7ADqfmaQqP/I2N4rlfrLl7AWuzDaoIpz6MNjoEcTPzgZYIrhnA==
date-fns@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.3.1.tgz#7581daca0892d139736697717a168afbb908cfed"
integrity sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==
dateformat@^3.0.3:
version "3.0.3"
@@ -8715,6 +8840,14 @@ get-tsconfig@^4.7.0:
dependencies:
resolve-pkg-maps "^1.0.0"
get-user-locale@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-2.3.1.tgz#fc7319429c8a70fac01b3b2a0b08b0c71c1d3fe2"
integrity sha512-VEvcsqKYx7zhZYC1CjecrDC5ziPSpl1gSm0qFFJhHSGDrSC+x4+p1KojWC/83QX//j476gFhkVXP/kNUc9q+bQ==
dependencies:
"@types/lodash.memoize" "^4.1.7"
lodash.memoize "^4.1.1"
git-raw-commits@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-3.0.0.tgz#5432f053a9744f67e8db03dbc48add81252cfdeb"
@@ -11007,7 +11140,7 @@ lodash.isstring@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
lodash.memoize@^4.1.2:
lodash.memoize@^4.1.1, lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz"
integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
@@ -11060,7 +11193,7 @@ long@^5.2.0:
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
loose-envify@^1.1.0, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -13276,7 +13409,7 @@ promzard@^1.0.0:
dependencies:
read "^2.0.0"
prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.1:
prop-types@15.8.1, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -13435,6 +13568,17 @@ react-app-polyfill@^3.0.0:
regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2"
react-calendar@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-4.8.0.tgz#61edbba6d17e7ef8a8012de9143b5e5ff41104c8"
integrity sha512-qFgwo+p58sgv1QYMI1oGNaop90eJVKuHTZ3ZgBfrrpUb+9cAexxsKat0sAszgsizPMVo7vOXedV7Lqa0GQGMvA==
dependencies:
"@wojtekmaj/date-utils" "^1.1.3"
clsx "^2.0.0"
get-user-locale "^2.2.1"
prop-types "^15.6.0"
warning "^4.0.0"
react-code-blocks@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2"
@@ -15026,6 +15170,20 @@ tailwind-merge@1.8.1:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.8.1.tgz#0e56c8afbab2491f72e06381043ffec8b720ba04"
integrity sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww==
tailwind-merge@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.1.tgz#3f10f296a2dba1d88769de8244fafd95c3324aeb"
integrity sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==
dependencies:
"@babel/runtime" "^7.23.7"
tailwind-variants@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/tailwind-variants/-/tailwind-variants-0.2.0.tgz#5c3a24afb6e8bb23f33aa3aa3550add8848de8a3"
integrity sha512-EuW5Sic7c0tzp+p5rJwAgb7398Jb0hi4zkyCstOoZPW0DWwr+EWkNtnZYEo5CjgE1tazHUzyt4oIhss64UXRVA==
dependencies:
tailwind-merge "^2.2.0"
tailwindcss@^3.0.2, tailwindcss@^3.3.6:
version "3.3.6"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz"
@@ -15861,6 +16019,13 @@ walker@^1.0.7:
dependencies:
makeerror "1.0.12"
warning@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz"