diff --git a/package.json b/package.json index c54f63bc2a871d9d3ee8a327df5a934244e58a02..96d4b973defc76609d2aa596a0820306a048aac6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "devDependencies": { "@fontsource/open-sans": "^4.4.5", "@preact/preset-vite": "^2.0.0", + "@styled-icons/bootstrap": "^10.34.0", "@styled-icons/feather": "^10.34.0", "@types/styled-components": "^5.1.10", "preact-i18n": "^1.5.0", diff --git a/src/app.tsx b/src/app.tsx index 9bff1aa28887548258707522fd155ee82ae3e347..5b9d0496ad17aef8fd8baee8909f06f62d3d9566 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -6,6 +6,10 @@ import { Banner } from './components/ui/Banner'; import { Checkbox } from './components/ui/Checkbox'; import { ComboBox } from './components/ui/ComboBox'; import { InputBox } from './components/ui/InputBox'; +import { ColourSwatches } from './components/ui/ColourSwatches'; +import { Tip } from './components/ui/Tip'; +import { Radio } from './components/ui/Radio'; +import { Overline } from './components/ui/Overline'; // ! TEMP START let a = {"light":false,"accent":"#FD6671","background":"#191919","foreground":"#F6F6F6","block":"#2D2D2D","message-box":"#363636","mention":"rgba(251, 255, 0, 0.06)","success":"#65E572","warning":"#FAA352","error":"#F06464","hover":"rgba(0, 0, 0, 0.1)","sidebar-active":"#FD6671","scrollbar-thumb":"#CA525A","scrollbar-track":"transparent","primary-background":"#242424","primary-header":"#363636","secondary-background":"#1E1E1E","secondary-foreground":"#C8C8C8","secondary-header":"#2D2D2D","tertiary-background":"#4D4D4D","tertiary-foreground":"#848484","status-online":"#3ABF7E","status-away":"#F39F00","status-busy":"#F84848","status-streaming":"#977EFF","status-invisible":"#A5A5A5"}; @@ -32,6 +36,8 @@ export const UIDemo = styled.div` export function App() { let [checked, setChecked] = useState(false); + let [colour, setColour] = useState('#FD6671'); + let [selected, setSelected] = useState<'a' | 'b' | 'c'>('a'); return ( <> @@ -53,6 +59,16 @@ export function App() { <InputBox placeholder="Contrast input box..." contrast /> <InputBox value="Input box with value" /> <InputBox value="Contrast with value" contrast /> + <ColourSwatches value={colour} onChange={v => setColour(v)} /> + <Tip>I am a tip! I provide valuable information.</Tip> + <Radio checked={selected === 'a'} onSelect={() => setSelected('a')}>First option</Radio> + <Radio checked={selected === 'b'} onSelect={() => setSelected('b')}>Second option</Radio> + <Radio checked={selected === 'c'} onSelect={() => setSelected('c')}>Last option</Radio> + <Overline>Normal overline</Overline> + <Overline type="subtle">Subtle overline</Overline> + <Overline type="error">Error overline</Overline> + <Overline error="with error">Normal overline</Overline> + <Overline type="subtle" error="with error">Subtle overline</Overline> </UIDemo> </> ) diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index 732b6efa84acbae643f30c07deca098f3ff535af..bccbefb12a5d033796790faf38e1a6cfe8c88ea6 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -1,6 +1,6 @@ import { Check } from '@styled-icons/feather'; +import { Children } from "../../types/Preact"; import styled, { css } from 'styled-components'; -import { VNode } from 'preact'; const CheckboxBase = styled.label` gap: 4px; @@ -62,8 +62,8 @@ interface Props { checked: boolean; disabled?: boolean; className?: string; - children: VNode | string; - description?: VNode | string; + children: Children; + description?: Children; onChange: (state: boolean) => void; } diff --git a/src/components/ui/ColourSwatches.tsx b/src/components/ui/ColourSwatches.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4c33340b24c279e13e04acb147439d4c63956ce7 --- /dev/null +++ b/src/components/ui/ColourSwatches.tsx @@ -0,0 +1,118 @@ +import { useRef } from 'preact/hooks'; +import { Check } from '@styled-icons/feather'; +import styled, { css } from 'styled-components'; +import { Pencil } from '@styled-icons/bootstrap'; + +interface Props { + value: string; + onChange: (value: string) => void; +} + +const presets = [ + [ + "#7B68EE", + "#3498DB", + "#1ABC9C", + "#F1C40F", + "#FF7F50", + "#FD6671", + "#E91E63", + "#D468EE" + ], + [ + "#594CAD", + "#206694", + "#11806A", + "#C27C0E", + "#CD5B45", + "#FF424F", + "#AD1457", + "#954AA8" + ] +]; + +const SwatchesBase = styled.div` + gap: 8px; + display: flex; + + input { + opacity: 0; + margin-top: 44px; + position: absolute; + pointer-events: none; + } +`; + +const Swatch = styled.div<{ type: 'small' | 'large', colour: string }>` + flex-shrink: 0; + cursor: pointer; + border-radius: 4px; + background-color: ${ props => props.colour }; + + display: grid; + place-items: center; + + &:hover { + border: 3px solid var(--foreground); + transition: border ease-in-out .07s; + } + + svg { + color: white; + } + + ${ props => props.type === 'small' ? css` + width: 30px; + height: 30px; + + svg { + stroke-width: 2; + } + ` : css` + width: 68px; + height: 68px; + ` } +`; + +const Rows = styled.div` + gap: 8px; + display: flex; + flex-direction: column; + + > div { + gap: 8px; + display: flex; + flex-direction: row; + } +`; + +export function ColourSwatches({ value, onChange }: Props) { + const ref = useRef<HTMLInputElement>(); + + return ( + <SwatchesBase> + <Swatch colour={value} type='large' + onClick={() => ref.current.click()}> + <Pencil size={32} /> + </Swatch> + <input + type="color" + value={value} + ref={ref} + onChange={ev => onChange(ev.currentTarget.value)} + /> + <Rows> + {presets.map(row => ( + <div> + { row.map(swatch => ( + <Swatch colour={swatch} type='small' + onClick={() => onChange(swatch)}> + {swatch === value && <Check size={18} strokeWidth={2} />} + </Swatch> + )) } + </div> + ))} + </Rows> + </SwatchesBase> + ) +} diff --git a/src/components/ui/LineDivider.tsx b/src/components/ui/LineDivider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93fbcdfc73167415fcca567632f8da15dde80157 --- /dev/null +++ b/src/components/ui/LineDivider.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const LineDivider = styled.div` + height: 0px; + opacity: 0.6; + flex-shrink: 0; + margin: 8px 10px; + border-top: 1px solid var(--tertiary-foreground); +`; diff --git a/src/components/ui/Overline.tsx b/src/components/ui/Overline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4aaef59f837f4324c368eea6e7cf357d8f240214 --- /dev/null +++ b/src/components/ui/Overline.tsx @@ -0,0 +1,47 @@ +import styled, { css } from 'styled-components'; +import { Children } from '../../types/Preact'; + +interface Props { + block?: boolean; + error?: Children; + children?: Children; + type?: "default" | "subtle" | "error"; +} + +const OverlineBase = styled.div<Omit<Props, 'children' | 'error'>>` + display: inline; + margin: 0.4em 0; + margin-top: 0.8em; + + font-size: 14px; + font-weight: 600; + color: var(--foreground); + text-transform: uppercase; + + ${ props => props.type === 'subtle' && css` + font-size: 12px; + color: var(--secondary-foreground); + ` } + + ${ props => props.type === 'error' && css` + font-size: 12px; + font-weight: 400; + color: var(--error); + ` } + + ${ props => props.block && css`display: block;` } +`; + +export function Overline(props: Props) { + return ( + <OverlineBase {...props}> + { props.children } + { props.children && props.error && <> · </> } + { props.error && ( + <Overline type="error"> + { props.error } + </Overline> + ) } + </OverlineBase> + ) +} diff --git a/src/components/ui/Preloader.tsx b/src/components/ui/Preloader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..79cd39c59cdbf1a2bd3688be5e68fa28386b4bd4 --- /dev/null +++ b/src/components/ui/Preloader.tsx @@ -0,0 +1,3 @@ +export function Preloader() { + return <span>LOADING</span> +} diff --git a/src/components/ui/Radio.tsx b/src/components/ui/Radio.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ca1828d869d434b43c6c8ea5c81a080ee229baf --- /dev/null +++ b/src/components/ui/Radio.tsx @@ -0,0 +1,108 @@ +import { Children } from "../../types/Preact"; +import styled, { css } from 'styled-components'; +import { CircleFill } from "@styled-icons/bootstrap"; + +interface Props { + children: Children; + description?: Children; + + checked: boolean; + disabled?: boolean; + onSelect: () => void; +} + +interface BaseProps { + selected: boolean +} + +const RadioBase = styled.label<BaseProps>` + gap: 4px; + z-index: 1; + padding: 4px; + display: flex; + cursor: pointer; + align-items: center; + + font-size: 1rem; + font-weight: 600; + user-select: none; + border-radius: 4px; + transition: .2s ease all; + + &:hover { + background: var(--hover); + } + + > input { + display: none; + } + + > div { + margin: 4px; + width: 24px; + height: 24px; + display: grid; + border-radius: 50%; + place-items: center; + background: var(--foreground); + + svg { + color: var(--foreground); + stroke-width: 2; + } + } + + ${ props => props.selected && css` + color: white; + cursor: default; + background: var(--accent); + + > div { + background: white; + } + + > div svg { + color: var(--accent); + } + + &:hover { + background: var(--accent); + } + ` } +`; + +const RadioDescription = styled.span<BaseProps>` + font-size: 0.8em; + font-weight: 400; + color: var(--secondary-foreground); + + ${ props => props.selected && css` + color: white; + ` } +`; + +export function Radio(props: Props) { + return ( + <RadioBase + selected={props.checked} + disabled={props.disabled} + onClick={() => !props.disabled && props.onSelect && props.onSelect()} + > + <div> + <CircleFill size={12} /> + </div> + <input + type="radio" + checked={props.checked} + /> + <span> + <span>{props.children}</span> + {props.description && ( + <RadioDescription selected={props.checked}> + {props.description} + </RadioDescription> + )} + </span> + </RadioBase> + ); +} \ No newline at end of file diff --git a/src/components/ui/Tip.tsx b/src/components/ui/Tip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cc4a1da019b1af2d6fe425f54abe708ebf2d97a0 --- /dev/null +++ b/src/components/ui/Tip.tsx @@ -0,0 +1,36 @@ +import styled from "styled-components"; +import { Info } from "@styled-icons/feather"; +import { Children } from "../../types/Preact"; + +export const TipBase = styled.div` + display: flex; + padding: 12px; + overflow: hidden; + align-items: center; + + font-size: 14px; + border-radius: 7px; + background: var(--primary-header); + border: 2px solid var(--secondary-header); + + a { + cursor: pointer; + &:hover { + text-decoration: underline; + } + } + + svg { + flex-shrink: 0; + margin-right: 10px; + } +`; + +export function Tip(props: { children: Children }) { + return ( + <TipBase> + <Info size={20} strokeWidth={2} /> + <span>{ props.children }</span> + </TipBase> + ) +} diff --git a/src/types/Preact.ts b/src/types/Preact.ts new file mode 100644 index 0000000000000000000000000000000000000000..1563a6c11285612fd835c78ff82ac3d2f1acc482 --- /dev/null +++ b/src/types/Preact.ts @@ -0,0 +1,3 @@ +import { VNode } from 'preact'; + +export type Children = VNode | (VNode | string)[] | string; diff --git a/yarn.lock b/yarn.lock index 975b71e5dd87ffa66180665f812a5b3c12a58aa4..85009143f473799b1ea519f68cf3f97648e23472 100644 --- a/yarn.lock +++ b/yarn.lock @@ -286,6 +286,14 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@styled-icons/bootstrap@^10.34.0": + version "10.34.0" + resolved "https://registry.yarnpkg.com/@styled-icons/bootstrap/-/bootstrap-10.34.0.tgz#d9142e9eb70dc437f7ef62ffc40168e1ae13ab12" + integrity sha512-UpzdVUR7r9BNqEfPrMchJdgMZEg9eXQxLQJUXM0ouvbI5o9j21/y1dGameO4PZtYbutT/dWv5O6y24z5JWzd5w== + dependencies: + "@babel/runtime" "^7.14.0" + "@styled-icons/styled-icon" "^10.6.3" + "@styled-icons/feather@^10.34.0": version "10.34.0" resolved "https://registry.yarnpkg.com/@styled-icons/feather/-/feather-10.34.0.tgz#fdef1b4231e1ff6cfe454da741161f532788177b"