diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6af245..0bc9382 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,4 @@ jobs: node-version: 22 cache: npm - run: npm ci - - run: npm run typecheck - - run: npm run test - - run: npm run build + - run: npm run verify diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7ed026b..eb69b29 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -37,9 +37,7 @@ jobs: registry-url: https://registry.npmjs.org - run: npm ci - - run: npm run typecheck - - run: npm run test - - run: npm run build + - run: npm run verify - name: Check release version matches package.json run: | diff --git a/README.ko.md b/README.ko.md index 4c1e24f..a6c3eb1 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,5 +1,11 @@ # numkey +
+ +[DevsLab 오픈소스](https://devslab.kr/brand/open-source/) + [](https://www.npmjs.com/package/@devslab/numkey) [](https://github.com/devslab-kr/numkey/actions/workflows/ci.yml)  diff --git a/README.md b/README.md index 4b3a7b5..bc0ef37 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # numkey + + +Open source by [DevsLab](https://devslab.kr/brand/open-source/) + [](https://www.npmjs.com/package/@devslab/numkey) [](https://github.com/devslab-kr/numkey/actions/workflows/ci.yml)  diff --git a/docs/assets/brand/checksums.txt b/docs/assets/brand/checksums.txt new file mode 100644 index 0000000..80fb0a7 --- /dev/null +++ b/docs/assets/brand/checksums.txt @@ -0,0 +1,21 @@ +298658c5507dd8acacfa0b2c26b0cd00b612f0932698e4223f0a8a97957fafed apple-touch-icon.png +4205e96a25955ce59c730fd380e45db18b153e152424661df1822ed296ccaf1c favicon.ico +4ec733f9542323f6d2840ba743c4a11df4d416a7efa1d214567d62b58653619b favicon.svg +4ec733f9542323f6d2840ba743c4a11df4d416a7efa1d214567d62b58653619b glyph-color.svg +03f6342931d1660554b44bb5704aeb4639fa5fb6d12dbf6286418ecfaeddc654 glyph-monochrome.svg +dc08a6ba51a2a8e9cb82d56f900ef2de9330ac72fbb3c727e0436a4ab2f3944f glyph-reversed.svg +f5cba92cab71ae542c297205daca2f50bf7a64279da964a0239f85b062b7e36d icons/icon-16.png +298658c5507dd8acacfa0b2c26b0cd00b612f0932698e4223f0a8a97957fafed icons/icon-180.png +8e9148efe85095d304c182fe0fe3fa2a2b44bcfe2a4e26077c21947baf92be96 icons/icon-192.png +3c696ba85751c9a9e805141adf07242f495d8ffc115b2477074862a3843a19fa icons/icon-32.png +bfa28751bd6e26e47dfb8458a23d309082bb7db6e293957438ac02fdd4af0fa8 icons/icon-48.png +3004387fcbc066076f70f383e74e11e9a94f6cd771e032ca8ff993daede614dc icons/icon-512.png +905cf0a05ab5d3787bc02ff7d4591bfa020d72aefdd0e0d0c53271da6d3f383c lockup-endorsed.svg +ee4822f0054f88b5a386dc53d5034af1577309b0425e78ed2af85167e8467aa2 lockup.svg +90062f969fca1da22b05ec5d3f92bbc0f46e736b473351078e54317df19c41be maskable-192.png +18b8c7957b532b78ef631fca894a245bc4bd1eb7a36255e6ec1a7da857b698c0 maskable-512.png +affd93edb1d511780bad26de47cc673a5e93014967916029dd8db68ef9ec394e og.png +8e9148efe85095d304c182fe0fe3fa2a2b44bcfe2a4e26077c21947baf92be96 pwa-192.png +3004387fcbc066076f70f383e74e11e9a94f6cd771e032ca8ff993daede614dc pwa-512.png +481391d02b1f50ed6dba1790c0899bcc7d16948421bba66778756a89921b5dbb readme-header.png +27a520d8daf6752076ac90d52b611034af0d2e29a8167e3e1ef7d4f0246b52e9 wordmark.svg diff --git a/docs/assets/brand/project-lockup.svg b/docs/assets/brand/project-lockup.svg new file mode 100644 index 0000000..906db47 --- /dev/null +++ b/docs/assets/brand/project-lockup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/brand/project-mark.svg b/docs/assets/brand/project-mark.svg new file mode 100644 index 0000000..e7b2f73 --- /dev/null +++ b/docs/assets/brand/project-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/brand/readme-header.png b/docs/assets/brand/readme-header.png new file mode 100644 index 0000000..bf795f9 Binary files /dev/null and b/docs/assets/brand/readme-header.png differ diff --git a/package.json b/package.json index 4d21668..3a2774c 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,12 @@ "sideEffects": false, "scripts": { "build": "tsup", + "check:brand": "node scripts/check-brand-assets.mjs", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", - "prepublishOnly": "npm run typecheck && npm run test && npm run build" + "verify": "npm run check:brand && npm run typecheck && npm run test && npm run build", + "prepublishOnly": "npm run verify" }, "peerDependencies": { "react": ">=18", diff --git a/scripts/check-brand-assets.mjs b/scripts/check-brand-assets.mjs new file mode 100644 index 0000000..07712ab --- /dev/null +++ b/scripts/check-brand-assets.mjs @@ -0,0 +1,70 @@ +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import assert from 'node:assert/strict' + +const root = resolve(import.meta.dirname, '..') +const read = (file) => readFile(resolve(root, file), 'utf8') +const hash = async (file) => createHash('sha256').update(await readFile(resolve(root, file))).digest('hex') + +const assets = { + 'docs/assets/brand/project-mark.svg': '4ec733f9542323f6d2840ba743c4a11df4d416a7efa1d214567d62b58653619b', + 'docs/assets/brand/project-lockup.svg': '905cf0a05ab5d3787bc02ff7d4591bfa020d72aefdd0e0d0c53271da6d3f383c', + 'docs/assets/brand/readme-header.png': '481391d02b1f50ed6dba1790c0899bcc7d16948421bba66778756a89921b5dbb', + 'site/favicon.svg': '4ec733f9542323f6d2840ba743c4a11df4d416a7efa1d214567d62b58653619b', + 'site/favicon.ico': '4205e96a25955ce59c730fd380e45db18b153e152424661df1822ed296ccaf1c', + 'site/apple-touch-icon.png': '298658c5507dd8acacfa0b2c26b0cd00b612f0932698e4223f0a8a97957fafed', + 'site/og.png': 'affd93edb1d511780bad26de47cc673a5e93014967916029dd8db68ef9ec394e', + 'site/project-mark-color.svg': '4ec733f9542323f6d2840ba743c4a11df4d416a7efa1d214567d62b58653619b', + 'site/project-mark-reversed.svg': 'dc08a6ba51a2a8e9cb82d56f900ef2de9330ac72fbb3c727e0436a4ab2f3944f', +} + +for (const [file, expected] of Object.entries(assets)) { + assert.equal(await hash(file), expected, `${file} must be the approved O04 asset`) +} + +const checksumFile = 'docs/assets/brand/checksums.txt' +assert.equal(await hash(checksumFile), 'f1c266d086750eed1fc053e10e858ad9d33c3227d468b823cc7c7d8ca2f4a1ed', 'vendored O04 checksums.txt must remain exact') +const checksums = await read(checksumFile) +for (const [asset, expected] of Object.entries({ + 'glyph-color.svg': '4ec733f9542323f6d2840ba743c4a11df4d416a7efa1d214567d62b58653619b', + 'lockup-endorsed.svg': '905cf0a05ab5d3787bc02ff7d4591bfa020d72aefdd0e0d0c53271da6d3f383c', + 'readme-header.png': '481391d02b1f50ed6dba1790c0899bcc7d16948421bba66778756a89921b5dbb', +})) { + assert.match(checksums, new RegExp(`${expected} ${asset}`), `checksums.txt must declare ${asset}`) +} + +const preview = 'https://raw.githubusercontent.com/devslab-kr/numkey/main/docs/preview.png' +assert.equal(await hash('docs/preview.png'), 'c26e0695abc6122c508d1af04173ad95c864b8c82965b827b210f4e5a7b67715', 'docs/preview.png must be preserved byte-for-byte') +for (const [file, endorsement] of [ + ['README.md', /Open source by \[DevsLab\]\(https:\/\/devslab\.kr\/brand\/open-source\/\)/], + ['README.ko.md', /\[DevsLab 오픈소스\]\(https:\/\/devslab\.kr\/brand\/open-source\/\)/], +]) { + const content = await read(file) + assert.match(content, endorsement, `${file} needs its localized DevsLab endorsement`) + assert.match(content, /docs\/assets\/brand\/readme-header\.png/, `${file} needs the O04 README header`) + assert.match(content, new RegExp(preview.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `${file} must retain the preview reference`) +} + +const site = await read('site/index.html') +assert.match(site, /srcset="project-mark-reversed\.svg"/, 'dark mode must use the explicit O04 reversed mark') +assert.match(site, /src="project-mark-color\.svg"/, 'light mode must use the explicit O04 color mark') +assert.doesNotMatch(site, /src="project-lockup\.svg"/, 'site must not render an unstyleable currentColor lockup through img') +assert.match(site, /rel="canonical" href="https:\/\/devslab-kr\.github\.io\/numkey\/"/, 'site needs a canonical URL') +assert.match(site, /property="og:title" content="numkey/, 'site needs an Open Graph title') +assert.match(site, /property="og:image" content="https:\/\/devslab-kr\.github\.io\/numkey\/og\.png"/, 'site needs the O04 Open Graph image') +assert.match(site, /name="twitter:card" content="summary_large_image"/, 'site needs a Twitter card') +assert.match(site, /name="twitter:image:alt" content="numkey - a stable caret across grouped numeric units"/, 'site needs Twitter image alt text') +const lightGlow = site.indexOf('rgb(6 182 212 / .11)') +const darkGlow = site.lastIndexOf('@media (prefers-color-scheme: dark)') +assert.ok(darkGlow > lightGlow, 'dark atmosphere override must follow the light glow in source order') +assert.match(site.slice(darkGlow), /rgb\(6 182 212 \/ \.10\)/, 'dark atmosphere must cap cyan opacity at .10') + +const pkg = JSON.parse(await read('package.json')) +assert.equal(pkg.scripts.verify, 'npm run check:brand && npm run typecheck && npm run test && npm run build', 'routine verification must include the brand check') +assert.equal(pkg.scripts.prepublishOnly, 'npm run verify', 'publishing must use routine verification') +for (const workflow of ['.github/workflows/ci.yml', '.github/workflows/publish.yml']) { + assert.match(await read(workflow), /npm run verify/, `${workflow} must use the routine verification gate`) +} + +console.log(`Verified ${Object.keys(assets).length} approved O04 assets and numkey brand surfaces.`) diff --git a/site/apple-touch-icon.png b/site/apple-touch-icon.png new file mode 100644 index 0000000..46d81b0 Binary files /dev/null and b/site/apple-touch-icon.png differ diff --git a/site/favicon.ico b/site/favicon.ico new file mode 100644 index 0000000..6ca6a49 Binary files /dev/null and b/site/favicon.ico differ diff --git a/site/favicon.svg b/site/favicon.svg new file mode 100644 index 0000000..e7b2f73 --- /dev/null +++ b/site/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/index.html b/site/index.html index d46ec52..71a5827 100644 --- a/site/index.html +++ b/site/index.html @@ -3,17 +3,32 @@ -It's a string, but it's a number. — Numeric input formatting, done once.
-문자열인데 숫자인 필드 — 콤마·커서·정렬·앞자리 0, 한 번에 끝.
It's a string, but it's a number. Numeric input formatting, done once.
+ 문자열인데 숫자인 필드. 콤마·커서·정렬·앞자리 0, 한 번에 끝.