├── .browserslistrc ├── Dockerfile ├── .stylelintignore ├── icon.png ├── htdocs ├── favicon.ico ├── assets │ ├── thumbnails │ │ ├── blisk.png │ │ ├── chrome.png │ │ ├── firefox.png │ │ ├── forkme.png │ │ ├── github.png │ │ ├── modal.png │ │ ├── npms-io.png │ │ ├── pingdom.png │ │ ├── pixabay.png │ │ ├── schema.png │ │ ├── trello.png │ │ ├── vivaldi.png │ │ ├── analytics.png │ │ ├── bitbucket.png │ │ ├── modern-ie.png │ │ ├── pagespeed.png │ │ ├── safari-tp.png │ │ ├── sdi-blog.png │ │ ├── autocomplete.png │ │ ├── browserlist.png │ │ ├── codecademy.png │ │ ├── design-uber.png │ │ ├── firefox-dev.png │ │ ├── google-dev.png │ │ ├── microformats.png │ │ ├── mozilla-dev.png │ │ ├── rich-snippet.png │ │ ├── sdi-homepage.png │ │ ├── smartmockups.png │ │ ├── accessibility.png │ │ ├── design-airbnb.png │ │ ├── design-firefox.png │ │ ├── design-google.png │ │ ├── html-validator.png │ │ ├── magic-mockups.png │ │ ├── sdi-startpage.png │ │ ├── webmastertools.png │ │ ├── codepen-patterns.png │ │ ├── design-atlassian.png │ │ ├── design-facebook.png │ │ ├── design-invision.png │ │ ├── design-material.png │ │ ├── design-microsoft.png │ │ ├── design-unsplash.png │ │ ├── mobile-friendly.png │ │ ├── sdi-personalnews.png │ │ ├── frontend-bookmarks.png │ │ └── sdi-little-helpers.png │ ├── wallpaper │ │ └── default.jpg │ ├── qr-codes │ │ ├── design-system.png │ │ ├── github-project.png │ │ ├── little-helpers.png │ │ ├── metafolio-de.png │ │ ├── metaideen-de.png │ │ ├── startpage-demo.png │ │ └── personalnews-landing.png │ ├── js │ │ └── site.js │ └── css │ │ └── site.css ├── manifest.json ├── favicon.svg ├── maskIcon.svg ├── application.manifest.php ├── serviceworker.js ├── config │ └── config.php ├── index.php └── data │ └── data.json ├── src ├── assets │ ├── scss │ │ ├── _your-themes │ │ │ ├── variables.scss │ │ │ └── your-theme.scss │ │ ├── 2-tools │ │ │ ├── _align-vertical.scss │ │ │ ├── _clearfix.scss │ │ │ ├── _content-visibility.scss │ │ │ ├── _align-horizontal.scss │ │ │ ├── _ul-reset.scss │ │ │ ├── _align-centered.scss │ │ │ └── _responsive.scss │ │ ├── 4-elements │ │ │ ├── _links.scss │ │ │ ├── _navigation.scss │ │ │ ├── _buttons.scss │ │ │ ├── _main.scss │ │ │ ├── _footer.scss │ │ │ ├── _details-summary.scss │ │ │ └── _header.scss │ │ ├── 7-utilities │ │ │ ├── sticky.scss │ │ │ ├── _js-utilities.scss │ │ │ └── _theme-default.scss │ │ ├── 3-generic │ │ │ ├── _font-smoothing.scss │ │ │ ├── _box-sizing.scss │ │ │ ├── _page.scss │ │ │ └── _reset.scss │ │ ├── 5-objects │ │ │ ├── _list-horizontal.scss │ │ │ ├── _tabbed-content.scss │ │ │ ├── _list-vertical.scss │ │ │ └── _list-tiles.scss │ │ ├── 6-components │ │ │ ├── _modal-qr.scss │ │ │ ├── _wallpaper.scss │ │ │ ├── _backdrop.scss │ │ │ ├── _notification.scss │ │ │ ├── _overlay.scss │ │ │ ├── _modal-list.scss │ │ │ ├── _modal.scss │ │ │ ├── _tile.scss │ │ │ └── _flyout.scss │ │ ├── _shame.scss │ │ ├── 1-settings │ │ │ ├── _globals.scss │ │ │ └── _colors.scss │ │ └── site.scss │ └── js │ │ ├── functions │ │ ├── find.js │ │ ├── scrollToPos.js │ │ ├── addClass.js │ │ ├── findAll.js │ │ ├── removeClass.js │ │ ├── toggleClass.js │ │ ├── localStorage.js │ │ ├── stickyElement.js │ │ ├── fixScrollPos.js │ │ └── handleTriggers.js │ │ ├── components │ │ ├── setJsAvailability.js │ │ ├── notificationKeydown.js │ │ └── handleTabsLocalStorage.js │ │ └── site.js ├── images │ └── favicon.svg ├── serviceworker.js └── manifests │ └── default.pp ├── .screenshots └── startpage-macbook-iphone.jpg ├── .gitignore ├── __tests__ └── e2e │ ├── modal.spec.ts-snapshots │ ├── open-firefox-darwin.png │ ├── open-chromium-darwin.png │ └── open-Mobile-Safari-darwin.png │ ├── render.spec.ts-snapshots │ ├── render-chromium-darwin.png │ ├── render-firefox-darwin.png │ └── render-Mobile-Safari-darwin.png │ ├── sidebar.spec.ts-snapshots │ ├── sidebar-firefox-darwin.png │ ├── sidebar-chromium-darwin.png │ └── sidebar-Mobile-Safari-darwin.png │ ├── render.spec.ts │ ├── modal.spec.ts │ ├── tabswitch.spec.ts │ └── sidebar.spec.ts ├── .editorconfig ├── _tasks ├── _config.json ├── clean.js ├── scripts.js ├── environment.js ├── imagemin.js ├── styles.js └── vm.js ├── docker-compose.yml ├── gulpfile.js ├── postcss.config.js ├── LICENSE ├── webpack.config.js ├── Vagrantfile ├── package.json ├── playwright.config.ts ├── README.md ├── .stylelintrc.json └── tests └── example.spec.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | not dead 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3-apache 2 | ADD htdocs /htdocs 3 | EXPOSE 8080 4 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | htdocs 2 | node_modules 3 | *.md 4 | src/assets/scss/site.scss 5 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/icon.png -------------------------------------------------------------------------------- /htdocs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/favicon.ico -------------------------------------------------------------------------------- /src/assets/scss/_your-themes/variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | put values you want to overwrite here 3 | e.g. $brand-color: #f00; 4 | */ 5 | -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/blisk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/blisk.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/chrome.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/firefox.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/forkme.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/github.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/modal.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/npms-io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/npms-io.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/pingdom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/pingdom.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/pixabay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/pixabay.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/schema.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/trello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/trello.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/vivaldi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/vivaldi.png -------------------------------------------------------------------------------- /htdocs/assets/wallpaper/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/wallpaper/default.jpg -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/analytics.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/bitbucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/bitbucket.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/modern-ie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/modern-ie.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/pagespeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/pagespeed.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/safari-tp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/safari-tp.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/sdi-blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/sdi-blog.png -------------------------------------------------------------------------------- /.screenshots/startpage-macbook-iphone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/.screenshots/startpage-macbook-iphone.jpg -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/design-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/design-system.png -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/github-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/github-project.png -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/little-helpers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/little-helpers.png -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/metafolio-de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/metafolio-de.png -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/metaideen-de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/metaideen-de.png -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/startpage-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/startpage-demo.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/autocomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/autocomplete.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/browserlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/browserlist.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/codecademy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/codecademy.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-uber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-uber.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/firefox-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/firefox-dev.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/google-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/google-dev.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/microformats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/microformats.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/mozilla-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/mozilla-dev.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/rich-snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/rich-snippet.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/sdi-homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/sdi-homepage.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/smartmockups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/smartmockups.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/accessibility.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-airbnb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-airbnb.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-firefox.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-google.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/html-validator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/html-validator.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/magic-mockups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/magic-mockups.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/sdi-startpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/sdi-startpage.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/webmastertools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/webmastertools.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/codepen-patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/codepen-patterns.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-atlassian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-atlassian.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-facebook.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-invision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-invision.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-material.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-microsoft.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/design-unsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/design-unsplash.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/mobile-friendly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/mobile-friendly.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/sdi-personalnews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/sdi-personalnews.png -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_align-vertical.scss: -------------------------------------------------------------------------------- 1 | @mixin align-vertical { 2 | position: absolute; 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_clearfix.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &::after { 3 | clear: both; 4 | content: ""; 5 | display: table; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_content-visibility.scss: -------------------------------------------------------------------------------- 1 | @mixin content-visibility { 2 | contain-intrinsic-size: 1px 2000px; 3 | content-visibility: auto; 4 | } 5 | -------------------------------------------------------------------------------- /htdocs/assets/qr-codes/personalnews-landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/qr-codes/personalnews-landing.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/frontend-bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/frontend-bookmarks.png -------------------------------------------------------------------------------- /htdocs/assets/thumbnails/sdi-little-helpers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/htdocs/assets/thumbnails/sdi-little-helpers.png -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_align-horizontal.scss: -------------------------------------------------------------------------------- 1 | @mixin align-horizontal { 2 | left: 50%; 3 | position: absolute; 4 | transform: translateX(-50%); 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_ul-reset.scss: -------------------------------------------------------------------------------- 1 | @mixin ul-reset { 2 | list-style-type: none; 3 | margin-bottom: 0; 4 | margin-top: 0; 5 | padding-left: 0; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _private 2 | _private_m 3 | node_modules 4 | .idea 5 | .vscode 6 | .DS_Store 7 | /__test-results__/ 8 | /playwright-report/ 9 | /playwright/.cache/ 10 | -------------------------------------------------------------------------------- /__tests__/e2e/modal.spec.ts-snapshots/open-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/modal.spec.ts-snapshots/open-firefox-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/modal.spec.ts-snapshots/open-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/modal.spec.ts-snapshots/open-chromium-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/render.spec.ts-snapshots/render-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/render.spec.ts-snapshots/render-chromium-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/render.spec.ts-snapshots/render-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/render.spec.ts-snapshots/render-firefox-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/sidebar.spec.ts-snapshots/sidebar-firefox-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/sidebar.spec.ts-snapshots/sidebar-firefox-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/modal.spec.ts-snapshots/open-Mobile-Safari-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/modal.spec.ts-snapshots/open-Mobile-Safari-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/sidebar.spec.ts-snapshots/sidebar-chromium-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/sidebar.spec.ts-snapshots/sidebar-chromium-darwin.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /__tests__/e2e/render.spec.ts-snapshots/render-Mobile-Safari-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/render.spec.ts-snapshots/render-Mobile-Safari-darwin.png -------------------------------------------------------------------------------- /__tests__/e2e/sidebar.spec.ts-snapshots/sidebar-Mobile-Safari-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saschadiercks/browserStartpage/HEAD/__tests__/e2e/sidebar.spec.ts-snapshots/sidebar-Mobile-Safari-darwin.png -------------------------------------------------------------------------------- /_tasks/_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": "src", 3 | "dist": "htdocs", 4 | "assetSrc": "src/assets", 5 | "assetDist": "htdocs/assets", 6 | "envProduction": "production", 7 | "envDevelopment": "development" 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | web: 4 | build: . 5 | command: php -S 0.0.0.0:8080 -t /htdocs 6 | ports: 7 | - "8080:8080" 8 | volumes: 9 | - ./htdocs:/htdocs 10 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_links.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | a { 9 | color: inherit; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/scss/7-utilities/sticky.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | .sdi-sticky { 9 | left: 0; 10 | position: sticky; 11 | top: 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/js/functions/find.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function find(selector) { 9 | return document.querySelector(selector); 10 | } 11 | -------------------------------------------------------------------------------- /htdocs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Startpage", 3 | "name": "Browser Startpage", 4 | "icons": [ 5 | { 6 | "src": "favicon.svg", 7 | "type": "image/svg+xml", 8 | "sizes": "any" 9 | } 10 | ], 11 | "start_url": "./?utm_source=homescreen" 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/scss/3-generic/_font-smoothing.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | body { 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/scss/5-objects/_list-horizontal.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | .list-horizontal { 9 | @include ul-reset; 10 | 11 | li { 12 | display: inline-block; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_align-centered.scss: -------------------------------------------------------------------------------- 1 | @mixin align-centered($scale, $position: absolute) { 2 | @if $scale { 3 | transform: translate(-50%, -50%) scale(#{$scale}); 4 | } 5 | 6 | @else { 7 | transform: translate(-50%, -50%); 8 | } 9 | left: 50%; 10 | position: #{$position}; 11 | top: 50%; 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/js/functions/scrollToPos.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function scrollToPos(x,y) { 9 | window.scroll({ 10 | top: y, 11 | left: x, 12 | behavior: 'smooth' 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_modal-qr.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | .modal-content--qr { 9 | text-align: center; 10 | } 11 | 12 | .modal-content--qr img { 13 | mix-blend-mode: multiply; 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/scss/_shame.scss: -------------------------------------------------------------------------------- 1 | /* 2 | The class js-hidden is used to hide the notification 3 | this class uses display:none. To be able to animate the notification via css, 4 | we need to overwrite that class 5 | */ 6 | #notification { 7 | display: block !important; 8 | } 9 | 10 | .overlay { 11 | display: block !important; 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/js/functions/addClass.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function addClass(elements, className) { 9 | elements.forEach(function(element){ 10 | element.classList.add(className); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/js/functions/findAll.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function findAll(selector) { 9 | var elements = document.querySelectorAll(selector); 10 | return Array.prototype.slice.call(elements) 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/js/functions/removeClass.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function removeClass(elements,className) { 9 | elements.forEach(function(element){ 10 | element.classList.remove(className); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/js/functions/toggleClass.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function toggleClass(elements, className) { 9 | elements.forEach(function(element){ 10 | element.classList.toggle(className); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_navigation.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | nav { 9 | ul { 10 | @include ul-reset; 11 | @include clearfix; 12 | } 13 | 14 | a { 15 | display: block; 16 | text-decoration: none; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /_tasks/clean.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_config.json"); 3 | const gulp = require("gulp"); 4 | const del = require("del"); 5 | 6 | /* ################# */ 7 | /* ##### Tasks ##### */ 8 | /* ################# */ 9 | gulp.task("clean:scripts", function () { 10 | return del([config.assetDist + "/js/**", config.assetDist + "/css/**"]); 11 | }); 12 | -------------------------------------------------------------------------------- /src/assets/js/functions/localStorage.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | 7 | // ###### script ###### 8 | export default function doLocalStorage(item,value) { 9 | if(value) { 10 | localStorage.setItem(item,value); 11 | } else { 12 | return localStorage.getItem(item); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/e2e/render.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("Render", async ({ page }) => { 4 | // Go to http://localhost:8080/ 5 | await page.goto("/"); 6 | 7 | // take a screenshot 8 | test.slow(); // give time to fetch 9 | expect(await page.screenshot()).toMatchSnapshot("render.png", { 10 | threshold: 0.3 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/assets/scss/3-generic/_box-sizing.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Set the global `box-sizing` state to `border-box`. 3 | * 4 | * css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice 5 | * paulirish.com/2012/box-sizing-border-box-ftw 6 | */ 7 | 8 | html { 9 | box-sizing: border-box; 10 | } 11 | 12 | *, 13 | *::before, 14 | *::after { 15 | box-sizing: inherit; 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_buttons.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $button-background-color: transparent !default; 5 | 6 | //--------------------- 7 | // ###### Layout ###### 8 | //--------------------- 9 | button { 10 | background-color: $button-background-color; 11 | border-width: 0; 12 | color: inherit; 13 | cursor: pointer; 14 | font-size: inherit; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_main.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | main { 9 | // we need to set the width via CSS to avoid jumping, when position:fixed is added 10 | left: 0; 11 | margin: $base-spacing-size; 12 | position: relative; 13 | right: 0; 14 | z-index: 1; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/scss/5-objects/_tabbed-content.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | .tabbed-content { 9 | @include content-visibility; 10 | } 11 | 12 | // Hide Tabs if JS is available, otherwise show all content 13 | .js .tabbed-content:not(.sdi-is-active) { 14 | display: none; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_wallpaper.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | #wallpaper { 9 | background-position: center center; 10 | background-size: cover; 11 | bottom: 0; 12 | left: 0; 13 | position: fixed; 14 | right: 0; 15 | top: 0; 16 | filter: var(--wallpaper-filter); 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/scss/5-objects/_list-vertical.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | .list-vertical { 9 | @include ul-reset; 10 | padding-bottom: $base-spacing-size * 0.75; 11 | } 12 | 13 | .list-vertical__link { 14 | display: block; 15 | text-decoration: none; 16 | 17 | &:hover { 18 | text-decoration: underline; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /_tasks/scripts.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_config.json"); 3 | const gulp = require("gulp"); 4 | const exec = require("child_process").exec; 5 | 6 | /* ################# */ 7 | /* ##### Tasks ##### */ 8 | /* ################# */ 9 | gulp.task("scripts:build", function(cb) { 10 | exec("npx webpack --config webpack.config.js", function(err, stdout, stderr) { 11 | console.log(stdout); 12 | console.log(stderr); 13 | cb(err); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/assets/scss/2-tools/_responsive.scss: -------------------------------------------------------------------------------- 1 | // Responsive Layout 2 | @mixin phone-only { 3 | @media (max-width: 670px) { 4 | @content; 5 | } 6 | } 7 | 8 | @mixin tablet-portrait { 9 | @media (min-width: 671px) and (max-width: 1023px) { 10 | @content; 11 | } 12 | } 13 | 14 | @mixin tablet-landscape-up { 15 | @media (min-width: 1024px) { 16 | @content; 17 | } 18 | } 19 | 20 | @mixin default { 21 | @media (min-width: 671px) { 22 | @content; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /_tasks/environment.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_config.json"); 3 | const gulp = require("gulp"); 4 | 5 | /* ################# */ 6 | /* ##### Tasks ##### */ 7 | /* ################# */ 8 | gulp.task("set-dev-node-env", function () { 9 | return Promise.resolve((process.env.NODE_ENV = config.envDevelopment)); 10 | }); 11 | 12 | gulp.task("set-prod-node-env", function () { 13 | return Promise.resolve((process.env.NODE_ENV = config.envProduction)); 14 | }); 15 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_backdrop.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $backdrop-color__this: var(--backdrop-color) !default; 5 | 6 | //--------------------- 7 | // ###### Layout ###### 8 | //--------------------- 9 | .backdrop { 10 | background-color: $backdrop-color__this; 11 | backdrop-filter: blur(10px); 12 | height: 100vh; 13 | position: absolute; 14 | top: 0; 15 | transition: opacity 0s ease; 16 | width: 100vw; 17 | z-index: -1; 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/scss/3-generic/_page.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | 9 | // ---- theme: auto 10 | // the default is light 11 | html { 12 | @include theme-light; 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | html { 17 | @include theme-dark; 18 | } 19 | } 20 | 21 | body { 22 | background-color: var(--bg); 23 | font-family: $document-font-family; 24 | width: 100%; 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_notification.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $notification-background-color: var(--bg) !default; 5 | $notification-color: var(--color) !default; 6 | 7 | //--------------------- 8 | // ###### Layout ###### 9 | //--------------------- 10 | .notification { 11 | transition: all 0.2s; 12 | 13 | &.js-hidden { 14 | transform: translateX(100%); 15 | } 16 | 17 | &.js-visible { 18 | right: $base-spacing-size; 19 | transform: translateX(0%); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/scss/7-utilities/_js-utilities.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | // ---- js-hidden (Elements to be hidden via JS) ---- 9 | .js-hidden { 10 | // display: none; 11 | // z-index: -1; 12 | } 13 | 14 | // ---- js-fixed (Element to be fixed via JS) ---- 15 | .sdi-is-fixed { 16 | position: fixed; 17 | } 18 | 19 | // ---- Js-sticky (Element to be fixed via JS) ---- 20 | .js-sticky { 21 | position: fixed; 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/scss/3-generic/_reset.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | html { 9 | font-size: 100%; 10 | height: 100%; 11 | overflow-x: hidden; 12 | overflow-y: scroll; 13 | -webkit-text-size-adjust: 100%; 14 | -ms-text-size-adjust: 100%; 15 | } 16 | 17 | body { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | img { 23 | height: auto; 24 | max-width: 100%; 25 | } 26 | 27 | /* HTML 5 fixes */ 28 | header, 29 | main, 30 | footer { 31 | display: block; 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/scss/_your-themes/your-theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | How to theme the site: 3 | Place your css/scss here to change the layout of the page 4 | Copy Variables from 1-settings/globals.scss and paste them here to change them 5 | Use 'gulp build' to build the theme 6 | 7 | If you want to make some deeper changes, duplicate 7-trumps/theme-default and 8 | name it to your liking. 9 | After that add the new file at the bottom of site.scss (look there for 10 | additonal information) 11 | 12 | This way it's the easiest to stay up to date if there are further changes in 13 | the repository - which is not unlikely. 14 | */ 15 | -------------------------------------------------------------------------------- /htdocs/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/js/components/setJsAvailability.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | import findAll from "../functions/findAll.js"; 3 | import addClass from "../functions/addClass.js"; 4 | import removeClass from "../functions/removeClass.js"; 5 | 6 | // #################### 7 | // ##### settings ##### 8 | // #################### 9 | const class__jsIsAvailable = 'js'; 10 | const class__jsIsNotAvailable = 'no-js'; 11 | 12 | // ###### script ###### 13 | export default function setJsAvailability(selector) { 14 | var selector = findAll(selector); 15 | 16 | selector.forEach(function() { 17 | addClass(selector, class__jsIsAvailable); 18 | removeClass(selector, class__jsIsNotAvailable); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/js/functions/stickyElement.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | import addClass from "./addClass.js"; 3 | import find from "./find.js"; 4 | import findAll from "./findAll.js"; 5 | 6 | // #################### 7 | // ##### settings ##### 8 | // #################### 9 | const class__sticky = 'js-sticky'; 10 | 11 | // ###### script ###### 12 | export default function stickyElement(selectorSticky, selectorCompensate, propertyCompensate) { 13 | var stickyElement = findAll(selectorSticky); 14 | var stickyHeight = find(selectorSticky).clientHeight + 'px'; 15 | addClass(stickyElement, class__sticky); 16 | 17 | find(selectorCompensate).style.setProperty(propertyCompensate,stickyHeight); 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_overlay.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $overlay-background-color: var(--bg) !default; 5 | $overlay-color: var(--color) !default; 6 | 7 | //--------------------- 8 | // ###### Layout ###### 9 | //--------------------- 10 | .overlay { 11 | display: block; 12 | position: fixed; 13 | z-index: 3; 14 | } 15 | 16 | .overlay-close { 17 | float: right; 18 | } 19 | 20 | .overlay-title { 21 | font-size: 1rem; 22 | font-weight: 600; 23 | margin-top: 0; 24 | padding-top: inherit; 25 | } 26 | 27 | .overlay-content { 28 | height: 100vh; 29 | overflow-y: auto; 30 | transition: all $base-animation-speed ease; 31 | width: 100%; 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/scss/5-objects/_list-tiles.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | 5 | //--------------------- 6 | // ###### Layout ###### 7 | //--------------------- 8 | .list-tiles { 9 | @include ul-reset; 10 | align-items: stretch; 11 | display: flex; 12 | flex-wrap: wrap; 13 | margin-left: auto; 14 | margin-right: auto; 15 | } 16 | 17 | @supports (display: grid) { 18 | .list-tiles { 19 | display: grid; 20 | grid-gap: $base-spacing-size; 21 | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); 22 | } 23 | 24 | @media (min-width: 768px) { 25 | .list-tiles { 26 | grid-template-columns: repeat(auto-fit, minmax(256px, 1fr)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /_tasks/imagemin.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_config.json"); 3 | const gulp = require("gulp"); 4 | const imagemin = require("gulp-imagemin"); 5 | 6 | const imgExt = "{jpg,png,svg}"; 7 | 8 | /* ################# */ 9 | /* ##### Tasks ##### */ 10 | /* ################# */ 11 | gulp.task("imagemin", function () { 12 | return gulp 13 | .src(config.assetDist + "/**/*." + imgExt) 14 | .pipe( 15 | imagemin([ 16 | imagemin.jpegtran({ progressive: true }), 17 | imagemin.optipng({ optimizationLevel: 5 }), 18 | imagemin.svgo({ 19 | plugins: [{ removeViewBox: true }, { cleanupIDs: false }], 20 | }), 21 | ]) 22 | ) 23 | .pipe(gulp.dest(config.assetDist)); 24 | }); 25 | -------------------------------------------------------------------------------- /src/assets/scss/1-settings/_globals.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | // ----- Setup ----- 4 | $document-font-family: -apple-system, sans-serif !default; 5 | $wallpaper-filter: blur(0.1px) !default; 6 | 7 | $base-spacing-size: 1vw !default; 8 | $base-spacing-size-px: 9px !default; 9 | $base-animation-speed: 0.2s !default; 10 | 11 | $tile-button-padding: math.div($base-spacing-size, 2) !default; 12 | $tile-button-color: inherit; 13 | 14 | $background-blur-setting: 1px; 15 | 16 | $modal-border: none !default; 17 | $modal-border-radius: 4px !default; 18 | $modal-content-max-height: 400px !default; 19 | $modal-content-height: auto !default; 20 | $modal-content-max-width: 500px !default; 21 | $modal-content-width: 90% !default; 22 | 23 | $bookmarks-max-width: 400px !default; 24 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_modal-list.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $modal-list-link-color-hover: var(--border-color) !default; 5 | 6 | //--------------------- 7 | // ###### Layout ###### 8 | //--------------------- 9 | .modal-list { 10 | @include ul-reset; 11 | } 12 | 13 | .modal-list__item { 14 | display: inline-block; 15 | width: 49%; 16 | } 17 | 18 | @supports (display: grid) { 19 | .modal-list { 20 | display: grid; 21 | grid-template-columns: 50% auto; 22 | } 23 | 24 | .modal-list__item { 25 | width: 100%; 26 | } 27 | } 28 | 29 | .modal-list__link { 30 | display: block; 31 | padding: $base-spacing-size-px; 32 | text-decoration: none; 33 | 34 | &:hover { 35 | background-color: $modal-list-link-color-hover; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/js/functions/fixScrollPos.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | 3 | // #################### 4 | // ##### settings ##### 5 | // #################### 6 | const selector__body = document.querySelector('body'); 7 | const class__elementIsFixed = 'sdi-is-fixed'; 8 | 9 | var scrollYSaved; 10 | 11 | // ###### script ###### 12 | export default function fixScrollPos(event) { 13 | var scrollY = window.pageYOffset; 14 | //console.log(event); 15 | 16 | if(selector__body.classList.contains(class__elementIsFixed)) { 17 | selector__body.classList.remove(class__elementIsFixed); 18 | selector__body.style.top = ''; 19 | window.scrollTo(0,scrollYSaved); 20 | } else { 21 | selector__body.classList.add(class__elementIsFixed); 22 | selector__body.style.top = '-' + scrollY + 'px'; 23 | scrollYSaved = scrollY; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_footer.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $footer-bg: var(--brand-color) !default; 5 | $footer-color: var(--color-light) !default; 6 | 7 | //--------------------- 8 | // ###### Layout ###### 9 | //--------------------- 10 | footer { 11 | @include clearfix; 12 | background-color: $footer-bg; 13 | color: $footer-color; 14 | font-size: 0.75rem; 15 | padding-left: $base-spacing-size; 16 | padding-right: $base-spacing-size; 17 | width: 100%; 18 | z-index: 1; 19 | 20 | &.js-sticky { 21 | bottom: 0; 22 | } 23 | 24 | a { 25 | color: inherit; 26 | display: inline-block; 27 | padding-bottom: $base-spacing-size-px; 28 | padding-top: $base-spacing-size-px; 29 | } 30 | 31 | .description { 32 | float: left; 33 | } 34 | 35 | .social-profiles { 36 | float: right; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const gulp = require("gulp"); 3 | require("require-dir")("./_tasks"); 4 | 5 | /* ################# */ 6 | /* ##### Tasks ##### */ 7 | /* ################# */ 8 | // --- set environment ---- 9 | gulp.task("production", gulp.series("set-prod-node-env")); 10 | gulp.task("development", gulp.series("set-dev-node-env")); 11 | 12 | // --- group tasks ---- 13 | gulp.task("clean", gulp.series("clean:scripts")); 14 | gulp.task("lint", gulp.series("lint:css")); 15 | gulp.task("scripts", gulp.series("scripts:build")); 16 | gulp.task("styles", gulp.series("lint:css", "compile:css")); 17 | 18 | // --- run tasks ---- 19 | gulp.task("update", gulp.series("development", "styles", "scripts")); 20 | gulp.task( 21 | "build", 22 | gulp.series("production", "clean", "styles", "scripts", "imagemin") 23 | ); 24 | 25 | // --- run application ---- 26 | gulp.task("serve", gulp.series("build", "docker:up")); 27 | gulp.task("stop", gulp.series("docker:down")); 28 | -------------------------------------------------------------------------------- /src/assets/js/components/notificationKeydown.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | import findAll from "../functions/findAll.js"; 3 | import addClass from "../functions/addClass.js"; 4 | import removeClass from "../functions/removeClass.js"; 5 | 6 | // #################### 7 | // ##### settings ##### 8 | // #################### 9 | const class__isHidden = 'js-hidden'; 10 | const class__isVisible = 'js-visible'; 11 | 12 | // ###### script ###### 13 | export default function notificationKeyDown(selector) { 14 | var targetElement = findAll(selector); 15 | 16 | document.addEventListener('keydown', function() { 17 | targetElement.forEach( function() { 18 | addClass(targetElement, class__isVisible); 19 | removeClass(targetElement, class__isHidden); 20 | }); 21 | }); 22 | 23 | document.addEventListener('keyup', function() { 24 | targetElement.forEach( function() { 25 | addClass(targetElement, class__isHidden); 26 | removeClass(targetElement, class__isVisible); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/js/functions/handleTriggers.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | import findAll from "./findAll.js"; 3 | import toggleClass from "./toggleClass.js"; 4 | 5 | // #################### 6 | // ##### settings ##### 7 | // #################### 8 | const class__isActive = 'js-is-active'; 9 | 10 | // ###### script ###### 11 | export default function handleTriggers(selector, callback) { 12 | let targetElementsObject = findAll(selector); 13 | 14 | // convert the object into an array, so we can run forEach in IE on it 15 | let targetElements = Array.prototype.slice.call(targetElementsObject); 16 | 17 | targetElements.forEach(function(element) { 18 | element.addEventListener('click', function() { 19 | let elementTarget = findAll(this.getAttribute('data-target')); 20 | toggleClass(elementTarget, class__isActive); 21 | 22 | // check if a callback is defined 23 | if(typeof callback === "function") { 24 | callback(); 25 | } 26 | event.preventDefault(); 27 | }); 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | // `api.file` - path to the file 3 | // `api.mode` - `mode` value of webpack, please read https://webpack.js.org/configuration/mode/ 4 | // `api.webpackLoaderContext` - loader context for complex use cases 5 | // `api.env` - alias `api.mode` for compatibility with `postcss-cli` 6 | // `api.options` - the `postcssOptions` options 7 | 8 | if (/\.sss$/.test(api.file)) { 9 | return { 10 | // You can specify any options from https://postcss.org/api/#processoptions here 11 | parser: "sugarss", 12 | plugins: [ 13 | // Plugins for PostCSS 14 | ["postcss-short", { prefix: "x" }], 15 | "postcss-preset-env", 16 | ], 17 | }; 18 | } 19 | 20 | return { 21 | // You can specify any options from https://postcss.org/api/#processoptions here 22 | plugins: [ 23 | // Plugins for PostCSS 24 | ["postcss-short", { prefix: "x" }], 25 | "postcss-preset-env", 26 | ], 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /_tasks/styles.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_config.json"); 3 | const gulp = require("gulp"); 4 | const sass = require("gulp-sass")(require('sass')); 5 | const gulpStylelint = require("gulp-stylelint"); 6 | const autoprefixer = require("gulp-autoprefixer"); 7 | 8 | /* ################# */ 9 | /* ##### Tasks ##### */ 10 | /* ################# */ 11 | gulp.task("compile:css", function () { 12 | return gulp 13 | .src(config.assetSrc + "/scss/*.scss") 14 | .pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError)) 15 | .pipe( 16 | autoprefixer({ 17 | browsers: ["last 2 versions", ">5%"], 18 | cascade: false, 19 | }) 20 | ) 21 | .pipe(gulp.dest(config.assetDist + "/css")); 22 | }); 23 | 24 | // lint 25 | gulp.task("lint:css", function () { 26 | return gulp.src(config.assetSrc + "/scss/**/*.scss").pipe( 27 | gulpStylelint({ 28 | fix: true, 29 | reporters: [{ formatter: "string", console: true }], 30 | }) 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_modal.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $modal-header-background-color: transparent !default; 5 | $modal-header-color: var(--color) !default; 6 | $modal-header-border: 2px solid var(--brand-color) !default; 7 | $modal-content-background-color: var(--bg) !default; 8 | $modal-content-color: var(--color) !default; 9 | 10 | //--------------------- 11 | // ###### Layout ###### 12 | //--------------------- 13 | .js .modal { 14 | transition: visibility $base-animation-speed ease-in-out; 15 | visibility: hidden; 16 | } 17 | 18 | .modal.js-is-active { 19 | position: fixed; 20 | top: 0; 21 | visibility: visible; 22 | z-index: 2; 23 | } 24 | 25 | .modal-overlay { 26 | @include align-centered($scale: 0, $position: fixed); 27 | } 28 | 29 | .modal-header { 30 | font-weight: 600; 31 | position: relative; 32 | } 33 | 34 | .modal-content { 35 | overflow-y: auto; 36 | } 37 | 38 | .modal-header__close { 39 | @include align-vertical; 40 | right: 0; 41 | } 42 | -------------------------------------------------------------------------------- /__tests__/e2e/modal.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | //which modal to test for? 4 | const modalSelector = "#linksasmodal"; 5 | 6 | test("modal", async ({ page }) => { 7 | // Go to http://localhost:8080/ 8 | await page.goto("/"); 9 | 10 | // find modal link 11 | await page.locator(`a[data-target="${modalSelector}"]`).click(); 12 | 13 | // open modal 14 | expect(page.locator(`${modalSelector} .modal-overlay`)).toBeVisible; 15 | 16 | // click on uncritical element to wait until the animation is done 17 | await page.locator(`${modalSelector} .modal-header`).click(); 18 | 19 | // take a screenshot 20 | test.slow(); // give time to fetch 21 | expect(await page.screenshot()).toMatchSnapshot("open.png", { 22 | threshold: 0.3 23 | }); 24 | 25 | // click on close button to hide modal 26 | await page 27 | .locator(`button.js-modal-trigger[data-target="${modalSelector}"]`) 28 | .click(); 29 | 30 | expect(page.locator(`${modalSelector} .modal-overlay`)).toBeHidden; 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/e2e/tabswitch.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | const menuSelector = "#header-nav"; 4 | 5 | test("test", async ({ page, browserName }) => { 6 | // Go to http://localhost:8080/ 7 | await page.goto("/"); 8 | 9 | // open the mobile menu 10 | if (browserName === "webkit") { 11 | await page.locator(`button[data-target="${menuSelector}"]`).click(); 12 | await expect(page.locator(menuSelector)).toBeVisible; 13 | } 14 | 15 | // click second tab 16 | await page.locator(".tablist .tablist__item:nth-child(2) a").click(); 17 | 18 | // is the url updated 19 | await expect(page).toHaveURL("/#tab-2"); 20 | 21 | // do the rtabs react in a correct manner? 22 | await expect(page.locator("#tab-1")).toBeHidden; 23 | await expect(page.locator("#tab-2")).toBeVisible; 24 | 25 | // close the mobile menu 26 | if (browserName === "webkit") { 27 | await page.locator(`button[data-target="${menuSelector}"]`).click(); 28 | await expect(page.locator(menuSelector)).toBeHidden; 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sascha Diercks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_tasks/vm.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_config.json"); 3 | const gulp = require("gulp"); 4 | const exec = require("child_process").exec; 5 | 6 | /* ################# */ 7 | /* ##### Tasks ##### */ 8 | /* ################# */ 9 | gulp.task("vagrant:up", cb => { 10 | exec("vagrant up", (err, stdout, stderr) => { 11 | console.log(stdout); 12 | console.log(stderr); 13 | cb(err); 14 | }); 15 | }); 16 | gulp.task("vagrant:halt", cb => { 17 | exec("vagrant halt", (err, stdout, stderr) => { 18 | console.log(stdout); 19 | console.log(stderr); 20 | cb(err); 21 | }); 22 | }); 23 | gulp.task("vagrant:reload", cb => { 24 | exec("vagrant reload", (err, stdout, stderr) => { 25 | console.log(stdout); 26 | console.log(stderr); 27 | cb(err); 28 | }); 29 | }); 30 | 31 | gulp.task("docker:up", cb => { 32 | exec("docker-compose up -d", (err, stdout, stderr) => { 33 | console.log(stdout); 34 | console.log(stderr); 35 | cb(err); 36 | }); 37 | }); 38 | gulp.task("docker:down", cb => { 39 | exec("docker-compose down", (err, stdout, stderr) => { 40 | console.log(stdout); 41 | console.log(stderr); 42 | cb(err); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* #### Setting #### */ 2 | const config = require("./_tasks/_config.json"); 3 | const path = require("path"); 4 | const ReplaceHashInFileWebpackPlugin = require("replace-hash-in-file-webpack-plugin"); 5 | 6 | /* ################# */ 7 | /* ##### Tasks ##### */ 8 | /* ################# */ 9 | module.exports = [ 10 | { 11 | name: "site", 12 | entry: { 13 | site: [`./${config.assetSrc}/js/site.js`] 14 | }, 15 | output: { 16 | filename: "[name].js", 17 | path: path.resolve(__dirname, `${config.assetDist}/js`) 18 | } 19 | }, 20 | { 21 | name: "serviceworker", 22 | entry: { 23 | serviceworker: [`./${config.src}/serviceworker.js`] 24 | }, 25 | output: { 26 | filename: "[name].js", 27 | path: path.resolve(__dirname, `${config.dist}`) 28 | }, 29 | plugins: [ 30 | new ReplaceHashInFileWebpackPlugin([ 31 | { 32 | dir: `${config.dist}`, 33 | files: ["serviceworker.js"], 34 | rules: [ 35 | { 36 | search: "[contenthash]", 37 | replace: "[hash]" 38 | } 39 | ] 40 | } 41 | ]) 42 | ] 43 | } 44 | ]; 45 | -------------------------------------------------------------------------------- /src/assets/js/site.js: -------------------------------------------------------------------------------- 1 | // ###### import ###### 2 | import setJsAvailability from "./components/setJsAvailability.js"; 3 | import notificationKeydown from "./components/notificationKeydown.js"; 4 | import handleTabs from "./components/handleTabsLocalStorage.js"; 5 | 6 | import stickyElement from "./functions/stickyElement.js"; 7 | import fixScrollPos from "./functions/fixScrollPos.js"; 8 | import handleTriggers from "./functions/handleTriggers.js"; 9 | 10 | // #################### 11 | // ##### settings ##### 12 | // #################### 13 | 14 | // ###### script ###### 15 | // is the DOM ready for manipulation? 16 | document.addEventListener("DOMContentLoaded", function() { 17 | // --- Toggle JS Availability 18 | setJsAvailability("body"); 19 | 20 | // handle tabs 21 | handleTabs(".js-tab-trigger", ".tabbed-content"); 22 | 23 | // handle triggers 24 | handleTriggers(".js-flyout-trigger", fixScrollPos); 25 | handleTriggers(".js-collapse-trigger", false); 26 | handleTriggers(".js-modal-trigger", fixScrollPos); 27 | 28 | // -- make elements sticky 29 | stickyElement("#application-footer", "#content", "padding-bottom"); 30 | 31 | // --- Show/hide notification 32 | notificationKeydown(".notification"); 33 | }); 34 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # Quelle: https://github.com/sapienza/vagrant-php-box 2 | 3 | # -*- mode: ruby -*- 4 | # vi: set ft=ruby : 5 | 6 | Vagrant.configure("2") do |config| 7 | # All Vagrant configuration is done here. The most common configuration 8 | # options are documented and commented below. For a complete reference, 9 | # please see the online documentation at vagrantup.com. 10 | 11 | # Every Vagrant virtual environment requires a box to build off of. 12 | config.vm.box = "ubuntu/trusty64" 13 | config.vm.post_up_message = "Box up and running (https://localhost:8080)" 14 | 15 | forward_port = ->(guest, host = guest) do 16 | config.vm.network :forwarded_port, 17 | guest: guest, 18 | host: host, 19 | auto_correct: true 20 | end 21 | 22 | # Sync between the web root of the VM and the 'sites' directory 23 | config.vm.synced_folder "./htdocs/", "/var/www/html" 24 | 25 | forward_port[1080] # mailcatcher 26 | forward_port[3306] # mysql 27 | forward_port[80, 8080] # nginx/apache 28 | 29 | config.vm.provision :puppet do |puppet| 30 | puppet.manifests_path = "src/manifests" 31 | puppet.manifest_file = "default.pp" 32 | end 33 | 34 | config.vm.network :private_network, ip: "33.33.33.10" 35 | end 36 | -------------------------------------------------------------------------------- /__tests__/e2e/sidebar.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | const sidebarSelector = "#bookmarks"; 4 | const collapseSelector = "#bookmarks details:first-of-type"; 5 | 6 | test("Sidebar", async ({ page }) => { 7 | // Go to http://localhost:8080/ 8 | await page.goto("/"); 9 | 10 | // Sidebar 11 | await page 12 | .locator(`#application-header button[data-target="${sidebarSelector}"]`) 13 | .click(); 14 | 15 | // Click on the sidebar to make sure it is stable 16 | await page.locator(`${sidebarSelector} .flyout-title`).click(); 17 | 18 | // take a screenshot 19 | test.slow(); // give time to fetch 20 | expect(await page.screenshot()).toMatchSnapshot("sidebar.png", { 21 | maxDiffPixels: 10 22 | }); 23 | 24 | // Open collapse 25 | await page.locator(`${collapseSelector}`).click(); 26 | expect(page.locator(`${collapseSelector} .list-vertical`)).toBeVisible; 27 | 28 | // close collapse 29 | await page.locator(`${collapseSelector} summary`).click(); 30 | expect(page.locator(`${collapseSelector} .list-vertical`)).toBeHidden; 31 | 32 | // close sidebar 33 | await page.locator(`${sidebarSelector} .flyout-close`).click(); 34 | expect(page.locator(`${sidebarSelector}`)).toBeHidden; 35 | }); 36 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_details-summary.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | // ###### switches ##### 4 | 5 | // ###### Settings ##### 6 | $details-border: 1px solid var(--border-color-hover) !default; 7 | $summary-marker: '+' !default; 8 | $summary-marker-size: 1.188em !default; /*-- 19px / 16px --*/ 9 | $summary-padding: math.div($base-spacing-size, 2) $summary-marker-size math.div($base-spacing-size, 2) 0 !default; 10 | 11 | //--------------------- 12 | // ###### Layout ###### 13 | //--------------------- 14 | details { 15 | border-top: $details-border; 16 | border-bottom: $details-border; 17 | 18 | & + details { 19 | border-top-width: 0; 20 | } 21 | } 22 | 23 | summary { 24 | cursor: pointer; 25 | display: block; 26 | font-size: inherit; 27 | margin: 0; 28 | padding: $summary-padding; 29 | position: relative; 30 | opacity: 1; 31 | 32 | &::before { 33 | content: $summary-marker; 34 | position: absolute; 35 | right: $summary-marker-size; 36 | display: inline-block; 37 | transition: .3s transform linear; 38 | } 39 | 40 | &::-webkit-details-marker { display: none } 41 | } 42 | 43 | // -- animate icon 44 | details[open] { 45 | summary { 46 | &::before { 47 | transform: rotate(45deg); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/scss/4-elements/_header.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $header-background-color: var(--bg) !default; 5 | $header-link-color: var(--color) !default; 6 | $header-link-active-bg: var(--brand-color) !default; 7 | $header-link-active-color: var(--color-light) !default; 8 | 9 | //--------------------- 10 | // ###### Layout ###### 11 | //--------------------- 12 | header { 13 | background-color: $header-background-color; 14 | color: $header-link-color; 15 | box-shadow: $box-shadow-default; 16 | position: relative; 17 | width: 100%; 18 | z-index: 2; 19 | 20 | @include default { 21 | padding: $base-spacing-size * 0.75 $base-spacing-size; 22 | a { 23 | padding: $base-spacing-size * 0.75 $base-spacing-size; 24 | } 25 | } 26 | 27 | @include phone-only { 28 | display: grid; 29 | grid-template-columns: repeat(2, 1fr); 30 | padding: $base-spacing-size * 2 $base-spacing-size * 1.5; 31 | a { 32 | padding: $base-spacing-size $base-spacing-size * 2; 33 | } 34 | } 35 | 36 | &.js-sticky { 37 | top: 0; 38 | } 39 | 40 | .collapse { 41 | border-width: 0; 42 | } 43 | 44 | // overwrite collapse-styles 45 | .collapse-main { 46 | overflow: visible; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /htdocs/maskIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_tile.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $main-link-border-color: var(--border-color) !default; 5 | $main-link-background-color: var(--bg) !default; 6 | $main-link-background-image: linear-gradient( 7 | to top, 8 | var(--bg), 9 | var(--border-color) 10 | ) !default; 11 | $main-link-background-image-hover: linear-gradient( 12 | to bottom, 13 | var(--bg), 14 | var(--border-color) 15 | ) !default; 16 | $main-link-active-border-color: var(--border-color-hover) !default; 17 | $main-link-description-bg: var(--border-color) !default; 18 | $main-link-description-color: var(--color) !default; 19 | $main-link-active-description-bg: var(--brand-color) !default; 20 | $main-link-active-description-color: var(--color-light) !default; 21 | 22 | //--------------------- 23 | // ###### Layout ###### 24 | //--------------------- 25 | 26 | .tile { 27 | display: block; 28 | opacity: 0.97; 29 | position: relative; // allow postioning of child-elements 30 | } 31 | 32 | // prevent whitespace on images 33 | .tile-image { 34 | display: inline-block; 35 | filter: var(--tile-filter); 36 | max-height: 100%; 37 | max-width: 100%; 38 | object-fit: contain; 39 | } 40 | 41 | // Clip text of Description if nessecary 42 | .tile-title { 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | white-space: nowrap; 46 | } 47 | -------------------------------------------------------------------------------- /htdocs/application.manifest.php: -------------------------------------------------------------------------------- 1 | IsFile() && $file != "'./' . $manifestUrl" && substr($file -> getFilename(), 0, 1) != ".") { 33 | // Replace spaces with %20 or it will break 34 | echo str_replace(' ', '%20', $file) . "\n"; 35 | // Add this file's hash to the $hashes string 36 | $hashes .= md5_file($file); 37 | } 38 | } 39 | } 40 | } 41 | create_manifest("."); 42 | 43 | // Write the $hashes string 44 | echo "# Hash: " . md5($hashes) . "\n"; 45 | -------------------------------------------------------------------------------- /src/assets/scss/6-components/_flyout.scss: -------------------------------------------------------------------------------- 1 | // ###### switches ##### 2 | 3 | // ###### Settings ##### 4 | $flyout-background-color: var(--bg) !default; 5 | $flyout-box-shadow: var(--box-shadow-heavy) !default; 6 | $flyout-color: var(--color) !default; 7 | //--------------------- 8 | // ###### Layout ###### 9 | //--------------------- 10 | .flyout { 11 | left: 0; 12 | position: fixed; 13 | top: 0; 14 | z-index: 3; 15 | } 16 | 17 | .flyout-backdrop { 18 | display: none; 19 | } 20 | 21 | .flyout-close { 22 | float: right; 23 | } 24 | 25 | .flyout-title { 26 | font-size: 1rem; 27 | font-weight: 600; 28 | margin-top: 0; 29 | padding-top: inherit; 30 | } 31 | 32 | .flyout-content { 33 | background-color: $flyout-background-color; 34 | box-shadow: $flyout-box-shadow; 35 | color: $flyout-color; 36 | height: 100vh; 37 | max-width: $bookmarks-max-width; 38 | overflow-y: auto; 39 | padding: $base-spacing-size $base-spacing-size * 1.5; 40 | position: fixed; 41 | right: 0; 42 | top: 0; 43 | transform: translateX(100%); 44 | transition: all $base-animation-speed ease; 45 | width: 100%; 46 | 47 | ul { 48 | @include ul-reset; 49 | } 50 | 51 | a { 52 | font-size: 0.875rem; /* 14px / 16px */ 53 | padding: 2px 0; 54 | } 55 | } 56 | 57 | // -- show flyout 58 | .js-is-active { 59 | .flyout-content { 60 | transform: translateX(0); 61 | } 62 | 63 | .flyout-backdrop { 64 | display: block; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /htdocs/serviceworker.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){e.exports=n(1)},function(e,t){var n=["./assets/css/site.css","./assets/js/site.js","./assets/wallpaper/default.jpg"];self.addEventListener("install",function(e){e.waitUntil(caches.open("4dd644c592e0090f2ea3").then(function(e){return e.addAll(n)}).then(function(){return self.skipWaiting()}))}),self.addEventListener("fetch",function(e){e.respondWith(caches.match(e.request).then(function(t){return t||("only-if-cached"!==e.request.cache||"same-origin"===e.request.mode?fetch(e.request):void 0)}))}),self.addEventListener("activate",function(e){e.waitUntil(self.clients.claim())})}]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserstartpage", 3 | "version": "1.0.0", 4 | "description": "Crossbrowser Speeddial and bookmarks", 5 | "main": "gulpfile.js", 6 | "devDependencies": { 7 | "@playwright/test": "^1.22.2", 8 | "del": "^3.0.0", 9 | "gulp": "^4.0.2", 10 | "gulp-autoprefixer": "^3.1.1", 11 | "gulp-if": "^3.0.0", 12 | "gulp-imagemin": "^4.1.0", 13 | "gulp-sass": "^5.1.0", 14 | "gulp-sourcemaps": "^1.9.1", 15 | "gulp-stylelint": "^8.0.0", 16 | "gulp-terser": "^1.2.0", 17 | "imagemin-gifsicle": "^7.0.0", 18 | "imagemin-jpegtran": "^7.0.0", 19 | "imagemin-optipng": "^8.0.0", 20 | "imagemin-svgo": "^10.0.1", 21 | "prettier-stylelint": "^0.4.2", 22 | "replace-hash-in-file-webpack-plugin": "^1.0.8", 23 | "require-dir": "^1.2.0", 24 | "sass": "^1.49.7", 25 | "stylelint": "^9.10.1", 26 | "stylelint-order": "^2.0.0", 27 | "webpack": "^4.39.3", 28 | "webpack-cli": "^4.6.0", 29 | "webpack-stream": "^4.0.3" 30 | }, 31 | "scripts": { 32 | "test": "npx playwright test", 33 | "test:record": "echo 'Test name' && read testname && npx playwright $testname localhost:8080", 34 | "test:report": "npx playwright show-report", 35 | "test:updateSnapshots": "npx playwright test --update-snapshots" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/saschadiercks/browserStartpage.git" 40 | }, 41 | "author": "Sascha Diercks", 42 | "license": "ISC", 43 | "bugs": { 44 | "url": "https://github.com/saschadiercks/browserStartpage/issues" 45 | }, 46 | "homepage": "https://github.com/saschadiercks/browserStartpage#readme" 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/scss/site.scss: -------------------------------------------------------------------------------- 1 | //----your overwrites: place files with variable overwrites here 2 | @import "_your-themes/variables"; 3 | 4 | // ---- Main Theme 5 | // 1 - Globals 6 | @import "1-settings/colors"; 7 | @import "1-settings/globals"; 8 | 9 | // 2 - Tools 10 | @import "2-tools/clearfix"; 11 | @import "2-tools/responsive"; 12 | @import "2-tools/ul-reset"; 13 | @import "2-tools/align-vertical"; 14 | @import "2-tools/align-centered"; 15 | @import "2-tools/content-visibility"; 16 | 17 | // 3 - Generic 18 | @import "3-generic/box-sizing"; 19 | @import "3-generic/font-smoothing"; 20 | @import "3-generic/page"; 21 | @import "3-generic/reset"; 22 | 23 | // 4 - Elements 24 | @import "4-elements/buttons"; 25 | @import "4-elements/details-summary"; 26 | @import "4-elements/header"; 27 | @import "4-elements/links"; 28 | @import "4-elements/main"; 29 | @import "4-elements/footer"; 30 | @import "4-elements/navigation"; 31 | 32 | // 5 - Objects 33 | @import "5-objects/list-tiles"; 34 | @import "5-objects/list-vertical"; 35 | @import "5-objects/list-horizontal"; 36 | @import "5-objects/tabbed-content"; 37 | 38 | // 6 - Components 39 | @import "6-components/backdrop"; 40 | @import "6-components/flyout"; 41 | @import "6-components/modal"; 42 | @import "6-components/modal-list"; 43 | @import "6-components/modal-qr"; 44 | @import "6-components/notification"; 45 | @import "6-components/overlay"; 46 | @import "6-components/tile"; 47 | @import "6-components/wallpaper"; 48 | 49 | // 7 - Utilities 50 | @import "7-utilities/sticky"; 51 | @import "7-utilities/js-utilities"; 52 | @import "7-utilities/theme-default"; 53 | 54 | // shame 55 | @import "shame"; 56 | 57 | // ---- Apply your own deeper layout changes 58 | @import "_your-themes/your-theme"; 59 | -------------------------------------------------------------------------------- /src/serviceworker.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = "[contenthash]"; 2 | var REQUIRED_FILES = [ 3 | "./assets/css/site.css", 4 | "./assets/js/site.js", 5 | "./assets/wallpaper/default.jpg" 6 | ]; 7 | 8 | self.addEventListener("install", function(event) { 9 | // Perform install step: loading each required file into cache 10 | event.waitUntil( 11 | caches 12 | .open(CACHE_NAME) 13 | .then(function(cache) { 14 | // Add all offline dependencies to the cache 15 | return cache.addAll(REQUIRED_FILES); 16 | }) 17 | .then(function() { 18 | // At this point everything has been cached 19 | return self.skipWaiting(); 20 | }) 21 | ); 22 | }); 23 | 24 | self.addEventListener("fetch", function(event) { 25 | event.respondWith( 26 | caches.match(event.request).then(function(response) { 27 | // Cache hit - return the response from the cached version 28 | if (response) { 29 | return response; 30 | } 31 | 32 | // DevTools opening will trigger these o-i-c requests, which this SW can't handle. 33 | // There's probaly more going on here, but I'd rather just ignore this problem. :) 34 | // https://github.com/paulirish/caltrainschedule.io/issues/49 35 | if ( 36 | event.request.cache === "only-if-cached" && 37 | event.request.mode !== "same-origin" 38 | ) 39 | return; 40 | 41 | // Not in cache - return the result from the live server 42 | // `fetch` is essentially a "fallback" 43 | return fetch(event.request); 44 | }) 45 | ); 46 | }); 47 | 48 | self.addEventListener("activate", function(event) { 49 | // Calling claim() to force a "controllerchange" event on navigator.serviceWorker 50 | event.waitUntil(self.clients.claim()); 51 | }); 52 | -------------------------------------------------------------------------------- /htdocs/config/config.php: -------------------------------------------------------------------------------- 1 | 0) { 20 | handleTabs(document.location.hash); 21 | 22 | // overwrite scroll-position of hash 23 | window.scrollTo(0, 0); 24 | } else { 25 | 26 | // -- check local storage 27 | var value__localStorage = localStorage.getItem(key__localStorage); 28 | if (value__localStorage !== null) { 29 | handleTabs(value__localStorage); 30 | } else { 31 | // wihtout localStorage we'll just show the first tab 32 | document.querySelector(selectorTrigger).classList.add(class__isActive); 33 | document.querySelector(selectorContent).classList.add(class__isActive); 34 | } 35 | } 36 | 37 | // -- listen for click on triggers and show/hide content 38 | tabTrigger.forEach(function (element) { 39 | element.addEventListener('click', function (event) { 40 | event.preventDefault(); 41 | 42 | var triggerTarget = this.getAttribute('data-target'); 43 | 44 | // now handle tabs 45 | handleTabs(triggerTarget); 46 | 47 | // save to local storage 48 | saveToLocalStorage(triggerTarget); 49 | 50 | // update hash in URL to allow easy copy/paste 51 | history.pushState(null, null, triggerTarget); 52 | }); 53 | }); 54 | 55 | // -- open tab and mark button as active 56 | function handleTabs(selector) { 57 | 58 | // hide all tabs and remove active class from buttons 59 | removeClass(tabTrigger, class__isActive); 60 | removeClass(tabContent, class__isActive); 61 | 62 | // open saved tab 63 | addClass(findAll(selector), class__isActive); 64 | 65 | // add active class to button 66 | addClass(findAll('a[data-target="' + selector + '"]'), class__isActive); 67 | } 68 | 69 | // -- save to localStorage 70 | function saveToLocalStorage(selector) { 71 | // save to local storage, when key is not pressed 72 | if (event.altKey !== true) { 73 | localStorage.setItem(key__localStorage, selector); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/manifests/default.pp: -------------------------------------------------------------------------------- 1 | # Puppet configurations 2 | 3 | Exec { path => [ "/bin/", "/sbin/" , "/usr/bin/", "/usr/sbin/" ] } 4 | 5 | class base { 6 | 7 | ## Update apt-get ## 8 | exec { 'apt-get update': 9 | command => '/usr/bin/apt-get update' 10 | } 11 | } 12 | 13 | class http { 14 | 15 | define apache::loadmodule () { 16 | exec { "/usr/sbin/a2enmod $name" : 17 | unless => "/bin/readlink -e /etc/apache2/mods-enabled/${name}.load", 18 | notify => Service[apache2] 19 | } 20 | } 21 | 22 | apache::loadmodule{"rewrite":} 23 | 24 | package { "apache2": 25 | ensure => present, 26 | } 27 | 28 | service { "apache2": 29 | ensure => running, 30 | require => Package["apache2"], 31 | } 32 | } 33 | 34 | class php{ 35 | 36 | package { "php5": 37 | ensure => present, 38 | } 39 | 40 | package { "php5-cli": 41 | ensure => present, 42 | } 43 | 44 | package { "php5-xdebug": 45 | ensure => present, 46 | } 47 | 48 | package { "php5-mysql": 49 | ensure => present, 50 | } 51 | 52 | package { "php5-imagick": 53 | ensure => present, 54 | } 55 | 56 | package { "php5-mcrypt": 57 | ensure => present, 58 | } 59 | 60 | package { "php-pear": 61 | ensure => present, 62 | } 63 | 64 | package { "php5-dev": 65 | ensure => present, 66 | } 67 | 68 | package { "php5-curl": 69 | ensure => present, 70 | } 71 | 72 | package { "php5-sqlite": 73 | ensure => present, 74 | } 75 | 76 | package { "libapache2-mod-php5": 77 | ensure => present, 78 | } 79 | 80 | } 81 | 82 | class mysql{ 83 | 84 | package { "mysql-server": 85 | ensure => present, 86 | } 87 | 88 | service { "mysql": 89 | ensure => running, 90 | require => Package["mysql-server"], 91 | notify => Exec["set-mysql-password"], 92 | } 93 | 94 | exec { "set-mysql-password": 95 | command => "mysqladmin -u root password root", 96 | } 97 | } 98 | 99 | class phpmyadmin{ 100 | 101 | package 102 | { 103 | "phpmyadmin": 104 | ensure => present, 105 | require => [ 106 | Exec['apt-get update'], 107 | Package["php5", "php5-mysql", "apache2"], 108 | ] 109 | } 110 | 111 | file 112 | { 113 | "/etc/apache2/conf-available/phpmyadmin.conf": 114 | ensure => link, 115 | target => "/etc/phpmyadmin/apache.conf", 116 | require => Package['apache2'], 117 | notify => Exec["load_phpmyadmin_conf"], 118 | } 119 | 120 | exec { "load_phpmyadmin_conf": 121 | command => "/usr/sbin/a2enconf phpmyadmin", 122 | notify => Exec["reload_apache"], 123 | } 124 | exec { "reload_apache": 125 | command => "/etc/init.d/apache2 reload", 126 | } 127 | } 128 | 129 | include base 130 | include http 131 | include php 132 | include mysql 133 | include phpmyadmin 134 | -------------------------------------------------------------------------------- /htdocs/assets/js/site.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(o,r,function(e){return t[e]}.bind(null,r));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){t.exports=n(1)},function(t,e,n){"use strict";function o(t){var e=document.querySelectorAll(t);return Array.prototype.slice.call(e)}function r(t,e){t.forEach(function(t){t.classList.add(e)})}function c(t,e){t.forEach(function(t){t.classList.remove(e)})}n.r(e);const i="js",a="no-js";const u="js-hidden",s="js-visible";const l="sdi-is-active",f="lastTabID";function d(t){return document.querySelector(t)}const p="js-sticky";const y=document.querySelector("body"),v="sdi-is-fixed";var g;function b(t){var e=window.pageYOffset;y.classList.contains(v)?(y.classList.remove(v),y.style.top="",window.scrollTo(0,g)):(y.classList.add(v),y.style.top="-"+e+"px",g=e)}const m="js-is-active";function h(t,e){let n=o(t);Array.prototype.slice.call(n).forEach(function(t){t.addEventListener("click",function(){let t=o(this.getAttribute("data-target"));var n;n=m,t.forEach(function(t){t.classList.toggle(n)}),"function"==typeof e&&e(),event.preventDefault()})})}document.addEventListener("DOMContentLoaded",function(){var t,e,n,y,v,g;(t=o(t="body")).forEach(function(){r(t,i),c(t,a)}),function(t,e){var n=o(t),i=o(e);if(document.location.hash&&o('a[data-target="'+document.location.hash+'"]').length>0)u(document.location.hash),window.scrollTo(0,0);else{var a=localStorage.getItem(f);null!==a?u(a):(document.querySelector(t).classList.add(l),document.querySelector(e).classList.add(l))}function u(t){c(n,l),c(i,l),r(o(t),l),r(o('a[data-target="'+t+'"]'),l)}function s(t){!0!==event.altKey&&localStorage.setItem(f,t)}n.forEach(function(t){t.addEventListener("click",function(t){t.preventDefault();var e=this.getAttribute("data-target");u(e),s(e),history.pushState(null,null,e)})})}(".js-tab-trigger",".tabbed-content"),h(".js-flyout-trigger",b),h(".js-modal-trigger",b),n="#content",y="padding-bottom",v=o(e="#application-footer"),g=d(e).clientHeight+"px",r(v,p),d(n).style.setProperty(y,g),function(t){var e=o(t);document.addEventListener("keydown",function(){e.forEach(function(){r(e,s),c(e,u)})}),document.addEventListener("keyup",function(){e.forEach(function(){r(e,u),c(e,s)})})}(".notification")})}]); -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | import { devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | const config: PlaywrightTestConfig = { 14 | testDir: './__tests__', 15 | /* Maximum time one test can run for. */ 16 | timeout: 30 * 1000, 17 | expect: { 18 | /** 19 | * Maximum time expect() should wait for the condition to be met. 20 | * For example in `await expect(locator).toHaveText();` 21 | */ 22 | timeout: 5000 23 | }, 24 | /* Run tests in files in parallel */ 25 | fullyParallel: true, 26 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 27 | forbidOnly: !!process.env.CI, 28 | /* Retry on CI only */ 29 | retries: process.env.CI ? 2 : 0, 30 | /* Opt out of parallel tests on CI. */ 31 | workers: process.env.CI ? 1 : undefined, 32 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 33 | reporter: 'html', 34 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 35 | use: { 36 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 37 | actionTimeout: 0, 38 | /* Base URL to use in actions like `await page.goto('/')`. */ 39 | baseURL: 'http://localhost:8080', 40 | 41 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 42 | trace: 'on-first-retry', 43 | }, 44 | 45 | /* Configure projects for major browsers */ 46 | projects: [ 47 | { 48 | name: 'chromium', 49 | use: { 50 | ...devices['Desktop Chrome'], 51 | }, 52 | }, 53 | 54 | { 55 | name: 'firefox', 56 | use: { 57 | ...devices['Desktop Firefox'], 58 | }, 59 | }, 60 | 61 | // { 62 | // name: 'webkit', 63 | // use: { 64 | // ...devices['Desktop Safari'], 65 | // }, 66 | // }, 67 | 68 | /* Test against mobile viewports. */ 69 | // { 70 | // name: 'Mobile Chrome', 71 | // use: { 72 | // ...devices['Pixel 5'], 73 | // }, 74 | // }, 75 | { 76 | name: 'Mobile Safari', 77 | use: { 78 | ...devices['iPhone 12'], 79 | }, 80 | }, 81 | 82 | /* Test against branded browsers. */ 83 | // { 84 | // name: 'Microsoft Edge', 85 | // use: { 86 | // channel: 'msedge', 87 | // }, 88 | // }, 89 | // { 90 | // name: 'Google Chrome', 91 | // use: { 92 | // channel: 'chrome', 93 | // }, 94 | // }, 95 | ], 96 | 97 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 98 | outputDir: '__test-results__/', 99 | 100 | /* Run your local dev server before starting the tests */ 101 | // webServer: { 102 | // command: 'npm run start', 103 | // port: 3000, 104 | // }, 105 | }; 106 | 107 | export default config; 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browserStartpage 2 | 3 | Are you switching browsers often? 4 | Are you tired, that every browser uses it's own speeddial and you can't import that in your new browser? 5 | The solution: host your own speeddial with this repo! 6 | This is especially useful if you need/want to share a of bunch links with friends or colleagues. 7 | 8 | ## How does it work? 9 | 10 | Just place the folder `/htdocs` on your own webserver. Make sure it supports _php_ - this is the only requirement. 11 | 12 | ### Want a demo? 13 | 14 | [https://demo.saschadiercks.de/startpage/](https://demo.saschadiercks.de/startpage/) 15 | 16 | Read more about it here (in german): [https://saschadiercks.de/projekte/browserstartpage/](https://saschadiercks.de/projekte/browserstartpage/) 17 | 18 | ![Screenshot](/.screenshots/startpage-macbook-iphone.jpg) 19 | 20 | ### Setup your own links 21 | 22 | The browserStartpage comes with a default list of links, to show you how it works. It shows up with a list of popular browsers and some development-ressources. You change that. Just head over to `/htdocs/data/data.json` and play with that file. You can edit the tabs and links to your own liking. Just play with it - it's quite self explanatory. All you need to do is to create images for your links and place them on your server too. Usually here `/htdocs/assets/thumbnails`. 23 | 24 | You can setup the usage of serviceworkers or define your own wallpaper in the json (on the top of the file). 25 | 26 | ## New 27 | 28 | Add a hash to the url to open tabs via direct call like so: `yourUrl#tab-1` 29 | You can just click on the desired tab and copy the url. 30 | 31 | ## Features 32 | 33 | - call tabs via hash 34 | - easy configurable Speeddial via json 35 | - easily add bookmarks via json 36 | - only requires php on your server 37 | - uses vanillaJS 38 | - uses apllicationCache to minimize traffic (it even works offline, after first visit) 39 | - uses localStorage to store last opened tab 40 | 41 | ### Planned Features 42 | 43 | - allow theming (see Hints & Tips) 44 | - allow onsite-editing so you don't have to fiddle with the json-file 45 | - allow static export of content to sync via Dropbox, iCloud or wathever 46 | 47 | ## Further insights (want to help building this?) 48 | 49 | - `/src/manifests` Docker is used as a local development-environment 50 | - `/src/scss` the development files to build the CSS (via gulp) 51 | - `/src/js` the development JS to compile the JS (via gulp) 52 | - `/src/data` dummy-datafile. Use `/htdocs/data/data.json` for local development 53 | - `/htdocs/startpage.manifest.php` automatic generation of application cache 54 | - `/htdocs/index.php`the speeddial itself 55 | - `/htdocs/assets/css` compiled css-files (uesd live) 56 | - `/htdocs/assets/js` compiled js-files (uesd live) 57 | - `/htdocs/assets/thumbnails` store your link-images here 58 | 59 | ### Usage of docker (preferred) 60 | 61 | 1. install docker on your machine (https://docs.docker.com/get-docker/) 62 | 2. head to the local repository and run `docker-compose up` 63 | 3. Wait a while until all components are loaded an the box is running. (The first start can take a while) 64 | 4. visit (http://127.0.0.1:8080/) 65 | 66 | ### Usage of Vagrant 67 | 68 | 1. install vagrant on your machine (https://www.vagrantup.com/) 69 | 2. install Virtualbox (https://www.virtualbox.org/wiki/Downloads) 70 | 3. head to your local repository an enter `vagrant up` 71 | 4. Wait a while until all components are loaded an the box is running. (The first start can take a while) 72 | 5. visit (http://127.0.0.1:8080/) 73 | 74 | ### Usage of gulp 75 | 76 | 1. Make sure, you have node.js installed on your computer (https://nodejs.org/en/) 77 | 2. run `npm install gulp-cli -g` to install gulp 78 | 3. run `npm install` to install gulp in your project 79 | 4. use `gulp build` to compile the css and minify Javascript for production (without sourcemaps) and imagemin 80 | 5. use `gulp update` to compile the css and Javascript for development 81 | 6. use `gulp serve` start the server for local development (localhost:8080) 82 | 7. use `gulp stop` stop the server 83 | 8. use `gulp reboot` restart the server and build assets 84 | 85 | Always run `gulp build`before deploying assets 86 | 87 | ### Hints & Tips 88 | 89 | - Change the Wallpaper by changing the value of variable `wallpaper` in `/data/data.json` (at the top of the document) 90 | - Do you want every link to be opened in a new tab? Change the value of `linktarget` in /data/data.json to a desired value. e.g. `_blank` 91 | - if you want to change the look of the page, you can find more information in `src/scss/7-utilities/your-theme.scss` 92 | -------------------------------------------------------------------------------- /src/assets/scss/7-utilities/_theme-default.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | // ---- Elements ---- 4 | .modal-overlay { 5 | @include align-centered($scale: 0, $position: fixed); 6 | background-color: $modal-content-background-color; 7 | border-radius: $modal-border-radius; 8 | box-shadow: $box-shadow-heavy; 9 | color: $modal-content-color; 10 | height: $modal-content-height; 11 | max-height: $modal-content-max-height; 12 | max-width: $modal-content-max-width; 13 | transition: transform $base-animation-speed ease-in-out; 14 | width: $modal-content-width; 15 | } 16 | 17 | .js-is-active .modal-overlay { 18 | @include align-centered($scale: 1, $position: fixed); 19 | } 20 | 21 | .modal-header { 22 | background-color: $modal-header-background-color; 23 | border-bottom: $modal-header-border; 24 | color: $modal-header-color; 25 | margin-bottom: $base-spacing-size-px; 26 | padding: $base-spacing-size-px * 2; 27 | } 28 | 29 | .modal-content { 30 | padding: $base-spacing-size-px; 31 | } 32 | 33 | .overlay { 34 | height: 100vh; 35 | width: 100%; 36 | } 37 | 38 | .overlay-content { 39 | background-color: $overlay-background-color; 40 | box-shadow: $box-shadow-heavy; 41 | color: $overlay-color; 42 | } 43 | 44 | .js-hidden { 45 | .backdrop { 46 | height: 0; 47 | opacity: 0; 48 | } 49 | 50 | .overlay-content { 51 | transform: translateX(100%); 52 | } 53 | } 54 | 55 | .js-visible { 56 | .backdrop { 57 | opacity: 1; 58 | } 59 | 60 | .overlay-content { 61 | transform: translateX(0); 62 | } 63 | } 64 | 65 | // ---- Content ---- 66 | // fallback for older browsers 67 | .tile-container { 68 | display: flex; 69 | height: 100px; 70 | padding: 0; 71 | position: relative; 72 | width: 25%; 73 | } 74 | 75 | @media (min-width: 768px) { 76 | .tile-container { 77 | height: 156px; 78 | } 79 | } 80 | 81 | // modern browsers 82 | @supports (display: grid) { 83 | .tile-container { 84 | width: auto; 85 | } 86 | } 87 | 88 | .tile { 89 | background-clip: padding-box; 90 | background-image: $main-link-background-image; 91 | border: 1px solid $main-link-border-color; 92 | border-radius: 2px; 93 | box-shadow: $box-shadow-default; 94 | display: flex; 95 | font-size: 0.75rem; 96 | position: relative; 97 | text-align: center; 98 | text-decoration: none; 99 | width: 100%; 100 | 101 | &:hover { 102 | background-image: $main-link-background-image-hover; 103 | border-color: $main-link-active-border-color; 104 | z-index: 2; 105 | } 106 | } 107 | 108 | .tile-title { 109 | background-color: $main-link-description-bg; 110 | bottom: 0; 111 | color: $main-link-description-color; 112 | display: block; 113 | left: 0; 114 | padding: math.div($base-spacing-size, 2); 115 | position: absolute; 116 | right: 0; 117 | } 118 | 119 | .tile__button { 120 | color: $tile-button-color; 121 | left: 1px; 122 | padding: $tile-button-padding; 123 | position: absolute; 124 | top: 1px; 125 | z-index: 2; 126 | } 127 | 128 | // center images when parent-item has display: flex; 129 | .tile-image { 130 | margin: auto; 131 | } 132 | 133 | @media (max-width: 767px) { 134 | .tile-image { 135 | max-height: 50%; 136 | max-width: 50%; 137 | } 138 | } 139 | 140 | // ---- Notification ---- 141 | .notification { 142 | background-color: $notification-background-color; 143 | bottom: $base-spacing-size * 3; 144 | box-shadow: $box-shadow-heavy; 145 | color: $notification-color; 146 | padding: $base-spacing-size; 147 | position: fixed; 148 | right: 0; 149 | z-index: 99; 150 | } 151 | 152 | // -- navigation 153 | nav { 154 | li { 155 | display: inline-block; 156 | } 157 | 158 | a { 159 | color: $header-link-color; 160 | 161 | &:hover, 162 | &.sdi-is-active { 163 | background-color: $header-link-active-bg; 164 | border-radius: 2px; 165 | color: $header-link-active-color; 166 | } 167 | } 168 | } 169 | 170 | // ---- fx when overlays are visible ---- 171 | .js-fx { 172 | header, 173 | main, 174 | footer { 175 | filter: blur($background-blur-setting); 176 | } 177 | } 178 | 179 | // ---- Responsive ---- 180 | // Mobile view 181 | @include phone-only { 182 | button { 183 | padding: $base-spacing-size $base-spacing-size * 2; 184 | } 185 | 186 | .overlay-content { 187 | padding: $base-spacing-size * 2 $base-spacing-size * 1.5; 188 | } 189 | 190 | .overlay-title { 191 | padding-bottom: $base-spacing-size; 192 | padding-top: $base-spacing-size; 193 | } 194 | 195 | #bookmarks-toggle { 196 | justify-self: end; 197 | } 198 | 199 | #header-nav-toggle { 200 | justify-self: start; 201 | } 202 | 203 | #header-nav { 204 | grid-column: 1 / span 2; 205 | overflow: hidden; 206 | transition: max-height 0.5s ease; 207 | 208 | &.js-opened { 209 | max-height: 100vh; 210 | } 211 | 212 | li { 213 | display: block; 214 | } 215 | } 216 | } 217 | 218 | // Default View 219 | @include default { 220 | button { 221 | padding: $base-spacing-size * 0.5 $base-spacing-size; 222 | } 223 | 224 | .overlay-content { 225 | padding: $base-spacing-size * 0.75 $base-spacing-size; 226 | } 227 | 228 | #bookmarks-toggle { 229 | float: right; 230 | } 231 | 232 | #application-header { 233 | .collapse-main { 234 | max-height: none; 235 | } 236 | } 237 | 238 | .overlay-title { 239 | padding-bottom: $base-spacing-size * 0.75; 240 | padding-top: $base-spacing-size * 0.75; 241 | } 242 | 243 | .tile-title { 244 | opacity: 0; 245 | transition: opacity $base-animation-speed; 246 | } 247 | 248 | .tile:hover { 249 | .tile-title { 250 | opacity: 1; 251 | } 252 | } 253 | 254 | button[data-target*="nav"] { 255 | display: none; 256 | } 257 | } 258 | 259 | // ---- Fallbacks / special cases ---- 260 | // $no-js-separator-color: var(--border-color) !default; 261 | // separation of content if JS is disabled / not loaded 262 | // body.no-js main nav:not(:last-child) { 263 | // border-bottom: 1px dotted $no-js-separator-color; 264 | // margin-bottom: $base-spacing-size*2; 265 | // padding-bottom: $base-spacing-size*2; 266 | // } 267 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["stylelint-order"], 3 | "rules": { 4 | "order/properties-alphabetical-order": true, 5 | 6 | "color-no-invalid-hex": true, 7 | "font-family-no-duplicate-names": true, 8 | "font-family-no-missing-generic-family-keyword": true, 9 | "function-calc-no-invalid": true, 10 | "function-calc-no-unspaced-operator": true, 11 | "function-linear-gradient-no-nonstandard-direction": true, 12 | "string-no-newline": true, 13 | "unit-no-unknown": true, 14 | "property-no-unknown": null, 15 | "keyframe-declaration-no-important": true, 16 | "declaration-block-no-duplicate-properties": [ 17 | true, 18 | { "ignore": ["consecutive-duplicates-with-different-values"] } 19 | ], 20 | "declaration-block-no-shorthand-property-overrides": true, 21 | "block-no-empty": true, 22 | "selector-pseudo-class-no-unknown": true, 23 | "selector-pseudo-element-no-unknown": true, 24 | "selector-type-no-unknown": true, 25 | "at-rule-no-unknown": [ 26 | true, 27 | { "ignoreAtRules": ["mixin", "each", "include", "if", "extend", "else", "use"] } 28 | ], 29 | "comment-no-empty": true, 30 | "no-descending-specificity": true, 31 | "no-duplicate-at-import-rules": true, 32 | "no-duplicate-selectors": true, 33 | "no-empty-source": true, 34 | "no-extra-semicolons": true, 35 | "no-invalid-double-slash-comments": true, 36 | 37 | "color-named": "never", 38 | "color-no-hex": null, 39 | "function-blacklist": [""], 40 | "function-url-no-scheme-relative": true, 41 | "function-url-scheme-blacklist": ["ftp", "http", "file", "data"], 42 | "function-url-scheme-whitelist": ["https"], 43 | "keyframes-name-pattern": null, 44 | "number-max-precision": 3, 45 | "time-min-milliseconds": 200, 46 | "unit-blacklist": ["in", "pc"], 47 | "shorthand-property-no-redundant-values": true, 48 | "value-no-vendor-prefix": [ 49 | true, 50 | { "ignoreValues": ["font-smoothing", "text-size-adjust"] } 51 | ], 52 | "custom-property-pattern": null, 53 | "property-blacklist": null, 54 | "property-no-vendor-prefix": [ 55 | true, 56 | { "ignoreProperties": ["text-size-adjust"] } 57 | ], 58 | "property-whitelist": null, 59 | "declaration-block-no-redundant-longhand-properties": [ 60 | true, 61 | { "ignoreShorthands": ["grid-row"] } 62 | ], 63 | "declaration-no-important": null, 64 | "declaration-property-unit-blacklist": null, 65 | "declaration-property-unit-whitelist": null, 66 | "declaration-property-value-blacklist": null, 67 | "declaration-property-value-whitelist": null, 68 | "declaration-block-single-line-max-declarations": 1, 69 | "selector-attribute-operator-blacklist": null, 70 | "selector-attribute-operator-whitelist": null, 71 | "selector-class-pattern": null, 72 | "selector-combinator-blacklist": [">"], 73 | "selector-combinator-whitelist": null, 74 | "selector-id-pattern": null, 75 | "selector-max-attribute": 1, 76 | "selector-max-class": 3, 77 | "selector-max-combinators": 1, 78 | "selector-max-compound-selectors": 2, 79 | "selector-max-empty-lines": 0, 80 | "selector-max-id": 1, 81 | "selector-max-pseudo-class": 2, 82 | "selector-max-specificity": "1,2,1", 83 | "selector-max-type": 2, 84 | "selector-max-universal": 1, 85 | "selector-nested-pattern": null, 86 | "selector-no-qualifying-type": [true, { "ignore": ["attribute", "class"] }], 87 | "selector-no-vendor-prefix": true, 88 | "selector-pseudo-class-blacklist": null, 89 | "selector-pseudo-class-whitelist": null, 90 | "selector-pseudo-element-blacklist": null, 91 | "selector-pseudo-element-whitelist": null, 92 | "media-feature-name-blacklist": null, 93 | "media-feature-name-no-vendor-prefix": true, 94 | "media-feature-name-value-whitelist": null, 95 | "media-feature-name-whitelist": [ 96 | "min-width", 97 | "max-width", 98 | "orientation", 99 | "prefers-color-scheme" 100 | ], 101 | "custom-media-pattern": null, 102 | "at-rule-blacklist": null, 103 | "at-rule-no-vendor-prefix": true, 104 | "at-rule-whitelist": null, 105 | "comment-word-blacklist": null, 106 | "max-nesting-depth": 3, 107 | 108 | "color-hex-case": "lower", 109 | "color-hex-length": "short", 110 | "font-family-name-quotes": "always-where-recommended", 111 | "font-weight-notation": "named-where-possible", 112 | "function-comma-newline-after": "never-multi-line", 113 | "function-comma-newline-before": "never-multi-line", 114 | "function-comma-space-after": "always-single-line", 115 | "function-comma-space-before": "never", 116 | "function-max-empty-lines": 0, 117 | "function-name-case": "lower", 118 | "function-parentheses-newline-inside": "never-multi-line", 119 | "function-parentheses-space-inside": "never", 120 | "function-whitespace-after": "always", 121 | "function-url-quotes": "always", 122 | "number-leading-zero": "never", 123 | "number-no-trailing-zeros": true, 124 | "string-quotes": "double", 125 | "length-zero-no-unit": true, 126 | "unit-case": "lower", 127 | "value-keyword-case": "lower", 128 | "value-list-comma-newline-after": "always-multi-line", 129 | "value-list-comma-newline-before": null, 130 | "value-list-comma-space-after": "always-single-line", 131 | "value-list-comma-space-before": "never", 132 | "value-list-max-empty-lines": 0, 133 | "custom-property-empty-line-before": [ 134 | "always", 135 | { 136 | "except": ["after-custom-property", "first-nested"], 137 | "ignore": ["after-comment", "inside-single-line-block"] 138 | } 139 | ], 140 | "property-case": "lower", 141 | "declaration-bang-space-after": "never", 142 | "declaration-bang-space-before": "always", 143 | "declaration-colon-newline-after": "always-multi-line", 144 | "declaration-colon-space-after": "always-single-line", 145 | "declaration-colon-space-before": "never", 146 | "declaration-empty-line-before": "never", 147 | "declaration-block-semicolon-newline-after": "always-multi-line", 148 | "declaration-block-semicolon-newline-before": null, 149 | "declaration-block-semicolon-space-after": "always-single-line", 150 | "declaration-block-semicolon-space-before": "never", 151 | "declaration-block-trailing-semicolon": "always", 152 | "block-closing-brace-empty-line-before": "never", 153 | "block-closing-brace-newline-after": "always", 154 | "block-closing-brace-newline-before": "always-multi-line", 155 | "block-closing-brace-space-after": null, 156 | "block-closing-brace-space-before": "always-single-line", 157 | "block-opening-brace-newline-after": "always-multi-line", 158 | "block-opening-brace-newline-before": null, 159 | "block-opening-brace-space-after": "always-single-line", 160 | "block-opening-brace-space-before": "always", 161 | "selector-attribute-brackets-space-inside": "never", 162 | "selector-attribute-operator-space-after": "never", 163 | "selector-attribute-operator-space-before": "never", 164 | "selector-attribute-quotes": "always", 165 | "selector-combinator-space-after": "always", 166 | "selector-combinator-space-before": "always", 167 | "selector-descendant-combinator-no-non-space": true, 168 | "selector-pseudo-class-case": "lower", 169 | "selector-pseudo-class-parentheses-space-inside": "never", 170 | "selector-pseudo-element-case": "lower", 171 | "selector-pseudo-element-colon-notation": "double", 172 | "selector-type-case": "lower", 173 | "selector-list-comma-newline-after": "always-multi-line", 174 | "selector-list-comma-newline-before": null, 175 | "selector-list-comma-space-after": "always-single-line", 176 | "selector-list-comma-space-before": "never", 177 | "rule-empty-line-before": [ 178 | "always-multi-line", 179 | { 180 | "except": ["first-nested"], 181 | "ignore": ["after-comment"] 182 | } 183 | ], 184 | "media-feature-colon-space-after": "always", 185 | "media-feature-colon-space-before": "never", 186 | "media-feature-name-case": "lower", 187 | "media-feature-parentheses-space-inside": "never", 188 | "media-feature-range-operator-space-after": "always", 189 | "media-feature-range-operator-space-before": "always", 190 | "media-query-list-comma-newline-after": "always-multi-line", 191 | "media-query-list-comma-newline-before": null, 192 | "media-query-list-comma-space-after": "always-single-line", 193 | "media-query-list-comma-space-before": "never", 194 | "at-rule-empty-line-before": [ 195 | "always", 196 | { 197 | "except": ["blockless-after-same-name-blockless", "first-nested"], 198 | "ignore": ["after-comment"] 199 | } 200 | ], 201 | "at-rule-name-case": "lower", 202 | "at-rule-name-newline-after": null, 203 | "at-rule-name-space-after": "always", 204 | "at-rule-semicolon-newline-after": "always", 205 | "at-rule-semicolon-space-before": "never", 206 | "comment-empty-line-before": [ 207 | "always", 208 | { 209 | "except": ["first-nested"], 210 | "ignore": ["stylelint-commands"] 211 | } 212 | ], 213 | "comment-whitespace-inside": "always", 214 | 215 | "indentation": 2, 216 | "linebreaks": "unix", 217 | "max-empty-lines": 1, 218 | "max-line-length": null, 219 | "no-eol-whitespace": true, 220 | "no-missing-end-of-source-newline": true, 221 | "no-empty-first-line": true 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /htdocs/assets/css/site.css: -------------------------------------------------------------------------------- 1 | html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html{--bg: #fdfdfd;--color: #333;--color-light: #fff;--backdrop-color: rgba(8, 34, 63, 0.9);--box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);--box-shadow-heavy: 0 2px 4px rgba(0, 0, 0, 0.2);--brand-color: #007acc;--border-color: #f5f5f5;--border-color-hover: #ddd;--tile-filter: none;--wallpaper-filter: none}@media(prefers-color-scheme: dark){html{--bg: #18191d;--color: #d2d0cc;--color-light: #fff;--backdrop-color: rgba(0, 0, 0, 0.9);--box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);--box-shadow-heavy: 0 2px 4px rgba(0, 0, 0, 0.2);--brand-color: #ff1448;--border-color: #313131;--border-color-hover: #333;--tile-filter: grayscale(1) contrast(1.5);--wallpaper-filter: grayscale(1) opacity(0.1)}}body{background-color:var(--bg);font-family:-apple-system,sans-serif;width:100%}html{font-size:100%;height:100%;overflow-x:hidden;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0;padding:0}img{height:auto;max-width:100%}header,main,footer{display:block}button{background-color:transparent;border-width:0;color:inherit;cursor:pointer;font-size:inherit}details{border-top:1px solid var(--border-color-hover);border-bottom:1px solid var(--border-color-hover)}details+details{border-top-width:0}summary{cursor:pointer;display:block;font-size:inherit;margin:0;padding:.5vw 1.188em .5vw 0;position:relative;opacity:1}summary::before{content:"+";position:absolute;right:1.188em;display:inline-block;-webkit-transition:.3s transform linear;transition:.3s transform linear}summary::-webkit-details-marker{display:none}details[open] summary::before{-webkit-transform:rotate(45deg);transform:rotate(45deg)}header{background-color:var(--bg);color:var(--color);box-shadow:0 1px 1px rgba(0,0,0,.2);position:relative;width:100%;z-index:2}@media(min-width: 671px){header{padding:.75vw 1vw}header a{padding:.75vw 1vw}}@media(max-width: 670px){header{display:-ms-grid;display:grid;-ms-grid-columns:(1fr)[2];grid-template-columns:repeat(2, 1fr);padding:2vw 1.5vw}header a{padding:1vw 2vw}}header.js-sticky{top:0}header .collapse{border-width:0}header .collapse-main{overflow:visible}a{color:inherit}main{left:0;margin:1vw;position:relative;right:0;z-index:1}footer{background-color:var(--brand-color);color:var(--color-light);font-size:.75rem;padding-left:1vw;padding-right:1vw;width:100%;z-index:1}footer::after{clear:both;content:"";display:table}footer.js-sticky{bottom:0}footer a{color:inherit;display:inline-block;padding-bottom:9px;padding-top:9px}footer .description{float:left}footer .social-profiles{float:right}nav ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:0}nav ul::after{clear:both;content:"";display:table}nav a{display:block;text-decoration:none}.list-tiles{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:0;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-left:auto;margin-right:auto}@supports((display: -ms-grid) or (display: grid)){.list-tiles{display:-ms-grid;display:grid;grid-gap:1vw;-ms-grid-columns:(minmax(160px, 1fr))[auto-fit];grid-template-columns:repeat(auto-fit, minmax(160px, 1fr))}@media(min-width: 768px){.list-tiles{-ms-grid-columns:(minmax(256px, 1fr))[auto-fit];grid-template-columns:repeat(auto-fit, minmax(256px, 1fr))}}}.list-vertical{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:0;padding-bottom:.75vw}.list-vertical__link{display:block;text-decoration:none}.list-vertical__link:hover{text-decoration:underline}.list-horizontal{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:0}.list-horizontal li{display:inline-block}.tabbed-content{contain-intrinsic-size:1px 2000px;content-visibility:auto}.js .tabbed-content:not(.sdi-is-active){display:none}.backdrop{background-color:var(--backdrop-color);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);height:100vh;position:absolute;top:0;-webkit-transition:opacity 0s ease;transition:opacity 0s ease;width:100vw;z-index:-1}.flyout{left:0;position:fixed;top:0;z-index:3}.flyout-backdrop{display:none}.flyout-close{float:right}.flyout-title{font-size:1rem;font-weight:600;margin-top:0;padding-top:inherit}.flyout-content{background-color:var(--bg);box-shadow:var(--box-shadow-heavy);color:var(--color);height:100vh;max-width:400px;overflow-y:auto;padding:1vw 1.5vw;position:fixed;right:0;top:0;-webkit-transform:translateX(100%);transform:translateX(100%);-webkit-transition:all .2s ease;transition:all .2s ease;width:100%}.flyout-content ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:0}.flyout-content a{font-size:.875rem;padding:2px 0}.js-is-active .flyout-content{-webkit-transform:translateX(0);transform:translateX(0)}.js-is-active .flyout-backdrop{display:block}.js .modal{-webkit-transition:visibility .2s ease-in-out;transition:visibility .2s ease-in-out;visibility:hidden}.modal.js-is-active{position:fixed;top:0;visibility:visible;z-index:2}.modal-overlay{-webkit-transform:translate(-50%, -50%) scale(0);transform:translate(-50%, -50%) scale(0);left:50%;position:fixed;top:50%}.modal-header{font-weight:600;position:relative}.modal-content{overflow-y:auto}.modal-header__close{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);right:0}.modal-list{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:0}.modal-list__item{display:inline-block;width:49%}@supports((display: -ms-grid) or (display: grid)){.modal-list{display:-ms-grid;display:grid;-ms-grid-columns:50% auto;grid-template-columns:50% auto}.modal-list__item{width:100%}}.modal-list__link{display:block;padding:9px;text-decoration:none}.modal-list__link:hover{background-color:var(--border-color)}.modal-content--qr{text-align:center}.modal-content--qr img{mix-blend-mode:multiply}.notification{-webkit-transition:all .2s;transition:all .2s}.notification.js-hidden{-webkit-transform:translateX(100%);transform:translateX(100%)}.notification.js-visible{right:1vw;-webkit-transform:translateX(0%);transform:translateX(0%)}.overlay{display:block;position:fixed;z-index:3}.overlay-close{float:right}.overlay-title{font-size:1rem;font-weight:600;margin-top:0;padding-top:inherit}.overlay-content{height:100vh;overflow-y:auto;-webkit-transition:all .2s ease;transition:all .2s ease;width:100%}.tile{display:block;opacity:.97;position:relative}.tile-image{display:inline-block;-webkit-filter:var(--tile-filter);filter:var(--tile-filter);max-height:100%;max-width:100%;object-fit:contain}.tile-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#wallpaper{background-position:center center;background-size:cover;bottom:0;left:0;position:fixed;right:0;top:0;-webkit-filter:var(--wallpaper-filter);filter:var(--wallpaper-filter)}.sdi-sticky{left:0;position:-webkit-sticky;position:sticky;top:0}.sdi-is-fixed{position:fixed}.js-sticky{position:fixed}.modal-overlay{-webkit-transform:translate(-50%, -50%) scale(0);transform:translate(-50%, -50%) scale(0);left:50%;position:fixed;top:50%;background-color:var(--bg);border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,.2);color:var(--color);height:auto;max-height:400px;max-width:500px;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out, -webkit-transform .2s ease-in-out;width:90%}.js-is-active .modal-overlay{-webkit-transform:translate(-50%, -50%) scale(1);transform:translate(-50%, -50%) scale(1);left:50%;position:fixed;top:50%}.modal-header{background-color:transparent;border-bottom:2px solid var(--brand-color);color:var(--color);margin-bottom:9px;padding:18px}.modal-content{padding:9px}.overlay{height:100vh;width:100%}.overlay-content{background-color:var(--bg);box-shadow:0 2px 4px rgba(0,0,0,.2);color:var(--color)}.js-hidden .backdrop{height:0;opacity:0}.js-hidden .overlay-content{-webkit-transform:translateX(100%);transform:translateX(100%)}.js-visible .backdrop{opacity:1}.js-visible .overlay-content{-webkit-transform:translateX(0);transform:translateX(0)}.tile-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:100px;padding:0;position:relative;width:25%}@media(min-width: 768px){.tile-container{height:156px}}@supports((display: -ms-grid) or (display: grid)){.tile-container{width:auto}}.tile{background-clip:padding-box;background-image:-webkit-linear-gradient(bottom, var(--bg), var(--border-color));background-image:linear-gradient(to top, var(--bg), var(--border-color));border:1px solid var(--border-color);border-radius:2px;box-shadow:0 1px 1px rgba(0,0,0,.2);display:-webkit-box;display:-ms-flexbox;display:flex;font-size:.75rem;position:relative;text-align:center;text-decoration:none;width:100%}.tile:hover{background-image:-webkit-linear-gradient(top, var(--bg), var(--border-color));background-image:linear-gradient(to bottom, var(--bg), var(--border-color));border-color:var(--border-color-hover);z-index:2}.tile-title{background-color:var(--border-color);bottom:0;color:var(--color);display:block;left:0;padding:.5vw;position:absolute;right:0}.tile__button{color:inherit;left:1px;padding:.5vw;position:absolute;top:1px;z-index:2}.tile-image{margin:auto}@media(max-width: 767px){.tile-image{max-height:50%;max-width:50%}}.notification{background-color:var(--bg);bottom:3vw;box-shadow:0 2px 4px rgba(0,0,0,.2);color:var(--color);padding:1vw;position:fixed;right:0;z-index:99}nav li{display:inline-block}nav a{color:var(--color)}nav a:hover,nav a.sdi-is-active{background-color:var(--brand-color);border-radius:2px;color:var(--color-light)}.js-fx header,.js-fx main,.js-fx footer{-webkit-filter:blur(1px);filter:blur(1px)}@media(max-width: 670px){button{padding:1vw 2vw}.overlay-content{padding:2vw 1.5vw}.overlay-title{padding-bottom:1vw;padding-top:1vw}#bookmarks-toggle{justify-self:end}#header-nav-toggle{justify-self:start}#header-nav{-ms-grid-column-span:2;-ms-grid-column:1;grid-column:1/span 2;overflow:hidden;-webkit-transition:max-height .5s ease;transition:max-height .5s ease}#header-nav.js-opened{max-height:100vh}#header-nav li{display:block}}@media(min-width: 671px){button{padding:.5vw 1vw}.overlay-content{padding:.75vw 1vw}#bookmarks-toggle{float:right}#application-header .collapse-main{max-height:none}.overlay-title{padding-bottom:.75vw;padding-top:.75vw}.tile-title{opacity:0;-webkit-transition:opacity .2s;transition:opacity .2s}.tile:hover .tile-title{opacity:1}button[data-target*=nav]{display:none}}#notification{display:block !important}.overlay{display:block !important} -------------------------------------------------------------------------------- /htdocs/index.php: -------------------------------------------------------------------------------- 1 | 0) { 25 | echo ''; 26 | } 27 | } 28 | 29 | function returnImage($url, $alt, $class) 30 | { 31 | list($width, $height, $type, $atr) = getimagesize($url); 32 | return '' . $alt . ''; 33 | } 34 | 35 | // define linktarget if isset and filled otherwise use default 36 | if (empty($linktarget)) { 37 | $linktarget = "_self"; 38 | } 39 | ?> 40 | 41 | 42 | > 48 | 49 | 50 | 51 | ' . $projectTitle . '' : FALSE; 53 | echo isset($projectDescription) ? '' : FALSE; 54 | echo isset($projectKeywords) ? '' : FALSE; 55 | echo isset($projectLanguage) ? '' : FALSE; 56 | ?> 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
93 | 0) { ?> 94 | 95 | 96 | 97 | 0) { ?> 98 | 99 | 100 | 101 | 0) { ?> 102 | 115 | 116 |
117 | 118 | 119 |
120 | 121 | 122 |
123 | 142 |
143 | 144 |
145 | 146 | 147 | 0) { ?> 148 |
149 |
150 | 151 |

Bookmarks

152 | $contentItems) : ?> 153 |
154 | 155 |
    156 | 157 |
  • 158 | 159 |
  • 160 | 161 |
162 |
163 | 164 |
165 |
166 |
167 | 168 | 169 | 170 | 171 | 172 | 0) { ?> 173 | 191 | 192 | 193 | 205 | 206 | 207 | 208 | 209 | 210 | 224 | 225 |
226 | press [alt] to open a tab and prevent remembering it 227 |
228 | 229 | 230 |
231 | 232 | 233 | 234 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /tests/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, type Page } from '@playwright/test'; 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto('https://demo.playwright.dev/todomvc'); 5 | }); 6 | 7 | const TODO_ITEMS = [ 8 | 'buy some cheese', 9 | 'feed the cat', 10 | 'book a doctors appointment' 11 | ]; 12 | 13 | test.describe('New Todo', () => { 14 | test('should allow me to add todo items', async ({ page }) => { 15 | // Create 1st todo. 16 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 17 | await page.locator('.new-todo').press('Enter'); 18 | 19 | // Make sure the list only has one todo item. 20 | await expect(page.locator('.view label')).toHaveText([ 21 | TODO_ITEMS[0] 22 | ]); 23 | 24 | // Create 2nd todo. 25 | await page.locator('.new-todo').fill(TODO_ITEMS[1]); 26 | await page.locator('.new-todo').press('Enter'); 27 | 28 | // Make sure the list now has two todo items. 29 | await expect(page.locator('.view label')).toHaveText([ 30 | TODO_ITEMS[0], 31 | TODO_ITEMS[1] 32 | ]); 33 | 34 | await checkNumberOfTodosInLocalStorage(page, 2); 35 | }); 36 | 37 | test('should clear text input field when an item is added', async ({ page }) => { 38 | // Create one todo item. 39 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 40 | await page.locator('.new-todo').press('Enter'); 41 | 42 | // Check that input is empty. 43 | await expect(page.locator('.new-todo')).toBeEmpty(); 44 | await checkNumberOfTodosInLocalStorage(page, 1); 45 | }); 46 | 47 | test('should append new items to the bottom of the list', async ({ page }) => { 48 | // Create 3 items. 49 | await createDefaultTodos(page); 50 | 51 | // Check test using different methods. 52 | await expect(page.locator('.todo-count')).toHaveText('3 items left'); 53 | await expect(page.locator('.todo-count')).toContainText('3'); 54 | await expect(page.locator('.todo-count')).toHaveText(/3/); 55 | 56 | // Check all items in one call. 57 | await expect(page.locator('.view label')).toHaveText(TODO_ITEMS); 58 | await checkNumberOfTodosInLocalStorage(page, 3); 59 | }); 60 | 61 | test('should show #main and #footer when items added', async ({ page }) => { 62 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 63 | await page.locator('.new-todo').press('Enter'); 64 | 65 | await expect(page.locator('.main')).toBeVisible(); 66 | await expect(page.locator('.footer')).toBeVisible(); 67 | await checkNumberOfTodosInLocalStorage(page, 1); 68 | }); 69 | }); 70 | 71 | test.describe('Mark all as completed', () => { 72 | test.beforeEach(async ({ page }) => { 73 | await createDefaultTodos(page); 74 | await checkNumberOfTodosInLocalStorage(page, 3); 75 | }); 76 | 77 | test.afterEach(async ({ page }) => { 78 | await checkNumberOfTodosInLocalStorage(page, 3); 79 | }); 80 | 81 | test('should allow me to mark all items as completed', async ({ page }) => { 82 | // Complete all todos. 83 | await page.locator('.toggle-all').check(); 84 | 85 | // Ensure all todos have 'completed' class. 86 | await expect(page.locator('.todo-list li')).toHaveClass(['completed', 'completed', 'completed']); 87 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 88 | }); 89 | 90 | test('should allow me to clear the complete state of all items', async ({ page }) => { 91 | // Check and then immediately uncheck. 92 | await page.locator('.toggle-all').check(); 93 | await page.locator('.toggle-all').uncheck(); 94 | 95 | // Should be no completed classes. 96 | await expect(page.locator('.todo-list li')).toHaveClass(['', '', '']); 97 | }); 98 | 99 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { 100 | const toggleAll = page.locator('.toggle-all'); 101 | await toggleAll.check(); 102 | await expect(toggleAll).toBeChecked(); 103 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 104 | 105 | // Uncheck first todo. 106 | const firstTodo = page.locator('.todo-list li').nth(0); 107 | await firstTodo.locator('.toggle').uncheck(); 108 | 109 | // Reuse toggleAll locator and make sure its not checked. 110 | await expect(toggleAll).not.toBeChecked(); 111 | 112 | await firstTodo.locator('.toggle').check(); 113 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 114 | 115 | // Assert the toggle all is checked again. 116 | await expect(toggleAll).toBeChecked(); 117 | }); 118 | }); 119 | 120 | test.describe('Item', () => { 121 | 122 | test('should allow me to mark items as complete', async ({ page }) => { 123 | // Create two items. 124 | for (const item of TODO_ITEMS.slice(0, 2)) { 125 | await page.locator('.new-todo').fill(item); 126 | await page.locator('.new-todo').press('Enter'); 127 | } 128 | 129 | // Check first item. 130 | const firstTodo = page.locator('.todo-list li').nth(0); 131 | await firstTodo.locator('.toggle').check(); 132 | await expect(firstTodo).toHaveClass('completed'); 133 | 134 | // Check second item. 135 | const secondTodo = page.locator('.todo-list li').nth(1); 136 | await expect(secondTodo).not.toHaveClass('completed'); 137 | await secondTodo.locator('.toggle').check(); 138 | 139 | // Assert completed class. 140 | await expect(firstTodo).toHaveClass('completed'); 141 | await expect(secondTodo).toHaveClass('completed'); 142 | }); 143 | 144 | test('should allow me to un-mark items as complete', async ({ page }) => { 145 | // Create two items. 146 | for (const item of TODO_ITEMS.slice(0, 2)) { 147 | await page.locator('.new-todo').fill(item); 148 | await page.locator('.new-todo').press('Enter'); 149 | } 150 | 151 | const firstTodo = page.locator('.todo-list li').nth(0); 152 | const secondTodo = page.locator('.todo-list li').nth(1); 153 | await firstTodo.locator('.toggle').check(); 154 | await expect(firstTodo).toHaveClass('completed'); 155 | await expect(secondTodo).not.toHaveClass('completed'); 156 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 157 | 158 | await firstTodo.locator('.toggle').uncheck(); 159 | await expect(firstTodo).not.toHaveClass('completed'); 160 | await expect(secondTodo).not.toHaveClass('completed'); 161 | await checkNumberOfCompletedTodosInLocalStorage(page, 0); 162 | }); 163 | 164 | test('should allow me to edit an item', async ({ page }) => { 165 | await createDefaultTodos(page); 166 | 167 | const todoItems = page.locator('.todo-list li'); 168 | const secondTodo = todoItems.nth(1); 169 | await secondTodo.dblclick(); 170 | await expect(secondTodo.locator('.edit')).toHaveValue(TODO_ITEMS[1]); 171 | await secondTodo.locator('.edit').fill('buy some sausages'); 172 | await secondTodo.locator('.edit').press('Enter'); 173 | 174 | // Explicitly assert the new text value. 175 | await expect(todoItems).toHaveText([ 176 | TODO_ITEMS[0], 177 | 'buy some sausages', 178 | TODO_ITEMS[2] 179 | ]); 180 | await checkTodosInLocalStorage(page, 'buy some sausages'); 181 | }); 182 | }); 183 | 184 | test.describe('Editing', () => { 185 | test.beforeEach(async ({ page }) => { 186 | await createDefaultTodos(page); 187 | await checkNumberOfTodosInLocalStorage(page, 3); 188 | }); 189 | 190 | test('should hide other controls when editing', async ({ page }) => { 191 | const todoItem = page.locator('.todo-list li').nth(1); 192 | await todoItem.dblclick(); 193 | await expect(todoItem.locator('.toggle')).not.toBeVisible(); 194 | await expect(todoItem.locator('label')).not.toBeVisible(); 195 | await checkNumberOfTodosInLocalStorage(page, 3); 196 | }); 197 | 198 | test('should save edits on blur', async ({ page }) => { 199 | const todoItems = page.locator('.todo-list li'); 200 | await todoItems.nth(1).dblclick(); 201 | await todoItems.nth(1).locator('.edit').fill('buy some sausages'); 202 | await todoItems.nth(1).locator('.edit').dispatchEvent('blur'); 203 | 204 | await expect(todoItems).toHaveText([ 205 | TODO_ITEMS[0], 206 | 'buy some sausages', 207 | TODO_ITEMS[2], 208 | ]); 209 | await checkTodosInLocalStorage(page, 'buy some sausages'); 210 | }); 211 | 212 | test('should trim entered text', async ({ page }) => { 213 | const todoItems = page.locator('.todo-list li'); 214 | await todoItems.nth(1).dblclick(); 215 | await todoItems.nth(1).locator('.edit').fill(' buy some sausages '); 216 | await todoItems.nth(1).locator('.edit').press('Enter'); 217 | 218 | await expect(todoItems).toHaveText([ 219 | TODO_ITEMS[0], 220 | 'buy some sausages', 221 | TODO_ITEMS[2], 222 | ]); 223 | await checkTodosInLocalStorage(page, 'buy some sausages'); 224 | }); 225 | 226 | test('should remove the item if an empty text string was entered', async ({ page }) => { 227 | const todoItems = page.locator('.todo-list li'); 228 | await todoItems.nth(1).dblclick(); 229 | await todoItems.nth(1).locator('.edit').fill(''); 230 | await todoItems.nth(1).locator('.edit').press('Enter'); 231 | 232 | await expect(todoItems).toHaveText([ 233 | TODO_ITEMS[0], 234 | TODO_ITEMS[2], 235 | ]); 236 | }); 237 | 238 | test('should cancel edits on escape', async ({ page }) => { 239 | const todoItems = page.locator('.todo-list li'); 240 | await todoItems.nth(1).dblclick(); 241 | await todoItems.nth(1).locator('.edit').press('Escape'); 242 | await expect(todoItems).toHaveText(TODO_ITEMS); 243 | }); 244 | }); 245 | 246 | test.describe('Counter', () => { 247 | test('should display the current number of todo items', async ({ page }) => { 248 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 249 | await page.locator('.new-todo').press('Enter'); 250 | await expect(page.locator('.todo-count')).toContainText('1'); 251 | 252 | await page.locator('.new-todo').fill(TODO_ITEMS[1]); 253 | await page.locator('.new-todo').press('Enter'); 254 | await expect(page.locator('.todo-count')).toContainText('2'); 255 | 256 | await checkNumberOfTodosInLocalStorage(page, 2); 257 | }); 258 | }); 259 | 260 | test.describe('Clear completed button', () => { 261 | test.beforeEach(async ({ page }) => { 262 | await createDefaultTodos(page); 263 | }); 264 | 265 | test('should display the correct text', async ({ page }) => { 266 | await page.locator('.todo-list li .toggle').first().check(); 267 | await expect(page.locator('.clear-completed')).toHaveText('Clear completed'); 268 | }); 269 | 270 | test('should remove completed items when clicked', async ({ page }) => { 271 | const todoItems = page.locator('.todo-list li'); 272 | await todoItems.nth(1).locator('.toggle').check(); 273 | await page.locator('.clear-completed').click(); 274 | await expect(todoItems).toHaveCount(2); 275 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 276 | }); 277 | 278 | test('should be hidden when there are no items that are completed', async ({ page }) => { 279 | await page.locator('.todo-list li .toggle').first().check(); 280 | await page.locator('.clear-completed').click(); 281 | await expect(page.locator('.clear-completed')).toBeHidden(); 282 | }); 283 | }); 284 | 285 | test.describe('Persistence', () => { 286 | test('should persist its data', async ({ page }) => { 287 | for (const item of TODO_ITEMS.slice(0, 2)) { 288 | await page.locator('.new-todo').fill(item); 289 | await page.locator('.new-todo').press('Enter'); 290 | } 291 | 292 | const todoItems = page.locator('.todo-list li'); 293 | await todoItems.nth(0).locator('.toggle').check(); 294 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 295 | await expect(todoItems).toHaveClass(['completed', '']); 296 | 297 | // Ensure there is 1 completed item. 298 | checkNumberOfCompletedTodosInLocalStorage(page, 1); 299 | 300 | // Now reload. 301 | await page.reload(); 302 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 303 | await expect(todoItems).toHaveClass(['completed', '']); 304 | }); 305 | }); 306 | 307 | test.describe('Routing', () => { 308 | test.beforeEach(async ({ page }) => { 309 | await createDefaultTodos(page); 310 | // make sure the app had a chance to save updated todos in storage 311 | // before navigating to a new view, otherwise the items can get lost :( 312 | // in some frameworks like Durandal 313 | await checkTodosInLocalStorage(page, TODO_ITEMS[0]); 314 | }); 315 | 316 | test('should allow me to display active items', async ({ page }) => { 317 | await page.locator('.todo-list li .toggle').nth(1).check(); 318 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 319 | await page.locator('.filters >> text=Active').click(); 320 | await expect(page.locator('.todo-list li')).toHaveCount(2); 321 | await expect(page.locator('.todo-list li')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 322 | }); 323 | 324 | test('should respect the back button', async ({ page }) => { 325 | await page.locator('.todo-list li .toggle').nth(1).check(); 326 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 327 | 328 | await test.step('Showing all items', async () => { 329 | await page.locator('.filters >> text=All').click(); 330 | await expect(page.locator('.todo-list li')).toHaveCount(3); 331 | }); 332 | 333 | await test.step('Showing active items', async () => { 334 | await page.locator('.filters >> text=Active').click(); 335 | }); 336 | 337 | await test.step('Showing completed items', async () => { 338 | await page.locator('.filters >> text=Completed').click(); 339 | }); 340 | 341 | await expect(page.locator('.todo-list li')).toHaveCount(1); 342 | await page.goBack(); 343 | await expect(page.locator('.todo-list li')).toHaveCount(2); 344 | await page.goBack(); 345 | await expect(page.locator('.todo-list li')).toHaveCount(3); 346 | }); 347 | 348 | test('should allow me to display completed items', async ({ page }) => { 349 | await page.locator('.todo-list li .toggle').nth(1).check(); 350 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 351 | await page.locator('.filters >> text=Completed').click(); 352 | await expect(page.locator('.todo-list li')).toHaveCount(1); 353 | }); 354 | 355 | test('should allow me to display all items', async ({ page }) => { 356 | await page.locator('.todo-list li .toggle').nth(1).check(); 357 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 358 | await page.locator('.filters >> text=Active').click(); 359 | await page.locator('.filters >> text=Completed').click(); 360 | await page.locator('.filters >> text=All').click(); 361 | await expect(page.locator('.todo-list li')).toHaveCount(3); 362 | }); 363 | 364 | test('should highlight the currently applied filter', async ({ page }) => { 365 | await expect(page.locator('.filters >> text=All')).toHaveClass('selected'); 366 | await page.locator('.filters >> text=Active').click(); 367 | // Page change - active items. 368 | await expect(page.locator('.filters >> text=Active')).toHaveClass('selected'); 369 | await page.locator('.filters >> text=Completed').click(); 370 | // Page change - completed items. 371 | await expect(page.locator('.filters >> text=Completed')).toHaveClass('selected'); 372 | }); 373 | }); 374 | 375 | async function createDefaultTodos(page: Page) { 376 | for (const item of TODO_ITEMS) { 377 | await page.locator('.new-todo').fill(item); 378 | await page.locator('.new-todo').press('Enter'); 379 | } 380 | } 381 | 382 | async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { 383 | return await page.waitForFunction(e => { 384 | return JSON.parse(localStorage['react-todos']).length === e; 385 | }, expected); 386 | } 387 | 388 | async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { 389 | return await page.waitForFunction(e => { 390 | return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; 391 | }, expected); 392 | } 393 | 394 | async function checkTodosInLocalStorage(page: Page, title: string) { 395 | return await page.waitForFunction(t => { 396 | return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); 397 | }, title); 398 | } 399 | -------------------------------------------------------------------------------- /htdocs/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "linktarget": "_self", 3 | "useServiceWorker": true, 4 | "wallpaper": "assets/wallpaper/default.jpg", 5 | "wallpaperPreload": false, 6 | "content": { 7 | "Browser": [ 8 | { 9 | "url": "https://github.com/saschadiercks/browserStartpage", 10 | "title": "Fork me on github", 11 | "image": "assets/thumbnails/forkme.png", 12 | "imageQr": "assets/qr-codes/github-project.png" 13 | }, 14 | { 15 | "url": "https://github.com/saschadiercks/browserStartpage", 16 | "title": "Links as modal", 17 | "image": "assets/thumbnails/modal.png", 18 | "modal": [ 19 | { 20 | "url": "https://www.google.de/chrome/browser/desktop/", 21 | "title": "Chrome" 22 | }, 23 | { 24 | "url": "https://www.mozilla.org/firefox", 25 | "title": "Firefox" 26 | }, 27 | { 28 | "url": "https://www.mozilla.org/de/firefox/developer/", 29 | "title": "Firefox (Dev)" 30 | }, 31 | { 32 | "url": "https://developer.apple.com/safari/technology-preview/", 33 | "title": "Safari (TP)" 34 | }, 35 | { 36 | "url": "https://vivaldi.com", 37 | "title": "Vivaldi" 38 | }, 39 | { 40 | "url": "https://blisk.io/", 41 | "title": "Blisk" 42 | }, 43 | { 44 | "url": "https://developer.microsoft.com/en-us/microsoft-edge/tools/", 45 | "title": "Modern IE" 46 | } 47 | ] 48 | }, 49 | { 50 | "url": "https://www.google.de/chrome/browser/desktop/", 51 | "title": "Chrome", 52 | "image": "assets/thumbnails/chrome.png" 53 | }, 54 | { 55 | "url": "https://www.mozilla.org/firefox", 56 | "title": "Firefox", 57 | "image": "assets/thumbnails/firefox.png" 58 | }, 59 | { 60 | "url": "https://www.mozilla.org/de/firefox/developer/", 61 | "title": "Firefox (Dev)", 62 | "image": "assets/thumbnails/firefox-dev.png" 63 | }, 64 | { 65 | "url": "https://developer.apple.com/safari/technology-preview/", 66 | "title": "Safari (TP)", 67 | "image": "assets/thumbnails/safari-tp.png" 68 | }, 69 | { 70 | "url": "https://vivaldi.com", 71 | "title": "Vivaldi", 72 | "image": "assets/thumbnails/vivaldi.png" 73 | }, 74 | { 75 | "url": "https://blisk.io/", 76 | "title": "Blisk", 77 | "image": "assets/thumbnails/blisk.png" 78 | }, 79 | { 80 | "url": "https://developer.microsoft.com/en-us/microsoft-edge/tools/", 81 | "title": "Modern IE", 82 | "image": "assets/thumbnails/modern-ie.png" 83 | } 84 | ], 85 | "Dev": [ 86 | { 87 | "url": "https://schema.org/docs/schemas.html", 88 | "title": "Schema.org", 89 | "image": "assets/thumbnails/schema.png" 90 | }, 91 | { 92 | "url": "https://html.spec.whatwg.org/multipage/forms.html#autofill", 93 | "title": "Autocomplete", 94 | "image": "assets/thumbnails/autocomplete.png" 95 | }, 96 | { 97 | "url": "https://microformats.org/wiki/hcard-input-formats", 98 | "title": "Microformats", 99 | "image": "assets/thumbnails/microformats.png" 100 | }, 101 | { 102 | "url": "https://github.com/", 103 | "title": "Github", 104 | "image": "assets/thumbnails/github.png" 105 | }, 106 | { 107 | "url": "https://bitbucket.org/", 108 | "title": "Bitbucket", 109 | "image": "assets/thumbnails/bitbucket.png" 110 | }, 111 | { 112 | "url": "https://validator.w3.org/nu/", 113 | "title": "HTML-Validator", 114 | "image": "assets/thumbnails/html-validator.png" 115 | }, 116 | { 117 | "url": "https://developers.google.com/structured-data/testing-tool/", 118 | "title": "RichSnippet-Testing", 119 | "image": "assets/thumbnails/rich-snippet.png" 120 | }, 121 | { 122 | "url": "https://developers.google.com/speed/pagespeed/insights/?hl=de", 123 | "title": "Google Pagespeeds", 124 | "image": "assets/thumbnails/pagespeed.png" 125 | }, 126 | { 127 | "url": "https://search.google.com/test/mobile-friendly", 128 | "title": "Google Mobiltest", 129 | "image": "assets/thumbnails/mobile-friendly.png" 130 | }, 131 | { 132 | "url": "https://www.google.com/webmasters/tools/", 133 | "title": "Google Webmaster-Tools", 134 | "image": "assets/thumbnails/webmastertools.png" 135 | }, 136 | { 137 | "url": "https://analytics.google.com/", 138 | "title": "Analytics", 139 | "image": "assets/thumbnails/analytics.png" 140 | }, 141 | { 142 | "url": "https://github.com/dypsilon/frontend-dev-bookmarks", 143 | "title": "Frontend Bookmarks", 144 | "image": "assets/thumbnails/frontend-bookmarks.png" 145 | }, 146 | { 147 | "url": "https://developer.mozilla.org/de/", 148 | "title": "Mozilla (dev)", 149 | "image": "assets/thumbnails/mozilla-dev.png" 150 | }, 151 | { 152 | "url": "https://developers.google.com/web/?hl=de", 153 | "title": "Google (dev)", 154 | "image": "assets/thumbnails/google-dev.png" 155 | }, 156 | { 157 | "url": "https://codepen.io/patterns/", 158 | "title": "Codepen patterns", 159 | "image": "assets/thumbnails/codepen-patterns.png" 160 | }, 161 | { 162 | "url": "https://browserl.ist/", 163 | "title": "Browserlist", 164 | "image": "assets/thumbnails/browserlist.png" 165 | }, 166 | { 167 | "url": "https://www.codecademy.com", 168 | "title": "Codecademy", 169 | "image": "assets/thumbnails/codecademy.png" 170 | }, 171 | { 172 | "url": "https://tools.pingdom.com/fpt/", 173 | "title": "Pingdom", 174 | "image": "assets/thumbnails/pingdom.png" 175 | }, 176 | { 177 | "url": "https://dequeuniversity.com/library/", 178 | "title": "Accessibility Library", 179 | "image": "assets/thumbnails/accessibility.png" 180 | }, 181 | { 182 | "url": "https://pixabay.com/de/", 183 | "title": "Pixabay", 184 | "image": "assets/thumbnails/pixabay.png" 185 | }, 186 | { 187 | "url": "https://npms.io/search?term=hyperterm", 188 | "title": "npms.io", 189 | "image": "assets/thumbnails/npms-io.png" 190 | } 191 | ], 192 | "Design": [ 193 | { 194 | "url": "https://www.microsoft.com/en-us/design", 195 | "image": "assets/thumbnails/design-microsoft.png", 196 | "title": "Microsoft Design" 197 | }, 198 | { 199 | "url": "https://design.google/resources/", 200 | "image": "assets/thumbnails/design-google.png", 201 | "title": "Google Design" 202 | }, 203 | { 204 | "url": "https://material.io/", 205 | "image": "assets/thumbnails/design-material.png", 206 | "title": "Material Design" 207 | }, 208 | { 209 | "url": "https://design.firefox.com/", 210 | "image": "assets/thumbnails/design-firefox.png", 211 | "title": "Firefox Design" 212 | }, 213 | { 214 | "url": "https://facebook.design", 215 | "image": "assets/thumbnails/design-facebook.png", 216 | "title": "Facebook Design" 217 | }, 218 | { 219 | "url": "https://design.trello.com/", 220 | "image": "assets/thumbnails/trello.png", 221 | "title": "Trello Design" 222 | }, 223 | { 224 | "url": "https://github.com/showcases/design-essentials", 225 | "image": "assets/thumbnails/github.png", 226 | "title": "Github Design Essentials" 227 | }, 228 | { 229 | "url": "https://www.uber.design", 230 | "image": "assets/thumbnails/design-uber.png", 231 | "title": "Uber design" 232 | }, 233 | { 234 | "url": "https://airbnb.design", 235 | "image": "assets/thumbnails/design-airbnb.png", 236 | "title": "airbnb Design" 237 | }, 238 | { 239 | "url": "https://atlassian.design", 240 | "image": "assets/thumbnails/design-atlassian.png", 241 | "title": "Atlassian Design" 242 | }, 243 | 244 | { 245 | "url": "https://unsplash.com/search/photos/macbook", 246 | "image": "assets/thumbnails/design-unsplash.png", 247 | "title": "Unsplash" 248 | }, 249 | { 250 | "url": "https://magicmockups.com/", 251 | "image": "assets/thumbnails/magic-mockups.png", 252 | "title": "Magic Mockups" 253 | }, 254 | { 255 | "url": "https://smartmockups.com/", 256 | "image": "assets/thumbnails/smartmockups.png", 257 | "title": "Smart Mockups" 258 | }, 259 | { 260 | "url": "https://www.designbetter.co", 261 | "image": "assets/thumbnails/design-invision.png", 262 | "title": "Design better" 263 | } 264 | ], 265 | "SDI": [ 266 | { 267 | "url": "https://www.saschadiercks.de/", 268 | "image": "assets/thumbnails/sdi-homepage.png", 269 | "imageQr": "assets/qr-codes/metafolio-de.png", 270 | "title": "About me" 271 | }, 272 | { 273 | "url": "https://design.saschadiercks.de/", 274 | "image": "assets/thumbnails/sdi-blog.png", 275 | "imageQr": "assets/qr-codes/design-system.png", 276 | "title": "SDI Design System" 277 | }, 278 | { 279 | "url": "https://demo.saschadiercks.de/personalnews/", 280 | "image": "assets/thumbnails/sdi-personalnews.png", 281 | "imageQr": "assets/qr-codes/personalnews-landing.png", 282 | "title": "personalNews" 283 | }, 284 | { 285 | "url": "https://demo.saschadiercks.de/little-helpers/", 286 | "image": "assets/thumbnails/sdi-little-helpers.png", 287 | "imageQr": "assets/qr-codes/little-helpers.png", 288 | "title": "Little Helpers" 289 | }, 290 | { 291 | "url": "https://demo.saschadiercks.de/startpage/", 292 | "image": "assets/thumbnails/sdi-startpage.png", 293 | "imageQr": "assets/qr-codes/startpage-demo.png", 294 | "title": "Browser Startpage (this)" 295 | } 296 | ] 297 | }, 298 | "bookmarks": { 299 | "Frontend": [ 300 | { 301 | "url": "https://github.com/dypsilon/frontend-dev-bookmarks", 302 | "title": "Frontend Dev Bookmarks" 303 | }, 304 | { 305 | "url": "https://github.com/thedaviddias/Front-End-Checklist", 306 | "title": "Frontend Checklist" 307 | }, 308 | { 309 | "url": "https://github.com/bendc/frontend-guidelines", 310 | "title": "Frontend Guidelines" 311 | }, 312 | { 313 | "url": "https://codeguide.co/", 314 | "title": "Codeguide by @mdo" 315 | }, 316 | { 317 | "url": "https://cssreference.io", 318 | "title": "CSS Reference" 319 | }, 320 | { 321 | "url": "https://cssguidelin.es/", 322 | "title": "CSS Guidelines" 323 | }, 324 | { 325 | "url": "https://bundlephobia.com/", 326 | "title": "Bundlephobia" 327 | }, 328 | { 329 | "url": "https://ausi.github.io/respimagelint/", 330 | "title": "ImageLint for responsive Images" 331 | }, 332 | { 333 | "url": "https://uitest.com/de/", 334 | "title": "UI-Test" 335 | }, 336 | { 337 | "url": "https://whatdoesmysitecost.com/", 338 | "title": "What does my site cost" 339 | }, 340 | { 341 | "url": "https://www.campaignmonitor.com/css/", 342 | "title": "CSS in eMails" 343 | }, 344 | { 345 | "url": "https://webfieldmanual.com/", 346 | "title": "Webfield manual" 347 | }, 348 | { 349 | "url": "https://docs.google.com/spreadsheets/d/1tZYPnzLG0y51QinLxrV97Xflzr2MbTqwWNvaHYN04BE/edit#gid=0", 350 | "title": "Styleguide/Boilerplate patterns" 351 | }, 352 | { 353 | "url": "https://www.filamentgroup.com/lab/font-events.html", 354 | "title": "Font Loading via API" 355 | }, 356 | { 357 | "url": "https://developer.apple.com", 358 | "title": "Developer @Apple" 359 | }, 360 | { 361 | "url": "https://www.interaction-design.org", 362 | "title": "Interaction Design" 363 | }, 364 | { 365 | "url": "https://www.barrierefreies-webdesign.de/knowhow/", 366 | "title": "Barrierefreies Webdesign" 367 | }, 368 | { 369 | "url": "https://webkrauts.de/", 370 | "title": "Webkrauts" 371 | }, 372 | { 373 | "url": "https://a11yproject.com/", 374 | "title": "A11Y" 375 | }, 376 | { 377 | "url": "https://www.w3.org/WAI/beta/", 378 | "title": "WAI" 379 | } 380 | ], 381 | "Frameworks": [ 382 | { 383 | "url": "https://bulma.io/", 384 | "title": "Bulma" 385 | }, 386 | { 387 | "url": "https://getbootstrap.com/", 388 | "title": "Bootstrap" 389 | }, 390 | { 391 | "url": "https://foundation.zurb.com", 392 | "title": "Foundation" 393 | }, 394 | { 395 | "url": "https://jquery.com/", 396 | "title": "jQuery" 397 | } 398 | ], 399 | "Design Systems": [ 400 | { 401 | "url": "https://www.designsystems.com/", 402 | "title": "Design Systems (by figma)" 403 | }, 404 | { 405 | "url": "https://designsystemsrepo.com/", 406 | "title": "Design Systems Repo" 407 | }, 408 | { 409 | "url": "https://www.lightningdesignsystem.com/", 410 | "title": "Lightning Design System" 411 | }, 412 | { 413 | "url": "https://ux.mailchimp.com/patterns", 414 | "title": "Mailchimp UX" 415 | }, 416 | { 417 | "url": "https://www.otto.de/pattern-library/", 418 | "title": "Otto Pattern Library" 419 | }, 420 | { 421 | "url": "https://patternlab.io/", 422 | "title": "Patternlab" 423 | } 424 | ], 425 | "Patterns": [ 426 | { 427 | "url": "https://codepen.io/patterns/", 428 | "title": "Patterns @codepen" 429 | }, 430 | { 431 | "url": "https://inclusive-components.design/", 432 | "title": "Inclusive components" 433 | }, 434 | { 435 | "url": "https://patterntap.com/patterntap", 436 | "title": "Patterns by Zurb" 437 | }, 438 | { 439 | "url": "https://ui-patterns.com/", 440 | "title": "UI-Patterns" 441 | }, 442 | { 443 | "url": "https://www.goodui.org/", 444 | "title": "Good UI" 445 | }, 446 | { 447 | "url": "https://ui-patterns.com/blog/How-to-get-better-at-UI-design", 448 | "title": "Get better at UI" 449 | } 450 | ], 451 | "Styleguides": [ 452 | { 453 | "url": "https://www.designtagebuch.de/wiki/corporate-design-manuals/", 454 | "title": "Corporate Design manuals" 455 | }, 456 | { 457 | "url": "https://www.designtagebuch.de/wiki/", 458 | "title": "Designtagebuch Wiki" 459 | }, 460 | { 461 | "url": "https://saijogeorge.com/brand-style-guide-examples/", 462 | "title": "Styleguide Examples" 463 | }, 464 | { 465 | "url": "https://www.theuxbookmark.com/2010/08/interaction-design/a-monster-list-of-ui-guidelines-style-guides", 466 | "title": "UX-Bookmark: Styleguides" 467 | } 468 | ], 469 | "Design": [ 470 | { 471 | "url": "https://makersandfounders.com/DIETER-RAMS", 472 | "title": "Dieter Rams" 473 | }, 474 | { 475 | "url": "https://www.customspaces.com/", 476 | "title": "Custom spaces" 477 | }, 478 | { 479 | "url": "https://startupsthisishowdesignworks.com/", 480 | "title": "this is how design works" 481 | } 482 | ], 483 | "Helpers": [ 484 | { 485 | "url": "https://scrumguides.org/", 486 | "title": "Scrum Guides" 487 | }, 488 | { 489 | "url": "https://support.apple.com/de-de/HT205655", 490 | "title": "Ergonomie @Apple" 491 | }, 492 | { 493 | "url": "https://makerbook.net/", 494 | "title": "Makerbook" 495 | }, 496 | { 497 | "url": "https://www.paypalobjects.com/en_AU/vhelp/paypalmanager_help/credit_card_numbers.htm", 498 | "title": "Test Creditcard Numbers" 499 | }, 500 | { 501 | "url": "https://sizecalc.com", 502 | "title": "Size Calculator" 503 | }, 504 | { 505 | "url": "https://www.flickr.com/photos/jasontravis/sets/72157603258446753/", 506 | "title": "Persona (Flickr)" 507 | }, 508 | { 509 | "url": "https://randomuser.me/", 510 | "title": "Random User Generator" 511 | }, 512 | { 513 | "url": "https://sneakpeekit.com", 514 | "title": "Sketch Sheets for Webdesingers" 515 | }, 516 | { 517 | "url": "https://ctamagazine.unbounce.com/", 518 | "title": "Call to Action Magazine" 519 | } 520 | ], 521 | "eBooks": [ 522 | { 523 | "url": "https://adaptivewebdesign.info/1st-edition/read/", 524 | "title": "Adaptive Webdesign" 525 | }, 526 | { 527 | "url": "https://eloquentjavascript.net/", 528 | "title": "eloquent Javascipt" 529 | }, 530 | { 531 | "url": "https://resilientwebdesign.com/", 532 | "title": "resilient webdesign" 533 | }, 534 | { 535 | "url": "https://www.thebookoflife.org/", 536 | "title": "The book of life" 537 | } 538 | ] 539 | }, 540 | "footer": { 541 | "description": [ 542 | { 543 | "url": "https://github.com/saschadiercks/browserStartpage", 544 | "title": "Fork me on Github" 545 | } 546 | ], 547 | "links": [ 548 | { 549 | "url": "https://saschadiercks.de", 550 | "title": "home" 551 | }, 552 | { 553 | "url": "https://github.com/saschadiercks", 554 | "title": "github" 555 | }, 556 | { 557 | "url": "https://m.twitter.com/saschadiercks", 558 | "title": "twitter" 559 | } 560 | ] 561 | } 562 | } 563 | --------------------------------------------------------------------------------