dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

commit ec1d571896e8139b4bb36df90b37af20087f06d7
parent 118b53393e60f709c6cbe21762c621029bde8083
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Tue, 17 Jan 2023 13:51:48 +0900

feat: 로그인 폼 추가

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
Acomponents/elements/Button.module.css | 1+
Acomponents/elements/Button.module.css.map | 2++
Acomponents/elements/Button.module.scss | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Acomponents/elements/Button.tsx | 24++++++++++++++++++++++++
Mcomponents/elements/Logo.module.css.map | 4++--
Acomponents/elements/Title.module.css | 1+
Acomponents/elements/Title.module.css.map | 2++
Acomponents/elements/Title.module.scss | 44++++++++++++++++++++++++++++++++++++++++++++
Acomponents/elements/Title.tsx | 22++++++++++++++++++++++
Acomponents/form/Field.module.css | 1+
Acomponents/form/Field.module.css.map | 2++
Acomponents/form/Field.module.scss | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acomponents/form/Field.tsx | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acomponents/form/Fields.module.css | 1+
Acomponents/form/Fields.module.css.map | 2++
Acomponents/form/Fields.module.scss | 12++++++++++++
Acomponents/form/Fields.tsx | 14++++++++++++++
Mcomponents/layout/Container.module.css | 2+-
Mcomponents/layout/Container.module.css.map | 4++--
Mcomponents/layout/Header.tsx | 3++-
Acomponents/layout/Hero.module.css | 1+
Acomponents/layout/Hero.module.css.map | 2++
Acomponents/layout/Hero.module.scss | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acomponents/layout/Hero.tsx | 32++++++++++++++++++++++++++++++++
Mcomponents/layout/Navbar.module.css.map | 4++--
Acomponents/layout/Section.module.css | 1+
Acomponents/layout/Section.module.css.map | 2++
Acomponents/layout/Section.module.scss | 11+++++++++++
Acomponents/layout/Section.tsx | 22++++++++++++++++++++++
Apages/login/index.tsx | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dstyles/Home.module.css | 278-------------------------------------------------------------------------------
Mstyles/core/_colors.scss | 10+++++++++-
Astyles/core/_elevate.scss | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mstyles/core/_responsive.scss | 4++--
Mstyles/globals.css.map | 4++--
35 files changed, 633 insertions(+), 291 deletions(-)

diff --git a/components/elements/Button.module.css b/components/elements/Button.module.css @@ -0,0 +1 @@ +.button{display:inline-flex;min-width:5rem;padding:0 1rem;height:2rem;justify-content:center;align-items:center;border:1px solid rgba(0,0,0,0);border-radius:9999px;vertical-align:top;transition:background-color 100ms,box-shadow 100ms;user-select:none;cursor:pointer;background-color:#e8ebe8;color:#191c1b}.button:hover{background-color:#d8dbd9}.button:active{background-color:#e8ebe8}.button.is-disabled{background-color:#fafdfa;color:rgba(25,28,27,.5)}.button.is-primary{background-color:#e6f1ed;color:#006b5a}.button.is-primary:hover{background-color:#d5e7e2}.button.is-primary:active{background-color:#e6f1ed}.button.is-primary.is-disabled{background-color:#fafdfa;color:rgba(0,107,90,.5)}.button.is-secondary{background-color:#f0eff2;color:#7b4998}.button.is-secondary:hover{background-color:#e7e2eb}.button.is-secondary:active{background-color:#f0eff2}.button.is-secondary.is-disabled{background-color:#fafdfa;color:rgba(123,73,152,.5)}.button.is-tertiary{background-color:#ebf1f0;color:#426278}.button.is-tertiary:hover{background-color:#dee6e7}.button.is-tertiary:active{background-color:#ebf1f0}.button.is-tertiary.is-disabled{background-color:#fafdfa;color:rgba(66,98,120,.5)}.button.is-error{background-color:#fae9e6;color:red}.button.is-error:hover{background-color:#fbd7d5}.button.is-error:active{background-color:#fae9e6}.button.is-error.is-disabled{background-color:#fafdfa;color:rgba(255,0,0,.5)}@media(prefers-color-scheme: dark){.button{background-color:#292c2b;color:#e1e3e0}.button:hover{background-color:#373a39}.button:active{background-color:#292c2b}.button.is-disabled{background-color:#191c1b;color:rgba(225,227,224,.5)}.button.is-primary{background-color:#1b2c28;color:#2cdebf}.button.is-primary:hover{background-color:#1c3934}.button.is-primary:active{background-color:#1b2c28}.button.is-primary.is-disabled{background-color:#191c1b;color:rgba(44,222,191,.5)}.button.is-secondary{background-color:#29282d;color:#e6b4ff}.button.is-secondary:hover{background-color:#38333d}.button.is-secondary:active{background-color:#29282d}.button.is-secondary.is-disabled{background-color:#191c1b;color:rgba(230,180,255,.5)}.button.is-tertiary{background-color:#252a2b;color:#aacbe4}.button.is-tertiary:hover{background-color:#2f3639}.button.is-tertiary:active{background-color:#252a2b}.button.is-tertiary.is-disabled{background-color:#191c1b;color:rgba(170,203,228,.5)}.button.is-error{background-color:#2b2827;color:#ffb4ab}.button.is-error:hover{background-color:#3c3331}.button.is-error:active{background-color:#2b2827}.button.is-error.is-disabled{background-color:#191c1b;color:rgba(255,180,171,.5)}}/*# sourceMappingURL=Button.module.css.map */ diff --git a/components/elements/Button.module.css.map b/components/elements/Button.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["Button.module.scss","../../styles/core/_elevate.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AA4BA,QACE,oBACA,UA1BU,KA2BV,QA1BQ,OA2BR,OA1BO,KA2BP,uBACA,mBACA,+BACA,cA5Bc,OA6Bd,mBACA,mDACA,iBACA,eCYA,yBDxCA,cAEA,cCsCA,yBDlCA,eCkCA,yBD9BA,oBC8BA,yBD5BE,wBAsBE,mBCMJ,yBDxCA,cAEA,yBCsCA,yBDlCA,0BCkCA,yBD9BA,+BC8BA,yBD5BE,wBAsBE,qBCMJ,yBDxCA,cAEA,2BCsCA,yBDlCA,4BCkCA,yBD9BA,iCC8BA,yBD5BE,0BAsBE,oBCMJ,yBDxCA,cAEA,0BCsCA,yBDlCA,2BCkCA,yBD9BA,gCC8BA,yBD5BE,yBAsBE,iBCMJ,yBDxCA,UAEA,uBCsCA,yBDlCA,wBCkCA,yBD9BA,6BC8BA,yBD5BE,uBEwGF,mCFpGF,QCwBE,yBDxCA,cAEA,cCsCA,yBDlCA,eCkCA,yBD9BA,oBC8BA,yBD5BE,2BAsBE,mBCMJ,yBDxCA,cAEA,yBCsCA,yBDlCA,0BCkCA,yBD9BA,+BC8BA,yBD5BE,0BAsBE,qBCMJ,yBDxCA,cAEA,2BCsCA,yBDlCA,4BCkCA,yBD9BA,iCC8BA,yBD5BE,2BAsBE,oBCMJ,yBDxCA,cAEA,0BCsCA,yBDlCA,2BCkCA,yBD9BA,gCC8BA,yBD5BE,2BAsBE,iBCMJ,yBDxCA,cAEA,uBCsCA,yBDlCA,wBCkCA,yBD9BA,6BC8BA,yBD5BE","file":"Button.module.css"} +\ No newline at end of file diff --git a/components/elements/Button.module.scss b/components/elements/Button.module.scss @@ -0,0 +1,52 @@ +@use 'sass:map'; +@use 'core/elevate'; +@use 'core/colors'; + +$min-width: 5rem; +$padding: 0 1rem; +$height: 2rem; +$border-color: 'outline'; +$border-radius: 9999px; + +@mixin _button-color($theme, $color) { + @include elevate.apply-background-color($theme, 1, $color); + color: colors.get($theme, $color); + + &:hover { + @include elevate.apply-background-color($theme, 2, $color); + } + + &:active { + @include elevate.apply-background-color($theme, 1, $color); + } + + &.is-disabled { + @include elevate.apply-background-color($theme, 0, $color); + color: rgba(colors.get($theme, $color), 0.5); + } +} + +.button { + display: inline-flex; + min-width: $min-width; + padding: $padding; + height: $height; + justify-content: center; + align-items: center; + border: 1px solid transparent; + border-radius: $border-radius; + vertical-align: top; + transition: background-color 100ms, box-shadow 100ms; + user-select: none; + cursor: pointer; + + @include colors.apply-themes using ($theme) { + @include _button-color($theme, 'on-surface'); + + @include colors.sets($theme) using ($key, $color, $color-on) { + &.is-#{$key} { + @include _button-color($theme, $color); + } + } + } +} diff --git a/components/elements/Button.tsx b/components/elements/Button.tsx @@ -0,0 +1,24 @@ +import classNames from '@/lib/classnames' +import { HTMLAttributes } from 'react' +import styles from './Button.module.css' + +export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> { + color?: 'primary' | 'secondary' | 'tertiary' | 'error' + disabled?: boolean +} + +export default function Button (props: ButtonProps) { + const { color, disabled, ...restProps } = props + + return ( + <button + {...classNames( + styles.button, + color ? `is-${props.color}` : null, + disabled ? styles['is-disabled'] : null, + )} + disabled={disabled} + {...restProps} + /> + ) +} diff --git a/components/elements/Logo.module.css.map b/components/elements/Logo.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["Logo.module.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AAEA,MAEI,aCoHF,mCDtHF,MAEI","file":"Logo.module.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["Logo.module.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AAEA,MAEI,aC4HF,mCD9HF,MAEI","file":"Logo.module.css"} +\ No newline at end of file diff --git a/components/elements/Title.module.css b/components/elements/Title.module.css @@ -0,0 +1 @@ +.is-display.is-large{line-height:4rem;font-size:3.5625rem;letter-spacing:0rem;font-weight:400}.is-display.is-medium{line-height:3.25rem;font-size:2.8125rem;letter-spacing:0rem;font-weight:400}.is-display.is-small{line-height:2.75rem;font-size:2.25rem;letter-spacing:0rem;font-weight:400}.is-headline.is-large{line-height:2.5rem;font-size:2rem;letter-spacing:0rem;font-weight:400}.is-headline.is-medium{line-height:2.25rem;font-size:1.75rem;letter-spacing:0rem;font-weight:400}.is-headline.is-small{line-height:2rem;font-size:1.5rem;letter-spacing:0rem;font-weight:400}.is-title.is-large{line-height:1.75rem;font-size:1.375rem;letter-spacing:0rem;font-weight:400}.is-title.is-medium{line-height:1.5rem;font-size:1rem;letter-spacing:.009375rem;font-weight:500}.is-title.is-small{line-height:1.25rem;font-size:.875rem;letter-spacing:.0071428571rem;font-weight:500}/*# sourceMappingURL=Title.module.css.map */ diff --git a/components/elements/Title.module.css.map b/components/elements/Title.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["Title.module.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAGE,qBCmHE,iBACA,oBACA,oBACA,gBDlHF,sBC+GE,oBACA,oBACA,oBACA,gBD9GF,qBC2GE,oBACA,kBACA,oBACA,gBDxGF,sBCqGE,mBACA,eACA,oBACA,gBDpGF,uBCiGE,oBACA,kBACA,oBACA,gBDhGF,sBC6FE,iBACA,iBACA,oBACA,gBD1FF,mBCuFE,oBACA,mBACA,oBACA,gBDtFF,oBCmFE,mBACA,eACA,0BACA,gBDlFF,mBC+EE,oBACA,kBACA,8BACA","file":"Title.module.css"} +\ No newline at end of file diff --git a/components/elements/Title.module.scss b/components/elements/Title.module.scss @@ -0,0 +1,44 @@ +@use 'core/typography'; + +.is-display { + &.is-large { + @include typography.apply('display-large') + } + + &.is-medium { + @include typography.apply('display-medium') + } + + &.is-small { + @include typography.apply('display-small') + } +} + +.is-headline { + &.is-large { + @include typography.apply('headline-large') + } + + &.is-medium { + @include typography.apply('headline-medium') + } + + &.is-small { + @include typography.apply('headline-small') + } +} + +.is-title { + &.is-large { + @include typography.apply('title-large') + } + + &.is-medium { + @include typography.apply('title-medium') + } + + &.is-small { + @include typography.apply('title-small') + } +} + diff --git a/components/elements/Title.tsx b/components/elements/Title.tsx @@ -0,0 +1,22 @@ +import classNames from '@/lib/classnames' +import { ReactNode } from 'react' +import styles from './Title.module.css' + +export interface TitleProps { + size?: 'small' | 'medium' | 'large' + kind?: 'title' | 'headline' | 'display' + children: ReactNode +} + +export default function Title (props: TitleProps) { + const { size, kind, children } = props + + return ( + <h1 {...classNames( + styles[`is-${size == null ? 'large' : size}`], + styles[`is-${kind == null ? 'title' : kind}`], + )}> + {children} + </h1> + ) +} diff --git a/components/form/Field.module.css b/components/form/Field.module.css @@ -0,0 +1 @@ +.field{display:flex;position:relative;border:1px solid rgba(0,0,0,0);margin-bottom:1rem;border-radius:4px;background-color:inherit;box-shadow:0 0 0 1px rgba(0,0,0,0);transition:all 200ms;color:#191c1b;border-color:#6f7975}.field:focus-within{border-color:#006b5a;box-shadow:0 0 0 1px #006b5a}.field.is-disabled{color:rgba(25,28,27,.5);border-color:rgba(111,121,117,.5)}@media(prefers-color-scheme: dark){.field{color:#e1e3e0;border-color:#89938f}.field:focus-within{border-color:#2cdebf;box-shadow:0 0 0 1px #2cdebf}.field.is-disabled{color:rgba(225,227,224,.5);border-color:rgba(137,147,143,.5)}}.input{width:100%;height:3.5rem;border:0;padding:0 1rem;color:inherit;background:rgba(0,0,0,0);outline:none}.field.has-icon-left .input{padding-left:3.25rem}.field.has-icon-right .input{padding-right:3.25rem}.label-background{position:absolute;top:-3px;left:.6875rem;height:4px;padding:0 .3125rem;line-height:0;color:rgba(0,0,0,0);background-color:inherit;transform:translateX(-10%) scaleX(0);transition:200ms transform ease-in-out}.field.has-icon-left .label-background{left:.75rem}.field:focus-within .label-background,.field.has-content .label-background{transform:translateX(-10%) scaleX(0.8)}.label{position:absolute;top:50%;left:1rem;pointer-events:none;transform-origin:left top;transform:translateY(-50%);transition:all .2s ease-in-out;z-index:1;color:#3f4946}.field.has-icon-left .label{left:.75rem}.field:focus-within .label,.field.has-content .label{top:0;transform:scale(0.8) translateY(-50%)}.field:focus-within .label{color:#006b5a}.field.is-disabled .label{color:rgba(63,73,70,.5)}@media(prefers-color-scheme: dark){.label{color:#bfc9c4}.field:focus-within .label{color:#2cdebf}.field.is-disabled .label{color:rgba(191,201,196,.5)}}/*# sourceMappingURL=Field.module.css.map */ diff --git a/components/form/Field.module.css.map b/components/form/Field.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["Field.module.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AAUA,OACE,aACA,kBACA,+BACA,mBACA,kBACA,yBACA,mCACA,qBAGE,cACA,qBAEA,oBACE,qBACA,6BAGF,mBACE,wBACA,kCCiGJ,mCDtHF,OAWI,cACA,qBAEA,oBACE,qBACA,6BAGF,mBACE,2BACA,mCAKN,OACE,WACA,OAnCO,OAoCP,SACA,eACA,cACA,yBACA,aAIA,4BACE,aAHwB,QAM1B,6BACE,cAPwB,QAW5B,kBAGE,kBACA,SACA,cACA,WACA,mBACA,cACA,oBACA,yBACA,qCACA,uCAEA,uCACE,KAhEgB,OAmElB,2EAEE,uCAIJ,OACE,kBACA,QACA,KA7EqB,KA8ErB,oBACA,0BACA,2BACA,+BACA,UAaE,cAXF,4BACE,KApFgB,OAuFlB,qDAEE,MACA,sCAMA,2BACE,cAGF,0BACE,wBCoBJ,mCDhDF,OAqBI,cAEA,2BACE,cAGF,0BACE","file":"Field.module.css"} +\ No newline at end of file diff --git a/components/form/Field.module.scss b/components/form/Field.module.scss @@ -0,0 +1,112 @@ +@use 'sass:math'; +@use 'core/colors'; + +$height: 3.5rem; +$icon-size: 1.5rem; +$field-gap: 1rem; +$padding-without-icon: 1rem; +$padding-with-icon: 0.75rem; +$label-focused-scale: 0.8; + +.field { + display: flex; + position: relative; + border: 1px solid transparent; + margin-bottom: 1rem; + border-radius: 4px; + background-color: inherit; + box-shadow: 0 0 0 1px transparent; + transition: all 200ms; + + @include colors.apply-themes using ($theme) { + color: colors.get($theme, 'on-surface'); + border-color: colors.get($theme, 'outline'); + + &:focus-within { + border-color: colors.get($theme, 'primary'); + box-shadow: 0 0 0 1px colors.get($theme, 'primary'); + } + + &.is-disabled { + color: rgba(colors.get($theme, 'on-surface'), 0.5); + border-color: rgba(colors.get($theme, 'outline'), 0.5); + } + } +} + +.input { + width: 100%; + height: $height; + border: 0; + padding: 0 $padding-without-icon; + color: inherit; + background: transparent; + outline: none; + + $input-padding-with-icon: $padding-with-icon + $icon-size + $field-gap; + + .field.has-icon-left & { + padding-left: $input-padding-with-icon; + } + + .field.has-icon-right & { + padding-right: $input-padding-with-icon; + } +} + +.label-background { + $label-background-padding: math.div(0.25rem, $label-focused-scale); + + position: absolute; + top: -3px; + left: $padding-without-icon - $label-background-padding; + height: 4px; + padding: 0 $label-background-padding; + line-height: 0; + color: transparent; + background-color: inherit; + transform: translateX(-10%) scaleX(0); + transition: 200ms transform ease-in-out; + + .field.has-icon-left & { + left: $padding-with-icon; + } + + .field:focus-within &, + .field.has-content & { + transform: translateX(-10%) scaleX($label-focused-scale); + } +} + +.label { + position: absolute; + top: 50%; + left: $padding-without-icon; + pointer-events: none; + transform-origin: left top; + transform: translateY(-50%); + transition: all 0.2s ease-in-out; + z-index: 1; + + .field.has-icon-left & { + left: $padding-with-icon; + } + + .field:focus-within &, + .field.has-content & { + top: 0; + transform: scale($label-focused-scale) translateY(-50%); + } + + @include colors.apply-themes using ($theme) { + color: colors.get($theme, 'on-surface-variant'); + + .field:focus-within & { + color: colors.get($theme, 'primary'); + } + + .field.is-disabled & { + color: rgba(colors.get($theme, 'on-surface-variant'), 0.5); + } + } +} diff --git a/components/form/Field.tsx b/components/form/Field.tsx @@ -0,0 +1,53 @@ +import classNames from '@/lib/classnames' +import { FormEvent, HTMLAttributes, useMemo } from 'react' +import styles from './Field.module.css' + +export interface FieldProps extends HTMLAttributes<HTMLLabelElement> { + type: 'date' | 'datetime-local' | 'email' | 'month' | 'number' + | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'text' + disabled?: boolean + value?: string + + onValueChange?: (value: string) => void +} + +// Field is a wrapper around an input element that provides a label and +// error message. +// Parent of this component is required to have background-color. +export default function Field (props: FieldProps) { + const { + className: originalClassName, + placeholder, + type, + disabled, + value: initialValue, + onValueChange, + ...restProps + } = props + + const handleInput = (e: FormEvent<HTMLInputElement>) => { + onValueChange?.(e.currentTarget.value) + } + + return ( + <label + {...classNames( + originalClassName, + styles['field'], + (props.value?.length ?? 0) > 0 ? styles['has-content'] : null, + disabled === true ? styles['is-disabled'] : null, + )} + {...restProps} + > + <input + className={styles['input']} + type={type} + disabled={disabled} + value={props.value} + onInput={handleInput} + /> + <span className={styles['label']}>{placeholder}</span> + <div className={styles['label-background']}>{placeholder}</div> + </label> + ) +} diff --git a/components/form/Fields.module.css b/components/form/Fields.module.css @@ -0,0 +1 @@ +.fields-wrapper{max-width:20rem;background-color:#fafdfa;color:#191c1b}@media(prefers-color-scheme: dark){.fields-wrapper{background-color:#191c1b;color:#e1e3e0}}/*# sourceMappingURL=Fields.module.css.map */ diff --git a/components/form/Fields.module.css.map b/components/form/Fields.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["Fields.module.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AAIA,gBACE,UAHU,MAMR,yBACA,cCuHF,mCD5HF,gBAII,yBACA","file":"Fields.module.css"} +\ No newline at end of file diff --git a/components/form/Fields.module.scss b/components/form/Fields.module.scss @@ -0,0 +1,12 @@ +@use 'core/colors'; + +$max-width: 20rem; + +.fields-wrapper { + max-width: $max-width; + + @include colors.apply-themes using ($theme) { + background-color: colors.get($theme, 'surface'); + color: colors.get($theme, 'on-surface'); + } +} diff --git a/components/form/Fields.tsx b/components/form/Fields.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from 'react' +import styles from './Fields.module.css' + +export interface FormProps { + children: ReactNode +} + +export default function Fields (props: FormProps) { + return ( + <div className={styles['fields-wrapper']}> + {props.children} + </div> + ) +} diff --git a/components/layout/Container.module.css b/components/layout/Container.module.css @@ -1 +1 @@ -.container{width:100%;padding:0 1rem;margin:0 auto;max-width:100%}@media(min-width: 40rem){.container{max-width:40rem}}@media(min-width: 60rem){.container{max-width:60rem}}.container.is-fluid{max-width:none}/*# sourceMappingURL=Container.module.css.map */ +.container{width:100%;padding:0 1rem;margin:0 auto;max-width:100%}@media(min-width: 30rem){.container{max-width:30rem}}@media(min-width: 50rem){.container{max-width:50rem}}.container.is-fluid{max-width:none}/*# sourceMappingURL=Container.module.css.map */ diff --git a/components/layout/Container.module.css.map b/components/layout/Container.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["Container.module.scss","../styles/core/_responsive.scss"],"names":[],"mappings":"AAMA;EACE;EACA,SAJQ;EAKR;EAGE,WCUO;;AAET;EDlBF;IAMI,WCZY;;;AA4Bd;EDtBF;IAMI,WCXY;;;ADcd;EACE","file":"Container.module.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["Container.module.scss","../../styles/core/_responsive.scss"],"names":[],"mappings":"AAMA,WACE,WACA,QAJQ,OAKR,cAGE,UCUO,KAET,yBDlBF,WAMI,UCZY,OA4Bd,yBDtBF,WAMI,UCXY,ODcd,oBACE","file":"Container.module.css"} +\ No newline at end of file diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx @@ -7,7 +7,8 @@ export default function Header () { return ( <header {...classNames(styles.header)}> <Navbar> - <NavbarItem href={'/'}>Home</NavbarItem> + <NavbarItem href={'/login'}>Login</NavbarItem> + <NavbarItem href={'/register'}>Register</NavbarItem> </Navbar> </header> ) diff --git a/components/layout/Hero.module.css b/components/layout/Hero.module.css @@ -0,0 +1 @@ +.hero{display:flex;flex-direction:column;position:relative;padding:4rem 0;overflow:hidden;justify-content:center;background-color:#fafdfa;color:#191c1b}.hero.is-small{min-height:10rem}.hero.is-medium{min-height:15rem}.hero.is-large{min-height:20rem}@media(min-width: 30rem){.hero.is-small{min-height:15rem}.hero.is-medium{min-height:20rem}.hero.is-large{min-height:25rem}}.hero.is-primary{background-color:#58fbda;color:#00201a}.hero.is-secondary{background-color:#f5d9ff;color:#30004a}.hero.is-tertiary{background-color:#c7e7ff;color:#001e2e}.hero.is-error{background-color:#ffdad6;color:#410002}@media(prefers-color-scheme: dark){.hero{background-color:#191c1b;color:#e1e3e0}.hero.is-primary{background-color:#005143;color:#58fbda}.hero.is-secondary{background-color:#61317e;color:#f5d9ff}.hero.is-tertiary{background-color:#2a4a5f;color:#c7e7ff}.hero.is-error{background-color:#93000a;color:#ffdad6}}.hero .hero-body :last-child{margin-bottom:0}.hero .hero-body a[href]{color:inherit;text-decoration:underline}/*# sourceMappingURL=Hero.module.css.map */ diff --git a/components/layout/Hero.module.css.map b/components/layout/Hero.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["Hero.module.scss","../../styles/core/_responsive.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AAyBA,MACE,aACA,sBACA,kBACA,QArBQ,OAsBR,gBACA,uBASE,yBACA,cAtBA,eACE,iBADF,gBACE,iBADF,eACE,iBCfF,yBDcA,eACE,iBADF,gBACE,iBADF,eACE,kBAwBA,iBACE,iBEmGM,QFlGN,MEmGS,QFrGX,mBACE,iBEmGM,QFlGN,MEmGS,QFrGX,kBACE,iBEmGM,QFlGN,MEmGS,QFrGX,eACE,iBEmGM,QFlGN,MEmGS,QAjBf,mCFvGF,MAeI,yBACA,cAGE,iBACE,iBEmGM,QFlGN,MEmGS,QFrGX,mBACE,iBEmGM,QFlGN,MEmGS,QFrGX,kBACE,iBEmGM,QFlGN,MEmGS,QFrGX,eACE,iBEmGM,QFlGN,MEmGS,SF7Fb,6BACE,gBAGF,yBACE,cACA","file":"Hero.module.css"} +\ No newline at end of file diff --git a/components/layout/Hero.module.scss b/components/layout/Hero.module.scss @@ -0,0 +1,62 @@ +@use 'sass:list'; +@use 'core/colors'; +@use 'core/responsive'; + +$xs-height: 10rem; +$sm-height: 15rem; +$md-height: 20rem; +$lg-height: 25rem; +$padding: 4rem 0; + +$_heights: ( + // key: (desktop-or-tablet, mobile) + small: ($sm-height, $xs-height), + medium: ($md-height, $sm-height), + large: ($lg-height, $md-height), +); + +@mixin _apply-height($n) { + @each $key, $heights in $_heights { + &.is-#{$key} { + min-height: list.nth($heights, $n); + } + } +} + +.hero { + display: flex; + flex-direction: column; + position: relative; + padding: $padding; + overflow: hidden; + justify-content: center; + + @include _apply-height(2); + + @include responsive.tablet(true) using ($max-width) { + @include _apply-height(1); + } + + @include colors.apply-themes using ($theme) { + background-color: colors.get($theme, 'surface'); + color: colors.get($theme, 'on-surface'); + + @include colors.container-sets($theme) using ($key, $color, $on-color) { + &.is-#{$key} { + background-color: $color; + color: $on-color; + } + } + } + + .hero-body { + :last-child { + margin-bottom: 0; + } + + a[href] { + color: inherit; + text-decoration: underline; + } + } +} diff --git a/components/layout/Hero.tsx b/components/layout/Hero.tsx @@ -0,0 +1,32 @@ +import Container from '@/components/layout/Container' +import classNames from '@/lib/classnames' +import { HTMLAttributes } from 'react' +import styles from './Hero.module.css' + +export interface HeroProps extends HTMLAttributes<HTMLDivElement> { + color?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'ie-warn' + | 'primary-container' | 'secondary-container' | 'tertiary-container' | 'error-container' + size?: 'small' | 'medium' | 'large' +} + +export default function Hero(props: HeroProps) { + const { className, color, size, children, ...restProps } = props + + return ( + <div + {...classNames( + className, + styles['hero'], + color ? styles[`is-${color}`] : null, + size ? styles[`is-${size}`] : null + )} + {...restProps} + > + <Container> + <div className={styles['hero-body']}> + {children} + </div> + </Container> + </div> + ) +} diff --git a/components/layout/Navbar.module.css.map b/components/layout/Navbar.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["Navbar.module.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAsBA,QACE,gBACA,WAVA,yBACA,cAEA,oBACE,mCAYE,mBAhBJ,yBACA,cAEA,+BACE,kCAYE,qBAhBJ,yBACA,cAEA,iCACE,kCAYE,oBAhBJ,yBACA,cAEA,gCACE,kCAYE,iBAhBJ,yBACA,cAEA,6BACE,iCCsGF,mCDlGF,QARE,yBACA,cAEA,oBACE,sCAYE,mBAhBJ,yBACA,cAEA,+BACE,qCAYE,qBAhBJ,yBACA,cAEA,iCACE,sCAYE,oBAhBJ,yBACA,cAEA,gCACE,sCAYE,iBAhBJ,yBACA,cAEA,6BACE,uCAkBF,mBACE,aACA,oBACA,WA/BU,OAkCZ,6BAEE,aACA,oBAGF,eACE,cACA,kBEkEA,mBACA,eACA,0BACA,gBFlEA,4BACE,iBAIJ,cACE,OACA,yBACA,mBAEA,0BACE,uBACA,eAIJ,cACE,aACA,mBACA,eACA,YAjEU,OAkEV,cAhEU,OAmEZ,eACE,cAEA,oCACE","file":"Navbar.module.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["Navbar.module.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAqBA,QACE,gBACA,WAVA,yBACA,cAEA,oBACE,mCAYE,mBAhBJ,yBACA,cAEA,+BACE,kCAYE,qBAhBJ,yBACA,cAEA,iCACE,kCAYE,oBAhBJ,yBACA,cAEA,gCACE,kCAYE,iBAhBJ,yBACA,cAEA,6BACE,iCC+GF,mCD3GF,QARE,yBACA,cAEA,oBACE,sCAYE,mBAhBJ,yBACA,cAEA,+BACE,qCAYE,qBAhBJ,yBACA,cAEA,iCACE,sCAYE,oBAhBJ,yBACA,cAEA,gCACE,sCAYE,iBAhBJ,yBACA,cAEA,6BACE,uCAkBF,mBACE,aACA,oBACA,WA/BU,OAkCZ,6BAEE,aACA,oBAGF,eACE,cACA,kBEqEA,mBACA,eACA,0BACA,gBFrEA,4BACE,iBAIJ,cACE,OACA,yBACA,mBAEA,0BACE,uBACA,eAIJ,cACE,aACA,mBACA,eACA,YAjEU,OAkEV,cAhEU,OAmEZ,eACE,cAEA,oCACE","file":"Navbar.module.css"} +\ No newline at end of file diff --git a/components/layout/Section.module.css b/components/layout/Section.module.css @@ -0,0 +1 @@ +.section{margin-bottom:4rem}.section.has-margin-top{margin-top:4rem}/*# sourceMappingURL=Section.module.css.map */ diff --git a/components/layout/Section.module.css.map b/components/layout/Section.module.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["Section.module.scss"],"names":[],"mappings":"AAIA,SACE,cAHe,KAKf,wBACE,WANa","file":"Section.module.css"} +\ No newline at end of file diff --git a/components/layout/Section.module.scss b/components/layout/Section.module.scss @@ -0,0 +1,11 @@ +@use 'core/vars'; + +$section-margin: 4 * vars.$gap; + +.section { + margin-bottom: $section-margin; + + &.has-margin-top { + margin-top: $section-margin; + } +} diff --git a/components/layout/Section.tsx b/components/layout/Section.tsx @@ -0,0 +1,22 @@ +import classNames from '@/lib/classnames' +import { HTMLAttributes } from 'react' +import styles from './Section.module.css' + +export interface SectionProps extends HTMLAttributes<HTMLDivElement> { + hasMarginTop?: boolean +} + +export default function Section (props: SectionProps) { + const { className, hasMarginTop, ...restProps } = props + + return ( + <div + {...classNames( + className, + styles['section'], + hasMarginTop ? styles['has-margin-top'] : null, + )} + {...restProps} + /> + ) +} diff --git a/pages/login/index.tsx b/pages/login/index.tsx @@ -0,0 +1,69 @@ +import Button from '@/components/elements/Button' +import Title from '@/components/elements/Title' +import Field from '@/components/form/Field' +import Fields from '@/components/form/Fields' +import Container from '@/components/layout/Container' +import Hero from '@/components/layout/Hero' +import Section from '@/components/layout/Section' +import { MouseEvent, useState } from 'react' + +export default function LoginPage() { + return ( + <> + <Hero> + <Title kind="headline">Login</Title> + </Hero> + + <Section> + <Container> + <LoginForm /> + </Container> + </Section> + </> + ) +} + +function LoginForm () { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + + const [isLoading, setLoading] = useState(false) + const [error, setError] = useState<Error | null>(null) + + async function handleSubmit (e: MouseEvent<HTMLButtonElement>) { + if (isLoading) { + return + } + + setLoading(true) + + console.log('TODO: Login') + } + + return ( + <Fields> + <Field + type="text" + disabled={isLoading} + placeholder="Email" + value={email} + onValueChange={setEmail} + /> + <Field + type="password" + disabled={isLoading} + placeholder="Password" + value={password} + onValueChange={setPassword} + /> + + <Button + color="primary" + disabled={isLoading} + onClick={handleSubmit} + > + Login + </Button> + </Fields> + ) +} diff --git a/styles/Home.module.css b/styles/Home.module.css @@ -1,278 +0,0 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - min-height: 100vh; -} - -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - width: var(--max-width); - max-width: 100%; -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ''; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo, -.thirteen { - position: relative; -} - -.thirteen { - display: flex; - justify-content: center; - align-items: center; - width: 75px; - height: 75px; - padding: 25px 10px; - margin-left: 16px; - transform: translateZ(0); - border-radius: var(--border-radius); - overflow: hidden; - box-shadow: 0px 2px 8px -1px #0000001a; -} - -.thirteen::before, -.thirteen::after { - content: ''; - position: absolute; - z-index: -1; -} - -/* Conic Gradient Animation */ -.thirteen::before { - animation: 6s rotate linear infinite; - width: 200%; - height: 200%; - background: var(--tile-border); -} - -/* Inner Square */ -.thirteen::after { - inset: 0; - padding: 1px; - border-radius: var(--border-radius); - background: linear-gradient( - to bottom right, - rgba(var(--tile-start-rgb), 1), - rgba(var(--tile-end-rgb), 1) - ); - background-clip: content-box; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .thirteen::before { - animation: none; - } - - .card:hover span { - transform: none; - } -} - -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo, - .thirteen img { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); - } -} diff --git a/styles/core/_colors.scss b/styles/core/_colors.scss @@ -107,8 +107,16 @@ $themes: ( ); @function get($theme, $key) { + @if meta.type-of($key) == 'color' { + @return $key; + } + + @if meta.type-of($key) != 'string' { + @error 'Expected a string or color, but got #{$key}.'; + } + @if not map.has-key($themes, $key) { - @error 'invalid color key: #{$key}' + @error 'Invalid color key: #{$key}' } $colors: map.get($themes, $key); diff --git a/styles/core/_elevate.scss b/styles/core/_elevate.scss @@ -0,0 +1,64 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:meta'; +@use 'sass:color' as sass-color; +@use 'colors'; + +$_elevate-opacity: (0%, 8%, 15%, 21%, 26%, 30%); + +$_key-opacity: 0.2; +$_ambient-opacity: 0.1; +$_shadow-map: ( + 0: ( + key: '0px 0px 0px 0px', + ambient: '0px 0px 0px 0px', + ), + 1: ( + key: '0px 1px 2px 0px', + ambient: '0px 1px 3px 1px', + ), + 2: ( + key: '0px 1px 2px 0px', + ambient: '0px 2px 6px 2px', + ), + 3: ( + key: '0px 1px 3px 0px', + ambient: '0px 4px 8px 3px', + ), + 4: ( + key: '0px 2px 3px 0px', + ambient: '0px 6px 10px 4px', + ), + 5: ( + key: '0px 4px 4px 0px', + ambient: '0px 8px 12px 6px', + ), +); +$_shadow-color: #000000; + +@function _overlay-opacity($level) { + @return list.nth($_elevate-opacity, $level + 1); +} + +@function _background-color($theme, $level, $overlay: 'primary', $base: 'surface') { + $base-color: colors.get($theme, $base); + $overlay-color: colors.get($theme, $overlay); + $overlay-opacity: _overlay-opacity($level); + $mixed-color: sass-color.mix($overlay-color, $base-color, $overlay-opacity); + + @return $mixed-color; +} + +@mixin apply-background-color($theme, $level, $overlay) { + background-color: _background-color($theme, $level, $overlay); +} + +@mixin apply-box-shadow($level) { + $key-spread: map.get($_shadow-map, $level, key); + $ambient-spread: map.get($_shadow-map, $level, ambient); + + $key-color: rgba($_shadow-color, $_key-opacity); + $ambient-color: rgba($_shadow-color, $_ambient-opacity); + + box-shadow: (#{$key-spread} #{$key-color}, #{$ambient-spread} #{$ambient-color}); +} diff --git a/styles/core/_responsive.scss b/styles/core/_responsive.scss @@ -1,5 +1,5 @@ -$screen-mobile: 40rem; -$screen-tablet: 60rem; +$screen-mobile: 30rem; +$screen-tablet: 50rem; @mixin tablet($with-desktop: false) { @if $with-desktop == true { diff --git a/styles/globals.css.map b/styles/globals.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["vendors/_normalize.scss","globals.scss","core/_colors.scss"],"names":[],"mappings":"AAAA,4EAUA,KACI,iBACA,8BAUJ,KACI,SAOJ,KACI,cAQJ,GACI,cACA,eAWJ,GACI,uBACA,SACA,iBAQJ,IACI,gCACA,cAUJ,EACI,+BAQJ,YACI,mBACA,0BACA,iCAOJ,SAEI,mBAQJ,cAGI,gCACA,cAOJ,MACI,cAQJ,QAEI,cACA,cACA,kBACA,wBAGJ,IACI,eAGJ,IACI,WAUJ,IACI,kBAWJ,sCAKI,oBACA,eACA,iBACA,SAQJ,aAEI,iBAQJ,cAEI,oBAOJ,gDAII,0BAOJ,wHAII,kBACA,UAOJ,4GAII,8BAOJ,SACI,2BAUJ,OACI,sBACA,cACA,cACA,eACA,UACA,mBAOJ,SACI,wBAOJ,SACI,cAQJ,6BAEI,sBACA,UAOJ,kFAEI,YAQJ,cACI,6BACA,oBAOJ,yCACI,wBAQJ,6BACI,0BACA,aAUJ,QACI,cAOJ,QACI,kBAUJ,SACI,aAOJ,SACI,aCxVJ,KACE,qCAEE,yBACA,cCiHF,mCDrHF,KAGI,yBACA","file":"globals.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["vendors/_normalize.scss","globals.scss","core/_colors.scss"],"names":[],"mappings":"AAAA,4EAUA,KACI,iBACA,8BAUJ,KACI,SAOJ,KACI,cAQJ,GACI,cACA,eAWJ,GACI,uBACA,SACA,iBAQJ,IACI,gCACA,cAUJ,EACI,+BAQJ,YACI,mBACA,0BACA,iCAOJ,SAEI,mBAQJ,cAGI,gCACA,cAOJ,MACI,cAQJ,QAEI,cACA,cACA,kBACA,wBAGJ,IACI,eAGJ,IACI,WAUJ,IACI,kBAWJ,sCAKI,oBACA,eACA,iBACA,SAQJ,aAEI,iBAQJ,cAEI,oBAOJ,gDAII,0BAOJ,wHAII,kBACA,UAOJ,4GAII,8BAOJ,SACI,2BAUJ,OACI,sBACA,cACA,cACA,eACA,UACA,mBAOJ,SACI,wBAOJ,SACI,cAQJ,6BAEI,sBACA,UAOJ,kFAEI,YAQJ,cACI,6BACA,oBAOJ,yCACI,wBAQJ,6BACI,0BACA,aAUJ,QACI,cAOJ,QACI,kBAUJ,SACI,aAOJ,SACI,aCxVJ,KACE,qCAEE,yBACA,cCyHF,mCD7HF,KAGI,yBACA","file":"globals.css"} +\ No newline at end of file