@bridgeline-digital/hawk-app-cli 0.0.0-stage → 0.1.0-dev.20261020202308
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/LICENSE +7 -0
- package/README.md +770 -3
- package/agents/AGENTS.md +192 -0
- package/agents/project/custom.md +21 -0
- package/agents/project/next-mui.md +21 -0
- package/agents/project/next.md +24 -0
- package/agents/project/vanilla.md +22 -0
- package/agents/project/vite.md +19 -0
- package/bin/hawk-app.mjs +10 -0
- package/docs/bridge/README.md +1315 -0
- package/docs/showcase/README.md +47 -0
- package/docs/showcase/functions/echo.ts +12 -0
- package/docs/showcase/functions/long-task.ts +31 -0
- package/docs/showcase/functions/showcase.ts +186 -0
- package/docs/showcase/hawk-app.json +73 -0
- package/docs/showcase/queries/assistant-prompts-by-day.json +6 -0
- package/docs/showcase/queries/assistant-prompts-by-day.sql +8 -0
- package/docs/showcase/queries/searches-by-day.json +6 -0
- package/docs/showcase/queries/searches-by-day.sql +7 -0
- package/docs/showcase/src/App.tsx +1436 -0
- package/docs/showcase/src/main.tsx +10 -0
- package/docs/showcase/src/styles.css +71 -0
- package/docs/showcase/src/useHawkApp.ts +44 -0
- package/docs/showcase/src/vite-env.d.ts +1 -0
- package/docs/ui/README.md +166 -0
- package/package.json +50 -4
- package/schema/hawk-app.schema.json +611 -0
- package/src/accessibility.mjs +105 -0
- package/src/agents.mjs +145 -0
- package/src/aliases.mjs +30 -0
- package/src/app-icon.mjs +82 -0
- package/src/channel.mjs +116 -0
- package/src/cli.mjs +825 -0
- package/src/clients.mjs +221 -0
- package/src/config.mjs +148 -0
- package/src/create.mjs +186 -0
- package/src/credentials.mjs +56 -0
- package/src/data.mjs +169 -0
- package/src/deploy.mjs +349 -0
- package/src/dev.mjs +459 -0
- package/src/doctor.mjs +176 -0
- package/src/env.mjs +140 -0
- package/src/functions.mjs +313 -0
- package/src/login.mjs +166 -0
- package/src/logs.mjs +64 -0
- package/src/manifest.mjs +425 -0
- package/src/mcp-docs.mjs +168 -0
- package/src/mcp-hawksearch.mjs +342 -0
- package/src/mcp-reports.mjs +130 -0
- package/src/mcp-rules.mjs +109 -0
- package/src/mcp-ui.mjs +73 -0
- package/src/mcp.mjs +539 -0
- package/src/platform.mjs +116 -0
- package/src/project-checks.mjs +153 -0
- package/src/queries.mjs +289 -0
- package/src/scope-check.mjs +91 -0
- package/src/secrets.mjs +86 -0
- package/src/slug-guard.mjs +80 -0
- package/src/sso.mjs +182 -0
- package/src/store-listing.mjs +106 -0
- package/src/telemetry.mjs +50 -0
- package/src/template-history.json +34 -0
- package/src/upgrade.mjs +406 -0
- package/src/versions.mjs +102 -0
- package/templates/next/next.config.ts +13 -0
- package/templates/next/package.json +26 -0
- package/templates/next/src/app/globals.css +5 -0
- package/templates/next/src/app/layout.tsx +14 -0
- package/templates/next/src/app/page.tsx +86 -0
- package/templates/next/src/hawk-elements.d.ts +10 -0
- package/templates/next/src/lib/useHawkApp.ts +46 -0
- package/templates/next/tsconfig.json +21 -0
- package/templates/next-mui/next.config.ts +13 -0
- package/templates/next-mui/package.json +31 -0
- package/templates/next-mui/src/app/layout.tsx +20 -0
- package/templates/next-mui/src/app/page.tsx +79 -0
- package/templates/next-mui/src/app/providers.tsx +11 -0
- package/templates/next-mui/tsconfig.json +21 -0
- package/templates/preact/index.html +12 -0
- package/templates/preact/package.json +26 -0
- package/templates/preact/src/App.tsx +83 -0
- package/templates/preact/src/hawk-elements.d.ts +10 -0
- package/templates/preact/src/main.tsx +8 -0
- package/templates/preact/src/styles.css +5 -0
- package/templates/preact/src/useHawkApp.ts +44 -0
- package/templates/preact/src/vite-env.d.ts +1 -0
- package/templates/preact/tsconfig.json +17 -0
- package/templates/preact/vite.config.ts +11 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +29 -0
- package/templates/react/src/App.tsx +83 -0
- package/templates/react/src/hawk-elements.d.ts +10 -0
- package/templates/react/src/main.tsx +13 -0
- package/templates/react/src/styles.css +5 -0
- package/templates/react/src/useHawkApp.ts +44 -0
- package/templates/react/src/vite-env.d.ts +1 -0
- package/templates/react/tsconfig.json +16 -0
- package/templates/react/vite.config.ts +11 -0
- package/templates/vanilla/package.json +15 -0
- package/templates/vanilla/public/app.js +59 -0
- package/templates/vanilla/public/index.html +37 -0
- package/templates/vanilla/public/styles.css +5 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { HawkApiError, type AnalyticsReportRow } from '@bridgeline-digital/hawk-app-bridge/client';
|
|
2
|
+
import { createFormat } from '@bridgeline-digital/hawk-app-ui/elements';
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useHawkApp } from './useHawkApp';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starter screen: the context from the host and one real data call, shown with the UI kit (Spark's look: the kit's
|
|
8
|
+
* stylesheet and its elements, <hawk-kpi> and <hawk-column-chart>). Replace it with your app. Data comes only through
|
|
9
|
+
* the bridge: no API keys in the app.
|
|
10
|
+
*/
|
|
11
|
+
export function App() {
|
|
12
|
+
const hawk = useHawkApp();
|
|
13
|
+
const [rows, setRows] = useState<AnalyticsReportRow<'searches'>[] | null>(null);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
const app = hawk.status === 'ready' ? hawk.app : undefined;
|
|
17
|
+
const context = hawk.status === 'ready' ? hawk.context : undefined;
|
|
18
|
+
const format = useMemo(() => createFormat(context?.locale), [context?.locale]);
|
|
19
|
+
|
|
20
|
+
// Reload whenever the user changes the date range or engine in the host.
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!app || !context) return;
|
|
23
|
+
let current = true;
|
|
24
|
+
setRows(null);
|
|
25
|
+
setError(null);
|
|
26
|
+
const filters = context.engine ? [{ member: 'engine_id' as const, operator: 'equals', values: [context.engine.id] }] : undefined;
|
|
27
|
+
app.analytics
|
|
28
|
+
.report('searches', { filters, limit: 1000 }) // dates default to the host's date range
|
|
29
|
+
.then((report) => current && setRows(report.data))
|
|
30
|
+
.catch((e: unknown) => current && setError(e instanceof HawkApiError ? `${e.message}${e.code ? ` (${e.code})` : ''}` : String(e)));
|
|
31
|
+
return () => {
|
|
32
|
+
current = false;
|
|
33
|
+
};
|
|
34
|
+
}, [app, context?.dateRange?.from, context?.dateRange?.to, context?.engine?.id]);
|
|
35
|
+
|
|
36
|
+
// One column per day (a day can have a row per engine).
|
|
37
|
+
const days = useMemo(() => {
|
|
38
|
+
const byDay = new Map<string, number>();
|
|
39
|
+
for (const row of rows ?? []) byDay.set(row.search_date, (byDay.get(row.search_date) ?? 0) + row.search_count);
|
|
40
|
+
return [...byDay].sort(([a], [b]) => a.localeCompare(b)).map(([date, value]) => ({ label: format.date(date, { year: false }), value }));
|
|
41
|
+
}, [rows, format]);
|
|
42
|
+
|
|
43
|
+
if (hawk.status === 'connecting') return <span className="hawk-spinner" aria-label="Connecting" />;
|
|
44
|
+
if (hawk.status === 'error') return <div className="hawk-notice is-error">{hawk.message}</div>;
|
|
45
|
+
|
|
46
|
+
const total = days.reduce((sum, d) => sum + d.value, 0);
|
|
47
|
+
const { tenant, engine, dateRange } = hawk.context;
|
|
48
|
+
return (
|
|
49
|
+
<main className="hawk-stack">
|
|
50
|
+
<div>
|
|
51
|
+
<h1>__NAME__</h1>
|
|
52
|
+
<p className="hawk-muted">
|
|
53
|
+
Client {tenant.clientId}
|
|
54
|
+
{engine ? ` · ${engine.name ?? `Engine ${engine.id}`}` : ''}
|
|
55
|
+
{dateRange ? ` · ${format.date(dateRange.from)} – ${format.date(dateRange.to)}` : ''}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{error && <div className="hawk-notice is-error">{error}</div>}
|
|
60
|
+
|
|
61
|
+
<div className="hawk-kpi-row">
|
|
62
|
+
<hawk-kpi label="Searches" value={rows ? format.integer(total) : ''} loading={rows ? undefined : ''} highlight="" />
|
|
63
|
+
<hawk-kpi label="Searches per day" value={rows ? format.integer(days.length ? total / days.length : 0) : ''} loading={rows ? undefined : ''} hint={rows ? `over ${days.length} days` : undefined} />
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<section className="hawk-section">
|
|
67
|
+
<div className="hawk-section-header">
|
|
68
|
+
<div>
|
|
69
|
+
<h2 className="hawk-section-title">Searches per day</h2>
|
|
70
|
+
<p className="hawk-section-subtitle">In the host's date range. Hover a column for its number.</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
{rows ? <hawk-column-chart label="Searches per day" value-format="integer" columns={days} /> : <span className="hawk-spinner" />}
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<div>
|
|
77
|
+
<button className="hawk-button" onClick={() => hawk.app.toast('Hello from __NAME__!', 'success')}>
|
|
78
|
+
Say hello
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</main>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The UI kit's custom elements in JSX (<hawk-kpi>, <hawk-data-table>, <hawk-bar-list>, <hawk-column-chart>).
|
|
2
|
+
import type { HawkElementTags } from '@bridgeline-digital/hawk-app-ui/elements';
|
|
3
|
+
|
|
4
|
+
declare module 'react' {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
6
|
+
namespace JSX {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
8
|
+
interface IntrinsicElements extends HawkElementTags {}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
// The UI kit: Spark's look (stylesheet, with Montserrat) and its elements (<hawk-kpi>, <hawk-data-table>, …).
|
|
4
|
+
import '@bridgeline-digital/hawk-app-ui/css';
|
|
5
|
+
import '@bridgeline-digital/hawk-app-ui/elements';
|
|
6
|
+
import { App } from './App';
|
|
7
|
+
import './styles.css';
|
|
8
|
+
|
|
9
|
+
createRoot(document.getElementById('root')!).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<App />
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Your app's own styles. Spark's look comes from the UI kit's stylesheet (imported in main.tsx): its hawk-* classes
|
|
3
|
+
* (hawk-section, hawk-button, hawk-notice, hawk-table, …) and --hawk-ui-* variables, which follow the host's colors
|
|
4
|
+
* and light or dark mode. Use those before adding your own.
|
|
5
|
+
*/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createApp, type HawkApp, type HawkAppContext } from '@bridgeline-digital/hawk-app-bridge/client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Shown when the app is opened directly instead of inside a host. */
|
|
5
|
+
function notEmbeddedMessage(): string {
|
|
6
|
+
return `Open this app from the Apps page in HawkSearch. Developing locally? Add ?devUrl=${window.location.href} to the app's address in HawkSearch.`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type HawkAppState =
|
|
10
|
+
| { status: 'connecting' }
|
|
11
|
+
| { status: 'ready'; app: HawkApp; context: HawkAppContext }
|
|
12
|
+
| { status: 'error'; message: string };
|
|
13
|
+
|
|
14
|
+
/** Connects to the host (the HawkSearch dashboard / Spark) once and keeps the context (client, engine, dates, theme) current. */
|
|
15
|
+
export function useHawkApp(): HawkAppState {
|
|
16
|
+
const [state, setState] = useState<HawkAppState>({ status: 'connecting' });
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let app: HawkApp | undefined;
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
|
|
22
|
+
createApp({ autoResize: true })
|
|
23
|
+
.then((connected) => {
|
|
24
|
+
if (cancelled) return connected.destroy();
|
|
25
|
+
app = connected;
|
|
26
|
+
setState({ status: 'ready', app: connected, context: connected.context });
|
|
27
|
+
connected.onContextChange((context) => setState({ status: 'ready', app: connected, context }));
|
|
28
|
+
})
|
|
29
|
+
.catch((error: { code?: string; message?: string }) => {
|
|
30
|
+
if (cancelled) return;
|
|
31
|
+
setState({
|
|
32
|
+
status: 'error',
|
|
33
|
+
message: error.code === 'not_embedded' ? notEmbeddedMessage() : `Could not connect to the host: ${error.message}`,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
app?.destroy();
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"types": ["vite/client", "node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src", "vite.config.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
// Each deploy is served from the apps CDN at /<slug>/v<n>/, so every asset URL needs that prefix.
|
|
5
|
+
// `hawk-app deploy` builds with HAWK_APP_BASE_PATH=/<slug>/v<n>; local dev uses /<slug>/.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
base: `${process.env.HAWK_APP_BASE_PATH ?? '/__SLUG__'}/`,
|
|
8
|
+
plugins: [react()],
|
|
9
|
+
// Port 3100: the dev platform accepts data calls from localhost:3100-3103 (HawkSearch dev mode, ?devUrl=).
|
|
10
|
+
server: { port: 3100, strictPort: true },
|
|
11
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__SLUG__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "serve public -l 3100",
|
|
7
|
+
"app:dev": "hawk-app dev",
|
|
8
|
+
"app:deploy": "hawk-app deploy",
|
|
9
|
+
"app:audit": "npm audit --omit=dev --audit-level=high"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@bridgeline-digital/hawk-app-cli": "^__CLI_VERSION__",
|
|
13
|
+
"serve": "^14.2.4"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// No build step: the bridge and the UI kit's elements are loaded straight from the CDN (the kit's stylesheet is in
|
|
2
|
+
// index.html). Data comes only through the bridge (no API keys).
|
|
3
|
+
import { createApp } from 'https://cdn.jsdelivr.net/npm/@bridgeline-digital/hawk-app-bridge@0.1/dist/client/index.js';
|
|
4
|
+
import { createFormat } from 'https://cdn.jsdelivr.net/npm/@bridgeline-digital/hawk-app-ui@0.1/dist/elements/index.js';
|
|
5
|
+
|
|
6
|
+
const $ = (id) => document.getElementById(id);
|
|
7
|
+
|
|
8
|
+
async function loadSearches(app) {
|
|
9
|
+
const { engine, locale } = app.context;
|
|
10
|
+
const format = createFormat(locale);
|
|
11
|
+
$('total').setAttribute('loading', '');
|
|
12
|
+
$('per-day').setAttribute('loading', '');
|
|
13
|
+
$('error').hidden = true;
|
|
14
|
+
try {
|
|
15
|
+
const filters = engine ? [{ member: 'engine_id', operator: 'equals', values: [engine.id] }] : undefined;
|
|
16
|
+
const report = await app.analytics.report('searches', { filters, limit: 1000 }); // dates default to the host's range
|
|
17
|
+
// One column per day (a day can have a row per engine).
|
|
18
|
+
const byDay = new Map();
|
|
19
|
+
for (const row of report.data) byDay.set(row.search_date, (byDay.get(row.search_date) ?? 0) + row.search_count);
|
|
20
|
+
const days = [...byDay].sort(([a], [b]) => a.localeCompare(b)).map(([date, value]) => ({ label: format.date(date, { year: false }), value }));
|
|
21
|
+
const total = days.reduce((sum, d) => sum + d.value, 0);
|
|
22
|
+
|
|
23
|
+
$('total').setAttribute('value', format.integer(total));
|
|
24
|
+
$('per-day').setAttribute('value', format.integer(days.length ? total / days.length : 0));
|
|
25
|
+
$('per-day').setAttribute('hint', `over ${days.length} days`);
|
|
26
|
+
$('chart').columns = days;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
$('error').textContent = `${error.message}${error.code ? ` (${error.code})` : ''}`;
|
|
29
|
+
$('error').hidden = false;
|
|
30
|
+
} finally {
|
|
31
|
+
$('total').removeAttribute('loading');
|
|
32
|
+
$('per-day').removeAttribute('loading');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function showContext({ tenant, engine, dateRange, locale }) {
|
|
37
|
+
const format = createFormat(locale);
|
|
38
|
+
$('context').textContent = [
|
|
39
|
+
`Client ${tenant.clientId}`,
|
|
40
|
+
engine && (engine.name ?? `Engine ${engine.id}`),
|
|
41
|
+
dateRange && `${format.date(dateRange.from)} – ${format.date(dateRange.to)}`,
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(' · ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const app = await createApp({ autoResize: true });
|
|
49
|
+
showContext(app.context);
|
|
50
|
+
loadSearches(app);
|
|
51
|
+
app.onContextChange((context) => {
|
|
52
|
+
showContext(context);
|
|
53
|
+
loadSearches(app);
|
|
54
|
+
});
|
|
55
|
+
$('hello').onclick = () => app.toast('Hello from __NAME__!', 'success');
|
|
56
|
+
} catch (error) {
|
|
57
|
+
$('context').textContent = error.code === 'not_embedded' ? `Open this app from the Apps page in HawkSearch. Developing locally? Add ?devUrl=${location.href} to the app's address in HawkSearch.` : `Could not connect: ${error.message}`;
|
|
58
|
+
$('context').className = 'hawk-error-text';
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__NAME__</title>
|
|
7
|
+
<!-- The UI kit: Spark's look (with Montserrat), in the host's colors and mode. Same version as the bridge. -->
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@bridgeline-digital/hawk-app-ui@0.1/dist/css/hawk.css" />
|
|
9
|
+
<link rel="stylesheet" href="styles.css" />
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<main class="hawk-stack">
|
|
13
|
+
<div>
|
|
14
|
+
<h1>__NAME__</h1>
|
|
15
|
+
<p id="context" class="hawk-muted">Connecting…</p>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="error" class="hawk-notice is-error" hidden></div>
|
|
18
|
+
<div class="hawk-kpi-row">
|
|
19
|
+
<hawk-kpi id="total" label="Searches" highlight loading></hawk-kpi>
|
|
20
|
+
<hawk-kpi id="per-day" label="Searches per day" loading></hawk-kpi>
|
|
21
|
+
</div>
|
|
22
|
+
<section class="hawk-section">
|
|
23
|
+
<div class="hawk-section-header">
|
|
24
|
+
<div>
|
|
25
|
+
<h2 class="hawk-section-title">Searches per day</h2>
|
|
26
|
+
<p class="hawk-section-subtitle">In the host's date range. Hover a column for its number.</p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<hawk-column-chart id="chart" label="Searches per day" value-format="integer"></hawk-column-chart>
|
|
30
|
+
</section>
|
|
31
|
+
<div>
|
|
32
|
+
<button id="hello" class="hawk-button">Say hello</button>
|
|
33
|
+
</div>
|
|
34
|
+
</main>
|
|
35
|
+
<script type="module" src="app.js"></script>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Your app's own styles. Spark's look comes from the UI kit's stylesheet (linked in index.html): its hawk-* classes
|
|
3
|
+
* (hawk-section, hawk-button, hawk-notice, hawk-table, …) and --hawk-ui-* variables, which follow the host's colors
|
|
4
|
+
* and light or dark mode. Use those before adding your own.
|
|
5
|
+
*/
|