diff --git a/.github/workflows/gh-pages-preview.yml b/.github/workflows/gh-pages-preview.yml new file mode 100644 index 0000000..2f51ef5 --- /dev/null +++ b/.github/workflows/gh-pages-preview.yml @@ -0,0 +1,31 @@ +# .github/workflows/preview.yml +name: Deploy PR previews + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - closed + +concurrency: preview-${{ github.ref }} + +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install and Build + if: github.event.action != 'closed' # You might want to skip the build if the PR has been closed + run: | + npm install + npm run build-storybook + + - name: Deploy preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: ./storybook-static/ + preview-branch: gh-pages \ No newline at end of file diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 7808cb7..1410534 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -11,7 +11,7 @@ on: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: - contents: read + contents: write pages: write id-token: write @@ -23,7 +23,7 @@ concurrency: jobs: # Build job - build: + deploy: runs-on: ubuntu-latest steps: - name: Checkout @@ -34,19 +34,9 @@ jobs: run: | npm install npm run build-storybook - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + - uses: JamesIves/github-pages-deploy-action@v4 with: - path: storybook-static/ - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + folder: ./storybook-static/ + branch: gh-pages + clean-exclude: pr-preview + force: false diff --git a/.gitignore b/.gitignore index 398fa0f..fb302d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules .cache .next storybook-static -dist \ No newline at end of file +dist +package-lock.json \ No newline at end of file diff --git a/src/components/dropdowns/dropdown.css b/src/components/dropdowns/dropdown.css new file mode 100644 index 0000000..53fc802 --- /dev/null +++ b/src/components/dropdowns/dropdown.css @@ -0,0 +1,81 @@ +.dropdown-wrapper { + font-family: sans-serif; + width: 291px; + user-select: none; + position: relative; + overflow: visible; + /* padding: 10px 20px 10px 20px; */ + + font-family: "DM Sans", sans-serif; + } + + /* Button part (closed or open) */ + .dropdown-header { + width: 100%; + padding: 10px 20px; + border-bottom: none; + font-size: 16px; + height: 67px; + border: 1px solid black; + border-radius: 16px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + background-color: white; + } + + .dropdown-wrapper.open .dropdown-header { + border-radius: 16px 16px 0 0; + /* border-bottom-left-radius: 0; + border-bottom-right-radius: 0; */ + } + + /* Arrow */ + .arrow { + width: 24px; + height: 24px; + flex-shrink: 0px; + } + + /* Dropdown menu */ + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + padding: 0px 20px 10px 20px; + width: 100%; + margin: 0; + display: flex; + list-style: none; + flex-direction: column; + gap: 10px; + background-color: white; + border: 1px solid black; + /* border-top: none; */ + border-radius: 0 0 12px 12px; + + z-index: 10; + } + + /* Divider between header and options + .dropdown-menu::before { + content: ''; + display: block; + height: 1px; + background-color: black; + margin-bottom: 8px; + } */ + + /* Each option */ + .dropdown-item { + line-height: 24px; + font-size: 16px; + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + } + + .dropdown-item:hover { + background-color: #f5f5f5; + } \ No newline at end of file diff --git a/src/components/dropdowns/dropdown.tsx b/src/components/dropdowns/dropdown.tsx new file mode 100644 index 0000000..ec62853 --- /dev/null +++ b/src/components/dropdowns/dropdown.tsx @@ -0,0 +1,49 @@ +import React, {useState} from 'react'; +import './dropdown.css'; + +export interface DropdownProps { + theme?: 'light' | 'dark'; + size?: 'desktop' | 'mobile'; + suggestions?: string[]; + onChange?: (value: string) => void; +} + +export const Dropdown: React.FC = ({ + theme = 'light', + size = "desktop", + suggestions = [], + onChange + +}) => { + const [isOpen, setIsOpen] = useState(false); + const [selected, setIsSelected] = useState(null); + // const options = {[...(selected ? [selected] : []), ...suggestions.filter(s => s !== selected)]}; + // const filtered = selected ? suggestions.filter(s => s !== selected) : suggestions; + const options = [...suggestions].sort((a, b) => a.localeCompare(b)); + const toggleDropdown = () => setIsOpen(!isOpen); + const handleSelect = (option: string) => { + setIsSelected(option); + setIsOpen(false); + onChange?.(option); + } + + return ( +
+
+ {selected ?? 'Select an option'} + + + +
+ {isOpen && ( +
    + {options.map((option, i) => ( +
  • handleSelect(option)}> + {option} +
  • + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/inputs-dropdowns/Input.tsx b/src/components/inputs-dropdowns/Input.tsx new file mode 100644 index 0000000..a5e4ab8 --- /dev/null +++ b/src/components/inputs-dropdowns/Input.tsx @@ -0,0 +1,66 @@ +import React, { useState, useRef } from 'react'; +import './inputs.css' + +export interface InputProps extends React.InputHTMLAttributes { + theme?: 'light' | 'dark'; // Define theme prop + variant?: 'primary' | 'error'; + inputSize?: 'desktop' | 'mobile'; + suggestions?: string[]; + onValueChange?: (value: string) => void; + +} + +export const Input: React.FC = ({ + theme = "light", + variant = 'primary', + inputSize = 'desktop', + suggestions = [], + onValueChange, + ...rest +}) => { + const [value, setValue]= useState(''); + const inputRef = useRef(null); + + const bestSuggestion = suggestions.find(s => + s.toLowerCase().startsWith(value.toLowerCase()) && s.toLowerCase() !== value.toLowerCase()) + || ''; + const handleChange = (e: React.ChangeEvent) => { + const newVal = e.target.value; + setValue(newVal); + onValueChange?.(newVal); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.key === 'ArrowRight' || e.key === 'Tab') && bestSuggestion) { + e.preventDefault(); + setValue(bestSuggestion); + onValueChange?.(bestSuggestion); + + } + } + return ( + +
+ + {value && bestSuggestion && ( +
+ {value} + { + setValue(bestSuggestion); + onValueChange?.(bestSuggestion); + inputRef.current?.focus(); // optional: put focus back in input + }} + >{bestSuggestion.slice(value.length)} +
+ )} +
+ + ); +} \ No newline at end of file diff --git a/src/components/inputs-dropdowns/inputs.css b/src/components/inputs-dropdowns/inputs.css new file mode 100644 index 0000000..1228bda --- /dev/null +++ b/src/components/inputs-dropdowns/inputs.css @@ -0,0 +1,94 @@ + +.inline-suggest-wrapper { + position: relative; + display: inline-block; + width: 291px; /* match input width */ + font-family: "DM Sans", sans-serif; + } + +.inline-suggest-wrapper.desktop { + font-size: 1.125rem; /* match .input.desktop */ + line-height: 1.5rem; /* match .input.desktop */ +} +.inline-suggest-wrapper.mobile { + font-size: 1rem; + line-height: 1.3125rem; +} + +.inline-suggestion { + top: 50%; + position: absolute; + transform: translateY(-50%); + pointer-events: none; + left: 0; + box-sizing: border-box; + /* padding: 10px 20px 10px 20px; */ + padding: 0 20px; + height: 100%; + display: flex; + align-items: center; + font-family: inherit; + font-weight: 400; + font-size: inherit; + line-height: inherit; + white-space: pre; +} +.inline-suggestion .hint { + pointer-events: auto; + cursor: pointer; + } +.inline-suggestion span:first-child { + color: transparent; + user-select: none; + } +.inline-suggestion.light { + color: var(--Surface-Elevation-border, #979797); +} +.inline-suggestion.dark { + color: #797C8B; +} +.input { + position: relative; + width: 291px; + border-radius: 16px; + justify-content: space-between; + padding: 10px 20px 10px 20px; + font-family: "DM Sans", sans-serif; + font-weight: 400; + box-sizing: border-box; + letter-spacing: 0; + vertical-align: middle; +} + + +.input.light { + background-color: #FBFBFB; + +} + +.input.dark { + background-color: #2F3037; + color: var(--Surface-Text-color-text-color, #FFFFFF); + +} + +.input.primary { + border: 1px solid var(--Buttons-CTA-Button-Secondary-secondary-text, #000000); +} + +.input.error { + border: 1px solid #E36370; + color: #E36370; +} + +.input.desktop { + font-size: 1.125rem; + line-height: 1.5e; + height: 67px; +} + +.input.mobile { + font-size: 1rem; + line-height: 1.3125rem; + height: 58px; +} \ No newline at end of file diff --git a/src/stories/Dropdown.stories.ts b/src/stories/Dropdown.stories.ts new file mode 100644 index 0000000..6316db8 --- /dev/null +++ b/src/stories/Dropdown.stories.ts @@ -0,0 +1,53 @@ +import { fn } from '@storybook/test'; + +import { Dropdown } from '../components/dropdowns/dropdown'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +export default { + title: 'Example/Dropdown', + component: Dropdown, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + suggestions: { + control: { type: 'object' }, + description: 'Autocomplete suggestions', + defaultValue: ['abacus', 'boy', 'car'], + } + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn(), + suggestions: ['a', 'b', 'c', 'd', 'e'], }, +}; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary = { + args: { + theme: 'light', // Define theme prop + type: 'primary', + size: 'desktop', + }, + +}; + +export const Dark = { + args: { + theme: 'dark', + type: 'primary', + size: 'desktop' + } +} + +export const Danger = { + args: { + theme: 'light', + type: 'danger', + size: 'desktop' + } +} diff --git a/src/stories/Inputs.stories.ts b/src/stories/Inputs.stories.ts new file mode 100644 index 0000000..5801264 --- /dev/null +++ b/src/stories/Inputs.stories.ts @@ -0,0 +1,53 @@ +import { fn } from '@storybook/test'; + +import { Input } from '../components/inputs-dropdowns/Input'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +export default { + title: 'Example/Input', + component: Input, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + suggestions: { + control: { type: 'object' }, + description: 'Autocomplete suggestions', + defaultValue: ['abacus', 'boy', 'car'], + } + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onValueChange: fn(), + suggestions: ['acm', 'ai', 'hack', 'design', 'alex zheng'], }, +}; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary = { + args: { + theme: 'light', // Define theme prop + variant: 'primary', + inputSize: 'desktop', + }, + +}; + +export const Dark = { + args: { + theme: 'dark', + variant: 'primary', + inputSize: 'desktop' + } +} + +export const Danger = { + args: { + theme: 'light', + variant: 'error', + inputSize: 'desktop' + } +}