From 9ac2316ed084b81af078159d66335be6370f98f2 Mon Sep 17 00:00:00 2001
From: Paul <paulmakles@gmail.com>
Date: Fri, 18 Jun 2021 14:20:57 +0100
Subject: [PATCH] Port over more UI elements.

---
 package.json                         |   1 +
 src/app.tsx                          |  16 ++++
 src/components/ui/Checkbox.tsx       |   6 +-
 src/components/ui/ColourSwatches.tsx | 118 +++++++++++++++++++++++++++
 src/components/ui/LineDivider.tsx    |   9 ++
 src/components/ui/Overline.tsx       |  47 +++++++++++
 src/components/ui/Preloader.tsx      |   3 +
 src/components/ui/Radio.tsx          | 108 ++++++++++++++++++++++++
 src/components/ui/Tip.tsx            |  36 ++++++++
 src/types/Preact.ts                  |   3 +
 yarn.lock                            |   8 ++
 11 files changed, 352 insertions(+), 3 deletions(-)
 create mode 100644 src/components/ui/ColourSwatches.tsx
 create mode 100644 src/components/ui/LineDivider.tsx
 create mode 100644 src/components/ui/Overline.tsx
 create mode 100644 src/components/ui/Preloader.tsx
 create mode 100644 src/components/ui/Radio.tsx
 create mode 100644 src/components/ui/Tip.tsx
 create mode 100644 src/types/Preact.ts

diff --git a/package.json b/package.json
index c54f63b..96d4b97 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 9bff1aa..5b9d049 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 732b6ef..bccbefb 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 0000000..4c33340
--- /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 0000000..93fbcdf
--- /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 0000000..4aaef59
--- /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 && <> &middot; </> }
+            { 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 0000000..79cd39c
--- /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 0000000..0ca1828
--- /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 0000000..cc4a1da
--- /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 0000000..1563a6c
--- /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 975b71e..8500914 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"
-- 
GitLab