@groveback/ui 0.1.1 → 0.3.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.
package/README.md CHANGED
@@ -14,7 +14,14 @@ npm install @groveback/ui
14
14
  bun add @groveback/ui
15
15
  ```
16
16
 
17
- React 18 or 19 is a peer dependency. The components are styled with Tailwind utility classes, so a Tailwind setup is assumed.
17
+ React 18 or 19 is a peer dependency. The components are styled with Tailwind utility classes and ship no CSS of their own, so your app's Tailwind (v4) build has to scan them — one line in your CSS does it:
18
+
19
+ ```css
20
+ @import "tailwindcss";
21
+ @import "@groveback/ui/tailwind.css";
22
+ ```
23
+
24
+ That file carries the `@source` pointing at the package's own bundle, so it works from any install layout. Dark mode follows the viewer's `prefers-color-scheme`, which is Tailwind's default.
18
25
 
19
26
  ## Usage
20
27
 
@@ -69,15 +76,43 @@ A relation field stores a document id. Pass `resolveRelation` and a `display` hi
69
76
 
70
77
  The picker loads a bounded first page and filters in memory, and it tells the user when the list is truncated — the data API has no text search to delegate to, and silently hiding matches would be worse than saying so.
71
78
 
79
+ ### The routing shell lives here, not in your repo
80
+
81
+ `grove gen --ui` writes `routes.tsx` as a table — `ROUTES`, one entry per Studio screen — and a one-line `GroveRoutes` that mounts it on this package's shell:
82
+
83
+ ```tsx
84
+ import { GroveRoutes } from './routes'; // generated
85
+ import { SignIn } from '@groveback/ui';
86
+ import { createGrove } from './grove';
87
+
88
+ const grove = createGrove();
89
+
90
+ export default function App() {
91
+ return (
92
+ <SignIn auth={grove.auth}>
93
+ <GroveRoutes />
94
+ </SignIn>
95
+ );
96
+ }
97
+ ```
98
+
99
+ `GroveRoutes` matches the current URL against the table (specific routes ahead of parameterised ones), pushes history on navigation, and provides the context the primitives dispatch through. The generated mount already passes `locale` (the viewer's language when the document has it — `detectLocale`) and `callEndpoint` (custom-endpoint actions run through `grove.client.run`, so they carry the session). Pass `context={{ currency, confirm, … }}` for the rest, `fallback` for the unmatched case, and `children` for chrome that should render on every screen. With no `fallback`, the root with no home screen shows an index of the document's pages (`RouteIndex`) rather than "Not found". Already on a router? Feed `ROUTES` to it and skip `GroveRoutes` — `matchRoute` and `resolveRoute` are exported for that.
100
+
101
+ Multilingual documents compile each screen with a `MESSAGES` catalogue and `const t = useMessages(MESSAGES, "en")`: the active locale is the shell's, so every screen switches together — pass `locale` to the generated `GroveRoutes` to drive it from your own switcher.
102
+
103
+ ### Sign-in is a gate around the app
104
+
105
+ `SignIn` takes the SDK's `auth` object and renders its children once a session exists: email + password, an account-creation form (when `auth.register` exists), and the second-factor step when a login answers `mfaRequired`. Errors show the API's own message. Persisting the session across reloads is the app's job — hand the SDK an `onTokensChanged` and restore with `setTokens` on boot.
106
+
72
107
  ### Deletes always confirm
73
108
 
74
109
  The `delete` action lets you override the confirmation *wording*, never whether the prompt happens. That guarantee lives in the library, not in the calling screen.
75
110
 
76
111
  ## Components
77
112
 
78
- `Stack` · `Grid` · `Divider` · `Heading` · `Text` · `Button` · `DataTable` · `Detail` · `Form` · `RelationSelect`
113
+ `Stack` · `Grid` · `Divider` · `Heading` · `Text` · `Button` · `Header` · `DataTable` · `Detail` · `Form` · `RelationSelect` · `MarkdownField` · `GroveRoutes` · `RouteIndex` · `SignIn`
79
114
 
80
- Plus `GroveUiProvider`, `useAction`, `useGroveUi`, `formatValue`, `resolveHref`.
115
+ Plus `GroveUiProvider`, `useAction`, `useGroveUi`, `useMessages`, `detectLocale`, `formatValue`, `resolveHref`, `matchRoute`, `resolveRoute`.
81
116
 
82
117
  ## License
83
118
 
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The editor a `format: "markdown"` field gets in a Form: a formatting toolbar over a
3
+ * textarea, plus a rendered preview.
4
+ *
5
+ * Ported from the dashboard's own MarkdownEditor so a generated app gets the same editor the
6
+ * Studio shows — a preview that flattered the real thing would be the usual trap. The port
7
+ * drops the shadcn/lucide imports for plain elements and inline SVG: this package ships to a
8
+ * user's app and must not drag a component library or an icon dependency in with it.
9
+ *
10
+ * The renderer is a deliberately small subset (headings, bold/italic, code, lists, links,
11
+ * paragraphs) and ESCAPES HTML FIRST, which is what makes injecting it safe — the stored
12
+ * value is untrusted content from whoever filled the form in.
13
+ */
14
+ /** Markdown → HTML for the preview pane. Input is escaped before any tag is produced. */
15
+ export declare function renderMarkdown(src: string): string;
16
+ export declare function MarkdownField({ value, onChange, }: {
17
+ value: string;
18
+ onChange: (next: string) => void;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The sign-in gate a generated app mounts around its routes.
3
+ *
4
+ * Lives in the package rather than in the scaffold so it keeps improving with `npm update`:
5
+ * a scaffold is written once into the user's repo and never touched again by the generator,
6
+ * which is the right contract for a screen the user edits and the wrong one for an auth form
7
+ * that will grow a password reset, OAuth buttons and better errors.
8
+ *
9
+ * It talks to the SDK's `auth` object and nothing else — no URL, no key — so signing in goes
10
+ * through the same endpoints, and the same rate limits, as a hand-written form would.
11
+ */
12
+ import type { ReactNode } from 'react';
13
+ import type { Doc } from './types';
14
+ /**
15
+ * The slice of the SDK's `client.auth` the gate uses. `login` may resolve with
16
+ * `{ mfaRequired: true }`, in which case `verifyMfa` finishes the sign-in — mirrors the SDK.
17
+ */
18
+ export interface AuthLike {
19
+ login(email: string, password: string): Promise<Doc>;
20
+ register?(email: string, password: string, name?: string): Promise<Doc>;
21
+ verifyMfa?(code: string): Promise<Doc>;
22
+ /** Non-null when a session exists. The SDK keeps this in memory; persisting it is the app's job. */
23
+ getTokens(): {
24
+ accessToken: string;
25
+ } | null;
26
+ }
27
+ export interface SignInProps {
28
+ auth: AuthLike;
29
+ /** Shown above the form. Defaults to "Sign in". */
30
+ title?: ReactNode | undefined;
31
+ /** Offer an account-creation form. On by default when `auth.register` exists. */
32
+ allowRegister?: boolean | undefined;
33
+ /** Called with the user after a successful sign-in or registration. */
34
+ onSignedIn?: ((user: Doc) => void) | undefined;
35
+ /** What a signed-in viewer sees — normally the app's routes. */
36
+ children: ReactNode;
37
+ }
38
+ export declare function SignIn(props: SignInProps): import("react").JSX.Element;
@@ -7,7 +7,7 @@
7
7
  * could not reach by hand.
8
8
  */
9
9
  import type { ReactNode } from 'react';
10
- import type { ButtonVariant, CollectionLike, Doc, FieldRef, RowAction } from './types';
10
+ import type { AnyCollection, ButtonVariant, CollectionLike, Doc, FieldRef, RowAction } from './types';
11
11
  import type { Action } from './types';
12
12
  declare const WIDTH: {
13
13
  full: string;
@@ -50,16 +50,38 @@ export declare function Text({ muted, children }: {
50
50
  muted?: boolean;
51
51
  children: ReactNode;
52
52
  }): import("react").JSX.Element;
53
+ /**
54
+ * Site header: brand, navigation, trailing actions.
55
+ *
56
+ * Navigation goes through the host's `navigate` (via `useAction`) rather than a bare `<a>`,
57
+ * so a header in a client-routed app does not full-page-reload on every link. The anchor's
58
+ * `href` is still real, which is what keeps middle-click, "open in new tab" and crawlers
59
+ * working — the click handler only takes over the plain-left-click case.
60
+ */
61
+ export declare function Header(props: {
62
+ brand?: ReactNode;
63
+ brandHref?: string | undefined;
64
+ links?: ReadonlyArray<{
65
+ label: ReactNode;
66
+ href: string;
67
+ }> | undefined;
68
+ actions?: ReadonlyArray<{
69
+ label: ReactNode;
70
+ action: Action;
71
+ variant?: ButtonVariant | undefined;
72
+ }> | undefined;
73
+ sticky?: boolean | undefined;
74
+ }): import("react").JSX.Element;
53
75
  export declare function Button(props: {
54
76
  action: Action;
55
77
  variant?: ButtonVariant | undefined;
56
78
  doc?: Doc | undefined;
57
- collection?: CollectionLike | undefined;
79
+ collection?: AnyCollection | undefined;
58
80
  onDone?: (() => void) | undefined;
59
81
  children: ReactNode;
60
82
  }): import("react").JSX.Element;
61
83
  export declare function DataTable(props: {
62
- collection: CollectionLike;
84
+ collection: AnyCollection;
63
85
  options?: {
64
86
  filter?: Record<string, unknown>;
65
87
  limit?: number;
@@ -73,13 +95,13 @@ export declare function DataTable(props: {
73
95
  resolveRelation?: (field: string) => CollectionLike;
74
96
  }): import("react").JSX.Element;
75
97
  export declare function Detail(props: {
76
- collection: CollectionLike;
98
+ collection: AnyCollection;
77
99
  id: string;
78
100
  fields: FieldRef[];
79
101
  resolveRelation?: (field: string) => CollectionLike;
80
102
  }): import("react").JSX.Element;
81
103
  export declare function Form(props: {
82
- collection: CollectionLike;
104
+ collection: AnyCollection;
83
105
  mode: 'create' | 'update';
84
106
  id?: string;
85
107
  fields: FieldRef[];
package/dist/index.d.ts CHANGED
@@ -10,10 +10,15 @@
10
10
  * convention — this package is browser React bundled by `bun build`, not backend ESM, so the
11
11
  * convention buys nothing and extensionless is what every bundler agrees on.
12
12
  */
13
- import { Button, DataTable, Detail, Divider, Form, Grid, Heading, Stack, Text } from './components';
13
+ import { Button, DataTable, Detail, Divider, Form, Grid, Header, Heading, Stack, Text } from './components';
14
+ import { MarkdownField, renderMarkdown } from './MarkdownField';
14
15
  import { RelationSelect } from './RelationSelect';
15
- import { GroveUiProvider, formatValue, resolveHref, useAction, useGroveUi } from './runtime';
16
- export { Button, DataTable, Detail, Divider, Form, Grid, GroveUiProvider, Heading, RelationSelect, Stack, Text, formatValue, resolveHref, useAction, useGroveUi, };
16
+ import { GroveRoutes, RouteIndex, matchRoute, resolveRoute } from './routes';
17
+ import { SignIn } from './SignIn';
18
+ import { GroveUiProvider, detectLocale, formatValue, resolveHref, useAction, useGroveUi, useMessages } from './runtime';
19
+ export { Button, DataTable, Detail, Divider, Form, Grid, GroveRoutes, GroveUiProvider, Header, Heading, MarkdownField, renderMarkdown, RelationSelect, RouteIndex, SignIn, Stack, Text, detectLocale, formatValue, matchRoute, resolveHref, resolveRoute, useAction, useGroveUi, useMessages, };
17
20
  export type { RelationSelectProps } from './RelationSelect';
18
21
  export type { GroveUiContext } from './runtime';
19
- export type { Action, ButtonVariant, CollectionLike, Doc, FieldRef, Format, RowAction } from './types';
22
+ export type { GroveRoute } from './routes';
23
+ export type { AuthLike, SignInProps } from './SignIn';
24
+ export type { Action, AnyCollection, ButtonVariant, CollectionLike, Doc, FieldRef, Format, RowAction } from './types';