diff --git a/.gitignore b/.gitignore index 609f3b6..0bd1163 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.direnv /.local __pycache__ +/snikket_web/static/css/*.css diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ca09a10 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +scss_files = $(filter-out snikket_web/scss/_%.scss,$(wildcard snikket_web/scss/*.scss)) +scss_includes = $(filter snikket_web/scss/_%.scss,$(wildcard snikket_web/scss/*.scss)) +generated_css_files = $(patsubst snikket_web/scss/%.scss,snikket_web/static/css/%.css,$(scss_files)) + +PYTHON3 ?= python3 +SCSSC ?= $(PYTHON3) -m scss --load-path snikket_web/scss/ + +build_css: $(generated_css_files) + +$(generated_css_files): snikket_web/static/css/%.css: snikket_web/scss/%.scss $(scss_includes) + $(SCSSC) -o "$@" "$<" + +clean: + rm -f $(generated_css_files) + +.PHONY: build_css clean diff --git a/build-requirements.txt b/build-requirements.txt new file mode 100644 index 0000000..2bb5bdc --- /dev/null +++ b/build-requirements.txt @@ -0,0 +1 @@ +pyscss~=1.3 diff --git a/docs/colours.svg b/docs/colours.svg new file mode 100644 index 0000000..ba8c75b --- /dev/null +++ b/docs/colours.svg @@ -0,0 +1,434 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Grayscale + Success + Accent + Alert + Primary + + diff --git a/snikket_web/__init__.py b/snikket_web/__init__.py index 116b175..f30ee89 100644 --- a/snikket_web/__init__.py +++ b/snikket_web/__init__.py @@ -34,5 +34,14 @@ async def home(): return redirect(url_for('login')) +@app.route("/meta/about.html") +async def about(): + return await render_template("about.html") + + +@app.route("/meta/demo.html") +async def demo(): + return await render_template("demo.html") + from .user import user_bp app.register_blueprint(user_bp) diff --git a/snikket_web/scss/_baseline.scss b/snikket_web/scss/_baseline.scss new file mode 100644 index 0000000..8f6a38b --- /dev/null +++ b/snikket_web/scss/_baseline.scss @@ -0,0 +1,132 @@ +/** + * On the scaling of the headers. I’m a nerd, so here we go. + * + * I tried to determine a good scale a priori. It was clear to me that the + * observed difference between a 48px and 64px font is much smaller than the + * perceived difference between a 8px and 16px font size. + * + * Thus, the perception is *not* linear in the font size. + * + * I set the edge points to 200% and 100% (the h6 would get a bold font face) + * to compensate. + * + * The first attempt to get a visually appealing header size scale was thus to + * generate a logarithmic scale: + * + * numpy.logspace(np.log10(200), 2, 6, base=10) + * + * This leads to the following sizes: + * + * $_h-sizes: [200%, 174.11011266%, 151.57165665%, 131.95079108%, 114.8698355%, 100%]; + * + * This scale has too large differences between the larger font sizes, and too + * small differences between the smaller font sizes. Thus, I tried to invert + * this: + * + * 200 - numpy.logspace(2, np.log10(200), 6, base=10) + 100 + * + * This leads to the following sizes: + * + * $_h-sizes: [200.0%, 185.13016450029647%, 168.0492089227105%, 148.42834334896025%, 125.88988734077518%, 100%]; + * + * While this was better, it still didn’t look quite right yet. The next + * attempt was to go about a square function instead of log. The idea behind + * this is that the font size is essentially one edge of a rectangle, where the + * second edge depends on the first. A square function should thus generate a + * nicely appealing sequence: + * + * Again, we want the large differences to be on the large scales, too: + * + * xs = numpy.linspace(5, 0, 6); 4*xs*xs + 100 + * + * This leads to the following sizes: + * + * $_h-sizes: [200.0%, 164.0%, 136.0%, 116.0%, 104.0%, 100.0%]; + * + * While the first three headings looked nice with that, the others did not. + * Further research has shown me that others use an exponential scale (instead + * of a log scale), but with a rather small base (<1.6). + * + * Instead of taking one of the well-known factors (like golden ratio or major + * second), I opted for choosing a factor which gives me a clean 200%-100% + * range: + * + * numpy.power(math.pow(2, 1/5), numpy.linspace(5, 0, 6)) * 100 + * + * The result (rounded to 8 digits) is: + * + * $_h-sizes: [200.0%, 174.11011266%, 151.57165665%, 131.95079108%, 114.8698355%, 100.0%]; + * + * And... This is the first logspace range. Derp. So why did I discard it in + * the first place? Now that I look at it, it looks amazing. Brains are weird. + */ +$h-sizes: [200.0%, 174.11011266%, 151.57165665%, 131.95079108%, 114.8698355%, 100.0%]; + +/** + * And for mobile devices, we want an even less aggressive scale. Let’s try + * 150%-100%. + */ +$h-small-sizes: [150.0%, 138.31618672%, 127.54245006%, 117.60790225%, 108.44717712%, 100.0%]; +$small-screen-threshold: 40rem; + +html { + font-size: 100%; +} + +body { + font-family: $font-sans; + color: $gray-100; +} + +p { + line-height: 1.5; + margin: 1.5em 0; + font-family: $font-bulk; + color: inherit; +} + +h1, h2, h3, h4, h5, h6 { + /* normalise */ + font-weight: 400; + text-decoration: none; + font-style: normal; + font-family: $font-heading; + color: black; +} + +input, button, label, select, textarea, pre, code { + font-size: 100%; + color: inherit; + line-height: 1.5; +} + +textarea { + font-family: $font-bulk; +} + +option { + padding: 0; + margin: 0; +} + +@for $n from 1 through 6 { + h#{$n} { + font-size: nth($h-sizes, $n); + line-height: 1.5 / (nth($h-sizes, $n) / 100%); + margin: 1.5em / (nth($h-sizes, $n) / 100%) 0; + } +} + +h6 { + font-weight: bold; +} + +@media screen and (max-width: $small-screen-threshold) { + @for $n from 1 through 6 { + h#{$n} { + font-size: nth($h-small-sizes, $n); + line-height: 1.5 / (nth($h-small-sizes, $n) / 100%); + margin: 1.5em / (nth($h-small-sizes, $n) / 100%) 0; + } + } +} diff --git a/snikket_web/scss/_theme.scss b/snikket_web/scss/_theme.scss new file mode 100644 index 0000000..56409ff --- /dev/null +++ b/snikket_web/scss/_theme.scss @@ -0,0 +1,172 @@ +$colours: ( + "gray": [ + #1f1b17, + #3d3833, + #4e4a46, + #706965, + #8f8983, + #b1aca6, + #cac3bd, + #e3e1df, + #f6f5f4 + ], + "primary": [ + #062943, + #0f3d62, + #0e4c76, + #226494, + #418fc7, + #72b2e3, + #9dccf0, + #b5d8f3, + #e4f3fd + ], + "alert": [ + #340e03, + #681f0b, + #883017, + #a33d21, + #c95e40, + #ed947c, + #f2ac99, + #fbc2b3, + #fef1ed + ], + "accent": [ + #302100, + #563600, + #795b00, + #a07501, + #c79b0e, + #f4ce3f, + #feed93, + #fef6c1, + #fffbe8 + ], + "success": [ + #052f03, + #0c4608, + #197713, + #218a1b, + #55c644, + #81e06e, + #abed9c, + #cef6c5, + #ecfbe6 + ] +); + +$gray-100: nth(map-get($colours, "gray"), 1); +$gray-200: nth(map-get($colours, "gray"), 2); +$gray-300: nth(map-get($colours, "gray"), 3); +$gray-400: nth(map-get($colours, "gray"), 4); +$gray-500: nth(map-get($colours, "gray"), 5); +$gray-600: nth(map-get($colours, "gray"), 6); +$gray-700: nth(map-get($colours, "gray"), 7); +$gray-800: nth(map-get($colours, "gray"), 8); +$gray-900: nth(map-get($colours, "gray"), 9); + +$primary-100: nth(map-get($colours, "primary"), 1); +$primary-200: nth(map-get($colours, "primary"), 2); +$primary-300: nth(map-get($colours, "primary"), 3); +$primary-400: nth(map-get($colours, "primary"), 4); +$primary-500: nth(map-get($colours, "primary"), 5); +$primary-600: nth(map-get($colours, "primary"), 6); +$primary-700: nth(map-get($colours, "primary"), 7); +$primary-800: nth(map-get($colours, "primary"), 8); +$primary-900: nth(map-get($colours, "primary"), 9); + +$alert-100: nth(map-get($colours, "alert"), 1); +$alert-200: nth(map-get($colours, "alert"), 2); +$alert-300: nth(map-get($colours, "alert"), 3); +$alert-400: nth(map-get($colours, "alert"), 4); +$alert-500: nth(map-get($colours, "alert"), 5); +$alert-600: nth(map-get($colours, "alert"), 6); +$alert-700: nth(map-get($colours, "alert"), 7); +$alert-800: nth(map-get($colours, "alert"), 8); +$alert-900: nth(map-get($colours, "alert"), 9); + +$accent-100: nth(map-get($colours, "accent"), 1); +$accent-200: nth(map-get($colours, "accent"), 2); +$accent-300: nth(map-get($colours, "accent"), 3); +$accent-400: nth(map-get($colours, "accent"), 4); +$accent-500: nth(map-get($colours, "accent"), 5); +$accent-600: nth(map-get($colours, "accent"), 6); +$accent-700: nth(map-get($colours, "accent"), 7); +$accent-800: nth(map-get($colours, "accent"), 8); +$accent-900: nth(map-get($colours, "accent"), 9); + +$success-100: nth(map-get($colours, "success"), 1); +$success-200: nth(map-get($colours, "success"), 2); +$success-300: nth(map-get($colours, "success"), 3); +$success-400: nth(map-get($colours, "success"), 4); +$success-500: nth(map-get($colours, "success"), 5); +$success-600: nth(map-get($colours, "success"), 6); +$success-700: nth(map-get($colours, "success"), 7); +$success-800: nth(map-get($colours, "success"), 8); +$success-900: nth(map-get($colours, "success"), 9); + +/* +$primary-100: $gray-100; +$primary-200: $gray-200; +$primary-300: $gray-300; +$primary-400: $gray-400; +$primary-500: $gray-500; +$primary-600: $gray-600; +$primary-700: $gray-700; +$primary-800: $gray-800; +$primary-900: $gray-900; + +$alert-100: $gray-100; +$alert-200: $gray-200; +$alert-300: $gray-300; +$alert-400: $gray-400; +$alert-500: $gray-500; +$alert-600: $gray-600; +$alert-700: $gray-700; +$alert-800: $gray-800; +$alert-900: $gray-900; + +$accent-100: $gray-100; +$accent-200: $gray-200; +$accent-300: $gray-300; +$accent-400: $gray-400; +$accent-500: $gray-500; +$accent-600: $gray-600; +$accent-700: $gray-700; +$accent-800: $gray-800; +$accent-900: $gray-900; + +$success-100: $gray-100; +$success-200: $gray-200; +$success-300: $gray-300; +$success-400: $gray-400; +$success-500: $gray-500; +$success-600: $gray-600; +$success-700: $gray-700; +$success-800: $gray-800; +$success-900: $gray-900; +*/ + + +$w-s5: 0.0625rem; +$w-s4: 0.125rem; +$w-s3: 0.25rem; +$w-s2: 0.5rem; +$w-s1: 0.75rem; +$w-0: 1rem; +$w-l1: 1.5rem; +$w-l2: 2rem; +$w-l3: 3rem; +$w-l4: 4rem; +$w-l5: 6rem; +$w-l6: 8rem; +$w-l7: 12rem; + +$font-sans: "Noto Sans", sans-serif; +$font-serif: serif; +$font-monospace: monospace; + +$font-heading: $font-sans; +$font-bulk: $font-sans; +$font-code: $font-monospace; diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss new file mode 100644 index 0000000..508bb8b --- /dev/null +++ b/snikket_web/scss/app.scss @@ -0,0 +1,666 @@ +@import "_theme.scss"; +@import "_baseline.scss"; + +/* coarse layout */ + +body { + margin: 0; + padding: 0; + background-color: $gray-900; +} + +body > main { + padding: $w-l1; +} + +/* top bar */ + +@mixin snikket-logo { + background-image: url('/static/img/snikket-logo.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: $w-s2 0em; + padding-left: 2em; +} + +div#topbar { + background-color: white; + @extend .el-1; + color: $primary-200; + margin: 0; + padding: $w-s1; + display: flex; + align-items: center; + + & > header { + flex: 0 1 auto; + color: black; + font-size: nth($h-sizes, 1); + + @media screen and (max-width: $small-screen-threshold) { + font-size: nth($h-small-sizes, 1); + } + + & > a { + color: inherit; + text-decoration: none; + + span { + @include snikket-logo; + } + } + + & > a:visited, & > a:hover, & > a:focus, & > a:active { + color: inherit; + text-decoration: none; + } + } + + & > div.filler { + flex: 1 1 0px; + } + + & > nav.usermenu { + flex: 0 1 auto; + } +} + +/* main content */ + +main { + max-width: 60rem; + margin-left: auto; + margin-right: auto; +} + +/* standard elevations */ + +.el-1, .box.el-1, div.form.el-1 { + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); +} + +.el-2, .box.el-2, div.form.el-2 { + box-shadow: + 0 3px 6px rgba(0, 0, 0, 0.15), + 0 2px 4px rgba(0, 0, 0, 0.12); +} + +.el-3, .box.el-3, div.form.el-3 { + box-shadow: + 0 10px 20px rgba(0, 0, 0, 0.15), + 0 3px 6px rgba(0, 0, 0, 0.10); +} + +.el-4, .box.el-4, div.form.el-4 { + box-shadow: + 0 15px 25px rgba(0, 0, 0, 0.15), + 0 5px 10px rgba(0, 0, 0, 0.05); +} + +.el-5, .box.el-5, div.form.el-5 { + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.2); +} + + +/* footer */ + +body > footer { + display: block; + background-color: $gray-100; + color: $gray-800; + padding: 0 $w-l1; + + ul { + display: block; + padding: 0; + margin: 0; + list-style-type: none; + } + + li { + display: inline-block; + margin: $w-l1 0; + } + + li:before { + content: '•'; + padding-right: $w-s2; + } + + a, a:visited, a:hover, a:active, a:focus { + color: $gray-900; + font-weight: bold; + text-decoration-line: underline; + text-decoration-color: $gray-500; + text-decoration-width: $w-s4; + text-decoration-thickness: $w-s4; + text-underline-offset: 0; + } + + a:hover { + text-decoration-color: $gray-700; + } +} + +/* form support */ + +@for $n from 1 through 6 { + div.form h#{$n}.form-title { + font-size: 100%; + font-weight: bold; + line-height: 1.5; + margin-bottom: 1.5em; + } +} + +label.required:after { + content: '*'; + color: $alert-400; + padding: $w-s4; +} + +p.form-desc.weak { + color: $gray-300; +} + +$text-entry-inputs: "text" "password"; + +div.f-errbox { + background-color: $alert-800; + border: $w-s4 solid $alert-200; + color: $alert-100; + border-radius: $w-s4; + padding: 0 $w-0; + margin: 1em 0; + + p { + line-height: 1; + margin: 1em 0; + } + + ul { + margin: 1em 0; + padding: 0; + padding-left: $w-0; + } + + li { + } +} + +div.form { + @extend .el-2; + + margin: $w-l1; + padding: $w-l1; + background-color: white; +} + +div.form.layout-expanded { + label { + display: block; + color: $gray-200; + } + + div.f-ebox { + margin-bottom: $w-l1; + } + + div.f-bbox { + text-align: right; + padding: $w-s2 0; + } + + @each $type in $text-entry-inputs { + input[type=$type] { + width: 100%; + border: none; + border-bottom: $w-s4 solid $primary-500; + margin-bottom: -$w-s4; + padding: 0 $w-s3; + } + + input[type=$type].has-error { + border-right: $w-s4 solid $alert-500; + } + + input[type=$type]:hover { + border-bottom-color: $primary-700; + } + + input[type=$type]:focus { + border-bottom-color: $primary-800; + } + } + + input[type="checkbox"], input[type="radio"] { + position: absolute; + z-index: -1; + } + + input[type="checkbox"] + label:before { + background-color: transparent; + color: transparent; + content: "✔"; + display: inline-block; + width: $w-0; + height: $w-0; + border-radius: $w-s4; + border: $w-s4 solid $primary-500; + text-align: center; + font-size: $w-0; + margin-right: $w-s2; + line-height: 1; + } + + input[type="radio"] + label:before { + background-color: transparent; + color: transparent; + content: "✔"; + display: inline-block; + width: $w-0; + height: $w-0; + border-radius: $w-s1; + border: $w-s4 solid $primary-500; + text-align: center; + font-size: $w-0; + margin-right: $w-s2; + line-height: 1; + } + + input[type="checkbox"] + label:hover:before, + input[type="radio"] + label:hover:before { + border-color: $primary-700; + } + + input[type="checkbox"]:focus + label:before, + input[type="radio"]:focus + label:before { + border-color: $primary-800; + } + + input[type="checkbox"]:checked + label:before { + background-color: $primary-500; + color: white; + } + + input[type="radio"]:checked + label:before { + background-color: $primary-500; + box-shadow: inset 0 0 0 $w-s3 white; + } + + input[type="checkbox"] + label, input[type="radio"] + label { + display: block; + } + + div.select-wrap { + display: block; + border-bottom: $w-s4 solid $primary-500; + margin-bottom: -$w-s4; + position: relative; + + & > select { + width: 100%; + background-color: transparent; + border: none; + outline: none; + padding: 0 $w-s3; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + padding-right: $w-l1 + $w-s3; + margin-top: -1px; + margin-bottom: -1px; + } + + &:after { + content: "\25bc"; + position: absolute; + right: 0; + width: $w-l1; + text-align: center; + top: 0; + bottom: 0; + pointer-events: none; + color: $primary-500; + font-size: 80%; + // now we can build a padding which eats the remaining 20% + // em is now 0.8 parent-em + // we want to eat 20% of 1 parent-em -> 0.2 parent-em + // 1 parent-em = 1.25 em + // 0.2 parent-em = 0.25 em + padding-top: 0.25em; + padding-bottom: 0.25em; + } + + &:hover { + border-bottom-color: $primary-700; + + &:after { + color: $primary-700; + } + } + + &:focus-within { + border-bottom-color: $primary-800; + + &:after { + color: $primary-800; + } + } + } + + textarea { + width: 100%; + border: none; + border-bottom: $w-s4 solid $primary-500; + } + + textarea:hover { + border-bottom-color: $primary-700; + } + + textarea:focus { + border-bottom-color: $primary-800; + } +} + + +/* form buttons */ + +button, .button { + margin: 0 $w-s2; + padding: $w-s3 $w-s1; +} + +a.button { + text-decoration: none; + cursor: default; +} + +button.primary, .button.primary { + background: linear-gradient(0deg, $primary-500, $primary-600); + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); + color: $primary-900; + border: none; + /* TODO: fix vertical rhyhtm ... */ + border-radius: $w-s4; + // border: $w-s4 solid transparent; + + &:hover, &:focus { + background: linear-gradient(0deg, $primary-600, $primary-700); + color: white; + } + + &:active { + background: $primary-500; + box-shadow: inset 0px 2px 4px rgba(0, 0, 0, 0.2); + color: white; + } + + &.accent { + background: linear-gradient(0deg, $accent-500, $accent-600); + color: $accent-900; + + &:hover, &:focus { + background: linear-gradient(0deg, $accent-600, $accent-700); + } + + &:active { + background: $accent-500; + } + } + + &.danger { + background: linear-gradient(0deg, $alert-500, $alert-600); + color: $alert-900; + + &:hover, &:focus { + background: linear-gradient(0deg, $alert-600, $alert-700); + } + + &:active { + background: $alert-500; + } + } +} + +button.secondary, .button.secondary { + background: linear-gradient(0deg, $gray-600, $gray-700); + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1); + color: $gray-200; + border: none; + /* TODO: fix vertical rhyhtm ... */ + border-radius: $w-s4; + // border: $w-s4 solid transparent; + + &:hover, &:focus { + background: linear-gradient(0deg, $gray-700, $gray-800); + color: black; + } + + &:active { + background: $gray-600; + box-shadow: inset 0px 2px 4px rgba(0, 0, 0, 0.2); + color: black; + } + + &.accent { + background: linear-gradient(0deg, $accent-600, $accent-700); + color: $accent-200; + + &:hover, &:focus { + background: linear-gradient(0deg, $accent-700, $accent-800); + } + + &:active { + background: $accent-600; + } + } + + &.danger { + background: linear-gradient(0deg, $alert-600, $alert-700); + color: $alert-200; + + &:hover, &:focus { + background: linear-gradient(0deg, $alert-700, $alert-800); + } + + &:active { + background: $alert-600; + } + } +} + +button.tertiary, .button.tertiary { + background-color: transparent; + color: $gray-100; + border: none; + /* TODO: fix vertical rhyhtm ... */ + border-radius: $w-s4; + + &:hover { + background-color: $gray-900; + border-color: $gray-800; + color: black; + } + + &.accent { + &:hover { + background-color: $accent-900; + border-color: $accent-800; + color: black; + } + } + + &.danger { + &:hover { + background-color: $alert-900; + border-color: $alert-800; + color: black; + } + } +} + +button.fullwidth, .button.fullwidth { + display: block; + width: 100%; + margin-left: 0; + margin-right: 0; +} + +/* button, .button { + margin: 0 $w-s2; +} + +button.lv-primary, .button.lv-primary { + background-color: $gray-500; + color: $gray-900; + border-radius: $w-s4; + border: $w-s4 solid $gray-400; + + @each $type, $values in $colours { + &.c-#{$type} { + border-color: nth($values, 4); + background-color: nth($values, 5); + color: nth($values, 9); + } + + &.c-#{$type}:hover { + background-color: nth($values, 4); + } + } +} + +button.lv-secondary, .button.lv-secondary { + background-color: $gray-700; + color: $gray-100; + border-radius: $w-s4; + + @each $type, $values in $colours { + &.c-#{$type} { + background-color: nth($values, 7); + color: nth($values, 1); + } + } +} + +button.lv-tertiary, .button.lv-tertiary { + background-color: inherit; + color: $gray-300; + border-radius: $w-s4; + text-decoration: underline; + + @each $type, $values in $colours { + &.c-#{$type} { + color: nth($values, 3); + } + } +} +*/ + +/* + button.lv-secondary.c-#{$type}, .button.lv-secondary.c-#{$type} { + background-color: nth($values, 7); + color: nth($values, 1); + } + + button.lv-tertiary.c-#{$type}, .button.lv-tertiary.c-#{$type} { + color: nth($values, 3); + text-decoration: underline; + background-color: transparent; + } +}*/ + +/* boxes */ + +.box { + display: block; + /* border-width: $w-s4 / 2; + border-style: solid; */ + padding: $w-l1 /* - $w-s4 / 2; */; + margin: $w-l1; + border-radius: $w-s4; + + border-color: $gray-200; + background-color: white; + color: $gray-100; + + @extend .el-1; + + @each $type, $values in $colours { + &.#{$type} { + border-color: nth($values, 2); + background-color: nth($values, 9); + color: nth($values, 1); + } + } +} + +.box > header { + font-weight: bold; + display: block; + margin-bottom: $w-l1; + line-height: 1.5; + color: black; +} + +.box > p:last-child { + margin-bottom: 0; +} + +.box.slim { + & > header { + display: inline; + margin: 0; + } + + & > header:after { + content: ':'; + } + + & > p { + display: inline; + margin: 0; + } +} + +/* login page specials */ + +body#login { + .form { + font-size: nth($h-sizes, 4); + } + + .form-title { + color: $primary-200; + } + + h1 { + @include snikket-logo; + padding-left: 2em; + background-position: 0 0em; + } +} + +/* linearisation / responsive stuff */ + +@media screen and (max-width: $small-screen-threshold) { + main > .form.layout-expanded { + margin-left: 0; + margin-right: 0; + } + + .form.layout-expanded .box { + margin-left: 0; + margin-right: 0; + } + + .box > ul { + padding-left: $w-0; + } + + +} diff --git a/snikket_web/scss/common.scss b/snikket_web/scss/common.scss new file mode 100644 index 0000000..7644b81 --- /dev/null +++ b/snikket_web/scss/common.scss @@ -0,0 +1,7 @@ +.a11y-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + top: -100px; +} diff --git a/snikket_web/scss/login.scss b/snikket_web/scss/login.scss new file mode 100644 index 0000000..3c6b400 --- /dev/null +++ b/snikket_web/scss/login.scss @@ -0,0 +1,97 @@ +@import "_theme.scss"; +@import "_baseline.scss"; + +body { + margin: 0; + padding: 0; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + display: flex; + background: + url('../img/noise.png') 0 0 / 180px 180px, + $gray-900; + flex-direction: column; + + & > div.login-wrap { + justify-content: center; + align-items: center; + display: flex; + flex: 1 0 auto; + } + + & > footer { + flex: 0 0 auto; + background: + url('../img/noise.png') 0 0 / 180px 180px, + $gray-800; + box-shadow: inset 0 0.1em 0.2em rgba(0, 0, 0, 0.2); + color: $gray-200; + + ul { + display: block; + margin: 1.5em; + padding: 0; + text-align: center; + } + + ul > li { + display: inline-block; + } + + /* ul > li:before { + content: '•'; + padding-right: 0.5em; + } */ + } + + & > div.login-wrap > main { + width: 40rem; + border: 0.2em solid $primary-900; + padding: 1.5em; + margin: 0; + border-radius: 0.2em; + box-shadow: 0 0.5em 1.5em rgba(0, 0, 0, 0.2); + background-color: white; + } + + div.fbox, div.abox { + font-size: 150%; + line-height: 1; + margin: 0 0 1em 0; + } + + div.abox { + margin-bottom: 0; + text-align: right; + } + + input[type="password"], + input[type="text"] { + border: 0; + width: 100%; + border-bottom: 0.1em solid $primary-900; + padding: 0.05em; + + &:focus { + border-bottom-color: $primary-700; + } + } + + button { + border: 0; + background-color: $primary-500; + color: $gray-900; + padding: 0.375em 0.75em; + } + + h1 { + background-image: url('/static/img/snikket-logo.svg'); + background-size: contain; + background-repeat: no-repeat; + background-position: 0.25em 0em; + padding-left: 1.5em; + } +} diff --git a/snikket_web/scss/theme-demo.scss b/snikket_web/scss/theme-demo.scss new file mode 100644 index 0000000..1a60d15 --- /dev/null +++ b/snikket_web/scss/theme-demo.scss @@ -0,0 +1,132 @@ +@import "./app.scss"; + +.accent-100 { background-color: $accent-100; } +.success-100 { background-color: $success-100; } +.gray-100 { background-color: $gray-100; } +.alert-100 { background-color: $alert-100; } +.primary-100 { background-color: $primary-100; } +.accent-200 { background-color: $accent-200; } +.success-200 { background-color: $success-200; } +.gray-200 { background-color: $gray-200; } +.alert-200 { background-color: $alert-200; } +.primary-200 { background-color: $primary-200; } +.accent-300 { background-color: $accent-300; } +.success-300 { background-color: $success-300; } +.gray-300 { background-color: $gray-300; } +.alert-300 { background-color: $alert-300; } +.primary-300 { background-color: $primary-300; } +.accent-400 { background-color: $accent-400; } +.success-400 { background-color: $success-400; } +.gray-400 { background-color: $gray-400; } +.alert-400 { background-color: $alert-400; } +.primary-400 { background-color: $primary-400; } +.accent-500 { background-color: $accent-500; } +.success-500 { background-color: $success-500; } +.gray-500 { background-color: $gray-500; } +.alert-500 { background-color: $alert-500; } +.primary-500 { background-color: $primary-500; } +.accent-600 { background-color: $accent-600; } +.success-600 { background-color: $success-600; } +.gray-600 { background-color: $gray-600; } +.alert-600 { background-color: $alert-600; } +.primary-600 { background-color: $primary-600; } +.accent-700 { background-color: $accent-700; } +.success-700 { background-color: $success-700; } +.gray-700 { background-color: $gray-700; } +.alert-700 { background-color: $alert-700; } +.primary-700 { background-color: $primary-700; } +.accent-800 { background-color: $accent-800; } +.success-800 { background-color: $success-800; } +.gray-800 { background-color: $gray-800; } +.alert-800 { background-color: $alert-800; } +.primary-800 { background-color: $primary-800; } +.accent-900 { background-color: $accent-900; } +.success-900 { background-color: $success-900; } +.gray-900 { background-color: $gray-900; } +.alert-900 { background-color: $alert-900; } +.primary-900 { background-color: $primary-900; } + +div.samplerow { + display: flex; + align-items: center; + background: white; + color: black; +} + +div.samplerow.dark { + background: black; + color: white; +} + +div.samplehead { + flex: 1 0 auto; + padding: 8px; +} + +div.samplebox { + flex: 0 0 auto; + width: 32px; + height: 32px; + // border: 1px solid $gray-900; + border-radius: 3px; + margin: 8px; + box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.5); +} + +div.samplerow.dark div.samplebox { + //border: 1px solid $gray-100; +} + +body { + max-width: 60em; + margin-left: auto; + margin-right: auto; +} + +div.demo-columns { + display: flex; +} + +div.demo-column { + flex: 1 1 1px; + margin: 0 $w-l1; +} + +div.demo-column:first-child { + margin-left: 0; +} + +div.demo-column:last-child { + margin-right: 0; +} + +body { + background: + url('../img/line.png'), + $gray-900; + background-size: 1.5em 1.5em; + background-position: 0em -0.3em; +} + +body:target { + background: $gray-900; +} + +body #enable-lines { + display: none; +} + +body:target #disable-lines { + display: none; +} + +body:target #enable-lines { + display: inline; +} + +@media screen and (max-width: 40rem) { + div.demo-columns, div.demo-column { + display: block; + margin: 0; + } +} diff --git a/snikket_web/static/img/line.png b/snikket_web/static/img/line.png new file mode 100644 index 0000000..a8d232d Binary files /dev/null and b/snikket_web/static/img/line.png differ diff --git a/snikket_web/static/img/noise.png b/snikket_web/static/img/noise.png new file mode 100644 index 0000000..1e62b47 Binary files /dev/null and b/snikket_web/static/img/noise.png differ diff --git a/snikket_web/static/img/snikket-logo.svg b/snikket_web/static/img/snikket-logo.svg new file mode 100644 index 0000000..d9cf2e8 --- /dev/null +++ b/snikket_web/static/img/snikket-logo.svg @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snikket_web/templates/about.html b/snikket_web/templates/about.html new file mode 100644 index 0000000..fbaf626 --- /dev/null +++ b/snikket_web/templates/about.html @@ -0,0 +1,22 @@ + + + + About Snikket + + + +

About Snikket

+

What is Snikket?

+

Snikket is an easy-to-use and easy-to-setup Instant Messaging platform for you and your friends and family.

+

What is the Snikket Web Portal?

+

Behind the Scenes and Licenses

+

Quart Microframework

+
+

h1

+

h2

+

h3

+

h4

+
h5
+
h6
+ + diff --git a/snikket_web/templates/app.html b/snikket_web/templates/app.html new file mode 100644 index 0000000..23ce20f --- /dev/null +++ b/snikket_web/templates/app.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block head_lead %} +Snikket Web Portal +{% endblock %} +{% block style %} + +{{ super() }} +{% endblock %} +{% block body %} +
+
{{ config["SNIKKET_DOMAIN"] }}
+
+ +
+
{% block content %}{% endblock %}
+ +{% endblock %} diff --git a/snikket_web/templates/base.html b/snikket_web/templates/base.html new file mode 100644 index 0000000..755a10b --- /dev/null +++ b/snikket_web/templates/base.html @@ -0,0 +1,12 @@ + + + + + {% block head_lead %}{% endblock %} + + {% block style %} + + {% endblock %} + + {% block body %}{% endblock %} + diff --git a/snikket_web/templates/demo.html b/snikket_web/templates/demo.html new file mode 100644 index 0000000..413628d --- /dev/null +++ b/snikket_web/templates/demo.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} +{% from "library.j2" import box %} +{% set body_id = "no-lines" %} +{% block head_lead %} +Theme Demo – Snikket Web Portal +{% endblock %} +{% block style %} + +{% endblock %} +{% block body %} +

Theme Demo

+

This page is to demonstrate the Snikket Web Portal theme and allow development. You should not see this during normal use.

+

Disable rhythm linesEnable rhythm lines

+

Headings

+

This subsection is responsible for demonstrating the heading sizes, with the relation between the different headings and also the relation between headings and text.

+
+

Sub-heading

+

Repudiandae voluptatem ratione voluptatem facere. Rerum recusandae nemo commodi provident praesentium est dignissimos. Aut provident nisi omnis tempore veritatis voluptatem minus esse. Non nulla consequatur id est doloribus quos voluptates. Quae suscipit fugiat minima.

+

Nested Sub—heading

+

Omnis sit temporibus soluta et inventore. Doloribus velit unde placeat aut necessitatibus distinctio. Sapiente sunt corporis neque ducimus officiis qui. Maxime officia et architecto dolor quos autem expedita. Omnis architecto atque facilis iste dolorem voluptatem consectetur. Cupiditate et officiis accusantium inventore.

+
Deeper Sub-heading
+

Perferendis vitae doloribus praesentium natus non. Itaque hic numquam dolorum non vero et excepturi consequatur. Accusantium doloribus molestias impedit laudantium quis accusantium. Repellendus et assumenda voluptate aut ipsa quod. Dolores rerum accusantium cumque voluptatem rerum iure.

+
Paragraph heading
+

Suscipit quis tempora officiis voluptatem sint. Sed vel perferendis libero similique pariatur. Quo corporis perferendis omnis laboriosam nesciunt. Ut fuga quis deserunt maiores voluptas id fugiat odio. Omnis et facilis officia. Rerum quia quia exercitationem qui quibusdam quia et.

+

Another nested Sub-heading

+

Saepe distinctio illo et illum quia quo. Maxime eveniet voluptate non voluptatibus commodi et. Dicta consequuntur voluptatum sint ab voluptatem tenetur. Ad rem et eveniet ea animi voluptatum laborum.

+
+

The other column of this demo demonstrates the headings right beneath each other.

+

Main Title (h1)

+

Heading (h2)

+

Sub-Heading (h3)

+

Nested Sub-Heading (h4)

+
Deep Sub-Heading (h5)
+
Paragraph Heading (h6)
+
+ +

Palette

+
gray
+
success
+
alert
+
accent
+
primary
+ +
gray
+
success
+
alert
+
accent
+
primary
+ +

Boxes

+

Boxes can be used to draw the attention to the user to a specific thing. In general, they will be used to inform the user about the result of an action or about an action item.

+
+

Full-size boxes

+

Full-size boxes visually separate the header and the following content into separate lines. This allows the use of all and multiple block format elements inside full-size boxes.

+

The following box contains an error message:

+ {% call box("alert", "Password change failed") %} +

You need to provide a new password.

+ {% endcall %} +

The following box contains a success message:

+ {% call box("success", "Bookmark created") %} +

The channel was added to your list.

+ {% endcall %} +

The following box contains a notice:

+ {% call box("accent", "Quota warning") %} +

You have nearly reached your HTTP upload storage quota.

+ {% endcall %} +

The following box contains a hint:

+ {% call box("primary", "Update available") %} +

There is a new version of the Snikket Server available.

+ {% endcall %} +

Finally, the following box has unspecified content:

+ {% call box("", "Something happened") %} +

But we don’t know if it’s good or bad or anything.

+ {% endcall %} +
+

Slim boxes

+

Slim boxes use inline elements only. They only support the <header/> and a single <p/>. Since, on the semantic level, the header and p are still separated, CSS is used to insert a colon for visual reading.

+

The following box contains an error message:

+ {% call box("alert", "Password change failed", slim=True) %} +

You need to provide a new password.

+ {% endcall %} +

The following box contains a success message:

+ {% call box("success", "Bookmark created", slim=True) %} +

The channel was added to your list.

+ {% endcall %} +

The following box contains a notice:

+ {% call box("accent", "Quota warning", slim=True) %} +

You have nearly reached your HTTP upload storage quota.

+ {% endcall %} +

The following box contains a hint:

+ {% call box("primary", "Update available", slim=True) %} +

There is a new version of the Snikket Server available.

+ {% endcall %} +

Finally, the following box has unspecified content:

+ {% call box("", "Something happened", slim=True) %} +

But we don’t know if it’s good or bad or anything.

+ {% endcall %} +
+ +

Elevation levels

+

Demonstrated with boxes:

+ + + + + + +

Forms

+

Standard Forms (Expanded)

+

Forms support a single title (on any h-level to fit semantically), a description text and fields. Fields and their labels are on separate lines by default.

+
+

Random Preferences

+

This form illustrates the various controls supported by the theme, without being semantically useful.

+
+ + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+ + +
+

The following demonstrate the different button styles. First normal buttons:

+
+ + + +
+

Then the accentuated versions:

+
+ + + +
+

And finally, the Danger Zone:

+
+ + + +
+{# +{% for style in ["primary", "secondary", "tertiary"] %} +

The following demonstrate the {{ style }} button styles:

+
+ +
+{% endfor %} +#} +
+ +{% endblock %} diff --git a/snikket_web/templates/library.j2 b/snikket_web/templates/library.j2 new file mode 100644 index 0000000..e2f9d20 --- /dev/null +++ b/snikket_web/templates/library.j2 @@ -0,0 +1,3 @@ +{% macro box(type, title, slim=False, caller=None) %} + +{% endmacro %} diff --git a/snikket_web/templates/login.html b/snikket_web/templates/login.html index e6faa1d..fdff05a 100644 --- a/snikket_web/templates/login.html +++ b/snikket_web/templates/login.html @@ -1,13 +1,31 @@ - - - - Snikket Web Portal - - -
- - - - - - +{% extends "base.html" %} +{% set body_id = "login" %} +{% block head_lead %} +Snikket Web Portal +{% endblock %} +{% block style %} + +{{ super() }} +{% endblock %} +{% block body %} +
+

{{ config["SNIKKET_DOMAIN"] }}

+

Enter your Snikket address and password to manage your account.

+ +
+ + +
+
+ + +
+
+ +
+ +
+ +{% endblock %} diff --git a/snikket_web/templates/user_home.html b/snikket_web/templates/user_home.html index f1bfbbb..13afb55 100644 --- a/snikket_web/templates/user_home.html +++ b/snikket_web/templates/user_home.html @@ -1,14 +1,11 @@ - - - - Snikket Web Portal - - -

Welcome!

-

Welcome home, {{ user_info.username }}.

- - - +{% extends "app.html" %} +{% block content %} +

Welcome!

+

Welcome home, {{ user_info.username }}.

+

Next?

+

What do you want to do today?

⎄ + +{% endblock %} diff --git a/snikket_web/templates/user_logout.html b/snikket_web/templates/user_logout.html index 6dbf915..34363ac 100644 --- a/snikket_web/templates/user_logout.html +++ b/snikket_web/templates/user_logout.html @@ -1,9 +1,15 @@ - - - - - {{ form.csrf_token }} - -
- - +{% extends "app.html" %} +{% block head_lead %} +Snikket Web Portal +{% endblock %} +{% block content %} +
+

Sign out of the Snikket Web Portal

+

Click below to log yourself out of the web portal. This does not affect any other connected devices.

+ {{ form.csrf_token }} +
+ Back + +
+
+{% endblock %} diff --git a/snikket_web/templates/user_passwd.html b/snikket_web/templates/user_passwd.html index e113828..d13e472 100644 --- a/snikket_web/templates/user_passwd.html +++ b/snikket_web/templates/user_passwd.html @@ -1,19 +1,39 @@ - - - -
- {{ form.csrf_token }} - {{ form.current_password }} - {{ form.new_password }} - {{ form.new_password_confirm }} - - -
- - +{% extends "app.html" %} +{% block head_lead %} +Snikket Web Portal +{% endblock %} +{% block content %} +
+

Change your password

+

To change your password, you need to provide the current password as well as the new one. To reduce the chance of typos, we ask for your new password twice.

+ {{ form.csrf_token }} + {% if form.errors %} +
+
Password change failed
+
    + {% for field, errors in form.errors.items() %} + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} +
+
+ {% endif %} +
+ {{ form.current_password.label(class="required") }} + {{ form.current_password(class=("has-error" if form.current_password.name in form.errors else "")) }} +
+
+ {{ form.new_password.label(class="required") }} + {{ form.new_password }} +
+
+ {{ form.new_password_confirm.label(class="required") }} + {{ form.new_password_confirm(class=("has-error" if form.new_password_confirm.name in form.errors else "")) }} +
+
+ Back + +
+
+{% endblock %} diff --git a/snikket_web/user/__init__.py b/snikket_web/user/__init__.py index 1b8922e..cd10b39 100644 --- a/snikket_web/user/__init__.py +++ b/snikket_web/user/__init__.py @@ -10,18 +10,33 @@ from snikket_web.prosodyclient import client user_bp = Blueprint('user', __name__, url_prefix="/user") +@user_bp.context_processor +async def proc(): + return {"user_info": await client.get_user_info()} + + class ChangePasswordForm(FlaskForm): current_password = wtforms.PasswordField( + # TODO(i18n) + "Current password", validators=[wtforms.validators.InputRequired()] ) new_password = wtforms.PasswordField( + # TODO(i18n) + "New password", validators=[wtforms.validators.InputRequired()] ) new_password_confirm = wtforms.PasswordField( + # TODO(i18n) + "Confirm new password", validators=[wtforms.validators.InputRequired(), - wtforms.validators.EqualTo("new_password")] + wtforms.validators.EqualTo( + "new_password", + # TODO(i18n) + "The new passwords must match." + )] )