create-keboola-app 0.2.0 → 0.2.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.
@@ -3,23 +3,14 @@ import { useRouterState } from '@tanstack/react-router';
3
3
 
4
4
  import { Expired, Restoring, SignInForm, Unavailable } from './states';
5
5
 
6
- /**
7
- * The signed-out screen's path.
8
- *
9
- * - Spelled again as a literal in `createFileRoute`. TanStack's generator reads that argument as
10
- * source text, so it cannot be a reference.
11
- */
6
+ /** Keep in sync with the literal in `createFileRoute` — TanStack's generator needs source text. */
12
7
  export const AUTH_ROUTE = '/signed-out';
13
8
 
14
9
  /**
15
10
  * Whether the router is on the signed-out screen.
16
11
  *
17
- * - The root layout asks, because it is the only component the chromed and chromeless branches
18
- * share.
19
- * - `select` returns the boolean, not the pathname, so navigating between two chromed routes does
20
- * not re-render the root.
21
- * - A real app asks its session instead. This screen belongs to *having no session*, not to a path
22
- * a visitor can type — the path exists so the screen is reachable without a backend.
12
+ * - Selects the boolean, so moving between chromed routes does not re-render the root.
13
+ * - A real app asks its session instead; the path only makes the screen reachable without a backend.
23
14
  */
24
15
  export const useIsAuthRoute = (): boolean =>
25
16
  useRouterState({ select: (state) => state.location.pathname === AUTH_ROUTE });
@@ -27,22 +18,15 @@ export const useIsAuthRoute = (): boolean =>
27
18
  /** One pre-project state: the card's heading, and the body that goes under it. */
28
19
  type AuthStateView = {
29
20
  title: string;
30
- /** Handed `onRetry`, which returns the screen to the sign-in form. States that offer no retry
31
- * simply ignore it. */
21
+ /** `onRetry` returns the screen to the sign-in form. */
32
22
  body: ComponentType<{ onRetry: () => void }>;
33
23
  };
34
24
 
35
25
  /**
36
26
  * The pre-project states, keyed by the session status that produces each.
37
27
  *
38
- * - Four, not one: a signed-out screen that models only "signed out" leaves the other three to be
39
- * invented per app, and they are the ones that get a11y and copy wrong.
40
- * - Every state renders inside the same `AuthScreen`, so the common path does not jump when one
41
- * replaces another.
42
- * - The keys are the contract — `AuthState` is derived from them, so adding a state here is all it
43
- * takes for the route to accept and render it.
44
- * - Components, not render functions, so this module holds no JSX and stays the one place the
45
- * route's search param is validated.
28
+ * - `AuthState` derives from the keys: a state added here is accepted and rendered by the route.
29
+ * - Components, not render functions, so this module holds no JSX.
46
30
  */
47
31
  export const AUTH_STATES = {
48
32
  'signed-out': { title: 'Sign in', body: SignInForm },
@@ -53,6 +37,6 @@ export const AUTH_STATES = {
53
37
 
54
38
  export type AuthState = keyof typeof AUTH_STATES;
55
39
 
56
- /** Narrows an unvalidated search param. Anything else is not a state, including `undefined`. */
40
+ /** Narrows an unvalidated search param. */
57
41
  export const isAuthState = (value: unknown): value is AuthState =>
58
42
  typeof value === 'string' && Object.hasOwn(AUTH_STATES, value);
@@ -1,18 +1,11 @@
1
1
  import { Alert, Button, DotsLoader, useAppForm } from '@keboola/design';
2
2
 
3
- /** What every state's body is handed. `onRetry` returns the screen to the sign-in form. */
4
3
  type AuthStateProps = { onRetry: () => void };
5
4
 
6
5
  /**
7
- * The credential form — the one part that legitimately differs per app.
6
+ * The credential form — the one part that differs per app.
8
7
  *
9
- * - Presentational on purpose: `onSubmit` reaches no network, because only the app knows what it
10
- * authenticates against (password, PAT, OAuth) and what to do with the answer. Replace the body
11
- * of `onSubmit`; nothing else in this directory has to move.
12
- * - `useAppForm` rather than a bare `useForm`: the connectors carry the label, the required marker
13
- * and the error slot, so the field markup does not restate them.
14
- * - `w-full` on the submit: it is the card's only action, so a content-width button reads as one
15
- * option among several.
8
+ * - `onSubmit` reaches no network; each app replaces its body with its own auth.
16
9
  */
17
10
  export const SignInForm = () => {
18
11
  const form = useAppForm({
@@ -37,12 +30,7 @@ export const SignInForm = () => {
37
30
  );
38
31
  };
39
32
 
40
- /**
41
- * A persisted session being rehydrated.
42
- *
43
- * `aria-live="polite"`: the wait resolves without the reader acting, so the outcome has to be
44
- * announced rather than found.
45
- */
33
+ /** A persisted session being rehydrated. */
46
34
  export const Restoring = () => (
47
35
  <div
48
36
  className="tw:flex tw:items-center tw:gap-3 tw:text-sm tw:text-muted-foreground"
@@ -53,12 +41,7 @@ export const Restoring = () => (
53
41
  </div>
54
42
  );
55
43
 
56
- /**
57
- * A credential that was accepted once and is not any more.
58
- *
59
- * `warning`, not `error`: nothing malfunctioned and there is nothing to report — the session ran
60
- * out, and the fix is one click away.
61
- */
44
+ /** A credential that was accepted once and is not any more. */
62
45
  export const Expired = ({ onRetry }: AuthStateProps) => (
63
46
  <>
64
47
  <Alert variant="warning">
@@ -73,8 +56,7 @@ export const Expired = ({ onRetry }: AuthStateProps) => (
73
56
  /**
74
57
  * The service could not be reached, so the credential was never judged.
75
58
  *
76
- * Distinct from `expired` on purpose: telling someone their session expired while the network is
77
- * down sends them to re-enter a password that would have worked.
59
+ * - Not `Expired`: that would send the user to re-enter a password that works.
78
60
  */
79
61
  export const Unavailable = ({ onRetry }: AuthStateProps) => (
80
62
  <>
@@ -13,10 +13,8 @@ import { AssistantToggle } from './AssistantToggle';
13
13
  import { ColorModeToggle } from './ColorModeToggle';
14
14
  import { SidebarModeMenu } from './SidebarModeMenu';
15
15
 
16
- // Chrome is composed inline (not a shared package) so the AppShell/Navbar/Sidebar
17
- // wiring is legible in this exemplar. The Navbar composition follows kbc-ui's
18
- // AppSiteHeader — Header (brand) / Content (primary nav) / Footer (actions).
19
- // The rail's destinations and the active-match rule live in `src/nav.ts`.
16
+ // Composed inline so the AppShell/Navbar/Sidebar wiring stays legible.
17
+ // Navbar follows kbc-ui's AppSiteHeader: Header (brand) / Content (nav) / Footer (actions).
20
18
  export function AppChrome({ children }: { children: ReactNode }) {
21
19
  const pathname = useRouterState({ select: (s) => s.location.pathname });
22
20
  const helpVisible = useAppStore((s) => s.helpVisible);
@@ -29,62 +27,39 @@ export function AppChrome({ children }: { children: ReactNode }) {
29
27
 
30
28
  return (
31
29
  <SidebarProvider
32
- // Left rail — app navigation.
33
- // - Controlled: the mode is a persisted user preference, and the store is its
34
- // single owner. `SidebarModeMenu` in the rail's footer writes it.
30
+ // Controlled: the store owns the persisted mode; `SidebarModeMenu` writes it.
35
31
  leftMode={sidebarMode}
36
32
  onLeftModeChange={setSidebarMode}
37
33
  leftVariant="inset"
38
- // `icon` collapse keeps every destination reachable; `off-canvas` would hide them
39
- // all. It is also what `expand-on-hover` collapses to between hovers.
34
+ // Keeps every destination reachable; also what `expand-on-hover` collapses to.
40
35
  leftCollapsible="icon"
41
36
  // #region block:assistant
42
- // Right rail — the assistant. `rightVariant` is required: a `Sidebar` whose variant
43
- // is `none` (the provider's default) renders NOTHING, silently, so configuring only
44
- // the left side makes the assistant disappear with no error. Mirrors kbc-ui's Kai
45
- // rail, which uses the same pair.
37
+ // Required: variant `none` (the default) renders nothing, silently.
46
38
  rightVariant="sided-floating"
47
39
  rightCollapsible="off-canvas"
48
- // Closed until asked for. The assistant being *available* on every route is
49
- // the point; holding ~30% of the viewport open on a transcript nobody has
50
- // started is not. The Navbar's AssistantToggle opens it (and badges unread
51
- // replies seeded from the page's prompt while it's shut).
40
+ // Closed until the Navbar's AssistantToggle opens it.
52
41
  defaultRightMode="collapsed"
53
- // Matches kbc-ui's Kai rail. The provider's default is narrower than a chat
54
- // transcript wants — message bubbles wrap after a few words at that width.
42
+ // The provider default is too narrow for chat; matches kbc-ui's Kai rail.
55
43
  rightWidth={384}
56
44
  // #endregion block:assistant
57
45
  >
58
- {/* `headerSpan="full"` puts the header above both rails so the brand mark aligns to
59
- the viewport's left edge — the geometry kbc-ui uses. Without it the left rail
60
- owns the top-left corner and the bar starts beside it. */}
46
+ {/* Header above both rails, so the brand mark aligns to the viewport edge. */}
61
47
  <AppShell headerSpan="full">
62
- {/* `flush` drops the slot's padding, background AND bottom border, so the Navbar
63
- renders edge to edge. The border is restated here.
64
- - Both rails below carry a hairline; without a matching edge under the header
65
- those lines start mid-air instead of closing a frame.
66
- - `sidebar-border`, not `border`: the two meet at the corners, and `border`
67
- is 4 units lighter. */}
48
+ {/* `flush` drops padding, background and bottom border; the border is restated:
49
+ - It closes the frame the rails' hairlines start.
50
+ - `sidebar-border`, not `border`: they meet at the corners, `border` is lighter. */}
68
51
  <AppShell.Header flush className="tw:border-b tw:border-sidebar-border">
69
- {/* `flex-1` makes the bar fill the header — as a flex item it would otherwise
70
- shrink to its content width. */}
52
+ {/* As a flex item the bar would otherwise shrink to its content. */}
71
53
  <Navbar bordered className="tw:flex-1">
72
- {/* Extra left gutter, set here rather than on `Navbar`: the bar's own
73
- `pl-2` comes from a cva, which concatenates instead of tailwind-merging,
74
- so a competing `pl-*` there would be settled by cascade order. This slot
75
- merges with `cn`. The DS default is tuned for the header sitting beside
76
- the rail; with `headerSpan="full"` the logo would otherwise hug the
77
- viewport edge instead of lining up with the rail's icons below it.
78
- `pl-2.5` on top of the bar's `pl-2` puts the logo at 18px — the same
79
- x as the rail's icons, measured rather than guessed. */}
54
+ {/* Gutter here, not on `Navbar`: the bar's `pl-2` comes from a cva, which doesn't
55
+ tailwind-merge. With it the logo sits at 18px, aligned with the rail's icons. */}
80
56
  <Navbar.Header className="tw:pl-2.5">
81
57
  <Link to="/" aria-label="Home" className="tw:flex tw:items-center">
82
58
  <BrandLogo className="tw:h-6" />
83
59
  </Link>
84
60
  </Navbar.Header>
85
61
 
86
- {/* Navbar.Content is the flexible middle band: it carries `flex-1` inside the
87
- bar, so it is what pushes Navbar.Footer out to the right edge. */}
62
+ {/* Carries `flex-1`, pushing Navbar.Footer to the right edge. */}
88
63
  <Navbar.Content>
89
64
  <Navbar.Menu marker="forward-slash">
90
65
  <Navbar.MenuItem>
@@ -113,31 +88,20 @@ export function AppChrome({ children }: { children: ReactNode }) {
113
88
  </AppShell.Header>
114
89
 
115
90
  <AppShell.Sidebar>
116
- {/* Elevation while `expand-on-hover` is hovered open. That mode widens the rail
117
- without widening the column it occupies, so the expanded rail overlays the
118
- page, and `inset`'s 1px `border-r` does not read as "above".
119
-
120
- App-scoped rather than a `Sidebar` variant rule: kbc-ui runs this mode by
121
- default and paints its own corner over the rail's right edge with tuned
122
- `::before` / `::after` wedges, so a shadow from the design side would land
123
- on that seam unreviewed. Promote it once that composition has been looked
124
- at in a browser. */}
91
+ {/* Elevation while `expand-on-hover` is open: the rail overlays the page.
92
+ - App-scoped, not a `Sidebar` variant: kbc-ui paints `::before`/`::after` wedges on
93
+ that edge. Promote once reviewed there. */}
125
94
  <Sidebar
126
95
  side="left"
127
96
  className="tw:group-data-[mode=expand-on-hover]/sidebar:group-data-[state=expanded]/sidebar:[&>[data-slot=sidebar]]:shadow-lift"
128
97
  >
129
98
  <Sidebar.Content>
130
- {/* AppShell.Sidebar and Sidebar are plain divs, so without this the rail's
131
- links sit outside any landmark — axe flags the content as unreachable by
132
- landmark navigation. Labelled distinctly from the Navbar's "Main
133
- navigation" so the two are tellable apart in a landmark list. */}
99
+ {/* The rail's landmark (axe); labelled apart from the Navbar's "Main navigation". */}
134
100
  <nav aria-label="Sections">
135
101
  <Sidebar.Menu>
136
102
  {NAV.map((item) => (
137
103
  <Sidebar.MenuItem key={item.to}>
138
- {/* The slot renders an <a>, so the active state is `aria-current="page"`
139
- — `aria-selected` is not a supported attribute on a link. Both get
140
- the same active styling from Sidebar.MenuButton. */}
104
+ {/* A link takes `aria-current`, not `aria-selected`. */}
141
105
  <Sidebar.MenuButton
142
106
  asChild
143
107
  tooltip={item.label}
@@ -156,8 +120,7 @@ export function AppChrome({ children }: { children: ReactNode }) {
156
120
  </nav>
157
121
  </Sidebar.Content>
158
122
 
159
- {/* Footer, not Content: `Sidebar.Content` is the scroll container, so a
160
- control inside it scrolls away with the nav. The footer is pinned. */}
123
+ {/* Footer, not Content: `Sidebar.Content` scrolls, the footer is pinned. */}
161
124
  <Sidebar.Footer>
162
125
  <Sidebar.Menu>
163
126
  <SidebarModeMenu />
@@ -168,23 +131,12 @@ export function AppChrome({ children }: { children: ReactNode }) {
168
131
 
169
132
  <AppShell.Main>{children}</AppShell.Main>
170
133
  {/* #region block:assistant */}
171
- {/* The assistant is chrome, not a page feature: it rides every route. In normal
172
- use it is the right rail; fullscreen adds it as a sibling overlay after
173
- AppShell (it renders its own `tw:fixed tw:inset-0 tw:z-50`) instead of
174
- replacing the rail, so exiting fullscreen returns to the page underneath.
175
- Render exactly one instance per mode — the rail is omitted from the Aside
176
- slot while fullscreen, so the two never appear at once. */}
134
+ {/* The assistant rides every route, one instance per mode:
135
+ - Normal: the right rail.
136
+ - Fullscreen: a fixed overlay after AppShell, no rail; exit returns to the page. */}
177
137
  {!fullscreen && (
178
- // The right rail is a `Sidebar`, not a bare div: it brings the width, border,
179
- // scroll container and a drag-to-resize handle, so the panel doesn't reimplement
180
- // them. `flush` stops the slot painting a second surface underneath it. Same
181
- // composition kbc-ui uses for its Kai rail.
182
- // The parked `off-canvas` rail used to need `overflow-x-clip` here. The
183
- // design system now clips this slot itself while the rail is collapsed
184
- // (see `styles.css`), which an unconditional class could not do safely:
185
- // a filled rail widens past the slot and would be cropped. The two are
186
- // therefore coupled — reverting that rule alone brings the horizontal
187
- // scroll back here, so restore this class if you ever do.
138
+ // `Sidebar` brings width, border, scroll and resize; `flush` avoids a second surface.
139
+ // `styles.css` clips this slot while collapsed; without that rule, add `overflow-x-clip`.
188
140
  <AppShell.Aside flush>
189
141
  <Sidebar side="right" container resizable>
190
142
  <AssistantPanel />
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import type { RefObject } from 'react';
2
+ import type { CSSProperties, RefObject } from 'react';
3
3
 
4
4
  import { Button } from '@keboola/design';
5
5
 
@@ -78,8 +78,15 @@ export function SelectionFloater({
78
78
  <Button
79
79
  variant="secondary"
80
80
  size="small"
81
- className="tw:fixed tw:z-50"
82
- style={{ top: selection.top - 40, left: selection.left }}
81
+ className="tw:fixed tw:top-(--floater-top) tw:left-(--floater-left) tw:z-50"
82
+ // Custom properties, not inline `top`/`left`: the utilities above read them,
83
+ // and the position is measured from the live selection at runtime.
84
+ style={
85
+ {
86
+ '--floater-top': `${selection.top - 40}px`,
87
+ '--floater-left': `${selection.left}px`,
88
+ } as CSSProperties
89
+ }
83
90
  onClick={handleClick}
84
91
  >
85
92
  Explain with Kai
@@ -9,14 +9,12 @@ const MODES: { value: SidebarMode; label: string }[] = [
9
9
  ];
10
10
 
11
11
  /**
12
- * Picks the left rail's mode. All three come from `SidebarProvider`.
12
+ * Picks the left rail's mode.
13
13
  *
14
- * - `expand-on-hover` is a mode of its own, not `collapsed` plus a hover handler: the
15
- * rail reports `mode="expand-on-hover"` while its `state` flips on hover. Styling that
16
- * distinguishes "the user collapsed this" from "collapsed until hovered" reads `mode`.
17
- * - A radio group, not a cycling button: three modes have no natural next, and the menu
18
- * shows which is active without clicking through.
19
- * - `setMode` writes through `onLeftModeChange` into the app store, which persists it.
14
+ * - `expand-on-hover` is its own mode: `state` flips on hover, so styling that tells it from
15
+ * `collapsed` reads `mode`.
16
+ * - A radio group, not a cycling button: three modes have no natural next.
17
+ * - `setMode` persists through `onLeftModeChange` into the app store.
20
18
  */
21
19
  export function SidebarModeMenu() {
22
20
  const { setMode, mode } = useSidebar('left');
@@ -25,8 +23,7 @@ export function SidebarModeMenu() {
25
23
  <Sidebar.MenuItem>
26
24
  <DropdownMenu>
27
25
  <DropdownMenu.Trigger asChild>
28
- {/* `w-fit` keeps the trigger icon-sized rather than rail-wide, so it holds its
29
- position when the rail collapses to icons. */}
26
+ {/* `w-fit` holds the trigger in place when the rail collapses. */}
30
27
  <Sidebar.MenuButton
31
28
  tooltip="Sidebar mode"
32
29
  className="tw:w-fit"
@@ -34,12 +31,10 @@ export function SidebarModeMenu() {
34
31
  aria-label="Sidebar mode"
35
32
  />
36
33
  </DropdownMenu.Trigger>
37
- {/* `align="start"` — the trigger sits at the rail's left edge, so a centered
38
- menu would hang off the viewport when the rail is collapsed. */}
34
+ {/* A centered menu would hang off the viewport when the rail is collapsed. */}
39
35
  <DropdownMenu.Content align="start">
40
36
  <DropdownMenu.Label>Sidebar mode</DropdownMenu.Label>
41
- {/* `RadioGroup` is generic over its value: `value={mode}` pins it to
42
- `SidebarMode`, so `setMode` slots in without a cast. */}
37
+ {/* `value={mode}` types the group as `SidebarMode`, so `setMode` needs no cast. */}
43
38
  <DropdownMenu.RadioGroup value={mode} onValueChange={setMode}>
44
39
  {MODES.map(({ value, label }) => (
45
40
  <DropdownMenu.RadioItem key={value} closeOnSelect indicatorAlign="end" value={value}>
@@ -11,12 +11,9 @@ export type NavItem = { to: string; label: string; icon: ComponentType };
11
11
  /**
12
12
  * The left rail's destinations, in render order.
13
13
  *
14
- * - `as const` keeps each `to` a literal. `Link`'s `to` is typed against the generated
15
- * route tree, so a widened `string` fails type-check at the call site.
16
- * - `satisfies` checks the shape without erasing those literals.
17
- * - Icons render with no props (`<item.icon />`).
18
- * - Each block contributes its own entry from a module the block owns, so a deselected block
19
- * takes its import with it.
14
+ * - `as const` keeps each `to` literal for `Link`'s route-typed `to`; `satisfies` won't widen it.
15
+ * - Icons render with no props.
16
+ * - Each block owns its entry, so a deselected block takes its import with it.
20
17
  */
21
18
  export const NAV = [
22
19
  { to: '/', label: 'Overview', icon: LayoutDashboard },
@@ -29,10 +26,8 @@ export const NAV = [
29
26
  * Whether `to` is the active destination for `pathname`.
30
27
  *
31
28
  * - The root matches exactly.
32
- * - Any other item stays active on its nested routes, matching Router's default
33
- * non-exact `Link` behaviour.
34
- * - A sibling that merely shares the prefix (`/tokens-archive` under `/tokens`) is not
35
- * nested — hence the `/` in the boundary check.
29
+ * - Others stay active on nested routes, like Router's non-exact `Link`.
30
+ * - The `/` boundary keeps `/tokens-archive` from matching `/tokens`.
36
31
  */
37
32
  export const isActive = (pathname: string, to: string) =>
38
33
  to === '/' ? pathname === '/' : pathname === to || pathname.startsWith(`${to}/`);
@@ -1,7 +1,5 @@
1
- // In-memory stand-in for a real overview endpoint. Swap `fetchOverview` for the
2
- // api-client call (e.g. `api.metrics.overview(signal)`) and delete this file.
3
- // The call takes an AbortSignal and honors it, so the query hook threads
4
- // cancellation exactly as it would against a real client — see overview-hooks.ts.
1
+ // In-memory stand-in for an overview endpoint; swap `fetchOverview` for the api-client call.
2
+ // It honors its AbortSignal like a real client — see overview-hooks.ts.
5
3
 
6
4
  export type Kpi = {
7
5
  label: string;
@@ -33,16 +31,10 @@ const OVERVIEW: Overview = {
33
31
  { label: 'Job success rate', value: '98.7%', deltaPct: 0.3 },
34
32
  { label: 'Storage used', value: '1.4 TB', deltaPct: 4.2 },
35
33
  { label: 'Active configurations', value: '312', deltaPct: 2.1 },
36
- // No delta on purpose: `KpiCard` paints a rise green, which is the wrong
37
- // signal for a cost. Leaving it off also exercises the optional branch.
34
+ // No delta: `KpiCard` paints a rise green, wrong for a cost.
38
35
  { label: 'Credits this month', value: '8,420' },
39
36
  ],
40
- // Two series of the same order of magnitude, so they read against one linear
41
- // axis. Pairing a ~60k series with a ~1k one renders the small one as a flat
42
- // line on the floor — a chart that looks informative and isn't.
43
- //
44
- // Consumption climbs from a low base, so a zero baseline is honest *and* uses
45
- // the plot: the story is the gap to the ceiling closing month over month.
37
+ // Same order of magnitude, so both read on one linear axis from zero.
46
38
  credits: [
47
39
  { month: 'Jan', used: 4200, budgeted: 8000 },
48
40
  { month: 'Feb', used: 5100, budgeted: 8000 },
@@ -51,9 +43,7 @@ const OVERVIEW: Overview = {
51
43
  { month: 'May', used: 7600, budgeted: 8500 },
52
44
  { month: 'Jun', used: 8420, budgeted: 8500 },
53
45
  ],
54
- // The grid *is* the budget the chart plots: each month's column sums to that
55
- // month's `budgeted` value above (8000 ×3, then 8500 ×3). Fixtures that don't
56
- // reconcile are the fastest way to make a demo look fake.
46
+ // Each month's column sums to that month's `budgeted` above.
57
47
  budget: [
58
48
  {
59
49
  id: 'ex-db',
@@ -82,9 +72,7 @@ const OVERVIEW: Overview = {
82
72
  ],
83
73
  };
84
74
 
85
- // Simulates a network round-trip that resolves after `ms`, or rejects the moment
86
- // an AbortSignal fires — mirroring how a real fetch-based client aborts in-flight
87
- // when TanStack Query cancels the query.
75
+ // Resolves after `ms`; rejects when the signal aborts, like a fetch-based client.
88
76
  const delay = (ms: number, signal?: AbortSignal): Promise<void> =>
89
77
  new Promise((resolve, reject) => {
90
78
  if (signal?.aborted) {
@@ -13,9 +13,7 @@ it('renders the chart card title and legend labels', () => {
13
13
  { month: 'Jan', mrr: 10 },
14
14
  { month: 'Feb', mrr: 12 },
15
15
  ]}
16
- // recharts' <ResponsiveContainer> only renders once it measures a
17
- // non-zero size via ResizeObserver, which jsdom never provides — an
18
- // explicit width keeps the chart (and its legend) deterministic.
16
+ // jsdom never sizes recharts' <ResponsiveContainer>, so it would render nothing.
19
17
  width={400}
20
18
  />,
21
19
  );
@@ -1,10 +1,8 @@
1
1
  import { Card } from '@keboola/design';
2
2
  import { type ChartColorToken, LineChart } from '@keboola/design/charts';
3
3
 
4
- // Token-aware chart recipe — colors come from CSS vars (semantic/brand tokens),
5
- // never hex, so it re-themes with the active brand. `LineChartCard` is a thin
6
- // app-level card wrapper over the DS `@keboola/design/charts` `LineChart`.
7
- // The chart palette slots the DS exposes; narrowed so a typo is a compile error.
4
+ // Colors come from DS palette tokens, never hex, so the chart re-themes with the brand.
5
+ // Narrowed so a typo is a compile error.
8
6
  type ChartTokenVar = '--chart-1' | '--chart-2' | '--chart-3' | '--chart-4' | '--chart-5';
9
7
 
10
8
  type Series = { key: string; label: string; tokenVar: ChartTokenVar };
@@ -14,16 +12,11 @@ type LineChartCardProps = {
14
12
  data: readonly Record<string, number | string>[];
15
13
  xKey: string;
16
14
  series: readonly Series[];
17
- // Forwarded to the DS `LineChart` only when set; otherwise its responsive
18
- // `width="100%"` default applies. Tests pass an explicit size — recharts'
19
- // <ResponsiveContainer> only renders once it measures a non-zero size via
20
- // ResizeObserver, which jsdom never provides.
15
+ // Unset → responsive `width="100%"`. Tests set it: jsdom never sizes <ResponsiveContainer>.
21
16
  width?: number;
22
17
  };
23
18
 
24
- // Maps a `--chart-N` token var name to the design system's `--color-chart-N`
25
- // palette slot (1–5). `LineChart` resolves colors from this slot internally —
26
- // it never accepts a raw CSS var or hex value.
19
+ // `LineChart` takes a palette slot (1–5), never a raw CSS var or hex.
27
20
  const CHART_COLOR_SLOTS: Record<ChartTokenVar, ChartColorToken> = {
28
21
  '--chart-1': 1,
29
22
  '--chart-2': 2,
@@ -11,19 +11,14 @@ import { NumberInput, Table } from '@keboola/design';
11
11
  import type { BudgetRow } from '../overview-fixtures';
12
12
  import { BUDGET_MONTHS } from '../overview-fixtures';
13
13
 
14
- // Instantiates the DS editable-grid recipe (`Table.DataGridRecipe` story,
15
- // `EditableDataGrid`): TanStack Table v8 (headless) for the column/row model,
16
- // design `Table` for styling, design `NumberInput` as the editable cell, with a
17
- // live-computed row total column and footer grand total.
14
+ // Instance of the DS editable-grid recipe (`Table.DataGridRecipe` story, `EditableDataGrid`).
18
15
  const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0);
19
16
 
20
17
  const columnHelper = createColumnHelper<BudgetRow>();
21
18
 
22
19
  export function CreditBudgetGrid({ rows }: { rows: BudgetRow[] }) {
23
- // Server data seeds local edit state, re-seeding when a fetch returns new rows.
24
- // Adjusted during render rather than in an effect: an effect would commit the
25
- // stale rows first and repaint, and React documents this as the way to reset
26
- // state on a prop change (design's own SidebarProvider does the same).
20
+ // Re-seeds edit state when `rows` changes.
21
+ // During render, not in an effect: an effect would paint the stale rows first.
27
22
  const [data, setData] = useState<BudgetRow[]>(rows);
28
23
  const [seed, setSeed] = useState<BudgetRow[]>(rows);
29
24
  if (seed !== rows) {
@@ -113,17 +108,8 @@ export function CreditBudgetGrid({ rows }: { rows: BudgetRow[] }) {
113
108
  ? 'tw:min-w-52'
114
109
  : cell.column.id === 'total'
115
110
  ? 'tw:text-right tw:tabular-nums'
116
- : // Wide enough for the editor, not just the number. `NumberInput`
117
- // carries 16px of padding on each side, so a `w-20` (80px) cell
118
- // minus the `px-3` gutter left ~22px of text box — enough to
119
- // render "8" and silently hide the "00" of 800.
120
- //
121
- // `min-w`, not `w`: a width is only a hint, and a `w-full` table
122
- // squeezed by the assistant rail compresses past it and clips the
123
- // inputs again. A floor makes the table outgrow its container
124
- // instead, which the wrapper's `overflow-x-auto` then scrolls — a
125
- // single-line input can't be scrolled by eye, so its value has to
126
- // stay fully visible at any container width.
111
+ : // Fits `NumberInput`'s 16px side padding; narrower clips "800" to "8".
112
+ // `min-w`, not `w`: a squeezed table then scrolls instead of clipping.
127
113
  'tw:min-w-28'
128
114
  }
129
115
  >
@@ -13,19 +13,12 @@ export const Route = createRootRoute({
13
13
 
14
14
  function RootLayout() {
15
15
  // #region block:auth
16
- // The signed-out screen replaces the chrome instead of rendering inside it.
17
- //
18
- // - A visitor with no session must see no rail, no breadcrumb and no history.
19
- // - Nesting it would also give the page a second `header`/`main` pair inside AppChrome's, which
20
- // axe reports as duplicate landmarks.
21
- // - Decided here because the root layout is the only component both branches share, and
22
- // TanStack nests every file route under it.
23
- // - The preflight scope is restated even though `AuthScreen` carries its own: that scope is a
24
- // class on an element, so it does not reach `Toaster` beside it, and design's preflight is
25
- // opt-in. Measured outside it, a bare `button` gets a 2px outset border and Arial 13.3px.
26
- // - The blank line above the region's closing marker is inside the region on purpose. Left
27
- // outside, it becomes the whole body of an app generated without this block, and that app's
28
- // `oxfmt --check` fails on a file nobody wrote.
16
+ // The signed-out screen replaces the chrome instead of nesting in it.
17
+ // - No session → no rail, breadcrumb or history.
18
+ // - Nesting would duplicate AppChrome's `header`/`main` landmarks (axe).
19
+ // - Preflight is restated: `AuthScreen`'s own scope does not reach `Toaster` beside it.
20
+ // - The blank line before the closing marker must stay inside the region, or an app generated
21
+ // without this block fails `oxfmt --check`.
29
22
  const isAuthRoute = useIsAuthRoute();
30
23
  if (isAuthRoute) {
31
24
  return (
@@ -7,13 +7,9 @@ import { AUTH_ROUTE, AUTH_STATES, isAuthState } from '../auth/session';
7
7
  /**
8
8
  * `/signed-out` — the pre-project screen.
9
9
  *
10
- * - `?state=` stands in for the app's session status, so all four states are reachable in a
11
- * browser and in the UI gate without a backend. A real app drops the search param and picks the
12
- * state from its session hook; nothing in `auth/` changes when it does.
13
- * - `validateSearch` narrows by hand rather than through a schema. One optional enum does not
14
- * need a validation library, and the block deliberately carries no dependency of its own.
15
- * - An unknown or missing `state` falls back to the sign-in form rather than throwing: this is the
16
- * screen a visitor lands on with no session, so it has to render whatever the URL says.
10
+ * - `?state=` stands in for the session hook, so every state renders without a backend.
11
+ * - `validateSearch` narrows by hand: the block carries no validation dependency.
12
+ * - An unknown or missing `state` falls back to sign-in rather than throwing.
17
13
  */
18
14
  export const Route = createFileRoute('/signed-out')({
19
15
  validateSearch: (search: Record<string, unknown>): { state: AuthState } => ({
@@ -6,11 +6,8 @@ import type { OverviewSection } from '../overview-sections';
6
6
  /** The overview's KPI row: one card per figure. */
7
7
  export const kpiSection: OverviewSection = {
8
8
  id: 'kpi',
9
- // Reflows on this column's own width, not the viewport's. A `lg:grid-cols-4` here
10
- // would keep four columns while the assistant rail squeezes the page to a third of
11
- // it — the rail narrows content without changing the viewport, so viewport
12
- // breakpoints lie. `auto-fit` + a `minmax` floor drops to three, two, then one
13
- // column as room runs out.
9
+ // Reflows on column width, not viewport: the assistant rail narrows the page
10
+ // without changing the viewport, so `lg:` breakpoints would lie.
14
11
  render: (overview) => (
15
12
  <div
16
13
  data-section="kpi"
@@ -21,8 +18,7 @@ export const kpiSection: OverviewSection = {
21
18
  ))}
22
19
  </div>
23
20
  ),
24
- // Four placeholders because four KPIs are coming — a skeleton that does not match
25
- // the shape it stands in for makes the page jump on load.
21
+ // Four slots to match the four KPIs, so the page does not jump on load.
26
22
  renderSkeleton: () => (
27
23
  <div
28
24
  data-section-skeleton="kpi"