diff --git a/package-lock.json b/package-lock.json index 941ec43..adfd6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@acmucsd/components", - "version": "1.0.9", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@acmucsd/components", - "version": "1.0.9", + "version": "1.0.10", "license": "ISC", "devDependencies": { "@chromatic-com/storybook": "^3.2.3", @@ -9686,19 +9686,6 @@ "typescript": ">= 4.3.x" } }, - "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "dev": true, - "peer": true, - "dependencies": { - "scheduler": "^0.25.0" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -10276,13 +10263,6 @@ } } }, - "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "dev": true, - "peer": true - }, "node_modules/schema-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx new file mode 100644 index 0000000..6949a9a --- /dev/null +++ b/src/components/checkbox/Checkbox.tsx @@ -0,0 +1,42 @@ +import React, { FC, useEffect, useState } from 'react'; +import './checkbox.css'; + +export interface CheckboxProps { + checked?: boolean; + disabled?: boolean; + mode?: 'light' | 'dark'; + label?: string; + onChange?: (event: React.ChangeEvent) => void; +} + +export const Checkbox: FC = ({ + checked = false, + disabled = false, + mode = 'light', + label, + onChange, +}) => { + const modeClass = mode === 'dark' ? 'storybook-checkbox--dark' : 'storybook-checkbox--light'; + const [isChecked, setIsChecked] = useState(checked); + + useEffect(() => { + setIsChecked(checked); + }, [checked]); + + const handleChange = (e: React.ChangeEvent) => { + setIsChecked(e.target.checked); + onChange?.(e); + }; + + return ( + + ); +}; diff --git a/src/components/checkbox/checkbox.css b/src/components/checkbox/checkbox.css new file mode 100644 index 0000000..670ca6f --- /dev/null +++ b/src/components/checkbox/checkbox.css @@ -0,0 +1,142 @@ +.storybook-checkbox { + display: inline-flex; + align-items: center; + cursor: pointer; + user-select: none; + font-family: 'DM Sans', sans-serif; + margin: 8px; +} + +.storybook-checkbox input[type="checkbox"] { + cursor: pointer; + appearance: none; + width: 24px; + height: 24px; + border-radius: 4px; + border: 2px solid #000000; + background-color: #ffffff; + position: relative; + outline: none; +} + +/* Label spacing */ +.storybook-checkbox__label { + margin-left: 8px; + font-size: 16px; +} + +/* —————————————————————— + Light mode – Checked + —————————————————————— */ +.storybook-checkbox--light input[type="checkbox"]:checked { + border-color: #0073e6; + background-color: #0073e6; +} + +.storybook-checkbox--light input[type="checkbox"]:checked::before { + content: ""; + position: absolute; + left: 6px; + width: 6px; + height: 14px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.storybook-checkbox--light input[type="checkbox"]:checked:hover { + background-color: #005cb8; +} + +.storybook-checkbox--light input[type="checkbox"]:checked:active { + background-color: #00458a; +} + +.storybook-checkbox--light input[type="checkbox"]:checked:disabled { + background-color: #979797; +} + +/* —————————————————————— + Light mode – Unchecked + —————————————————————— */ +.storybook-checkbox--light input[type="checkbox"] { + border-color: #000000; +} + +.storybook-checkbox--light input[type="checkbox"]:hover { + border-color: #005cb8; +} + +.storybook-checkbox--light input[type="checkbox"]:focus-visible { + outline: 2px solid #0073e6; +} + +.storybook-checkbox--light input[type="checkbox"]:active { + border-color: #00458a; +} + +.storybook-checkbox--light input[type="checkbox"]:disabled { + border-color: #979797; +} + +/* —————————————————————— + Dark mode – Base color + —————————————————————— */ +.storybook-checkbox--dark { + color: #ffffff; +} + +/* —————————————————————— + Dark mode – Checked + —————————————————————— */ +.storybook-checkbox--dark input[type="checkbox"]:checked { + border-color: #0073E6; + background-color: #0073E6; +} + +.storybook-checkbox--dark input[type="checkbox"]:checked::before { + content: ""; + position: absolute; + left: 6px; + width: 6px; + height: 14px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.storybook-checkbox--dark input[type="checkbox"]:checked:hover { + background-color: #57abff; +} + +.storybook-checkbox--dark input[type="checkbox"]:checked:active { + background-color: #8fc7ff; +} + +.storybook-checkbox--dark input[type="checkbox"]:checked:disabled { + background-color: #797c8b; +} + +/* —————————————————————— + Dark mode – Unchecked + —————————————————————— */ +.storybook-checkbox--dark input[type="checkbox"] { + border-color: #ffffff; + background-color: transparent; +} + +.storybook-checkbox--dark input[type="checkbox"]:hover { + border-color: #57abff; +} + +.storybook-checkbox--dark input[type="checkbox"]:focus-visible { + outline: 2px solid #1f8fff; +} + +.storybook-checkbox--dark input[type="checkbox"]:active { + border-color: #8fc7ff; +} + +.storybook-checkbox--dark input[type="checkbox"]:disabled { + border-color: #797c8b; +} \ No newline at end of file diff --git a/src/components/page/Page.tsx b/src/components/page/Page.tsx index 990749c..4652e34 100644 --- a/src/components/page/Page.tsx +++ b/src/components/page/Page.tsx @@ -49,6 +49,7 @@ export const Page = () => { .

+
Tip Adjust the width of the canvas with the{' '} diff --git a/src/components/radio/Radio.tsx b/src/components/radio/Radio.tsx new file mode 100644 index 0000000..d2d2cc4 --- /dev/null +++ b/src/components/radio/Radio.tsx @@ -0,0 +1,38 @@ +import React, { FC } from 'react'; +import './radio.css'; + +export interface RadioProps { + name: string; + value: string; + defaultChecked?: boolean; + disabled?: boolean; + mode?: 'light' | 'dark'; + label?: string; + onChange?: (event: React.ChangeEvent) => void; +} + +export const Radio: FC = ({ + name, + value, + defaultChecked = false, + disabled = false, + mode = 'light', + label, + onChange, +}) => { + const modeClass = mode === 'dark' ? 'storybook-radio--dark' : 'storybook-radio--light'; + + return ( + {label}} + + ); +}; diff --git a/src/components/radio/radio.css b/src/components/radio/radio.css new file mode 100644 index 0000000..551ddfc --- /dev/null +++ b/src/components/radio/radio.css @@ -0,0 +1,149 @@ +.storybook-radio { + display: inline-flex; + align-items: center; + cursor: pointer; + user-select: none; + font-family: 'DM Sans', sans-serif; + margin: 8px; +} + +.storybook-radio input[type="radio"] { + appearance: none; + -webkit-appearance: none; + margin: 0; + padding: 0; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #000000; + /* default (light mode) border color */ + background-color: #ffffff; + /* default (light mode) background */ + cursor: pointer; + position: relative; + outline: none; +} + +/* Label spacing */ +.storybook-radio__label { + margin-left: 8px; + font-size: 16px; +} + +/* —————————————————————— + Light mode – Checked + —————————————————————— */ +.storybook-radio--light input[type="radio"]:checked { + border-color: #0073e6; + background-color: #0073e6; +} + +.storybook-radio--light input[type="radio"]:checked::before { + content: ''; + display: block; + position: absolute; + top: 4px; + left: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #ffffff; +} + +.storybook-radio--light input[type="radio"]:checked:hover { + background-color: #005cb8; +} + +.storybook-radio--light input[type="radio"]:checked:active { + background-color: #00458a; +} + +.storybook-radio--light input[type="radio"]:checked:disabled { + background-color: #979797; +} + +/* —————————————————————— + Light mode – Unchecked + —————————————————————— */ +.storybook-radio--light input[type="radio"] { + border-color: #000000; +} + +.storybook-radio--light input[type="radio"]:hover { + border-color: #005cb8; +} + +.storybook-radio--light input[type="radio"]:focus-visible { + outline: 2px solid #0073e6; +} + +.storybook-radio--light input[type="radio"]:active { + border-color: #00458a; +} + +.storybook-radio--light input[type="radio"]:disabled { + border-color: #979797; +} + +/* —————————————————————— + Dark mode – Base color + —————————————————————— */ +.storybook-radio--dark { + color: #ffffff; +} + +/* —————————————————————— + Dark mode – Checked + —————————————————————— */ +.storybook-radio--dark input[type="radio"]:checked { + border-color: #0073E6; + background-color: #0073E6; +} + +.storybook-radio--dark input[type="radio"]:checked::before { + content: ''; + display: block; + position: absolute; + top: 4px; + left: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #25262B; +} + +.storybook-radio--dark input[type="radio"]:checked:hover { + background-color: #57abff; +} + +.storybook-radio--dark input[type="radio"]:checked:active { + background-color: #8fc7ff; +} + +.storybook-radio--dark input[type="radio"]:checked:disabled { + background-color: #797c8b; +} + +/* —————————————————————— + Dark mode – Unchecked + —————————————————————— */ +.storybook-radio--dark input[type="radio"] { + border-color: #ffffff; + background-color: transparent; +} + +.storybook-radio--dark input[type="radio"]:hover { + border-color: #57abff; +} + +.storybook-radio--dark input[type="radio"]:focus-visible { + outline: 2px solid #1f8fff; +} + +.storybook-radio--dark input[type="radio"]:active { + border-color: #8fc7ff; +} + +.storybook-radio--dark input[type="radio"]:disabled { + border-color: #797c8b; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 454e736..57e6f99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import { Button } from "./components/button/Button"; import { Header } from "./components/header/Header"; import { Page } from "./components/page/Page"; +import { Radio } from "./components/radio/Radio"; +import { Checkbox } from "./components/checkbox/Checkbox"; -export { Button, Header, Page }; \ No newline at end of file +export { Button, Header, Radio, Checkbox, Page }; \ No newline at end of file diff --git a/src/stories/Checkbox.stories.tsx b/src/stories/Checkbox.stories.tsx new file mode 100644 index 0000000..22ff0a8 --- /dev/null +++ b/src/stories/Checkbox.stories.tsx @@ -0,0 +1,129 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Checkbox } from '../components/checkbox/Checkbox'; +import React from 'react'; + +const DARK_BACKGROUND_COLOR = '#25262B'; + +const meta: Meta = { + title: 'Example/Checkbox', + component: Checkbox, + tags: ['autodocs'], + argTypes: { + onChange: { action: 'changed' }, + }, + decorators: [ + (Story, { args }) => ( +
+ +
+ ) + ], +}; + +export default meta; + +type Story = StoryObj; + +export const EnabledUnselectedLight: Story = { + args: { + checked: false, + disabled: false, + mode: 'light', + label: 'Label', + }, +}; + +export const EnabledSelectedLight: Story = { + args: { + checked: true, + disabled: false, + mode: 'light', + label: 'Label', + }, +}; + +export const DisabledUnselectedLight: Story = { + args: { + checked: false, + disabled: true, + mode: 'light', + label: 'Label', + }, +}; + +export const DisabledSelectedLight: Story = { + args: { + checked: true, + disabled: true, + mode: 'light', + label: 'Label', + }, +}; + +export const EnabledUnselectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + checked: false, + disabled: false, + mode: 'dark', + label: 'Label', + }, +}; + +export const EnabledSelectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + checked: true, + disabled: false, + mode: 'dark', + label: 'Label', + }, +}; + +export const DisabledUnselectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + checked: false, + disabled: true, + mode: 'dark', + label: 'Label', + }, +}; + +export const DisabledSelectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + checked: true, + disabled: true, + mode: 'dark', + label: 'Label', + }, +}; \ No newline at end of file diff --git a/src/stories/Radio.stories.tsx b/src/stories/Radio.stories.tsx new file mode 100644 index 0000000..40dc38d --- /dev/null +++ b/src/stories/Radio.stories.tsx @@ -0,0 +1,143 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Radio } from '../components/radio/Radio'; +import React from 'react'; + +const DARK_BACKGROUND_COLOR = '#25262B'; + +const meta: Meta = { + title: 'Example/Radio', + component: Radio, + tags: ['autodocs'], + argTypes: { + onChange: { action: 'changed' }, + }, + args: { + name: 'example', + value: 'example', + }, +}; + +export default meta; +type Story = StoryObj; + +export const MultipleRadioButtons: Story = { + args: { + disabled: false, + mode: 'light', + label: 'Label', + defaultChecked: false, + }, + decorators: [ + (Story, { args }) => ( +
+
+
+ +
+ ) + ], +}; + +export const EnabledUnselectedLight: Story = { + args: { + defaultChecked: false, + disabled: false, + mode: 'light', + label: 'Label', + }, +}; + +export const EnabledSelectedLight: Story = { + args: { + defaultChecked: true, + disabled: false, + mode: 'light', + label: 'Label', + }, +}; + +export const DisabledUnselectedLight: Story = { + args: { + defaultChecked: false, + disabled: true, + mode: 'light', + label: 'Label', + }, +}; + +export const DisabledSelectedLight: Story = { + args: { + defaultChecked: true, + disabled: true, + mode: 'light', + label: 'Label', + }, +}; + +export const EnabledUnselectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + defaultChecked: false, + disabled: false, + mode: 'dark', + label: 'Label', + }, +}; + +export const EnabledSelectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + defaultChecked: true, + disabled: false, + mode: 'dark', + label: 'Label', + }, +}; + +export const DisabledUnselectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + defaultChecked: false, + disabled: true, + mode: 'dark', + label: 'Label', + }, +}; + +export const DisabledSelectedDark: Story = { + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: DARK_BACKGROUND_COLOR } + ] + }, + }, + args: { + defaultChecked: true, + disabled: true, + mode: 'dark', + label: 'Label', + }, +};