@pylonsync/sdk 0.3.292 → 0.3.293

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,360 @@
1
+ export interface BrandConfig {
2
+ /** Display name in the sidebar header. Defaults to manifest.name. */
3
+ name?: string;
4
+ /**
5
+ * Logo source. One of:
6
+ * - data URL (`data:image/svg+xml;base64,...`)
7
+ * - absolute URL (`https://...`)
8
+ * - path served by your app (`/assets/logo.svg`)
9
+ * - emoji or single grapheme rendered as a glyph (`"⚡"`)
10
+ * Omit to fall back to the bolt icon.
11
+ */
12
+ logo?: string;
13
+ /** Optional subtitle shown under the brand name (e.g. "v1.2.3"). */
14
+ subtitle?: string;
15
+ }
16
+ /** Built-in accent palettes. The Studio ships CSS variables for each. */
17
+ export type ThemeAccent = "emerald" | "blue" | "violet" | "rose" | "amber";
18
+ /** Initial appearance — the user can still toggle via the UI (next-themes). */
19
+ export type ThemeAppearance = "dark" | "light" | "system";
20
+ export interface ThemeConfig {
21
+ accent?: ThemeAccent;
22
+ appearance?: ThemeAppearance;
23
+ /**
24
+ * Override the accent's primary color with a custom hex. When set, the
25
+ * accent CSS variables are derived from this value instead of the
26
+ * built-in palette. Use sparingly — the built-in palettes are tuned for
27
+ * AA contrast in both dark and light.
28
+ */
29
+ primary?: string;
30
+ }
31
+ /** Lucide icon name (kebab-case). Validated client-side. */
32
+ export type IconName = string;
33
+ export interface SidebarPageItem {
34
+ type: "page";
35
+ /**
36
+ * Built-in page id (`overview`, `users`, `roles`, `health`, `sync`,
37
+ * `manifest`, `functions`, `policies`, `routes`, `settings`) OR a key
38
+ * registered by `studio.entry.tsx` via `defineStudioExtensions({ pages })`.
39
+ */
40
+ id: string;
41
+ label: string;
42
+ icon?: IconName;
43
+ /** Hide unless the current viewer is_admin. */
44
+ requiresAdmin?: boolean;
45
+ /** Hide unless the current viewer has at least one of these roles. */
46
+ requiresRoles?: string[];
47
+ }
48
+ export interface SidebarResourceItem {
49
+ type: "resource";
50
+ /** Manifest entity name. The list page is auto-generated. */
51
+ entity: string;
52
+ /** Override the sidebar label. Defaults to entity name. */
53
+ label?: string;
54
+ icon?: IconName;
55
+ requiresAdmin?: boolean;
56
+ requiresRoles?: string[];
57
+ }
58
+ export interface SidebarLinkItem {
59
+ type: "link";
60
+ label: string;
61
+ href: string;
62
+ icon?: IconName;
63
+ /** Open in a new tab. Defaults to true for absolute URLs. */
64
+ external?: boolean;
65
+ }
66
+ export interface SidebarHeadingItem {
67
+ type: "heading";
68
+ label: string;
69
+ }
70
+ export type SidebarItem = SidebarPageItem | SidebarResourceItem | SidebarLinkItem | SidebarHeadingItem;
71
+ export interface SidebarSection {
72
+ /** Uppercase label rendered above the items. Pass empty string to omit. */
73
+ label: string;
74
+ items: SidebarItem[];
75
+ /** Default open state for the collapse toggle. Defaults to true. */
76
+ defaultOpen?: boolean;
77
+ }
78
+ export interface SidebarFooterCard {
79
+ type: "card";
80
+ title: string;
81
+ description: string;
82
+ /** Optional CTA at the bottom of the card. */
83
+ action?: {
84
+ label: string;
85
+ href: string;
86
+ };
87
+ /** Optional progress bar (0..1). Useful for "used space" cards. */
88
+ progress?: number;
89
+ }
90
+ export interface SidebarFooterCustom {
91
+ type: "custom";
92
+ /** Component id registered via `defineStudioExtensions({ layouts: { footer: { id: ... }}})`. */
93
+ componentId: string;
94
+ }
95
+ export type SidebarFooter = SidebarFooterCard | SidebarFooterCustom | null;
96
+ export interface SidebarConfig {
97
+ sections: SidebarSection[];
98
+ footer?: SidebarFooter;
99
+ /** Org switcher button at the top. Pass `null` to hide. */
100
+ orgSwitcher?: {
101
+ items: {
102
+ id: string;
103
+ label: string;
104
+ }[];
105
+ } | null;
106
+ /** Show the collapse-to-icons toggle. Defaults to true. */
107
+ collapsible?: boolean;
108
+ }
109
+ /** Built-in cell renderer kinds. Custom renderers register an id under `componentId`. */
110
+ export type RendererKind = "text" | "avatar" | "badge" | "date" | "link" | "boolean" | "number" | "json" | "custom";
111
+ export interface RendererText {
112
+ kind: "text";
113
+ /** Truncate long strings to this many chars. Default 80. */
114
+ truncate?: number;
115
+ /** Render as monospace. */
116
+ mono?: boolean;
117
+ }
118
+ export interface RendererAvatar {
119
+ kind: "avatar";
120
+ /** Field on the row holding the image URL. If omitted, initials only. */
121
+ imageField?: string;
122
+ /** Field holding the secondary line under the name. */
123
+ subtitleField?: string;
124
+ /** Source field for the displayed name. Defaults to the column field. */
125
+ nameField?: string;
126
+ }
127
+ export interface RendererBadge {
128
+ kind: "badge";
129
+ /**
130
+ * Map of value → variant. Variants: green, red, amber, blue, gray.
131
+ * Unknown values render gray. Use `*` as a fallback key.
132
+ */
133
+ variants?: Record<string, "green" | "red" | "amber" | "blue" | "gray">;
134
+ /** Render a leading dot before the label. Defaults to true. */
135
+ dot?: boolean;
136
+ }
137
+ export interface RendererDate {
138
+ kind: "date";
139
+ /** "relative" → "3 days ago"; "absolute" → "May 3, 2026"; "iso" → raw. */
140
+ format?: "relative" | "absolute" | "iso";
141
+ }
142
+ export interface RendererLink {
143
+ kind: "link";
144
+ /**
145
+ * Template for the href. `{value}` and `{row.field}` are interpolated.
146
+ * Example: `"mailto:{value}"` or `"/users/{row.id}"`.
147
+ */
148
+ href: string;
149
+ external?: boolean;
150
+ }
151
+ export interface RendererBoolean {
152
+ kind: "boolean";
153
+ trueLabel?: string;
154
+ falseLabel?: string;
155
+ }
156
+ export interface RendererNumber {
157
+ kind: "number";
158
+ /** "decimal" | "percent" | "currency" | "bytes". Default "decimal". */
159
+ style?: "decimal" | "percent" | "currency" | "bytes";
160
+ /** ISO currency code when style="currency". */
161
+ currency?: string;
162
+ /** Locale for Intl.NumberFormat. Default browser locale. */
163
+ locale?: string;
164
+ }
165
+ export interface RendererJson {
166
+ kind: "json";
167
+ /** Truncate JSON preview length. Default 60. */
168
+ truncate?: number;
169
+ }
170
+ export interface RendererCustom {
171
+ kind: "custom";
172
+ /** Id registered via `defineStudioExtensions({ renderers: { ... } })`. */
173
+ componentId: string;
174
+ }
175
+ export type ColumnRenderer = RendererText | RendererAvatar | RendererBadge | RendererDate | RendererLink | RendererBoolean | RendererNumber | RendererJson | RendererCustom;
176
+ export interface ColumnConfig {
177
+ /** Field name on the row. Supports dotted paths (`profile.name`). */
178
+ field: string;
179
+ /** Header label. Defaults to humanized field name. */
180
+ label?: string;
181
+ /** Display order in the table. Lower = earlier. Defaults to definition order. */
182
+ order?: number;
183
+ /** Hide column by default; user can toggle in the column picker. */
184
+ hidden?: boolean;
185
+ /** Server-side sort supported. */
186
+ sortable?: boolean;
187
+ /** Include in the search input's matched fields. */
188
+ searchable?: boolean;
189
+ /** Show a filter chip in the toolbar. */
190
+ filterable?: boolean | {
191
+ options?: {
192
+ label: string;
193
+ value: unknown;
194
+ }[];
195
+ };
196
+ /** Cell renderer config. Defaults to text. */
197
+ renderer?: ColumnRenderer;
198
+ /** Width hint (CSS, e.g. "200px" or "30%"). */
199
+ width?: string;
200
+ /** Right-align the column (good for numbers). */
201
+ align?: "left" | "center" | "right";
202
+ }
203
+ export interface BulkAction {
204
+ id: string;
205
+ label: string;
206
+ icon?: IconName;
207
+ /** Built-in action ids: "delete" | "export". Otherwise resolved against extensions. */
208
+ kind?: "delete" | "export" | "custom";
209
+ /** Confirmation dialog message. */
210
+ confirm?: string;
211
+ /** Hide unless viewer is_admin. */
212
+ requiresAdmin?: boolean;
213
+ }
214
+ export interface RowAction {
215
+ id: string;
216
+ label: string;
217
+ icon?: IconName;
218
+ kind?: "delete" | "edit" | "view" | "custom";
219
+ confirm?: string;
220
+ requiresAdmin?: boolean;
221
+ }
222
+ export interface ResourceListConfig {
223
+ /** Columns. If omitted, columns are inferred from the manifest entity fields. */
224
+ columns?: ColumnConfig[];
225
+ /** Search box in the toolbar. Defaults to true if any column is searchable. */
226
+ searchable?: boolean;
227
+ /** Filters dropdown in the toolbar. Defaults to true if any column is filterable. */
228
+ filterable?: boolean;
229
+ /** Bulk action menu (shown when rows are selected). */
230
+ bulkActions?: BulkAction[];
231
+ /** Per-row trailing action menu. */
232
+ rowActions?: RowAction[];
233
+ /** Default sort: field + direction. */
234
+ defaultSort?: {
235
+ field: string;
236
+ order: "asc" | "desc";
237
+ };
238
+ /** Page size options. Default [10, 25, 50, 100]. */
239
+ pageSizes?: number[];
240
+ /** Initial page size. Default 10. */
241
+ defaultPageSize?: number;
242
+ }
243
+ export interface ResourceConfig {
244
+ /** Sidebar / breadcrumb label override. Defaults to entity name. */
245
+ label?: string;
246
+ /** Plural label for headings ("Users" vs "User"). Defaults to label + "s". */
247
+ pluralLabel?: string;
248
+ icon?: IconName;
249
+ /** Hide from the auto-derived sidebar. The user's explicit sidebar config still wins. */
250
+ hidden?: boolean;
251
+ list?: ResourceListConfig;
252
+ }
253
+ export interface PageConfig {
254
+ /** Optional subtitle under the page header. */
255
+ subtitle?: string;
256
+ /** Custom component id (extensions). Required for non-built-in ids. */
257
+ componentId?: string;
258
+ }
259
+ export interface StudioConfig {
260
+ brand?: BrandConfig;
261
+ theme?: ThemeConfig;
262
+ sidebar?: SidebarConfig;
263
+ resources?: Record<string, ResourceConfig>;
264
+ pages?: Record<string, PageConfig>;
265
+ /**
266
+ * When true, the Studio will dynamic-import `/studio/extensions.js` at
267
+ * boot. The CLI sets this automatically when `studio.entry.tsx` is
268
+ * present in the project, so users rarely set it explicitly.
269
+ */
270
+ hasExtensions?: boolean;
271
+ /**
272
+ * URL to send unauthenticated callers to when they hit `/studio`.
273
+ * Lets a host app (Pylon Cloud, an enterprise dashboard) point the
274
+ * Studio gate at its own email/password login page instead of the
275
+ * built-in `/studio/login` admin-token form.
276
+ *
277
+ * The framework appends `?next=/studio` so the host app can redirect
278
+ * back after sign-in. Authenticated-but-not-admin users still see
279
+ * the framework's "access denied" page (no point sending them back
280
+ * to a login they're already past).
281
+ *
282
+ * Example: `loginUrl: "/login"` — www.pylonsync.com handles `/login`
283
+ * at the dashboard, and the user's existing session cookie lifts
284
+ * them to admin via `auth.user.adminField` on the way back.
285
+ */
286
+ loginUrl?: string;
287
+ }
288
+ /**
289
+ * Identity helper for `studio.config.ts`. Provides type checking + lets
290
+ * the SDK evolve the shape under one declaration. The returned object is
291
+ * what the CLI serializes to JSON.
292
+ *
293
+ * ```ts
294
+ * // studio.config.ts
295
+ * import { defineStudioConfig } from "@pylon/sdk";
296
+ * export default defineStudioConfig({
297
+ * brand: { name: "Acme" },
298
+ * theme: { accent: "emerald", appearance: "dark" },
299
+ * sidebar: { sections: [...] },
300
+ * resources: { User: { list: { columns: [...] } } },
301
+ * });
302
+ * ```
303
+ *
304
+ * The CLI's bun runner expects the file's *default export* to be the
305
+ * config object, OR the module to print it via `console.log`. The
306
+ * recommended pattern is `console.log(JSON.stringify(defineStudioConfig({...})))`
307
+ * — the `pylon-studio-config.mjs` shim added by `pylon dev` does this
308
+ * automatically when you `export default`.
309
+ */
310
+ export declare function defineStudioConfig(config: StudioConfig): StudioConfig;
311
+ /**
312
+ * Component shape for column renderers. The Studio passes the cell value
313
+ * + the full row + the column config; the renderer returns a React node.
314
+ *
315
+ * Kept as `unknown` here (rather than the actual React types) so this
316
+ * module stays usable from non-React contexts — `studio.config.ts`
317
+ * doesn't import React. The web shell narrows these to the right
318
+ * function signatures at registration time.
319
+ */
320
+ export interface StudioCellRendererProps {
321
+ value: unknown;
322
+ row: Record<string, unknown>;
323
+ column: ColumnConfig;
324
+ }
325
+ export interface StudioPageProps {
326
+ /** Manifest passed to every custom page so they can introspect entities. */
327
+ manifest: unknown;
328
+ /** Auth context — `me`, `is_admin`, etc. */
329
+ auth: unknown;
330
+ /** Imperative `api()` helper, same one built-in pages use. */
331
+ api: <T = unknown>(path: string, opts?: unknown) => Promise<T>;
332
+ }
333
+ /** Registry shape — what `studio.entry.tsx` exports / registers. */
334
+ export interface StudioExtensions {
335
+ renderers?: Record<string, unknown>;
336
+ pages?: Record<string, unknown>;
337
+ layouts?: {
338
+ footer?: unknown;
339
+ headerActions?: unknown;
340
+ };
341
+ }
342
+ /**
343
+ * Identity helper for `studio.entry.tsx`. The user passes their component
344
+ * map and exports the result as default. The Studio shell imports the
345
+ * bundle and reads either `default` or the `registerStudioExtensions`
346
+ * global, whichever the user used.
347
+ *
348
+ * ```tsx
349
+ * // studio.entry.tsx
350
+ * import { defineStudioExtensions } from "@pylon/sdk";
351
+ * import { CurrencyCell } from "./renderers/currency";
352
+ * import { Dashboard } from "./pages/dashboard";
353
+ *
354
+ * export default defineStudioExtensions({
355
+ * renderers: { currency: CurrencyCell },
356
+ * pages: { dashboard: Dashboard },
357
+ * });
358
+ * ```
359
+ */
360
+ export declare function defineStudioExtensions(ext: StudioExtensions): StudioExtensions;
package/package.json CHANGED
@@ -3,12 +3,17 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.292",
6
+ "version": "0.3.293",
7
7
  "type": "module",
8
- "main": "src/index.ts",
9
- "types": "src/index.ts",
8
+ "main": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
10
  "scripts": {
11
- "build": "tsc -p tsconfig.json --noEmit",
12
- "check": "tsc -p tsconfig.json --noEmit"
13
- }
11
+ "build": "tsc -p tsconfig.build.json",
12
+ "check": "tsc -p tsconfig.json --noEmit",
13
+ "prepack": "bun run build"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "dist"
18
+ ]
14
19
  }
package/src/index.ts CHANGED
@@ -665,6 +665,39 @@ export interface ManifestCron {
665
665
  description?: string;
666
666
  }
667
667
 
668
+ /** A self-hosted web font. Declared via `font({...})`; at build the runtime
669
+ * fetches the family from Google Fonts, self-hosts the woff2 same-origin,
670
+ * generates `@font-face` + a size-adjusted fallback face (zero layout shift),
671
+ * and auto-injects the preload + faces into every SSR page's `<head>`. The app
672
+ * references the font through the CSS `variable`. */
673
+ export interface ManifestFont {
674
+ /** Google Fonts family name, e.g. "Geist" or "Inter". */
675
+ family: string;
676
+ /** CSS custom property the app uses to apply the font, e.g. "--font-geist".
677
+ * Set `font-family: var(--font-geist)` (it resolves to the family + the
678
+ * size-adjusted fallback + your `fallback` stack). */
679
+ variable: string;
680
+ /** Weights to load — specific (`["400","500","700"]`) or a CSS2 range
681
+ * (`["300..700"]`). Defaults to `["400"]`. */
682
+ weights?: string[];
683
+ /** Styles to load. Defaults to `["normal"]`. */
684
+ styles?: ("normal" | "italic")[];
685
+ /** Unicode subsets, e.g. `["latin"]`. Defaults to `["latin"]`. */
686
+ subsets?: string[];
687
+ /** CSS `font-display`. Defaults to `"swap"`. */
688
+ display?: "auto" | "block" | "swap" | "fallback" | "optional";
689
+ /** Emit `<link rel="preload">` for the font files so the browser fetches
690
+ * them early. Defaults to `true`. */
691
+ preload?: boolean;
692
+ /** Fallback stack appended after the family + the adjusted fallback face,
693
+ * e.g. `["system-ui", "sans-serif"]`. Defaults to `["sans-serif"]`. */
694
+ fallback?: string[];
695
+ /** Generate a size-adjusted fallback `@font-face` (matching x-height +
696
+ * metrics) so there's no layout shift when the web font swaps in. Defaults
697
+ * to `true`. Serialized snake_case to match the kernel manifest wire shape. */
698
+ adjust_font_fallback?: boolean;
699
+ }
700
+
668
701
  export interface AppManifest {
669
702
  manifest_version: number;
670
703
  name: string;
@@ -682,6 +715,9 @@ export interface AppManifest {
682
715
  connections?: ManifestConnection[];
683
716
  /** Recurring jobs — run a function on a cron schedule. */
684
717
  crons?: ManifestCron[];
718
+ /** Self-hosted web fonts. Fetched + self-hosted at build; preload +
719
+ * `@font-face` auto-injected into the SSR `<head>`. */
720
+ fonts?: ManifestFont[];
685
721
  }
686
722
 
687
723
  /**
@@ -725,6 +761,49 @@ export function cron(
725
761
  };
726
762
  }
727
763
 
764
+ /**
765
+ * Declare a self-hosted web font (Google Fonts). At build, Pylon fetches the
766
+ * family, self-hosts the woff2 same-origin, generates `@font-face` + a
767
+ * size-adjusted fallback face, and auto-injects the preload + faces into every
768
+ * SSR page's `<head>` — no render-blocking third-party request, no layout shift.
769
+ *
770
+ * ```ts
771
+ * buildManifest({
772
+ * // ...
773
+ * fonts: [
774
+ * font({ family: "Geist", variable: "--font-sans", weights: ["300..700"], subsets: ["latin"] }),
775
+ * font({ family: "Geist Mono", variable: "--font-mono", weights: ["400..600"] }),
776
+ * ],
777
+ * });
778
+ * ```
779
+ *
780
+ * Then use it in CSS: `font-family: var(--font-sans);` (resolves to the family,
781
+ * the zero-CLS fallback, then your `fallback` stack).
782
+ */
783
+ export function font(opts: {
784
+ family: string;
785
+ variable: string;
786
+ weights?: string[];
787
+ styles?: ("normal" | "italic")[];
788
+ subsets?: string[];
789
+ display?: "auto" | "block" | "swap" | "fallback" | "optional";
790
+ preload?: boolean;
791
+ fallback?: string[];
792
+ adjustFontFallback?: boolean;
793
+ }): ManifestFont {
794
+ return {
795
+ family: opts.family,
796
+ variable: opts.variable,
797
+ weights: opts.weights ?? ["400"],
798
+ styles: opts.styles ?? ["normal"],
799
+ subsets: opts.subsets ?? ["latin"],
800
+ display: opts.display ?? "swap",
801
+ preload: opts.preload ?? true,
802
+ ...(opts.fallback ? { fallback: opts.fallback } : {}),
803
+ adjust_font_fallback: opts.adjustFontFallback ?? true,
804
+ };
805
+ }
806
+
728
807
  export function entitiesToManifest(
729
808
  entities: EntityDefinition[]
730
809
  ): ManifestEntity[] {
@@ -1411,6 +1490,7 @@ export function buildManifest(options: {
1411
1490
  llm?: ManifestLlmConfig;
1412
1491
  connections?: ManifestConnection[];
1413
1492
  crons?: ManifestCron[];
1493
+ fonts?: ManifestFont[];
1414
1494
  }): AppManifest {
1415
1495
  // Pull policies attached via the fluent `e.entity().policies(...)`
1416
1496
  // chain onto the top-level policies list. Without this, fluent
@@ -1458,6 +1538,9 @@ export function buildManifest(options: {
1458
1538
  ...(options.crons && options.crons.length > 0
1459
1539
  ? { crons: options.crons }
1460
1540
  : {}),
1541
+ ...(options.fonts && options.fonts.length > 0
1542
+ ? { fonts: options.fonts }
1543
+ : {}),
1461
1544
  };
1462
1545
  }
1463
1546
 
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "include": [
4
- "src"
5
- ]
6
- }
7
-