diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index a949825..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -custom: - - "https://buymeacoffee.com/anurag629" - - "https://www.paypal.com/paypalme/codercops" diff --git a/.github/workflows/accepted-label.yml b/.github/workflows/accepted-label.yml index 2f88c9d..6f6b8f1 100644 --- a/.github/workflows/accepted-label.yml +++ b/.github/workflows/accepted-label.yml @@ -1,7 +1,8 @@ name: Accepted label -# A merged PR counts on the leaderboard only with the hacktoberfest-accepted -# label, and only for an issue that was assigned to its author (CONTRIBUTING.md). +# A merged PR counts on the leaderboard (oss.codercops.com) only with the +# codercops-accepted label, and only for an issue that was assigned to its +# author (CONTRIBUTING.md). PRs merged in October also get hacktoberfest-accepted. # Maintainers add the label when they merge. This adds it when that was missed, # by the same rule as the claim check: every issue the PR closes is assigned to # the author. It runs once, at the merge, so removing the label later sticks. @@ -20,12 +21,13 @@ permissions: jobs: label: name: Label a merged contributor PR - # Maintainers and bots don't claim issues, and spam never counts. + # Maintainers and bots don't claim issues, and spam never counts. Outside + # collaborators (contributors with Triage) are labeled like everyone else. if: >- github.event.pull_request.merged == true && github.event.pull_request.user.type != 'Bot' && - !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) && - !contains(github.event.pull_request.labels.*.name, 'hacktoberfest-accepted') && + !contains(fromJSON('["OWNER", "MEMBER"]'), github.event.pull_request.author_association) && + !contains(github.event.pull_request.labels.*.name, 'codercops-accepted') && !contains(github.event.pull_request.labels.*.name, 'spam') runs-on: ubuntu-latest steps: @@ -34,6 +36,7 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} PR: ${{ github.event.pull_request.number }} + MERGED_AT: ${{ github.event.pull_request.merged_at }} run: | set -euo pipefail @@ -70,7 +73,7 @@ jobs: fi if [ "$(jq length <<<"$linked")" -eq 0 ]; then - echo "::notice title=Not labeled::This PR isn't linked to an issue, so it doesn't get hacktoberfest-accepted." + echo "::notice title=Not labeled::This PR isn't linked to an issue, so it doesn't get codercops-accepted." exit 0 fi @@ -81,9 +84,12 @@ jobs: | join(", ")' <<<"$linked") if [ -n "$missing" ]; then - echo "::notice title=Not labeled::$missing isn't assigned to @$author, so this PR doesn't get hacktoberfest-accepted." + echo "::notice title=Not labeled::$missing isn't assigned to @$author, so this PR doesn't get codercops-accepted." exit 0 fi - gh api -X POST "repos/$REPO/issues/$PR/labels" -f "labels[]=hacktoberfest-accepted" --silent - echo "Added hacktoberfest-accepted: $(jq -r '[.[].ref] | join(", ")' <<<"$linked") assigned to @$author" + labels=(-f "labels[]=codercops-accepted") + # Hacktoberfest only counts PRs merged in October (UTC). + if [ "${MERGED_AT:5:2}" = "10" ]; then labels+=(-f "labels[]=hacktoberfest-accepted"); fi + gh api -X POST "repos/$REPO/issues/$PR/labels" "${labels[@]}" --silent + echo "Labeled: $(jq -r '[.[].ref] | join(", ")' <<<"$linked") assigned to @$author" diff --git a/.github/workflows/claim-check.yml b/.github/workflows/claim-check.yml index ce89d32..b704041 100644 --- a/.github/workflows/claim-check.yml +++ b/.github/workflows/claim-check.yml @@ -26,7 +26,7 @@ jobs: # Maintainers and bots don't claim issues. if: >- github.event.pull_request.user.type != 'Bot' && - !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) + !contains(fromJSON('["OWNER", "MEMBER"]'), github.event.pull_request.author_association) runs-on: ubuntu-latest steps: - name: Check the linked issue diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 0000000..42647b8 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://oss.codercops.com/funding.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83ed03c..76e26de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,17 +2,17 @@ Thank you for your interest in contributing! OGCOPS is open source and welcomes contributions of all kinds. -## Hacktoberfest 2026 +## How to contribute -We're taking part in Hacktoberfest this year, and first-time contributors are very welcome. +First-time contributors are very welcome. -1. Pick an open issue labeled [`hacktoberfest`](https://github.com/codercops/ogcops/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest+no%3Aassignee) or [`good first issue`](https://github.com/codercops/ogcops/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+no%3Aassignee) that nobody is assigned to. New template ideas are listed in [#5](https://github.com/codercops/ogcops/issues/5). +1. Pick an open issue labeled [`good first issue`](https://github.com/codercops/ogcops/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+no%3Aassignee) that nobody is assigned to. New template ideas are listed in [#5](https://github.com/codercops/ogcops/issues/5). 2. Comment on it to ask for it. I'll assign it to you, usually within a day. Please don't start on an issue that's assigned to someone else. 3. Take one issue at a time. Once your PR is merged, grab the next one. After 3 merged PRs with us you can hold two issues at once. 4. If you're assigned and there's no PR or update for 5 days, I'll free the issue up for someone else. Just comment if you need more time. 5. Open your PR against `dev` here in codercops/ogcops and put `Closes #` in the description. A PR merged inside your own fork never reaches us, so it doesn't count. A check on the PR ("Claim check") fails if that issue isn't assigned to you. Another one ("Description check") fails while the "How I tested this" or "AI assistance" section of the description is missing or empty. -PRs count for Hacktoberfest once they're merged or labeled `hacktoberfest-accepted`. PRs that aren't linked to an issue, only reformat code, or change things nobody asked for will be closed, and spammy ones get the `spam` label. If you have an idea that isn't an issue yet, open an issue first so we can agree on it before you write code. The full rules are under [Ground Rules](#ground-rules) below. +A merged PR counts on the monthly leaderboard once it's labeled `codercops-accepted`. A workflow adds that label when the PR is merged, as long as every issue it closes is assigned to you. PRs that aren't linked to an issue, only reformat code, or change things nobody asked for will be closed, and spammy ones get the `spam` label. If you have an idea that isn't an issue yet, open an issue first so we can agree on it before you write code. The full rules are under [Ground Rules](#ground-rules) below. Stuck? Ask on the issue, or in `#oss-help` on our [Discord](https://discord.gg/mpGdJyjaRQ). Questions are welcome. Claims still happen on the issue, so the assignment stays visible to everyone. @@ -38,7 +38,7 @@ These get closed and labeled `spam`: - claiming a pile of issues at once, or bulk and automated submissions - untested generated code -Two spam PRs and you won't be assigned more issues this month, and you're out of the CODERCOPS goodies. +Two spam PRs and you won't be assigned more issues this month, and your PRs stop counting on the leaderboard. ### Reviews @@ -48,15 +48,15 @@ For a bug fix, check that your new test fails without the fix: undo the change i I review most PRs within a few days. Please don't ping repeatedly or DM maintainers about a review. If three days pass with no reply, one follow-up comment on the PR is fine. -### Goodies +### Monthly leaderboard -The top 5 contributors this October get a CODERCOPS T-shirt and stickers. You need at least 5 merged PRs labeled `hacktoberfest-accepted` across chatcops, ogcops, toolbelt and discord-bot, merged between Oct 1 and Oct 31 (UTC), and ties go to whoever got there first. To keep it fair: +Every month has its own leaderboard at [oss.codercops.com/leaderboard](https://oss.codercops.com/leaderboard). It counts PRs labeled `codercops-accepted` across chatcops, ogcops, toolbelt and discord-bot that were merged in that month (UTC). You're ranked from 3 merged PRs, and the top 5 get a shout-out on our X and LinkedIn pages. To keep it fair: - only PRs for issues that were assigned to you count - a fix split into several PRs counts once - what counts is the maintainers' call, and that call is final -These goodies come from CODERCOPS through our own open source program. They're separate from Hacktoberfest, so anything Hacktoberfest itself gives out is a different thing with its own rules. +People who keep contributing can be invited as maintainers with Triage access. [GOVERNANCE.md](https://github.com/codercops/.github/blob/main/GOVERNANCE.md) explains how that works and who maintains what. ## Ways to Contribute diff --git a/README.md b/README.md index 48d5f54..80421f2 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,10 @@ git checkout -b your-feature-branch - [Join the Discord](https://discord.gg/mpGdJyjaRQ) to chat with other contributors - [Start a discussion](https://github.com/codercops/ogcops/discussions) — questions, ideas, show & tell +## Maintainers and funding + +OGCOPS is maintained by CODERCOPS together with contributors who have Triage access. [GOVERNANCE.md](https://github.com/codercops/.github/blob/main/GOVERNANCE.md) lists who they are and how to become one. The project is free and MIT licensed, and CODERCOPS doesn't sell it or a hosted version of it. If it's useful to you, you can [sponsor the maintainers](https://github.com/sponsors/anurag629). The money pays for review time and hosting. + ## License [MIT](LICENSE) diff --git a/package-lock.json b/package-lock.json index cd9260a..dbd4d5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "typescript": "^5.9.3", - "vitest": "^4.1.11", + "vitest": "^4.1.12", "wrangler": "^4.141.0" } }, @@ -437,9 +437,9 @@ } }, "node_modules/@astrojs/sitemap/node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -2774,16 +2774,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", + "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2792,13 +2792,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.11", + "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2818,20 +2818,10 @@ } } }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", + "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", "dev": true, "license": "MIT", "dependencies": { @@ -2842,13 +2832,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "pathe": "^2.0.3" }, "funding": { @@ -2856,14 +2846,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", + "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2872,9 +2862,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", "dev": true, "license": "MIT", "funding": { @@ -2882,13 +2872,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", + "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", + "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3256,9 +3246,9 @@ } }, "node_modules/astro/node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -3844,6 +3834,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -3889,9 +3889,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -4616,9 +4616,9 @@ } }, "node_modules/miniflare/node_modules/undici": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", - "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -5541,9 +5541,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", - "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -5905,19 +5905,19 @@ } }, "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/expect": "4.1.12", + "@vitest/mocker": "4.1.12", + "@vitest/pretty-format": "4.1.12", + "@vitest/runner": "4.1.12", + "@vitest/snapshot": "4.1.12", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -5945,12 +5945,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@vitest/browser-playwright": "4.1.12", + "@vitest/browser-preview": "4.1.12", + "@vitest/browser-webdriverio": "^4.1.11", + "@vitest/coverage-istanbul": "4.1.12", + "@vitest/coverage-v8": "4.1.12", + "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 04f92c7..510c44e 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,11 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "typescript": "^5.9.3", - "vitest": "^4.1.11", + "vitest": "^4.1.12", "wrangler": "^4.141.0" }, "overrides": { - "sharp": "^0.35.5" + "sharp": "^0.35.5", + "undici@7": "^7.30.1" } } diff --git a/public/.well-known/funding-manifest-urls b/public/.well-known/funding-manifest-urls new file mode 100644 index 0000000..42647b8 --- /dev/null +++ b/public/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://oss.codercops.com/funding.json diff --git a/src/components/editor/ColorPicker.tsx b/src/components/editor/ColorPicker.tsx index 413e82a..984618b 100644 --- a/src/components/editor/ColorPicker.tsx +++ b/src/components/editor/ColorPicker.tsx @@ -1,4 +1,5 @@ -import { useId } from 'react'; +import { useId, useRef } from 'react'; +import { normalizeHexColor } from '@/templates/utils'; interface ColorPickerProps { label: string; @@ -10,7 +11,9 @@ export function ColorPicker({ label, value, onChange }: ColorPickerProps) { const id = useId(); const colorId = `${id}-color`; const hexId = `${id}-hex`; - const displayValue = value.startsWith('#') ? value : `#${value}`; + const displayValue = value && !value.startsWith('#') ? `#${value}` : value; + const lastValid = useRef('#000000'); + lastValid.current = normalizeHexColor(value, lastValid.current); return (
@@ -23,7 +26,7 @@ export function ColorPicker({ label, value, onChange }: ColorPickerProps) { id={colorId} type="color" className="editor-color-swatch" - value={displayValue} + value={lastValid.current} onChange={(e) => onChange(e.target.value)} /> diff --git a/src/components/editor/CustomizePanel.tsx b/src/components/editor/CustomizePanel.tsx index 8e1e1eb..a781bb5 100644 --- a/src/components/editor/CustomizePanel.tsx +++ b/src/components/editor/CustomizePanel.tsx @@ -113,7 +113,7 @@ function renderField( onChange(field.key, v)} /> ); diff --git a/src/components/editor/EditorApp.tsx b/src/components/editor/EditorApp.tsx index 34df60a..4f5e61f 100644 --- a/src/components/editor/EditorApp.tsx +++ b/src/components/editor/EditorApp.tsx @@ -236,7 +236,7 @@ function EditorAppInner({ initialCategory }: EditorAppProps) { {currentTemplate.name}
- + {/* Mobile Tabs */} @@ -278,7 +278,7 @@ function EditorAppInner({ initialCategory }: EditorAppProps) { {/* The topbar ExportBar always mounts and owns the shortcuts, so this second one opts out. */} - + ; templateId: string; + svg: string | null; /** * Whether this bar owns the keyboard shortcuts. Pass false on a second * ExportBar so Ctrl/Cmd+S and Ctrl/Cmd+Shift+C fire once, not once per bar. @@ -24,22 +26,32 @@ export function ExportBar({ downloadUrl, params, templateId, + svg, shortcuts = true, }: ExportBarProps) { const [downloading, setDownloading] = useState(false); const [copied, setCopied] = useState(null); const downloadRef = useRef(downloadUrl); const apiUrlRef = useRef(apiUrl); + const svgRef = useRef(svg); + const hasLocalImage = containsLocalImage(params); downloadRef.current = downloadUrl; apiUrlRef.current = apiUrl; + svgRef.current = svg; const handleDownload = useCallback(async () => { setDownloading(true); try { - const res = await fetch(downloadRef.current); - if (!res.ok) throw new Error('Download failed'); - const blob = await res.blob(); + let blob: Blob; + if (hasLocalImage) { + if (!svgRef.current) throw new Error('Preview is not ready'); + blob = await svgToPngBlob(svgRef.current); + } else { + const res = await fetch(downloadRef.current); + if (!res.ok) throw new Error('Download failed'); + blob = await res.blob(); + } const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -55,7 +67,11 @@ export function ExportBar({ } finally { setDownloading(false); } - }, [templateId]); + }, [hasLocalImage, templateId]); + + const showLocalImageWarning = useCallback(() => { + window.showToast?.('Local images cannot be included in a shareable URL.', 'error'); + }, []); const copyToClipboard = useCallback(async (text: string, label: string) => { try { @@ -96,17 +112,19 @@ export function ExportBar({ // Ctrl/Cmd+Shift+C → Copy API URL if (e.key === 'C' && e.shiftKey) { e.preventDefault(); - copyToClipboard(apiUrlRef.current, 'url'); + if (hasLocalImage) showLocalImageWarning(); + else copyToClipboard(apiUrlRef.current, 'url'); } } document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [handleDownload, copyToClipboard, shortcuts]); + }, [handleDownload, copyToClipboard, hasLocalImage, showLocalImageWarning, shortcuts]); const metaTags = generateMetaTags({ title: params.title ? String(params.title) : undefined, description: params.description ? String(params.description) : undefined, + siteName: params.siteName ? String(params.siteName) : undefined, imageUrl: apiUrl, }); @@ -125,7 +143,7 @@ export function ExportBar({