diff --git a/package.json b/package.json index be80e0a..9e60aab 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "dependencies": { "@astrojs/starlight": "^0.37.1", "@bomb.sh/args": "^0.3.1", - "@clack/core": "^1.0.0", - "@clack/prompts": "1.0.0", + "@clack/core": "^1.2.0", + "@clack/prompts": "^1.2.0", "@types/node": "^22.19.3", "@webcontainer/api": "^1.6.1", "@webcontainer/snapshot": "^0.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce5be6c..a8a71be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: ^0.3.1 version: 0.3.1 '@clack/core': - specifier: ^1.0.0 - version: 1.0.0 + specifier: ^1.2.0 + version: 1.2.0 '@clack/prompts': - specifier: 1.0.0 - version: 1.0.0 + specifier: ^1.2.0 + version: 1.2.0 '@types/node': specifier: ^22.19.3 version: 22.19.3 @@ -128,11 +128,11 @@ packages: resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==} engines: {node: '>=18'} - '@clack/core@1.0.0': - resolution: {integrity: sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==} + '@clack/core@1.2.0': + resolution: {integrity: sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg==} - '@clack/prompts@1.0.0': - resolution: {integrity: sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==} + '@clack/prompts@1.2.0': + resolution: {integrity: sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w==} '@cloudflare/kv-asset-handler@0.4.2': resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} @@ -567,155 +567,183 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -862,66 +890,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -1398,6 +1439,15 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-string-truncated-width@1.2.1: + resolution: {integrity: sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow==} + + fast-string-width@1.1.0: + resolution: {integrity: sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ==} + + fast-wrap-ansi@0.1.6: + resolution: {integrity: sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2504,15 +2554,16 @@ snapshots: dependencies: fontkit: 2.0.4 - '@clack/core@1.0.0': + '@clack/core@1.2.0': dependencies: - picocolors: 1.1.1 + fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 - '@clack/prompts@1.0.0': + '@clack/prompts@1.2.0': dependencies: - '@clack/core': 1.0.0 - picocolors: 1.1.1 + '@clack/core': 1.2.0 + fast-string-width: 1.1.0 + fast-wrap-ansi: 0.1.6 sisteransi: 1.0.5 '@cloudflare/kv-asset-handler@0.4.2': {} @@ -3628,6 +3679,16 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-string-truncated-width@1.2.1: {} + + fast-string-width@1.1.0: + dependencies: + fast-string-truncated-width: 1.2.1 + + fast-wrap-ansi@0.1.6: + dependencies: + fast-string-width: 1.1.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 diff --git a/scripts/snapshot.ts b/scripts/snapshot.ts index 7acb123..1714790 100644 --- a/scripts/snapshot.ts +++ b/scripts/snapshot.ts @@ -1,27 +1,42 @@ +/** + * Builds `public/snapshot`: a serialized node_modules tree for StackBlitz WebContainer. + * The docs site mounts this in the browser so live examples can `import` Clack without + * running `npm install` inside the VM. Versions come from the repo root package.json + * so playground code matches Twoslash / the docs. + */ import fs from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { snapshot } from '@webcontainer/snapshot'; -import {x} from 'tinyexec'; -import { createHash } from "node:crypto"; +import { x } from 'tinyexec'; +import { createHash } from 'node:crypto'; -const PACKAGE_JSON = { - name: 'example', - type: 'module', - version: '0.0.0', - dependencies: { - "@bomb.sh/args": "latest", - "@clack/core": "1.0.0-alpha.0", - "@clack/prompts": "1.0.0-alpha.0" - } +const rootDir = new URL('../', import.meta.url); + +async function packageJsonForSnapshot() { + const raw = await fs.readFile(new URL('package.json', rootDir), 'utf8'); + const root = JSON.parse(raw) as { + dependencies: Record; + }; + const { dependencies: d } = root; + return { + name: 'example', + type: 'module', + version: '0.0.0', + dependencies: { + '@bomb.sh/args': d['@bomb.sh/args'], + '@clack/core': d['@clack/core'], + '@clack/prompts': d['@clack/prompts'], + }, + }; } const IGNORE_FILES = ['*.md', '*.d.*', '*.map', 'LICENSE', 'license']; -const rootDir = new URL('../', import.meta.url); const snapshotDir = new URL(`./snapshot-${hash()}/`, rootDir); const outFile = new URL('./public/snapshot', rootDir); async function run() { + const pkg = await packageJsonForSnapshot(); await fs.mkdir(snapshotDir, { recursive: true }); - await fs.writeFile(new URL('package.json', snapshotDir), JSON.stringify(PACKAGE_JSON)); + await fs.writeFile(new URL('package.json', snapshotDir), JSON.stringify(pkg)); await x('npm', ['install'], { nodeOptions: { cwd: fileURLToPath(snapshotDir), diff --git a/src/content/docs/clack/basics/getting-started.mdx b/src/content/docs/clack/basics/getting-started.mdx index fa86179..2f32fa1 100644 --- a/src/content/docs/clack/basics/getting-started.mdx +++ b/src/content/docs/clack/basics/getting-started.mdx @@ -90,17 +90,20 @@ Clack provides several high-level components that make it easy to build interact - `text()` - For text input with validation - `password()` - For secure password input with masking - `select()` - For selection menus +- `selectKey()` - For choosing an option by pressing its key - `confirm()` - For yes/no confirmations - `multiselect()` - For multiple selections - `groupMultiselect()` - For grouped multiple selections - `autocomplete()` - For searchable selection menus - `path()` - For file/directory path selection with autocomplete +- `date()` - For structured date entry (YMD / MDY / DMY) - `note()` - For displaying information - `box()` - For boxed text display - `spinner()` - For loading states - `progress()` - For progress bar display - `tasks()` - For sequential task execution - `taskLog()` - For log output that clears on success +- `stream` - For multi-line streamed output (async iterators, readable streams) ### Low-Level Primitives diff --git a/src/content/docs/clack/guides/examples.mdx b/src/content/docs/clack/guides/examples.mdx index 602a813..5195b9a 100644 --- a/src/content/docs/clack/guides/examples.mdx +++ b/src/content/docs/clack/guides/examples.mdx @@ -745,6 +745,53 @@ async function collectUserData(): Promise { ## New Features Examples +### Date picker + +Use `date` with `format` (`YMD`, `MDY`, or `DMY`) and optional bounds: + +```ts twoslash +import { date, isCancel, cancel, outro } from '@clack/prompts'; + +async function pickReleaseDate() { + const result = await date({ + message: 'Pick a release date', + format: 'YMD', + minDate: new Date('2026-01-01'), + maxDate: new Date('2026-12-31'), + }); + + if (isCancel(result)) { + cancel('Operation cancelled.'); + process.exit(0); + } + + outro(`Selected: ${result.toISOString().slice(0, 10)}`); +} +``` + +### Select by key + +Compact menus where each choice is bound to a key: + +```ts twoslash +import { selectKey, isCancel } from '@clack/prompts'; + +async function confirmDestructive() { + const choice = await selectKey({ + message: 'Delete all cache?', + options: [ + { value: 'y', label: 'Yes, delete' }, + { value: 'n', label: 'No, keep' }, + ], + }); + + if (isCancel(choice) || choice === 'n') { + return false; + } + return choice === 'y'; +} +``` + ### Path Selection The `path` prompt provides file system navigation with autocomplete: @@ -756,6 +803,7 @@ async function selectConfigFile() { const configPath = await path({ message: 'Select a configuration file:', root: process.cwd(), + directory: false, validate: (value) => { if (!value?.endsWith('.json') && !value?.endsWith('.yaml')) { return 'Please select a .json or .yaml file'; @@ -773,6 +821,33 @@ async function selectConfigFile() { } ``` +With `directory: true`, only folders appear in suggestions; with an existing directory as `initialValue`, **Enter** submits that folder directly (v1.2.0). + +### Autocomplete Tab and placeholder + +When the search box is empty and `placeholder` is set, **Tab** inserts the placeholder string—useful for default tokens: + +```ts twoslash +import { autocomplete, isCancel } from '@clack/prompts'; + +async function pickWithDefaultQuery() { + const tag = await autocomplete({ + message: 'Filter by tag:', + placeholder: 'latest', + options: [ + { value: 'latest', label: 'latest' }, + { value: 'stable', label: 'stable' }, + { value: 'canary', label: 'canary' }, + ], + }); + + if (isCancel(tag)) { + return null; + } + return tag; +} +``` + ### Progress Bar Display progress for long-running operations: diff --git a/src/content/docs/clack/packages/core.mdx b/src/content/docs/clack/packages/core.mdx index d029639..731bdab 100644 --- a/src/content/docs/clack/packages/core.mdx +++ b/src/content/docs/clack/packages/core.mdx @@ -43,16 +43,23 @@ To start using the core package, first install it: Then import the components you need: ```ts twoslash -import { +import { // Prompt classes - TextPrompt, - SelectPrompt, + TextPrompt, + SelectPrompt, ConfirmPrompt, PasswordPrompt, MultiSelectPrompt, GroupMultiSelectPrompt, SelectKeyPrompt, AutocompletePrompt, + DatePrompt, + + // Layout helpers + block, + getColumns, + getRows, + wrapTextWithPrefix, // Utilities isCancel, @@ -68,6 +75,9 @@ import { type GroupMultiSelectOptions, type SelectKeyOptions, type AutocompleteOptions, + type DateFormat, + type DateOptions, + type DateParts, } from '@clack/core'; ``` @@ -144,7 +154,7 @@ p.on('cancel', () => { - Supports validation - Placeholder text - Initial value - - Separate `userInput` and `value` tracking + - Separate `userInput` and `value` tracking; use **`userInputWithCursor`** when rendering so the raw buffer and cursor position match what the user sees 2. **SelectPrompt**: For selection from options - Custom rendering @@ -158,9 +168,10 @@ p.on('cancel', () => { 4. **AutocompletePrompt**: For searchable selection - Type-ahead filtering - - Custom filtering logic + - Custom filtering logic via the `filter` option - Multiple selection support (`multiple: true`) - Dynamic options (function or array) + - As of v1.2.0, the built-in default filter runs only when `filter` is set explicitly **or** when `options` is not a getter—so lazy `options` getters are not pre-filtered unexpectedly 5. **PasswordPrompt**: For secure input - Character masking @@ -184,6 +195,22 @@ p.on('cancel', () => { - Custom key bindings - Multiple selection support +9. **DatePrompt**: For structured date entry + - Segment-based editing (year, month, day) with `DateFormat` (`YMD`, `MDY`, or `DMY`) + - Optional `locale` for segment order and separator via `Intl` + - Optional `separator` override for display + - `minDate` / `maxDate` bounds and `DateParts` segment values + +10. **Multi-line input**: v1.2.0 does not include a dedicated multi-line prompt in `@clack/core` or `@clack/prompts`. Use `text()` for a single line, an external editor or stdin for paragraphs, or extend `Prompt` for custom multi-line TTY behavior. + +## Layout utilities + +These helpers are useful when building custom prompts or lists that respect terminal width: + +- **`getColumns(output?)` / `getRows(output?)`**: Terminal size for the given writable (defaults to `stdout`). +- **`wrapTextWithPrefix(output, text, prefix, ...)`**: Wrap lines to the terminal width while repeating a prefix on each line (used heavily by prompts for guide-aligned output). +- **`block`**: Lower-level TTY helper (raw mode, keypress handling, optional cursor hide) used when building custom full-screen or overlay flows; most apps use the prompt classes instead. + ## Global Settings The core package provides global settings that affect all prompts: diff --git a/src/content/docs/clack/packages/prompts.mdx b/src/content/docs/clack/packages/prompts.mdx index 9f3984c..27d7888 100644 --- a/src/content/docs/clack/packages/prompts.mdx +++ b/src/content/docs/clack/packages/prompts.mdx @@ -48,7 +48,7 @@ All prompts share these common options: ### Guide Lines -The `withGuide` option controls whether the default Clack border/guide lines are displayed. You can disable them globally or per-prompt: +The `withGuide` option (boolean **option**, not a separate API) turns Clack’s border/guide lines on or off. Every prompt accepts it alongside `message` and friends. You can set it globally with `updateSettings` or override it per call. ```ts twoslash import { text, updateSettings } from '@clack/prompts'; @@ -63,6 +63,10 @@ const name = await text({ }); ``` +Session helpers use the same option on their **second argument**: `intro(title, { withGuide: false })`, `outro(message, { withGuide: false })`, and `cancel(message, { withGuide: false })`. + +`autocomplete` and `multiselect` respect `withGuide: false` per prompt (as of v1.2.0), matching other prompts. + ### AbortController Support All prompts accept a `signal` option for programmatic cancellation: @@ -245,6 +249,28 @@ const framework = await multiselect({ │ ◻ SvelteKit (Compile-time framework) └ +### Select by key + +`selectKey` shows each option with a visible key (the option `value`, typically one character). The user presses that key instead of moving a cursor with arrows—useful for compact yes/no/maybe menus or vim-style shortcuts. + +```ts twoslash +import { selectKey, isCancel } from '@clack/prompts'; + +const action = await selectKey({ + message: 'What next?', + options: [ + { value: 'y', label: 'Continue' }, + { value: 'n', label: 'Stop' }, + { value: 's', label: 'Skip', hint: 'optional' }, + ], + caseSensitive: false, +}); + +if (isCancel(action)) { + process.exit(0); +} +``` + ### Autocomplete The `autocomplete` prompt combines text input with a searchable list of options. It's perfect for when you have a large list of options and want to help users find what they're looking for quickly. @@ -274,6 +300,55 @@ const framework = await autocomplete({ │ ○ Nuxt (Vue framework) └ +If you set `placeholder` and the search field is **empty**, pressing **Tab** copies the placeholder string into the input (v1.2.0)—handy for default search tokens or quick acceptance of a suggested value. + +#### Dynamic options (getter) + +Instead of a static array, `options` can be a **function** whose `this` is the underlying [`AutocompletePrompt`](https://github.com/bombshell-dev/clack/blob/main/packages/core/src/prompts/autocomplete.ts) from `@clack/core`. The function runs again whenever the search text changes, so you can read **`this.userInput`** and return a **new array in display order**—for example closest / highest-score matches first (similar to [fzf](https://github.com/junegunn/fzf)-style UIs). This pattern is what [issue #467](https://github.com/bombshell-dev/clack/issues/467) discusses for custom ranking and libraries like [Fuse.js](https://fusejs.io/). + +The high-level `autocomplete` wrapper still applies its **default `filter`** (substring match on label, hint, and value) to whatever your getter returns. If you already narrow or rank items in the getter, disable that second pass with **`filter: (_search, _option) => true`**, or pass a custom `(search, option) => boolean` aligned with your getter. + +`options` as a getter must be **synchronous**; there is no async API here—preload or sync work inside the function. + +The same `options` shape is supported on **`autocompleteMultiselect`**. + +```ts twoslash +import { autocomplete } from '@clack/prompts'; +import type { AutocompletePrompt } from '@clack/core'; +import type { Option } from '@clack/prompts'; + +const pool: Option[] = [ + { value: 'next', label: 'Next.js', hint: 'React' }, + { value: 'nuxt', label: 'Nuxt', hint: 'Vue' }, + { value: 'nest', label: 'NestJS', hint: 'Node' }, +]; + +function rankByQuery(query: string, items: Option[]): Option[] { + const q = query.trim().toLowerCase(); + if (!q) return [...items]; + return [...items] + .filter((o) => { + const t = `${o.label ?? ''} ${o.hint ?? ''} ${o.value}`.toLowerCase(); + return t.includes(q); + }) + .sort((a, b) => { + const la = (a.label ?? '').toLowerCase(); + const lb = (b.label ?? '').toLowerCase(); + const sa = la.startsWith(q) ? 0 : 1; + const sb = lb.startsWith(q) ? 0 : 1; + return sa - sb || la.localeCompare(lb); + }); +} + +const picked = await autocomplete({ + message: 'Pick a framework', + options(this: AutocompletePrompt>) { + return rankByQuery(this.userInput, pool); + }, + filter: (_search, _option) => true, +}); +``` + ### Autocomplete Multiselect The `autocompleteMultiselect` combines the search functionality of autocomplete with the ability to select multiple options. @@ -329,10 +404,33 @@ const selectedPath = await path({ Options: - `message`: The prompt message to display - `root`: The starting directory for path suggestions (defaults to current working directory) -- `directory`: When `true`, only directories are shown (defaults to `false`) -- `initialValue`: Pre-fill the path input with an initial value +- `directory`: When `true`, only **directories** appear in suggestions while you navigate; you still move through the tree normally (v1.2.0 fixes for directory-only mode). +- `initialValue`: Pre-fill the path input. In **directory** mode, if `initialValue` already points at an existing directory, pressing **Enter** submits **that** directory immediately instead of jumping to the first child (v1.2.0). - `validate`: Custom validation function for the selected path +### Date input + +`date` returns a `Date` on success or a cancel symbol. Use `format` to pick segment order: `YMD`, `MDY`, or `DMY`. Pass `locale` (BCP 47 string) to derive order and separators from `Intl`, or rely on the format alone. + +```ts twoslash +import { date, isCancel } from '@clack/prompts'; + +const picked = await date({ + message: 'Pick a date', + format: 'YMD', + minDate: new Date('2026-01-01'), + maxDate: new Date('2026-12-31'), +}); + +if (isCancel(picked)) { + process.exit(0); +} + +// picked is a Date +``` + +Options include `defaultValue`, `initialValue`, `minDate`, `maxDate`, `validate`, and the usual `signal`, `input`, `output`, and `withGuide` settings. + ### Confirmation ```ts twoslash @@ -348,6 +446,10 @@ const shouldProceed = await confirm({ │ ● Yes / ○ No └ +Options: +- `vertical: true`: Stack Yes / No vertically instead of inline (v1.0.1+). +- Multi-line `message` strings wrap correctly; guide lines apply to wrapped confirmation text (v1.2.0). + ## Grouping ### Group Multiselect @@ -866,15 +968,27 @@ log.step('Check files'); The prompts package supports internationalization through the `updateSettings` function. You can customize the messages used by various prompts to match your preferred language. ```ts twoslash -// @errors: 2353 import { updateSettings, select, cancel } from '@clack/prompts'; // Update global messages updateSettings({ messages: { cancel: "Operación cancelada", - error: "Se ha producido un error" - } + error: "Se ha producido un error", + }, + date: { + monthNames: [ + "enero", "febrero", "marzo", "abril", "mayo", "junio", + "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre", + ], + messages: { + required: "Introduce una fecha válida", + invalidMonth: "Solo hay 12 meses", + invalidDay: (days, month) => `Solo hay ${days} días en ${month}`, + afterMin: (min) => `La fecha debe ser el ${min.toISOString().slice(0, 10)} o posterior`, + beforeMax: (max) => `La fecha debe ser el ${max.toISOString().slice(0, 10)} o anterior`, + }, + }, }); // Use the select prompt with translated content @@ -961,3 +1075,21 @@ await stream.step([ ◇ Job1{`...`} done │ Job2{`...`} done │ Job3{`...`} done + +### limitOptions + +`limitOptions` trims a long option list to what fits the terminal, returning the lines to render and keeping the active index visible—intended for **custom** prompts that mirror Clack’s sliding window (see `@clack/prompts` source). Pass `options`, `cursor`, a `style` callback, optional `maxItems`, `columnPadding`, `rowPadding`, and `output` if not using `stdout`. + +```ts twoslash +import { limitOptions } from '@clack/prompts'; +import { styleText } from 'node:util'; + +const options = ['apple', 'banana', 'cherry', 'date']; +const lines = limitOptions({ + options, + cursor: 2, + style: (opt, active) => + active ? styleText('cyan', opt) : styleText('dim', opt), + maxItems: 8, +}); +```