@maykonpaulo/maestro-admin 0.3.0 → 0.4.0-next.1

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
@@ -409,7 +409,7 @@ data, textarea/JSON…).
409
409
 
410
410
  ## 11. A UI de admin (`maestro-admin`)
411
411
 
412
- A UI (React + Vite + Tailwind) lê `GET /metadata` e monta sozinha: navegação por entidade, tabela
412
+ A UI (React + Vite, CSS puro com temas) lê `GET /metadata` e monta sozinha: navegação por entidade, tabela
413
413
  (com busca/ordenação/paginação/filtros vindos do metadata), detalhe, e formulários de criar/editar.
414
414
  Ela respeita as **capacidades** (esconde New/Edit/Delete quando desabilitados) e o **RBAC** (403 vira
415
415
  erro na tela). **Uma tela, todas as entidades.**
@@ -430,7 +430,7 @@ VITE_MAESTRO_API_URL=http://localhost:3000 \
430
430
 
431
431
  ```tsx
432
432
  import { MaestroAdmin } from '@maykonpaulo/maestro-admin';
433
- import '@maykonpaulo/maestro-admin/src/styles.css';
433
+ import '@maykonpaulo/maestro-admin/styles.css';
434
434
 
435
435
  export function AdminPage() {
436
436
  return (
@@ -569,7 +569,7 @@ The **generic, metadata-driven admin UI** for Maestro, per [ADR 0008 — Camada
569
569
 
570
570
  Point it at a running [`@maykonpaulo/maestro-server`](../server) and it reads `GET /metadata` and **auto-builds the whole admin**: a navigation sidebar, a list table, a detail view and create/update forms **for every entity/collection** — no per-entity code. It respects each entity's `capabilities` (a read-only collection shows no New/Edit/Delete) and the server's RBAC (a 403 surfaces as an error, never a broken screen).
571
571
 
572
- Built with **React 19 + Vite + Tailwind**. It ships two ways:
572
+ Built with **React 19 + Vite**, styled with plain CSS custom properties (three built-in themes, light and dark). It ships two ways:
573
573
 
574
574
  - a **runnable static app** (`vite build`) you configure with an env var, and
575
575
  - **exported React components** (`import { MaestroAdmin } from '@maykonpaulo/maestro-admin'`) to embed in your own app.
@@ -591,7 +591,7 @@ VITE_MAESTRO_API_URL=http://localhost:3000 pnpm --filter @maykonpaulo/maestro-ad
591
591
 
592
592
  ```tsx
593
593
  import { MaestroAdmin } from '@maykonpaulo/maestro-admin';
594
- import '@maykonpaulo/maestro-admin/src/styles.css'; // or include the package in your Tailwind content
594
+ import '@maykonpaulo/maestro-admin/styles.css';
595
595
 
596
596
  export function AdminPage() {
597
597
  return (
@@ -606,24 +606,72 @@ export function AdminPage() {
606
606
 
607
607
  `headers` may be a function so a fresh auth token is read on every request. Pass a pre-built `client` (a `MaestroClient`) instead of `apiUrl`/`headers` for full control (custom `fetch`, SSR).
608
608
 
609
- ### Styling
609
+ The admin fills the box you give it — set a height on the container (`height: 100vh`, a grid cell, whatever). It never styles `html`/`body`.
610
610
 
611
- Tailwind v4. Either import the package's source stylesheet (`@maykonpaulo/maestro-admin/src/styles.css`) or, if you already run Tailwind, add the package to your content so its class names are scanned:
611
+ ### Styling and themes
612
612
 
613
- ```css
614
- /* your app.css */
615
- @import 'tailwindcss';
616
- @source '../node_modules/@maykonpaulo/maestro-admin/dist';
613
+ One plain-CSS import, no build step and no Tailwind: `@maykonpaulo/maestro-admin/styles.css` (`@maykonpaulo/maestro-admin/src/styles.css` still resolves to the same file). Every colour is a CSS custom property, so nothing depends on your bundler discovering class names.
614
+
615
+ Three themes ship with the package — `cobalto` (default), `latao` and `teal` — each in light and dark:
616
+
617
+ ```tsx
618
+ <MaestroAdmin apiUrl="…" theme="latao" />
619
+ ```
620
+
621
+ The header carries a palette picker, a light/dark toggle (system → light → dark) and a density toggle, and remembers what the operator chose. Use the object form for more control:
622
+
623
+ ```tsx
624
+ <MaestroAdmin
625
+ apiUrl="…"
626
+ theme={{
627
+ preset: 'teal',
628
+ scheme: 'dark', // 'system' (default) | 'light' | 'dark'
629
+ density: 'compact', // 'comfortable' (default) | 'compact'
630
+ picker: false, // hide the header controls
631
+ remember: false, // don't persist the operator's choices
632
+ tokens: { '--maestro-brand': '#167B61', '--maestro-shell': '#0B2119' },
633
+ }}
634
+ />
617
635
  ```
618
636
 
637
+ `tokens` is applied inline on the admin root, so it wins over the preset in both light and dark. The full token list is at the top of `src/styles.css`; the ones you will reach for most are `--maestro-brand`, `--maestro-shell`, `--maestro-bg`, `--maestro-surface`, `--maestro-border`, `--maestro-fg` and `--maestro-radius`.
638
+
639
+ ### The mark
640
+
641
+ `MaestroMark` and `MaestroSeal` are exported, drawn inline in `currentColor`, and carry no colour of their own — set `color` on them and they follow whatever palette is active. That is why the sidebar logo changes with the theme without shipping one asset per palette:
642
+
643
+ ```tsx
644
+ <MaestroMark size={30} style={{ color: 'var(--maestro-brand)' }} title="Maestro" />
645
+ ```
646
+
647
+ Use `MaestroMark` where there is horizontal room and `MaestroSeal` where the shape is square by obligation or below 16px, where the mark's slices close up. The geometry, the spacing rules and the source SVGs live in [`brand/`](../../brand) at the repo root.
648
+
649
+ ### Favicon
650
+
651
+ A favicon is a separate document: it cannot read the admin's theme attribute, so following the palette means swapping the file. That is the host page's asset, so it is opt-in:
652
+
653
+ ```tsx
654
+ <MaestroAdmin theme={{ favicon: (name) => `/favicon-${name}.svg` }} />
655
+ ```
656
+
657
+ The admin then updates an existing `<link rel="icon">` whenever the operator switches palette. It never injects one, and without the option the tab icon stays exactly as the host set it. `brand/` ships a ready favicon per palette, each already switching between light and dark via `prefers-color-scheme`.
658
+
659
+ ### The admin never scrolls sideways
660
+
661
+ Every screen scrolls vertically only. The hub uses fluid card tracks, the sidebar collapses to icons under 900px, and list tables have fixed column widths with truncation — so an entity with thirty fields does not become thirty columns. Which columns are on screen is the operator's call, from **Columns** in the list toolbar, remembered per entity; the default selection comes from `primaryListFields()` and **Reset columns** hands the decision back to it.
662
+
619
663
  ## Compose your own layout
620
664
 
621
665
  Beyond the all-in-one `<MaestroAdmin>`, the building blocks are exported so you can assemble a custom shell:
622
666
 
623
- - Components: `EntityList`, `EntityDetail`, `EntityForm`, `Sidebar`, `FieldValue`, `FieldInput`
624
- - Hooks: `useMetadata`, `useEntityList`, `useEntityRecord`, `useAsync`, and `listFields`/`detailFields`/`formFields`
667
+ - Shell: `Sidebar`, `Header`, `Hub`, `CommandPalette` (+ `entityCommands`), `MaestroThemeProvider`, `useTheme`
668
+ - Screens: `EntityList`, `EntityDetail`, `EntityForm`, `FieldValue`, `FieldInput`
669
+ - Primitives: `Button`, `IconButton`, `Badge`, `Spinner`, `EmptyState`, `ErrorBanner`, `Toast`, `Icon`, `entityIcon`
670
+ - Hooks: `useMetadata`, `useEntityList`, `useEntityRecord`, `useAsync`, `useColumnPrefs`, `useRecents`, and `listFields`/`detailFields`/`formFields`
625
671
  - Client: `MaestroClient`, `MaestroApiError`, `AdminClientProvider`, `useClient`
626
672
 
673
+ Composing your own shell means owning the token root too: wrap it in `<MaestroThemeProvider className="mst-root">`, which is what paints `data-maestro-theme`/`-scheme`/`-density`.
674
+
627
675
  All of them talk to the server through a single `MaestroClient` provided via `AdminClientProvider`.
628
676
 
629
677
  ## What it renders from metadata
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode, ReactElement, ButtonHTMLAttributes } from 'react';
2
3
 
3
4
  type FieldType = 'string' | 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'enum' | 'json' | 'relation' | 'uuid' | 'email' | (string & {});
4
5
  interface EnumOption {
@@ -186,6 +187,34 @@ interface AdminStrings {
186
187
  recents: string;
187
188
  menus: string;
188
189
  noMenuResults: string;
190
+ hub: string;
191
+ breadcrumb: string;
192
+ search: string;
193
+ commandKey: string;
194
+ commandPalette: string;
195
+ palettePlaceholder: string;
196
+ goTo: string;
197
+ onThisScreen: string;
198
+ appearance: string;
199
+ palette: string;
200
+ density: string;
201
+ colorScheme: string;
202
+ schemeSystem: string;
203
+ schemeLight: string;
204
+ schemeDark: string;
205
+ entities: string;
206
+ hubMeta: (entities: number, sources: number) => string;
207
+ otherEntities: string;
208
+ noEntities: string;
209
+ noEntitiesHint: string;
210
+ fieldCount: (count: number) => string;
211
+ capRead: string;
212
+ capCreate: string;
213
+ capUpdate: string;
214
+ capDelete: string;
215
+ columns: string;
216
+ columnsCount: (visible: number, total: number) => string;
217
+ resetColumns: string;
189
218
  }
190
219
  declare function stringsFor(locale: AdminLocale): AdminStrings;
191
220
  declare const I18nProvider: react.Provider<AdminStrings>;
@@ -217,6 +246,78 @@ interface AdminLabels {
217
246
  /** Returns a copy of `metadata` with every label override applied. No-op when `labels` is empty. */
218
247
  declare function applyLabels(metadata: MaestroMetadata, labels: AdminLabels | undefined): MaestroMetadata;
219
248
 
249
+ /** Built-in palettes. Each ships a light and a dark set of the same tokens. */
250
+ type MaestroThemeName = 'cobalto' | 'latao' | 'teal';
251
+ /** `system` follows the OS `prefers-color-scheme`; the other two force the decision. */
252
+ type MaestroScheme = 'system' | 'light' | 'dark';
253
+ /** Table row height. `compact` fits roughly 40% more rows on a screen and changes nothing else. */
254
+ type MaestroDensity = 'comfortable' | 'compact';
255
+ interface MaestroThemeOption {
256
+ id: MaestroThemeName;
257
+ label: string;
258
+ /** Light-mode brand colour — what the picker shows as the palette's dot. */
259
+ swatch: string;
260
+ }
261
+ declare const MAESTRO_THEMES: readonly MaestroThemeOption[];
262
+ interface MaestroThemeConfig {
263
+ /** Palette to start on. Defaults to `cobalto`. */
264
+ preset?: MaestroThemeName;
265
+ /** Light/dark decision to start on. Defaults to `system`. */
266
+ scheme?: MaestroScheme;
267
+ /** Row density to start on. Defaults to `comfortable`. */
268
+ density?: MaestroDensity;
269
+ /**
270
+ * Token overrides applied inline on the admin root, so they win over the preset in both schemes:
271
+ * `{ '--maestro-brand': '#167B61' }`. Use this to dress the admin in your own brand.
272
+ */
273
+ tokens?: Record<string, string>;
274
+ /** Show the palette / light-dark / density controls in the header. Defaults to `true`. */
275
+ picker?: boolean;
276
+ /** Remember the operator's choices in `localStorage`. Defaults to `true`. */
277
+ remember?: boolean;
278
+ /**
279
+ * Keeps the page's `<link rel="icon">` in step with the active palette. Given the palette name,
280
+ * return the URL of the matching icon:
281
+ *
282
+ * ```ts
283
+ * favicon: (theme) => `/favicon-${theme}.svg`
284
+ * ```
285
+ *
286
+ * Opt-in, and deliberately a function rather than a built-in path: a favicon is a document-level
287
+ * asset the host page owns, and an embedded admin has no business guessing that
288
+ * `/favicon-latao.svg` is served. Without it the tab icon simply stays as the host set it. Only an
289
+ * existing `<link rel="icon">` is updated — the admin never injects one.
290
+ */
291
+ favicon?: (preset: MaestroThemeName) => string | undefined;
292
+ }
293
+ /** `theme="cobalto"` is shorthand for `theme={{ preset: 'cobalto' }}`. */
294
+ type MaestroThemeInput = MaestroThemeName | MaestroThemeConfig;
295
+ interface MaestroThemeState {
296
+ preset: MaestroThemeName;
297
+ scheme: MaestroScheme;
298
+ density: MaestroDensity;
299
+ picker: boolean;
300
+ setPreset: (preset: MaestroThemeName) => void;
301
+ setScheme: (scheme: MaestroScheme) => void;
302
+ setDensity: (density: MaestroDensity) => void;
303
+ }
304
+ /** The active appearance and its setters. Safe outside a provider — falls back to the defaults. */
305
+ declare function useTheme(): MaestroThemeState;
306
+ /**
307
+ * Owns the appearance state and paints the root element that carries the tokens. Everything below it
308
+ * reads colours from CSS custom properties, so a theme switch is one attribute change — no re-render
309
+ * of the tree's styles, no class swapping.
310
+ *
311
+ * The props are the *initial* values: once the operator picks a palette from the header it is
312
+ * remembered (unless `remember: false`), and the stored choice wins on the next mount.
313
+ */
314
+ declare function MaestroThemeProvider({ theme, className, children, rootRef, }: {
315
+ theme?: MaestroThemeInput;
316
+ className?: string;
317
+ children: ReactNode;
318
+ rootRef?: (node: HTMLDivElement | null) => void;
319
+ }): react.JSX.Element;
320
+
220
321
  interface MaestroAdminProps {
221
322
  /** Base URL of the running maestro-server. Ignored when `client` is given. */
222
323
  apiUrl?: string;
@@ -230,17 +331,29 @@ interface MaestroAdminProps {
230
331
  locale?: AdminLocale;
231
332
  /** Label rewrites (localization/humanization) applied over the served metadata. */
232
333
  labels?: AdminLabels;
334
+ /**
335
+ * Appearance: `"cobalto" | "latao" | "teal"`, or an object to also set the light/dark scheme, the
336
+ * row density, whether the header shows the pickers, and per-token overrides for your own brand.
337
+ */
338
+ theme?: MaestroThemeInput;
233
339
  }
234
340
  /**
235
341
  * The whole metadata-driven admin in one component. Point it at a running maestro-server and it reads
236
342
  * `GET /metadata` and auto-builds navigation, tables, detail views and create/update forms for every
237
343
  * entity — respecting each entity's capabilities and the server's RBAC. No per-entity code.
344
+ *
345
+ * Styles ship separately so the host app controls bundling:
346
+ * `import '@maykonpaulo/maestro-admin/styles.css'`.
238
347
  */
239
348
  declare function MaestroAdmin(props: MaestroAdminProps): react.JSX.Element;
240
349
 
241
350
  declare const AdminClientProvider: react.Provider<MaestroClient | null>;
242
351
  /** Returns the `MaestroClient` provided at the root of the admin. Throws if used outside the provider. */
243
352
  declare function useClient(): MaestroClient;
353
+ /** All entities from the loaded metadata — lets deep components (relation chips) resolve a target entity. */
354
+ declare function useEntities(): EntityMetadata[];
355
+ declare function useEntity(entityId: string | undefined): EntityMetadata | undefined;
356
+ declare function useAdminRoot(): HTMLElement | null;
244
357
 
245
358
  interface AsyncState<T> {
246
359
  data: T | undefined;
@@ -302,6 +415,27 @@ declare function useRecents(scope?: string): {
302
415
  push: (entityId: string) => void;
303
416
  };
304
417
 
418
+ interface ColumnPrefs {
419
+ /** The fields to render, in metadata order. */
420
+ columns: FieldMetadata[];
421
+ /** Every field that *could* be a column, for the picker. */
422
+ available: FieldMetadata[];
423
+ visible: (name: string) => boolean;
424
+ toggle: (name: string) => void;
425
+ reset: () => void;
426
+ /** True when the operator has overridden the metadata's default selection. */
427
+ customized: boolean;
428
+ }
429
+ /**
430
+ * Which columns an entity's table shows, remembered per entity.
431
+ *
432
+ * The admin never scrolls sideways, so an entity with thirty fields cannot simply render thirty
433
+ * columns — something has to choose. The default choice is `primaryListFields`, which reads the
434
+ * metadata; this hook lets the operator override it for the entities they actually work in, and
435
+ * `reset` hands the decision back to the metadata.
436
+ */
437
+ declare function useColumnPrefs(entity: EntityMetadata, max?: number): ColumnPrefs;
438
+
305
439
  declare function EntityList({ entity, onOpen, onCreate, onEdit, onClone, onNavigate, initialFilters, }: {
306
440
  entity: EntityMetadata;
307
441
  onOpen: (id: string) => void;
@@ -332,7 +466,7 @@ declare function EntityForm({ entity, mode, id, cloneFromId, onCancel, onSaved,
332
466
  onSaved: (record: Record_) => void;
333
467
  }): react.JSX.Element;
334
468
 
335
- declare function Sidebar({ entities, activeId, onSelect, title, labels, recents, }: {
469
+ declare function Sidebar({ entities, activeId, onSelect, title, labels, recents, onHome, homeActive, }: {
336
470
  entities: EntityMetadata[];
337
471
  activeId: string | undefined;
338
472
  onSelect: (entityId: string) => void;
@@ -341,11 +475,189 @@ declare function Sidebar({ entities, activeId, onSelect, title, labels, recents,
341
475
  labels?: AdminLabels;
342
476
  /** Recently-visited entity ids, most recent first. */
343
477
  recents?: string[];
478
+ /** Opens the hub. Omit to hide the hub entry (standalone/embedded use). */
479
+ onHome?: () => void;
480
+ homeActive?: boolean;
481
+ }): react.JSX.Element;
482
+
483
+ /**
484
+ * The admin's icon set, inline.
485
+ *
486
+ * Twenty-odd 24×24 stroke glyphs are not worth a runtime dependency: `@maykonpaulo/maestro-admin`
487
+ * ships with `dependencies: {}` and this file is why. Every glyph is drawn on the same grid with the
488
+ * same stroke weight, and takes its colour from `currentColor`.
489
+ */
490
+ declare const PATHS: {
491
+ readonly hub: react.JSX.Element;
492
+ readonly search: react.JSX.Element;
493
+ readonly chevronRight: react.JSX.Element;
494
+ readonly chevronLeft: react.JSX.Element;
495
+ readonly chevronsLeft: react.JSX.Element;
496
+ readonly chevronsRight: react.JSX.Element;
497
+ readonly plus: react.JSX.Element;
498
+ readonly close: react.JSX.Element;
499
+ readonly check: react.JSX.Element;
500
+ readonly filter: react.JSX.Element;
501
+ readonly download: react.JSX.Element;
502
+ readonly columns: react.JSX.Element;
503
+ readonly rows: react.JSX.Element;
504
+ readonly sun: react.JSX.Element;
505
+ readonly moon: react.JSX.Element;
506
+ readonly swatch: react.JSX.Element;
507
+ readonly dots: react.JSX.Element;
508
+ readonly trash: react.JSX.Element;
509
+ readonly pencil: react.JSX.Element;
510
+ readonly eye: react.JSX.Element;
511
+ readonly copy: react.JSX.Element;
512
+ readonly clone: react.JSX.Element;
513
+ readonly arrowUpRight: react.JSX.Element;
514
+ readonly arrowLeft: react.JSX.Element;
515
+ readonly sort: react.JSX.Element;
516
+ readonly sortAsc: react.JSX.Element;
517
+ readonly sortDesc: react.JSX.Element;
518
+ readonly database: react.JSX.Element;
519
+ readonly table: react.JSX.Element;
520
+ readonly users: react.JSX.Element;
521
+ readonly box: react.JSX.Element;
522
+ readonly receipt: react.JSX.Element;
523
+ readonly key: react.JSX.Element;
524
+ readonly shield: react.JSX.Element;
525
+ readonly activity: react.JSX.Element;
526
+ readonly tag: react.JSX.Element;
527
+ readonly map: react.JSX.Element;
528
+ readonly calendar: react.JSX.Element;
529
+ readonly mail: react.JSX.Element;
530
+ readonly file: react.JSX.Element;
531
+ readonly alert: react.JSX.Element;
532
+ };
533
+ type IconName = keyof typeof PATHS;
534
+ declare function Icon({ name, size, className, title, }: {
535
+ name: IconName;
536
+ size?: number;
537
+ className?: string;
538
+ title?: string;
539
+ }): ReactElement;
540
+ /**
541
+ * A glyph for an entity, guessed from its names. A metadata-driven admin has no idea what a
542
+ * collection *means*, so this is deliberately a hint and not a promise: anything unrecognised gets
543
+ * the neutral table glyph, which is always a defensible answer.
544
+ */
545
+ declare function entityIcon(entity: EntityMetadata): IconName;
546
+
547
+ interface Crumb {
548
+ label: string;
549
+ icon?: IconName;
550
+ /** Omit on the last crumb — the current place is not a link. */
551
+ onClick?: () => void;
552
+ }
553
+ /**
554
+ * The bar above the content: where you are (breadcrumbs), how to get anywhere (⌘K), and how the admin
555
+ * looks (palette, light/dark, density). The appearance controls live here rather than in a settings
556
+ * page because they are the operator's, changed mid-task and never worth a navigation.
557
+ */
558
+ declare function Header({ crumbs, onOpenPalette }: {
559
+ crumbs: Crumb[];
560
+ onOpenPalette: () => void;
561
+ }): react.JSX.Element;
562
+
563
+ /**
564
+ * The landing screen: every entity the server serves, in its sidebar folder, with what you can do to
565
+ * it and where it actually lives. That last part is the whole point — Maestro speaks a dozen
566
+ * providers, so "which database is this collection in" is a real question with a real answer, and no
567
+ * other admin can show it.
568
+ *
569
+ * Record counts are deliberately absent: metadata does not carry them, and one `count` request per
570
+ * entity would turn opening the admin into thirty round-trips. The count you get is the real one, in
571
+ * the list header, once you open an entity.
572
+ */
573
+ declare function Hub({ entities, labels, onOpen, }: {
574
+ entities: EntityMetadata[];
575
+ labels?: AdminLabels;
576
+ onOpen: (entityId: string) => void;
577
+ }): react.JSX.Element;
578
+
579
+ interface PaletteCommand {
580
+ id: string;
581
+ label: string;
582
+ icon?: IconName;
583
+ /** Right-aligned affordance: a shortcut, or the folder an entity belongs to. */
584
+ hint?: string;
585
+ /** Extra words matched by the search but never displayed (table name, singular label). */
586
+ keywords?: string;
587
+ run: () => void;
588
+ }
589
+ interface PaletteSection {
590
+ id: string;
591
+ label: string;
592
+ commands: PaletteCommand[];
593
+ }
594
+ /** Turns the entity list into palette commands — the navigation half of the palette. */
595
+ declare function entityCommands(entities: EntityMetadata[], groupOf: Map<string, string>, onSelect: (entityId: string) => void): PaletteCommand[];
596
+ /**
597
+ * ⌘K. Everything reachable in the admin — every entity, plus whatever the current screen can do —
598
+ * behind one accent-insensitive search, driven entirely from the keyboard.
599
+ */
600
+ declare function CommandPalette({ sections, onClose }: {
601
+ sections: PaletteSection[];
602
+ onClose: () => void;
603
+ }): react.ReactPortal;
604
+
605
+ /**
606
+ * The mark: an M sliced into three horizontal bands — a stave and a table in the same shape. Use it
607
+ * wherever there is horizontal room (sidebar, header, docs). Below 16px of height the slices close
608
+ * up; use `<MaestroSeal>` there instead.
609
+ */
610
+ declare function MaestroMark({ size, className, title, }: {
611
+ size?: number;
612
+ className?: string;
613
+ title?: string;
614
+ }): ReactElement;
615
+ /**
616
+ * The seal: the same M knocked out of a solid container, keeping only the first slice — the
617
+ * container holds the form at sizes where the mark's bands would close. Use it where the shape is
618
+ * square by obligation (app icon, avatar) or below 16px.
619
+ */
620
+ declare function MaestroSeal({ size, className, title, }: {
621
+ size?: number;
622
+ className?: string;
623
+ title?: string;
624
+ }): ReactElement;
625
+
626
+ type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
627
+ declare function Button({ variant, icon, className, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & {
628
+ variant?: Variant;
629
+ icon?: IconName;
630
+ }): react.JSX.Element;
631
+ /** A square, label-less button. `label` is required — it is the accessible name and the tooltip. */
632
+ declare function IconButton({ icon, label, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & {
633
+ icon: IconName;
634
+ label: string;
635
+ }): react.JSX.Element;
636
+ type BadgeTone = 'neutral' | 'brand' | 'ok' | 'warn' | 'danger';
637
+ declare function Badge({ children, tone, dot }: {
638
+ children: ReactNode;
639
+ tone?: BadgeTone;
640
+ dot?: boolean;
641
+ }): react.JSX.Element;
642
+ declare function Spinner({ label }: {
643
+ label?: string;
644
+ }): react.JSX.Element;
645
+ declare function EmptyState({ title, hint }: {
646
+ title: string;
647
+ hint?: string;
648
+ }): react.JSX.Element;
649
+ declare function ErrorBanner({ error }: {
650
+ error: Error;
651
+ }): react.JSX.Element;
652
+ /** The transient confirmation strip ("Copied!"). Callers own the timer; this only draws it. */
653
+ declare function Toast({ children }: {
654
+ children: ReactNode;
344
655
  }): react.JSX.Element;
345
656
 
346
657
  /**
347
658
  * Renders a single record value read-only, shaped by the field's type (for tables and detail views).
348
- * `compact` is the table-cell mode: long text truncates instead of stretching the row.
659
+ * `compact` is the table-cell mode: the cell itself truncates, so this only has to avoid rendering
660
+ * anything that would force the row taller.
349
661
  */
350
662
  declare function FieldValue({ field, value, compact }: {
351
663
  field: FieldMetadata;
@@ -386,4 +698,4 @@ declare function JsonInput({ field, value, onChange, disabled, }: {
386
698
  disabled?: boolean;
387
699
  }): react.JSX.Element;
388
700
 
389
- export { AdminClientProvider, type AdminLabels, type AdminLocale, type AdminStrings, type AsyncState, type EntityCapabilities, EntityDetail, EntityForm, type EntityLabelOverride, EntityList, type EntityMetadata, FieldInput, type FieldMetadata, FieldValue, type FilterInput, I18nProvider, JsonInput, type ListQuery, type ListResult, MaestroAdmin, type MaestroAdminProps, MaestroApiError, MaestroClient, type MaestroClientConfig, type MaestroMetadata, type MenuGroup, ObjectValue, Sidebar, type SidebarMenu, applyLabels, buildMenu, detailFields, formFields, listFields, primaryListFields, stringsFor, useAsync, useClient, useEntityList, useEntityRecord, useMetadata, useRecents, useT };
701
+ export { AdminClientProvider, type AdminLabels, type AdminLocale, type AdminStrings, type AsyncState, Badge, Button, type ColumnPrefs, CommandPalette, type Crumb, EmptyState, type EntityCapabilities, EntityDetail, EntityForm, type EntityLabelOverride, EntityList, type EntityMetadata, ErrorBanner, FieldInput, type FieldMetadata, FieldValue, type FilterInput, Header, Hub, I18nProvider, Icon, IconButton, type IconName, JsonInput, type ListQuery, type ListResult, MAESTRO_THEMES, MaestroAdmin, type MaestroAdminProps, MaestroApiError, MaestroClient, type MaestroClientConfig, type MaestroDensity, MaestroMark, type MaestroMetadata, type MaestroScheme, MaestroSeal, type MaestroThemeConfig, type MaestroThemeInput, type MaestroThemeName, type MaestroThemeOption, MaestroThemeProvider, type MaestroThemeState, type MenuGroup, ObjectValue, type PaletteCommand, type PaletteSection, Sidebar, type SidebarMenu, Spinner, Toast, applyLabels, buildMenu, detailFields, entityCommands, entityIcon, formFields, listFields, primaryListFields, stringsFor, useAdminRoot, useAsync, useClient, useColumnPrefs, useEntities, useEntity, useEntityList, useEntityRecord, useMetadata, useRecents, useT, useTheme };