Add svelte as a view engine to swaf #33
@ -36,6 +36,7 @@
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/feather-icons": "^4.7.0",
|
||||
"@types/formidable": "^1.0.31",
|
||||
"@types/geoip-lite": "^1.1.31",
|
||||
"@types/jest": "^26.0.4",
|
||||
@ -61,6 +62,7 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-svelte3": "^3.1.2",
|
||||
"feather-icons": "^4.28.0",
|
||||
"jest": "^26.1.0",
|
||||
"jest-resolve": "^26.6.2",
|
||||
"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 MagicLinkWebSocketListener from "./auth/magic_link/MagicLinkWebSocketListener.js";
|
||||
import MakeMagicLinksSessionNotUniqueMigration from "./auth/magic_link/MakeMagicLinksSessionNotUniqueMigration.js";
|
||||
import AddApprovedFieldToUsersTableMigration from "./auth/migrations/AddApprovedFieldToUsersTableMigration.js";
|
||||
import AddNameChangedAtToUsersMigration from "./auth/migrations/AddNameChangedAtToUsersMigration.js";
|
||||
import AddNameToUsersMigration from "./auth/migrations/AddNameToUsersMigration.js";
|
||||
import CreateUsersAndUserEmailsTableMigration from "./auth/migrations/CreateUsersAndUserEmailsTableMigration.js";
|
||||
@ -64,7 +65,7 @@ export default class TestApp extends Application {
|
||||
}
|
||||
|
||||
protected getMigrations(): MigrationType<Migration>[] {
|
||||
return MIGRATIONS;
|
||||
return [...MIGRATIONS, AddApprovedFieldToUsersTableMigration];
|
||||
}
|
||||
|
||||
protected async init(): Promise<void> {
|
||||
@ -140,6 +141,14 @@ export default class TestApp extends Application {
|
||||
this.get('/tests', (req, res) => {
|
||||
res.render('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";
|
||||
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
body {
|
||||
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 "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";
|
||||
|
||||
export let flashed = $locals.flash();
|
||||
console.log('Flashed:', flashed)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.messages :global(.message:not(:last-child)) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="messages">
|
||||
{#if flashed}
|
||||
{#each Object.entries(flashed) as [key, bag], i}
|
||||
|
@ -1,12 +1,112 @@
|
||||
<script>
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
|
||||
export let type;
|
||||
export let content;
|
||||
export let raw = 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>
|
||||
|
||||
<div class="message" class:message-discreet={discreet} data-type="{type}">
|
||||
<i class="icon"></i>
|
||||
<style lang="scss">
|
||||
@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">
|
||||
{#if raw}
|
||||
{@html content}
|
||||
@ -14,4 +114,10 @@
|
||||
{content}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if !sticky}
|
||||
<button type="button" on:click={hide}>
|
||||
<Icon name="x"/>
|
||||
</button>
|
||||
{/if}
|
||||
</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>
|
||||
import CsrfTokenField from "../utils/CsrfTokenField.svelte";
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
import Form from "../utils/Form.svelte";
|
||||
|
||||
export let href;
|
||||
export let icon;
|
||||
@ -7,17 +8,69 @@
|
||||
export let action = false;
|
||||
</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>
|
||||
{#if action}
|
||||
<form action={href} method="POST">
|
||||
<button><i data-feather={icon}></i> <span class="tip">{text}</span></button>
|
||||
|
||||
<CsrfTokenField/>
|
||||
</form>
|
||||
<Form action={href} submitIcon={icon} submitText={text}/>
|
||||
{:else}
|
||||
<a href={href}>
|
||||
<i data-feather={icon}></i> <span class="tip">{text}</span>
|
||||
</a>
|
||||
<Icon name={icon}/>
|
||||
<span class="tip">{text}</span></a>
|
||||
{/if}
|
||||
</li>
|
||||
|
@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {route} from "../../../common/Routing.js";
|
||||
import {Pagination} from "../../../common/Pagination.js";
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
|
||||
export let pagination: string;
|
||||
export let routeName: string;
|
||||
export let contextSize: number;
|
||||
|
||||
if (typeof contextSize !== 'number') contextSize = parseInt(contextSize);
|
||||
|
||||
$: paginationObj = pagination ? Pagination.deserialize(pagination) : null;
|
||||
</script>
|
||||
|
||||
@ -14,7 +17,7 @@
|
||||
<ul>
|
||||
{#if paginationObj.hasPrevious()}
|
||||
<li><a href={route(routeName, {page: paginationObj.page - 1})}>
|
||||
<i data-feather="chevron-left"></i> Previous
|
||||
<Icon name="chevron-left"/> Previous
|
||||
</a></li>
|
||||
|
||||
{#each paginationObj.previousPages(contextSize) as i}
|
||||
@ -38,7 +41,7 @@
|
||||
{/each}
|
||||
|
||||
<li><a href={route(routeName, {page: paginationObj.page + 1})}>
|
||||
Next <i data-feather="chevron-right"></i>
|
||||
Next <Icon name="chevron-right"/>
|
||||
</a></li>
|
||||
{/if}
|
||||
</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";
|
||||
</script>
|
||||
|
||||
<BaseLayout title="{$locals.app.name}">
|
||||
<BaseLayout title="{$locals.app.name}" h1={false}>
|
||||
<div class="panel">
|
||||
<h1>swaf - Svelte Web Application Framework</h1>
|
||||
<p>Welcome to {$locals.app.name}!</p>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href={route('tests')}>Frontend tests</a></li>
|
||||
<li><a href={route('design')}>Design test</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
import {route} from "../../../common/Routing.js";
|
||||
import FlashMessages from "../components/FlashMessages.svelte";
|
||||
import NavMenuItem from "../components/NavMenuItem.svelte";
|
||||
import NavMenu from "../components/NavMenu.svelte";
|
||||
|
||||
export let title: string;
|
||||
export let h1: string = title;
|
||||
@ -11,13 +12,63 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../scss/vars";
|
||||
@import "../../scss/helpers";
|
||||
|
||||
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 {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flash-messages {
|
||||
@include container;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
@ -41,10 +92,8 @@
|
||||
</svelte:head>
|
||||
|
||||
<header>
|
||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="Logo"> {$locals.app.name}</a>
|
||||
<nav>
|
||||
<button id="menu-button"><i data-feather="menu"></i></button>
|
||||
<ul id="main-menu">
|
||||
<a href="/" class="logo"><img src="/img/logo.svg" alt="{$locals.app.name} logo"> {$locals.app.name}</a>
|
||||
<NavMenu>
|
||||
{#if $locals.user}
|
||||
{#if $locals.user.is_admin}
|
||||
<NavMenuItem href={route('backend')} icon="settings" text="Backend"/>
|
||||
@ -55,11 +104,10 @@
|
||||
{:else}
|
||||
<NavMenuItem href={route('auth')} icon="log-in" text="Log in / Register"/>
|
||||
{/if}
|
||||
</ul>
|
||||
</nav>
|
||||
</NavMenu>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="flash-messages">
|
||||
<FlashMessages/>
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {locals} from '../../ts/stores.js';
|
||||
import {FileSize} from "../../../common/FileSize.js";
|
||||
import Message from "../components/Message.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import {getContext} from "svelte";
|
||||
@ -12,11 +13,11 @@
|
||||
export let hint: string | undefined = undefined;
|
||||
export let extraData: 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 fieldId = `${formId}-${name}-field`;
|
||||
|
||||
const validation = $locals.validation()?.[name];
|
||||
const previousFormData = $locals.previousFormData() || [];
|
||||
|
||||
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
|
||||
// whatever behaviour you need
|
||||
value = type.match(/^(number|range)$/)
|
||||
? +e.target.value
|
||||
: e.target.value;
|
||||
? +this.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>
|
||||
|
||||
<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 validation}
|
||||
<Message type="error" content={validation.message}/>
|
||||
{/if}
|
||||
<input type="hidden" name={name} value={value}>
|
||||
{:else}
|
||||
<div class="form-field" class:inline={type === 'checkbox'}>
|
||||
<div class="control">
|
||||
<div class="form-field"
|
||||
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}
|
||||
<Icon name={icon}/>
|
||||
{/if}
|
||||
|
||||
{#if type === 'duration'}
|
||||
<div class="input-group">
|
||||
<fieldset>
|
||||
<legend>{placeholder}</legend>
|
||||
{#each extraData as f}
|
||||
<div class="time-input">
|
||||
<input type="number" name="{name}[{f}]" id="{fieldId}-{f}"
|
||||
value={durationValue(f)}
|
||||
min="0" max={(f === 's' || f === 'm') && '60' || undefined}
|
||||
{...$$restProps}>
|
||||
<label for="{fieldId}-{f}">{{ f }}</label>
|
||||
{...$$restProps} on:click={e => e.stopPropagation()}>
|
||||
<label for="{fieldId}-{f}" on:click={e => e.stopPropagation()}>{f}</label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
{: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}
|
||||
<option value={(option.display === undefined || option.value !== undefined) && (option.value || option)}
|
||||
selected={value === (option.value || option)}>{option.display || option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<i data-feather="chevron-down"></i>
|
||||
<Icon name="chevron-down"/>
|
||||
{: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'}
|
||||
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps}>
|
||||
<input {type} {name} id={fieldId} checked={value === 'on'} {...$$restProps} bind:this={input}>
|
||||
{: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}
|
||||
|
||||
<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>
|
||||
|
||||
{#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 hint}
|
||||
<div class="hint"><i data-feather="info"></i> {hint}</div>
|
||||
<div class="hint">
|
||||
<Icon name="info"/> {hint}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -1,11 +1,50 @@
|
||||
<script lang="ts">
|
||||
import {replaceIcons} from "../../ts/featherIcons.js";
|
||||
import {afterUpdate, onMount} from "svelte";
|
||||
|
||||
export let name: string;
|
||||
|
||||
|
||||
onMount(() => {
|
||||
replaceIcons();
|
||||
});
|
||||
afterUpdate(() => {
|
||||
replaceIcons(false);
|
||||
});
|
||||
</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.startsWith('fa') }
|
||||
<i class="{name} feather icon"></i>
|
||||
<i class="{name} feather icon" aria-hidden="true" {...$$restProps}></i>
|
||||
{:else}
|
||||
<i data-feather="{name}" class="icon"></i>
|
||||
<i data-feather="{name}" class="feather icon" aria-hidden="true" {...$$restProps}></i>
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -34,6 +34,8 @@ export class Pagination {
|
||||
}
|
||||
|
||||
public previousPages(contextSize: number): number[] {
|
||||
if (typeof contextSize !== 'number') throw new Error('contextSize must be a number');
|
||||
|
||||
const pages = [];
|
||||
|
||||
let i = 1;
|
||||
@ -60,6 +62,8 @@ export class Pagination {
|
||||
}
|
||||
|
||||
public nextPages(contextSize: number): number[] {
|
||||
if (typeof contextSize !== 'number') throw new Error('contextSize must be a number');
|
||||
|
||||
const pages = [];
|
||||
|
||||
let i = this.page + 1;
|
||||
|
@ -4,7 +4,6 @@ import {Router} from "express";
|
||||
import session from "express-session";
|
||||
|
||||
import ApplicationComponent from "../ApplicationComponent.js";
|
||||
import ViewEngine from "../frontend/ViewEngine.js";
|
||||
import SecurityError from "../SecurityError.js";
|
||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
||||
import RedisComponent from "./RedisComponent.js";
|
||||
@ -82,6 +81,7 @@ export default class SessionComponent extends ApplicationComponent {
|
||||
success: req.flash('success'),
|
||||
warning: req.flash('warning'),
|
||||
error: req.flash('error'),
|
||||
'error-alert': req.flash('error-alert'),
|
||||
};
|
||||
}
|
||||
return _flash._messages;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Pagination from "../../src/common/Pagination.js";
|
||||
import {Pagination} from "../../src/common/Pagination.js";
|
||||
|
||||
describe('Pagination', () => {
|
||||
const pagination = new Pagination(3, 5, 31);
|
||||
|
23
yarn.lock
23
yarn.lock
@ -754,6 +754,11 @@
|
||||
"@types/qs" "*"
|
||||
"@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":
|
||||
version "1.2.1"
|
||||
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"
|
||||
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:
|
||||
version "4.2.3"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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:
|
||||
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:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
|
||||
|
Loading…
Reference in New Issue
Block a user