@bonnard/cli 0.2.15 → 0.2.16

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.
package/README.md CHANGED
@@ -98,7 +98,7 @@ Agent support:
98
98
  Set up your MCP server so agents can query your semantic layer directly:
99
99
 
100
100
  ```bash
101
- bon mcp setup # Configure MCP server
101
+ bon mcp # Show MCP server setup instructions
102
102
  bon mcp test # Verify the connection
103
103
  ```
104
104
 
@@ -172,7 +172,7 @@ Handles automatic datasource synchronisation and skips interactive prompts. Fits
172
172
  | `bon diff` | Preview changes before deploying |
173
173
  | `bon annotate` | Add metadata and descriptions to models |
174
174
  | `bon query` | Run queries from the terminal (JSON or SQL) |
175
- | `bon mcp setup` | Configure MCP server for agent access |
175
+ | `bon mcp` | Show MCP server setup instructions |
176
176
  | `bon mcp test` | Test MCP connection |
177
177
  | `bon docs` | Browse or search documentation from the CLI |
178
178
  | `bon login` / `bon logout` | Manage authentication |
package/dist/bin/bon.mjs CHANGED
@@ -13,6 +13,8 @@ import YAML from "yaml";
13
13
  import http from "node:http";
14
14
  import crypto from "node:crypto";
15
15
  import { encode } from "@toon-format/toon";
16
+ import { createInterface } from "node:readline";
17
+ import open from "open";
16
18
 
17
19
  //#region \0rolldown/runtime.js
18
20
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -611,10 +613,12 @@ function createAgentTemplates(cwd, env) {
611
613
  fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-get-started"), { recursive: true });
612
614
  fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
613
615
  fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-design-guide"), { recursive: true });
616
+ fs.mkdirSync(path.join(claudeSkillsDir, "bonnard-build-dashboard"), { recursive: true });
614
617
  writeTemplateFile(sharedBonnard, path.join(claudeRulesDir, "bonnard.md"), createdFiles);
615
618
  writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(claudeSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
616
619
  writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(claudeSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
617
620
  writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(claudeSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
621
+ writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(claudeSkillsDir, "bonnard-build-dashboard", "SKILL.md"), createdFiles);
618
622
  mergeSettingsJson(loadJsonTemplate("claude/settings.json"), path.join(cwd, ".claude", "settings.json"), createdFiles);
619
623
  const cursorRulesDir = path.join(cwd, ".cursor", "rules");
620
624
  fs.mkdirSync(cursorRulesDir, { recursive: true });
@@ -622,14 +626,17 @@ function createAgentTemplates(cwd, env) {
622
626
  writeTemplateFile(loadTemplate("cursor/rules/bonnard-get-started.mdc"), path.join(cursorRulesDir, "bonnard-get-started.mdc"), createdFiles);
623
627
  writeTemplateFile(loadTemplate("cursor/rules/bonnard-metabase-migrate.mdc"), path.join(cursorRulesDir, "bonnard-metabase-migrate.mdc"), createdFiles);
624
628
  writeTemplateFile(loadTemplate("cursor/rules/bonnard-design-guide.mdc"), path.join(cursorRulesDir, "bonnard-design-guide.mdc"), createdFiles);
629
+ writeTemplateFile(loadTemplate("cursor/rules/bonnard-build-dashboard.mdc"), path.join(cursorRulesDir, "bonnard-build-dashboard.mdc"), createdFiles);
625
630
  const codexSkillsDir = path.join(cwd, ".agents", "skills");
626
631
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-get-started"), { recursive: true });
627
632
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-metabase-migrate"), { recursive: true });
628
633
  fs.mkdirSync(path.join(codexSkillsDir, "bonnard-design-guide"), { recursive: true });
634
+ fs.mkdirSync(path.join(codexSkillsDir, "bonnard-build-dashboard"), { recursive: true });
629
635
  writeTemplateFile(sharedBonnard, path.join(cwd, "AGENTS.md"), createdFiles);
630
636
  writeTemplateFile(loadTemplate("claude/skills/bonnard-get-started/SKILL.md"), path.join(codexSkillsDir, "bonnard-get-started", "SKILL.md"), createdFiles);
631
637
  writeTemplateFile(loadTemplate("claude/skills/bonnard-metabase-migrate/SKILL.md"), path.join(codexSkillsDir, "bonnard-metabase-migrate", "SKILL.md"), createdFiles);
632
638
  writeTemplateFile(loadTemplate("claude/skills/bonnard-design-guide/SKILL.md"), path.join(codexSkillsDir, "bonnard-design-guide", "SKILL.md"), createdFiles);
639
+ writeTemplateFile(loadTemplate("claude/skills/bonnard-build-dashboard/SKILL.md"), path.join(codexSkillsDir, "bonnard-build-dashboard", "SKILL.md"), createdFiles);
633
640
  return createdFiles;
634
641
  }
635
642
  async function initCommand() {
@@ -670,12 +677,12 @@ async function initCommand() {
670
677
 
671
678
  //#endregion
672
679
  //#region src/commands/login.ts
673
- const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
680
+ const APP_URL$3 = process.env.BON_APP_URL || "https://app.bonnard.dev";
674
681
  const TIMEOUT_MS = 120 * 1e3;
675
682
  async function loginCommand() {
676
683
  const state = crypto.randomUUID();
677
684
  const { port, close } = await startCallbackServer(state);
678
- const url = `${APP_URL}/auth/device?state=${state}&port=${port}`;
685
+ const url = `${APP_URL$3}/auth/device?state=${state}&port=${port}`;
679
686
  console.log(pc.dim(`Opening browser to ${url}`));
680
687
  const open = (await import("open")).default;
681
688
  await open(url);
@@ -3870,6 +3877,140 @@ async function keysRevokeCommand(nameOrPrefix) {
3870
3877
  }
3871
3878
  }
3872
3879
 
3880
+ //#endregion
3881
+ //#region src/commands/dashboard/deploy.ts
3882
+ const APP_URL$2 = process.env.BON_APP_URL || "https://app.bonnard.dev";
3883
+ /**
3884
+ * Extract <title> content from HTML string
3885
+ */
3886
+ function extractTitle(html) {
3887
+ const match = html.match(/<title[^>]*>(.*?)<\/title>/is);
3888
+ return match ? match[1].trim() : null;
3889
+ }
3890
+ /**
3891
+ * Derive slug from filename: strip extension, lowercase, replace non-alphanumeric with hyphens
3892
+ */
3893
+ function slugFromFilename(filename) {
3894
+ return path.basename(filename, path.extname(filename)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3895
+ }
3896
+ async function dashboardDeployCommand(file, options) {
3897
+ const filePath = path.resolve(file);
3898
+ if (!fs.existsSync(filePath)) {
3899
+ console.error(pc.red(`File not found: ${file}`));
3900
+ process.exit(1);
3901
+ }
3902
+ const content = fs.readFileSync(filePath, "utf-8");
3903
+ if (content.length > 2 * 1024 * 1024) {
3904
+ console.error(pc.red("File exceeds 2MB limit."));
3905
+ process.exit(1);
3906
+ }
3907
+ const slug = options.slug || slugFromFilename(file);
3908
+ const title = options.title || extractTitle(content) || slug;
3909
+ console.log(pc.dim(`Deploying dashboard "${slug}"...`));
3910
+ try {
3911
+ const dashboard = (await post("/api/dashboards", {
3912
+ slug,
3913
+ title,
3914
+ content
3915
+ })).dashboard;
3916
+ const url = `${APP_URL$2}/d/${dashboard.org_slug}/${dashboard.slug}`;
3917
+ console.log(pc.green(`Dashboard deployed successfully`));
3918
+ console.log();
3919
+ console.log(` ${pc.bold("Slug")} ${dashboard.slug}`);
3920
+ console.log(` ${pc.bold("Title")} ${dashboard.title}`);
3921
+ console.log(` ${pc.bold("Version")} ${dashboard.version}`);
3922
+ console.log(` ${pc.bold("URL")} ${url}`);
3923
+ } catch (err) {
3924
+ console.error(pc.red(`Deploy failed: ${err instanceof Error ? err.message : err}`));
3925
+ process.exit(1);
3926
+ }
3927
+ }
3928
+
3929
+ //#endregion
3930
+ //#region src/commands/dashboard/list.ts
3931
+ const APP_URL$1 = process.env.BON_APP_URL || "https://app.bonnard.dev";
3932
+ async function dashboardListCommand() {
3933
+ try {
3934
+ const dashboards = (await get("/api/dashboards")).dashboards;
3935
+ if (dashboards.length === 0) {
3936
+ console.log(pc.dim("No dashboards deployed yet. Run `bon dashboard deploy <file>` to publish."));
3937
+ return;
3938
+ }
3939
+ const slugWidth = Math.max(4, ...dashboards.map((d) => d.slug.length));
3940
+ const titleWidth = Math.max(5, ...dashboards.map((d) => d.title.length));
3941
+ const versionWidth = 7;
3942
+ const dateWidth = 10;
3943
+ const header = [
3944
+ "SLUG".padEnd(slugWidth),
3945
+ "TITLE".padEnd(titleWidth),
3946
+ "VERSION".padEnd(versionWidth),
3947
+ "UPDATED".padEnd(dateWidth),
3948
+ "URL"
3949
+ ].join(" ");
3950
+ console.log(pc.dim(header));
3951
+ for (const d of dashboards) {
3952
+ const url = `${APP_URL$1}/d/${d.org_slug}/${d.slug}`;
3953
+ const date = new Date(d.updated_at).toLocaleDateString();
3954
+ const row = [
3955
+ d.slug.padEnd(slugWidth),
3956
+ d.title.padEnd(titleWidth),
3957
+ `v${d.version}`.padEnd(versionWidth),
3958
+ date.padEnd(dateWidth),
3959
+ url
3960
+ ].join(" ");
3961
+ console.log(row);
3962
+ }
3963
+ } catch (err) {
3964
+ console.error(pc.red(`Failed to list dashboards: ${err instanceof Error ? err.message : err}`));
3965
+ process.exit(1);
3966
+ }
3967
+ }
3968
+
3969
+ //#endregion
3970
+ //#region src/commands/dashboard/remove.ts
3971
+ async function confirm(message) {
3972
+ const rl = createInterface({
3973
+ input: process.stdin,
3974
+ output: process.stdout
3975
+ });
3976
+ return new Promise((resolve) => {
3977
+ rl.question(`${message} (y/N) `, (answer) => {
3978
+ rl.close();
3979
+ resolve(answer.toLowerCase() === "y");
3980
+ });
3981
+ });
3982
+ }
3983
+ async function dashboardRemoveCommand(slug, options) {
3984
+ if (!options.force) {
3985
+ if (!await confirm(`Remove dashboard "${slug}"? This cannot be undone.`)) {
3986
+ console.log(pc.dim("Cancelled."));
3987
+ return;
3988
+ }
3989
+ }
3990
+ try {
3991
+ await del(`/api/dashboards/${encodeURIComponent(slug)}`);
3992
+ console.log(pc.green(`Dashboard "${slug}" removed.`));
3993
+ } catch (err) {
3994
+ console.error(pc.red(`Failed to remove dashboard: ${err instanceof Error ? err.message : err}`));
3995
+ process.exit(1);
3996
+ }
3997
+ }
3998
+
3999
+ //#endregion
4000
+ //#region src/commands/dashboard/open.ts
4001
+ const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
4002
+ async function dashboardOpenCommand(slug) {
4003
+ try {
4004
+ const result = await get(`/api/dashboards/${encodeURIComponent(slug)}`);
4005
+ const url = `${APP_URL}/d/${result.dashboard.org_slug}/${result.dashboard.slug}`;
4006
+ console.log(pc.dim(`Opening ${url}`));
4007
+ await open(url);
4008
+ } catch (err) {
4009
+ console.error(pc.red(`Failed to open dashboard: ${err instanceof Error ? err.message : err}`));
4010
+ process.exit(1);
4011
+ }
4012
+ }
4013
+
3873
4014
  //#endregion
3874
4015
  //#region src/bin/bon.ts
3875
4016
  const { version } = createRequire(import.meta.url)("../../package.json");
@@ -3895,6 +4036,11 @@ const keys = program.command("keys").description("Manage API keys for the Bonnar
3895
4036
  keys.command("list").description("List all API keys for your organization").action(keysListCommand);
3896
4037
  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);
3897
4038
  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);
4039
+ const dashboard = program.command("dashboard").description("Manage hosted HTML dashboards");
4040
+ dashboard.command("deploy").description("Deploy an HTML file as a hosted dashboard").argument("<file>", "Path to HTML file").option("--slug <slug>", "Dashboard slug (defaults to filename)").option("--title <title>", "Dashboard title (defaults to <title> tag or slug)").action(dashboardDeployCommand);
4041
+ dashboard.command("list").description("List deployed dashboards").action(dashboardListCommand);
4042
+ dashboard.command("remove").description("Remove a deployed dashboard").argument("<slug>", "Dashboard slug to remove").option("--force", "Skip confirmation prompt").action(dashboardRemoveCommand);
4043
+ dashboard.command("open").description("Open a deployed dashboard in the browser").argument("<slug>", "Dashboard slug to open").action(dashboardOpenCommand);
3898
4044
  const metabase = program.command("metabase").description("Connect to and explore Metabase content");
3899
4045
  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);
3900
4046
  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);
@@ -0,0 +1,281 @@
1
+ # ApexCharts + Bonnard SDK
2
+
3
+ > Build HTML dashboards with ApexCharts and the Bonnard SDK. No build step required.
4
+
5
+ ApexCharts has the best visual defaults out of the box — no configuration needed for tooltips, responsive behavior, or dark mode. SVG-based rendering produces sharp visuals at any resolution. Moderate payload (~130KB gzip).
6
+
7
+ ## Starter template
8
+
9
+ Copy this complete HTML file as a starting point. Replace `bon_pk_YOUR_KEY_HERE` with your publishable API key, and update the view/measure/dimension names to match your schema.
10
+
11
+ Use `explore()` to discover available views and fields — see [sdk.query-reference](sdk.query-reference).
12
+
13
+ ```html
14
+ <!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8">
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ <title>Dashboard</title>
20
+ <script src="https://cdn.jsdelivr.net/npm/apexcharts@3/dist/apexcharts.min.js"></script>
21
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
22
+ <style>
23
+ * { margin: 0; padding: 0; box-sizing: border-box; }
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
26
+ background: #09090b; color: #fafafa; padding: 24px;
27
+ }
28
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; }
29
+ .error { color: #ef4444; background: #1c0a0a; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; }
30
+ .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }
31
+ .kpi { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; }
32
+ .kpi-label { font-size: 14px; color: #a1a1aa; margin-bottom: 8px; }
33
+ .kpi-value { font-size: 32px; font-weight: 600; }
34
+ .kpi-value.loading { color: #3f3f46; }
35
+ .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; }
36
+ .chart-card { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; }
37
+ .chart-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; }
38
+ .chart-container { height: 300px; }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <h1>Dashboard</h1>
43
+ <div id="error" class="error"></div>
44
+
45
+ <div class="kpis">
46
+ <div class="kpi">
47
+ <div class="kpi-label">Revenue</div>
48
+ <div class="kpi-value loading" id="kpi-revenue">--</div>
49
+ </div>
50
+ <div class="kpi">
51
+ <div class="kpi-label">Orders</div>
52
+ <div class="kpi-value loading" id="kpi-orders">--</div>
53
+ </div>
54
+ <div class="kpi">
55
+ <div class="kpi-label">Avg Value</div>
56
+ <div class="kpi-value loading" id="kpi-avg">--</div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="charts">
61
+ <div class="chart-card">
62
+ <div class="chart-title">Revenue by City</div>
63
+ <div class="chart-container" id="bar-chart"></div>
64
+ </div>
65
+ <div class="chart-card">
66
+ <div class="chart-title">Revenue Trend</div>
67
+ <div class="chart-container" id="line-chart"></div>
68
+ </div>
69
+ </div>
70
+
71
+ <script>
72
+ const bon = Bonnard.createClient({
73
+ apiKey: 'bon_pk_YOUR_KEY_HERE',
74
+ });
75
+
76
+ // --- Helpers ---
77
+ function showError(msg) {
78
+ const el = document.getElementById('error');
79
+ el.textContent = msg;
80
+ el.style.display = 'block';
81
+ }
82
+
83
+ function formatNumber(v) {
84
+ return new Intl.NumberFormat().format(v);
85
+ }
86
+
87
+ function formatCurrency(v) {
88
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v);
89
+ }
90
+
91
+ // ApexCharts dark mode defaults
92
+ const darkTheme = {
93
+ chart: { background: 'transparent', foreColor: '#a1a1aa' },
94
+ theme: { mode: 'dark' },
95
+ grid: { borderColor: '#27272a' },
96
+ tooltip: { theme: 'dark' },
97
+ };
98
+
99
+ // --- Load data ---
100
+ (async () => {
101
+ try {
102
+ // KPIs
103
+ const kpis = await bon.query({
104
+ measures: ['orders.revenue', 'orders.count', 'orders.avg_value'],
105
+ });
106
+ if (kpis.data.length > 0) {
107
+ const row = kpis.data[0];
108
+ document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']);
109
+ document.getElementById('kpi-revenue').classList.remove('loading');
110
+ document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']);
111
+ document.getElementById('kpi-orders').classList.remove('loading');
112
+ document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']);
113
+ document.getElementById('kpi-avg').classList.remove('loading');
114
+ }
115
+
116
+ // Bar chart — revenue by city
117
+ const byCity = await bon.query({
118
+ measures: ['orders.revenue'],
119
+ dimensions: ['orders.city'],
120
+ orderBy: { 'orders.revenue': 'desc' },
121
+ limit: 10,
122
+ });
123
+
124
+ new ApexCharts(document.getElementById('bar-chart'), {
125
+ ...darkTheme,
126
+ chart: { ...darkTheme.chart, type: 'bar', height: 300 },
127
+ series: [{ name: 'Revenue', data: byCity.data.map(d => d['orders.revenue']) }],
128
+ xaxis: { categories: byCity.data.map(d => d['orders.city']) },
129
+ yaxis: { labels: { formatter: v => formatCurrency(v) } },
130
+ plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
131
+ colors: ['#3b82f6'],
132
+ dataLabels: { enabled: false },
133
+ }).render();
134
+
135
+ // Line chart — revenue trend
136
+ const trend = await bon.query({
137
+ measures: ['orders.revenue'],
138
+ timeDimension: {
139
+ dimension: 'orders.created_at',
140
+ granularity: 'month',
141
+ dateRange: 'last 12 months',
142
+ },
143
+ });
144
+
145
+ new ApexCharts(document.getElementById('line-chart'), {
146
+ ...darkTheme,
147
+ chart: { ...darkTheme.chart, type: 'area', height: 300 },
148
+ series: [{ name: 'Revenue', data: trend.data.map(d => d['orders.revenue']) }],
149
+ xaxis: {
150
+ categories: trend.data.map(d => {
151
+ const date = new Date(d['orders.created_at']);
152
+ return date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
153
+ }),
154
+ },
155
+ yaxis: { labels: { formatter: v => formatCurrency(v) } },
156
+ colors: ['#3b82f6'],
157
+ fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
158
+ stroke: { curve: 'smooth', width: 2 },
159
+ dataLabels: { enabled: false },
160
+ }).render();
161
+ } catch (err) {
162
+ showError('Failed to load dashboard: ' + err.message);
163
+ }
164
+ })();
165
+ </script>
166
+ </body>
167
+ </html>
168
+ ```
169
+
170
+ ## Chart types
171
+
172
+ ### Bar chart
173
+
174
+ ```javascript
175
+ new ApexCharts(el, {
176
+ chart: { type: 'bar', height: 300 },
177
+ series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }],
178
+ xaxis: { categories: data.map(d => d['view.dimension']) },
179
+ plotOptions: { bar: { borderRadius: 4 } },
180
+ colors: ['#3b82f6'],
181
+ }).render();
182
+ ```
183
+
184
+ ### Horizontal bar chart
185
+
186
+ ```javascript
187
+ new ApexCharts(el, {
188
+ chart: { type: 'bar', height: 300 },
189
+ series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }],
190
+ xaxis: { categories: data.map(d => d['view.dimension']) },
191
+ plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
192
+ }).render();
193
+ ```
194
+
195
+ ### Line chart
196
+
197
+ ```javascript
198
+ new ApexCharts(el, {
199
+ chart: { type: 'line', height: 300 },
200
+ series: [{ name: 'Revenue', data: values }],
201
+ xaxis: { categories: labels },
202
+ stroke: { curve: 'smooth', width: 2 },
203
+ }).render();
204
+ ```
205
+
206
+ ### Area chart
207
+
208
+ ```javascript
209
+ new ApexCharts(el, {
210
+ chart: { type: 'area', height: 300 },
211
+ series: [{ name: 'Revenue', data: values }],
212
+ xaxis: { categories: labels },
213
+ fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
214
+ stroke: { curve: 'smooth', width: 2 },
215
+ }).render();
216
+ ```
217
+
218
+ ### Pie / donut chart
219
+
220
+ ```javascript
221
+ new ApexCharts(el, {
222
+ chart: { type: 'donut', height: 300 }, // or 'pie'
223
+ series: data.map(d => d['view.measure']),
224
+ labels: data.map(d => d['view.dimension']),
225
+ colors: ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'],
226
+ }).render();
227
+ ```
228
+
229
+ ### Multi-series line chart
230
+
231
+ ```javascript
232
+ const cities = [...new Set(data.map(d => d['orders.city']))];
233
+ const dates = [...new Set(data.map(d => d['orders.created_at']))];
234
+
235
+ new ApexCharts(el, {
236
+ chart: { type: 'line', height: 300 },
237
+ series: cities.map(city => ({
238
+ name: city,
239
+ data: dates.map(date =>
240
+ data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0
241
+ ),
242
+ })),
243
+ xaxis: { categories: dates.map(d => new Date(d).toLocaleDateString()) },
244
+ stroke: { curve: 'smooth', width: 2 },
245
+ }).render();
246
+ ```
247
+
248
+ ## Dark mode
249
+
250
+ ApexCharts has built-in dark mode support:
251
+
252
+ ```javascript
253
+ const darkTheme = {
254
+ chart: { background: 'transparent', foreColor: '#a1a1aa' },
255
+ theme: { mode: 'dark' },
256
+ grid: { borderColor: '#27272a' },
257
+ tooltip: { theme: 'dark' },
258
+ };
259
+
260
+ new ApexCharts(el, {
261
+ ...darkTheme,
262
+ chart: { ...darkTheme.chart, type: 'bar', height: 300 },
263
+ // ... rest of config
264
+ }).render();
265
+ ```
266
+
267
+ ## Color palette
268
+
269
+ ```javascript
270
+ const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
271
+
272
+ // Apply globally:
273
+ colors: COLORS,
274
+ ```
275
+
276
+ ## See also
277
+
278
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
279
+ - [sdk.query-reference](sdk.query-reference) — Full query API
280
+ - [sdk.chartjs](sdk.chartjs) — Chart.js alternative (smallest payload)
281
+ - [sdk.echarts](sdk.echarts) — ECharts alternative (more chart types)
@@ -0,0 +1,130 @@
1
+ # Authentication
2
+
3
+ > How to authenticate SDK requests — publishable keys for public dashboards, token exchange for multi-tenant apps.
4
+
5
+ ## Publishable keys
6
+
7
+ Publishable keys (`bon_pk_...`) are safe to use in client-side code — HTML pages, browser apps, mobile apps. They grant read-only access to your org's semantic layer.
8
+
9
+ ```javascript
10
+ const bon = Bonnard.createClient({
11
+ apiKey: 'bon_pk_...',
12
+ });
13
+ ```
14
+
15
+ Create publishable keys in the Bonnard web app under **Settings > API Keys**.
16
+
17
+ **What publishable keys can do:**
18
+ - Query measures and dimensions
19
+ - Explore schema (views, fields)
20
+
21
+ **What they cannot do:**
22
+ - Modify data or schema
23
+ - Access other orgs' data
24
+ - Bypass governance policies (if configured at org level)
25
+
26
+ ## Token exchange (multi-tenant)
27
+
28
+ For B2B apps where each customer should only see their own data, use **secret key token exchange**. Your server exchanges a secret key for a short-lived JWT with a security context, then your frontend queries with that token.
29
+
30
+ ### Server-side: exchange secret key for scoped token
31
+
32
+ ```javascript
33
+ // Your backend (Node.js, Python, etc.)
34
+ const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, // bon_sk_...
38
+ 'Content-Type': 'application/json',
39
+ },
40
+ body: JSON.stringify({
41
+ security_context: {
42
+ tenant_id: currentCustomer.id, // your tenant identifier
43
+ },
44
+ }),
45
+ });
46
+
47
+ const { token } = await res.json();
48
+ // Pass this token to your frontend
49
+ ```
50
+
51
+ ### Client-side: query with scoped token
52
+
53
+ ```javascript
54
+ const bon = Bonnard.createClient({
55
+ fetchToken: async () => {
56
+ const res = await fetch('/my-backend/bonnard-token');
57
+ const { token } = await res.json();
58
+ return token;
59
+ },
60
+ });
61
+
62
+ const { data } = await bon.query({
63
+ measures: ['orders.revenue'],
64
+ dimensions: ['orders.status'],
65
+ });
66
+ // Only returns rows matching the tenant's security context
67
+ ```
68
+
69
+ ### How token refresh works
70
+
71
+ The SDK automatically:
72
+ 1. Calls `fetchToken()` on the first query
73
+ 2. Caches the returned JWT
74
+ 3. Parses the JWT `exp` claim
75
+ 4. Refreshes 60 seconds before expiry by calling `fetchToken()` again
76
+
77
+ You don't need to manage token lifecycle — just provide the `fetchToken` callback.
78
+
79
+ ### Security context and governance
80
+
81
+ The `security_context` object you pass during token exchange becomes available in your Cube models as `{securityContext.attrs.*}`. Use it in access policies to enforce row-level security:
82
+
83
+ ```yaml
84
+ # In your Cube view definition
85
+ access_policy:
86
+ - role: "*"
87
+ conditions:
88
+ - sql: "{TABLE}.tenant_id = '{securityContext.attrs.tenant_id}'"
89
+ ```
90
+
91
+ See [security-context](security-context) for the full governance setup guide.
92
+
93
+ ## Browser HTML with token exchange
94
+
95
+ For HTML dashboards that need multi-tenant auth, your page fetches a token from your backend:
96
+
97
+ ```html
98
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
99
+ <script>
100
+ const bon = Bonnard.createClient({
101
+ fetchToken: async () => {
102
+ const res = await fetch('/api/bonnard-token');
103
+ const { token } = await res.json();
104
+ return token;
105
+ },
106
+ });
107
+
108
+ (async () => {
109
+ const { data } = await bon.query({
110
+ measures: ['orders.revenue'],
111
+ });
112
+ // Data is scoped to the authenticated tenant
113
+ })();
114
+ </script>
115
+ ```
116
+
117
+ ## When to use which
118
+
119
+ | Scenario | Auth method | Key type |
120
+ |----------|------------|----------|
121
+ | Internal dashboard (your team) | Publishable key | `bon_pk_...` |
122
+ | Public dashboard (anyone can view) | Publishable key | `bon_pk_...` |
123
+ | Embedded analytics (customer sees their data only) | Token exchange | `bon_sk_...` → JWT |
124
+ | Server-side data pipeline | Secret key directly | `bon_sk_...` |
125
+
126
+ ## See also
127
+
128
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
129
+ - [sdk.query-reference](sdk.query-reference) — Full query API
130
+ - [security-context](security-context) — Row-level security setup