diff --git a/README.md b/README.md index 3235c273c..2a849ab65 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Daily downloads of Observable Framework -Daily downloads of Observable Framework · [oss-analytics](https://github.com/observablehq/oss-analytics/) +Daily downloads of Observable Framework · [oss-analytics](https://observablehq.observablehq.cloud/oss-analytics/) ## Documentation 📚 diff --git a/docs/imports.md b/docs/imports.md index f6f1bed1d..a9cb8707e 100644 --- a/docs/imports.md +++ b/docs/imports.md @@ -230,7 +230,7 @@ Click on any of the imported symbols below to learn more.
import * as duckdb from "npm:@duckdb/duckdb-wasm";
import {DuckDBClient} from "npm:@observablehq/duckdb";
import {sql} from "npm:@observablehq/duckdb";
-
import * as Inputs from "npm:@observablehq/inputs";
+
import * as Inputs from "npm:@observablehq/inputs";
import mapboxgl from "npm:mapbox-gl";
import mermaid from "npm:@observablehq/mermaid";
import * as Plot from "npm:@observablehq/plot";
diff --git a/docs/lib/inputs.md b/docs/inputs/index.md similarity index 64% rename from docs/lib/inputs.md rename to docs/inputs/index.md index 324ad0800..c92c9d41b 100644 --- a/docs/lib/inputs.md +++ b/docs/inputs/index.md @@ -30,10 +30,10 @@ const checkout = view( checkout ``` -To demonstrate Observable Inputs, let’s look at a sample dataset of athletes from the 2016 Rio olympics via [Matt Riggott](https://flother.is/2017/olympic-games-data/). Here’s a [table input](../inputs/table) — always a good starting point for an agnostic view of the data: +To demonstrate Observable Inputs, let’s look at a sample dataset of athletes from the 2016 Rio olympics via [Matt Riggott](https://flother.is/2017/olympic-games-data/). Here’s a [table input](./table) — always a good starting point for an agnostic view of the data: ```js -const olympians = await d3.csv("https://static.observableusercontent.com/files/31ca24545a0603dce099d10ee89ee5ae72d29fa55e8fc7c9ffb5ded87ac83060d80f1d9e21f4ae8eb04c1e8940b7287d179fe8060d887fb1f055f430e210007c", (d) => (delete d.id, delete d.info, d3.autoType(d))); +const olympians = await d3.csv(import.meta.resolve("npm:@observablehq/sample-datasets/olympians.csv"), (d) => (delete d.id, delete d.info, d3.autoType(d))); ``` ```js echo @@ -42,7 +42,7 @@ Inputs.table(olympians)
Tables can be inputs, too! The value of the table is the subset of rows that you select using the checkboxes in the first column.
-Now let’s wire up the table to a [search input](../inputs/search). Type anything into the box and the search input will find the matching rows in the data. The value of the search input is the subset of rows that match the query. +Now let’s wire up the table to a [search input](./search). Type anything into the box and the search input will find the matching rows in the data. The value of the search input is the subset of rows that match the query. A few examples to try: **[mal]** will match _sex_ = male, but also names that start with “mal”, such as Anna Malova; **[1986]** will match anyone born in 1986 (and a few other results); **[USA gym]** will match USA’s gymnastics team. Each space-separated term in your query is prefix-matched against all columns in the data. @@ -80,7 +80,7 @@ Plot.plot({ }) ``` -You can pass grouped data to a [select input](../inputs/select) as a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) from key to array of values, say using [d3.group](https://d3js.org/d3-array/group). The value of the select input in this mode is the data in the selected group. Note that _unique_ is no longer required, and that _sort_ works here, too, sorting the keys of the map returned by d3.group. +You can pass grouped data to a [select input](./select) as a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) from key to array of values, say using [d3.group](https://d3js.org/d3-array/group). The value of the select input in this mode is the data in the selected group. Note that _unique_ is no longer required, and that _sort_ works here, too, sorting the keys of the map returned by d3.group. ```js echo const sportAthletes = view( @@ -95,7 +95,7 @@ const sportAthletes = view( Inputs.table(sportAthletes) ``` -The select input works well for categorical data, such as sports or nationalities, but how about quantitative dimensions such as height or weight? Here’s a [range input](../inputs/range) that lets you pick a target weight; we then filter the table rows for any athlete within 10% of the target weight. Notice that some columns, such as sport, are strongly correlated with weight. +The select input works well for categorical data, such as sports or nationalities, but how about quantitative dimensions such as height or weight? Here’s a [range input](./range) that lets you pick a target weight; we then filter the table rows for any athlete within 10% of the target weight. Notice that some columns, such as sport, are strongly correlated with weight. ```js echo const weight = view( @@ -115,16 +115,16 @@ Inputs.table( For more, see the individual input pages: -- [Button](../inputs/button) - do something when a button is clicked -- [Toggle](../inputs/toggle) - toggle between two values (on or off) -- [Checkbox](../inputs/checkbox) - choose any from a set -- [Radio](../inputs/radio) - choose one from a set -- [Range](../inputs/range) or [Number](../inputs/range) - choose a number in a range (slider) -- [Select](../inputs/select) - choose one or any from a set (drop-down menu) -- [Text](../inputs/text) - enter freeform single-line text -- [Textarea](../inputs/textarea) - enter freeform multi-line text -- [Date](../inputs/date) or [Datetime](../inputs/date) - choose a date -- [Color](../inputs/color) - choose a color -- [File](../inputs/file) - choose a local file -- [Search](../inputs/search) - query a tabular dataset -- [Table](../inputs/table) - browse a tabular dataset +- [Button](./button) - do something when a button is clicked +- [Toggle](./toggle) - toggle between two values (on or off) +- [Checkbox](./checkbox) - choose any from a set +- [Radio](./radio) - choose one from a set +- [Range](./range) or [Number](./range) - choose a number in a range (slider) +- [Select](./select) - choose one or any from a set (drop-down menu) +- [Text](./text) - enter freeform single-line text +- [Textarea](./textarea) - enter freeform multi-line text +- [Date](./date) or [Datetime](./date) - choose a date +- [Color](./color) - choose a color +- [File](./file) - choose a local file +- [Search](./search) - query a tabular dataset +- [Table](./table) - browse a tabular dataset diff --git a/docs/lib/sqlite.md b/docs/lib/sqlite.md index b5259410d..1e812b6b3 100644 --- a/docs/lib/sqlite.md +++ b/docs/lib/sqlite.md @@ -2,13 +2,13 @@ [SQLite](https://sqlite.org/) is “a small, fast, self-contained, high-reliability, full-featured, SQL database engine” and “the most used database engine in the world.” Observable provides a ESM-compatible distribution of [sql.js](https://sql.js.org), a WASM-based distribution of SQLite. It is available by default as `SQLite` in Markdown, but you can import it like so: -```js echo +```js run=false import SQLite from "npm:@observablehq/sqlite"; ``` We also provide `SQLiteDatabaseClient`, a [`DatabaseClient`](https://observablehq.com/@observablehq/database-client-specification) implementation. -```js echo +```js run=false import {SQLiteDatabaseClient} from "npm:@observablehq/sqlite"; ``` diff --git a/docs/loaders.md b/docs/loaders.md deleted file mode 100644 index 75bb3ef78..000000000 --- a/docs/loaders.md +++ /dev/null @@ -1,3 +0,0 @@ - - -Moved to [Data loaders](./data-loaders). diff --git a/docs/reactivity.md b/docs/reactivity.md index 8d57f7c9b..9a5885e4c 100644 --- a/docs/reactivity.md +++ b/docs/reactivity.md @@ -267,7 +267,7 @@ My favorite baseball team is the ${team}! My favorite baseball team is the ${team}! ``` -The above example uses `Inputs.radio`, which is provided by [Observable Inputs](./lib/inputs). You can also implement custom inputs using arbitrary HTML. For example, here is a [range input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range) that lets you choose an integer between 1 and 15 (inclusive): +The above example uses `Inputs.radio`, which is provided by [Observable Inputs](./inputs/). You can also implement custom inputs using arbitrary HTML. For example, here is a [range input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range) that lets you choose an integer between 1 and 15 (inclusive): ```js echo const n = view(html``); diff --git a/docs/sql.md b/docs/sql.md index 05361025e..3796f1a53 100644 --- a/docs/sql.md +++ b/docs/sql.md @@ -174,11 +174,12 @@ Plot.plot({ marks: [ Plot.axisY({tickFormat: (d) => d / 1000, label: "count (thousands)"}), Plot.rectY(await sql` - SELECT - FLOOR(phot_g_mean_mag / 0.2) * 0.2 AS mag1 - , mag1 + 0.2 AS mag2 - , COUNT() AS count - FROM gaia GROUP BY 1 + SELECT FLOOR(phot_g_mean_mag / 0.2) * 0.2 AS mag1 + , mag1 + 0.2 AS mag2 + , COUNT() AS count + FROM gaia + WHERE phot_g_mean_mag IS NOT NULL + GROUP BY 1 `, {x1: "mag1", x2: "mag2", y: "count", tip: true}) ] }) diff --git a/docs/style.css b/docs/style.css index 468ffe45c..1948c7779 100644 --- a/docs/style.css +++ b/docs/style.css @@ -51,17 +51,13 @@ code:not(pre code, h1 code, h2 code, h3 code, h4 code, h5 code, h6 code) { text-decoration: underline; } -#observablehq-header a[target="_blank"]::after, -.observablehq-link a[target="_blank"]::after { - content: "\2197"; -} - #observablehq-header a[target="_blank"][data-decoration]::after { content: attr(data-decoration); } -#observablehq-header a[target="_blank"]:not(:hover, :focus)::after { - color: var(--theme-foreground-muted); +#observablehq-header a[target="_blank"]:is(:hover, :focus), +#observablehq-header a[target="_blank"]:is(:hover, :focus)::after { + color: var(--theme-foreground-focus); } .observablehq-link a[target="_blank"]:not(:hover, :focus)::after { @@ -90,11 +86,13 @@ h1 { font-weight: 500; } -#observablehq-header { - container-type: inline-size; +@media (min-width: 640px) { + .hide-if-large { + display: none !important; + } } -@container not (min-width: 640px) { +@media not (min-width: 640px) { .hide-if-small { display: none !important; } diff --git a/observablehq.config.ts b/observablehq.config.ts index 9c4ebf910..59b54bdf3 100644 --- a/observablehq.config.ts +++ b/observablehq.config.ts @@ -44,6 +44,7 @@ export default { name: "Inputs", open: false, pager: "inputs", + path: "/inputs/", pages: [ {name: "Button", path: "/inputs/button"}, {name: "Checkbox", path: "/inputs/checkbox"}, @@ -81,7 +82,6 @@ export default { {name: "Microsoft Excel (XLSX)", path: "/lib/xlsx"}, {name: "Mosaic vgplot", path: "/lib/mosaic"}, {name: "Observable Generators", path: "/lib/generators"}, - {name: "Observable Inputs", path: "/lib/inputs"}, {name: "Observable Plot", path: "/lib/plot"}, {name: "Shapefile", path: "/lib/shapefile"}, {name: "SQLite", path: "/lib/sqlite"}, @@ -120,37 +120,35 @@ export default { : "" } `, - home: ` - - - - Observable Framework + home: ` + ${logo()} Framework `, header: `
- - - - - - Observable - Framework - + + ${logo()} Framework
- - ​ - + ${ process.env.npm_package_version } - GitHub️ ${ stargazers_count ? formatPrefix(".1s", 1000)(stargazers_count) : "" } - + + + + + + +
`, footer: `© ${new Date().getUTCFullYear()} Observable, Inc.`, style: "style.css", @@ -187,3 +185,9 @@ async function github( if (!response.ok) throw new Error(`fetch error: ${response.status} ${url}`); return await response.json(); } + +function logo() { + return ` + +`; +} diff --git a/src/build.ts b/src/build.ts index b34d8796c..ccf9df04d 100644 --- a/src/build.ts +++ b/src/build.ts @@ -237,7 +237,7 @@ export async function build( if (!path.endsWith(".js")) continue; const sourcePath = join(cacheRoot, path); effects.output.write(`${faint("build")} ${path} ${faint("→")} `); - const resolveImport = (i: string) => relativePath(path, aliases.get((i = resolvePath(path, i))) ?? i); + const resolveImport = (i: string) => isPathImport(i) ? relativePath(path, aliases.get((i = resolvePath(path, i))) ?? i) : i; // prettier-ignore await effects.writeFile(aliases.get(path)!, rewriteNpmImports(await readFile(sourcePath, "utf-8"), resolveImport)); } diff --git a/src/client/sidebar-init.ts b/src/client/sidebar-init.ts index 6380c680d..5fdc1b85d 100644 --- a/src/client/sidebar-init.ts +++ b/src/client/sidebar-init.ts @@ -1,6 +1,3 @@ -// Remove basic authentication in the URL, if any (to fix file attachments). -if (Object.assign(document.createElement("a"), {href: ""}).password) location.replace(location.href); - const sidebar = document.querySelector("#observablehq-sidebar")!; const toggle = document.querySelector("#observablehq-sidebar-toggle")!; diff --git a/src/render.ts b/src/render.ts index ba1dc9fdb..309d4b1ef 100644 --- a/src/render.ts +++ b/src/render.ts @@ -36,6 +36,8 @@ export async function renderPage(page: MarkdownPage, options: RenderOptions & Re const toc = mergeToc(data.toc, options.toc); const {files, resolveFile, resolveImport} = resolvers; return String(html` + + ${path === "/404" ? html`\n` : ""} @@ -81,13 +83,17 @@ import ${preview || page.code.length ? `{${preview ? "open, " : ""}define} from ${preview ? `\nopen({hash: ${JSON.stringify(resolvers.hash)}, eval: (body) => eval(body)});\n` : ""}${page.code .map(({node, id, mode}) => `\n${transpileJavaScript(node, {id, path, params, mode, resolveImport, resolveFile})}`) .join("")}`)} -${sidebar ? html`\n${await renderSidebar(options, resolvers)}` : ""} + + +${sidebar ? html`\n${await renderSidebar(options, resolvers)}` : ""}
${renderHeader(page.header, resolvers)}${ toc.show ? html`\n${renderToc(findHeaders(page), toc.label)}` : "" }
${html.unsafe(rewriteHtml(page.body, resolvers))}
${renderFooter(page.footer, resolvers, options)}
+ + `); } diff --git a/src/sql.ts b/src/sql.ts index 29206041c..c18852cbb 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -9,10 +9,10 @@ export function transpileSql(content: string, {id, display}: Record (display(Inputs.table(_)), _))(await ${sql});`; + : `const ${id} = ((_) => (display(Inputs.table(_, {select: false})), _))(await ${sql});`; } function isValidBinding(input: string): boolean { diff --git a/src/style/layout.css b/src/style/layout.css index 18b2d1e47..2d7c373e2 100644 --- a/src/style/layout.css +++ b/src/style/layout.css @@ -27,7 +27,7 @@ body { top: 0; left: calc(max(0rem, (100vw - var(--observablehq-max-width)) / 2) + var(--observablehq-inset-left) + 2rem); right: calc(max(0rem, (100vw - var(--observablehq-max-width)) / 2) + var(--observablehq-inset-right) + 2rem); - z-index: 1; + z-index: 2; display: flex; align-items: center; gap: 0.5rem; diff --git a/test/input/build/imports/foo/foo.js b/test/input/build/imports/foo/foo.js index 41f582b58..7a1409aef 100644 --- a/test/input/build/imports/foo/foo.js +++ b/test/input/build/imports/foo/foo.js @@ -1,4 +1,5 @@ import "npm:d3"; +import "npm:@example/url-import"; import {bar} from "../bar/bar.js"; export {top} from "/top.js"; diff --git a/test/javascript/module-test.ts b/test/javascript/module-test.ts index 31ed9b172..aad6773a8 100644 --- a/test/javascript/module-test.ts +++ b/test/javascript/module-test.ts @@ -8,8 +8,8 @@ const emptyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b // through the code and verifying that they consider all the relevant files. describe("getModuleHash(root, path)", () => { it("returns the transitive content hash for the specified module", () => { - assert.strictEqual(getModuleHash("test/input/build/imports", "foo/foo.js"), "32f934a52fa34ba1b06aa6089fe5922dc442c9bf2dcddef864bc649a39d9eace"); // prettier-ignore - assert.strictEqual(getModuleHash("test/input/build/imports", "bar/bar.js"), "7fe009c8bb0049d9b84d53a00b29fb172bbf07d8232d2ace5f7c6f220b23eb16"); // prettier-ignore + assert.strictEqual(getModuleHash("test/input/build/imports", "foo/foo.js"), "e743cc5455594df5a3bd78622594dfb7a8ddb9277957be9b9732f33a88955d82"); // prettier-ignore + assert.strictEqual(getModuleHash("test/input/build/imports", "bar/bar.js"), "34442bce5f38762986a81229c551723cdc3d4c1509ac14dde193555e65013d76"); // prettier-ignore assert.strictEqual(getModuleHash("test/input/build/imports", "top.js"), "160847a6b4890d59f8e8862911bfbe3b8066955d31f2708cafbe51945c3c57b6"); // prettier-ignore assert.strictEqual(getModuleHash("test/input/build/fetches", "foo/foo.js"), "3bb4a170d2f3539934168741572d4aa3cd11da649d4ca88b408edefb5c287360"); // prettier-ignore assert.strictEqual(getModuleHash("test/input/build/fetches", "top.js"), "6c858de52de6ff26b19508e95448288da02fac62251b7ca2710a308a0ebfd7ba"); // prettier-ignore @@ -27,8 +27,8 @@ describe("getModuleInfo(root, path)", () => { assert.deepStrictEqual(redactModuleInfo("test/input/build/imports", "foo/foo.js"), { fileMethods: new Set(), files: new Set(), - hash: "c77c2490ea7b9a89dce7bad39973995e5158921bf8576955ae4a596c47a5a2a4", - globalStaticImports: new Set(["npm:d3"]), + hash: "17e03fbc08c28530c84ab1163901890915302d3f1d5af2c9256e3e8cab1324a9", + globalStaticImports: new Set(["npm:@example/url-import", "npm:d3"]), globalDynamicImports: new Set(), localDynamicImports: new Set(), localStaticImports: new Set(["../bar/bar.js", "../top.js"]) diff --git a/test/mocks/jsdelivr.ts b/test/mocks/jsdelivr.ts index 1dfbd4eee..70119be4b 100644 --- a/test/mocks/jsdelivr.ts +++ b/test/mocks/jsdelivr.ts @@ -1,7 +1,8 @@ import {getCurrentAgent, mockAgent} from "./undici.js"; -const packages: [name: string, {version: string; dependencies?: Record}][] = [ +const packages: [name: string, {version: string; contents?: string; dependencies?: Record}][] = [ ["@duckdb/duckdb-wasm", {version: "1.28.0"}], + ["@example/url-import", {version: "1.0.0", contents: "import('https://example.com');"}], ["@observablehq/inputs", {version: "0.10.6"}], ["@observablehq/plot", {version: "0.6.11"}], ["@observablehq/sample-datasets", {version: "1.0.1"}], @@ -50,7 +51,7 @@ export function mockJsDelivr() { .persist(); // prettier-ignore cdnClient .intercept({path: new RegExp(`^/npm/${name}@${pkg.version}/`), method: "GET"}) - .reply(200, "", {headers: {"cache-control": "public, immutable", "content-type": "text/javascript; charset=utf-8"}}) + .reply(200, pkg.contents ?? "", {headers: {"cache-control": "public, immutable", "content-type": "text/javascript; charset=utf-8"}}) .persist(); // prettier-ignore } }); diff --git a/test/output/build/404/404.html b/test/output/build/404/404.html index 238bbb31e..de7987e63 100644 --- a/test/output/build/404/404.html +++ b/test/output/build/404/404.html @@ -1,4 +1,6 @@ + + @@ -25,6 +27,8 @@ import "./_observablehq/client.00000001.js"; + +
+ + diff --git a/test/output/build/archives.posix/tar.html b/test/output/build/archives.posix/tar.html index 80134e2c1..df09c13b5 100644 --- a/test/output/build/archives.posix/tar.html +++ b/test/output/build/archives.posix/tar.html @@ -1,4 +1,6 @@ + + @@ -46,6 +48,8 @@ }}); + +