Add svelte as a view engine to swaf #33
@ -36,6 +36,7 @@
|
|||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
|
"@types/feather-icons": "^4.7.0",
|
||||||
"@types/formidable": "^1.0.31",
|
"@types/formidable": "^1.0.31",
|
||||||
"@types/geoip-lite": "^1.1.31",
|
"@types/geoip-lite": "^1.1.31",
|
||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^26.0.4",
|
||||||
@ -61,6 +62,7 @@
|
|||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
"eslint-plugin-svelte3": "^3.1.2",
|
"eslint-plugin-svelte3": "^3.1.2",
|
||||||
|
"feather-icons": "^4.28.0",
|
||||||
"jest": "^26.1.0",
|
"jest": "^26.1.0",
|
||||||
"jest-resolve": "^26.6.2",
|
"jest-resolve": "^26.6.2",
|
||||||
"jest-ts-webcompat-resolver": "^1.0.0",
|
"jest-ts-webcompat-resolver": "^1.0.0",
|
||||||
|
@ -10,6 +10,7 @@ import MagicLinkAuthMethod from "./auth/magic_link/MagicLinkAuthMethod.js";
|
|||||||
import MagicLinkController from "./auth/magic_link/MagicLinkController.js";
|
import MagicLinkController from "./auth/magic_link/MagicLinkController.js";
|
||||||
import MagicLinkWebSocketListener from "./auth/magic_link/MagicLinkWebSocketListener.js";
|
import MagicLinkWebSocketListener from "./auth/magic_link/MagicLinkWebSocketListener.js";
|
||||||
import MakeMagicLinksSessionNotUniqueMigration from "./auth/magic_link/MakeMagicLinksSessionNotUniqueMigration.js";
|
import MakeMagicLinksSessionNotUniqueMigration from "./auth/magic_link/MakeMagicLinksSessionNotUniqueMigration.js";
|
||||||
|
import AddApprovedFieldToUsersTableMigration from "./auth/migrations/AddApprovedFieldToUsersTableMigration.js";
|
||||||
import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAtToUsersMigration.js";
|
import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAtToUsersMigration.js";
|
||||||
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration.js";
|
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration.js";
|
||||||
import CreateUsersAndUserEmailsTableMigration from "./auth/migrations/CreateUsersAndUserEmailsTableMigration.js";
|
import CreateUsersAndUserEmailsTableMigration from "./auth/migrations/CreateUsersAndUserEmailsTableMigration.js";
|
||||||
@ -64,7 +65,7 @@ export default class TestApp extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getMigrations(): MigrationType<Migration>[] {
|
protected getMigrations(): MigrationType<Migration>[] {
|
||||||
return MIGRATIONS;
|
return [...MIGRATIONS, AddApprovedFieldToUsersTableMigration];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
@ -140,6 +141,14 @@ export default class TestApp extends Application {
|
|||||||
this.get('/tests', (req, res) => {
|
this.get('/tests', (req, res) => {
|
||||||
res.render('tests');
|
res.render('tests');
|
||||||
}, 'tests');
|
}, 'tests');
|
||||||
|
this.get('/design', (req, res) => {
|
||||||
|
req.flash('success', 'Success.');
|
||||||
|
req.flash('info', 'Info.');
|
||||||
|
req.flash('warning', 'Warning.');
|
||||||
|
req.flash('error', 'Error.');
|
||||||
|
req.flash('error-alert', 'Error alert.');
|
||||||
|
res.render('design');
|
||||||
|
}, 'design');
|
||||||
}
|
}
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
81
src/assets/scss/_fonts.scss
Normal file
81
src/assets/scss/_fonts.scss
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5iU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5jU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5tU1E.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cceyI9tScg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8ccezI9tScg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cce9I9s.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5iU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5jU1EQVg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nunito Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5tU1E.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
129
src/assets/scss/_helpers.scss
Normal file
129
src/assets/scss/_helpers.scss
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
@mixin darkMode() {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin surface($shadowStrength: 0) {
|
||||||
|
background-color: var(--surface);
|
||||||
|
color: var(--on-surface);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-on-surface);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-light-on-surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(a) {
|
||||||
|
color: var(--primary-on-surface);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-light-on-surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// States modifiers
|
||||||
|
.primary:not(.bold) {
|
||||||
|
--color: var(--primary-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.info:not(.bold) {
|
||||||
|
--color: var(--info-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.success:not(.bold) {
|
||||||
|
--color: var(--success-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.warning:not(.bold) {
|
||||||
|
--color: var(--warning-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
.error:not(.bold), .danger:not(.bold) {
|
||||||
|
--color: var(--error-on-surface);
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
button {
|
||||||
|
--background-color: var(--surface);
|
||||||
|
}
|
||||||
|
button:hover::after {
|
||||||
|
background-color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(button:hover::after) {
|
||||||
|
background-color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($shadowStrength > 0) {
|
||||||
|
box-shadow: 0 #{$shadowStrength}px #{$shadowStrength}px #00000045;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Responsivity ---
|
||||||
|
@mixin mobile-le {
|
||||||
|
@media (max-width: $mobileThreshold - 1px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin mobile-ge {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin medium-le {
|
||||||
|
@media (max-width: $desktopThreshold - 1px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin medium-ge {
|
||||||
|
@media (min-width: $mobileThreshold) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin large-le {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin large-ge {
|
||||||
|
@media (min-width: $desktopThreshold) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
|
@include medium-ge {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
width: $desktopThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin fake-hide {
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
@ -1 +1,110 @@
|
|||||||
$primary: #ff0080;
|
//
|
||||||
|
// --- Color palette ---
|
||||||
|
//
|
||||||
|
$onLight: #222;
|
||||||
|
$onDark: #eee;
|
||||||
|
|
||||||
|
|
||||||
|
// Primary
|
||||||
|
$primary: #0046af;
|
||||||
|
$primaryLight: lighten($primary, 10%);
|
||||||
|
$onPrimary: $onDark;
|
||||||
|
$primaryOnBackground: $primary;
|
||||||
|
$primaryLightOnBackground: $primaryLight;
|
||||||
|
$primaryOnSurface: $primary;
|
||||||
|
$primaryLightOnSurface: $primaryLight;
|
||||||
|
|
||||||
|
$primaryDarkMode: #0054c9;
|
||||||
|
$primaryLightDarkMode: lighten($primaryDarkMode, 23%);
|
||||||
|
$onPrimaryDarkMode: $onDark;
|
||||||
|
$primaryOnBackgroundDarkMode: lighten($primaryDarkMode, 20%);
|
||||||
|
$primaryLightOnBackgroundDarkMode: $primaryLightDarkMode;
|
||||||
|
$primaryOnSurfaceDarkMode: lighten($primaryDarkMode, 29%);
|
||||||
|
$primaryLightOnSurfaceDarkMode: lighten($primaryOnSurfaceDarkMode, 15%);
|
||||||
|
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
$secondary: #f21170;
|
||||||
|
$onSecondary: $onLight;
|
||||||
|
$secondaryDarkMode: $secondary;
|
||||||
|
$onSecondaryDarkMode: $onSecondary;
|
||||||
|
|
||||||
|
|
||||||
|
// Background
|
||||||
|
$backgroundBase: #eee;
|
||||||
|
$background: mix($backgroundBase, $primary, 98%);
|
||||||
|
$onBackground: $onLight;
|
||||||
|
$backgroundBaseDarkMode: #222;
|
||||||
|
$backgroundDarkMode: mix($backgroundBaseDarkMode, $primaryDarkMode, 98%);
|
||||||
|
$onBackgroundDarkMode: $onDark;
|
||||||
|
|
||||||
|
|
||||||
|
// Surface
|
||||||
|
$surface: lighten($background, 6%);
|
||||||
|
$onSurface: $onLight;
|
||||||
|
$surfaceDarkMode: darken($backgroundDarkMode, 4.5%);
|
||||||
|
$onSurfaceDarkMode: $onDark;
|
||||||
|
|
||||||
|
|
||||||
|
// Input
|
||||||
|
$input: darken($surface, 5%);
|
||||||
|
$onInput: $onLight;
|
||||||
|
$inputDarkMode: darken($surfaceDarkMode, 5%);
|
||||||
|
$onInputDarkMode: $onDark;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// --- Layout ---
|
||||||
|
//
|
||||||
|
$header: $surface;
|
||||||
|
$headerDarkMode: $surfaceDarkMode;
|
||||||
|
$headerContainer: true;
|
||||||
|
$headerHeight: 72px;
|
||||||
|
$footer: transparent;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// --- State palette ---
|
||||||
|
//
|
||||||
|
$info: #4499ff;
|
||||||
|
$onInfo: darken($info, 50%);
|
||||||
|
$infoOnBackground: darken($info, 20%);
|
||||||
|
$infoOnSurface: darken($info, 20%);
|
||||||
|
$infoDarkMode: darken($info, 40%);
|
||||||
|
$onInfoDarkMode: lighten($info, 20%);
|
||||||
|
$infoOnBackgroundDarkMode: $info;
|
||||||
|
$infoOnSurfaceDarkMode: $info;
|
||||||
|
|
||||||
|
$success: #55ff55;
|
||||||
|
$onSuccess: darken($success, 45%);
|
||||||
|
$successOnBackground: darken($success, 45%);
|
||||||
|
$successOnSurface: darken($success, 45%);
|
||||||
|
$successDarkMode: darken($success, 45%);
|
||||||
|
$onSuccessDarkMode: lighten($success, 20%);
|
||||||
|
$successOnBackgroundDarkMode: $success;
|
||||||
|
$successOnSurfaceDarkMode: $success;
|
||||||
|
|
||||||
|
$warning: #ffcc00;
|
||||||
|
$onWarning: darken($warning, 30%);
|
||||||
|
$warningOnBackground: darken($warning, 25%);
|
||||||
|
$warningOnSurface: darken($warning, 25%);
|
||||||
|
$warningDarkMode: darken($warning, 30%);
|
||||||
|
$onWarningDarkMode: lighten($warning, 20%);
|
||||||
|
$warningOnBackgroundDarkMode: $warning;
|
||||||
|
$warningOnSurfaceDarkMode: $warning;
|
||||||
|
|
||||||
|
$error: #ff0000;
|
||||||
|
$onError: darken($error, 40%);
|
||||||
|
$errorOnBackground: darken($error, 10%);
|
||||||
|
$errorOnSurface: darken($error, 10%);
|
||||||
|
$errorDarkMode: darken($error, 30%);
|
||||||
|
$onErrorDarkMode: lighten($error, 20%);
|
||||||
|
$errorOnBackgroundDarkMode: lighten($error, 15%);
|
||||||
|
$errorOnSurfaceDarkMode: lighten($error, 3%);
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// --- Responsivity ---
|
||||||
|
//
|
||||||
|
$mobileThreshold: 632px;
|
||||||
|
$desktopThreshold: 940px;
|
||||||
|
@ -1,9 +1,255 @@
|
|||||||
|
@import "vars";
|
||||||
|
@import "helpers";
|
||||||
|
|
||||||
|
@import "fonts";
|
||||||
@import "../../../node_modules/normalize.css/normalize";
|
@import "../../../node_modules/normalize.css/normalize";
|
||||||
|
|
||||||
|
|
||||||
|
// --- Css variables, dark mode ---
|
||||||
|
:root {
|
||||||
|
// Primary
|
||||||
|
--primary: #{$primary};
|
||||||
|
--primary-light: #{$primaryLight};
|
||||||
|
--on-primary: #{$onPrimary};
|
||||||
|
--primary-on-background: #{$primaryOnBackground};
|
||||||
|
--primary-light-on-background: #{$primaryLightOnBackground};
|
||||||
|
--primary-on-surface: #{$primaryOnSurface};
|
||||||
|
--primary-light-on-surface: #{$primaryLightOnSurface};
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
--secondary: #{$secondary};
|
||||||
|
--on-secondary: #{$onSecondary};
|
||||||
|
|
||||||
|
// Background
|
||||||
|
--background: #{$background};
|
||||||
|
--on-background: #{$onBackground};
|
||||||
|
|
||||||
|
// Surface
|
||||||
|
--surface: #{$surface};
|
||||||
|
--on-surface: #{$onSurface};
|
||||||
|
|
||||||
|
// Input
|
||||||
|
--input: #{$input};
|
||||||
|
--on-input: #{$onInput};
|
||||||
|
|
||||||
|
|
||||||
|
// States
|
||||||
|
--info: #{$info};
|
||||||
|
--success: #{$success};
|
||||||
|
--warning: #{$warning};
|
||||||
|
--error: #{$error};
|
||||||
|
|
||||||
|
// States text
|
||||||
|
--on-info: #{$onInfo};
|
||||||
|
--on-success: #{$onSuccess};
|
||||||
|
--on-warning: #{$onWarning};
|
||||||
|
--on-error: #{$onError};
|
||||||
|
|
||||||
|
// States text on background
|
||||||
|
--info-on-background: #{$infoOnBackground};
|
||||||
|
--success-on-background: #{$successOnBackground};
|
||||||
|
--warning-on-background: #{$warningOnBackground};
|
||||||
|
--error-on-background: #{$errorOnBackground};
|
||||||
|
|
||||||
|
// States text on surface
|
||||||
|
--info-on-surface: #{$infoOnSurface};
|
||||||
|
--success-on-surface: #{$successOnSurface};
|
||||||
|
--warning-on-surface: #{$warningOnSurface};
|
||||||
|
--error-on-surface: #{$errorOnSurface};
|
||||||
|
|
||||||
|
@include darkMode {
|
||||||
|
// Primary
|
||||||
|
--primary: #{$primaryDarkMode};
|
||||||
|
--primary-light: #{$primaryLightDarkMode};
|
||||||
|
--on-primary: #{$onPrimaryDarkMode};
|
||||||
|
--primary-on-background: #{$primaryOnBackgroundDarkMode};
|
||||||
|
--primary-light-on-background: #{$primaryLightOnBackgroundDarkMode};
|
||||||
|
--primary-on-surface: #{$primaryOnSurfaceDarkMode};
|
||||||
|
--primary-light-on-surface: #{$primaryLightOnSurfaceDarkMode};
|
||||||
|
|
||||||
|
// Secondary
|
||||||
|
--secondary: #{$secondaryDarkMode};
|
||||||
|
--on-secondary: #{$onSecondaryDarkMode};
|
||||||
|
|
||||||
|
// Background
|
||||||
|
--background: #{$backgroundDarkMode};
|
||||||
|
--on-background: #{$onBackgroundDarkMode};
|
||||||
|
|
||||||
|
// Surface
|
||||||
|
--surface: #{$surfaceDarkMode};
|
||||||
|
--on-surface: #{$onSurfaceDarkMode};
|
||||||
|
|
||||||
|
// Input
|
||||||
|
--input: #{$inputDarkMode};
|
||||||
|
--on-input: #{$onInputDarkMode};
|
||||||
|
|
||||||
|
|
||||||
|
// States
|
||||||
|
--info: #{$infoDarkMode};
|
||||||
|
--success: #{$successDarkMode};
|
||||||
|
--warning: #{$warningDarkMode};
|
||||||
|
--error: #{$errorDarkMode};
|
||||||
|
|
||||||
|
// States text
|
||||||
|
--on-info: #{$onInfoDarkMode};
|
||||||
|
--on-success: #{$onSuccessDarkMode};
|
||||||
|
--on-warning: #{$onWarningDarkMode};
|
||||||
|
--on-error: #{$onErrorDarkMode};
|
||||||
|
|
||||||
|
// States text on background
|
||||||
|
--info-on-background: #{$infoOnBackgroundDarkMode};
|
||||||
|
--success-on-background: #{$successOnBackgroundDarkMode};
|
||||||
|
--warning-on-background: #{$warningOnBackgroundDarkMode};
|
||||||
|
--error-on-background: #{$errorOnBackgroundDarkMode};
|
||||||
|
|
||||||
|
// States text on surface
|
||||||
|
--info-on-surface: #{$infoOnSurfaceDarkMode};
|
||||||
|
--success-on-surface: #{$successOnSurfaceDarkMode};
|
||||||
|
--warning-on-surface: #{$warningOnSurfaceDarkMode};
|
||||||
|
--error-on-surface: #{$errorOnSurfaceDarkMode};
|
||||||
|
}
|
||||||
|
|
||||||
|
--color: var(--on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible,
|
||||||
|
button:focus-visible,
|
||||||
|
[type="button"]:focus-visible,
|
||||||
|
[type="reset"]:focus-visible,
|
||||||
|
[type="submit"]:focus-visible {
|
||||||
|
outline: 3px solid var(--primary-light);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
color: var(--color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-on-background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-light-on-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.feather.feather-external-link { //todo add js
|
||||||
|
--icon-size: 16px;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: '- ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary, .bold {
|
||||||
|
--color: var(--primary-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-primary);
|
||||||
|
--background-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
--color: var(--info-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-info);
|
||||||
|
--background-color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
--color: var(--success-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-success);
|
||||||
|
--background-color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
--color: var(--warning-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-warning);
|
||||||
|
--background-color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error, .danger {
|
||||||
|
--color: var(--error-on-background);
|
||||||
|
--background-color: var(--background);
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
--color: var(--on-error);
|
||||||
|
--background-color: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button, .button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
|
||||||
|
color: var(--color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: bolder;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.bold {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feather.last {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--on-background);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,26 @@
|
|||||||
@import "vars";
|
@import "vars";
|
||||||
|
@import "helpers";
|
||||||
|
@import "base";
|
||||||
|
@import "panel";
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Nunito Sans", sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--on-background);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
50
src/assets/scss/panel.scss
Normal file
50
src/assets/scss/panel.scss
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@import "vars";
|
||||||
|
@import "helpers";
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
margin: 16px 0 48px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
@include surface;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 16px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .feather:first-child {
|
||||||
|
--icon-size: 24px;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.2;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h1, > h2, > h3, > h4, > h5, > h6 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 16px;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 1px solid var(--on-surface);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/assets/scss/tip.scss
Normal file
46
src/assets/scss/tip.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
@mixin tip {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
|
height: 30px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
line-height: 22px;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--on-surface);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity ease-out 100ms, visibility step-end 150ms;
|
||||||
|
transition-delay: 0ms;
|
||||||
|
background-color: var(--surface);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
text-transform: initial;
|
||||||
|
font-weight: initial;
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
.tip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-out 100ms;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/assets/ts/featherIcons.ts
Normal file
10
src/assets/ts/featherIcons.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import feather from "feather-icons";
|
||||||
|
|
||||||
|
let alreadyReplaced = false;
|
||||||
|
|
||||||
|
export function replaceIcons(once: boolean = true) {
|
||||||
|
if (!once || !alreadyReplaced) {
|
||||||
|
alreadyReplaced = true;
|
||||||
|
feather.replace();
|
||||||
|
}
|
||||||
|
}
|
49
src/assets/views/DesignButtons.svelte
Normal file
49
src/assets/views/DesignButtons.svelte
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "./utils/Icon.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<p style="display: flex; flex-direction: column; align-items: start">
|
||||||
|
<button>Default button</button>
|
||||||
|
<button class="bold">Default bold button</button>
|
||||||
|
<button class="primary">
|
||||||
|
<Icon name="square"/>
|
||||||
|
Primary button
|
||||||
|
</button>
|
||||||
|
<button class="primary bold">
|
||||||
|
<Icon name="square"/>
|
||||||
|
Primary bold button
|
||||||
|
</button>
|
||||||
|
<button class="success">
|
||||||
|
<Icon name="check"/>
|
||||||
|
Success button
|
||||||
|
</button>
|
||||||
|
<button class="success bold">
|
||||||
|
<Icon name="check"/>
|
||||||
|
Success bold button
|
||||||
|
</button>
|
||||||
|
<button class="info">
|
||||||
|
<Icon name="info"/>
|
||||||
|
Info button
|
||||||
|
</button>
|
||||||
|
<button class="info bold">
|
||||||
|
<Icon name="info"/>
|
||||||
|
Info bold button
|
||||||
|
</button>
|
||||||
|
<button class="warning">
|
||||||
|
<Icon name="alert-triangle"/>
|
||||||
|
Warning button
|
||||||
|
</button>
|
||||||
|
<button class="warning bold">
|
||||||
|
<Icon name="alert-triangle"/>
|
||||||
|
Warning bold button
|
||||||
|
</button>
|
||||||
|
<button class="error">
|
||||||
|
<Icon name="x-circle"/>
|
||||||
|
Error button
|
||||||
|
</button>
|
||||||
|
<button class="error bold">
|
||||||
|
<Icon name="x-circle"/>
|
||||||
|
Error bold button
|
||||||
|
</button>
|
||||||
|
</p>
|
@ -3,9 +3,14 @@
|
|||||||
import Message from "./Message.svelte";
|
import Message from "./Message.svelte";
|
||||||
|
|
||||||
export let flashed = $locals.flash();
|
export let flashed = $locals.flash();
|
||||||
console.log('Flashed:', flashed)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.messages :global(.message:not(:last-child)) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
{#if flashed}
|
{#if flashed}
|
||||||
{#each Object.entries(flashed) as [key, bag], i}
|
{#each Object.entries(flashed) as [key, bag], i}
|
||||||
|
@ -1,12 +1,112 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
export let type;
|
export let type;
|
||||||
export let content;
|
export let content;
|
||||||
export let raw = false;
|
export let raw = false;
|
||||||
export let discreet = false;
|
export let discreet = false;
|
||||||
|
export let sticky = false;
|
||||||
|
|
||||||
|
let icon = undefined;
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
icon = 'check';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
icon = 'info';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
icon = 'alert-triangle';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
icon = 'x-circle';
|
||||||
|
break;
|
||||||
|
case 'error-alert':
|
||||||
|
icon = 'alert-circle';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message;
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
message.remove();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="message" class:message-discreet={discreet} data-type="{type}">
|
<style lang="scss">
|
||||||
<i class="icon"></i>
|
@import "../../scss/vars";
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(&-discreet) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.33);
|
||||||
|
|
||||||
|
&[data-type=info], &[data-type=question] {
|
||||||
|
color: var(--on-info);
|
||||||
|
background-color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type=success] {
|
||||||
|
color: var(--on-success);
|
||||||
|
background-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type=warning] {
|
||||||
|
color: var(--on-warning);
|
||||||
|
background-color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-type=error], &[data-type=error-alert] {
|
||||||
|
color: var(--on-error);
|
||||||
|
background-color: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-discreet {
|
||||||
|
color: var(--on-surface);
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
--icon-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="message" class:message-discreet={discreet} data-type="{type}" bind:this={message}>
|
||||||
|
{#if icon}
|
||||||
|
<Icon name={icon}/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<span class="content">
|
<span class="content">
|
||||||
{#if raw}
|
{#if raw}
|
||||||
{@html content}
|
{@html content}
|
||||||
@ -14,4 +114,10 @@
|
|||||||
{content}
|
{content}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{#if !sticky}
|
||||||
|
<button type="button" on:click={hide}>
|
||||||
|
<Icon name="x"/>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
123
src/assets/views/components/NavMenu.svelte
Normal file
123
src/assets/views/components/NavMenu.svelte
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
|
let open = false;
|
||||||
|
let locked = false;
|
||||||
|
|
||||||
|
function stopPropagation(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu() {
|
||||||
|
if (locked) return;
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
if (locked) return;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
locked = true;
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
locked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let nav;
|
||||||
|
onMount(() => {
|
||||||
|
nav.querySelectorAll('ul li > a, ul li > form > button')
|
||||||
|
.forEach(el => {
|
||||||
|
el.addEventListener('focus', () => {
|
||||||
|
openMenu();
|
||||||
|
});
|
||||||
|
el.addEventListener('blur', () => {
|
||||||
|
closeMenu();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/vars";
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
nav {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
@include medium-le {
|
||||||
|
z-index: 1;
|
||||||
|
position: fixed;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
@include surface(3);
|
||||||
|
|
||||||
|
transition: transform ease-out 150ms;
|
||||||
|
|
||||||
|
&:not(.open) {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: #{$headerHeight - 16px};
|
||||||
|
height: #{$headerHeight - 16px};
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--on-surface);
|
||||||
|
|
||||||
|
border-radius: $headerHeight;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 28px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:window on:click={closeMenu}/>
|
||||||
|
|
||||||
|
<button on:click={openMenu} on:click={stopPropagation}
|
||||||
|
on:focus={openMenu} on:blur={closeMenu}
|
||||||
|
tabindex="0" aria-label="Toggle menu">
|
||||||
|
<Icon name="menu"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav class:open on:click={openMenu} on:click={stopPropagation} on:mousedown={lock} bind:this={nav}
|
||||||
|
aria-hidden={open ? 'false' : 'true'}>
|
||||||
|
<ul>
|
||||||
|
<slot/>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import CsrfTokenField from "../utils/CsrfTokenField.svelte";
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
import Form from "../utils/Form.svelte";
|
||||||
|
|
||||||
export let href;
|
export let href;
|
||||||
export let icon;
|
export let icon;
|
||||||
@ -7,17 +8,69 @@
|
|||||||
export let action = false;
|
export let action = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
@include medium-le {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include large-ge {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.07);
|
||||||
|
|
||||||
|
@include darkMode {
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(form) {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:global(button) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
{#if action}
|
{#if action}
|
||||||
<form action={href} method="POST">
|
<Form action={href} submitIcon={icon} submitText={text}/>
|
||||||
<button><i data-feather={icon}></i> <span class="tip">{text}</span></button>
|
|
||||||
|
|
||||||
<CsrfTokenField/>
|
|
||||||
</form>
|
|
||||||
{:else}
|
{:else}
|
||||||
<a href={href}>
|
<a href={href}>
|
||||||
<i data-feather={icon}></i> <span class="tip">{text}</span>
|
<Icon name={icon}/>
|
||||||
</a>
|
<span class="tip">{text}</span></a>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {route} from "../../../common/Routing.js";
|
import {route} from "../../../common/Routing.js";
|
||||||
import {Pagination} from "../../../common/Pagination.js";
|
import {Pagination} from "../../../common/Pagination.js";
|
||||||
|
import Icon from "../utils/Icon.svelte";
|
||||||
|
|
||||||
export let pagination: string;
|
export let pagination: string;
|
||||||
export let routeName: string;
|
export let routeName: string;
|
||||||
export let contextSize: number;
|
export let contextSize: number;
|
||||||
|
|
||||||
|
if (typeof contextSize !== 'number') contextSize = parseInt(contextSize);
|
||||||
|
|
||||||
$: paginationObj = pagination ? Pagination.deserialize(pagination) : null;
|
$: paginationObj = pagination ? Pagination.deserialize(pagination) : null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -14,7 +17,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{#if paginationObj.hasPrevious()}
|
{#if paginationObj.hasPrevious()}
|
||||||
<li><a href={route(routeName, {page: paginationObj.page - 1})}>
|
<li><a href={route(routeName, {page: paginationObj.page - 1})}>
|
||||||
<i data-feather="chevron-left"></i> Previous
|
<Icon name="chevron-left"/> Previous
|
||||||
</a></li>
|
</a></li>
|
||||||
|
|
||||||
{#each paginationObj.previousPages(contextSize) as i}
|
{#each paginationObj.previousPages(contextSize) as i}
|
||||||
@ -38,7 +41,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<li><a href={route(routeName, {page: paginationObj.page + 1})}>
|
<li><a href={route(routeName, {page: paginationObj.page + 1})}>
|
||||||
Next <i data-feather="chevron-right"></i>
|
Next <Icon name="chevron-right"/>
|
||||||
</a></li>
|
</a></li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
119
src/assets/views/design.svelte
Normal file
119
src/assets/views/design.svelte
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<script>
|
||||||
|
import {route} from "../../common/Routing";
|
||||||
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
|
import Message from "./components/Message.svelte";
|
||||||
|
import Form from "./utils/Form.svelte";
|
||||||
|
import Field from "./utils/Field.svelte";
|
||||||
|
import Pagination from "./components/Pagination.svelte";
|
||||||
|
import {Pagination as PaginationData} from "../../common/Pagination.js";
|
||||||
|
import Breadcrumb from "./components/Breadcrumb.svelte";
|
||||||
|
import Icon from "./utils/Icon.svelte";
|
||||||
|
import DesignButtons from "./DesignButtons.svelte";
|
||||||
|
|
||||||
|
const paginationData = new PaginationData(20, 20, 1000).serialize();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseLayout title="Design test">
|
||||||
|
<DesignButtons/>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="git-commit"/>
|
||||||
|
Common elements
|
||||||
|
</h2>
|
||||||
|
<p>Paragraph</p>
|
||||||
|
|
||||||
|
<p>Paragraph, <a href={route('home')}>Link</a></p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Unordered</li>
|
||||||
|
<li>List</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Ordered</li>
|
||||||
|
<li>List</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<DesignButtons/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="eye-off"/>
|
||||||
|
Discreet messages
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Message type="success" content="Discreet success." discreet/>
|
||||||
|
<Message type="info" content="Discreet info." discreet/>
|
||||||
|
<Message type="warning" content="Discreet warning." discreet/>
|
||||||
|
<Message type="error" content="Discreet sticky error." discreet sticky/>
|
||||||
|
<Message type="error-alert" content="Discreet error-alert." discreet/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="file-text"/>
|
||||||
|
Forms
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Form action="javascript: void(0)"
|
||||||
|
submitText="Submit" submitIcon="check"
|
||||||
|
confirm="Are you sure?">
|
||||||
|
|
||||||
|
<Field type="text" name="text" placeholder="Text field" icon="type"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="password" name="password" placeholder="Password field" icon="key"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="duration" name="duration" placeholder="Duration field" icon="clock" extraData={[
|
||||||
|
'h',
|
||||||
|
'm',
|
||||||
|
's',
|
||||||
|
]} hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="select" name="select" placeholder="Select field" icon="list" extraData={[
|
||||||
|
'Option 1',
|
||||||
|
'Option 2',
|
||||||
|
'Option 3',
|
||||||
|
]} hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="textarea" name="textarea" placeholder="Textarea field" icon="file-text"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="checkbox" name="checkbox" placeholder="Checkbox field" icon="check-square"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
|
||||||
|
<Field type="color" name="color" placeholder="Color field" icon="aperture"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
|
||||||
|
<Field type="file" name="file" placeholder="Choose a file" icon="upload"
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
<Field type="file" name="files" placeholder="Choose files" icon="upload" multiple
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
|
||||||
|
<Field type="text" name="disabled_text" placeholder="Disabled text field" icon="type" disabled
|
||||||
|
hint="This is a hint" validation={{message: 'This is a validation error'}}/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="file"/>
|
||||||
|
Pagination
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Pagination routeName="home" contextSize="3" pagination={paginationData}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>
|
||||||
|
<Icon name="map-pin"/>
|
||||||
|
Breadcrumb
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Breadcrumb currentPageTitle="Design test" pages={[
|
||||||
|
{link: route('home'), title: 'Home'},
|
||||||
|
{link: route('home'), title: 'Home again'},
|
||||||
|
{link: route('design'), title: 'self'},
|
||||||
|
]}/>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
|
@ -4,13 +4,15 @@
|
|||||||
import BaseLayout from "./layouts/BaseLayout.svelte";
|
import BaseLayout from "./layouts/BaseLayout.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseLayout title="{$locals.app.name}">
|
<BaseLayout title="{$locals.app.name}" h1={false}>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
<h1>swaf - Svelte Web Application Framework</h1>
|
||||||
<p>Welcome to {$locals.app.name}!</p>
|
<p>Welcome to {$locals.app.name}!</p>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href={route('tests')}>Frontend tests</a></li>
|
<li><a href={route('tests')}>Frontend tests</a></li>
|
||||||
|
<li><a href={route('design')}>Design test</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import {route} from "../../../common/Routing.js";
|
import {route} from "../../../common/Routing.js";
|
||||||
import FlashMessages from "../components/FlashMessages.svelte";
|
import FlashMessages from "../components/FlashMessages.svelte";
|
||||||
import NavMenuItem from "../components/NavMenuItem.svelte";
|
import NavMenuItem from "../components/NavMenuItem.svelte";
|
||||||
|
import NavMenu from "../components/NavMenu.svelte";
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let h1: string = title;
|
export let h1: string = title;
|
||||||
@ -11,13 +12,63 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import "../../scss/vars";
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
header {
|
header {
|
||||||
text-align: center;
|
@if $headerContainer {
|
||||||
|
@include container;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
height: $headerHeight;
|
||||||
|
|
||||||
|
@include medium-le {
|
||||||
|
z-index: 1;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
@include surface(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: initial;
|
||||||
|
height: calc(#{$headerHeight} - 16px);
|
||||||
|
margin-right: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
@include container;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash-messages {
|
||||||
|
@include container;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -41,25 +92,22 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {$locals.app.name}</a>
|
<a href="/" class="logo"><img src="/img/logo.svg" alt="{$locals.app.name} logo"> {$locals.app.name}</a>
|
||||||
<nav>
|
<NavMenu>
|
||||||
<button id="menu-button"><i data-feather="menu"></i></button>
|
{#if $locals.user}
|
||||||
<ul id="main-menu">
|
{#if $locals.user.is_admin}
|
||||||
{#if $locals.user}
|
<NavMenuItem href={route('backend')} icon="settings" text="Backend"/>
|
||||||
{#if $locals.user.is_admin}
|
|
||||||
<NavMenuItem href={route('backend')} icon="settings" text="Backend"/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<NavMenuItem href={route('account')} icon="user" text={$locals.user.name || 'Account'}/>
|
|
||||||
<NavMenuItem href={route('logout')} icon="log-out" text="Logout" action/>
|
|
||||||
{:else}
|
|
||||||
<NavMenuItem href={route('auth')} icon="log-in" text="Log in / Register"/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
|
||||||
</nav>
|
<NavMenuItem href={route('account')} icon="user" text={$locals.user.name || 'Account'}/>
|
||||||
|
<NavMenuItem href={route('logout')} icon="log-out" text="Logout" action/>
|
||||||
|
{:else}
|
||||||
|
<NavMenuItem href={route('auth')} icon="log-in" text="Log in / Register"/>
|
||||||
|
{/if}
|
||||||
|
</NavMenu>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container">
|
<div class="flash-messages">
|
||||||
<FlashMessages/>
|
<FlashMessages/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {locals} from '../../ts/stores.js';
|
import {locals} from '../../ts/stores.js';
|
||||||
|
import {FileSize} from "../../../common/FileSize.js";
|
||||||
import Message from "../components/Message.svelte";
|
import Message from "../components/Message.svelte";
|
||||||
import Icon from "./Icon.svelte";
|
import Icon from "./Icon.svelte";
|
||||||
import {getContext} from "svelte";
|
import {getContext} from "svelte";
|
||||||
@ -12,11 +13,11 @@
|
|||||||
export let hint: string | undefined = undefined;
|
export let hint: string | undefined = undefined;
|
||||||
export let extraData: string[] | undefined = undefined;
|
export let extraData: string[] | undefined = undefined;
|
||||||
export let icon: string | undefined = undefined;
|
export let icon: string | undefined = undefined;
|
||||||
|
export let validation: { message: string, value?: string } | undefined = $locals.validation()?.[name];
|
||||||
|
|
||||||
const formId = getContext('formId');
|
const formId = getContext('formId');
|
||||||
const fieldId = `${formId}-${name}-field`;
|
const fieldId = `${formId}-${name}-field`;
|
||||||
|
|
||||||
const validation = $locals.validation()?.[name];
|
|
||||||
const previousFormData = $locals.previousFormData() || [];
|
const previousFormData = $locals.previousFormData() || [];
|
||||||
|
|
||||||
value = type !== 'hidden' && previousFormData[name] || value || validation?.value || '';
|
value = type !== 'hidden' && previousFormData[name] || value || validation?.value || '';
|
||||||
@ -38,63 +39,384 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(e) {
|
function focusInput() {
|
||||||
|
if (input) {
|
||||||
|
if (['file', 'checkbox', 'color'].indexOf(input.type) >= 0) {
|
||||||
|
input.click();
|
||||||
|
} else {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.querySelector('input')?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput() {
|
||||||
// in here, you can switch on type and implement
|
// in here, you can switch on type and implement
|
||||||
// whatever behaviour you need
|
// whatever behaviour you need
|
||||||
value = type.match(/^(number|range)$/)
|
value = type.match(/^(number|range)$/)
|
||||||
? +e.target.value
|
? +this.value
|
||||||
: e.target.value;
|
: this.value;
|
||||||
|
|
||||||
|
if (this.type === 'file') {
|
||||||
|
handleFileInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input: HTMLInputElement;
|
||||||
|
|
||||||
|
function chooseFile() {
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
let files: FileList | undefined;
|
||||||
|
|
||||||
|
function handleFileInput() {
|
||||||
|
files = input.files;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../scss/helpers";
|
||||||
|
|
||||||
|
.form-field:not(.hidden) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 16px auto;
|
||||||
|
|
||||||
|
.control {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
color: var(--on-input);
|
||||||
|
background-color: var(--input);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
> :global(.feather.icon) {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin: 18px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
left: 68px;
|
||||||
|
top: 22px;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
transition-property: top, font-size;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&, * {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
z-index: 1;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.empty), select ~, [type="file"] ~, [type="color"] ~, :focus ~ {
|
||||||
|
.sections label {
|
||||||
|
top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
.form-display,
|
||||||
|
.textarea-growing-wrapper,
|
||||||
|
.textarea-growing-wrapper:after {
|
||||||
|
display: block;
|
||||||
|
margin-left: -60px;
|
||||||
|
padding: 32px 8px 8px 68px;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
position: relative;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
||||||
|
&::-ms-expand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + :global(.feather) {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
right: 8px;
|
||||||
|
top: 30px;
|
||||||
|
|
||||||
|
transition: transform 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Temporary
|
||||||
|
&:focus + :global(.feather) {
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-growing-wrapper {
|
||||||
|
display: grid;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: revert;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: attr(data-value) " ";
|
||||||
|
color: red;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: auto;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after, textarea {
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
margin-left: revert;
|
||||||
|
min-height: 100px;
|
||||||
|
height: revert;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=color] {
|
||||||
|
height: calc(32px + 8px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checkbox, &.color, &.file {
|
||||||
|
input, label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checkbox {
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: auto 8px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
& ~ .sections {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: static;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.file {
|
||||||
|
input {
|
||||||
|
@include fake-hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: revert !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
color: var(--on-primary);
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
:global(.feather) {
|
||||||
|
--icon-size: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: start;
|
||||||
|
margin: 16px auto;
|
||||||
|
|
||||||
|
.time-input {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
label {
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> :not(.form-field) {
|
||||||
|
padding: 32px 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ {
|
||||||
|
.error, .hint {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field, fieldset + {
|
||||||
|
.error, .hint {
|
||||||
|
padding: 2px 2px 2px 4px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
{#if type === 'hidden'}
|
{#if type === 'hidden'}
|
||||||
{#if validation}
|
{#if validation}
|
||||||
<Message type="error" content={validation.message}/>
|
<Message type="error" content={validation.message}/>
|
||||||
{/if}
|
{/if}
|
||||||
<input type="hidden" name={name} value={value}>
|
<input type="hidden" name={name} value={value}>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="form-field" class:inline={type === 'checkbox'}>
|
<div class="form-field"
|
||||||
<div class="control">
|
class:checkbox={type === 'checkbox'}
|
||||||
|
class:color={type === 'color'}
|
||||||
|
class:file={type === 'file'}
|
||||||
|
class:empty={value === ''}
|
||||||
|
class:disabled={Object.keys($$restProps).indexOf('disabled') >= 0}>
|
||||||
|
<div class="control" on:click={focusInput}>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon name={icon}/>
|
<Icon name={icon}/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if type === 'duration'}
|
{#if type === 'duration'}
|
||||||
<div class="input-group">
|
<fieldset>
|
||||||
|
<legend>{placeholder}</legend>
|
||||||
{#each extraData as f}
|
{#each extraData as f}
|
||||||
<div class="time-input">
|
<div class="time-input">
|
||||||
<input type="number" name="{name}[{f}]" id="{fieldId}-{f}"
|
<input type="number" name="{name}[{f}]" id="{fieldId}-{f}"
|
||||||
value={durationValue(f)}
|
value={durationValue(f)}
|
||||||
min="0" max={(f === 's' || f === 'm') && '60' || undefined}
|
min="0" max={(f === 's' || f === 'm') && '60' || undefined}
|
||||||
{...$$restProps}>
|
{...$$restProps} on:click={e => e.stopPropagation()}>
|
||||||
<label for="{fieldId}-{f}">{{ f }}</label>
|
<label for="{fieldId}-{f}" on:click={e => e.stopPropagation()}>{f}</label>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</fieldset>
|
||||||
{:else if type === 'select'}
|
{:else if type === 'select'}
|
||||||
<select name={name} id={fieldId} {...$$restProps} on:input={handleInput}>
|
<select name={name} id={fieldId} {...$$restProps} bind:this={input} on:input={handleInput}>
|
||||||
{#each extraData as option}
|
{#each extraData as option}
|
||||||
<option value={(option.display === undefined || option.value !== undefined) && (option.value || option)}
|
<option value={(option.display === undefined || option.value !== undefined) && (option.value || option)}
|
||||||
selected={value === (option.value || option)}>{option.display || option}</option>
|
selected={value === (option.value || option)}>{option.display || option}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<i data-feather="chevron-down"></i>
|
<Icon name="chevron-down"/>
|
||||||
{:else if type === 'textarea'}
|
{:else if type === 'textarea'}
|
||||||
<textarea {name} id={fieldId} bind:value={value} {...$$restProps}></textarea>
|
<div class="textarea-growing-wrapper" data-value={value}>
|
||||||
|
<textarea {name} id={fieldId} {value} {...$$restProps} bind:this={input}
|
||||||
|
on:input={handleInput}></textarea>
|
||||||
|
</div>
|
||||||
{:else if type === 'checkbox'}
|
{:else if type === 'checkbox'}
|
||||||
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps}>
|
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps} bind:this={input}>
|
||||||
{:else}
|
{:else}
|
||||||
<input {type} {name} id={fieldId} {value} {...$$restProps} on:input={handleInput}>
|
<input {type} {name} id={fieldId} {value} {...$$restProps} bind:this={input} on:input={handleInput}
|
||||||
|
tabindex={type === 'file' ? '-1' : undefined}>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<label for="{fieldId}{type === 'duration' && '-' + extraData[0] || ''}">{@html placeholder || ''}<slot/></label>
|
<div class="sections">
|
||||||
|
{#if type !== 'duration'}
|
||||||
|
<label for={fieldId}>{@html placeholder || ''}
|
||||||
|
<slot/>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if type === 'file'}
|
||||||
|
{#if files}
|
||||||
|
<div class="files">
|
||||||
|
{#each files as file}
|
||||||
|
<div class="file">
|
||||||
|
<div class="name" title="Type: {file.type}">
|
||||||
|
<Icon name="file"/> {file.name}
|
||||||
|
</div>
|
||||||
|
<div class="size" title="{file.size} bytes">
|
||||||
|
{FileSize.humanizeFileSize(file.size, true)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<button type="button" on:click={chooseFile}>Browse...</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if validation}
|
{#if validation}
|
||||||
<div class="error"><i data-feather="x-circle"></i> {validation.message}</div>
|
<div class="error">
|
||||||
|
<Icon name="alert-circle"/> {validation.message}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hint}
|
{#if hint}
|
||||||
<div class="hint"><i data-feather="info"></i> {hint}</div>
|
<div class="hint">
|
||||||
|
<Icon name="info"/> {hint}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,11 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {replaceIcons} from "../../ts/featherIcons.js";
|
||||||
|
import {afterUpdate, onMount} from "svelte";
|
||||||
|
|
||||||
export let name: string;
|
export let name: string;
|
||||||
|
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
replaceIcons();
|
||||||
|
});
|
||||||
|
afterUpdate(() => {
|
||||||
|
replaceIcons(false);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// ---
|
||||||
|
// --- Feather
|
||||||
|
// ---
|
||||||
|
:global(.feather) {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: var(--icon-size);
|
||||||
|
height: var(--icon-size);
|
||||||
|
|
||||||
|
--icon-size: 16px;
|
||||||
|
font-size: var(--icon-size);
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: square;
|
||||||
|
stroke-linejoin: miter;
|
||||||
|
fill: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
h1 > &, h2 > &, h3 > & {
|
||||||
|
--icon-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
{#if name}
|
{#if name}
|
||||||
{#if name.startsWith('fa') }
|
{#if name.startsWith('fa') }
|
||||||
<i class="{name} feather icon"></i>
|
<i class="{name} feather icon" aria-hidden="true" {...$$restProps}></i>
|
||||||
{:else}
|
{:else}
|
||||||
<i data-feather="{name}" class="icon"></i>
|
<i data-feather="{name}" class="feather icon" aria-hidden="true" {...$$restProps}></i>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -34,6 +34,8 @@ export class Pagination {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public previousPages(contextSize: number): number[] {
|
public previousPages(contextSize: number): number[] {
|
||||||
|
if (typeof contextSize !== 'number') throw new Error('contextSize must be a number');
|
||||||
|
|
||||||
const pages = [];
|
const pages = [];
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
@ -60,6 +62,8 @@ export class Pagination {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public nextPages(contextSize: number): number[] {
|
public nextPages(contextSize: number): number[] {
|
||||||
|
if (typeof contextSize !== 'number') throw new Error('contextSize must be a number');
|
||||||
|
|
||||||
const pages = [];
|
const pages = [];
|
||||||
|
|
||||||
let i = this.page + 1;
|
let i = this.page + 1;
|
||||||
|
@ -4,7 +4,6 @@ import {Router} from "express";
|
|||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
|
|
||||||
import ApplicationComponent from "../ApplicationComponent.js";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import ViewEngine from "../frontend/ViewEngine.js";
|
|
||||||
import SecurityError from "../SecurityError.js";
|
import SecurityError from "../SecurityError.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||||
import RedisComponent from "./RedisComponent.js";
|
import RedisComponent from "./RedisComponent.js";
|
||||||
@ -82,6 +81,7 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
success: req.flash('success'),
|
success: req.flash('success'),
|
||||||
warning: req.flash('warning'),
|
warning: req.flash('warning'),
|
||||||
error: req.flash('error'),
|
error: req.flash('error'),
|
||||||
|
'error-alert': req.flash('error-alert'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return _flash._messages;
|
return _flash._messages;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Pagination from "../../src/common/Pagination.js";
|
import {Pagination} from "../../src/common/Pagination.js";
|
||||||
|
|
||||||
describe('Pagination', () => {
|
describe('Pagination', () => {
|
||||||
const pagination = new Pagination(3, 5, 31);
|
const pagination = new Pagination(3, 5, 31);
|
||||||
|
23
yarn.lock
23
yarn.lock
@ -754,6 +754,11 @@
|
|||||||
"@types/qs" "*"
|
"@types/qs" "*"
|
||||||
"@types/serve-static" "*"
|
"@types/serve-static" "*"
|
||||||
|
|
||||||
|
"@types/feather-icons@^4.7.0":
|
||||||
|
version "4.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/feather-icons/-/feather-icons-4.7.0.tgz#ec66bc046bcd1513835f87541ecef54b50c57ec9"
|
||||||
|
integrity sha512-vflOrmlHMGIGVN4AEl6ErPCNKBLcP1ehEpLqnJkTgf69r5QmJzy7BF1WzbBc8Hqs9KffROPT/JqlSKH4o5vB/w==
|
||||||
|
|
||||||
"@types/formidable@^1.0.31":
|
"@types/formidable@^1.0.31":
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.2.1.tgz#d17006b48acd314df9bcfa04236b3e32de0cc817"
|
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.2.1.tgz#d17006b48acd314df9bcfa04236b3e32de0cc817"
|
||||||
@ -1966,6 +1971,11 @@ class-utils@^0.3.5:
|
|||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
|
classnames@^2.2.5:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
|
||||||
clean-css@^4.2.1:
|
clean-css@^4.2.1:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
|
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
|
||||||
@ -2263,6 +2273,11 @@ copy-descriptor@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||||
|
|
||||||
|
core-js@^3.1.3:
|
||||||
|
version "3.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.14.0.tgz#62322b98c71cc2018b027971a69419e2425c2a6c"
|
||||||
|
integrity sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==
|
||||||
|
|
||||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||||
@ -3481,6 +3496,14 @@ fd-slicer@~1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pend "~1.2.0"
|
pend "~1.2.0"
|
||||||
|
|
||||||
|
feather-icons@^4.28.0:
|
||||||
|
version "4.28.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/feather-icons/-/feather-icons-4.28.0.tgz#e1892a401fe12c4559291770ff6e68b0168e760f"
|
||||||
|
integrity sha512-gRdqKESXRBUZn6Nl0VBq2wPHKRJgZz7yblrrc2lYsS6odkNFDnA4bqvrlEVRUPjE1tFax+0TdbJKZ31ziJuzjg==
|
||||||
|
dependencies:
|
||||||
|
classnames "^2.2.5"
|
||||||
|
core-js "^3.1.3"
|
||||||
|
|
||||||
figures@^1.3.5:
|
figures@^1.3.5:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
|
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
|
||||||
|
Loading…
Reference in New Issue
Block a user