This commit is contained in:
parent
650f381148
commit
11fa3d1558
38 changed files with 819 additions and 148 deletions
|
|
@ -9,7 +9,10 @@
|
||||||
"languages": {
|
"languages": {
|
||||||
"CSS": {
|
"CSS": {
|
||||||
"formatter": null,
|
"formatter": null,
|
||||||
"format_on_save": "off"
|
"format_on_save": "off",
|
||||||
|
"code_actions_on_format": {
|
||||||
|
"source.fixAll.stylelint": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Vue.js": {
|
"Vue.js": {
|
||||||
"code_actions_on_format": {
|
"code_actions_on_format": {
|
||||||
|
|
@ -31,6 +34,9 @@
|
||||||
"shortenToSingleLine": true
|
"shortenToSingleLine": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"stylelint": {
|
||||||
|
"configFile": "cfg/stylelint.config.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
bun.lock
66
bun.lock
|
|
@ -6,6 +6,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "latest",
|
"@effect/platform": "latest",
|
||||||
"@effect/sql-drizzle": "latest",
|
"@effect/sql-drizzle": "latest",
|
||||||
|
"@thi.ng/color-palettes": "latest",
|
||||||
"@thi.ng/pixel": "latest",
|
"@thi.ng/pixel": "latest",
|
||||||
"@thi.ng/pixel-dither": "latest",
|
"@thi.ng/pixel-dither": "latest",
|
||||||
"a11y-dialog": "latest",
|
"a11y-dialog": "latest",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"prettier-plugin-sh": "latest",
|
"prettier-plugin-sh": "latest",
|
||||||
"stylelint": "latest",
|
"stylelint": "latest",
|
||||||
"stylelint-config-clean-order": "latest",
|
"stylelint-config-clean-order": "latest",
|
||||||
|
"stylelint-config-recommended-vue": "latest",
|
||||||
"stylelint-config-standard": "latest",
|
"stylelint-config-standard": "latest",
|
||||||
"stylelint-declaration-block-no-ignored-properties": "latest",
|
"stylelint-declaration-block-no-ignored-properties": "latest",
|
||||||
"stylelint-plugin-logical-css": "latest",
|
"stylelint-plugin-logical-css": "latest",
|
||||||
|
|
@ -369,20 +371,54 @@
|
||||||
|
|
||||||
"@thi.ng/api": ["@thi.ng/api@8.11.21", "", {}, "sha512-J6BUdUtFtwZirL3M9tkCiqBXj228z7zkxWOaDWTymwBeqY9s02vJP3mQV8l5p+YPDIRmYx/q7XVuLW1UTJRN/A=="],
|
"@thi.ng/api": ["@thi.ng/api@8.11.21", "", {}, "sha512-J6BUdUtFtwZirL3M9tkCiqBXj228z7zkxWOaDWTymwBeqY9s02vJP3mQV8l5p+YPDIRmYx/q7XVuLW1UTJRN/A=="],
|
||||||
|
|
||||||
|
"@thi.ng/arrays": ["@thi.ng/arrays@2.10.18", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/checks": "^3.7.1", "@thi.ng/compare": "^2.4.13", "@thi.ng/equiv": "^2.1.77", "@thi.ng/errors": "^2.5.27", "@thi.ng/random": "^4.1.12" } }, "sha512-4cJCVm67MqtrnwOWNdgeyM/WYAfEYeFtCh1x/3VptV6Ct8DSYyVbSK6RLYgsM04KQqAxEQniCl5/S+ZS3GFd+Q=="],
|
||||||
|
|
||||||
|
"@thi.ng/base-n": ["@thi.ng/base-n@2.7.33", "", {}, "sha512-WAVoTt1ZIYpDUs/+FauMh0p25MsG2BK0twNmJKWqAcPdP0zxb8TSQJCNrej/vTeq5c5XVsNpfGXIEa2WiXy5XQ=="],
|
||||||
|
|
||||||
|
"@thi.ng/binary": ["@thi.ng/binary@3.4.44", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-kd6ZZ0xWR5JivkSIjqb6MvFpK2us7dU3ruwmiBAw4oZKsTnfibTbgvtJLL8Q5BJnxYmI3aILQ1sCamxRMwbzBw=="],
|
||||||
|
|
||||||
"@thi.ng/canvas": ["@thi.ng/canvas@1.0.8", "", {}, "sha512-r4bRWAsiaaNx+ihTtQDi1RQFMfOLwGgoSbZhdxLzEo6t1Ga5a/cqv/WKqUWLjD1LHjxhtflS8uPcihD2ETyAOg=="],
|
"@thi.ng/canvas": ["@thi.ng/canvas@1.0.8", "", {}, "sha512-r4bRWAsiaaNx+ihTtQDi1RQFMfOLwGgoSbZhdxLzEo6t1Ga5a/cqv/WKqUWLjD1LHjxhtflS8uPcihD2ETyAOg=="],
|
||||||
|
|
||||||
"@thi.ng/checks": ["@thi.ng/checks@3.7.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-RfVBQgJN0kr00SKptAAzdDOaaRtWlctqegYogynTLkUhf8Ck516Efk/macgWTzSLEPgcumN7E9F1F3lgfRCWew=="],
|
"@thi.ng/checks": ["@thi.ng/checks@3.7.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-RfVBQgJN0kr00SKptAAzdDOaaRtWlctqegYogynTLkUhf8Ck516Efk/macgWTzSLEPgcumN7E9F1F3lgfRCWew=="],
|
||||||
|
|
||||||
|
"@thi.ng/color": ["@thi.ng/color@5.7.27", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/arrays": "^2.10.18", "@thi.ng/binary": "^3.4.44", "@thi.ng/checks": "^3.7.1", "@thi.ng/compare": "^2.4.13", "@thi.ng/compose": "^3.0.24", "@thi.ng/defmulti": "^3.0.61", "@thi.ng/errors": "^2.5.27", "@thi.ng/math": "^5.11.21", "@thi.ng/random": "^4.1.12", "@thi.ng/strings": "^3.9.6", "@thi.ng/transducers": "^9.2.21", "@thi.ng/vectors": "^7.12.23" } }, "sha512-zXI2X9lzGrp2I3o+qAg0ZFj+vWN9JMAFLfXvzLC/aAstn5N78eryhfQU55JXnlpuN4RkPbL+wlO/CZ/Sc37F1g=="],
|
||||||
|
|
||||||
|
"@thi.ng/color-palettes": ["@thi.ng/color-palettes@1.4.36", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/base-n": "^2.7.33", "@thi.ng/checks": "^3.7.1", "@thi.ng/color": "^5.7.27", "@thi.ng/errors": "^2.5.27", "@thi.ng/hex": "^2.3.65" } }, "sha512-D1dSSp1/RtTnqrBPAvvGzyZZcGFu+fempdUcSlLAd4SvE9zOZfWRS5kXn8IjwfzGCRtXL5vGcJ98Fv8N+BfYew=="],
|
||||||
|
|
||||||
|
"@thi.ng/compare": ["@thi.ng/compare@2.4.13", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-OOhxV5N7zrqKNS07ME8WZNBL81sFnE0BIjyVU0Z6zlwu2oH9mF7pZ6voF/wZH5mFfse8kcSxOA8oiQCCzCnPIQ=="],
|
||||||
|
|
||||||
|
"@thi.ng/compose": ["@thi.ng/compose@3.0.24", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/errors": "^2.5.27" } }, "sha512-WN6EeKt6EqDMICqrfGWqNpneMWw9OGaMA3T4ZFGD/AFEFmREZ0xsOMDR91oYotlvVDJeVKPYVa5CCLGx6ppQLQ=="],
|
||||||
|
|
||||||
|
"@thi.ng/defmulti": ["@thi.ng/defmulti@3.0.61", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/errors": "^2.5.27", "@thi.ng/logger": "^3.1.2" } }, "sha512-BfS0RtXgvphbvpQ9n5mDUbjsXlOafxEC8t8qL41bnPK0UJhA7IrJ/yfraDB2WmeTPGid70L9NdX0bg4ih7GGyg=="],
|
||||||
|
|
||||||
|
"@thi.ng/equiv": ["@thi.ng/equiv@2.1.77", "", {}, "sha512-qcpq7yMKNanK6NvoJaJOQeY2lHXuVWWERAHcM+wi3EpQwlVDO0smG3DOdZuAEcynnMli8JX3cyzAGFyZE86tZw=="],
|
||||||
|
|
||||||
"@thi.ng/errors": ["@thi.ng/errors@2.5.27", "", {}, "sha512-t1sgGuZqHv81lzNPSRySGHnDBvtt6h3MIMn3LdZnqMR0swwOIApw6YlheMYB63u94NdtuneQYcjdvbhOsHbwPQ=="],
|
"@thi.ng/errors": ["@thi.ng/errors@2.5.27", "", {}, "sha512-t1sgGuZqHv81lzNPSRySGHnDBvtt6h3MIMn3LdZnqMR0swwOIApw6YlheMYB63u94NdtuneQYcjdvbhOsHbwPQ=="],
|
||||||
|
|
||||||
|
"@thi.ng/hex": ["@thi.ng/hex@2.3.65", "", {}, "sha512-rX3U8DCayQVLkm6J6uMf6w1TSRQ3pE4okwkahS/A6/sDmABUlMo7t+s1psQhV07Jigr8q103+JHQpDdrcURiWw=="],
|
||||||
|
|
||||||
|
"@thi.ng/logger": ["@thi.ng/logger@3.1.2", "", {}, "sha512-WU/WCAOkxaLvGI2purG0iabueIG3Pq4CeoUogSHp/ctIh9vSVXplJYXlMy5PizttrHIJShJeHHu15x7IRyrAEQ=="],
|
||||||
|
|
||||||
"@thi.ng/math": ["@thi.ng/math@5.11.21", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-JLjHdQbCpIP6F76Vq8yBtRtDOaUJ5HKsyEhU++8bjPX7VeDf8um4Ba/PUQZS+SpX0ghTZrqirqoI7H6wWz/TaQ=="],
|
"@thi.ng/math": ["@thi.ng/math@5.11.21", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-JLjHdQbCpIP6F76Vq8yBtRtDOaUJ5HKsyEhU++8bjPX7VeDf8um4Ba/PUQZS+SpX0ghTZrqirqoI7H6wWz/TaQ=="],
|
||||||
|
|
||||||
|
"@thi.ng/memoize": ["@thi.ng/memoize@4.0.11", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-acxutHHnYAF8WfxCgDN+V/euhmC/FnjcPg1oLxgCQNET42X5jzhYyxhOEwOnkv+DRezkcgpkM9TFNF/raawlpQ=="],
|
||||||
|
|
||||||
"@thi.ng/pixel": ["@thi.ng/pixel@7.3.19", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/canvas": "^1.0.8", "@thi.ng/checks": "^3.7.1", "@thi.ng/errors": "^2.5.27", "@thi.ng/math": "^5.11.21", "@thi.ng/porter-duff": "^2.1.99" } }, "sha512-k1oUFPktxOcJEh0RuzgwoZGBkNqB1zMNvk2QILph54Cj1JJH3CcAuqJTu+N6QkXoxsgoM4DtMiDcmIdyrRokJQ=="],
|
"@thi.ng/pixel": ["@thi.ng/pixel@7.3.19", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/canvas": "^1.0.8", "@thi.ng/checks": "^3.7.1", "@thi.ng/errors": "^2.5.27", "@thi.ng/math": "^5.11.21", "@thi.ng/porter-duff": "^2.1.99" } }, "sha512-k1oUFPktxOcJEh0RuzgwoZGBkNqB1zMNvk2QILph54Cj1JJH3CcAuqJTu+N6QkXoxsgoM4DtMiDcmIdyrRokJQ=="],
|
||||||
|
|
||||||
"@thi.ng/pixel-dither": ["@thi.ng/pixel-dither@1.1.159", "", { "dependencies": { "@thi.ng/checks": "^3.7.1", "@thi.ng/math": "^5.11.21", "@thi.ng/pixel": "^7.3.19" } }, "sha512-1tQfZUPgTEe/sn0URTHbkDEtWpJyqC4WqR9BMdOcE+g+pHhp1AjGB+eru1Mhx0WKJ/jcYl669U73Qooh8L2V6A=="],
|
"@thi.ng/pixel-dither": ["@thi.ng/pixel-dither@1.1.159", "", { "dependencies": { "@thi.ng/checks": "^3.7.1", "@thi.ng/math": "^5.11.21", "@thi.ng/pixel": "^7.3.19" } }, "sha512-1tQfZUPgTEe/sn0URTHbkDEtWpJyqC4WqR9BMdOcE+g+pHhp1AjGB+eru1Mhx0WKJ/jcYl669U73Qooh8L2V6A=="],
|
||||||
|
|
||||||
"@thi.ng/porter-duff": ["@thi.ng/porter-duff@2.1.99", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-HC4rqfHGfAMijUoNlZslRZypb7MJ8BO6XpRF2Ol/O6JqHzhHtWlwbB/WGfoSiAUfdEHprSZ3KsPCDEl7MnRdpg=="],
|
"@thi.ng/porter-duff": ["@thi.ng/porter-duff@2.1.99", "", { "dependencies": { "@thi.ng/api": "^8.11.21" } }, "sha512-HC4rqfHGfAMijUoNlZslRZypb7MJ8BO6XpRF2Ol/O6JqHzhHtWlwbB/WGfoSiAUfdEHprSZ3KsPCDEl7MnRdpg=="],
|
||||||
|
|
||||||
|
"@thi.ng/random": ["@thi.ng/random@4.1.12", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/errors": "^2.5.27" } }, "sha512-IpcAgCGDdaHAahY1LJbJ9oULsmhCrQpGeAjPfB0gpGM1D7JXeOaY83ctt3tdqovT2YIaCMIUd8ahJt7VBMD+Ig=="],
|
||||||
|
|
||||||
|
"@thi.ng/strings": ["@thi.ng/strings@3.9.6", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/errors": "^2.5.27", "@thi.ng/hex": "^2.3.65", "@thi.ng/memoize": "^4.0.11" } }, "sha512-kdh66IfvlHz+iywyojTZCmUXg5d4NISrA3MwNm6fLUsuBA+yNVTOdf484Azbe1SQEUU5e9RLc6bfVeqkdthNxw=="],
|
||||||
|
|
||||||
|
"@thi.ng/timestamp": ["@thi.ng/timestamp@1.1.6", "", {}, "sha512-LGVbm9AiGwBcNHh2jieEtccy2edYCdHG3aK0no6ChH1AyAzonDdr05D+nBv0pl1h9C2AFPe9UQj43RdFgQJuUQ=="],
|
||||||
|
|
||||||
|
"@thi.ng/transducers": ["@thi.ng/transducers@9.2.21", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/arrays": "^2.10.18", "@thi.ng/checks": "^3.7.1", "@thi.ng/compare": "^2.4.13", "@thi.ng/compose": "^3.0.24", "@thi.ng/errors": "^2.5.27", "@thi.ng/math": "^5.11.21", "@thi.ng/random": "^4.1.12", "@thi.ng/timestamp": "^1.1.6" } }, "sha512-5uOiddZICcOGW5L8Y301bwjshfLZOvBlJpLneQzzsYbpqfQdOR4NlkbrJVw+IKEuqZU5B64eRqzJzOwjDAsd0g=="],
|
||||||
|
|
||||||
|
"@thi.ng/vectors": ["@thi.ng/vectors@7.12.23", "", { "dependencies": { "@thi.ng/api": "^8.11.21", "@thi.ng/binary": "^3.4.44", "@thi.ng/checks": "^3.7.1", "@thi.ng/equiv": "^2.1.77", "@thi.ng/errors": "^2.5.27", "@thi.ng/math": "^5.11.21", "@thi.ng/memoize": "^4.0.11", "@thi.ng/random": "^4.1.12", "@thi.ng/strings": "^3.9.6", "@thi.ng/transducers": "^9.2.21" } }, "sha512-+PbtaA2cYApQrzGpOKC3Yg4jxWiJw6qEchTH+YRyjVA6YdAlSdptwrvEYdUCiKhPLqa0WUrKzCUw4mqIE75yDA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
|
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
|
||||||
|
|
@ -439,7 +475,7 @@
|
||||||
|
|
||||||
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.4.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.23.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.23.0", "vue-eslint-parser": "^9.4.3" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-daU+eAekEeVz3CReE4PRW25fe+OJDKwE28jHN6LimDEnuFMbJ6H4WGogEpNof276wVP6UvzOeJQfLFjB5mW29A=="],
|
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.4.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.23.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.23.0", "vue-eslint-parser": "^9.4.3" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-daU+eAekEeVz3CReE4PRW25fe+OJDKwE28jHN6LimDEnuFMbJ6H4WGogEpNof276wVP6UvzOeJQfLFjB5mW29A=="],
|
||||||
|
|
||||||
"@vue/language-core": ["@vue/language-core@2.2.4", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-eGGdw7eWUwdIn9Fy/irJ7uavCGfgemuHQABgJ/hU1UgZFnbTg9VWeXvHQdhY+2SPQZWJqWXvRWIg67t4iWEa+Q=="],
|
"@vue/language-core": ["@vue/language-core@2.2.8", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ=="],
|
||||||
|
|
||||||
"@vue/reactivity": ["@vue/reactivity@3.5.13", "", { "dependencies": { "@vue/shared": "3.5.13" } }, "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg=="],
|
"@vue/reactivity": ["@vue/reactivity@3.5.13", "", { "dependencies": { "@vue/shared": "3.5.13" } }, "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg=="],
|
||||||
|
|
||||||
|
|
@ -451,7 +487,7 @@
|
||||||
|
|
||||||
"@vue/shared": ["@vue/shared@3.5.13", "", {}, "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="],
|
"@vue/shared": ["@vue/shared@3.5.13", "", {}, "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="],
|
||||||
|
|
||||||
"@vue/typescript-plugin": ["@vue/typescript-plugin@2.2.4", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.4", "@vue/shared": "^3.5.0" } }, "sha512-bwklUVy7TZu0Nn9d+oII0KA10eYSgqarf47/E1QvkoHS8EP5SJzPKvOhMPGJMyxwHtHE4aOu8jdWZcJ25X5eiQ=="],
|
"@vue/typescript-plugin": ["@vue/typescript-plugin@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8", "@vue/shared": "^3.5.0" } }, "sha512-9fzhFYrzIsPm5Qylv6yBmV1tISqkUhE1PD5uBwkeLCxIwNUjIbnGBdN8HszDa1ZWFWuBsbQpx7FxmV7vQincDw=="],
|
||||||
|
|
||||||
"a11y-dialog": ["a11y-dialog@8.1.1", "", { "dependencies": { "focusable-selectors": "^0.8.0" } }, "sha512-7SBLXFwhQBnEHOaIiKUUQZ5VKJa/b1jBDvPJvlejlqX2w9cpi+iHBrdjcmd4Xd6vIdsuMHGo9Is2SWu0Hzu0zg=="],
|
"a11y-dialog": ["a11y-dialog@8.1.1", "", { "dependencies": { "focusable-selectors": "^0.8.0" } }, "sha512-7SBLXFwhQBnEHOaIiKUUQZ5VKJa/b1jBDvPJvlejlqX2w9cpi+iHBrdjcmd4Xd6vIdsuMHGo9Is2SWu0Hzu0zg=="],
|
||||||
|
|
||||||
|
|
@ -573,6 +609,14 @@
|
||||||
|
|
||||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||||
|
|
||||||
|
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
|
|
||||||
|
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||||
|
|
||||||
|
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
|
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.40.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7ptk/HQiMSrEZHnAsSlBESXWj52VwgMmyTEfoNmpNN2ZXpcz13LwHfXTIghsAEud7Z5UJhDOp8U07ujcqme7wg=="],
|
"drizzle-orm": ["drizzle-orm@0.40.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7ptk/HQiMSrEZHnAsSlBESXWj52VwgMmyTEfoNmpNN2ZXpcz13LwHfXTIghsAEud7Z5UJhDOp8U07ujcqme7wg=="],
|
||||||
|
|
@ -703,6 +747,8 @@
|
||||||
|
|
||||||
"html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
|
"html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="],
|
||||||
|
|
||||||
|
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
|
||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
@ -735,7 +781,7 @@
|
||||||
|
|
||||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
|
|
||||||
|
|
@ -867,6 +913,8 @@
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||||
|
|
||||||
|
"postcss-html": ["postcss-html@1.8.0", "", { "dependencies": { "htmlparser2": "^8.0.0", "js-tokens": "^9.0.0", "postcss": "^8.5.0", "postcss-safe-parser": "^6.0.0" } }, "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ=="],
|
||||||
|
|
||||||
"postcss-resolve-nested-selector": ["postcss-resolve-nested-selector@0.1.6", "", {}, "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw=="],
|
"postcss-resolve-nested-selector": ["postcss-resolve-nested-selector@0.1.6", "", {}, "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw=="],
|
||||||
|
|
||||||
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
|
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
|
||||||
|
|
@ -879,7 +927,7 @@
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="],
|
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||||
|
|
||||||
"prettier-plugin-pkg": ["prettier-plugin-pkg@0.18.1", "", { "peerDependencies": { "prettier": "^3.0.3" } }, "sha512-FuUxvsYZR/8rsLH8s/jbPQmgYvv0yxW8LoIHCy6+Q7p4FBjjdP3DNKx8fMTOsc0SlEB1skB4o1LcahRceIh87A=="],
|
"prettier-plugin-pkg": ["prettier-plugin-pkg@0.18.1", "", { "peerDependencies": { "prettier": "^3.0.3" } }, "sha512-FuUxvsYZR/8rsLH8s/jbPQmgYvv0yxW8LoIHCy6+Q7p4FBjjdP3DNKx8fMTOsc0SlEB1skB4o1LcahRceIh87A=="],
|
||||||
|
|
||||||
|
|
@ -949,8 +997,12 @@
|
||||||
|
|
||||||
"stylelint-config-clean-order": ["stylelint-config-clean-order@7.0.0", "", { "dependencies": { "stylelint-order": "^6.0.4" }, "peerDependencies": { "stylelint": ">=14" } }, "sha512-R28w1xNliIbem3o+VIrNjAU8cMgxrGlDoXVqWW7lJ1OvSDsmNGj5aKSW6Xm7i5PK4E99T3Hs19BJFni5IbE56g=="],
|
"stylelint-config-clean-order": ["stylelint-config-clean-order@7.0.0", "", { "dependencies": { "stylelint-order": "^6.0.4" }, "peerDependencies": { "stylelint": ">=14" } }, "sha512-R28w1xNliIbem3o+VIrNjAU8cMgxrGlDoXVqWW7lJ1OvSDsmNGj5aKSW6Xm7i5PK4E99T3Hs19BJFni5IbE56g=="],
|
||||||
|
|
||||||
|
"stylelint-config-html": ["stylelint-config-html@1.1.0", "", { "peerDependencies": { "postcss-html": "^1.0.0", "stylelint": ">=14.0.0" } }, "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ=="],
|
||||||
|
|
||||||
"stylelint-config-recommended": ["stylelint-config-recommended@15.0.0", "", { "peerDependencies": { "stylelint": "^16.13.0" } }, "sha512-9LejMFsat7L+NXttdHdTq94byn25TD+82bzGRiV1Pgasl99pWnwipXS5DguTpp3nP1XjvLXVnEJIuYBfsRjRkA=="],
|
"stylelint-config-recommended": ["stylelint-config-recommended@15.0.0", "", { "peerDependencies": { "stylelint": "^16.13.0" } }, "sha512-9LejMFsat7L+NXttdHdTq94byn25TD+82bzGRiV1Pgasl99pWnwipXS5DguTpp3nP1XjvLXVnEJIuYBfsRjRkA=="],
|
||||||
|
|
||||||
|
"stylelint-config-recommended-vue": ["stylelint-config-recommended-vue@1.6.0", "", { "dependencies": { "semver": "^7.3.5", "stylelint-config-html": ">=1.0.0", "stylelint-config-recommended": ">=6.0.0" }, "peerDependencies": { "postcss-html": "^1.0.0", "stylelint": ">=14.0.0" } }, "sha512-syk1adIHvbH2T1OiR/spUK4oQy35PZIDw8Zmc7E0+eVK9Z9SK3tdMpGRT/bgGnAPpMt/WaL9K1u0tlF6xM0sMQ=="],
|
||||||
|
|
||||||
"stylelint-config-standard": ["stylelint-config-standard@37.0.0", "", { "dependencies": { "stylelint-config-recommended": "^15.0.0" }, "peerDependencies": { "stylelint": "^16.13.0" } }, "sha512-+6eBlbSTrOn/il2RlV0zYGQwRTkr+WtzuVSs1reaWGObxnxLpbcspCUYajVQHonVfxVw2U+h42azGhrBvcg8OA=="],
|
"stylelint-config-standard": ["stylelint-config-standard@37.0.0", "", { "dependencies": { "stylelint-config-recommended": "^15.0.0" }, "peerDependencies": { "stylelint": "^16.13.0" } }, "sha512-+6eBlbSTrOn/il2RlV0zYGQwRTkr+WtzuVSs1reaWGObxnxLpbcspCUYajVQHonVfxVw2U+h42azGhrBvcg8OA=="],
|
||||||
|
|
||||||
"stylelint-declaration-block-no-ignored-properties": ["stylelint-declaration-block-no-ignored-properties@2.8.0", "", { "peerDependencies": { "stylelint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-Ws8Cav7Y+SPN0JsV407LrnNXWOrqGjxShf+37GBtnU/C58Syve9c0+I/xpLcFOosST3ternykn3Lp77f3ITnFw=="],
|
"stylelint-declaration-block-no-ignored-properties": ["stylelint-declaration-block-no-ignored-properties@2.8.0", "", { "peerDependencies": { "stylelint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-Ws8Cav7Y+SPN0JsV407LrnNXWOrqGjxShf+37GBtnU/C58Syve9c0+I/xpLcFOosST3ternykn3Lp77f3ITnFw=="],
|
||||||
|
|
@ -1013,7 +1065,7 @@
|
||||||
|
|
||||||
"vue-router": ["vue-router@4.5.0", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w=="],
|
"vue-router": ["vue-router@4.5.0", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w=="],
|
||||||
|
|
||||||
"vue-tsc": ["vue-tsc@2.2.4", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.4" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-3EVHlxtpMXcb5bCaK7QDFTbEkMusDfVk0HVRrkv5hEb+Clpu9a96lKUXJAeD/akRlkoA4H8MCHgBDN19S6FnzA=="],
|
"vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="],
|
||||||
|
|
||||||
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
|
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
|
||||||
|
|
||||||
|
|
@ -1037,6 +1089,8 @@
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
|
"zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
|
||||||
|
|
||||||
|
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"@csstools/selector-specificity/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
"@csstools/selector-specificity/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
@ -1093,6 +1147,8 @@
|
||||||
|
|
||||||
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
"node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
|
||||||
|
|
||||||
|
"postcss-html/postcss-safe-parser": ["postcss-safe-parser@6.0.0", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ=="],
|
||||||
|
|
||||||
"stylelint/file-entry-cache": ["file-entry-cache@10.0.6", "", { "dependencies": { "flat-cache": "^6.1.6" } }, "sha512-0wvv16mVo9nN0Md3k7DMjgAPKG/TY4F/gYMBVb/wMThFRJvzrpaqBFqF6km9wf8QfYTN+mNg5aeaBLfy8k35uA=="],
|
"stylelint/file-entry-cache": ["file-entry-cache@10.0.6", "", { "dependencies": { "flat-cache": "^6.1.6" } }, "sha512-0wvv16mVo9nN0Md3k7DMjgAPKG/TY4F/gYMBVb/wMThFRJvzrpaqBFqF6km9wf8QfYTN+mNg5aeaBLfy8k35uA=="],
|
||||||
|
|
||||||
"stylelint/ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
|
"stylelint/ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,18 @@
|
||||||
"useTabs": false
|
"useTabs": false
|
||||||
},
|
},
|
||||||
"markup": {
|
"markup": {
|
||||||
|
"astro.scriptIndent": true,
|
||||||
|
"astro.styleIndent": true,
|
||||||
|
"astroAttrShorthand": true,
|
||||||
"closingBracketSameLine": false,
|
"closingBracketSameLine": false,
|
||||||
"closingTagLineBreakForEmpty": "never",
|
"closingTagLineBreakForEmpty": "never",
|
||||||
"component.selfClosing": false,
|
"component.selfClosing": false,
|
||||||
|
"component.whitespaceSensitivity": "strict",
|
||||||
"doctypeKeywordCase": "lower",
|
"doctypeKeywordCase": "lower",
|
||||||
"formatComments": true,
|
"formatComments": true,
|
||||||
"html.normal.selfClosing": false,
|
"html.normal.selfClosing": false,
|
||||||
|
"html.scriptIndent": true,
|
||||||
|
"html.styleIndent": true,
|
||||||
"html.void.selfClosing": false,
|
"html.void.selfClosing": false,
|
||||||
"indentWidth": 2,
|
"indentWidth": 2,
|
||||||
"lineBreak": "lf",
|
"lineBreak": "lf",
|
||||||
|
|
@ -60,8 +66,15 @@
|
||||||
"scriptFormatter": "dprint",
|
"scriptFormatter": "dprint",
|
||||||
"scriptIndent": true,
|
"scriptIndent": true,
|
||||||
"styleIndent": true,
|
"styleIndent": true,
|
||||||
"svg.selfClosing": true,
|
"svg.selfClosing": false,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
|
"vBindSameNameShortHand": true,
|
||||||
|
"vBindStyle": "short",
|
||||||
|
"vForDelimiterStyle": "of",
|
||||||
|
"vOnStyle": "short",
|
||||||
|
"vSlotStyle": "short",
|
||||||
|
"vue.scriptIndent": true,
|
||||||
|
"vue.styleIndent": true,
|
||||||
"whitespaceSensitivity": "strict"
|
"whitespaceSensitivity": "strict"
|
||||||
},
|
},
|
||||||
"newLineKind": "lf",
|
"newLineKind": "lf",
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
// @ts-expect-error -- La dépendance ne dispose pas de types.
|
// @ts-expect-error -- La dépendance ne dispose pas de types.
|
||||||
import { propertyGroups } from "stylelint-config-clean-order";
|
import { propertyGroups } from "stylelint-config-clean-order";
|
||||||
|
|
||||||
|
|
||||||
const propertiesOrder = Array.from(propertyGroups).map(properties => ({
|
const propertiesOrder = Array.from(propertyGroups).map(properties => ({
|
||||||
emptyLineBefore: "never",
|
emptyLineBefore: "never",
|
||||||
noEmptyLineBetween: true,
|
noEmptyLineBetween: true,
|
||||||
properties,
|
properties,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
/** @type {import("stylelint").Config} */
|
/** @type {import("stylelint").Config} */
|
||||||
export default {
|
export default {
|
||||||
extends: ["stylelint-config-standard", "stylelint-config-clean-order"],
|
extends: ["stylelint-config-standard", "stylelint-config-clean-order", "stylelint-config-recommended-vue"],
|
||||||
plugins: ["stylelint-declaration-block-no-ignored-properties"],
|
plugins: ["stylelint-declaration-block-no-ignored-properties"],
|
||||||
rules: {
|
rules: {
|
||||||
"custom-property-pattern": null,
|
"custom-property-pattern": null,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"vtsls",
|
"vtsls",
|
||||||
"quartary",
|
"quartary",
|
||||||
"fieldset",
|
"fieldset",
|
||||||
"tabindex"
|
"tabindex",
|
||||||
|
"currentcolor"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ services:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: journal-media-vue
|
container_name: journal-media-vue
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8080:80
|
- 127.0.0.1:8080:8
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
develop:
|
develop:
|
||||||
watch:
|
watch:
|
||||||
|
|
|
||||||
8
justfile
8
justfile
|
|
@ -33,7 +33,7 @@ dev:
|
||||||
|
|
||||||
# Compile le projet.
|
# Compile le projet.
|
||||||
build:
|
build:
|
||||||
-bun --bun vue-tsc --build .
|
-bun --bun vue-tsc --build --noEmit
|
||||||
bun --bun vite build
|
bun --bun vite build
|
||||||
|
|
||||||
# Génère l'image Docker.
|
# Génère l'image Docker.
|
||||||
|
|
@ -84,7 +84,7 @@ lint-css:
|
||||||
--cache --cache-location "{{ cacheFolder }}/{{ stylelintCacheFile }}" \
|
--cache --cache-location "{{ cacheFolder }}/{{ stylelintCacheFile }}" \
|
||||||
--config "{{ stylelintConfigFile }}" \
|
--config "{{ stylelintConfigFile }}" \
|
||||||
--fix \
|
--fix \
|
||||||
{{ stylesFolder }}
|
"**/*.{css,vue}"
|
||||||
|
|
||||||
# Analyse le code TypeScript et Vue.
|
# Analyse le code TypeScript et Vue.
|
||||||
lint-js fix="":
|
lint-js fix="":
|
||||||
|
|
@ -93,6 +93,10 @@ lint-js fix="":
|
||||||
--config "{{ esLintConfigFile }}" \
|
--config "{{ esLintConfigFile }}" \
|
||||||
{{ fix }}
|
{{ fix }}
|
||||||
|
|
||||||
|
# Vérifie les types du code TypeScript et Vue avec le compilateur TypeScript.
|
||||||
|
lint-types:
|
||||||
|
bun vue-tsc --noEmit
|
||||||
|
|
||||||
# Analyse le code CSS avec ESLint.
|
# Analyse le code CSS avec ESLint.
|
||||||
lint-css-eslint fix="":
|
lint-css-eslint fix="":
|
||||||
bun --bun eslint --config "{{ esLintCssConfigFile }}" {{ fix }}
|
bun --bun eslint --config "{{ esLintCssConfigFile }}" {{ fix }}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "^0.77.4",
|
"@effect/platform": "^0.77.4",
|
||||||
"@effect/sql-drizzle": "^0.29.4",
|
"@effect/sql-drizzle": "^0.29.4",
|
||||||
|
"@thi.ng/color-palettes": "^1.4.36",
|
||||||
"@thi.ng/pixel": "^7.3.19",
|
"@thi.ng/pixel": "^7.3.19",
|
||||||
"@thi.ng/pixel-dither": "^1.1.159",
|
"@thi.ng/pixel-dither": "^1.1.159",
|
||||||
"a11y-dialog": "^8.1.1",
|
"a11y-dialog": "^8.1.1",
|
||||||
|
|
@ -22,7 +23,7 @@
|
||||||
"@types/bun": "^1.2.4",
|
"@types/bun": "^1.2.4",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/eslint-config-typescript": "^14.4.0",
|
"@vue/eslint-config-typescript": "^14.4.0",
|
||||||
"@vue/typescript-plugin": "^2.2.4",
|
"@vue/typescript-plugin": "^2.2.8",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"cspell": "^8.17.5",
|
"cspell": "^8.17.5",
|
||||||
"drizzle-kit": "^0.30.5",
|
"drizzle-kit": "^0.30.5",
|
||||||
|
|
@ -33,17 +34,18 @@
|
||||||
"jiti": "^2.4.2",
|
"jiti": "^2.4.2",
|
||||||
"knip": "^5.45.0",
|
"knip": "^5.45.0",
|
||||||
"lightningcss": "^1.29.1",
|
"lightningcss": "^1.29.1",
|
||||||
"prettier": "^3.5.2",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-pkg": "^0.18.1",
|
"prettier-plugin-pkg": "^0.18.1",
|
||||||
"prettier-plugin-sh": "^0.15.0",
|
"prettier-plugin-sh": "^0.15.0",
|
||||||
"stylelint": "^16.15.0",
|
"stylelint": "^16.15.0",
|
||||||
"stylelint-config-clean-order": "^7.0.0",
|
"stylelint-config-clean-order": "^7.0.0",
|
||||||
|
"stylelint-config-recommended-vue": "^1.6.0",
|
||||||
"stylelint-config-standard": "^37.0.0",
|
"stylelint-config-standard": "^37.0.0",
|
||||||
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
|
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
|
||||||
"stylelint-plugin-logical-css": "^1.2.1",
|
"stylelint-plugin-logical-css": "^1.2.1",
|
||||||
"tsr": "^1.3.4",
|
"tsr": "^1.3.4",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.0",
|
||||||
"vue-tsc": "^2.2.4"
|
"vue-tsc": "^2.2.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
#app-loading {
|
#app-loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
|
||||||
text-align: center;
|
|
||||||
align-content: center;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: salmon;
|
inset: 0;
|
||||||
|
align-content: center;
|
||||||
font-family: Banquise;
|
font-family: Banquise;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: salmon;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
width: 10ch;
|
width: 10ch;
|
||||||
|
|
@ -25,15 +25,19 @@
|
||||||
0% {
|
0% {
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
25% {
|
25% {
|
||||||
content: ".";
|
content: ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
content: "..";
|
content: "..";
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
content: "...";
|
content: "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
<style>
|
<style>
|
||||||
path { fill: #000; }
|
path { fill: #000; }
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
path { fill: #FFF; }
|
path { fill: #FFF; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 750 B |
96
public/sortable-table.css
Normal file
96
public/sortable-table.css
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
top: -30em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable td, table.sortable th {
|
||||||
|
width: 8em;
|
||||||
|
padding: 0.125em 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: thin solid #888;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th.no-sort {
|
||||||
|
padding-top: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th:nth-child(5) {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th button {
|
||||||
|
cursor: pointer;
|
||||||
|
inset: 0;
|
||||||
|
display: inline;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1px;
|
||||||
|
padding: 4px;
|
||||||
|
border: none;
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th button span {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th[aria-sort="descending"] span::after {
|
||||||
|
content: "▼";
|
||||||
|
top: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th[aria-sort="ascending"] span::after {
|
||||||
|
content: "▲";
|
||||||
|
top: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.show-unsorted-icon th:not([aria-sort]) button span::after {
|
||||||
|
content: "♢";
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
left: -4px;
|
||||||
|
font-size: 100%;
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable td.num {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable tbody tr:nth-child(odd) {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus and hover styling */
|
||||||
|
|
||||||
|
table.sortable th button:focus, table.sortable th button:hover {
|
||||||
|
padding: 2px;
|
||||||
|
border: 2px solid currentcolor;
|
||||||
|
background-color: #e5f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th button:focus span, table.sortable th button:hover span {
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.sortable th:not([aria-sort]) button:focus span::after, table.sortable
|
||||||
|
th:not([aria-sort])
|
||||||
|
button:hover
|
||||||
|
span::after {
|
||||||
|
content: "▼";
|
||||||
|
top: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
167
public/sortable-table.js
Normal file
167
public/sortable-table.js
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* This content is licensed according to the W3C Software License at
|
||||||
|
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||||
|
*
|
||||||
|
* File: sortable-table.js
|
||||||
|
*
|
||||||
|
* Desc: Adds sorting to a HTML data table that implements ARIA Authoring Practices
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
class SortableTable {
|
||||||
|
constructor(tableNode) {
|
||||||
|
this.tableNode = tableNode;
|
||||||
|
|
||||||
|
this.columnHeaders = tableNode.querySelectorAll("thead th");
|
||||||
|
|
||||||
|
this.sortColumns = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < this.columnHeaders.length; i++) {
|
||||||
|
var ch = this.columnHeaders[i];
|
||||||
|
var buttonNode = ch.querySelector("button");
|
||||||
|
if (buttonNode) {
|
||||||
|
this.sortColumns.push(i);
|
||||||
|
buttonNode.setAttribute("data-column-index", i);
|
||||||
|
buttonNode.addEventListener("click", this.handleClick.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.optionCheckbox = document.querySelector(
|
||||||
|
"input[type=\"checkbox\"][value=\"show-unsorted-icon\"]",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.optionCheckbox) {
|
||||||
|
this.optionCheckbox.addEventListener(
|
||||||
|
"change",
|
||||||
|
this.handleOptionChange.bind(this),
|
||||||
|
);
|
||||||
|
if (this.optionCheckbox.checked) {
|
||||||
|
this.tableNode.classList.add("show-unsorted-icon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(event) {
|
||||||
|
var tgt = event.currentTarget;
|
||||||
|
this.setColumnHeaderSort(tgt.getAttribute("data-column-index"));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOptionChange(event) {
|
||||||
|
var tgt = event.currentTarget;
|
||||||
|
|
||||||
|
if (tgt.checked) {
|
||||||
|
this.tableNode.classList.add("show-unsorted-icon");
|
||||||
|
} else {
|
||||||
|
this.tableNode.classList.remove("show-unsorted-icon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EVENT HANDLERS */
|
||||||
|
|
||||||
|
setColumnHeaderSort(columnIndex) {
|
||||||
|
if (typeof columnIndex === "string") {
|
||||||
|
columnIndex = parseInt(columnIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < this.columnHeaders.length; i++) {
|
||||||
|
var ch = this.columnHeaders[i];
|
||||||
|
var buttonNode = ch.querySelector("button");
|
||||||
|
if (i === columnIndex) {
|
||||||
|
var value = ch.getAttribute("aria-sort");
|
||||||
|
if (value === "descending") {
|
||||||
|
ch.setAttribute("aria-sort", "ascending");
|
||||||
|
this.sortColumn(
|
||||||
|
columnIndex,
|
||||||
|
"ascending",
|
||||||
|
ch.classList.contains("num"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ch.setAttribute("aria-sort", "descending");
|
||||||
|
this.sortColumn(
|
||||||
|
columnIndex,
|
||||||
|
"descending",
|
||||||
|
ch.classList.contains("num"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ch.hasAttribute("aria-sort") && buttonNode) {
|
||||||
|
ch.removeAttribute("aria-sort");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortColumn(columnIndex, sortValue, isNumber) {
|
||||||
|
function compareValues(a, b) {
|
||||||
|
if (sortValue === "ascending") {
|
||||||
|
if (a.value === b.value) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
if (isNumber) {
|
||||||
|
return a.value - b.value;
|
||||||
|
} else {
|
||||||
|
return a.value < b.value ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (a.value === b.value) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
if (isNumber) {
|
||||||
|
return b.value - a.value;
|
||||||
|
} else {
|
||||||
|
return a.value > b.value ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof isNumber !== "boolean") {
|
||||||
|
isNumber = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tbodyNode = this.tableNode.querySelector("tbody");
|
||||||
|
var rowNodes = [];
|
||||||
|
var dataCells = [];
|
||||||
|
|
||||||
|
var rowNode = tbodyNode.firstElementChild;
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
while (rowNode) {
|
||||||
|
rowNodes.push(rowNode);
|
||||||
|
var rowCells = rowNode.querySelectorAll("th, td");
|
||||||
|
var dataCell = rowCells[columnIndex];
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
data.index = index;
|
||||||
|
data.value = dataCell.textContent.toLowerCase().trim();
|
||||||
|
if (isNumber) {
|
||||||
|
data.value = parseFloat(data.value);
|
||||||
|
}
|
||||||
|
dataCells.push(data);
|
||||||
|
rowNode = rowNode.nextElementSibling;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataCells.sort(compareValues);
|
||||||
|
|
||||||
|
// remove rows
|
||||||
|
while (tbodyNode.firstChild) {
|
||||||
|
tbodyNode.removeChild(tbodyNode.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add sorted rows
|
||||||
|
for (var i = 0; i < dataCells.length; i += 1) {
|
||||||
|
tbodyNode.appendChild(rowNodes[dataCells[i].index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize sortable table buttons
|
||||||
|
window.addEventListener("load", function() {
|
||||||
|
var sortableTables = document.querySelectorAll("table.sortable");
|
||||||
|
for (var i = 0; i < sortableTables.length; i++) {
|
||||||
|
new SortableTable(sortableTables[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { TmdbMovieSearchResponseResult } from "@/libs/apis/tmdb/schemas";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
entryId: number;
|
|
||||||
tmdbSearchData: TmdbMovieSearchResponseResult;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template></template>
|
|
||||||
|
|
@ -1,35 +1,58 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
|
import type { MergedTmdbLocalData } from "@/libs/search/schemas.ts";
|
||||||
import type { Values } from "@/libs/utils/types";
|
import type { AriaSortValues } from "@/libs/search/types.ts";
|
||||||
|
import type { Values } from "@/libs/utils/types.ts";
|
||||||
|
|
||||||
import { tupleByTitle } from "@/libs/apis/tmdb/orders.ts";
|
import TableHeadingSortableColumn from "@/components/tables/TableHeadingSortableColumn.vue";
|
||||||
|
import {
|
||||||
|
getTmdbSortFunction,
|
||||||
|
TMDB_SORT_VALUES,
|
||||||
|
type TmdbSortData,
|
||||||
|
type TmdbSortValues,
|
||||||
|
toggleSortOrder,
|
||||||
|
} from "@/libs/apis/tmdb/orders.ts";
|
||||||
|
import { ARIA_SORT_VALUES } from "@/libs/search/constants";
|
||||||
import { Array as Arr, Match, pipe } from "effect";
|
import { Array as Arr, Match, pipe } from "effect";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
const SORT_ORDERS = {
|
const emit = defineEmits<(e: "entry-dialog-wanted", tmdbId: number) => void>();
|
||||||
ORIGINAL: "original",
|
|
||||||
POPULARITY: "popularity",
|
|
||||||
RELEASE_DATE: "release_date",
|
|
||||||
TITLE: "title",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const { searchData } = defineProps<{ searchData: Map<number, MergedTmdbLocalData> }>();
|
const { searchData } = defineProps<{ searchData: Map<number, MergedTmdbLocalData> }>();
|
||||||
const sortOrder = ref<Values<typeof SORT_ORDERS>>(SORT_ORDERS.TITLE);
|
const sort = ref<TmdbSortData>({
|
||||||
|
sortOrder: ARIA_SORT_VALUES.NONE,
|
||||||
|
sortValue: TMDB_SORT_VALUES.ORIGINAL,
|
||||||
|
});
|
||||||
|
|
||||||
const sortedData = computed(() =>
|
const sortedData = computed(() =>
|
||||||
pipe(
|
pipe(
|
||||||
Array.from(searchData.entries()),
|
Array.from(searchData.entries()),
|
||||||
(result: [number, MergedTmdbLocalData][]) =>
|
(result: [number, MergedTmdbLocalData][]) => Arr.sort(result, getTmdbSortFunction(sort.value)),
|
||||||
Match.value(sortOrder.value).pipe(
|
|
||||||
Match.when(SORT_ORDERS.ORIGINAL, () => result),
|
|
||||||
Match.when(SORT_ORDERS.POPULARITY, () => Arr.sort(result, tupleByTitle)),
|
|
||||||
Match.when(SORT_ORDERS.RELEASE_DATE, () => Arr.sort(result, tupleByTitle)),
|
|
||||||
Match.when(SORT_ORDERS.TITLE, () => Arr.sort(result, tupleByTitle)),
|
|
||||||
Match.exhaustive,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.debug(sortedData.value);
|
|
||||||
|
const updateSort = (newSortValue: Values<typeof TMDB_SORT_VALUES>): void => {
|
||||||
|
const oldSort = sort.value;
|
||||||
|
const isNewSortValue = oldSort.sortValue !== newSortValue;
|
||||||
|
|
||||||
|
const newSortOrder: AriaSortValues = Match.value(isNewSortValue).pipe(
|
||||||
|
Match.when(false, () => toggleSortOrder(oldSort.sortOrder)),
|
||||||
|
Match.orElse(() => ARIA_SORT_VALUES.ASCENDING),
|
||||||
|
);
|
||||||
|
|
||||||
|
sort.value = { sortOrder: newSortOrder, sortValue: newSortValue };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortData = (sortValue: TmdbSortValues): TmdbSortData => {
|
||||||
|
return {
|
||||||
|
sortOrder: sortValue === sort.value.sortValue ? sort.value.sortOrder : ARIA_SORT_VALUES.NONE,
|
||||||
|
sortValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gestionnaire d'événements
|
||||||
|
|
||||||
|
const onRowClicked = (tmdbId: number) => {
|
||||||
|
emit("entry-dialog-wanted", tmdbId);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -37,18 +60,38 @@
|
||||||
<table v-show="sortedData?.length">
|
<table v-show="sortedData?.length">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Nom</th>
|
<TableHeadingSortableColumn
|
||||||
<th scope="col">Année</th>
|
:sort-data="getSortData(TMDB_SORT_VALUES.ORIGINAL)" @click="updateSort(TMDB_SORT_VALUES.ORIGINAL)"
|
||||||
<th scope="col">Popularité</th>
|
>
|
||||||
|
Index
|
||||||
|
</TableHeadingSortableColumn>
|
||||||
|
<TableHeadingSortableColumn
|
||||||
|
:sort-data="getSortData(TMDB_SORT_VALUES.TITLE)" @click="updateSort(TMDB_SORT_VALUES.TITLE)"
|
||||||
|
>
|
||||||
|
Nom
|
||||||
|
</TableHeadingSortableColumn>
|
||||||
|
<TableHeadingSortableColumn
|
||||||
|
:sort-data="getSortData(TMDB_SORT_VALUES.RELEASE_DATE)" @click="updateSort(TMDB_SORT_VALUES.RELEASE_DATE)"
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
</TableHeadingSortableColumn>
|
||||||
|
<TableHeadingSortableColumn
|
||||||
|
:sort-data="getSortData(TMDB_SORT_VALUES.POPULARITY)" @click="updateSort(TMDB_SORT_VALUES.POPULARITY)"
|
||||||
|
>
|
||||||
|
Popularité
|
||||||
|
</TableHeadingSortableColumn>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="result in sortedData" :key="result[0]" class="row-link"
|
v-for="result of sortedData" :key="result[0]" class="row-link"
|
||||||
:data-artwork-id="result[1].artWorkId" :data-entry-id="result[1].entryId" :data-tmdb-id="result[0]"
|
:data-artwork-id="result[1].artWorkId" :data-entry-id="result[1].entryId" :data-tmdb-id="result[0]"
|
||||||
tabindex="0"
|
tabindex="0" @click="onRowClicked(result[0])" @keypress="onRowClicked(result[0])"
|
||||||
>
|
>
|
||||||
|
<th class="name" scope="row">
|
||||||
|
{{ result[1].original_result_index }}
|
||||||
|
</th>
|
||||||
<th class="name" scope="row">
|
<th class="name" scope="row">
|
||||||
{{ result[1].original_title }}
|
{{ result[1].original_title }}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -80,13 +123,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th {
|
|
||||||
font-weight: 120;
|
|
||||||
font-size: var(--s0);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr > * + * {
|
tbody tr > * + * {
|
||||||
padding-inline-start: var(--s-2);
|
padding-inline-start: var(--s-2);
|
||||||
}
|
}
|
||||||
|
|
@ -100,8 +136,8 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--root-text-color);
|
|
||||||
color: var(--root-background-color);
|
color: var(--root-background-color);
|
||||||
|
background: var(--root-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
|
|
|
||||||
78
src/components/dialogs/EditEntryDialog.vue
Normal file
78
src/components/dialogs/EditEntryDialog.vue
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
|
||||||
|
|
||||||
|
import ImposterBox from "@/components/dialogs/ImposterBox.vue";
|
||||||
|
import { Images } from "@/services/images.ts";
|
||||||
|
import { RuntimeClient } from "@/services/runtime-client";
|
||||||
|
import { Url } from "@effect/platform";
|
||||||
|
import { Effect } from "effect";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { watchEffect } from "vue";
|
||||||
|
import { useTemplateRef } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["dialog-hidden"]);
|
||||||
|
const { entryData } = defineProps<{ entryData: MergedTmdbLocalData }>();
|
||||||
|
|
||||||
|
const ditheredPoster = ref<HTMLCanvasElement>();
|
||||||
|
const imageContainer = useTemplateRef("imageContainer");
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
emit("dialog-hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(async () => {
|
||||||
|
ditheredPoster.value = await RuntimeClient.runPromise(Effect.gen(function*() {
|
||||||
|
if (!entryData.artWorkCoverPath || !imageContainer.value) return undefined;
|
||||||
|
const imageService: Images = yield* Images;
|
||||||
|
const originalUrl = yield* Url.fromString(`https://image.tmdb.org/t/p/w500/${entryData.artWorkCoverPath}`);
|
||||||
|
const originalImage = yield* imageService.imageFromUrl(originalUrl);
|
||||||
|
const ditheredImage = yield* imageService.ditherImage(originalImage, imageContainer.value);
|
||||||
|
|
||||||
|
return ditheredImage;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.debug("EditEntryDialog mounted");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ImposterBox dialog-id="edit-entry" :is-toggled="true" @dialog-hidden="closeDialog">
|
||||||
|
<template #title>Éditer une entrée</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<section aria-labelledby="media-title" class="switcher">
|
||||||
|
<div ref="imageContainer" class="canvas-container"> </div>
|
||||||
|
|
||||||
|
<div class="stack">
|
||||||
|
<h3 id="media-title">{{ entryData.original_title }}</h3>
|
||||||
|
<p class="center">{{ entryData.release_date }} | {{ entryData.original_language }} </p>
|
||||||
|
<p class="overview">{{ entryData.overview }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</ImposterBox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="css">
|
||||||
|
.canvas-container {
|
||||||
|
aspect-ratio: 0.6;
|
||||||
|
width: 400px;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 600px;
|
||||||
|
max-height: 600px;
|
||||||
|
background: var(--bg25-secondary);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
max-inline-size: 40rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import A11yDialog from "a11y-dialog";
|
import A11yDialog from "a11y-dialog";
|
||||||
import { computed, ref, useTemplateRef, watchEffect } from "vue";
|
import { computed, ref, useTemplateRef, watchEffect } from "vue";
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
const { dialogId, isToggled } = defineProps<{
|
const { dialogId, isToggled } = defineProps<{
|
||||||
/** ID de la modale. */
|
/** ID de la modale. */
|
||||||
|
|
@ -26,6 +27,14 @@
|
||||||
dialog.value?.show();
|
dialog.value?.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.debug("ImposterBox mounted");
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
console.debug("ImposterBox unmounted");
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Search from "@/libs/search/search.ts";
|
import { formDataToRecord } from "@/libs/search/search.ts";
|
||||||
import { Effect, pipe } from "effect";
|
import { Effect, pipe } from "effect";
|
||||||
import { useTemplateRef } from "vue";
|
import { useTemplateRef } from "vue";
|
||||||
|
import { onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
import ImposterBox from "./ImposterBox.vue";
|
import ImposterBox from "./ImposterBox.vue";
|
||||||
|
|
@ -29,11 +30,15 @@
|
||||||
await pipe(
|
await pipe(
|
||||||
Effect.fromNullable(form.value),
|
Effect.fromNullable(form.value),
|
||||||
Effect.andThen((form: HTMLFormElement) => new FormData(form)),
|
Effect.andThen((form: HTMLFormElement) => new FormData(form)),
|
||||||
Effect.andThen((formData: FormData) => Search.formDataToRecord(formData)),
|
Effect.andThen((formData: FormData) => formDataToRecord(formData)),
|
||||||
Effect.tap(query => router.push({ path: "/search", query })),
|
Effect.tap(query => router.push({ path: "/search", query })),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.debug("SearchMediaDialog mounted");
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -66,8 +71,8 @@
|
||||||
<div class="field stack">
|
<div class="field stack">
|
||||||
<label for="query">Titre</label>
|
<label for="query">Titre</label>
|
||||||
<input
|
<input
|
||||||
id="query" for="add-media-form" name="query"
|
id="query" autofocus for="add-media-form"
|
||||||
required type="text"
|
name="query" required type="text"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="field stack">
|
<div class="field stack">
|
||||||
70
src/components/tables/TableHeadingSortableColumn.vue
Normal file
70
src/components/tables/TableHeadingSortableColumn.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TmdbSortData } from "@/libs/apis/tmdb/orders";
|
||||||
|
|
||||||
|
const emit = defineEmits(["click"]);
|
||||||
|
|
||||||
|
const { sortData } = defineProps<{ sortData: TmdbSortData }>();
|
||||||
|
|
||||||
|
const onButtonClicked = (event: Event): void => {
|
||||||
|
event.preventDefault();
|
||||||
|
emit("click");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<th :aria-sort="sortData.sortOrder" scope="col">
|
||||||
|
<button
|
||||||
|
class="button-invisible" :data-sort-value="sortData.sortValue" role="button"
|
||||||
|
@click="onButtonClicked"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
<span aria-hidden="true" class="sort-indicator"></span>
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
th {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
align-content: center;
|
||||||
|
font-size: var(--s-1);
|
||||||
|
font-weight: var(--brkly-font-weight-semibold);
|
||||||
|
text-align: left;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--letter-spacing-small);
|
||||||
|
background: inherit;
|
||||||
|
|
||||||
|
.sort-indicator {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "♢";
|
||||||
|
display: inline-block;
|
||||||
|
min-inline-size: var(--s-1);
|
||||||
|
border-inline-end-style: var(--s3);
|
||||||
|
color: currentcolor;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
table th[aria-sort="descending"] & {
|
||||||
|
content: "▼";
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th[aria-sort="ascending"] & {
|
||||||
|
content: "▲";
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.sort-indicator {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
CREATE TABLE `diary_entries` (
|
CREATE TABLE `diary_entries` (
|
||||||
`art_work_id` integer NOT NULL,
|
`art_work_id` integer NOT NULL,
|
||||||
`date_created` text(10) NOT NULL,
|
`date_created` integer NOT NULL,
|
||||||
`date_modified` text(10) NOT NULL,
|
`date_modified` integer NOT NULL,
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
`state_id` integer NOT NULL,
|
`state_id` integer NOT NULL,
|
||||||
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action,
|
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action,
|
||||||
|
|
@ -24,13 +24,16 @@ CREATE TABLE `diary_entries_states` (
|
||||||
CREATE UNIQUE INDEX `diary_entries_states_state_unique` ON `diary_entries_states` (`state`);--> statement-breakpoint
|
CREATE UNIQUE INDEX `diary_entries_states_state_unique` ON `diary_entries_states` (`state`);--> statement-breakpoint
|
||||||
CREATE TABLE `viewings` (
|
CREATE TABLE `viewings` (
|
||||||
`art_work_id` integer NOT NULL,
|
`art_work_id` integer NOT NULL,
|
||||||
`date` text(10) NOT NULL,
|
`date` integer NOT NULL,
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action
|
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE `art_works` (
|
CREATE TABLE `art_works` (
|
||||||
`cover_path` text,
|
`cover_path` text,
|
||||||
|
`date_created` integer NOT NULL,
|
||||||
|
`date_metadata_updated` integer NOT NULL,
|
||||||
|
`date_updated` integer NOT NULL,
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
`medium_type_id` integer NOT NULL,
|
`medium_type_id` integer NOT NULL,
|
||||||
`name` text NOT NULL,
|
`name` text NOT NULL,
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "da9e1cf6-aba7-4b5a-a839-3b0fe3dce876",
|
"id": "8b217318-7662-4f81-bc55-92d5c6ddf2b2",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"diary_entries": {
|
"diary_entries": {
|
||||||
|
|
@ -16,14 +16,14 @@
|
||||||
},
|
},
|
||||||
"date_created": {
|
"date_created": {
|
||||||
"name": "date_created",
|
"name": "date_created",
|
||||||
"type": "text(10)",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"date_modified": {
|
"date_modified": {
|
||||||
"name": "date_modified",
|
"name": "date_modified",
|
||||||
"type": "text(10)",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
"name": "date",
|
"name": "date",
|
||||||
"type": "text(10)",
|
"type": "integer",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
|
|
@ -221,6 +221,27 @@
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"date_created": {
|
||||||
|
"name": "date_created",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"date_metadata_updated": {
|
||||||
|
"name": "date_metadata_updated",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"date_updated": {
|
||||||
|
"name": "date_updated",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1740666437095,
|
"when": 1740814587298,
|
||||||
"tag": "0000_open_the_twelve",
|
"tag": "0000_unusual_karen_page",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import { ArtWorks, Genres } from "./works";
|
||||||
|
|
||||||
export const DiaryEntries = table("diary_entries", {
|
export const DiaryEntries = table("diary_entries", {
|
||||||
artWorkId: t.integer("art_work_id").references((): AnySQLiteColumn => ArtWorks.id).notNull(),
|
artWorkId: t.integer("art_work_id").references((): AnySQLiteColumn => ArtWorks.id).notNull(),
|
||||||
dateCreated: t.text("date_created", { length: 10 }).notNull(),
|
dateCreated: t.integer("date_created", { mode: "timestamp" }).notNull(),
|
||||||
dateModified: t.text("date_modified", { length: 10 }).notNull(),
|
dateModified: t.integer("date_modified", { mode: "timestamp" }).notNull(),
|
||||||
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
||||||
stateId: t.integer("state_id").references((): AnySQLiteColumn => DiaryEntriesStates.id).notNull(),
|
stateId: t.integer("state_id").references((): AnySQLiteColumn => DiaryEntriesStates.id).notNull(),
|
||||||
});
|
});
|
||||||
|
|
@ -31,7 +31,7 @@ export const DiaryEntriesStates = table("diary_entries_states", {
|
||||||
|
|
||||||
export const Viewings = table("viewings", {
|
export const Viewings = table("viewings", {
|
||||||
artWorkId: t.integer("art_work_id").references((): AnySQLiteColumn => ArtWorks.id).notNull(),
|
artWorkId: t.integer("art_work_id").references((): AnySQLiteColumn => ArtWorks.id).notNull(),
|
||||||
date: t.text("date", { length: 10 }).notNull(),
|
date: t.integer("date", { mode: "timestamp" }).notNull(),
|
||||||
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -39,8 +39,8 @@ export const Viewings = table("viewings", {
|
||||||
|
|
||||||
export const DiaryEntrySchema = Schema.Struct({
|
export const DiaryEntrySchema = Schema.Struct({
|
||||||
artWorkId: Schema.NonNegativeInt,
|
artWorkId: Schema.NonNegativeInt,
|
||||||
dateCreated: Schema.String,
|
dateCreated: Schema.Number,
|
||||||
dateModified: Schema.String,
|
dateModified: Schema.Number,
|
||||||
id: Schema.NonNegativeInt,
|
id: Schema.NonNegativeInt,
|
||||||
stateId: Schema.NonNegativeInt,
|
stateId: Schema.NonNegativeInt,
|
||||||
});
|
});
|
||||||
|
|
@ -61,7 +61,7 @@ export type DiaryEntryState = Schema.Schema.Type<typeof DiaryEntryStateSchema>;
|
||||||
|
|
||||||
export const ViewingSchema = Schema.Struct({
|
export const ViewingSchema = Schema.Struct({
|
||||||
artWorkId: Schema.NonNegativeInt,
|
artWorkId: Schema.NonNegativeInt,
|
||||||
date: Schema.String,
|
date: Schema.Number,
|
||||||
id: Schema.NonNegativeInt,
|
id: Schema.NonNegativeInt,
|
||||||
});
|
});
|
||||||
export type Viewing = Schema.Schema.Type<typeof ViewingSchema>;
|
export type Viewing = Schema.Schema.Type<typeof ViewingSchema>;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ export const MediaTypes = table("media_types", {
|
||||||
|
|
||||||
export const ArtWorks = table("art_works", {
|
export const ArtWorks = table("art_works", {
|
||||||
coverPath: t.text("cover_path").unique(),
|
coverPath: t.text("cover_path").unique(),
|
||||||
|
dateCreated: t.integer("date_created", { mode: "timestamp" }).notNull(),
|
||||||
|
dateMetadataUpdated: t.integer("date_metadata_updated", { mode: "timestamp" }).notNull(),
|
||||||
|
dateUpdated: t.integer("date_updated", { mode: "timestamp" }).notNull(),
|
||||||
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
||||||
mediumTypeId: t.integer("medium_type_id").references((): AnySQLiteColumn => MediaTypes.id).notNull(),
|
mediumTypeId: t.integer("medium_type_id").references((): AnySQLiteColumn => MediaTypes.id).notNull(),
|
||||||
name: t.text("name").notNull(),
|
name: t.text("name").notNull(),
|
||||||
|
|
@ -45,6 +48,12 @@ export type MediaType = Schema.Schema.Type<typeof MediaTypeSchema>;
|
||||||
export const ArtWorkSchema = Schema.Struct({
|
export const ArtWorkSchema = Schema.Struct({
|
||||||
/** Le chemin de l'image de la pochette de l'oeuvre d'art. */
|
/** Le chemin de l'image de la pochette de l'oeuvre d'art. */
|
||||||
coverPath: Schema.Union(Schema.String, Schema.Null),
|
coverPath: Schema.Union(Schema.String, Schema.Null),
|
||||||
|
/** La date de création de l'entrée. */
|
||||||
|
dateCreated: Schema.Number,
|
||||||
|
/** La date de dernière mise à jour des métadonnées de l'entrée depuis l'API TMDB. */
|
||||||
|
dateMetadataUpdated: Schema.Number,
|
||||||
|
/** La date de dernière mise à jour de l'entrée. */
|
||||||
|
dateUpdated: Schema.Number,
|
||||||
/** L'ID numérique de l'ouvre d'art. */
|
/** L'ID numérique de l'ouvre d'art. */
|
||||||
id: Schema.NonNegativeInt,
|
id: Schema.NonNegativeInt,
|
||||||
/** L'ID numérique du type de l'oeuvre d'art. */
|
/** L'ID numérique du type de l'oeuvre d'art. */
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,63 @@
|
||||||
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
|
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
|
||||||
|
import type { AriaSortValues } from "@/libs/search/types";
|
||||||
|
import type { Values } from "@/libs/utils/types";
|
||||||
|
|
||||||
import { Order } from "effect";
|
import { ARIA_SORT_VALUES } from "@/libs/search/constants";
|
||||||
|
import { Match, Order, pipe } from "effect";
|
||||||
|
|
||||||
import type { TmdbMovieSearchResponseResult } from "./schemas";
|
export const TMDB_SORT_VALUES = {
|
||||||
|
ORIGINAL: "original",
|
||||||
|
POPULARITY: "popularity",
|
||||||
|
RELEASE_DATE: "release_date",
|
||||||
|
TITLE: "title",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const byTitle = Order.mapInput(
|
export type TmdbSortValues = Values<typeof TMDB_SORT_VALUES>;
|
||||||
Order.string,
|
|
||||||
(tmdbEntry: TmdbMovieSearchResponseResult) => tmdbEntry.original_title,
|
export interface TmdbSortData {
|
||||||
|
sortOrder: AriaSortValues;
|
||||||
|
sortValue: TmdbSortValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleSortOrder = (order: AriaSortValues): AriaSortValues =>
|
||||||
|
Match.value(order).pipe(
|
||||||
|
Match.when(ARIA_SORT_VALUES.ASCENDING, () => ARIA_SORT_VALUES.DESCENDING),
|
||||||
|
Match.when(ARIA_SORT_VALUES.DESCENDING, () => ARIA_SORT_VALUES.ASCENDING),
|
||||||
|
Match.when(ARIA_SORT_VALUES.NONE, () => ARIA_SORT_VALUES.ASCENDING),
|
||||||
|
Match.exhaustive,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const byReleaseDate = Order.mapInput(
|
export const getTmdbSortFunction = (sortData: TmdbSortData) =>
|
||||||
Order.string,
|
pipe(
|
||||||
(tmdbEntry: TmdbMovieSearchResponseResult) => tmdbEntry.release_date,
|
// Récupère la fonction de tri correspondant à la propriété demandé.
|
||||||
|
Match.value(sortData.sortValue).pipe(
|
||||||
|
Match.when(TMDB_SORT_VALUES.ORIGINAL, () => byOriginalIndexAscending),
|
||||||
|
Match.when(TMDB_SORT_VALUES.POPULARITY, () => byPopularityAscending),
|
||||||
|
Match.when(TMDB_SORT_VALUES.RELEASE_DATE, () => byReleaseDateAscending),
|
||||||
|
Match.when(TMDB_SORT_VALUES.TITLE, () => byTitleAscending),
|
||||||
|
Match.orElse(() => byTitleAscending),
|
||||||
|
),
|
||||||
|
// Applique le bon sens (ascendant/descendant).
|
||||||
|
(sortFunction: Order.Order<[number, MergedTmdbLocalData]>) =>
|
||||||
|
Match.value(sortData.sortOrder).pipe(
|
||||||
|
Match.when(ARIA_SORT_VALUES.DESCENDING, () => Order.reverse(sortFunction)),
|
||||||
|
Match.orElse(() => sortFunction),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tuples
|
export const byOriginalIndexAscending = Order.mapInput(
|
||||||
|
Order.number,
|
||||||
export const tupleByTitle = Order.mapInput(
|
(data: [number, MergedTmdbLocalData]) => data[1].original_result_index,
|
||||||
|
);
|
||||||
|
export const byPopularityAscending = Order.mapInput(
|
||||||
|
Order.number,
|
||||||
|
(data: [number, MergedTmdbLocalData]) => data[1].popularity,
|
||||||
|
);
|
||||||
|
export const byReleaseDateAscending = Order.mapInput(
|
||||||
|
Order.string,
|
||||||
|
(data: [number, MergedTmdbLocalData]) => data[1].release_date,
|
||||||
|
);
|
||||||
|
export const byTitleAscending = Order.mapInput(
|
||||||
Order.string,
|
Order.string,
|
||||||
(data: [number, MergedTmdbLocalData]) => data[1].title,
|
(data: [number, MergedTmdbLocalData]) => data[1].title,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
5
src/libs/search/constants.ts
Normal file
5
src/libs/search/constants.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const ARIA_SORT_VALUES = {
|
||||||
|
ASCENDING: "ascending",
|
||||||
|
DESCENDING: "descending",
|
||||||
|
NONE: "none",
|
||||||
|
} as const;
|
||||||
|
|
@ -11,12 +11,13 @@ export class MergedTmdbLocalData extends Schema.Class<MergedTmdbLocalData>("Merg
|
||||||
artWorkCoverPath: Schema.Union(Schema.String, Schema.Null),
|
artWorkCoverPath: Schema.Union(Schema.String, Schema.Null),
|
||||||
artWorkId: Schema.NonNegativeInt.pipe(Schema.optional),
|
artWorkId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||||
artWorkMediumTypeId: Schema.NonNegativeInt.pipe(Schema.optional),
|
artWorkMediumTypeId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||||
entryDateCreated: Schema.String.pipe(Schema.optional),
|
entryDateCreated: Schema.Date.pipe(Schema.optional),
|
||||||
entryDateModified: Schema.String.pipe(Schema.optional),
|
entryDateModified: Schema.Date.pipe(Schema.optional),
|
||||||
entryId: Schema.NonNegativeInt.pipe(Schema.optional),
|
entryId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||||
entryStateId: Schema.NonNegativeInt.pipe(Schema.optional),
|
entryStateId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||||
genre_ids: Schema.Array(Schema.NonNegativeInt),
|
genre_ids: Schema.Array(Schema.NonNegativeInt),
|
||||||
original_language: Schema.String,
|
original_language: Schema.String,
|
||||||
|
original_result_index: Schema.Int,
|
||||||
original_title: Schema.String,
|
original_title: Schema.String,
|
||||||
overview: Schema.String,
|
overview: Schema.String,
|
||||||
popularity: Schema.Number,
|
popularity: Schema.Number,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import type { NonEmptyArray } from "effect/Array";
|
import type { NonEmptyArray } from "effect/Array";
|
||||||
|
import type { Router } from "vue-router";
|
||||||
|
|
||||||
|
import { PrettyLogger } from "@/services/logger";
|
||||||
import { UrlParams } from "@effect/platform";
|
import { UrlParams } from "@effect/platform";
|
||||||
import { Effect, pipe } from "effect";
|
import { Effect, Match, pipe } from "effect";
|
||||||
|
|
||||||
|
import type { AriaSortValues } from "./types";
|
||||||
|
|
||||||
|
import { ARIA_SORT_VALUES } from "./constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforme les valeurs d'un `FormData` en `Record` trié.
|
* Transforme les valeurs d'un `FormData` en `Record` trié.
|
||||||
|
|
@ -9,7 +15,7 @@ import { Effect, pipe } from "effect";
|
||||||
* @param formData Les valeurs d'un formulaire.
|
* @param formData Les valeurs d'un formulaire.
|
||||||
* @returns Un `Effect` des valeurs.
|
* @returns Un `Effect` des valeurs.
|
||||||
*/
|
*/
|
||||||
const formDataToRecord = (formData: FormData): Effect.Effect<Record<string, NonEmptyArray<string> | string>> =>
|
export const formDataToRecord = (formData: FormData): Effect.Effect<Record<string, NonEmptyArray<string> | string>> =>
|
||||||
pipe(
|
pipe(
|
||||||
Effect.succeed(Array.from(formData.entries())),
|
Effect.succeed(Array.from(formData.entries())),
|
||||||
// @ts-expect-error -- Impossible de typer les valeurs de FormData comme string.
|
// @ts-expect-error -- Impossible de typer les valeurs de FormData comme string.
|
||||||
|
|
@ -23,4 +29,30 @@ const formDataToRecord = (formData: FormData): Effect.Effect<Record<string, NonE
|
||||||
Effect.andThen((urlParams: UrlParams.UrlParams) => UrlParams.toRecord(urlParams)),
|
Effect.andThen((urlParams: UrlParams.UrlParams) => UrlParams.toRecord(urlParams)),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default { formDataToRecord };
|
export const updateSortOrder = (sortOrder: AriaSortValues) =>
|
||||||
|
Match.value(sortOrder).pipe(
|
||||||
|
Match.when(ARIA_SORT_VALUES.NONE, () => ARIA_SORT_VALUES.ASCENDING),
|
||||||
|
Match.when(ARIA_SORT_VALUES.ASCENDING, () => ARIA_SORT_VALUES.DESCENDING),
|
||||||
|
Match.when(ARIA_SORT_VALUES.DESCENDING, () => ARIA_SORT_VALUES.ASCENDING),
|
||||||
|
Match.exhaustive,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateUrlQueryFromFormData =
|
||||||
|
(router: Router, form: HTMLFormElement | null) => async (event?: Event): Promise<void> => {
|
||||||
|
event?.preventDefault();
|
||||||
|
|
||||||
|
await pipe(
|
||||||
|
// Garantis que l'Élément soit bien présent.
|
||||||
|
Effect.fromNullable(form),
|
||||||
|
Effect.andThen((form: HTMLFormElement) => new FormData(form)),
|
||||||
|
Effect.andThen((searchFormData: FormData) => formDataToRecord(searchFormData)),
|
||||||
|
// Met à jour les paramètres de l'URL.
|
||||||
|
Effect.tap((routeQueryParams: Record<string, NonEmptyArray<string> | string>) =>
|
||||||
|
router.push({ force: true, query: routeQueryParams })
|
||||||
|
),
|
||||||
|
Effect.tapError(Effect.logError),
|
||||||
|
Effect.ignore,
|
||||||
|
Effect.provide(PrettyLogger),
|
||||||
|
Effect.runPromise,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
10
src/libs/search/types.d.ts
vendored
10
src/libs/search/types.d.ts
vendored
|
|
@ -1,10 +0,0 @@
|
||||||
import type { ArtWork, DiaryEntry } from "@/db/schemas";
|
|
||||||
|
|
||||||
import type { TmdbMovieSearchResponseResult } from "../apis/tmdb/schemas";
|
|
||||||
|
|
||||||
/** Page de réponse de l'API TMDB avec les données locales correspondantes. */
|
|
||||||
export interface TmdbDataWithLocalData {
|
|
||||||
artWork?: ArtWork;
|
|
||||||
entry?: DiaryEntry;
|
|
||||||
tmdbData: TmdbMovieSearchResponseResult;
|
|
||||||
}
|
|
||||||
4
src/libs/search/types.ts
Normal file
4
src/libs/search/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import type { Values } from "../utils/types";
|
||||||
|
import type { ARIA_SORT_VALUES } from "./constants";
|
||||||
|
|
||||||
|
export type AriaSortValues = Values<typeof ARIA_SORT_VALUES>;
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
|
||||||
|
import SearchMediaDialog from "@/components/dialogs/SearchMediaDialog.vue";
|
||||||
import LastAddedEntry from "@/components/LastAddedEntry.vue";
|
import LastAddedEntry from "@/components/LastAddedEntry.vue";
|
||||||
import SearchMediaDialog from "@/components/SearchMediaDialog.vue";
|
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
const toggleDialogStateRef = (stateRef: Ref<boolean, boolean>) => () => {
|
const toggleDialogStateRef = (stateRef: Ref<boolean, boolean>) => () => {
|
||||||
stateRef.value = !stateRef.value;
|
stateRef.value = !stateRef.value;
|
||||||
};
|
};
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
|
|
||||||
<section id="last-watched-media" class="stack">
|
<section id="last-watched-media" class="stack">
|
||||||
<h2>Derniers médias regardés</h2>
|
<h2>Derniers médias regardés</h2>
|
||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LastAddedEntry> </LastAddedEntry>
|
<LastAddedEntry> </LastAddedEntry>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NonEmptyArray } from "effect/Array";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
|
||||||
|
import EditEntryDialog from "@/components/dialogs/EditEntryDialog.vue";
|
||||||
import ErrorMessage from "@/components/ErrorMessage.vue";
|
import ErrorMessage from "@/components/ErrorMessage.vue";
|
||||||
import LoadingMessage from "@/components/LoadingMessage.vue";
|
import LoadingMessage from "@/components/LoadingMessage.vue";
|
||||||
import TmdbSearchResults from "@/components/TmdbSearchResults.vue";
|
import TmdbSearchResults from "@/components/TmdbSearchResults.vue";
|
||||||
|
|
@ -13,35 +13,36 @@
|
||||||
TmdbMovieSearchResponseResult,
|
TmdbMovieSearchResponseResult,
|
||||||
} from "@/libs/apis/tmdb/schemas.ts";
|
} from "@/libs/apis/tmdb/schemas.ts";
|
||||||
import { MergedTmdbLocalData, SearchPageQueryParams } from "@/libs/search/schemas.ts";
|
import { MergedTmdbLocalData, SearchPageQueryParams } from "@/libs/search/schemas.ts";
|
||||||
import Search from "@/libs/search/search.ts";
|
import { updateUrlQueryFromFormData } from "@/libs/search/search.ts";
|
||||||
import { getCurrentYear } from "@/libs/utils/dates.ts";
|
import { getCurrentYear } from "@/libs/utils/dates.ts";
|
||||||
import { getOrUndefined } from "@/libs/utils/effects.ts";
|
import { getOrUndefined } from "@/libs/utils/effects.ts";
|
||||||
import { PrettyLogger } from "@/services/logger.ts";
|
import { PrettyLogger } from "@/services/logger.ts";
|
||||||
import { ReadApi } from "@/services/read-api.ts";
|
import { ReadApi } from "@/services/read-api.ts";
|
||||||
import { RuntimeClient } from "@/services/runtime-client.ts";
|
import { RuntimeClient } from "@/services/runtime-client.ts";
|
||||||
import { TmdbApi } from "@/services/tmdb-api.ts";
|
import { TmdbApi } from "@/services/tmdb-api.ts";
|
||||||
import { Array as Arr, Effect, pipe, Schema } from "effect";
|
import { Effect, pipe, Schema } from "effect";
|
||||||
import { computed, onMounted, ref, useTemplateRef, watch } from "vue";
|
import { computed, onMounted, ref, useTemplateRef, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
// États
|
// États
|
||||||
|
|
||||||
/** L'année courante pour la limite supérieure du champs Année de la recherché. */
|
/** Année courante pour la limite supérieure du champs « Année » de la recherche. */
|
||||||
const currentYear: number = getCurrentYear();
|
const currentYear: number = getCurrentYear();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
/** Effet des paramètres validés de la route. */
|
|
||||||
|
/** Effet dérivé des paramètres validés de la route. */
|
||||||
const routeQueryParams = computed(() => Schema.decodeUnknown(SearchPageQueryParams)(route.query));
|
const routeQueryParams = computed(() => Schema.decodeUnknown(SearchPageQueryParams)(route.query));
|
||||||
|
|
||||||
/** Le formulaire de recherche. */
|
/** L'Élément DOM du formulaire de recherche. */
|
||||||
const form = useTemplateRef("form");
|
const form = useTemplateRef("form");
|
||||||
/** Les valeurs du formulaire de recherche. */
|
/** Valeurs du formulaire de recherche. */
|
||||||
const searchFormData: Ref<SearchPageQueryParams | undefined> = ref<SearchPageQueryParams>();
|
const searchFormData: Ref<SearchPageQueryParams | undefined> = ref<SearchPageQueryParams>();
|
||||||
|
|
||||||
/** Le retour de la requête de recherche de films auprès de l'API TMDB. */
|
/** Retour de la requête de recherche de films auprès de l'API TMDB. */
|
||||||
const search: Ref<TmdbMovieSearchResponse | undefined> = ref<TmdbMovieSearchResponse>();
|
const search: Ref<TmdbMovieSearchResponse | undefined> = ref<TmdbMovieSearchResponse>();
|
||||||
|
/** Données complètes de la recherche avec les données TMDB et locales. */
|
||||||
const searchData: Ref<Map<number, MergedTmdbLocalData>> = ref(new Map<number, MergedTmdbLocalData>());
|
const searchData: Ref<Map<number, MergedTmdbLocalData>> = ref(new Map<number, MergedTmdbLocalData>());
|
||||||
|
|
||||||
/** État du chargement de la requête auprès de l'API TMDB. */
|
/** État du chargement de la requête auprès de l'API TMDB. */
|
||||||
|
|
@ -51,25 +52,11 @@
|
||||||
/** Message affiché à l'Utilisateur. */
|
/** Message affiché à l'Utilisateur. */
|
||||||
const message: Ref<string> = ref("");
|
const message: Ref<string> = ref("");
|
||||||
|
|
||||||
|
const editedEntry: Ref<MergedTmdbLocalData | undefined> = ref();
|
||||||
|
|
||||||
// Fonctions
|
// Fonctions
|
||||||
|
|
||||||
const updateUrlQuery = async (event?: Event): Promise<void> => {
|
let updateUrlQuery = updateUrlQueryFromFormData(router, form.value);
|
||||||
event?.preventDefault();
|
|
||||||
|
|
||||||
await pipe(
|
|
||||||
Effect.fromNullable(form.value),
|
|
||||||
Effect.andThen((form: HTMLFormElement) => new FormData(form)),
|
|
||||||
Effect.andThen((searchFormData: FormData) => Search.formDataToRecord(searchFormData)),
|
|
||||||
// Met à jour les paramètres de l'URL.
|
|
||||||
Effect.tap((routeQueryParams: Record<string, NonEmptyArray<string> | string>) =>
|
|
||||||
router.push({ force: true, query: routeQueryParams })
|
|
||||||
),
|
|
||||||
Effect.tapError(Effect.logError),
|
|
||||||
Effect.ignore,
|
|
||||||
Effect.provide(PrettyLogger),
|
|
||||||
Effect.runPromise,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetInitialState = async (event: Event): Promise<void> => {
|
const resetInitialState = async (event: Event): Promise<void> => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -132,6 +119,15 @@
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleEntryDialog = (tmdbId?: number) => {
|
||||||
|
if (!tmdbId) {
|
||||||
|
editedEntry.value = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editedEntry.value = searchData.value.get(tmdbId);
|
||||||
|
};
|
||||||
|
|
||||||
// Cycles
|
// Cycles
|
||||||
|
|
||||||
watch(search, async (): Promise<void> => {
|
watch(search, async (): Promise<void> => {
|
||||||
|
|
@ -140,7 +136,7 @@
|
||||||
const results = search.value?.results ?? [];
|
const results = search.value?.results ?? [];
|
||||||
const readApi = yield* ReadApi;
|
const readApi = yield* ReadApi;
|
||||||
|
|
||||||
void results.map((result: TmdbMovieSearchResponseResult) =>
|
void results.map((result: TmdbMovieSearchResponseResult, index: number) =>
|
||||||
Effect.gen(function*() {
|
Effect.gen(function*() {
|
||||||
const entry = yield* pipe(
|
const entry = yield* pipe(
|
||||||
readApi.getEntryByTmdbId(result.id),
|
readApi.getEntryByTmdbId(result.id),
|
||||||
|
|
@ -152,6 +148,7 @@
|
||||||
effect => getOrUndefined(effect),
|
effect => getOrUndefined(effect),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Uniformiser la casse des propriétés.
|
||||||
searchData.value.set(
|
searchData.value.set(
|
||||||
result.id,
|
result.id,
|
||||||
yield* Schema.decodeUnknown(MergedTmdbLocalData)(
|
yield* Schema.decodeUnknown(MergedTmdbLocalData)(
|
||||||
|
|
@ -165,6 +162,7 @@
|
||||||
entryStateId: entry?.stateId,
|
entryStateId: entry?.stateId,
|
||||||
genre_ids: result.genre_ids,
|
genre_ids: result.genre_ids,
|
||||||
original_language: result.original_language,
|
original_language: result.original_language,
|
||||||
|
original_result_index: index,
|
||||||
original_title: result.original_title,
|
original_title: result.original_title,
|
||||||
overview: result.overview,
|
overview: result.overview,
|
||||||
popularity: result.popularity,
|
popularity: result.popularity,
|
||||||
|
|
@ -185,6 +183,7 @@
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.debug("SearchPage.vue -- Mounted");
|
console.debug("SearchPage.vue -- Mounted");
|
||||||
|
updateUrlQuery = updateUrlQueryFromFormData(router, form.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -251,9 +250,11 @@
|
||||||
|
|
||||||
<LoadingMessage v-if="isLoading">Récupération des résultats</LoadingMessage>
|
<LoadingMessage v-if="isLoading">Récupération des résultats</LoadingMessage>
|
||||||
<ErrorMessage v-if="isErrored">{{ message }}</ErrorMessage>
|
<ErrorMessage v-if="isErrored">{{ message }}</ErrorMessage>
|
||||||
<TmdbSearchResults v-else :search-data="searchData"></TmdbSearchResults>
|
<TmdbSearchResults v-else :search-data="searchData" @entry-dialog-wanted="toggleEntryDialog"></TmdbSearchResults>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EditEntryDialog v-if="editedEntry" :entry-data="editedEntry" @dialog-hidden="toggleEntryDialog()"></EditEntryDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
|
|
|
||||||
27
src/services/images.ts
Normal file
27
src/services/images.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { asInt } from "@thi.ng/color-palettes";
|
||||||
|
import { ARGB8888, canvasFromPixelBuffer, defIndexed, imageFromURL, intBufferFromImage } from "@thi.ng/pixel";
|
||||||
|
import { ATKINSON, ditherWith } from "@thi.ng/pixel-dither";
|
||||||
|
import { Data, Effect, pipe } from "effect";
|
||||||
|
|
||||||
|
class ImagesError extends Data.TaggedError("ImagesError")<{ cause: unknown }> {}
|
||||||
|
|
||||||
|
export class Images extends Effect.Service<Images>()("Images", {
|
||||||
|
effect: Effect.gen(function*() {
|
||||||
|
return {
|
||||||
|
ditherImage: (image: HTMLImageElement, parent?: HTMLElement) =>
|
||||||
|
Effect.gen(function*() {
|
||||||
|
const buf = intBufferFromImage(image, ARGB8888).scale(0.8, "cubic");
|
||||||
|
const theme = defIndexed(asInt(["salmon", "black"]));
|
||||||
|
const ditheredBuf = ditherWith(ATKINSON, buf.copy(), {}).as(theme);
|
||||||
|
|
||||||
|
const canvas = canvasFromPixelBuffer(ditheredBuf, parent, { pixelated: true });
|
||||||
|
return canvas;
|
||||||
|
}),
|
||||||
|
imageFromUrl: (url: URL) =>
|
||||||
|
pipe(
|
||||||
|
Effect.tryPromise(() => imageFromURL(url.toString())),
|
||||||
|
Effect.mapError(e => new ImagesError({ cause: e.message })),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { SQLocalDrizzle } from "sqlocal/drizzle";
|
import type { SQLocalDrizzle } from "sqlocal/drizzle";
|
||||||
|
|
||||||
import v0000 from "@/db/drizzle/0000_open_the_twelve.sql?raw";
|
import v0000 from "@/db/drizzle/0000_unusual_karen_page.sql?raw";
|
||||||
import { Data, Effect } from "effect";
|
import { Data, Effect } from "effect";
|
||||||
|
|
||||||
import { LocalSqlite } from "./db";
|
import { LocalSqlite } from "./db";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Layer, ManagedRuntime } from "effect";
|
import { Layer, ManagedRuntime } from "effect";
|
||||||
|
|
||||||
import { LocalSqlite } from "./db";
|
import { LocalSqlite } from "./db";
|
||||||
|
import { Images } from "./images";
|
||||||
import { PrettyLogger } from "./logger";
|
import { PrettyLogger } from "./logger";
|
||||||
import { Migrations } from "./migrations";
|
import { Migrations } from "./migrations";
|
||||||
import { ReadApi } from "./read-api";
|
import { ReadApi } from "./read-api";
|
||||||
|
|
@ -12,6 +13,7 @@ const MainLayer = Layer.mergeAll(
|
||||||
Migrations.Default,
|
Migrations.Default,
|
||||||
ReadApi.Default,
|
ReadApi.Default,
|
||||||
TmdbApi.Default,
|
TmdbApi.Default,
|
||||||
|
Images.Default,
|
||||||
).pipe(Layer.provide(PrettyLogger));
|
).pipe(Layer.provide(PrettyLogger));
|
||||||
|
|
||||||
export const RuntimeClient = ManagedRuntime.make(MainLayer);
|
export const RuntimeClient = ManagedRuntime.make(MainLayer);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
text-size-adjust: none;
|
|
||||||
text-size-adjust: none;
|
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
color-scheme: dark light;
|
color-scheme: dark light;
|
||||||
interpolate-size: allow-keywords;
|
interpolate-size: allow-keywords;
|
||||||
|
|
@ -48,7 +46,7 @@ body {
|
||||||
clip-path: inset(50%);
|
clip-path: inset(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where([hidden]), :where([aria-hidden="true"]) {
|
:where([hidden]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
inline-size: fit-content;
|
inline-size: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(a) {
|
:where(a:not([class])) {
|
||||||
text-decoration: underline dashed;
|
text-decoration: underline dashed;
|
||||||
text-decoration-skip-ink: all;
|
text-decoration-skip-ink: all;
|
||||||
|
|
||||||
.external {
|
&.external {
|
||||||
text-decoration: underline solid;
|
text-decoration: underline solid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
button {
|
button:not(.button-invisible) {
|
||||||
--button-background-color: var(--root-background-color);
|
--button-background-color: var(--root-background-color);
|
||||||
--button-border-color: var(--root-text-color);
|
--button-border-color: var(--root-text-color);
|
||||||
--button-font-weight: 100;
|
--button-font-weight: 100;
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"root":["./src/router/typed-routes.d.ts","./eslint.config.mts","./vite.config.mts","./cfg/drizzle.config.ts","./cfg/eslint-css.config.mts","./cfg/knip.config.ts","./cfg/prettier.config.mjs","./cfg/stylelint.config.mjs","./src/main.ts","./src/vite-env.d.ts","./src/db/schemas.ts","./src/db/schemas/constants.ts","./src/db/schemas/entries.ts","./src/db/schemas/works.ts","./src/libs/apis/clients.ts","./src/libs/apis/requests.ts","./src/libs/apis/routes.ts","./src/libs/apis/tmdb/constants.ts","./src/libs/apis/tmdb/schemas.ts","./src/libs/search/schemas.ts","./src/libs/search/search.ts","./src/libs/types/events.ts","./src/libs/utils/dates.ts","./src/libs/utils/effects.ts","./src/libs/utils/types.d.ts","./src/router/index.ts","./src/services/db.ts","./src/services/logger.ts","./src/services/migrations.ts","./src/services/read-api.ts","./src/services/runtime-client.ts","./src/services/tmdb-api.ts"],"errors":true,"version":"5.7.3"}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue