@groveback/ui 0.1.1 → 0.2.0

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.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The routing shell a generated app runs its screens in.
3
+ *
4
+ * `grove gen --ui` emits `routes.tsx` as DATA — the `ROUTES` table — and this module is the
5
+ * code that consumes it. The split is deliberate: the table is derived from the Studio
6
+ * document (routes, params, titles) and belongs to the user's repo, while the matcher and the
7
+ * History wiring are behaviour that should improve with `npm update`, not sit frozen in every
8
+ * app that was scaffolded before a fix.
9
+ *
10
+ * Nothing here depends on a router library. `ROUTES` is the useful part; an app that already
11
+ * uses React Router or TanStack feeds the table to that and never mounts `GroveRoutes`.
12
+ */
13
+ import type { ReactNode } from 'react';
14
+ import type { GroveUiContext } from './runtime';
15
+ export interface GroveRoute {
16
+ /** Route pattern, `:param` segments included. */
17
+ path: string;
18
+ /** Title authored in the Studio, when the screen has one. */
19
+ title?: string | undefined;
20
+ render: (params: Record<string, string>) => ReactNode;
21
+ }
22
+ /**
23
+ * Match a concrete path against a route pattern, capturing its params.
24
+ *
25
+ * Segment-wise, the same rule the Studio's validator and its preview use. Query and hash are
26
+ * ignored, a trailing slash is tolerated, and the root path is a real route: `/` must split to
27
+ * the same segments as the pattern `/`, which a bare "strip trailing slashes" would break by
28
+ * reducing it to an empty string.
29
+ */
30
+ export declare function matchRoute(path: string, pattern: string): Record<string, string> | null;
31
+ /**
32
+ * The route a path opens, and the params it carries.
33
+ *
34
+ * Specific routes win over parameterised ones regardless of the order given: `/articles/new`
35
+ * is a real page, and `/articles/:id` would otherwise swallow it by reading "new" as an id.
36
+ * Generic over the route shape so the Studio preview can resolve against its screens with the
37
+ * same rule the generated app uses.
38
+ */
39
+ export declare function resolveRoute<T extends {
40
+ path: string;
41
+ }>(routes: readonly T[], path: string): {
42
+ route: T;
43
+ params: Record<string, string>;
44
+ } | null;
45
+ /** The screen the current URL shows, under the provider the primitives dispatch through. */
46
+ export declare function GroveRoutes(props: {
47
+ routes: readonly GroveRoute[];
48
+ /**
49
+ * Everything the primitives need beyond navigation — `endpointBase`, `locale`, `currency`,
50
+ * `confirm`. Navigation itself is owned here, since it must update the matched route.
51
+ */
52
+ context?: Omit<GroveUiContext, 'navigate'> | undefined;
53
+ /**
54
+ * Rendered when no route matches. Default: at `/` with no home screen, an index of the
55
+ * document's pages (a generated app should never open on "Not found"); anywhere else, a
56
+ * plain "Not found" with a way home.
57
+ */
58
+ fallback?: ReactNode | undefined;
59
+ /**
60
+ * Rendered inside the provider, ahead of the screen — where an app puts its own chrome.
61
+ * A sign-in gate goes OUTSIDE instead (`<SignIn><GroveRoutes …/></SignIn>`): it needs no
62
+ * provider, and wrapping the router is what keeps every route behind it at once.
63
+ */
64
+ children?: ReactNode | undefined;
65
+ }): import("react").JSX.Element;
66
+ /**
67
+ * The pages of the document, for a root with no screen of its own. Only routes with no
68
+ * parameters are listed — `/books/:id` cannot be opened without a book.
69
+ */
70
+ export declare function RouteIndex({ routes }: {
71
+ routes: readonly GroveRoute[];
72
+ }): import("react").JSX.Element;
package/dist/runtime.d.ts CHANGED
@@ -9,7 +9,13 @@ import type { Action, CollectionLike, Doc, Format } from './types';
9
9
  export interface GroveUiContext {
10
10
  /** Navigate to a route. */
11
11
  navigate: (href: string) => void;
12
- /** Base URL for custom endpoint (`/run/*`) actions. */
12
+ /**
13
+ * Calls one of the project's custom endpoints WITH the app's session — `grove.client.run`
14
+ * in a generated app. Preferred over `endpointBase`, which is a bare `fetch` that carries
15
+ * no token and so runs the endpoint as nobody.
16
+ */
17
+ callEndpoint?: (path: string, method: 'POST' | 'GET') => Promise<unknown>;
18
+ /** Base URL for custom endpoint (`/run/*`) actions when no `callEndpoint` is given. */
13
19
  endpointBase?: string;
14
20
  /** Confirmation gate for destructive actions; defaults to window.confirm. */
15
21
  confirm?: (message: string) => boolean | Promise<boolean>;
@@ -43,8 +49,20 @@ export interface DispatchOptions {
43
49
  * left to whoever authored the screen.
44
50
  */
45
51
  export declare function useAction(): (action: Action, opts?: DispatchOptions) => Promise<void>;
46
- /** Render a stored value for display. Never throws — a bad value shows as empty. */
47
52
  export declare function formatValue(value: unknown, format: Format | undefined, opts?: {
48
53
  locale?: string | undefined;
49
54
  currency?: string | undefined;
50
55
  }): string;
56
+ /**
57
+ * A translation lookup over the catalogue a multilingual screen carries, reading the active
58
+ * locale from the provider. The screen keeps its MESSAGES; the locale lives in ONE place —
59
+ * the routing shell's context — instead of a module variable per screen that nothing could
60
+ * set for all of them at once.
61
+ */
62
+ export declare function useMessages(messages: Record<string, Record<string, string>>, fallback: string): (key: string) => string;
63
+ /**
64
+ * The locale to start in: the first of the viewer's preferred languages the document has,
65
+ * matched exactly and then by language (`es-MX` finds `es`), else the document's first —
66
+ * its default — locale.
67
+ */
68
+ export declare function detectLocale(locales: readonly string[], preferred?: readonly string[]): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The class strings the primitives share, in one place so no two components can drift apart
3
+ * on the same control. Internal: nothing here is exported from the package.
4
+ *
5
+ * Both halves of every pair are declared. A control that paints a background must also paint
6
+ * its foreground, and it must state the LIGHT case rather than leaving it to inherit: a bare
7
+ * `dark:bg-neutral-950` fires on a viewer whose OS is in dark mode even when the surrounding
8
+ * page is light, which left a black input holding black text — invisible — on a white page.
9
+ */
10
+ import type { ButtonVariant } from './types';
11
+ export declare function cx(...parts: Array<string | false | undefined>): string;
12
+ /** The one input skin, shared by text/number/date inputs and the enum select. */
13
+ export declare const INPUT_CLASS: string;
14
+ export declare const BUTTON_BASE: string;
15
+ export declare const VARIANT: Record<ButtonVariant, string>;
16
+ export declare const TONE: {
17
+ readonly error: "text-sm text-red-600";
18
+ readonly success: "text-sm text-green-700 dark:text-green-500";
19
+ readonly muted: "text-sm text-neutral-500";
20
+ };
package/dist/types.d.ts CHANGED
@@ -5,22 +5,54 @@
5
5
  */
6
6
  /** Minimal shape of a document as the SDK returns it. */
7
7
  export type Doc = Record<string, unknown>;
8
- /** The slice of the SDK's collection surface the primitives use. */
9
- export interface CollectionLike {
8
+ /**
9
+ * The slice of the SDK's collection surface the primitives use.
10
+ *
11
+ * Generic in the document type so a `grove gen` TYPED collection is assignable. It cannot be
12
+ * `Doc` alone: a generated `interface Customers` has no index signature, and TypeScript does
13
+ * not give interfaces one implicitly, so `TypedCollection<Customers, …>` failed to satisfy
14
+ * `Promise<Doc[]>` — every screen `grove gen --ui` writes was a type error in the user's app.
15
+ *
16
+ * `TInput` is separate for the same reason on the write side: a generated `CustomersInput`
17
+ * has REQUIRED fields, so a `create(doc: Doc)` signature is not one it can satisfy. Both
18
+ * parameters default to `Doc`, which is what an untyped collection and the preview pass.
19
+ */
20
+ export interface CollectionLike<TDoc extends Doc = Doc, TInput = Doc> {
10
21
  find(opts?: {
11
22
  filter?: Record<string, unknown>;
12
23
  limit?: number;
13
24
  skip?: number;
14
- }): Promise<Doc[]>;
15
- get(id: string): Promise<Doc>;
16
- create(doc: Doc): Promise<Doc>;
17
- update(id: string, patch: Doc): Promise<void>;
25
+ }): Promise<TDoc[]>;
26
+ get(id: string): Promise<TDoc>;
27
+ create(doc: TInput): Promise<TDoc>;
28
+ update(id: string, patch: Partial<TInput> & Doc): Promise<void>;
18
29
  delete(id: string): Promise<void>;
19
30
  }
20
- export type Format = 'text' | 'number' | 'currency' | 'date' | 'boolean';
31
+ /**
32
+ * A collection of ANY document/input shape — what the primitives' props accept.
33
+ *
34
+ * The props cannot say `CollectionLike` (that is `CollectionLike<Doc, Doc>`, which a typed
35
+ * `TypedCollection<Customers, CustomersInput>` does not satisfy: `create` would have to take
36
+ * a loose `Doc` where the generated input has required fields). Making every component
37
+ * generic would push those parameters into four public prop types for no gain, since a
38
+ * primitive never uses the row type — it reads fields by a name that came from the IR.
39
+ *
40
+ * So the props are deliberately shape-agnostic here, and the type safety lives where it is
41
+ * useful: in the caller's own code, where `grove.collections.customers` is still fully typed.
42
+ *
43
+ * `any` for the input is what makes BOTH directions work — a Form assembles a plain object
44
+ * and passes it to `create`, while a generated collection demands its own required fields.
45
+ * It is confined to this one alias and never reaches a caller's types.
46
+ */
47
+ export type AnyCollection = CollectionLike<Doc, any>;
48
+ export type Format = 'text' | 'markdown' | 'number' | 'currency' | 'date' | 'boolean';
21
49
  export interface FieldRef {
22
50
  field: string;
23
51
  label: string;
52
+ /** Allowed values (the schema's `enum`). A form renders these as a select. */
53
+ options?: string[] | undefined;
54
+ /** Column width in a table: `narrow` | `normal` | `wide`. Ignored outside a DataTable. */
55
+ width?: 'narrow' | 'normal' | 'wide' | undefined;
24
56
  format?: Format;
25
57
  /** For an x-ref field: show this field of the referenced document instead of the id. */
26
58
  display?: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@groveback/ui",
3
- "version": "0.1.1",
4
- "description": "React primitives for Groveback Studio screens \u2014 tables, forms, detail views and relation pickers that read through the Groveback SDK, so every query passes the policy engine.",
3
+ "version": "0.2.0",
4
+ "description": "React primitives for Groveback Studio screens tables, forms, detail views and relation pickers that read through the Groveback SDK, so every query passes the policy engine.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -10,15 +10,17 @@
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
- }
13
+ },
14
+ "./tailwind.css": "./tailwind.css"
14
15
  },
15
16
  "files": [
16
- "dist"
17
+ "dist",
18
+ "tailwind.css"
17
19
  ],
18
- "//sideEffects": "NOT set to false: bun 1.4's bundler treats it as licence to drop the entire entry point, emitting the export names with nothing behind them \u2014 a 193-byte module exporting symbols that do not exist, reported as a successful build. The check-bundle script guards against a regression.",
20
+ "//sideEffects": "NOT set to false: bun 1.4's bundler treats it as licence to drop the entire entry point, emitting the export names with nothing behind them a 193-byte module exporting symbols that do not exist, reported as a successful build. The check-bundle script guards against a regression.",
19
21
  "scripts": {
20
22
  "build": "rm -rf dist && bun build ./src/index.ts --outdir dist --target browser --format esm --external react --external react-dom && bun run check-bundle && bunx tsc -p tsconfig.json && cp ../../LICENSE .",
21
- "check-bundle": "bun -e \"const s=(await Bun.file('dist/index.js').text()); if(!s.includes('DataTable')||s.length<5000){console.error('dist/index.js looks empty ('+s.length+' bytes) \u2014 the bundler dropped modules');process.exit(1)}\""
23
+ "check-bundle": "bun -e \"const s=(await Bun.file('dist/index.js').text()); if(!s.includes('DataTable')||s.length<5000){console.error('dist/index.js looks empty ('+s.length+' bytes) the bundler dropped modules');process.exit(1)}\""
22
24
  },
23
25
  "peerDependencies": {
24
26
  "react": ">=18",
package/tailwind.css ADDED
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Tell Tailwind where @groveback/ui's classes live.
3
+ *
4
+ * The primitives are styled with Tailwind utilities but ship no CSS of their own, so the
5
+ * app's Tailwind build has to scan this package or every class goes unemitted and the
6
+ * screens render unstyled. `@source` here is relative to THIS file, so importing it works
7
+ * from any install layout (hoisted, pnpm, a workspace link) without the app guessing a
8
+ * node_modules path. In your app's CSS, after `@import "tailwindcss";`:
9
+ *
10
+ * @import "@groveback/ui/tailwind.css";
11
+ */
12
+ @source "./dist/index.js";