1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-25 02:36:26 +02:00

svg icon build system, theme switch

This commit is contained in:
Matthias Kretschmann 2023-09-02 14:29:03 +01:00
parent 6d4e7f3a2a
commit 73bc1bd199
Signed by: m
GPG Key ID: 606EEEF3C479A91F
40 changed files with 856 additions and 477 deletions

2
.gitignore vendored
View File

@ -10,5 +10,7 @@ src/@types/Gatsby.d.ts
# build output
dist/
src/images/icons/
# generated types
.astro/

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
18

386
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.0",
"@rainbow-me/rainbowkit": "^1.0.9",
"astro": "^3.0.3",
"astro": "^3.0.7",
"classnames": "^2.3.2",
"date-fns": "^2.30.0",
"dms2dec": "^1.1.0",
@ -28,7 +28,6 @@
"react": "^18.2.0",
"react-clipboard.js": "^2.0.16",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"slugify": "^1.6.6",
"use-debounce": "^9.0.4",
"viem": "^1.9.0",
@ -64,12 +63,13 @@
"stylelint-config-css-modules": "^4.3.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-prettier": "^4.0.2",
"svgo": "^3.0.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"typescript-plugin-css-modules": "^5.0.1"
},
"engines": {
"node": ">=16"
"node": "18"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -232,9 +232,9 @@
}
},
"node_modules/@astrojs/telemetry": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.0.0.tgz",
"integrity": "sha512-RhFlEXTiT0gbWX1osMuPS9IWm1SwhQmCZVAdAixrPyZ0xiLlHfw3Nkw3z6IYuzX3hqbx24G4XmkT/akBMBqxPg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.0.1.tgz",
"integrity": "sha512-7zJMuikRDQ0LLLivteu0+y4pqdgznrChFiRrY3qmKlOEkLWD1T3u1a5M970lvpErP7Vgh4P298JBPjv8LTj+sw==",
"dependencies": {
"ci-info": "^3.8.0",
"debug": "^4.3.4",
@ -4505,15 +4505,6 @@
"@svgr/core": "*"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz",
@ -4532,82 +4523,6 @@
"url": "https://github.com/sponsors/d-fischer"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"dev": true,
"dependencies": {
"css-tree": "~2.2.0"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/csso/node_modules/css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"dev": true,
"dependencies": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@svgr/plugin-svgo/node_modules/csso/node_modules/mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true
},
"node_modules/@svgr/plugin-svgo/node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
},
"node_modules/@svgr/plugin-svgo/node_modules/svgo": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
"integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==",
"dev": true,
"dependencies": {
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.2.1",
"csso": "^5.0.5",
"picocolors": "^1.0.0"
},
"bin": {
"svgo": "bin/svgo"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/svgo"
}
},
"node_modules/@svgr/webpack": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz",
@ -6689,14 +6604,14 @@
}
},
"node_modules/astro": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-3.0.3.tgz",
"integrity": "sha512-bugdGn9wIniVFbfyAHYtF9bc9pZpPaEs3gJAnK/XWROxCBAI2UQjR6lQuWM20iCc3snqu7GDgoW2MdzO7WFZZw==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/astro/-/astro-3.0.7.tgz",
"integrity": "sha512-slUnDBXfxMzq5abE4svcKbaeYC/tHZsJYOrzwDNU9lLye3/4cqYP7OuHMTXiRlx7LSpHQlUhwbMe2HqCv2GKag==",
"dependencies": {
"@astrojs/compiler": "^2.0.1",
"@astrojs/internal-helpers": "0.2.0",
"@astrojs/markdown-remark": "3.0.0",
"@astrojs/telemetry": "3.0.0",
"@astrojs/telemetry": "3.0.1",
"@babel/core": "^7.22.10",
"@babel/generator": "^7.22.10",
"@babel/parser": "^7.22.10",
@ -8375,6 +8290,19 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/css-what": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz",
@ -8403,6 +8331,39 @@
"node": ">=4"
}
},
"node_modules/csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"dev": true,
"dependencies": {
"css-tree": "~2.2.0"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso/node_modules/css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"dev": true,
"dependencies": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso/node_modules/mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@ -14978,6 +14939,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@ -17308,17 +17275,6 @@
"react": "^18.2.0"
}
},
"node_modules/react-feather": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/react-feather/-/react-feather-2.0.10.tgz",
"integrity": "sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">=16.8.6"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -19508,25 +19464,6 @@
"url": "https://github.com/sponsors/d-fischer"
}
},
"node_modules/stylelint/node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/stylelint/node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
},
"node_modules/stylelint/node_modules/signal-exit": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
@ -19656,6 +19593,39 @@
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
"dev": true
},
"node_modules/svgo": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
"integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==",
"dev": true,
"dependencies": {
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.2.1",
"csso": "^5.0.5",
"picocolors": "^1.0.0"
},
"bin": {
"svgo": "bin/svgo"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/svgo"
}
},
"node_modules/svgo/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -21970,9 +21940,9 @@
}
},
"@astrojs/telemetry": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.0.0.tgz",
"integrity": "sha512-RhFlEXTiT0gbWX1osMuPS9IWm1SwhQmCZVAdAixrPyZ0xiLlHfw3Nkw3z6IYuzX3hqbx24G4XmkT/akBMBqxPg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.0.1.tgz",
"integrity": "sha512-7zJMuikRDQ0LLLivteu0+y4pqdgznrChFiRrY3qmKlOEkLWD1T3u1a5M970lvpErP7Vgh4P298JBPjv8LTj+sw==",
"requires": {
"ci-info": "^3.8.0",
"debug": "^4.3.4",
@ -24922,12 +24892,6 @@
"svgo": "^3.0.2"
},
"dependencies": {
"commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true
},
"cosmiconfig": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz",
@ -24939,63 +24903,6 @@
"parse-json": "^5.0.0",
"path-type": "^4.0.0"
}
},
"css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"requires": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
}
},
"csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"dev": true,
"requires": {
"css-tree": "~2.2.0"
},
"dependencies": {
"css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"dev": true,
"requires": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
}
},
"mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true
}
}
},
"mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
},
"svgo": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
"integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==",
"dev": true,
"requires": {
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.2.1",
"csso": "^5.0.5",
"picocolors": "^1.0.0"
}
}
}
},
@ -26660,14 +26567,14 @@
"dev": true
},
"astro": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-3.0.3.tgz",
"integrity": "sha512-bugdGn9wIniVFbfyAHYtF9bc9pZpPaEs3gJAnK/XWROxCBAI2UQjR6lQuWM20iCc3snqu7GDgoW2MdzO7WFZZw==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/astro/-/astro-3.0.7.tgz",
"integrity": "sha512-slUnDBXfxMzq5abE4svcKbaeYC/tHZsJYOrzwDNU9lLye3/4cqYP7OuHMTXiRlx7LSpHQlUhwbMe2HqCv2GKag==",
"requires": {
"@astrojs/compiler": "^2.0.1",
"@astrojs/internal-helpers": "0.2.0",
"@astrojs/markdown-remark": "3.0.0",
"@astrojs/telemetry": "3.0.0",
"@astrojs/telemetry": "3.0.1",
"@babel/core": "^7.22.10",
"@babel/generator": "^7.22.10",
"@babel/parser": "^7.22.10",
@ -27835,6 +27742,16 @@
}
}
},
"css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"requires": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
}
},
"css-what": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz",
@ -27851,6 +27768,33 @@
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
},
"csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"dev": true,
"requires": {
"css-tree": "~2.2.0"
},
"dependencies": {
"css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"dev": true,
"requires": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
}
},
"mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true
}
}
},
"cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@ -32675,6 +32619,12 @@
"@types/mdast": "^3.0.0"
}
},
"mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@ -34241,14 +34191,6 @@
"scheduler": "^0.23.0"
}
},
"react-feather": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/react-feather/-/react-feather-2.0.10.tgz",
"integrity": "sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -35814,22 +35756,6 @@
"path-type": "^4.0.0"
}
},
"css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"requires": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
}
},
"mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
},
"signal-exit": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
@ -35974,6 +35900,28 @@
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
"dev": true
},
"svgo": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
"integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==",
"dev": true,
"requires": {
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^2.2.1",
"csso": "^5.0.5",
"picocolors": "^1.0.0"
},
"dependencies": {
"commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true
}
}
},
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",

View File

@ -5,10 +5,10 @@
"description": "Blog of Designer & Developer Matthias Kretschmann",
"homepage": "https://kremalicious.com",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "astro dev --config .config/astro.config.mjs",
"start": "astro dev --config .config/astro.config.mjs",
"build": "astro build --config .config/astro.config.mjs",
"start": "npm run create:icons && astro dev --config .config/astro.config.mjs",
"build": "npm run create:icons && astro build --config .config/astro.config.mjs",
"preview": "astro preview",
"astro": "astro",
"test": "astro check && tsc --noEmit && npm run lint && npm run type-check && npm run jest",
@ -20,14 +20,15 @@
"format": "prettier --ignore-path .gitignore --write '**/*.{js,jsx,ts,tsx,md,json,css}'",
"type-check": "tsc --noEmit",
"deploy:s3": "./scripts/deploy-s3.sh",
"new": "ts-node scripts/new.ts"
"new": "ts-node --esm scripts/new.ts",
"create:icons": "ts-node --esm scripts/create-icons/index.ts"
},
"dependencies": {
"@astrojs/react": "^3.0.0",
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.0",
"@rainbow-me/rainbowkit": "^1.0.9",
"astro": "^3.0.3",
"astro": "^3.0.7",
"classnames": "^2.3.2",
"date-fns": "^2.30.0",
"dms2dec": "^1.1.0",
@ -42,7 +43,6 @@
"react": "^18.2.0",
"react-clipboard.js": "^2.0.16",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"slugify": "^1.6.6",
"use-debounce": "^9.0.4",
"viem": "^1.9.0",
@ -78,12 +78,13 @@
"stylelint-config-css-modules": "^4.3.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-prettier": "^4.0.2",
"svgo": "^3.0.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"typescript-plugin-css-modules": "^5.0.1"
},
"engines": {
"node": ">=16"
"node": "18"
},
"repository": {
"type": "git",

View File

@ -0,0 +1,320 @@
// All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/
export interface AriaAttributes {
/** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */
'aria-activedescendant'?: string
/** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */
'aria-atomic'?: 'true' | 'false' | boolean
/**
* Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be
* presented if they are made.
*/
'aria-autocomplete'?: 'none' | 'inline' | 'list' | 'both'
/** Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. */
'aria-busy'?: 'true' | 'false' | boolean
/**
* Indicates the current 'checked' state of checkboxes, radio buttons, and other widgets.
* @see aria-pressed @see aria-selected.
*/
'aria-checked'?: 'false' | 'mixed' | 'true' | boolean
/**
* Defines the total number of columns in a table, grid, or treegrid.
* @see aria-colindex.
*/
'aria-colcount'?: number
/**
* Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid.
* @see aria-colcount @see aria-colspan.
*/
'aria-colindex'?: number
/**
* Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid.
* @see aria-colindex @see aria-rowspan.
*/
'aria-colspan'?: number
/**
* Identifies the element (or elements) whose contents or presence are controlled by the current element.
* @see aria-owns.
*/
'aria-controls'?: string
/** Indicates the element that represents the current item within a container or set of related elements. */
'aria-current'?:
| 'false'
| 'true'
| 'page'
| 'step'
| 'location'
| 'date'
| 'time'
| boolean
/**
* Identifies the element (or elements) that describes the object.
* @see aria-labelledby
*/
'aria-describedby'?: string
/**
* Identifies the element that provides a detailed, extended description for the object.
* @see aria-describedby.
*/
'aria-details'?: string
/**
* Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable.
* @see aria-hidden @see aria-readonly.
*/
'aria-disabled'?: 'true' | 'false' | boolean
/**
* Identifies the element that provides an error message for the object.
* @see aria-invalid @see aria-describedby.
*/
'aria-errormessage'?: string
/** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */
'aria-expanded'?: 'true' | 'false' | boolean
/**
* Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,
* allows assistive technology to override the general default of reading in document source order.
*/
'aria-flowto'?: string
/** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */
'aria-haspopup'?:
| 'false'
| 'true'
| 'menu'
| 'listbox'
| 'tree'
| 'grid'
| 'dialog'
| boolean
/**
* Indicates whether the element is exposed to an accessibility API.
* @see aria-disabled.
*/
'aria-hidden'?: 'true' | 'false' | boolean
/**
* Indicates the entered value does not conform to the format expected by the application.
* @see aria-errormessage.
*/
'aria-invalid'?: 'false' | 'true' | 'grammar' | 'spelling' | boolean
/** Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. */
'aria-keyshortcuts'?: string
/**
* Defines a string value that labels the current element.
* @see aria-labelledby.
*/
'aria-label'?: string
/**
* Identifies the element (or elements) that labels the current element.
* @see aria-describedby.
*/
'aria-labelledby'?: string
/** Defines the hierarchical level of an element within a structure. */
'aria-level'?: number
/** Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region. */
'aria-live'?: 'off' | 'assertive' | 'polite'
/** Indicates whether an element is modal when displayed. */
'aria-modal'?: 'true' | 'false' | boolean
/** Indicates whether a text box accepts multiple lines of input or only a single line. */
'aria-multiline'?: 'true' | 'false' | boolean
/** Indicates that the user may select more than one item from the current selectable descendants. */
'aria-multiselectable'?: 'true' | 'false' | boolean
/** Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous. */
'aria-orientation'?: 'horizontal' | 'vertical'
/**
* Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship
* between DOM elements where the DOM hierarchy cannot be used to represent the relationship.
* @see aria-controls.
*/
'aria-owns'?: string
/**
* Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.
* A hint could be a sample value or a brief description of the expected format.
*/
'aria-placeholder'?: string
/**
* Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
* @see aria-setsize.
*/
'aria-posinset'?: number
/**
* Indicates the current 'pressed' state of toggle buttons.
* @see aria-checked @see aria-selected.
*/
'aria-pressed'?: 'false' | 'mixed' | 'true' | boolean
/**
* Indicates that the element is not editable, but is otherwise operable.
* @see aria-disabled.
*/
'aria-readonly'?: 'true' | 'false' | boolean
/**
* Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified.
* @see aria-atomic.
*/
'aria-relevant'?:
| 'additions'
| 'additions removals'
| 'additions text'
| 'all'
| 'removals'
| 'removals additions'
| 'removals text'
| 'text'
| 'text additions'
| 'text removals'
/** Indicates that user input is required on the element before a form may be submitted. */
'aria-required'?: 'true' | 'false' | boolean
/** Defines a human-readable, author-localized description for the role of an element. */
'aria-roledescription'?: string
/**
* Defines the total number of rows in a table, grid, or treegrid.
* @see aria-rowindex.
*/
'aria-rowcount'?: number
/**
* Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid.
* @see aria-rowcount @see aria-rowspan.
*/
'aria-rowindex'?: number
/**
* Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid.
* @see aria-rowindex @see aria-colspan.
*/
'aria-rowspan'?: number
/**
* Indicates the current 'selected' state of various widgets.
* @see aria-checked @see aria-pressed.
*/
'aria-selected'?: 'true' | 'false' | boolean
/**
* Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
* @see aria-posinset.
*/
'aria-setsize'?: number
/** Indicates if items in a table or grid are sorted in ascending or descending order. */
'aria-sort'?: 'none' | 'ascending' | 'descending' | 'other'
/** Defines the maximum allowed value for a range widget. */
'aria-valuemax'?: number
/** Defines the minimum allowed value for a range widget. */
'aria-valuemin'?: number
/**
* Defines the current value for a range widget.
* @see aria-valuetext.
*/
'aria-valuenow'?: number
/** Defines the human readable text alternative of aria-valuenow for a range widget. */
'aria-valuetext'?: string
}
// All the WAI-ARIA 1.1 role attribute values from https://www.w3.org/TR/wai-aria-1.1/#role_definitions
export type AriaRole =
| 'alert'
| 'alertdialog'
| 'application'
| 'article'
| 'banner'
| 'button'
| 'cell'
| 'checkbox'
| 'columnheader'
| 'combobox'
| 'complementary'
| 'contentinfo'
| 'definition'
| 'dialog'
| 'directory'
| 'document'
| 'feed'
| 'figure'
| 'form'
| 'grid'
| 'gridcell'
| 'group'
| 'heading'
| 'img'
| 'link'
| 'list'
| 'listbox'
| 'listitem'
| 'log'
| 'main'
| 'marquee'
| 'math'
| 'menu'
| 'menubar'
| 'menuitem'
| 'menuitemcheckbox'
| 'menuitemradio'
| 'navigation'
| 'none'
| 'note'
| 'option'
| 'presentation'
| 'progressbar'
| 'radio'
| 'radiogroup'
| 'region'
| 'row'
| 'rowgroup'
| 'rowheader'
| 'scrollbar'
| 'search'
| 'searchbox'
| 'separator'
| 'slider'
| 'spinbutton'
| 'status'
| 'switch'
| 'tab'
| 'table'
| 'tablist'
| 'tabpanel'
| 'term'
| 'textbox'
| 'timer'
| 'toolbar'
| 'tooltip'
| 'tree'
| 'treegrid'
| 'treeitem'
| (string & object)
export interface HTMLAttributes extends AriaAttributes {
accesskey?: string
autocapitalize?: string
autofocus?: string
class?: string
contenteditable?: 'true' | 'false' | 'inherit' | boolean
contextMenu?: string
dir?: 'ltr' | 'rtl' | 'auto'
draggable?: 'true' | 'false' | boolean
enterkeyhint?: string
exportparts?: string
hidden?: string
id?: string
inputmode?: string
is?: string
lang?: string
placeholder?: string
role?: AriaRole
slot?: string
spellcheck?: 'true' | 'false' | boolean
style?: string
tabindex?: number
title?: string
translate?: 'yes' | 'no'
}
export interface Props extends HTMLAttributes {
fill?: string
'fill-opacity'?: number | string
'fill-rule'?: 'nonzero' | 'evenodd' | 'inherit'
height?: number | string
size?: number | string
stroke?: string
'stroke-dasharray'?: string | number
'stroke-dashoffset'?: string | number
'stroke-linecap'?: 'butt' | 'round' | 'square' | 'inherit'
'stroke-linejoin'?: 'miter' | 'round' | 'bevel' | 'inherit'
'stroke-miterlimit'?: number | string
'stroke-opacity'?: number | string
'stroke-width'?: number | string
viewBox?: string
width?: number | string
}

View File

@ -0,0 +1,89 @@
//
// Generate Astro components from SVG files.
// adapted from https://github.com/astro-community/icons
//
import fs from 'node:fs/promises'
import ps from 'node:path/posix'
import { toAstroComponent, toInnerSvg } from './svg.ts'
// Current directory.
const currentDir = ps.resolve('.')
// // Source directories
const srcDirs = [
ps.resolve(currentDir, 'node_modules/feather-icons/dist/icons'),
ps.resolve(currentDir, 'src/images')
]
// Distribution directory.
const distDir = ps.resolve(currentDir, 'src/images/icons')
// Data related to each icon exported by this package.
const icons = []
// clean the distribution directory
await fs.rm(distDir, { force: true, recursive: true })
await fs.mkdir(distDir, { recursive: true })
// copy the attribute typings file
await fs.copyFile(
ps.resolve(currentDir, 'scripts/create-icons/Props.ts'),
ps.resolve(distDir, 'Props.ts')
)
// convert the SVG files into Astro components
let contentOfIndexJS = ''
for (const src of srcDirs) {
for (let filepath of await fs.readdir(src, { encoding: 'utf8' })) {
// ignore non-svg files
if (!filepath.endsWith('.svg')) continue
// Base name of the SVG.
const name = filepath.replace(/\.svg$/, '')
// get filepath as a full path
filepath = ps.resolve(src, filepath)
// Inner contents of the SVG file.
const innerSVG = toInnerSvg(await fs.readFile(filepath, 'utf8'))
// Formatted title.
const title = name
.replace(
// uppercase alphabetic characters after the start or a dash
/(?<=^|-)([a-z])/g,
(_0, $1) => $1.toUpperCase()
)
.replace(
// replace non-alphanumeric characters with space
/[^A-Za-z0-9]+/g,
' '
)
.replace(
// respect 'GitHub' brand casing
'Github Logo',
'GitHub Logo'
)
// Base name, which is the formatted title without spaces (PascalCase)
const baseName = title.replace(/ /g, '')
// write the astro component to a file
await fs.writeFile(
ps.resolve(distDir, `${baseName}.astro`),
toAstroComponent(innerSVG, title),
'utf8'
)
// add the astro component export to the main entry `index.ts` file
contentOfIndexJS += `\nexport { default as ${baseName} } from './${baseName}.astro'`
icons.push({ name, baseName, title })
}
}
// write the main entry `index.ts` file
await fs.writeFile(ps.resolve(distDir, 'index.ts'), contentOfIndexJS, 'utf8')
console.log(`✔️ Generated ${icons.length} icons into @images/icons.`)

View File

@ -0,0 +1,85 @@
import { optimize as optimizeSVGNative } from 'svgo'
export const toAstroComponent = (innerSVG: string, title: string) => `---
export { Props } from './Props.ts';
let {
size = '24px',
title,
width = size,
height = size,
...props
} = {
'fill': 'none',
'title': '${title}',
'viewBox': '0 0 24 24',
...Astro.props
}
const toAttributeSize = (size: number) => String(size).replace(/(?<=[0-9])x$/, 'em')
size = toAttributeSize(size)
width = toAttributeSize(width)
height = toAttributeSize(height)
---
<style is:global>
.icon {
width: 1em;
height: 1em;
stroke: currentcolor;
stroke-width: var(--stroke-width);
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
vertical-align: baseline;
}
</style>
<svg {width} {height} {...props} class="icon">{title ? (<title>{title}</title>) : ''}${innerSVG}</svg>`
export const toInnerSvg = (input: string) =>
optimizeSVGNative(input, {
plugins: [
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeXMLNS',
'removeEditorsNSData',
'cleanupAttrs',
'minifyStyles',
'convertStyleToAttrs',
'cleanupIds',
'removeRasterImages',
'removeUselessDefs',
'cleanupNumericValues',
'cleanupListOfValues',
'convertColors',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
'removeViewBox',
'cleanupEnableBackground',
'removeHiddenElems',
'removeEmptyText',
'convertShapeToPath',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
'convertPathData',
'convertTransform',
'removeEmptyAttrs',
'removeEmptyContainers',
'mergePaths',
'removeUnusedNS',
'sortAttrs',
'removeTitle',
'removeDesc',
'removeDimensions',
'removeStyleElement',
'removeScriptElement'
]
})
.data.replace(/^<svg[^>]*>|<\/svg>$/g, '')
.replace(/ fill="currentColor"/g, '')
.replace(/ (clip|fill)-rule="evenodd"/g, '')
.replace(/\/>/g, ' />')

View File

@ -1,5 +1,5 @@
import fastExif from 'fast-exif'
import fs from 'fs-extra'
import fs from 'fs'
import iptc from 'node-iptc'
import ora from 'ora'
import path from 'path'

View File

@ -0,0 +1,30 @@
---
import styles from './Networks.module.css'
import { Twitter, Rss, Mastodon, Github, Jsonfeed } from '@images/icons'
type Props = {
links: string[]
}
const { links } = Astro.props
---
<p>
{
links.map((link: string) => (
<a class={styles.link} href={link} title={link} rel="me">
{link.includes('mas.to') ? (
<Mastodon />
) : link.includes('twitter') ? (
<Twitter />
) : link.includes('github') ? (
<Github />
) : link.includes('feed.xml') ? (
<Rss />
) : link.includes('feed.json') ? (
<Jsonfeed />
) : null}
</a>
))
}
</p>

View File

@ -1,39 +0,0 @@
import type { ReactElement } from 'react'
import Icon from '../core/Icon'
import styles from './Networks.module.css'
function NetworkIcon({ link }: { link: string }) {
let IconComp
if (link.includes('mas.to')) {
IconComp = <Icon name="Mastodon" />
} else if (link.includes('twitter')) {
IconComp = <Icon name="Twitter" />
} else if (link.includes('github')) {
IconComp = <Icon name="GitHub" />
} else if (link.includes('feed.xml')) {
IconComp = <Icon name="Rss" />
} else if (link.includes('feed.json')) {
IconComp = <Icon name="Jsonfeed" />
} else {
return null
}
return IconComp
}
export default function IconLinks({
links
}: {
links: string[]
}): ReactElement {
return (
<p>
{links.map((link: string) => (
<a key={link} className={styles.link} href={link} title={link} rel="me">
<NetworkIcon link={link} />
</a>
))}
</p>
)
}

View File

@ -1,8 +1,8 @@
---
import { Image } from 'astro:assets'
import Networks from './Networks'
import avatar from '../../images/avatar.jpg'
import config from '../../../.config/blog.config.mjs'
import Networks from './Networks.astro'
import avatar from '@images/avatar.jpg'
import config from '@config/blog.config.mjs'
const { author, rss, jsonfeed } = config
const { mastodon, twitter, github, name, uri } = author

View File

@ -1,5 +1,5 @@
---
import Icon from '../core/Icon'
import { Github, Bitcoin } from '@images/icons'
import Vcard from './Vcard.astro'
import styles from './index.module.css'
import config from '@config/blog.config.mjs'
@ -18,11 +18,11 @@ const { name, uri, github } = config.author
{name}
</a>
<a href={`${github}/blog`}>
<Icon name="GitHub" />
<Github />
View source
</a>
<a href="/thanks" class={styles.btc}>
<Icon name="Bitcoin" />
<Bitcoin />
Say Thanks
</a>
</p>

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react'
import Icon from '../../core/Icon'
import styles from './SearchButton.module.css'
import { Search } from '@images/icons'
const SearchButton = ({ onClick }: { onClick: () => void }): ReactElement => (
<button
@ -9,7 +9,7 @@ const SearchButton = ({ onClick }: { onClick: () => void }): ReactElement => (
className={styles.searchButton}
onClick={onClick}
>
<Icon name="Search" />
<Search />
</button>
)

View File

@ -1,7 +1,7 @@
import React, { ChangeEvent, ReactElement } from 'react'
import Icon from '../../core/Icon'
import Input from '../../core/Input'
import styles from './SearchInput.module.css'
import { X } from '@images/icons'
export default function SearchInput({
value,
@ -27,7 +27,7 @@ export default function SearchInput({
onClick={onToggle}
title="Close search"
>
<Icon name="X" />
<X />
</button>
</>
)

View File

@ -1,20 +0,0 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import ThemeSwitch from './ThemeSwitch'
describe('ThemeSwitch', () => {
it('renders correctly', async () => {
render(<ThemeSwitch />)
const element = await screen.findByTitle('Toggle Dark Mode')
expect(element).toBeInTheDocument()
})
it('checkbox can be changed', () => {
const { container } = render(<ThemeSwitch />)
const toggle = container.querySelector('input')
const label = container.querySelector('label')
fireEvent.click(label)
fireEvent.change(toggle, { target: { checked: true } })
})
})

View File

@ -1,33 +0,0 @@
import type { ReactElement } from 'react'
import useDarkMode from '../../hooks/useDarkMode'
import Icon from '../core/Icon'
import styles from './ThemeSwitch.module.css'
export default function ThemeSwitch(): ReactElement {
const { isDarkMode, setIsDarkMode } = useDarkMode()
return (
<div className={styles.themeSwitch} title="Toggle Dark Mode">
<label
htmlFor="toggle"
className={styles.checkbox}
onClick={() => setIsDarkMode(!isDarkMode)}
onKeyPress={() => setIsDarkMode(!isDarkMode)}
role="presentation"
>
<span className={styles.label}>Toggle Dark Mode</span>
<input
onChange={() => setIsDarkMode(!isDarkMode)}
type="checkbox"
name="toggle"
value="toggle"
aria-describedby="toggle"
checked={isDarkMode}
/>
<div aria-live="assertive">
{isDarkMode ? <Icon name="Sun" /> : <Icon name="Moon" />}
</div>
</label>
</div>
)
}

View File

@ -1,20 +1,20 @@
---
import logo from '@images/logo.svg'
import Menu from './Menu'
import Search from './Search'
import ThemeSwitch from './ThemeSwitch'
// import Search from './Search'
import ThemeSwitch from '../ThemeSwitch/index.astro'
import { Logo } from '@images/icons'
import styles from './index.module.css'
---
<header role="banner" class={styles.header}>
<div class={styles.headerContent}>
<a href="/" class={styles.title}>
<img src={logo} class={styles.logo} /> kremalicious
<Logo class={styles.logo} viewBox="0 0 191 36" /> kremalicious
</a>
<nav aria-label="Menu" class={styles.nav}>
<ThemeSwitch />
<Search />
<!-- <Search /> -->
<Menu />
</nav>
</div>

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react'
import Icon from './core/Icon'
import styles from './Pagination.module.css'
import { ChevronLeft, ChevronRight } from '@images/icons'
function PageNumber({
i,
@ -34,11 +34,7 @@ function PrevNext({
return (
<a href={link} rel={rel} title={title} className={styles.number}>
{prevPagePath ? (
<Icon name="ChevronLeft" />
) : (
<Icon name="ChevronRight" />
)}
{prevPagePath ? <ChevronLeft /> : <ChevronRight />}
</a>
)
}

View File

@ -0,0 +1,21 @@
---
import { Sun, Moon } from '@images/icons'
import styles from './index.module.css'
---
<div class={styles.themeSwitch} title="Toggle Theme" id="theme-toggle">
<label for="toggle" class={styles.checkbox} role="presentation">
<span class={styles.label}>Toggle Theme</span>
<input
type="checkbox"
name="theme-toggle"
aria-describedby="theme-toggle"
/>
<div aria-live="assertive">
<Sun id="sun" />
<Moon id="moon" />
</div>
</label>
</div>
<script src="./script.js"></script>

View File

@ -23,6 +23,10 @@
display: block;
}
.themeSwitch [hidden] {
display: none !important;
}
/* hide visually */
.checkbox [type='checkbox'],
.checkbox .label {

View File

@ -0,0 +1,57 @@
const themeToggle = document.querySelector('#theme-toggle')
const themeToggleInput = document.querySelector('#theme-toggle input')
const sun = document.querySelector('#sun')
const moon = document.querySelector('#moon')
const primaryColorScheme = null // "light" | "dark"
const currentTheme = localStorage.getItem('theme')
function getPreferTheme() {
if (currentTheme) return currentTheme
if (primaryColorScheme) return primaryColorScheme
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
let themeValue = getPreferTheme()
function setPreference() {
localStorage.setItem('theme', themeValue)
reflectPreference()
}
function reflectPreference() {
document.firstElementChild?.setAttribute('data-theme', themeValue)
themeToggle?.setAttribute('aria-label', themeValue)
themeToggleInput?.setAttribute('checked', `${themeValue === 'dark'}`)
if (themeValue === 'dark') {
sun?.removeAttribute('hidden')
moon?.setAttribute('hidden', '')
} else {
sun?.setAttribute('hidden', '')
moon?.removeAttribute('hidden')
}
}
// set early so no page flashes / CSS is made aware
reflectPreference()
window.onload = () => {
// set on load so screen readers can get the latest value on the button
reflectPreference()
themeToggle?.addEventListener('click', () => {
themeValue = themeValue === 'light' ? 'dark' : 'light'
setPreference()
})
}
// sync with system changes
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({ matches: isDark }) => {
themeValue = isDark ? 'dark' : 'light'
setPreference()
})

View File

@ -64,7 +64,7 @@
margin-left: -1rem;
}
:global(.dark) .inputInput {
:global([data-theme='dark']) .inputInput {
border-color: var(--border-color);
}
@ -83,7 +83,7 @@
align-items: center;
}
:global(.dark) .currency {
:global([data-theme='dark']) .currency {
border-right-color: #000;
}

View File

@ -1,13 +1,13 @@
import React, { ReactElement } from 'react'
import Clipboard from 'react-clipboard.js'
import { Copy } from '@images/icons'
import styles from './Copy.module.css'
import Icon from './Icon'
const onCopySuccess = (e: any) => {
e.trigger.classList.add(styles.copied)
}
export default function Copy({ text }: { text: string }): ReactElement {
export default function CopyAction({ text }: { text: string }): ReactElement {
return (
<Clipboard
data-clipboard-text={text}
@ -15,7 +15,7 @@ export default function Copy({ text }: { text: string }): ReactElement {
onSuccess={(e: ClipboardJS.Event) => onCopySuccess(e)}
className={styles.button}
>
<Icon name="Copy" />
<Copy />
</Clipboard>
)
}

View File

@ -13,6 +13,6 @@
border-bottom: 1px dashed #fff;
}
:global(.dark) .divider::before {
:global([data-theme='dark']) .divider::before {
border-bottom-color: var(--brand-grey);
}

View File

@ -1,10 +0,0 @@
.icon {
width: 1em;
height: 1em;
stroke: currentcolor;
stroke-width: var(--stroke-width);
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
vertical-align: baseline;
}

View File

@ -1,21 +0,0 @@
import React from 'react'
import { render } from '@testing-library/react'
import Icon from './Icon'
describe('Icon', () => {
it('renders correctly', () => {
const { container, rerender } = render(<Icon name={'Compass'} />)
expect(container.firstChild.nodeName).toBe('svg')
rerender(<Icon name={'Download'} />)
expect(container.firstChild.nodeName).toBe('svg')
rerender(<Icon name={'Twitter'} />)
expect(container.firstChild.nodeName).toBe('svg')
})
it('does not render with unknown name', () => {
const { container } = render(<Icon name={'whatever'} />)
expect(container.firstChild).not.toBeInTheDocument()
})
})

View File

@ -1,69 +0,0 @@
import type { FunctionComponent, ReactElement } from 'react'
// https://featherstyles.com
// import * as Feather from '@kremalicious/react-feather'
import {
Aperture,
ArrowDownCircle,
Camera,
ChevronLeft,
ChevronRight,
Compass,
Copy,
Crosshair,
Edit,
ExternalLink,
GitHub,
Link,
Maximize,
Moon,
Rss,
Search,
Sun,
Twitter,
X
} from 'react-feather'
import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
// custom icons
import { ReactComponent as Jsonfeed } from '../../images/jsonfeed.svg'
import { ReactComponent as Mastodon } from '../../images/mastodon.svg'
import { ReactComponent as Stopwatch } from '../../images/stopwatch.svg'
import styles from './Icon.module.css'
const components: {
[key: string]: FunctionComponent<React.SVGProps<SVGSVGElement>>
} = {
Download: ArrowDownCircle,
Jsonfeed,
Bitcoin,
Stopwatch,
ArrowDownCircle,
Edit,
GitHub,
Twitter,
Rss,
Sun,
Moon,
Compass,
X,
Copy,
Search,
ExternalLink,
Link,
ChevronRight,
ChevronLeft,
Camera,
Aperture,
Maximize,
Crosshair,
Mastodon
}
const Icon = ({ name, ...props }: { name: string }): ReactElement | null => {
const IconMapped = components[name]
// const IconFeather = (Feather as any)[name]
if (!IconMapped) return null
return <IconMapped className={styles.icon} {...props} />
}
export default Icon

View File

@ -28,7 +28,7 @@
transform: translate3d(0, calc(var(--spacer) * 2), 0);
}
:global(.dark) .document {
:global([data-theme='dark']) .document {
border-top-color: rgba(255 255 255 / 5%);
box-shadow:
0 1px 8px rgba(0 7 8 / 30%),

View File

@ -1,7 +1,7 @@
---
import Time from '@components/core/Time.astro'
import Icon from '../../core/Icon'
import styles from './Title.module.css'
import { ExternalLink } from '@images/icons'
type Props = {
linkurl?: string
@ -22,7 +22,7 @@ const linkHostname = linkurl ? new URL(linkurl).hostname : null
class={`${styles.title} ${styles.titleLink} ${className && className}`}
>
<a href={linkurl} title={`Go to source: ${linkurl}`}>
{title} <Icon name="ExternalLink" />
{title} <ExternalLink />
</a>
</h1>
<div class={styles.linkurl}>{linkHostname}</div>

View File

@ -19,11 +19,15 @@ kbd {
rgba(0, 0, 0, 0)
);
background-repeat: repeat-x;
box-shadow: 0 2px 0 #bbb, 0 3px 1px #999, 0 3px 0 #bbb, inset 0 1px 1px #fff,
box-shadow:
0 2px 0 #bbb,
0 3px 1px #999,
0 3px 0 #bbb,
inset 0 1px 1px #fff,
inset 0 -1px 3px #ccc;
}
kbd.dark {
kbd[data-theme='dark'] {
color: #eee;
text-shadow: 0 -1px 0 #000;
border-color: #000;
@ -34,7 +38,10 @@ kbd.dark {
rgba(0, 0, 0, 0)
);
background-repeat: no-repeat;
box-shadow: 0 2px 0 #000, 0 3px 1px #999, inset 0 1px 1px #aaa,
box-shadow:
0 2px 0 #000,
0 3px 1px #999,
inset 0 1px 1px #aaa,
inset 0 -1px 3px #272727;
}
@ -46,7 +53,9 @@ kbd.ios {
background-color: #b7b7bc;
background-image: linear-gradient(to bottom, #efeff0, #b7b7bc);
background-repeat: repeat-x;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6), 0 2px 3px rgba(0, 0, 0, 0.1),
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.6),
0 2px 3px rgba(0, 0, 0, 0.1),
inset 0 1px 0 #fff;
}
@ -59,24 +68,33 @@ kbd.android {
border-radius: 3px;
background-clip: padding-box;
background: #5e5e5e;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3), 0 1px 0 #444, inset 0 1px 0 #868686;
box-shadow:
0 2px 2px rgba(0, 0, 0, 0.3),
0 1px 0 #444,
inset 0 1px 0 #868686;
}
kbd.android.dark {
kbd.android[data-theme='dark'] {
background: #222;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #505050;
box-shadow:
0 2px 2px rgba(0, 0, 0, 0.7),
0 1px 0 #444,
inset 0 1px 0 #505050;
}
kbd.android.color {
background: #083c5b;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.7), 0 1px 0 #444, inset 0 1px 0 #36647b;
box-shadow:
0 2px 2px rgba(0, 0, 0, 0.7),
0 1px 0 #444,
inset 0 1px 0 #36647b;
}
@font-face {
font-family: 'RobotoRegular';
src: url('/media/Roboto-Regular-webfont.eot');
src: url('/media/Roboto-Regular-webfont.eot?#iefix')
format('embedded-opentype'),
src:
url('/media/Roboto-Regular-webfont.eot?#iefix') format('embedded-opentype'),
url('/media/Roboto-Regular-webfont.woff') format('woff'),
url('/media/Roboto-Regular-webfont.ttf') format('truetype'),
url('/media/Roboto-Regular-webfont.svg#RobotoRegular') format('svg');

View File

@ -1,6 +1,5 @@
import type { ReactElement } from 'react'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import Icon from '../../components/core/Icon'
import styles from './Actions.module.css'
interface ActionProps {
@ -14,7 +13,7 @@ interface ActionProps {
const Action = ({ title, text, url, icon, onClick }: ActionProps) => {
return (
<a className={styles.action} href={url} onClick={onClick}>
<Icon name={icon} />
{/* <Icon name={icon} /> */}
<h1 className={styles.actionTitle}>{title}</h1>
<p className={styles.actionText}>{text}</p>
</a>

View File

@ -1,7 +1,7 @@
import React, { ReactElement } from 'react'
import Icon from '../../components/core/Icon'
import styles from './LinkActions.module.css'
import stylesMore from './More.module.css'
import { ExternalLink, Link } from '@images/icons'
const PostLinkActions = ({
linkurl,
@ -12,10 +12,10 @@ const PostLinkActions = ({
}): ReactElement => (
<div className={styles.postLinkActions}>
<a className={stylesMore.postMore} href={linkurl}>
Go to source <Icon name="ExternalLink" />
Go to source <ExternalLink />
</a>
<a href={slug} rel="tooltip" title="Permalink">
<Icon name="Link" />
<Link />
</a>
</div>
)

View File

@ -1,5 +1,5 @@
import React, { ReactElement } from 'react'
import Icon from '../../components/core/Icon'
import { ChevronRight } from '@images/icons'
import styles from './More.module.css'
const PostMore = ({
@ -11,7 +11,7 @@ const PostMore = ({
}): ReactElement => (
<a className={styles.postMore} href={to}>
{children}
<Icon name="ChevronRight" />
<ChevronRight />
</a>
)

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react'
import Icon from '../../components/core/Icon'
import styles from './PrevNext.module.css'
import { ChevronLeft, ChevronRight } from '@images/icons'
interface Node {
title: string
@ -17,7 +17,7 @@ const PrevNext = ({ prev, next }: PrevNextProps): ReactElement => (
<div>
{prev && (
<a href={prev.slug}>
<Icon name="ChevronLeft" />
<ChevronLeft />
<p className={styles.label}>Newer</p>
<h3 className={styles.title}>{prev.title}</h3>
</a>
@ -28,7 +28,7 @@ const PrevNext = ({ prev, next }: PrevNextProps): ReactElement => (
<a href={next.slug}>
<p className={styles.label}>Older</p>
<h3 className={styles.title}>{next.title}</h3>
<Icon name="ChevronRight" />
<ChevronRight />
</a>
)}
</div>

View File

@ -5,9 +5,9 @@ import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import { chains, theme, wagmiConfig } from '../lib/rainbowkit'
import Copy from '../components/core/Copy'
import Meta, { HeadMetaProps } from '../components/core/HeadMeta'
import Icon from '../components/core/Icon'
import { useSiteMetadata } from '../hooks/useSiteMetadata'
import styles from './thanks.module.css'
import { ChevronLeft } from '@images/icons'
const meta: Partial<HeadMetaProps> = {
title: `Say Thanks`
@ -32,7 +32,7 @@ const BackButton = () => (
className={`link ${styles.buttonBack}`}
onClick={() => window.history.back()}
>
<Icon name="ChevronLeft" /> Go Back
<ChevronLeft /> Go Back
</button>
)

View File

@ -6,7 +6,7 @@ import PhotoTeaser from '@components/PhotoTeaser.astro'
import type { CollectionEntry } from 'astro:content'
const articles = await loadAndFormatCollection('articles')
const links = await loadAndFormatCollection('links')
// const links = await loadAndFormatCollection('links')
const photos = await loadAndFormatCollection('photos')
---

View File

@ -23,12 +23,12 @@
background: var(--alert-info);
}
.dark .alert-info {
[data-theme='dark'] .alert-info {
color: var(--alert-info);
background: rgba(248 241 227 / 20%);
}
/* .dark & {
/* [data-theme="dark"] & {
color: darken($alert-info, 40%);
background: darken($alert-info, 85%);
border-color: darken($alert-info, 90%);

View File

@ -100,11 +100,11 @@ a.btn-primary {
background-size: 100%;
}
.dark .btn[class*='icon-']::before {
[data-theme='dark'] .btn[class*='icon-']::before {
filter: invert(0.75);
}
.dark .btn.btn-primary[class*='icon-']::before {
[data-theme='dark'] .btn.btn-primary[class*='icon-']::before {
filter: invert(0);
}

View File

@ -120,7 +120,7 @@
--easing: cubic-bezier(0.75, 0, 0.08, 100%);
}
.dark {
[data-theme='dark'] {
--body-background-color: #161a1b;
--box-background-color: rgba(255 255 255 / 3%);