@rsc-kit/press 0.0.0-stage → 0.30.43

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.
Files changed (180) hide show
  1. package/README.md +117 -2
  2. package/dist/admin/actions.d.ts +30 -0
  3. package/dist/admin/actions.js +171 -0
  4. package/dist/admin/actions.js.map +1 -0
  5. package/dist/admin/index.d.ts +5 -0
  6. package/dist/admin/index.js +2 -0
  7. package/dist/admin/index.js.map +1 -0
  8. package/dist/admin/panel.d.ts +96 -0
  9. package/dist/admin/panel.js +354 -0
  10. package/dist/admin/panel.js.map +1 -0
  11. package/dist/admin/registry.d.ts +58 -0
  12. package/dist/admin/registry.js +90 -0
  13. package/dist/admin/registry.js.map +1 -0
  14. package/dist/admin/theme.d.ts +251 -0
  15. package/dist/admin/theme.js +2 -0
  16. package/dist/admin/theme.js.map +1 -0
  17. package/dist/admin/url.d.ts +24 -0
  18. package/dist/admin/url.js +45 -0
  19. package/dist/admin/url.js.map +1 -0
  20. package/dist/blocks.d.ts +46 -0
  21. package/dist/blocks.js +33 -0
  22. package/dist/blocks.js.map +1 -0
  23. package/dist/forms/actions.d.ts +7 -0
  24. package/dist/forms/actions.js +26 -0
  25. package/dist/forms/actions.js.map +1 -0
  26. package/dist/forms/form.d.ts +42 -0
  27. package/dist/forms/form.js +44 -0
  28. package/dist/forms/form.js.map +1 -0
  29. package/dist/forms/honeypot.d.ts +2 -0
  30. package/dist/forms/honeypot.js +3 -0
  31. package/dist/forms/honeypot.js.map +1 -0
  32. package/dist/forms/index.d.ts +3 -0
  33. package/dist/forms/index.js +2 -0
  34. package/dist/forms/index.js.map +1 -0
  35. package/dist/forms/registry.d.ts +10 -0
  36. package/dist/forms/registry.js +17 -0
  37. package/dist/forms/registry.js.map +1 -0
  38. package/dist/forms/theme.d.ts +29 -0
  39. package/dist/forms/theme.js +2 -0
  40. package/dist/forms/theme.js.map +1 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.js +8 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/installable.d.ts +173 -0
  45. package/dist/installable.js +490 -0
  46. package/dist/installable.js.map +1 -0
  47. package/dist/memory.d.ts +7 -0
  48. package/dist/memory.js +50 -0
  49. package/dist/memory.js.map +1 -0
  50. package/dist/pages/index.d.ts +2 -0
  51. package/dist/pages/index.js +2 -0
  52. package/dist/pages/index.js.map +1 -0
  53. package/dist/pages/pages.d.ts +78 -0
  54. package/dist/pages/pages.js +183 -0
  55. package/dist/pages/pages.js.map +1 -0
  56. package/dist/plugin.d.ts +51 -0
  57. package/dist/plugin.js +20 -0
  58. package/dist/plugin.js.map +1 -0
  59. package/dist/resource.d.ts +320 -0
  60. package/dist/resource.js +415 -0
  61. package/dist/resource.js.map +1 -0
  62. package/dist/shadcn/breadcrumbs.d.ts +6 -0
  63. package/dist/shadcn/breadcrumbs.js +21 -0
  64. package/dist/shadcn/breadcrumbs.js.map +1 -0
  65. package/dist/shadcn/components/ui/alert-dialog.d.ts +18 -0
  66. package/dist/shadcn/components/ui/alert-dialog.js +44 -0
  67. package/dist/shadcn/components/ui/alert-dialog.js.map +1 -0
  68. package/dist/shadcn/components/ui/avatar.d.ts +11 -0
  69. package/dist/shadcn/components/ui/avatar.js +25 -0
  70. package/dist/shadcn/components/ui/avatar.js.map +1 -0
  71. package/dist/shadcn/components/ui/badge.d.ts +7 -0
  72. package/dist/shadcn/components/ui/badge.js +34 -0
  73. package/dist/shadcn/components/ui/badge.js.map +1 -0
  74. package/dist/shadcn/components/ui/breadcrumb.d.ts +10 -0
  75. package/dist/shadcn/components/ui/breadcrumb.js +38 -0
  76. package/dist/shadcn/components/ui/breadcrumb.js.map +1 -0
  77. package/dist/shadcn/components/ui/button.d.ts +8 -0
  78. package/dist/shadcn/components/ui/button.js +35 -0
  79. package/dist/shadcn/components/ui/button.js.map +1 -0
  80. package/dist/shadcn/components/ui/card.d.ts +11 -0
  81. package/dist/shadcn/components/ui/card.js +26 -0
  82. package/dist/shadcn/components/ui/card.js.map +1 -0
  83. package/dist/shadcn/components/ui/checkbox.d.ts +3 -0
  84. package/dist/shadcn/components/ui/checkbox.js +10 -0
  85. package/dist/shadcn/components/ui/checkbox.js.map +1 -0
  86. package/dist/shadcn/components/ui/dropdown-menu.d.ts +29 -0
  87. package/dist/shadcn/components/ui/dropdown-menu.js +53 -0
  88. package/dist/shadcn/components/ui/dropdown-menu.js.map +1 -0
  89. package/dist/shadcn/components/ui/field.d.ts +24 -0
  90. package/dist/shadcn/components/ui/field.js +69 -0
  91. package/dist/shadcn/components/ui/field.js.map +1 -0
  92. package/dist/shadcn/components/ui/input.d.ts +3 -0
  93. package/dist/shadcn/components/ui/input.js +9 -0
  94. package/dist/shadcn/components/ui/input.js.map +1 -0
  95. package/dist/shadcn/components/ui/label.d.ts +3 -0
  96. package/dist/shadcn/components/ui/label.js +9 -0
  97. package/dist/shadcn/components/ui/label.js.map +1 -0
  98. package/dist/shadcn/components/ui/select.d.ts +15 -0
  99. package/dist/shadcn/components/ui/select.js +36 -0
  100. package/dist/shadcn/components/ui/select.js.map +1 -0
  101. package/dist/shadcn/components/ui/separator.d.ts +3 -0
  102. package/dist/shadcn/components/ui/separator.js +9 -0
  103. package/dist/shadcn/components/ui/separator.js.map +1 -0
  104. package/dist/shadcn/components/ui/sheet.d.ts +14 -0
  105. package/dist/shadcn/components/ui/sheet.js +39 -0
  106. package/dist/shadcn/components/ui/sheet.js.map +1 -0
  107. package/dist/shadcn/components/ui/sidebar.d.ts +63 -0
  108. package/dist/shadcn/components/ui/sidebar.js +257 -0
  109. package/dist/shadcn/components/ui/sidebar.js.map +1 -0
  110. package/dist/shadcn/components/ui/skeleton.d.ts +2 -0
  111. package/dist/shadcn/components/ui/skeleton.js +7 -0
  112. package/dist/shadcn/components/ui/skeleton.js.map +1 -0
  113. package/dist/shadcn/components/ui/sonner.d.ts +3 -0
  114. package/dist/shadcn/components/ui/sonner.js +38 -0
  115. package/dist/shadcn/components/ui/sonner.js.map +1 -0
  116. package/dist/shadcn/components/ui/switch.d.ts +5 -0
  117. package/dist/shadcn/components/ui/switch.js +9 -0
  118. package/dist/shadcn/components/ui/switch.js.map +1 -0
  119. package/dist/shadcn/components/ui/table.d.ts +10 -0
  120. package/dist/shadcn/components/ui/table.js +30 -0
  121. package/dist/shadcn/components/ui/table.js.map +1 -0
  122. package/dist/shadcn/components/ui/textarea.d.ts +3 -0
  123. package/dist/shadcn/components/ui/textarea.js +8 -0
  124. package/dist/shadcn/components/ui/textarea.js.map +1 -0
  125. package/dist/shadcn/components/ui/tooltip.d.ts +6 -0
  126. package/dist/shadcn/components/ui/tooltip.js +18 -0
  127. package/dist/shadcn/components/ui/tooltip.js.map +1 -0
  128. package/dist/shadcn/data-table.d.ts +12 -0
  129. package/dist/shadcn/data-table.js +195 -0
  130. package/dist/shadcn/data-table.js.map +1 -0
  131. package/dist/shadcn/fields.d.ts +20 -0
  132. package/dist/shadcn/fields.js +179 -0
  133. package/dist/shadcn/fields.js.map +1 -0
  134. package/dist/shadcn/flash-toasts.d.ts +2 -0
  135. package/dist/shadcn/flash-toasts.js +11 -0
  136. package/dist/shadcn/flash-toasts.js.map +1 -0
  137. package/dist/shadcn/hooks/use-mobile.d.ts +1 -0
  138. package/dist/shadcn/hooks/use-mobile.js +16 -0
  139. package/dist/shadcn/hooks/use-mobile.js.map +1 -0
  140. package/dist/shadcn/index.d.ts +10 -0
  141. package/dist/shadcn/index.js +31 -0
  142. package/dist/shadcn/index.js.map +1 -0
  143. package/dist/shadcn/mode-toggle.d.ts +7 -0
  144. package/dist/shadcn/mode-toggle.js +52 -0
  145. package/dist/shadcn/mode-toggle.js.map +1 -0
  146. package/dist/shadcn/nav.d.ts +5 -0
  147. package/dist/shadcn/nav.js +19 -0
  148. package/dist/shadcn/nav.js.map +1 -0
  149. package/dist/shadcn/pages.d.ts +15 -0
  150. package/dist/shadcn/pages.js +59 -0
  151. package/dist/shadcn/pages.js.map +1 -0
  152. package/dist/shadcn/plugin-forms.d.ts +16 -0
  153. package/dist/shadcn/plugin-forms.js +25 -0
  154. package/dist/shadcn/plugin-forms.js.map +1 -0
  155. package/dist/shadcn/public-form.d.ts +8 -0
  156. package/dist/shadcn/public-form.js +26 -0
  157. package/dist/shadcn/public-form.js.map +1 -0
  158. package/dist/shadcn/resource-form.d.ts +19 -0
  159. package/dist/shadcn/resource-form.js +30 -0
  160. package/dist/shadcn/resource-form.js.map +1 -0
  161. package/dist/shadcn/styles.css +12 -0
  162. package/dist/shadcn/tenant-switcher.d.ts +3 -0
  163. package/dist/shadcn/tenant-switcher.js +17 -0
  164. package/dist/shadcn/tenant-switcher.js.map +1 -0
  165. package/dist/shadcn/theme-script.d.ts +7 -0
  166. package/dist/shadcn/theme-script.js +10 -0
  167. package/dist/shadcn/theme-script.js.map +1 -0
  168. package/dist/shadcn/user-menu.d.ts +3 -0
  169. package/dist/shadcn/user-menu.js +23 -0
  170. package/dist/shadcn/user-menu.js.map +1 -0
  171. package/dist/signing.d.ts +43 -0
  172. package/dist/signing.js +82 -0
  173. package/dist/signing.js.map +1 -0
  174. package/dist/tenant.d.ts +12 -0
  175. package/dist/tenant.js +29 -0
  176. package/dist/tenant.js.map +1 -0
  177. package/dist/uploads.d.ts +32 -0
  178. package/dist/uploads.js +28 -0
  179. package/dist/uploads.js.map +1 -0
  180. package/package.json +74 -4
@@ -0,0 +1,59 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from '@rsc-kit/core/Link';
3
+ import { LayoutDashboard, Plus, Table2 } from 'lucide-react';
4
+ import { Badge } from './components/ui/badge.js';
5
+ import { Button } from './components/ui/button.js';
6
+ import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './components/ui/card.js';
7
+ import { Separator } from './components/ui/separator.js';
8
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarProvider, SidebarRail, SidebarTrigger, } from './components/ui/sidebar.js';
9
+ import { Skeleton } from './components/ui/skeleton.js';
10
+ import { TooltipProvider } from './components/ui/tooltip.js';
11
+ import { FlashToasts } from './flash-toasts.js';
12
+ import { ModeToggle } from './mode-toggle.js';
13
+ import { ActionButton, ManifestForm, SecretField } from './plugin-forms.js';
14
+ import { ResourceForm } from './resource-form.js';
15
+ import { themeScript } from './theme-script.js';
16
+ /** shadcn's sidebar layout: the sidebar, a header with the trigger and breadcrumbs, the page. */
17
+ export function Shell({ title, brand, nav, user, crumbs, modeToggle, children }) {
18
+ return (_jsxs(TooltipProvider, { children: [modeToggle && _jsx("script", { dangerouslySetInnerHTML: { __html: themeScript } }), _jsxs(SidebarProvider, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: brand }), _jsx(SidebarContent, { children: _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: title }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: nav }) })] }) }), user && _jsx(SidebarFooter, { children: user }), _jsx(SidebarRail, {})] }), _jsxs(SidebarInset, { children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b px-4", children: [_jsx(SidebarTrigger, { className: "-ml-1" }), _jsx(Separator, { orientation: "vertical", className: "mr-2 data-[orientation=vertical]:h-4" }), crumbs, _jsx("div", { className: "ml-auto", children: modeToggle && _jsx(ModeToggle, {}) })] }), _jsx("main", { className: "flex flex-1 flex-col gap-6 p-4 md:p-6", children: children })] })] }), _jsx(FlashToasts, {})] }));
19
+ }
20
+ export function Brand({ href, title, logo }) {
21
+ return (_jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { size: "lg", render: _jsx(Link, { href: href }), children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground [&_svg]:size-4", children: logo ?? _jsx(LayoutDashboard, {}) }), _jsx("span", { className: "truncate font-semibold", children: title })] }) }) }));
22
+ }
23
+ function Heading({ title, description, action }) {
24
+ return (_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: title }), description && _jsx("p", { className: "text-sm text-muted-foreground", children: description })] }), action] }));
25
+ }
26
+ export function Dashboard({ description, counts, widgets }) {
27
+ return (_jsxs(_Fragment, { children: [_jsx(Heading, { title: "Dashboard", description: description }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4", children: counts.map(({ name, label, href, total, icon }) => (_jsx(Link, { href: href, className: "rounded-xl outline-none focus-visible:ring-3 focus-visible:ring-ring/50", children: _jsx(Card, { className: "transition-colors hover:bg-muted/50", children: _jsxs(CardHeader, { children: [_jsx(CardDescription, { children: label }), _jsx(CardTitle, { className: "text-3xl font-semibold tabular-nums", children: total === null ? '—' : total.toLocaleString('en') }), _jsx(CardAction, { className: "text-muted-foreground [&_svg]:size-4", children: icon ?? _jsx(Table2, {}) })] }) }) }, name))) }), widgets.length > 0 && (_jsx("div", { className: "grid gap-4 lg:grid-cols-2", children: widgets.map(({ key, title, description: about, content }) => (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: title }), about && _jsx(CardDescription, { children: about })] }), _jsx(CardContent, { children: content })] }, key))) }))] }));
28
+ }
29
+ export function Index({ title, description, create, table }) {
30
+ return (_jsxs(_Fragment, { children: [_jsx(Heading, { title: title, description: description, action: create && (_jsxs(Button, { nativeButton: false, render: _jsx(Link, { href: create.href }), children: [_jsx(Plus, {}), create.label] })) }), table] }));
31
+ }
32
+ export function Form({ title, description, ...form }) {
33
+ return (_jsxs(_Fragment, { children: [_jsx(Heading, { title: title, description: description }), _jsx(ResourceForm, { ...form })] }));
34
+ }
35
+ export function Plugins({ description, allowed, review, reviewing, installed }) {
36
+ return (_jsxs(_Fragment, { children: [_jsx(Heading, { title: "Plugins", description: description }), reviewing && 'problem' in reviewing && (_jsx(Card, { className: "max-w-2xl", children: _jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "That plugin cannot be installed" }), _jsx(CardDescription, { children: reviewing.problem })] }) })), reviewing && 'install' in reviewing && (_jsxs(Card, { className: "max-w-2xl", children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { children: ["Install ", reviewing.label, reviewing.version && _jsx("span", { className: "ml-2 font-normal text-muted-foreground", children: reviewing.version })] }), _jsx(CardDescription, { children: reviewing.description })] }), _jsxs(CardContent, { className: "grid gap-4 text-sm", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("span", { className: "font-medium", children: "It adds" }), _jsx("span", { className: "text-muted-foreground", children: reviewing.adds.join(', ') })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx("span", { className: "font-medium", children: "It can" }), reviewing.can.length === 0 ? (_jsx("span", { className: "text-muted-foreground", children: "See nothing of yours beyond its own records." })) : (_jsx("ul", { className: "list-disc space-y-1 pl-5 text-muted-foreground", children: reviewing.can.map((words) => (_jsx("li", { children: words }, words))) }))] }), _jsxs("div", { className: "grid gap-1", children: [_jsx("span", { className: "font-medium", children: "It runs at" }), _jsx("code", { className: "font-mono text-xs text-muted-foreground", children: reviewing.endpoint })] })] }), _jsxs(CardFooter, { className: "gap-2", children: [_jsx(ActionButton, { action: reviewing.install, label: "Install", busy: "Installing\u2026" }), _jsx(Button, { variant: "ghost", size: "sm", nativeButton: false, render: _jsx(Link, { href: reviewing.cancel }), children: "Cancel" })] })] })), installed.length > 0 && (_jsx("div", { className: "grid gap-4 lg:grid-cols-2", children: installed.map((plugin) => (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: plugin.label }), _jsx(CardDescription, { children: plugin.description }), plugin.uninstall && (_jsx(CardAction, { children: _jsx(ActionButton, { action: plugin.uninstall, label: "Uninstall", busy: "Removing\u2026", variant: "outline" }) }))] }), _jsxs(CardContent, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [plugin.resources.map((label) => (_jsx(Badge, { variant: "secondary", children: label }, label))), plugin.scopes.map((scope) => (_jsx(Badge, { variant: "outline", children: scope }, scope)))] }), plugin.secret && (_jsxs("div", { className: "grid gap-3 rounded-lg border p-3", children: [_jsx("p", { className: "text-sm", children: "Give the plugin these, so it can tell this panel's calls from anyone else's. The secret is shown until you confirm." }), _jsx(SecretField, { installation: plugin.secret.installation, secret: plugin.secret.secret }), _jsx("div", { children: _jsx(ActionButton, { action: plugin.secret.copied, label: "I've copied it", busy: "Saving\u2026", variant: "outline" }) })] }))] })] }, plugin.id))) })), allowed && !(reviewing && 'install' in reviewing) && (_jsxs(Card, { className: "max-w-2xl", children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Install a plugin" }), _jsx(CardDescription, { children: installed.length === 0 ? 'Nothing is installed yet.' : 'Add another.' })] }), _jsx(CardContent, { children: _jsx(ManifestForm, { action: review }) })] }))] }));
37
+ }
38
+ const money = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD' });
39
+ const date = new Intl.DateTimeFormat('en', { dateStyle: 'medium' });
40
+ export function Cell({ format, value }) {
41
+ switch (format) {
42
+ case 'money':
43
+ return _jsx("span", { className: "tabular-nums", children: money.format(Number(value)) });
44
+ case 'badge':
45
+ return value === null || value === undefined || value === '' ? null : _jsx(Badge, { variant: "outline", children: String(value) });
46
+ case 'boolean':
47
+ return _jsx(Badge, { variant: value ? 'default' : 'outline', children: value ? 'Yes' : 'No' });
48
+ case 'date': {
49
+ const at = value instanceof Date ? value : new Date(String(value));
50
+ return Number.isNaN(at.getTime()) ? String(value ?? '') : _jsx("time", { dateTime: at.toISOString(), children: date.format(at) });
51
+ }
52
+ }
53
+ }
54
+ export function Loading({ kind }) {
55
+ if (kind === 'menu')
56
+ return _jsx(SidebarMenuSkeleton, { showIcon: true });
57
+ return (_jsxs("div", { className: "flex flex-col gap-6", "aria-busy": "true", "aria-label": "Loading", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-48" }), _jsx(Skeleton, { className: "h-4 w-72" })] }), kind === 'dashboard' && (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4", children: [0, 1, 2, 3].map((index) => (_jsx(Skeleton, { className: "h-28 rounded-xl" }, index))) })), kind === 'table' && (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-8 w-full sm:max-w-xs" }), _jsx(Skeleton, { className: "h-80 w-full rounded-lg" })] })), kind === 'form' && _jsx(Skeleton, { className: "h-96 w-full max-w-2xl rounded-xl" })] }));
58
+ }
59
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/shadcn/pages.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react'\nimport Link from '@rsc-kit/core/Link'\nimport { LayoutDashboard, Plus, Table2 } from 'lucide-react'\nimport type { BrandProps, DashboardProps, FormProps, IndexProps, PluginsProps, ShellProps } from '../admin/theme.js'\nimport { Badge } from './components/ui/badge.js'\nimport { Button } from './components/ui/button.js'\nimport { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './components/ui/card.js'\nimport { Separator } from './components/ui/separator.js'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInset,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSkeleton,\n SidebarProvider,\n SidebarRail,\n SidebarTrigger,\n} from './components/ui/sidebar.js'\nimport { Skeleton } from './components/ui/skeleton.js'\nimport { TooltipProvider } from './components/ui/tooltip.js'\nimport { FlashToasts } from './flash-toasts.js'\nimport { ModeToggle } from './mode-toggle.js'\nimport { ActionButton, ManifestForm, SecretField } from './plugin-forms.js'\nimport { ResourceForm } from './resource-form.js'\nimport { themeScript } from './theme-script.js'\n\n/** shadcn's sidebar layout: the sidebar, a header with the trigger and breadcrumbs, the page. */\nexport function Shell({ title, brand, nav, user, crumbs, modeToggle, children }: ShellProps) {\n return (\n <TooltipProvider>\n {modeToggle && <script dangerouslySetInnerHTML={{ __html: themeScript }} />}\n <SidebarProvider>\n <Sidebar collapsible=\"icon\">\n <SidebarHeader>{brand}</SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>{title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>{nav}</SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n {user && <SidebarFooter>{user}</SidebarFooter>}\n <SidebarRail />\n </Sidebar>\n <SidebarInset>\n <header className=\"flex h-14 shrink-0 items-center gap-2 border-b px-4\">\n <SidebarTrigger className=\"-ml-1\" />\n <Separator orientation=\"vertical\" className=\"mr-2 data-[orientation=vertical]:h-4\" />\n {crumbs}\n <div className=\"ml-auto\">{modeToggle && <ModeToggle />}</div>\n </header>\n <main className=\"flex flex-1 flex-col gap-6 p-4 md:p-6\">{children}</main>\n </SidebarInset>\n </SidebarProvider>\n <FlashToasts />\n </TooltipProvider>\n )\n}\n\nexport function Brand({ href, title, logo }: BrandProps) {\n return (\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" render={<Link href={href as never} />}>\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground [&_svg]:size-4\">\n {logo ?? <LayoutDashboard />}\n </div>\n <span className=\"truncate font-semibold\">{title}</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n )\n}\n\nfunction Heading({ title, description, action }: { title: string; description?: string | undefined; action?: ReactNode }) {\n return (\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl font-semibold tracking-tight\">{title}</h1>\n {description && <p className=\"text-sm text-muted-foreground\">{description}</p>}\n </div>\n {action}\n </div>\n )\n}\n\nexport function Dashboard({ description, counts, widgets }: DashboardProps) {\n return (\n <>\n <Heading title=\"Dashboard\" description={description} />\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {counts.map(({ name, label, href, total, icon }) => (\n <Link key={name} href={href as never} className=\"rounded-xl outline-none focus-visible:ring-3 focus-visible:ring-ring/50\">\n <Card className=\"transition-colors hover:bg-muted/50\">\n <CardHeader>\n <CardDescription>{label}</CardDescription>\n <CardTitle className=\"text-3xl font-semibold tabular-nums\">{total === null ? '—' : total.toLocaleString('en')}</CardTitle>\n <CardAction className=\"text-muted-foreground [&_svg]:size-4\">{icon ?? <Table2 />}</CardAction>\n </CardHeader>\n </Card>\n </Link>\n ))}\n </div>\n {widgets.length > 0 && (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {widgets.map(({ key, title, description: about, content }) => (\n <Card key={key}>\n <CardHeader>\n <CardTitle>{title}</CardTitle>\n {about && <CardDescription>{about}</CardDescription>}\n </CardHeader>\n <CardContent>{content}</CardContent>\n </Card>\n ))}\n </div>\n )}\n </>\n )\n}\n\nexport function Index({ title, description, create, table }: IndexProps) {\n return (\n <>\n <Heading\n title={title}\n description={description}\n action={\n create && (\n <Button nativeButton={false} render={<Link href={create.href as never} />}>\n <Plus />\n {create.label}\n </Button>\n )\n }\n />\n {table}\n </>\n )\n}\n\nexport function Form({ title, description, ...form }: FormProps) {\n return (\n <>\n <Heading title={title} description={description} />\n <ResourceForm {...form} />\n </>\n )\n}\n\nexport function Plugins({ description, allowed, review, reviewing, installed }: PluginsProps) {\n return (\n <>\n <Heading title=\"Plugins\" description={description} />\n {reviewing && 'problem' in reviewing && (\n <Card className=\"max-w-2xl\">\n <CardHeader>\n <CardTitle>That plugin cannot be installed</CardTitle>\n <CardDescription>{reviewing.problem}</CardDescription>\n </CardHeader>\n </Card>\n )}\n {reviewing && 'install' in reviewing && (\n <Card className=\"max-w-2xl\">\n <CardHeader>\n <CardTitle>\n Install {reviewing.label}\n {reviewing.version && <span className=\"ml-2 font-normal text-muted-foreground\">{reviewing.version}</span>}\n </CardTitle>\n <CardDescription>{reviewing.description}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 text-sm\">\n <div className=\"grid gap-1\">\n <span className=\"font-medium\">It adds</span>\n <span className=\"text-muted-foreground\">{reviewing.adds.join(', ')}</span>\n </div>\n <div className=\"grid gap-1\">\n <span className=\"font-medium\">It can</span>\n {reviewing.can.length === 0 ? (\n <span className=\"text-muted-foreground\">See nothing of yours beyond its own records.</span>\n ) : (\n <ul className=\"list-disc space-y-1 pl-5 text-muted-foreground\">\n {reviewing.can.map((words) => (\n <li key={words}>{words}</li>\n ))}\n </ul>\n )}\n </div>\n <div className=\"grid gap-1\">\n <span className=\"font-medium\">It runs at</span>\n <code className=\"font-mono text-xs text-muted-foreground\">{reviewing.endpoint}</code>\n </div>\n </CardContent>\n <CardFooter className=\"gap-2\">\n <ActionButton action={reviewing.install} label=\"Install\" busy=\"Installing…\" />\n <Button variant=\"ghost\" size=\"sm\" nativeButton={false} render={<Link href={reviewing.cancel as never} />}>\n Cancel\n </Button>\n </CardFooter>\n </Card>\n )}\n {installed.length > 0 && (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n {installed.map((plugin) => (\n <Card key={plugin.id}>\n <CardHeader>\n <CardTitle>{plugin.label}</CardTitle>\n <CardDescription>{plugin.description}</CardDescription>\n {plugin.uninstall && (\n <CardAction>\n <ActionButton action={plugin.uninstall} label=\"Uninstall\" busy=\"Removing…\" variant=\"outline\" />\n </CardAction>\n )}\n </CardHeader>\n <CardContent className=\"grid gap-4\">\n <div className=\"flex flex-wrap gap-1.5\">\n {plugin.resources.map((label) => (\n <Badge key={label} variant=\"secondary\">\n {label}\n </Badge>\n ))}\n {plugin.scopes.map((scope) => (\n <Badge key={scope} variant=\"outline\">\n {scope}\n </Badge>\n ))}\n </div>\n {plugin.secret && (\n <div className=\"grid gap-3 rounded-lg border p-3\">\n <p className=\"text-sm\">Give the plugin these, so it can tell this panel's calls from anyone else's. The secret is shown until you confirm.</p>\n <SecretField installation={plugin.secret.installation} secret={plugin.secret.secret} />\n <div>\n <ActionButton action={plugin.secret.copied} label=\"I've copied it\" busy=\"Saving…\" variant=\"outline\" />\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n {allowed && !(reviewing && 'install' in reviewing) && (\n <Card className=\"max-w-2xl\">\n <CardHeader>\n <CardTitle>Install a plugin</CardTitle>\n <CardDescription>{installed.length === 0 ? 'Nothing is installed yet.' : 'Add another.'}</CardDescription>\n </CardHeader>\n <CardContent>\n <ManifestForm action={review} />\n </CardContent>\n </Card>\n )}\n </>\n )\n}\n\nconst money = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD' })\nconst date = new Intl.DateTimeFormat('en', { dateStyle: 'medium' })\n\nexport function Cell({ format, value }: { format: 'money' | 'badge' | 'boolean' | 'date'; value: unknown }) {\n switch (format) {\n case 'money':\n return <span className=\"tabular-nums\">{money.format(Number(value))}</span>\n case 'badge':\n return value === null || value === undefined || value === '' ? null : <Badge variant=\"outline\">{String(value)}</Badge>\n case 'boolean':\n return <Badge variant={value ? 'default' : 'outline'}>{value ? 'Yes' : 'No'}</Badge>\n case 'date': {\n const at = value instanceof Date ? value : new Date(String(value))\n\n return Number.isNaN(at.getTime()) ? String(value ?? '') : <time dateTime={at.toISOString()}>{date.format(at)}</time>\n }\n }\n}\n\nexport function Loading({ kind }: { kind: 'dashboard' | 'table' | 'form' | 'menu' }) {\n if (kind === 'menu') return <SidebarMenuSkeleton showIcon />\n\n return (\n <div className=\"flex flex-col gap-6\" aria-busy=\"true\" aria-label=\"Loading\">\n <div className=\"space-y-2\">\n <Skeleton className=\"h-7 w-48\" />\n <Skeleton className=\"h-4 w-72\" />\n </div>\n {kind === 'dashboard' && (\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n {[0, 1, 2, 3].map((index) => (\n <Skeleton key={index} className=\"h-28 rounded-xl\" />\n ))}\n </div>\n )}\n {kind === 'table' && (\n <>\n <Skeleton className=\"h-8 w-full sm:max-w-xs\" />\n <Skeleton className=\"h-80 w-full rounded-lg\" />\n </>\n )}\n {kind === 'form' && <Skeleton className=\"h-96 w-full max-w-2xl rounded-xl\" />}\n </div>\n )\n}\n"]}
@@ -0,0 +1,16 @@
1
+ /** A manifest url, sent for review. Its problems land under the field. */
2
+ export declare function ManifestForm({ action }: {
3
+ action: (form: FormData) => Promise<unknown>;
4
+ }): import("react").JSX.Element;
5
+ /** A button that runs one action: install, uninstall, done. */
6
+ export declare function ActionButton({ action, label, busy, variant, }: {
7
+ action: () => Promise<unknown>;
8
+ label: string;
9
+ busy: string;
10
+ variant?: 'default' | 'outline' | 'destructive' | 'ghost';
11
+ }): import("react").JSX.Element;
12
+ /** The secret, shown once, with a copy button. */
13
+ export declare function SecretField({ secret, installation }: {
14
+ secret: string;
15
+ installation: string;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from 'react';
4
+ import Form from '@rsc-kit/core/form';
5
+ import { Check, Copy } from 'lucide-react';
6
+ import { Button } from './components/ui/button.js';
7
+ import { Field, FieldDescription, FieldError, FieldLabel } from './components/ui/field.js';
8
+ import { Input } from './components/ui/input.js';
9
+ /** A manifest url, sent for review. Its problems land under the field. */
10
+ export function ManifestForm({ action }) {
11
+ return (_jsx(Form, { action: action, children: ({ pending, error }) => (_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start", children: [_jsxs(Field, { "data-invalid": !!error('manifest') || undefined, className: "flex-1", children: [_jsx(FieldLabel, { htmlFor: "plugin-manifest", className: "sr-only", children: "Manifest url" }), _jsx(Input, { id: "plugin-manifest", name: "manifest", type: "url", required: true, placeholder: "https://plugin.example.com/manifest.json", "aria-invalid": !!error('manifest') || undefined }), error('manifest') ? _jsx(FieldError, { children: error('manifest') }) : _jsx(FieldDescription, { children: "The plugin's manifest. You review what it asks for before anything is installed." })] }), _jsx(Button, { type: "submit", disabled: pending, children: pending ? 'Reading…' : 'Review' })] })) }));
12
+ }
13
+ /** A button that runs one action: install, uninstall, done. */
14
+ export function ActionButton({ action, label, busy, variant = 'default', }) {
15
+ return (_jsx(Form, { action: action, children: ({ pending }) => (_jsx(Button, { type: "submit", variant: variant, size: "sm", disabled: pending, children: pending ? busy : label })) }));
16
+ }
17
+ /** The secret, shown once, with a copy button. */
18
+ export function SecretField({ secret, installation }) {
19
+ const [copied, setCopied] = useState(false);
20
+ return (_jsxs("div", { className: "grid gap-2 text-sm", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("span", { className: "text-muted-foreground", children: "Installation" }), _jsx("code", { className: "rounded-md bg-muted px-2 py-1 font-mono text-xs break-all", children: installation })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx("span", { className: "text-muted-foreground", children: "Secret" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "min-w-0 flex-1 rounded-md bg-muted px-2 py-1 font-mono text-xs break-all", children: secret }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", "aria-label": "Copy the secret", onClick: () => void navigator.clipboard.writeText(secret).then(() => {
21
+ setCopied(true);
22
+ setTimeout(() => setCopied(false), 1500);
23
+ }), children: copied ? _jsx(Check, {}) : _jsx(Copy, {}) })] })] })] }));
24
+ }
25
+ //# sourceMappingURL=plugin-forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-forms.js","sourceRoot":"","sources":["../../src/shadcn/plugin-forms.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { useState } from 'react'\nimport Form from '@rsc-kit/core/form'\nimport { Check, Copy } from 'lucide-react'\nimport { Button } from './components/ui/button.js'\nimport { Field, FieldDescription, FieldError, FieldLabel } from './components/ui/field.js'\nimport { Input } from './components/ui/input.js'\n\n/** A manifest url, sent for review. Its problems land under the field. */\nexport function ManifestForm({ action }: { action: (form: FormData) => Promise<unknown> }) {\n return (\n <Form action={action}>\n {({ pending, error }) => (\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start\">\n <Field data-invalid={!!error('manifest') || undefined} className=\"flex-1\">\n <FieldLabel htmlFor=\"plugin-manifest\" className=\"sr-only\">\n Manifest url\n </FieldLabel>\n <Input id=\"plugin-manifest\" name=\"manifest\" type=\"url\" required placeholder=\"https://plugin.example.com/manifest.json\" aria-invalid={!!error('manifest') || undefined} />\n {error('manifest') ? <FieldError>{error('manifest')}</FieldError> : <FieldDescription>The plugin's manifest. You review what it asks for before anything is installed.</FieldDescription>}\n </Field>\n <Button type=\"submit\" disabled={pending}>\n {pending ? 'Reading…' : 'Review'}\n </Button>\n </div>\n )}\n </Form>\n )\n}\n\n/** A button that runs one action: install, uninstall, done. */\nexport function ActionButton({\n action,\n label,\n busy,\n variant = 'default',\n}: {\n action: () => Promise<unknown>\n label: string\n busy: string\n variant?: 'default' | 'outline' | 'destructive' | 'ghost'\n}) {\n return (\n <Form action={action}>\n {({ pending }) => (\n <Button type=\"submit\" variant={variant} size=\"sm\" disabled={pending}>\n {pending ? busy : label}\n </Button>\n )}\n </Form>\n )\n}\n\n/** The secret, shown once, with a copy button. */\nexport function SecretField({ secret, installation }: { secret: string; installation: string }) {\n const [copied, setCopied] = useState(false)\n\n return (\n <div className=\"grid gap-2 text-sm\">\n <div className=\"grid gap-1\">\n <span className=\"text-muted-foreground\">Installation</span>\n <code className=\"rounded-md bg-muted px-2 py-1 font-mono text-xs break-all\">{installation}</code>\n </div>\n <div className=\"grid gap-1\">\n <span className=\"text-muted-foreground\">Secret</span>\n <div className=\"flex items-center gap-2\">\n <code className=\"min-w-0 flex-1 rounded-md bg-muted px-2 py-1 font-mono text-xs break-all\">{secret}</code>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Copy the secret\"\n onClick={() =>\n void navigator.clipboard.writeText(secret).then(() => {\n setCopied(true)\n setTimeout(() => setCopied(false), 1500)\n })\n }\n >\n {copied ? <Check /> : <Copy />}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import type { PublicFormProps } from '../forms/theme.js';
2
+ /**
3
+ * A form anyone may fill in, drawn with the same fields as the panel's: the
4
+ * browser checks each one first, and the server's refusals land under the
5
+ * same fields. Once sent it says so in its place - or the action redirects.
6
+ * No card around it: it sits in the app's own page, which decides that.
7
+ */
8
+ export declare function PublicForm({ action, fields, submit, done, honeypot }: PublicFormProps): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef } from 'react';
4
+ import Form from '@rsc-kit/core/form';
5
+ import { CircleCheck } from 'lucide-react';
6
+ import { Button } from './components/ui/button.js';
7
+ import { FieldGroup } from './components/ui/field.js';
8
+ import { FieldList, useBrowserErrors } from './fields.js';
9
+ /**
10
+ * A form anyone may fill in, drawn with the same fields as the panel's: the
11
+ * browser checks each one first, and the server's refusals land under the
12
+ * same fields. Once sent it says so in its place - or the action redirects.
13
+ * No card around it: it sits in the app's own page, which decides that.
14
+ */
15
+ export function PublicForm({ action, fields, submit, done, honeypot }) {
16
+ const form = useRef(null);
17
+ const browser = useBrowserErrors(form);
18
+ return (_jsx(Form, { action: action, ref: form, children: ({ pending, error: server, formError, succeeded }) => {
19
+ if (succeeded && done !== undefined) {
20
+ return (_jsxs("p", { role: "status", className: "flex items-center gap-2 text-sm", children: [_jsx(CircleCheck, { className: "size-4 text-muted-foreground" }), done] }));
21
+ }
22
+ const error = (name) => browser[name] ?? server(name);
23
+ return (_jsxs(FieldGroup, { children: [_jsx(FieldList, { fields: fields, values: {}, error: error }), _jsx("div", { "aria-hidden": "true", className: "absolute -left-[9999px] size-px overflow-hidden", children: _jsx("input", { type: "text", name: honeypot, tabIndex: -1, autoComplete: "off", defaultValue: "" }) }), formError && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: formError })), _jsx("div", { children: _jsx(Button, { type: "submit", disabled: pending, children: pending ? 'Sending…' : submit }) })] }));
24
+ } }));
25
+ }
26
+ //# sourceMappingURL=public-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-form.js","sourceRoot":"","sources":["../../src/shadcn/public-form.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9B,OAAO,IAAI,MAAM,oBAAoB,CAAA;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAmB;IACpF,MAAM,IAAI,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAA;IAC1C,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;IAEtC,OAAO,CACL,KAAC,IAAI,IAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,YAC5B,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;YACpD,IAAI,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACpC,OAAO,CACL,aAAG,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,iCAAiC,aAC1D,KAAC,WAAW,IAAC,SAAS,EAAC,8BAA8B,GAAG,EACvD,IAAI,IACH,CACL,CAAA;YACH,CAAC;YAED,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAA;YAE7D,OAAO,CACL,MAAC,UAAU,eACT,KAAC,SAAS,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,GAAI,EAEvD,6BAAiB,MAAM,EAAC,SAAS,EAAC,iDAAiD,YACjF,gBAAO,IAAI,EAAC,MAAM,EAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,YAAY,EAAC,KAAK,EAAC,YAAY,EAAC,EAAE,GAAG,GAClF,EACL,SAAS,IAAI,CACZ,YAAG,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,0BAA0B,YACjD,SAAS,GACR,CACL,EACD,wBACE,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,QAAQ,EAAE,OAAO,YACpC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,GACvB,GACL,IACK,CACd,CAAA;QACH,CAAC,GACI,CACR,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport Form from '@rsc-kit/core/form'\nimport { CircleCheck } from 'lucide-react'\nimport type { PublicFormProps } from '../forms/theme.js'\nimport { Button } from './components/ui/button.js'\nimport { FieldGroup } from './components/ui/field.js'\nimport { FieldList, useBrowserErrors } from './fields.js'\n\n/**\n * A form anyone may fill in, drawn with the same fields as the panel's: the\n * browser checks each one first, and the server's refusals land under the\n * same fields. Once sent it says so in its place - or the action redirects.\n * No card around it: it sits in the app's own page, which decides that.\n */\nexport function PublicForm({ action, fields, submit, done, honeypot }: PublicFormProps) {\n const form = useRef<HTMLFormElement>(null)\n const browser = useBrowserErrors(form)\n\n return (\n <Form action={action} ref={form}>\n {({ pending, error: server, formError, succeeded }) => {\n if (succeeded && done !== undefined) {\n return (\n <p role=\"status\" className=\"flex items-center gap-2 text-sm\">\n <CircleCheck className=\"size-4 text-muted-foreground\" />\n {done}\n </p>\n )\n }\n\n const error = (name: string) => browser[name] ?? server(name)\n\n return (\n <FieldGroup>\n <FieldList fields={fields} values={{}} error={error} />\n {/* For bots: out of sight, out of the tab order, and not read out. */}\n <div aria-hidden=\"true\" className=\"absolute -left-[9999px] size-px overflow-hidden\">\n <input type=\"text\" name={honeypot} tabIndex={-1} autoComplete=\"off\" defaultValue=\"\" />\n </div>\n {formError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {formError}\n </p>\n )}\n <div>\n <Button type=\"submit\" disabled={pending}>\n {pending ? 'Sending…' : submit}\n </Button>\n </div>\n </FieldGroup>\n )\n }}\n </Form>\n )\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { Field as FieldSpec } from '../resource.js';
2
+ /**
3
+ * Every resource's create and edit form, drawn from its fields. Uncontrolled:
4
+ * a value starts as its default, and a refused save keeps what was typed.
5
+ * The controls are shadcn's, and each submits under its field's name - a
6
+ * Select and a Switch through the hidden input base-ui renders for them.
7
+ *
8
+ * The browser checks each control first, against the constraints read from
9
+ * the schema (required, min, maxlength, pattern, type="email"). Its own
10
+ * bubble is replaced by its message in the field's FieldError, where the
11
+ * server's refusals show too; with JavaScript off the bubble is what shows.
12
+ */
13
+ export declare function ResourceForm({ action, fields, values, submit, cancel, }: {
14
+ action: (form: FormData) => Promise<unknown>;
15
+ fields: FieldSpec[];
16
+ values: Record<string, unknown>;
17
+ submit: string;
18
+ cancel: string;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRef } from 'react';
4
+ import Form from '@rsc-kit/core/form';
5
+ import Link from '@rsc-kit/core/Link';
6
+ import { Button } from './components/ui/button.js';
7
+ import { Card, CardContent, CardFooter } from './components/ui/card.js';
8
+ import { FieldGroup } from './components/ui/field.js';
9
+ import { FieldList, useBrowserErrors } from './fields.js';
10
+ /**
11
+ * Every resource's create and edit form, drawn from its fields. Uncontrolled:
12
+ * a value starts as its default, and a refused save keeps what was typed.
13
+ * The controls are shadcn's, and each submits under its field's name - a
14
+ * Select and a Switch through the hidden input base-ui renders for them.
15
+ *
16
+ * The browser checks each control first, against the constraints read from
17
+ * the schema (required, min, maxlength, pattern, type="email"). Its own
18
+ * bubble is replaced by its message in the field's FieldError, where the
19
+ * server's refusals show too; with JavaScript off the bubble is what shows.
20
+ */
21
+ export function ResourceForm({ action, fields, values, submit, cancel, }) {
22
+ const form = useRef(null);
23
+ const browser = useBrowserErrors(form);
24
+ return (_jsx(Form, { action: action, ref: form, children: ({ pending, error: server, formError }) => {
25
+ // The browser's message while the field is still wrong, else the server's.
26
+ const error = (name) => browser[name] ?? server(name);
27
+ return (_jsxs(Card, { className: "max-w-2xl", children: [_jsx(CardContent, { children: _jsxs(FieldGroup, { children: [_jsx(FieldList, { fields: fields, values: values, error: error }), formError && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: formError }))] }) }), _jsxs(CardFooter, { className: "justify-end gap-2 border-t", children: [_jsx(Button, { variant: "outline", nativeButton: false, render: _jsx(Link, { href: cancel }), children: "Cancel" }), _jsx(Button, { type: "submit", disabled: pending, children: pending ? 'Saving…' : submit })] })] }));
28
+ } }));
29
+ }
30
+ //# sourceMappingURL=resource-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-form.js","sourceRoot":"","sources":["../../src/shadcn/resource-form.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9B,OAAO,IAAI,MAAM,oBAAoB,CAAA;AACrC,OAAO,IAAI,MAAM,oBAAoB,CAAA;AAErC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,MAAM,EACN,MAAM,EACN,MAAM,EACN,MAAM,EACN,MAAM,GAOP;IACC,MAAM,IAAI,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAA;IAC1C,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;IAEtC,OAAO,CACL,KAAC,IAAI,IAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,YAC5B,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE;YACzC,2EAA2E;YAC3E,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAA;YAE7D,OAAO,CACL,MAAC,IAAI,IAAC,SAAS,EAAC,WAAW,aACzB,KAAC,WAAW,cACV,MAAC,UAAU,eACT,KAAC,SAAS,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAI,EAC1D,SAAS,IAAI,CACZ,YAAG,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,0BAA0B,YACjD,SAAS,GACR,CACL,IACU,GACD,EACd,MAAC,UAAU,IAAC,SAAS,EAAC,4BAA4B,aAChD,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,KAAC,IAAI,IAAC,IAAI,EAAE,MAAe,GAAI,uBAE7E,EACT,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,QAAQ,EAAE,OAAO,YACpC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,GACtB,IACE,IACR,CACR,CAAA;QACH,CAAC,GACI,CACR,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\nimport { useRef } from 'react'\nimport Form from '@rsc-kit/core/form'\nimport Link from '@rsc-kit/core/Link'\nimport type { Field as FieldSpec } from '../resource.js'\nimport { Button } from './components/ui/button.js'\nimport { Card, CardContent, CardFooter } from './components/ui/card.js'\nimport { FieldGroup } from './components/ui/field.js'\nimport { FieldList, useBrowserErrors } from './fields.js'\n\n/**\n * Every resource's create and edit form, drawn from its fields. Uncontrolled:\n * a value starts as its default, and a refused save keeps what was typed.\n * The controls are shadcn's, and each submits under its field's name - a\n * Select and a Switch through the hidden input base-ui renders for them.\n *\n * The browser checks each control first, against the constraints read from\n * the schema (required, min, maxlength, pattern, type=\"email\"). Its own\n * bubble is replaced by its message in the field's FieldError, where the\n * server's refusals show too; with JavaScript off the bubble is what shows.\n */\nexport function ResourceForm({\n action,\n fields,\n values,\n submit,\n cancel,\n}: {\n action: (form: FormData) => Promise<unknown>\n fields: FieldSpec[]\n values: Record<string, unknown>\n submit: string\n cancel: string\n}) {\n const form = useRef<HTMLFormElement>(null)\n const browser = useBrowserErrors(form)\n\n return (\n <Form action={action} ref={form}>\n {({ pending, error: server, formError }) => {\n // The browser's message while the field is still wrong, else the server's.\n const error = (name: string) => browser[name] ?? server(name)\n\n return (\n <Card className=\"max-w-2xl\">\n <CardContent>\n <FieldGroup>\n <FieldList fields={fields} values={values} error={error} />\n {formError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {formError}\n </p>\n )}\n </FieldGroup>\n </CardContent>\n <CardFooter className=\"justify-end gap-2 border-t\">\n <Button variant=\"outline\" nativeButton={false} render={<Link href={cancel as never} />}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={pending}>\n {pending ? 'Saving…' : submit}\n </Button>\n </CardFooter>\n </Card>\n )\n }}\n </Form>\n )\n}\n"]}
@@ -0,0 +1,12 @@
1
+ /*
2
+ * The panel's classes, for the app's Tailwind to generate.
3
+ *
4
+ * @import "@rsc-kit/press/shadcn.css";
5
+ *
6
+ * in the app's stylesheet, after shadcn's theme. The panel is drawn with
7
+ * shadcn components and Tailwind classes; the colours, radius and fonts are
8
+ * the app's own theme variables, so it looks like the rest of the app and
9
+ * follows its dark mode. Tailwind does not read node_modules, so without this
10
+ * the panel's classes are simply absent from the stylesheet.
11
+ */
12
+ @source "./";
@@ -0,0 +1,3 @@
1
+ import type { TenantSwitcherProps } from '../admin/theme.js';
2
+ /** The sidebar's header in a panel with tenants: who you are working for, and the others you could be. */
3
+ export declare function TenantSwitcher({ tenants, current, label, choose, }: TenantSwitcherProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTransition } from 'react';
4
+ import { Check, ChevronsUpDown } from 'lucide-react';
5
+ import { Avatar, AvatarFallback, AvatarImage } from './components/ui/avatar.js';
6
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from './components/ui/dropdown-menu.js';
7
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './components/ui/sidebar.js';
8
+ function TenantAvatar({ tenant }) {
9
+ return (_jsxs(Avatar, { className: "size-8 rounded-lg", children: [tenant.avatar && _jsx(AvatarImage, { src: tenant.avatar, alt: tenant.name }), _jsx(AvatarFallback, { className: "rounded-lg bg-sidebar-primary text-sidebar-primary-foreground", children: tenant.name.charAt(0).toUpperCase() })] }));
10
+ }
11
+ /** The sidebar's header in a panel with tenants: who you are working for, and the others you could be. */
12
+ export function TenantSwitcher({ tenants, current, label, choose, }) {
13
+ const { isMobile } = useSidebar();
14
+ const [pending, start] = useTransition();
15
+ return (_jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx(SidebarMenuButton, { size: "lg", "aria-busy": pending || undefined, className: "data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground" }), children: [_jsx(TenantAvatar, { tenant: current }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-medium", children: current.name }), current.description && _jsx("span", { className: "truncate text-xs text-muted-foreground", children: current.description })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }), _jsx(DropdownMenuContent, { className: "min-w-56 rounded-lg", align: "start", side: isMobile ? 'bottom' : 'right', sideOffset: 4, children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: label }), tenants.map((tenant) => (_jsxs(DropdownMenuItem, { className: "gap-2 p-2", disabled: pending, onClick: () => tenant.id !== current.id && start(async () => void (await choose(tenant.id))), children: [_jsx(TenantAvatar, { tenant: tenant }), _jsx("span", { className: "flex-1 truncate", children: tenant.name }), tenant.id === current.id && _jsx(Check, { className: "size-4" })] }, tenant.id)))] }) })] }) }) }));
16
+ }
17
+ //# sourceMappingURL=tenant-switcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-switcher.js","sourceRoot":"","sources":["../../src/shadcn/tenant-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { useTransition } from 'react'\nimport { Check, ChevronsUpDown } from 'lucide-react'\nimport type { TenantSwitcherProps } from '../admin/theme.js'\nimport type { Tenant } from '../resource.js'\nimport { Avatar, AvatarFallback, AvatarImage } from './components/ui/avatar.js'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuTrigger,\n} from './components/ui/dropdown-menu.js'\nimport { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './components/ui/sidebar.js'\n\nfunction TenantAvatar({ tenant }: { tenant: Tenant }) {\n return (\n <Avatar className=\"size-8 rounded-lg\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt={tenant.name} />}\n <AvatarFallback className=\"rounded-lg bg-sidebar-primary text-sidebar-primary-foreground\">\n {tenant.name.charAt(0).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n )\n}\n\n/** The sidebar's header in a panel with tenants: who you are working for, and the others you could be. */\nexport function TenantSwitcher({\n tenants,\n current,\n label,\n choose,\n}: TenantSwitcherProps) {\n const { isMobile } = useSidebar()\n const [pending, start] = useTransition()\n\n return (\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger\n render={\n <SidebarMenuButton\n size=\"lg\"\n aria-busy={pending || undefined}\n className=\"data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground\"\n />\n }\n >\n <TenantAvatar tenant={current} />\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-medium\">{current.name}</span>\n {current.description && <span className=\"truncate text-xs text-muted-foreground\">{current.description}</span>}\n </div>\n <ChevronsUpDown className=\"ml-auto\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-56 rounded-lg\" align=\"start\" side={isMobile ? 'bottom' : 'right'} sideOffset={4}>\n <DropdownMenuGroup>\n <DropdownMenuLabel className=\"text-xs text-muted-foreground\">{label}</DropdownMenuLabel>\n {tenants.map((tenant) => (\n <DropdownMenuItem\n key={tenant.id}\n className=\"gap-2 p-2\"\n disabled={pending}\n onClick={() => tenant.id !== current.id && start(async () => void (await choose(tenant.id)))}\n >\n <TenantAvatar tenant={tenant} />\n <span className=\"flex-1 truncate\">{tenant.name}</span>\n {tenant.id === current.id && <Check className=\"size-4\" />}\n </DropdownMenuItem>\n ))}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n )\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export declare const THEME_KEY = "rsc-kit-admin-theme";
2
+ /**
3
+ * Applies the stored theme, or the system's, before the first paint - as an
4
+ * inline script at the top of the panel, so a dark system never shows a light
5
+ * page first. A string, so it runs without React.
6
+ */
7
+ export declare const themeScript = "(function(){try{var t=localStorage.getItem('rsc-kit-admin-theme')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);document.documentElement.style.colorScheme=d?'dark':'light'})()";
@@ -0,0 +1,10 @@
1
+ // Not a client module: the panel's server layout reads the string, and a
2
+ // "use client" file would hand it a client reference instead.
3
+ export const THEME_KEY = 'rsc-kit-admin-theme';
4
+ /**
5
+ * Applies the stored theme, or the system's, before the first paint - as an
6
+ * inline script at the top of the panel, so a dark system never shows a light
7
+ * page first. A string, so it runs without React.
8
+ */
9
+ export const themeScript = `(function(){try{var t=localStorage.getItem('${THEME_KEY}')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);document.documentElement.style.colorScheme=d?'dark':'light'})()`;
10
+ //# sourceMappingURL=theme-script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-script.js","sourceRoot":"","sources":["../../src/shadcn/theme-script.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,8DAA8D;AAE9D,MAAM,CAAC,MAAM,SAAS,GAAG,qBAAqB,CAAA;AAE9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,+CAA+C,SAAS,sNAAsN,CAAA","sourcesContent":["// Not a client module: the panel's server layout reads the string, and a\n// \"use client\" file would hand it a client reference instead.\n\nexport const THEME_KEY = 'rsc-kit-admin-theme'\n\n/**\n * Applies the stored theme, or the system's, before the first paint - as an\n * inline script at the top of the panel, so a dark system never shows a light\n * page first. A string, so it runs without React.\n */\nexport const themeScript = `(function(){try{var t=localStorage.getItem('${THEME_KEY}')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);document.documentElement.style.colorScheme=d?'dark':'light'})()`\n"]}
@@ -0,0 +1,3 @@
1
+ import type { UserMenuProps } from '../admin/theme.js';
2
+ /** The sidebar's footer: who is signed in, and a way out when the app gave one. */
3
+ export declare function UserMenu({ user, signOut }: UserMenuProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronsUpDown, LogOut } from 'lucide-react';
4
+ import { Avatar, AvatarFallback, AvatarImage } from './components/ui/avatar.js';
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from './components/ui/dropdown-menu.js';
6
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './components/ui/sidebar.js';
7
+ function UserAvatar({ user }) {
8
+ return (_jsxs(Avatar, { className: "size-8 rounded-lg", children: [user.avatar && _jsx(AvatarImage, { src: user.avatar, alt: user.name }), _jsx(AvatarFallback, { className: "rounded-lg", children: user.name
9
+ .split(/\s+/)
10
+ .map((word) => word.charAt(0))
11
+ .slice(0, 2)
12
+ .join('')
13
+ .toUpperCase() })] }));
14
+ }
15
+ function Who({ user }) {
16
+ return (_jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-medium", children: user.name }), user.email && _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user.email })] }));
17
+ }
18
+ /** The sidebar's footer: who is signed in, and a way out when the app gave one. */
19
+ export function UserMenu({ user, signOut }) {
20
+ const { isMobile } = useSidebar();
21
+ return (_jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx(SidebarMenuButton, { size: "lg", className: "data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground" }), children: [_jsx(UserAvatar, { user: user }), _jsx(Who, { user: user }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }), _jsxs(DropdownMenuContent, { className: "min-w-56 rounded-lg", side: isMobile ? 'bottom' : 'right', align: "end", sideOffset: 4, children: [_jsx(DropdownMenuGroup, { children: _jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsx(UserAvatar, { user: user }), _jsx(Who, { user: user })] }) }) }), signOut && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => void signOut(), children: [_jsx(LogOut, {}), "Sign out"] })] }))] })] }) }) }));
22
+ }
23
+ //# sourceMappingURL=user-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-menu.js","sourceRoot":"","sources":["../../src/shadcn/user-menu.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAC/E,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAExG,SAAS,UAAU,CAAC,EAAE,IAAI,EAAuB;IAC/C,OAAO,CACL,MAAC,MAAM,IAAC,SAAS,EAAC,mBAAmB,aAClC,IAAI,CAAC,MAAM,IAAI,KAAC,WAAW,IAAC,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,GAAI,EACjE,KAAC,cAAc,IAAC,SAAS,EAAC,YAAY,YACnC,IAAI,CAAC,IAAI;qBACP,KAAK,CAAC,KAAK,CAAC;qBACZ,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;qBAC7B,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;qBACX,IAAI,CAAC,EAAE,CAAC;qBACR,WAAW,EAAE,GACD,IACV,CACV,CAAA;AACH,CAAC;AAED,SAAS,GAAG,CAAC,EAAE,IAAI,EAAuB;IACxC,OAAO,CACL,eAAK,SAAS,EAAC,6CAA6C,aAC1D,eAAM,SAAS,EAAC,sBAAsB,YAAE,IAAI,CAAC,IAAI,GAAQ,EACxD,IAAI,CAAC,KAAK,IAAI,eAAM,SAAS,EAAC,wCAAwC,YAAE,IAAI,CAAC,KAAK,GAAQ,IACvF,CACP,CAAA;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAiB;IACvD,MAAM,EAAE,QAAQ,EAAE,GAAG,UAAU,EAAE,CAAA;IAEjC,OAAO,CACL,KAAC,WAAW,cACV,KAAC,eAAe,cACd,MAAC,YAAY,eACX,MAAC,mBAAmB,IAClB,MAAM,EAAE,KAAC,iBAAiB,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kFAAkF,GAAG,aAEpI,KAAC,UAAU,IAAC,IAAI,EAAE,IAAI,GAAI,EAC1B,KAAC,GAAG,IAAC,IAAI,EAAE,IAAI,GAAI,EACnB,KAAC,cAAc,IAAC,SAAS,EAAC,gBAAgB,GAAG,IACzB,EACtB,MAAC,mBAAmB,IAAC,SAAS,EAAC,qBAAqB,EAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,EAAC,KAAK,EAAC,UAAU,EAAE,CAAC,aACjH,KAAC,iBAAiB,cAChB,KAAC,iBAAiB,IAAC,SAAS,EAAC,iBAAiB,YAC5C,eAAK,SAAS,EAAC,uDAAuD,aACpE,KAAC,UAAU,IAAC,IAAI,EAAE,IAAI,GAAI,EAC1B,KAAC,GAAG,IAAC,IAAI,EAAE,IAAI,GAAI,IACf,GACY,GACF,EACnB,OAAO,IAAI,CACV,8BACE,KAAC,qBAAqB,KAAG,EACzB,MAAC,gBAAgB,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,OAAO,EAAE,aAC7C,KAAC,MAAM,KAAG,gBAEO,IAClB,CACJ,IACmB,IACT,GACC,GACN,CACf,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\nimport { ChevronsUpDown, LogOut } from 'lucide-react'\nimport type { PanelUser, UserMenuProps } from '../admin/theme.js'\nimport { Avatar, AvatarFallback, AvatarImage } from './components/ui/avatar.js'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from './components/ui/dropdown-menu.js'\nimport { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './components/ui/sidebar.js'\n\nfunction UserAvatar({ user }: { user: PanelUser }) {\n return (\n <Avatar className=\"size-8 rounded-lg\">\n {user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}\n <AvatarFallback className=\"rounded-lg\">\n {user.name\n .split(/\\s+/)\n .map((word) => word.charAt(0))\n .slice(0, 2)\n .join('')\n .toUpperCase()}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nfunction Who({ user }: { user: PanelUser }) {\n return (\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-medium\">{user.name}</span>\n {user.email && <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span>}\n </div>\n )\n}\n\n/** The sidebar's footer: who is signed in, and a way out when the app gave one. */\nexport function UserMenu({ user, signOut }: UserMenuProps) {\n const { isMobile } = useSidebar()\n\n return (\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger\n render={<SidebarMenuButton size=\"lg\" className=\"data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground\" />}\n >\n <UserAvatar user={user} />\n <Who user={user} />\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-56 rounded-lg\" side={isMobile ? 'bottom' : 'right'} align=\"end\" sideOffset={4}>\n <DropdownMenuGroup>\n <DropdownMenuLabel className=\"p-0 font-normal\">\n <div className=\"flex items-center gap-2 px-1 py-1.5 text-left text-sm\">\n <UserAvatar user={user} />\n <Who user={user} />\n </div>\n </DropdownMenuLabel>\n </DropdownMenuGroup>\n {signOut && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={() => void signOut()}>\n <LogOut />\n Sign out\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n )\n}\n"]}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * How a panel and an installed plugin trust each other: every request the
3
+ * panel sends is signed with the secret made when the plugin was installed,
4
+ * and the plugin checks it before answering. Web Crypto only, so both ends
5
+ * run anywhere - Bun, Node, a Worker.
6
+ *
7
+ * x-rsc-admin-installation which installation is calling: the plugin finds its secret by it
8
+ * x-rsc-admin-timestamp seconds since the epoch, so a captured request cannot be replayed later
9
+ * x-rsc-admin-signature sha256=<hex HMAC-SHA256 of "<timestamp>.<body>">
10
+ *
11
+ * A plugin's handler starts with verifyPluginRequest(); nothing it answers
12
+ * is trusted by the panel either - see installable.ts.
13
+ */
14
+ export declare const INSTALLATION_HEADER = "x-rsc-admin-installation";
15
+ export declare const TIMESTAMP_HEADER = "x-rsc-admin-timestamp";
16
+ export declare const SIGNATURE_HEADER = "x-rsc-admin-signature";
17
+ /** A new installation's secret: 32 random bytes, hex. */
18
+ export declare function newSecret(): string;
19
+ /** The headers that sign `body` for one installation. */
20
+ export declare function signedHeaders(installation: string, secret: string, body: string, now?: number): Promise<Record<string, string>>;
21
+ export interface VerifiedRequest<Body = unknown> {
22
+ installation: string;
23
+ body: Body;
24
+ }
25
+ /**
26
+ * For a plugin's endpoint: the request's body, once its signature is checked
27
+ * against the secret its installation was given - or null, which the plugin
28
+ * answers with a 401 and nothing else.
29
+ *
30
+ * export async function POST(request: Request) {
31
+ * const verified = await verifyPluginRequest(request, (id) => secrets.get(id))
32
+ * if (!verified) return new Response(null, { status: 401 })
33
+ * ...
34
+ * }
35
+ *
36
+ * `secretOf` returning undefined - an installation the plugin never heard of,
37
+ * or one uninstalled since - is a refusal too. Older than `tolerance`
38
+ * seconds, either way, is a refusal: a replay.
39
+ */
40
+ export declare function verifyPluginRequest<Body = unknown>(request: Request, secretOf: (installation: string) => string | undefined | Promise<string | undefined>, { tolerance, now }?: {
41
+ tolerance?: number;
42
+ now?: number;
43
+ }): Promise<VerifiedRequest<Body> | null>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * How a panel and an installed plugin trust each other: every request the
3
+ * panel sends is signed with the secret made when the plugin was installed,
4
+ * and the plugin checks it before answering. Web Crypto only, so both ends
5
+ * run anywhere - Bun, Node, a Worker.
6
+ *
7
+ * x-rsc-admin-installation which installation is calling: the plugin finds its secret by it
8
+ * x-rsc-admin-timestamp seconds since the epoch, so a captured request cannot be replayed later
9
+ * x-rsc-admin-signature sha256=<hex HMAC-SHA256 of "<timestamp>.<body>">
10
+ *
11
+ * A plugin's handler starts with verifyPluginRequest(); nothing it answers
12
+ * is trusted by the panel either - see installable.ts.
13
+ */
14
+ export const INSTALLATION_HEADER = 'x-rsc-admin-installation';
15
+ export const TIMESTAMP_HEADER = 'x-rsc-admin-timestamp';
16
+ export const SIGNATURE_HEADER = 'x-rsc-admin-signature';
17
+ const encoder = new TextEncoder();
18
+ async function hmac(secret, message) {
19
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
20
+ const signature = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(message)));
21
+ return [...signature].map((byte) => byte.toString(16).padStart(2, '0')).join('');
22
+ }
23
+ /** Equal without saying where they differ: a byte-by-byte compare leaks the signature by its timing. */
24
+ function same(a, b) {
25
+ if (a.length !== b.length)
26
+ return false;
27
+ let difference = 0;
28
+ for (let index = 0; index < a.length; index++)
29
+ difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
30
+ return difference === 0;
31
+ }
32
+ /** A new installation's secret: 32 random bytes, hex. */
33
+ export function newSecret() {
34
+ return [...crypto.getRandomValues(new Uint8Array(32))].map((byte) => byte.toString(16).padStart(2, '0')).join('');
35
+ }
36
+ /** The headers that sign `body` for one installation. */
37
+ export async function signedHeaders(installation, secret, body, now = Date.now()) {
38
+ const timestamp = String(Math.floor(now / 1000));
39
+ return {
40
+ [INSTALLATION_HEADER]: installation,
41
+ [TIMESTAMP_HEADER]: timestamp,
42
+ [SIGNATURE_HEADER]: `sha256=${await hmac(secret, `${timestamp}.${body}`)}`,
43
+ };
44
+ }
45
+ /**
46
+ * For a plugin's endpoint: the request's body, once its signature is checked
47
+ * against the secret its installation was given - or null, which the plugin
48
+ * answers with a 401 and nothing else.
49
+ *
50
+ * export async function POST(request: Request) {
51
+ * const verified = await verifyPluginRequest(request, (id) => secrets.get(id))
52
+ * if (!verified) return new Response(null, { status: 401 })
53
+ * ...
54
+ * }
55
+ *
56
+ * `secretOf` returning undefined - an installation the plugin never heard of,
57
+ * or one uninstalled since - is a refusal too. Older than `tolerance`
58
+ * seconds, either way, is a refusal: a replay.
59
+ */
60
+ export async function verifyPluginRequest(request, secretOf, { tolerance = 300, now = Date.now() } = {}) {
61
+ const installation = request.headers.get(INSTALLATION_HEADER);
62
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
63
+ const signature = request.headers.get(SIGNATURE_HEADER);
64
+ if (!installation || !timestamp || !signature || !/^\d+$/.test(timestamp))
65
+ return null;
66
+ if (Math.abs(now / 1000 - Number(timestamp)) > tolerance)
67
+ return null;
68
+ const secret = await secretOf(installation);
69
+ // An empty secret signs as well as any: never one to accept.
70
+ if (!secret)
71
+ return null;
72
+ const body = await request.text();
73
+ if (!same(signature, `sha256=${await hmac(secret, `${timestamp}.${body}`)}`))
74
+ return null;
75
+ try {
76
+ return { installation, body: JSON.parse(body) };
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ //# sourceMappingURL=signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * How a panel and an installed plugin trust each other: every request the\n * panel sends is signed with the secret made when the plugin was installed,\n * and the plugin checks it before answering. Web Crypto only, so both ends\n * run anywhere - Bun, Node, a Worker.\n *\n * x-rsc-admin-installation which installation is calling: the plugin finds its secret by it\n * x-rsc-admin-timestamp seconds since the epoch, so a captured request cannot be replayed later\n * x-rsc-admin-signature sha256=<hex HMAC-SHA256 of \"<timestamp>.<body>\">\n *\n * A plugin's handler starts with verifyPluginRequest(); nothing it answers\n * is trusted by the panel either - see installable.ts.\n */\n\nexport const INSTALLATION_HEADER = 'x-rsc-admin-installation'\nexport const TIMESTAMP_HEADER = 'x-rsc-admin-timestamp'\nexport const SIGNATURE_HEADER = 'x-rsc-admin-signature'\n\nconst encoder = new TextEncoder()\n\nasync function hmac(secret: string, message: string): Promise<string> {\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const signature = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(message)))\n\n return [...signature].map((byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\n/** Equal without saying where they differ: a byte-by-byte compare leaks the signature by its timing. */\nfunction same(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n\n let difference = 0\n\n for (let index = 0; index < a.length; index++) difference |= a.charCodeAt(index) ^ b.charCodeAt(index)\n\n return difference === 0\n}\n\n/** A new installation's secret: 32 random bytes, hex. */\nexport function newSecret(): string {\n return [...crypto.getRandomValues(new Uint8Array(32))].map((byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\n/** The headers that sign `body` for one installation. */\nexport async function signedHeaders(installation: string, secret: string, body: string, now = Date.now()): Promise<Record<string, string>> {\n const timestamp = String(Math.floor(now / 1000))\n\n return {\n [INSTALLATION_HEADER]: installation,\n [TIMESTAMP_HEADER]: timestamp,\n [SIGNATURE_HEADER]: `sha256=${await hmac(secret, `${timestamp}.${body}`)}`,\n }\n}\n\nexport interface VerifiedRequest<Body = unknown> {\n installation: string\n body: Body\n}\n\n/**\n * For a plugin's endpoint: the request's body, once its signature is checked\n * against the secret its installation was given - or null, which the plugin\n * answers with a 401 and nothing else.\n *\n * export async function POST(request: Request) {\n * const verified = await verifyPluginRequest(request, (id) => secrets.get(id))\n * if (!verified) return new Response(null, { status: 401 })\n * ...\n * }\n *\n * `secretOf` returning undefined - an installation the plugin never heard of,\n * or one uninstalled since - is a refusal too. Older than `tolerance`\n * seconds, either way, is a refusal: a replay.\n */\nexport async function verifyPluginRequest<Body = unknown>(\n request: Request,\n secretOf: (installation: string) => string | undefined | Promise<string | undefined>,\n { tolerance = 300, now = Date.now() }: { tolerance?: number; now?: number } = {},\n): Promise<VerifiedRequest<Body> | null> {\n const installation = request.headers.get(INSTALLATION_HEADER)\n const timestamp = request.headers.get(TIMESTAMP_HEADER)\n const signature = request.headers.get(SIGNATURE_HEADER)\n\n if (!installation || !timestamp || !signature || !/^\\d+$/.test(timestamp)) return null\n if (Math.abs(now / 1000 - Number(timestamp)) > tolerance) return null\n\n const secret = await secretOf(installation)\n\n // An empty secret signs as well as any: never one to accept.\n if (!secret) return null\n\n const body = await request.text()\n\n if (!same(signature, `sha256=${await hmac(secret, `${timestamp}.${body}`)}`)) return null\n\n try {\n return { installation, body: JSON.parse(body) as Body }\n } catch {\n return null\n }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import type { DataSource } from './resource.js';
2
+ /**
3
+ * A source per tenant, made the first time a tenant is asked for and kept.
4
+ * For data that is split by tenant before it reaches you - a database per
5
+ * team, an API key per customer, rows in memory for a demo:
6
+ *
7
+ * source: perTenant((tenant) => memorySource(seed(tenant), 'id', make))
8
+ *
9
+ * A source that filters one table by a tenant column needs none of this: it
10
+ * reads context.tenant in each call.
11
+ */
12
+ export declare function perTenant<Row, Values>(make: (tenantId: string) => DataSource<Row, Values>): DataSource<Row, Values>;