@bridgeline-digital/hawk-app-ui
The Spark look for Hawk Apps, in the host's colors and mode. For Bridgeline developers. See every part live, in light and dark: the UI kit gallery (on the developer site).
- React + MUI apps (the
next-muitemplate): Spark's MUI theme, the provider that connects the app to its host, and the components apps keep needing: sections, KPI tiles, tables, small charts, a confirm dialog, a side drawer, toasts and formatting. Requires the Hawk Apps bridge. - Any other app (the
react,preact,nextandvanillatemplates): a stylesheet and a few custom elements, no React or MUI needed. See Without React and MUI.
npm install @bridgeline-digital/hawk-app-ui @mui/material@^9 @emotion/react @emotion/styled # React + MUI
npm install @bridgeline-digital/hawk-app-ui # any other app
Use it
// app/layout.tsx (once): Montserrat, Spark's font, bundled with the app
import '@bridgeline-digital/hawk-app-ui/fonts';
// app/providers.tsx
'use client';
import { HawkAppProvider } from '@bridgeline-digital/hawk-app-ui';
export default function Providers({ children }: { children: React.ReactNode }) {
return <HawkAppProvider>{children}</HawkAppProvider>;
}
// any page or component inside it
import { useHawk } from '@bridgeline-digital/hawk-app-ui';
export function Page() {
const { app, context } = useHawk(); // the connected app and its context (client, engine, dates, theme)
// …app.data.query(...), app.analytics.report(...)
}
HawkAppProviderconnects to the host (createApp({ autoResize: true }), change it withoptions), shows a spinner meanwhile and an explanation when the app is opened outside a host (connectinganderrorreplace them), then renders its children in the theme. It follows the host when its theme changes. The spinner and the explanation are a<main>landmark, as the app's page should be, so the accessibility check finds one in every state.useHawk()returns{ app, context }, always connected (the provider renders children only then).useHawkAppState()is the hook underneath, for an app that wants to render the states itself.<HawkComments target label?>(thecommentsscope) is a comment icon with a "!" when there are comments (the count in its tooltip); a click opens the thread in a drawer, where everyone at the client reads and adds comments (authors edit and delete their own). The page's badges share one count request. See the bridge guide's "Comments".usePdfExport(options | false)lets the user export the page as a PDF while it's mounted: the host shows a PDF icon, and a click downloads the whole page.usePdfExport({ fileName: 'Zero results' }), orfalsewhile loading. Leave out buttons and filters withdata-hawk-pdf-ignore. See the bridge guide's "Export a page as a PDF".useSavedState(initial)isuseStatefor what makes a saved view (filters, range, tab, sort): the value is the page's state for the host's Views menu, and opening a saved view or its link sets it back.const [filters, setFilters] = useSavedState({ range: 'l13', target: null }). One small JSON object, once per page. See the bridge guide's "Saved views".useRoute()is the app's route for deep links and Back:const [route, setRoute] = useRoute(), show the page the route names, andsetRoute('/widgets/12')when the user opens one ({ replace: true }for no Back step). The host keeps it in its URL, so a reload or a link opens the same page. In a preview (no host), routes work within the page. See the bridge guide's "Deep links and Back".useScreen(name | null)counts a screen the routes don't show, such as a tab, each time it's shown:useScreen('Logs')in the tab's component. Routes and views are counted by the bridge, and so are clicks ondata-hawk-usage="Export CSV"elements. Seehawk-app usage --screensand the bridge guide's "Errors and usage".usePageContext(object | null)says what the page shows, for the host's Explain this page button (and its assistant):usePageContext(rows ? { page: 'Zero-result searches', dateRange, top10 } : null). Plain names and numbers, at most 50 KB; sent shortly after it changes, cleared when the component unmounts. See the bridge guide's "Say what the page shows".useTour()is for a "Take the tour" button:{ available, active, start }. The bridge shows the tour (tourin hawk-app.json, cards bydata-hawk-tourelements) on its own on first open;start()shows every step again,availableis true once the version has steps,activewhile it shows. Nothing in a preview. See the bridge guide's "Tour".- Your own theme additions:
<HawkAppProvider theme={{ components: { … } }}>, merged last.
Components
import { ConfirmDialog, DataTable, EmptyState, KpiRow, KpiTile, LoadingSection, Notice, Section, SideDrawer, useFormat, useToast } from '@bridgeline-digital/hawk-app-ui';
import { BarList, ColumnChart, Sparkline, StackedColumnChart } from '@bridgeline-digital/hawk-app-ui/charts';
import { DataGrid } from '@bridgeline-digital/hawk-app-ui/data-grid'; // needs @mui/x-data-grid@^9
const f = useFormat(); // in the host's locale
<KpiRow>
<KpiTile highlight label="Revenue" value={f.money(rev)} change={rev / prevRev - 1} trend={weekly} />
<KpiTile label="No-result searches" value={f.integer(zero)} change={zero / prevZero - 1} goodWhen="down" />
</KpiRow>
<Section title="Searches per day" subtitle="UTC days" action={<Button>Refresh</Button>}>
<ColumnChart label="Searches per day" columns={days.map((d) => ({ label: f.date(d.date, { year: false }), value: d.searches }))} />
</Section>
<DataTable rows={terms} columns={[{ key: 'term', label: 'Term' }, { key: 'searches', label: 'Searches', align: 'right' }]}
defaultSort={{ key: 'searches', direction: 'desc' }} totals={{ term: 'Total', searches: f.integer(total) }} download="terms.csv" />
| Component | For |
|---|---|
Section | A titled card (Spark's card header and padding), with an optional subtitle and action |
KpiTile, KpiRow | A number with its change vs. the previous period (green when good: goodWhen), an optional trend; tiles side by side |
DataTable | Up to a few hundred rows: sortable columns, totals row, sticky header (maxHeight), CSV download of what it shows (text that a spreadsheet would run as a formula, such as a search term starting with =, opens as text) |
DataGrid (/data-grid) | Thousands of rows, paging and filtering: MUI X's community grid with Spark's defaults |
BarList, ColumnChart, StackedColumnChart, Sparkline (/charts) | Small charts in the theme's colors, without a chart library; each has a label for screen readers and tooltips with the values |
EmptyState, LoadingSection, Notice | Nothing to show; a section still loading; a message (onRetry adds Try again) |
ConfirmDialog | Asks before an action; destructive makes it red; a promise from onConfirm keeps it open with a spinner |
SideDrawer | Details of one item, from the right, with a title, close button and actions |
useToast() | toast(message, severity): the host's toast in the full app, a snackbar in a view |
useFormat() / createFormat(locale) | integer, number, compact, percent, change (+12.3%), money, date (a "2026-10-03" date stays that day), dateTime: dates from a Date or an ISO / Athena string; anything else shows "–" |
The host shows the frame over the whole page while a dialog or drawer is open, so they're always fully visible.
Every component, in light and dark, is in the UI kit gallery on the developer site (the repository's
gallery app, examples/ui-kit, rendered as a preview). <HawkAppProvider preview={{ mode: 'dark' }}> renders without a host, with sample context
(previewContext()), for galleries, docs and screenshots. New apps get the kit from the CLI's next-mui template
(hawk-app create my-app --template next-mui); an existing next-mui app gets it with hawk-app upgrade.
The theme
createHawkTheme(context.theme, ...overrides) (also at @bridgeline-digital/hawk-app-ui/theme):
| Part | From |
|---|---|
Typography (Montserrat, 14 px base, h1–h3, featuredValue), breakpoints (sm 640 … 2xl 1536), spacing, card and list overrides | Spark's theme (sparkThemeOptions) |
| Primary color and its text, backgrounds, text colors, dividers, border radius, light or dark | The host's theme tokens (color-button-accent-background, color-background, color-background-container, color-text-default, color-text-secondary, color-border-default, border-radius, mode), else Spark's. A color-border-default too faint to see on the host's surface (contrast under 1.2, such as HawkSearch's #f5f7fa on white) is left out, so dividers, table rows and outlined cards keep MUI's divider |
| Dialogs and snackbars in the part of the frame the user can see | The kit (frameThemeOptions, from the bridge's --hawk-visible-* variables) |
| Menus and selects that don't lock the frame's scrolling (a lock resizes the frame and the page flickers) | The kit (frameThemeOptions) |
Table rows and header (borders in the divider color, a header row on the page background, rows 10px above and below, every other body row tinted; the stylesheet's .hawk-table alike) | The kit (tableThemeOptions) |
The font is always Spark's, whatever the host uses, so apps look the same in HawkSearch and Spark.
Without React and MUI
For vanilla, Preact, React or Next apps without MUI: the same look as a stylesheet, and a few custom elements for what CSS alone can't do. No dependencies: no React, no MUI, no bridge.
// With a bundler (Vite, Next): once, where the app starts (Next: the css in app/layout.tsx).
import '@bridgeline-digital/hawk-app-ui/css';
import '@bridgeline-digital/hawk-app-ui/elements';
<!-- Without a build step: from the CDN, at the bridge's version (the vanilla template does this). -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@bridgeline-digital/[email protected]/dist/css/hawk.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/@bridgeline-digital/[email protected]/dist/elements/index.js"></script>
The stylesheet (/css, dist/css/hawk.css) carries Montserrat (no request to Google Fonts) and maps the host's
theme tokens, which the bridge sets as --hawk-* variables and <html data-hawk-theme>, as createHawkTheme does:
the accent, backgrounds, text, borders, radius and dark mode, else Spark's. Use its --hawk-ui-* variables
(--hawk-ui-accent, --hawk-ui-surface, --hawk-ui-text, --hawk-ui-text-secondary, --hawk-ui-border,
--hawk-ui-radius, …) for your own styles. A view's page (<html data-hawk-view>) stays transparent.
| Class | For |
|---|---|
hawk-section (hawk-section-header, -title, -subtitle) | A titled card, Spark's |
hawk-kpi-row, hawk-stack, hawk-row, hawk-spacer | Tiles side by side; a column or row of blocks |
hawk-button (is-outlined, is-text, is-small) | Buttons in the host's accent |
hawk-field, hawk-input, hawk-select | Form fields |
hawk-notice (is-error, is-warning, is-success) | A message |
hawk-chip (is-accent), hawk-empty, hawk-spinner, hawk-skeleton | Labels, nothing to show, loading |
hawk-table (in hawk-table-wrap) | A plain table (or use <hawk-data-table>) |
hawk-h1… hawk-h3, hawk-value, hawk-muted, hawk-caption, hawk-num | Type: headings, a headline number, secondary text |
The elements (/elements) render that markup. Data goes in as properties (el.rows = [...]), settings as
attributes. Importing defines them (in a browser; it's safe to import on a server, e.g. in Next).
| Element | Use |
|---|---|
<hawk-kpi label value hint change good-when highlight loading> | A number with its change vs. the previous period (change as a fraction, none for "No previous data"; green when good) |
<hawk-data-table> | columns ({ key, label, align?, format?, value?, sortable?, csvLabel? }; format: integer, number, compact, percent, money, date, dateTime or a function), rows, totals, sort; attributes download="file.csv", max-height, clickable (hawk-row-click events), empty. Sortable headers, CSV download of the rows as sorted (formulas made text) |
<hawk-bar-list> | items ({ label, value }); selectable (hawk-select events), value-format, color, label |
<hawk-column-chart> | columns ({ label, value, tooltip? }); height, value-format, color, label |
const table = document.querySelector('hawk-data-table');
table.columns = [{ key: 'keyword', label: 'Keyword' }, { key: 'search_count', label: 'Searches', format: 'integer' }];
table.rows = report.data;
table.setAttribute('download', 'searches.csv');
createFormat(locale) (from /elements too) formats numbers, money and dates like useFormat(). In React or Preact
with TypeScript, type the elements in JSX with HawkElementTags (the templates have src/hawk-elements.d.ts):
import type { HawkElementTags } from '@bridgeline-digital/hawk-app-ui/elements';
declare module 'react' { namespace JSX { interface IntrinsicElements extends HawkElementTags {} } } // Preact: 'preact'
Toasts: app.toast(message, severity) from the bridge shows the host's own.
Versions
Released with the bridge and the CLI (one version for all three). Peer dependencies of the React + MUI part: React 19, MUI 9, Emotion 11 (optional for the stylesheet and the elements), and the bridge.