@bonnard/cli 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,9 +67,12 @@ function get(path) {
67
67
  function post(path, body) {
68
68
  return request("POST", path, body);
69
69
  }
70
+ function put(path, body) {
71
+ return request("PUT", path, body);
72
+ }
70
73
  function del(path) {
71
74
  return request("DELETE", path);
72
75
  }
73
76
 
74
77
  //#endregion
75
- export { loadCredentials as a, clearCredentials as i, get as n, saveCredentials as o, post as r, del as t };
78
+ export { clearCredentials as a, put as i, get as n, loadCredentials as o, post as r, saveCredentials as s, del as t };
@@ -0,0 +1,3 @@
1
+ import { i as put, n as get, r as post, t as del } from "./api-CirD2MoX.mjs";
2
+
3
+ export { del, get };
package/dist/bin/bon.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as getProjectPaths, t as BONNARD_DIR } from "./project-Dj085D_B.mjs";
3
- import { a as loadCredentials, i as clearCredentials, n as get, o as saveCredentials, r as post, t as del } from "./api-DqgY-30K.mjs";
3
+ import { a as clearCredentials, i as put, n as get, o as loadCredentials, r as post, s as saveCredentials, t as del } from "./api-CirD2MoX.mjs";
4
4
  import { i as ensureBonDir, n as addLocalDatasource, o as loadLocalDatasources, r as datasourceExists, s as removeLocalDatasource, t as isDatasourcesTrackedByGit } from "./local-ByvuW3eV.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { program } from "commander";
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import os from "node:os";
10
10
  import pc from "picocolors";
11
11
  import { fileURLToPath } from "node:url";
12
- import YAML from "yaml";
12
+ import YAML, { parse, stringify } from "yaml";
13
13
  import http from "node:http";
14
14
  import crypto from "node:crypto";
15
15
  import { encode } from "@toon-format/toon";
@@ -1601,7 +1601,7 @@ async function listRemoteDatasources() {
1601
1601
  return;
1602
1602
  }
1603
1603
  try {
1604
- const { get } = await import("./api-B7cdKn9j.mjs");
1604
+ const { get } = await import("./api-DZ1Xj0_d.mjs");
1605
1605
  const result = await get("/api/datasources");
1606
1606
  if (result.dataSources.length === 0) {
1607
1607
  console.log(pc.dim("No remote data sources found."));
@@ -1662,7 +1662,7 @@ async function removeRemote(name) {
1662
1662
  process.exit(1);
1663
1663
  }
1664
1664
  try {
1665
- const { del } = await import("./api-B7cdKn9j.mjs");
1665
+ const { del } = await import("./api-DZ1Xj0_d.mjs");
1666
1666
  await del(`/api/datasources/${encodeURIComponent(name)}`);
1667
1667
  console.log(pc.green(`✓ Removed "${name}" from remote server`));
1668
1668
  } catch (err) {
@@ -1825,7 +1825,7 @@ async function deployCommand(options = {}) {
1825
1825
  async function testAndSyncDatasources(cwd, options = {}) {
1826
1826
  const { extractDatasourcesFromCubes } = await import("./cubes-BvtwNBUG.mjs");
1827
1827
  const { loadLocalDatasources } = await import("./local-BkK5XL7T.mjs");
1828
- const { pushDatasource } = await import("./push-BOkUmRL8.mjs");
1828
+ const { pushDatasource } = await import("./push-BRq4Yy0V.mjs");
1829
1829
  const references = extractDatasourcesFromCubes(cwd);
1830
1830
  if (references.length === 0) return false;
1831
1831
  console.log();
@@ -4093,7 +4093,7 @@ async function dashboardListCommand() {
4093
4093
 
4094
4094
  //#endregion
4095
4095
  //#region src/commands/dashboard/remove.ts
4096
- async function confirm(message) {
4096
+ async function confirm$1(message) {
4097
4097
  const rl = createInterface({
4098
4098
  input: process.stdin,
4099
4099
  output: process.stdout
@@ -4107,7 +4107,7 @@ async function confirm(message) {
4107
4107
  }
4108
4108
  async function dashboardRemoveCommand(slug, options) {
4109
4109
  if (!options.force) {
4110
- if (!await confirm(`Remove dashboard "${slug}"? This cannot be undone.`)) {
4110
+ if (!await confirm$1(`Remove dashboard "${slug}"? This cannot be undone.`)) {
4111
4111
  console.log(pc.dim("Cancelled."));
4112
4112
  return;
4113
4113
  }
@@ -4148,6 +4148,26 @@ function loadViewer() {
4148
4148
  }
4149
4149
  return fs.readFileSync(viewerPath, "utf-8");
4150
4150
  }
4151
+ function loadThemeFile(filePath) {
4152
+ try {
4153
+ const parsed = parse(fs.readFileSync(filePath, "utf-8"));
4154
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
4155
+ console.error(pc.yellow(`Warning: Theme file is not a valid object, ignoring.`));
4156
+ return null;
4157
+ } catch (err) {
4158
+ console.error(pc.yellow(`Warning: Could not load theme file: ${err instanceof Error ? err.message : err}`));
4159
+ return null;
4160
+ }
4161
+ }
4162
+ async function fetchOrgTheme() {
4163
+ try {
4164
+ const result = await get("/api/org/theme");
4165
+ if (result.theme && typeof result.theme === "object") return result.theme;
4166
+ return null;
4167
+ } catch {
4168
+ return null;
4169
+ }
4170
+ }
4151
4171
  async function dashboardDevCommand(file, options) {
4152
4172
  const filePath = path.resolve(file);
4153
4173
  if (!fs.existsSync(filePath)) {
@@ -4161,6 +4181,24 @@ async function dashboardDevCommand(file, options) {
4161
4181
  }
4162
4182
  const viewerHtml = loadViewer();
4163
4183
  const sseClients = /* @__PURE__ */ new Set();
4184
+ let orgTheme = null;
4185
+ if (options.theme) {
4186
+ const themePath = path.resolve(options.theme);
4187
+ if (!fs.existsSync(themePath)) {
4188
+ console.error(pc.red(`Theme file not found: ${options.theme}`));
4189
+ process.exit(1);
4190
+ }
4191
+ orgTheme = loadThemeFile(themePath);
4192
+ if (orgTheme) console.log(pc.dim(`Using local theme from ${path.basename(themePath)}`));
4193
+ fs.watch(themePath, () => {
4194
+ orgTheme = loadThemeFile(themePath);
4195
+ for (const client of sseClients) client.write("data: reload\n\n");
4196
+ });
4197
+ } else {
4198
+ console.log(pc.dim("Fetching org theme..."));
4199
+ orgTheme = await fetchOrgTheme();
4200
+ if (orgTheme) console.log(pc.dim("Using org theme from Bonnard."));
4201
+ }
4164
4202
  let debounce = null;
4165
4203
  fs.watch(filePath, () => {
4166
4204
  if (debounce) clearTimeout(debounce);
@@ -4190,7 +4228,8 @@ async function dashboardDevCommand(file, options) {
4190
4228
  });
4191
4229
  res.end(JSON.stringify({
4192
4230
  token: creds.token,
4193
- baseUrl: APP_URL
4231
+ baseUrl: APP_URL,
4232
+ orgTheme
4194
4233
  }));
4195
4234
  return;
4196
4235
  }
@@ -4216,6 +4255,107 @@ async function dashboardDevCommand(file, options) {
4216
4255
  });
4217
4256
  }
4218
4257
 
4258
+ //#endregion
4259
+ //#region src/commands/theme/get.ts
4260
+ async function themeGetCommand() {
4261
+ try {
4262
+ const result = await get("/api/org/theme");
4263
+ if (!result.theme) {
4264
+ console.log(pc.dim("No org theme set. Using defaults."));
4265
+ return;
4266
+ }
4267
+ console.log(pc.bold("Organization theme:\n"));
4268
+ console.log(stringify(result.theme).trimEnd());
4269
+ } catch (err) {
4270
+ console.error(pc.red(`Error: ${err instanceof Error ? err.message : err}`));
4271
+ process.exit(1);
4272
+ }
4273
+ }
4274
+
4275
+ //#endregion
4276
+ //#region src/commands/theme/set.ts
4277
+ const VALID_TOP_LEVEL_KEYS = new Set([
4278
+ "palette",
4279
+ "chartHeight",
4280
+ "colors"
4281
+ ]);
4282
+ async function themeSetCommand(file, options) {
4283
+ const filePath = path.resolve(file);
4284
+ if (!fs.existsSync(filePath)) {
4285
+ console.error(pc.red(`File not found: ${file}`));
4286
+ process.exit(1);
4287
+ }
4288
+ let theme;
4289
+ try {
4290
+ theme = parse(fs.readFileSync(filePath, "utf-8"));
4291
+ } catch (err) {
4292
+ console.error(pc.red(`Failed to parse ${file}: ${err instanceof Error ? err.message : err}`));
4293
+ process.exit(1);
4294
+ }
4295
+ if (!theme || typeof theme !== "object" || Array.isArray(theme)) {
4296
+ console.error(pc.red("Theme file must contain a YAML/JSON object."));
4297
+ process.exit(1);
4298
+ }
4299
+ const unknownKeys = Object.keys(theme).filter((k) => !VALID_TOP_LEVEL_KEYS.has(k));
4300
+ if (unknownKeys.length > 0) {
4301
+ console.error(pc.red(`Unknown top-level keys: ${unknownKeys.join(", ")}\nAllowed: ${[...VALID_TOP_LEVEL_KEYS].join(", ")}`));
4302
+ process.exit(1);
4303
+ }
4304
+ if (options.dryRun) {
4305
+ console.log(pc.bold("Theme to be set (dry run):\n"));
4306
+ console.log(stringify(theme).trimEnd());
4307
+ return;
4308
+ }
4309
+ try {
4310
+ await put("/api/org/theme", { theme });
4311
+ console.log(pc.green("Organization theme updated.\n"));
4312
+ const summary = [];
4313
+ if (theme.palette) {
4314
+ const p = theme.palette;
4315
+ summary.push(` palette: ${typeof p === "string" ? p : `[${p.length} colors]`}`);
4316
+ }
4317
+ if (theme.chartHeight) summary.push(` chartHeight: ${theme.chartHeight}`);
4318
+ if (theme.colors) {
4319
+ const count = Object.keys(theme.colors).length;
4320
+ summary.push(` colors: ${count} override${count !== 1 ? "s" : ""}`);
4321
+ }
4322
+ if (summary.length > 0) console.log(summary.join("\n"));
4323
+ } catch (err) {
4324
+ console.error(pc.red(`Failed to set theme: ${err instanceof Error ? err.message : err}`));
4325
+ process.exit(1);
4326
+ }
4327
+ }
4328
+
4329
+ //#endregion
4330
+ //#region src/commands/theme/reset.ts
4331
+ async function confirm(message) {
4332
+ const rl = createInterface({
4333
+ input: process.stdin,
4334
+ output: process.stdout
4335
+ });
4336
+ return new Promise((resolve) => {
4337
+ rl.question(`${message} (y/N) `, (answer) => {
4338
+ rl.close();
4339
+ resolve(answer.toLowerCase() === "y");
4340
+ });
4341
+ });
4342
+ }
4343
+ async function themeResetCommand(options) {
4344
+ if (!options.force) {
4345
+ if (!await confirm("Reset org theme to defaults?")) {
4346
+ console.log(pc.dim("Cancelled."));
4347
+ return;
4348
+ }
4349
+ }
4350
+ try {
4351
+ await put("/api/org/theme", { theme: null });
4352
+ console.log(pc.green("Org theme reset to defaults."));
4353
+ } catch (err) {
4354
+ console.error(pc.red(`Failed to reset theme: ${err instanceof Error ? err.message : err}`));
4355
+ process.exit(1);
4356
+ }
4357
+ }
4358
+
4219
4359
  //#endregion
4220
4360
  //#region src/bin/bon.ts
4221
4361
  const { version } = createRequire(import.meta.url)("../../package.json");
@@ -4242,11 +4382,15 @@ keys.command("list").description("List all API keys for your organization").acti
4242
4382
  keys.command("create").description("Create a new API key").requiredOption("--name <name>", "Key name (e.g. 'Production SDK')").requiredOption("--type <type>", "Key type: publishable or secret").action(keysCreateCommand);
4243
4383
  keys.command("revoke").description("Revoke an API key by name or prefix").argument("<name-or-prefix>", "Key name or key prefix to revoke").action(keysRevokeCommand);
4244
4384
  const dashboard = program.command("dashboard").description("Manage hosted dashboards");
4245
- dashboard.command("dev").description("Preview a markdown dashboard locally with live reload").argument("<file>", "Path to dashboard .md file").option("--port <port>", "Server port (default: random available port)").action(dashboardDevCommand);
4385
+ dashboard.command("dev").description("Preview a markdown dashboard locally with live reload").argument("<file>", "Path to dashboard .md file").option("--port <port>", "Server port (default: random available port)").option("--theme <file>", "Path to a local theme YAML/JSON file").action(dashboardDevCommand);
4246
4386
  dashboard.command("deploy").description("Deploy an HTML or markdown file as a hosted dashboard").argument("<file>", "Path to HTML or markdown file").option("--slug <slug>", "Dashboard slug (defaults to filename)").option("--title <title>", "Dashboard title (defaults to <title> tag or slug)").action(dashboardDeployCommand);
4247
4387
  dashboard.command("list").description("List deployed dashboards").action(dashboardListCommand);
4248
4388
  dashboard.command("remove").description("Remove a deployed dashboard").argument("<slug>", "Dashboard slug to remove").option("--force", "Skip confirmation prompt").action(dashboardRemoveCommand);
4249
4389
  dashboard.command("open").description("Open a deployed dashboard in the browser").argument("<slug>", "Dashboard slug to open").action(dashboardOpenCommand);
4390
+ const theme = program.command("theme").description("Manage organization dashboard theme");
4391
+ theme.command("get").description("Show the current organization theme").action(themeGetCommand);
4392
+ theme.command("set").description("Set the organization theme from a YAML or JSON file").argument("<file>", "Path to theme YAML or JSON file").option("--dry-run", "Validate and preview without uploading").action(themeSetCommand);
4393
+ theme.command("reset").description("Reset the organization theme to defaults").option("--force", "Skip confirmation prompt").action(themeResetCommand);
4250
4394
  const metabase = program.command("metabase").description("Connect to and explore Metabase content");
4251
4395
  metabase.command("connect").description("Configure Metabase API connection").option("--url <url>", "Metabase instance URL").option("--api-key <key>", "Metabase API key").option("--force", "Overwrite existing configuration").action(metabaseConnectCommand);
4252
4396
  metabase.command("explore").description("Browse Metabase databases, collections, cards, and dashboards").argument("[resource]", "databases, collections, cards, dashboards, card, dashboard, database, table, collection").argument("[id]", "Resource ID (e.g. card <id>, dashboard <id>, database <id>, table <id>, collection <id>)").action(metabaseExploreCommand);
@@ -1,4 +1,4 @@
1
- import { r as post } from "./api-DqgY-30K.mjs";
1
+ import { r as post } from "./api-CirD2MoX.mjs";
2
2
  import { a as getLocalDatasource, c as resolveEnvVarsInCredentials } from "./local-ByvuW3eV.mjs";
3
3
  import pc from "picocolors";
4
4
  import "@inquirer/prompts";
@@ -55,6 +55,7 @@ description: Monthly trends # Optional
55
55
  |-------|----------|-------------|
56
56
  | `title` | Yes | Dashboard title displayed in the viewer and listings |
57
57
  | `description` | No | Short description shown in dashboard listings |
58
+ | `theme` | No | Theme overrides for this dashboard (palette, colors, chartHeight) |
58
59
 
59
60
  ## Local Preview
60
61
 
@@ -109,4 +110,5 @@ Dashboard queries respect the same governance policies as all other queries. Whe
109
110
  - [Queries](dashboards.queries) — query syntax and properties
110
111
  - [Components](dashboards.components) — chart and display components
111
112
  - [Inputs](dashboards.inputs) — interactive filters
113
+ - [Theming](dashboards.theming) — customize colors, palettes, and styles
112
114
  - [Examples](dashboards.examples) — complete dashboard examples
@@ -0,0 +1,152 @@
1
+ # Theming
2
+
3
+ > Customize dashboard colors, chart palettes, and visual styles at the org or dashboard level.
4
+
5
+ ## Overview
6
+
7
+ Dashboards support two levels of theming:
8
+
9
+ - **Organization theme** — applies to all dashboards in the org
10
+ - **Dashboard theme** — per-file override via frontmatter
11
+
12
+ The cascade order is: defaults → org theme → dashboard frontmatter theme. Each level deep-merges into the previous, so you only need to specify the properties you want to change.
13
+
14
+ ## Theme Properties
15
+
16
+ A theme object can include any combination of these properties:
17
+
18
+ ### `palette`
19
+
20
+ The color palette used for chart series. Can be a named palette or an array of hex colors.
21
+
22
+ **Named palettes:**
23
+
24
+ | Name | Colors | Best for |
25
+ |------|--------|----------|
26
+ | `tableau` | 10 balanced colors | General use (default) |
27
+ | `default` | 8 saturated Tailwind colors | Bold dashboards |
28
+ | `observable` | 10 modern vibrant colors | Data-heavy visualizations |
29
+ | `metabase` | 8 soft muted colors | Friendly reports |
30
+
31
+ **Custom palette:**
32
+
33
+ ```yaml
34
+ palette: ["#2563eb", "#dc2626", "#16a34a", "#ca8a04"]
35
+ ```
36
+
37
+ ### `chartHeight`
38
+
39
+ Height of chart components in pixels. Default: `320`.
40
+
41
+ ```yaml
42
+ chartHeight: 400
43
+ ```
44
+
45
+ ### `colors`
46
+
47
+ Override individual color tokens that control the dashboard UI. Only specify the tokens you want to change — unspecified tokens keep their defaults.
48
+
49
+ | Token | Description |
50
+ |-------|-------------|
51
+ | `bg` | Page background |
52
+ | `bgMuted` | Muted/secondary background |
53
+ | `bgCard` | Card background |
54
+ | `border` | Default border color |
55
+ | `text` | Primary text |
56
+ | `textMuted` | Secondary/muted text |
57
+ | `textTitle` | Heading text |
58
+ | `textLabel` | Label text (axes, legends) |
59
+ | `shadow` | Card shadow |
60
+ | `radius` | Border radius |
61
+ | `gridLine` | Chart grid lines |
62
+ | `legendText` | Chart legend text |
63
+ | `tooltip.bg` | Tooltip background |
64
+ | `tooltip.border` | Tooltip border |
65
+ | `tooltip.text` | Tooltip text |
66
+ | `tooltip.shadow` | Tooltip shadow |
67
+ | `table.headerBg` | Data table header background |
68
+ | `table.hoverBg` | Data table row hover |
69
+
70
+ ## Organization Theme
71
+
72
+ Set a theme that applies to all dashboards in your organization.
73
+
74
+ ```bash
75
+ # View current theme
76
+ bon theme get
77
+
78
+ # Set theme from a file
79
+ bon theme set theme.yml
80
+
81
+ # Validate without uploading
82
+ bon theme set theme.yml --dry-run
83
+
84
+ # Reset to defaults
85
+ bon theme reset
86
+ ```
87
+
88
+ ### Example `theme.yml`
89
+
90
+ ```yaml
91
+ palette: observable
92
+ chartHeight: 360
93
+ colors:
94
+ bgCard: "#1e293b"
95
+ border: "#334155"
96
+ gridLine: "#334155"
97
+ tooltip:
98
+ bg: "#1e293b"
99
+ border: "#475569"
100
+ ```
101
+
102
+ ### Example with custom palette
103
+
104
+ ```yaml
105
+ palette:
106
+ - "#2563eb"
107
+ - "#dc2626"
108
+ - "#16a34a"
109
+ - "#ca8a04"
110
+ - "#9333ea"
111
+ chartHeight: 300
112
+ ```
113
+
114
+ ## Dashboard Theme (Frontmatter Override)
115
+
116
+ Override the org theme for a specific dashboard by adding `theme` to the frontmatter:
117
+
118
+ ```yaml
119
+ ---
120
+ title: Revenue Dashboard
121
+ theme:
122
+ palette: metabase
123
+ colors:
124
+ bgCard: "#f0f9ff"
125
+ ---
126
+ ```
127
+
128
+ This overrides the org theme for this dashboard only. Properties not specified in the dashboard theme inherit from the org theme (or defaults if no org theme is set).
129
+
130
+ ### Combining org and dashboard themes
131
+
132
+ If your org theme uses the Observable palette and a dashboard specifies `palette: metabase`, that dashboard uses Metabase colors while keeping all other org theme settings (chartHeight, colors, etc.).
133
+
134
+ ## Local Preview
135
+
136
+ Preview dashboards with theming applied locally:
137
+
138
+ ```bash
139
+ # Uses deployed org theme (fetched from API)
140
+ bon dashboard dev revenue.md
141
+
142
+ # Uses a local theme file instead
143
+ bon dashboard dev revenue.md --theme theme.yml
144
+ ```
145
+
146
+ The `--theme` flag watches the theme file for changes — edit and save to see updates live.
147
+
148
+ ## See Also
149
+
150
+ - [Dashboards](dashboards) — format and deployment
151
+ - [Components](dashboards.components) — chart reference
152
+ - [Examples](dashboards.examples) — complete examples
@@ -119,6 +119,16 @@ Options:
119
119
  - `--slug <slug>` — custom URL slug (default: derived from filename)
120
120
  - `--title <title>` — override frontmatter title
121
121
 
122
+ ## Theming (Optional)
123
+
124
+ Customize colors and palettes:
125
+
126
+ - **Per-dashboard**: Add `theme:` to frontmatter (e.g. `theme: { palette: observable }`)
127
+ - **Org-wide**: Create a `theme.yml` and run `bon theme set theme.yml`
128
+ - **Preview locally**: `bon dashboard dev dashboard.md --theme theme.yml`
129
+
130
+ See `bon docs dashboards.theming` for palette names, color tokens, and examples.
131
+
122
132
  ## Phase 6: View Live
123
133
 
124
134
  Open the deployed dashboard in the browser:
@@ -118,6 +118,16 @@ Options:
118
118
  - `--slug <slug>` — custom URL slug (default: derived from filename)
119
119
  - `--title <title>` — override frontmatter title
120
120
 
121
+ ## Theming (Optional)
122
+
123
+ Customize colors and palettes:
124
+
125
+ - **Per-dashboard**: Add `theme:` to frontmatter (e.g. `theme: { palette: observable }`)
126
+ - **Org-wide**: Create a `theme.yml` and run `bon theme set theme.yml`
127
+ - **Preview locally**: `bon dashboard dev dashboard.md --theme theme.yml`
128
+
129
+ See `bon docs dashboards.theming` for palette names, color tokens, and examples.
130
+
121
131
  ## Phase 6: View Live
122
132
 
123
133
  Open the deployed dashboard in the browser:
@@ -78,6 +78,9 @@ All tables are in the `contoso` schema. The datasource is named `contoso_demo`.
78
78
  | `bon dashboard list` | List deployed dashboards with URLs |
79
79
  | `bon dashboard remove <slug>` | Remove a deployed dashboard |
80
80
  | `bon dashboard open <slug>` | Open a deployed dashboard in the browser |
81
+ | `bon theme get` | Show current org dashboard theme |
82
+ | `bon theme set <file>` | Set org theme from YAML/JSON file |
83
+ | `bon theme reset` | Reset org theme to defaults |
81
84
  | `bon metabase connect` | Connect to a Metabase instance (API key) |
82
85
  | `bon metabase analyze` | Generate analysis report for semantic layer planning |
83
86
  | `bon metabase explore` | Browse Metabase databases, collections, cards, dashboards |