@cosmoops/connect-sdk 0.9.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +497 -0
- package/README.md +111 -41
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +20 -1
- package/dist/cjs/client/components/app-shell.js +75 -41
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +20 -1
- package/dist/cjs/client/components/confirm-dialog.js +40 -0
- package/dist/cjs/client/components/cursor-pagination.js +22 -0
- package/dist/cjs/client/components/filter-tabs.js +24 -0
- package/dist/cjs/client/components/grievance-notice.js +30 -0
- package/dist/cjs/client/components/link-button.js +17 -10
- package/dist/cjs/client/components/logo.js +4 -3
- package/dist/cjs/client/components/nav.js +41 -0
- package/dist/cjs/client/components/plan-pricing.js +27 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/platform-gate.js +48 -0
- package/dist/cjs/client/components/shell-context.js +149 -0
- package/dist/cjs/client/components/side-nav.js +49 -23
- package/dist/cjs/client/components/sidebar.js +127 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/user-menu.js +96 -37
- package/dist/cjs/client/components/workspace-switcher.js +100 -0
- package/dist/cjs/client/index.js +69 -2
- package/dist/cjs/client/provider.js +57 -2
- package/dist/cjs/client/use-checkout.js +9 -5
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +196 -18
- package/dist/cjs/server/http.js +6 -2
- package/dist/cjs/server/resources/acting-as.js +16 -0
- package/dist/cjs/server/resources/ai.js +15 -2
- package/dist/cjs/server/resources/data.js +374 -0
- package/dist/cjs/server/resources/identity.js +7 -3
- package/dist/cjs/server/resources/kv.js +84 -0
- package/dist/cjs/server/resources/platform.js +43 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +74 -0
- package/dist/cjs/store.js +152 -0
- package/dist/cjs/ui/empty-state.js +28 -0
- package/dist/cjs/ui/index.js +44 -0
- package/dist/cjs/ui/page-header.js +27 -0
- package/dist/cjs/ui/page-shell.js +44 -0
- package/dist/cjs/ui/section.js +20 -0
- package/dist/cjs/ui/setting-row.js +14 -0
- package/dist/cjs/ui/status-chip.js +17 -0
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +20 -1
- package/dist/esm/client/components/app-shell.js +81 -47
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +20 -1
- package/dist/esm/client/components/confirm-dialog.js +37 -0
- package/dist/esm/client/components/cursor-pagination.js +19 -0
- package/dist/esm/client/components/filter-tabs.js +21 -0
- package/dist/esm/client/components/grievance-notice.js +27 -0
- package/dist/esm/client/components/link-button.js +17 -10
- package/dist/esm/client/components/logo.js +4 -3
- package/dist/esm/client/components/nav.js +40 -0
- package/dist/esm/client/components/plan-pricing.js +25 -0
- package/dist/esm/client/components/plans.js +2 -2
- package/dist/esm/client/components/platform-gate.js +45 -0
- package/dist/esm/client/components/shell-context.js +141 -0
- package/dist/esm/client/components/side-nav.js +48 -21
- package/dist/esm/client/components/sidebar.js +120 -0
- package/dist/esm/client/components/subscribe-dialog.js +3 -3
- package/dist/esm/client/components/subscription.js +2 -2
- package/dist/esm/client/components/user-menu.js +95 -38
- package/dist/esm/client/components/workspace-switcher.js +97 -0
- package/dist/esm/client/index.js +46 -4
- package/dist/esm/client/provider.js +53 -2
- package/dist/esm/client/use-checkout.js +9 -5
- package/dist/esm/errors.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +196 -18
- package/dist/esm/server/http.js +6 -2
- package/dist/esm/server/resources/acting-as.js +13 -0
- package/dist/esm/server/resources/ai.js +15 -2
- package/dist/esm/server/resources/data.js +369 -0
- package/dist/esm/server/resources/identity.js +7 -3
- package/dist/esm/server/resources/kv.js +80 -0
- package/dist/esm/server/resources/platform.js +39 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/resources/workspaces.js +70 -0
- package/dist/esm/store.js +141 -0
- package/dist/esm/ui/empty-state.js +25 -0
- package/dist/esm/ui/index.js +34 -0
- package/dist/esm/ui/page-header.js +24 -0
- package/dist/esm/ui/page-shell.js +41 -0
- package/dist/esm/ui/section.js +17 -0
- package/dist/esm/ui/setting-row.js +11 -0
- package/dist/esm/ui/status-chip.js +14 -0
- package/dist/types/client/components/app-shell.d.ts +65 -27
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- package/dist/types/client/components/confirm-dialog.d.ts +34 -0
- package/dist/types/client/components/cursor-pagination.d.ts +27 -0
- package/dist/types/client/components/filter-tabs.d.ts +26 -0
- package/dist/types/client/components/grievance-notice.d.ts +31 -0
- package/dist/types/client/components/link-button.d.ts +32 -9
- package/dist/types/client/components/logo.d.ts +18 -2
- package/dist/types/client/components/nav.d.ts +24 -0
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/platform-gate.d.ts +53 -0
- package/dist/types/client/components/shell-context.d.ts +116 -0
- package/dist/types/client/components/side-nav.d.ts +27 -13
- package/dist/types/client/components/sidebar.d.ts +95 -0
- package/dist/types/client/components/user-menu.d.ts +68 -22
- package/dist/types/client/components/workspace-switcher.d.ts +49 -0
- package/dist/types/client/index.d.ts +27 -5
- package/dist/types/client/provider.d.ts +109 -6
- package/dist/types/client/use-checkout.d.ts +8 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +25 -2
- package/dist/types/server/http.d.ts +8 -2
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/resources/acting-as.d.ts +11 -0
- package/dist/types/server/resources/ai.d.ts +9 -1
- package/dist/types/server/resources/data.d.ts +258 -0
- package/dist/types/server/resources/kv.d.ts +63 -0
- package/dist/types/server/resources/platform.d.ts +32 -0
- package/dist/types/server/resources/workspaces.d.ts +55 -0
- package/dist/types/store.d.ts +96 -0
- package/dist/types/types.d.ts +319 -15
- package/dist/types/ui/empty-state.d.ts +34 -0
- package/dist/types/ui/index.d.ts +40 -0
- package/dist/types/ui/page-header.d.ts +34 -0
- package/dist/types/ui/page-shell.d.ts +39 -0
- package/dist/types/ui/section.d.ts +26 -0
- package/dist/types/ui/setting-row.d.ts +18 -0
- package/dist/types/ui/status-chip.d.ts +36 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ The fast path — most integrations are this and nothing else. Not on Next.js?
|
|
|
35
35
|
Skip to [Other frameworks](#other-frameworks); the shape is the same, you
|
|
36
36
|
just wire the session cookie yourself.
|
|
37
37
|
|
|
38
|
-
**1. Install and get credentials.** From your application's
|
|
38
|
+
**1. Install and get credentials.** From your application's Connect client section
|
|
39
39
|
in Connect, copy the API key and OAuth client id.
|
|
40
40
|
|
|
41
41
|
```bash
|
|
@@ -87,7 +87,7 @@ import "streamdown/styles.css";
|
|
|
87
87
|
Skip the last two if you pass `askAi={false}` to `AppShell`. The colour tokens
|
|
88
88
|
Streamdown expects are already mapped onto HeroUI's in `theme.css`.
|
|
89
89
|
|
|
90
|
-
**5. Register the redirect URI.** In the application's
|
|
90
|
+
**5. Register the redirect URI.** In the application's Connect client section, add
|
|
91
91
|
`{your origin}/api/connect/callback`. This is the one step the SDK cannot do
|
|
92
92
|
for you.
|
|
93
93
|
|
|
@@ -123,7 +123,7 @@ layout and Connect's dashboard rather than files it can safely write. See
|
|
|
123
123
|
|
|
124
124
|
Two things most integrations add next:
|
|
125
125
|
|
|
126
|
-
- **A shell** — `AppShell` from `/client` is the
|
|
126
|
+
- **A shell** — `AppShell` from `/client` is the rail and the access
|
|
127
127
|
gate every application platform needs. See [App shell](#app-shell).
|
|
128
128
|
- **Route protection before render** — see [Route protection and
|
|
129
129
|
`proxy.ts`](#route-protection-and-proxyts) below; `requireConnectSession()`
|
|
@@ -136,7 +136,7 @@ for you under the hood.
|
|
|
136
136
|
|
|
137
137
|
## Server setup
|
|
138
138
|
|
|
139
|
-
One credential does three jobs — the API key plaintext from your application's
|
|
139
|
+
One credential does three jobs — the API key plaintext from your application's Connect client section is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
|
|
140
140
|
|
|
141
141
|
```ts
|
|
142
142
|
import { ConnectClient } from "@cosmoops/connect-sdk/server";
|
|
@@ -518,17 +518,17 @@ that URL yourself instead.
|
|
|
518
518
|
import {
|
|
519
519
|
AppShell,
|
|
520
520
|
ConnectLoginButton, ConnectRegisterButton,
|
|
521
|
-
|
|
521
|
+
ConnectUserButton, ConnectAppsGrid, ConnectPlans,
|
|
522
522
|
} from "@cosmoops/connect-sdk/client";
|
|
523
523
|
|
|
524
|
-
// The whole frame —
|
|
525
|
-
<AppShell name="Dispatch" nav={nav}>{children}</AppShell>
|
|
524
|
+
// The whole frame — the rail, the access gate, Ask AI. See "App shell" below.
|
|
525
|
+
<AppShell name="Dispatch" nav={nav} secondaryNav={settingsNav}>{children}</AppShell>
|
|
526
526
|
|
|
527
527
|
// Or the pieces, if you already have a layout. The pair swaps automatically
|
|
528
528
|
// on sign-in state.
|
|
529
|
-
<ConnectAppsGrid />
|
|
529
|
+
<ConnectAppsGrid variant="icon" />
|
|
530
530
|
<ConnectLoginButton />
|
|
531
|
-
<
|
|
531
|
+
<ConnectUserButton variant="inline" />
|
|
532
532
|
|
|
533
533
|
// Pricing page
|
|
534
534
|
<ConnectPlans />
|
|
@@ -536,10 +536,13 @@ import {
|
|
|
536
536
|
|
|
537
537
|
| Component | What it does |
|
|
538
538
|
| --- | --- |
|
|
539
|
-
| `AppShell` | **The whole application frame** — collapsible
|
|
540
|
-
| `
|
|
539
|
+
| `AppShell` | **The whole application frame** — one collapsible rail carrying the brand, workspace switcher, both navs, the launcher and the account row, with your pages beside it. No top bar. Mounts `ConnectAccessGate` for you. The one `/client` export without the `Connect` prefix; alias it on import if your own layout shares the name. |
|
|
540
|
+
| `ConnectSidebar` · `ConnectSidebarHeader` · `ConnectSidebarBody` · `ConnectSidebarFooter` · `ConnectSidebarRow` | The rail in pieces, for chrome the props cannot express. `AppShell` composes exactly these; they coordinate through `ConnectShellProvider`. |
|
|
541
|
+
| `ConnectSidebarNav` | The rail's nav column on its own — sections, one level of children, active-route highlight — for an app that already has its own chrome. (Was `ConnectSideNav`.) |
|
|
541
542
|
| `ConnectLoginButton` · `ConnectRegisterButton` | "Continue with Connect", in the shape people know from Google. Render nothing when signed in. |
|
|
542
|
-
| `
|
|
543
|
+
| `ConnectUserButton` | The account row at the foot of the rail — circular avatar, name over email — opening a modal with identity, sessions and the Connect destinations. Profile, Manage subscription and Open Connect each open Connect in a new tab; Sign out acts in place. Renders nothing when signed out. (Was `ConnectUserMenu`.) |
|
|
544
|
+
| `ConnectProfileModal` | The same modal with no trigger, driven by `isOpen`/`onOpenChange` — for an app keeping its own account control. |
|
|
545
|
+
| `ConnectWorkspaceSwitcher` | Sits in the rail's header. Hides itself for an account with one workspace; shows unusable ones disabled, with the reason. |
|
|
543
546
|
| `ConnectAppsGrid` | Google-style launcher — grid icon opening a modal of every app this workspace can open. |
|
|
544
547
|
| `ConnectPlans` | Monthly/annual toggle, features, add-ons, live discount, coupon field, and the subscribe / upgrade / downgrade / cancel flow. |
|
|
545
548
|
| `ConnectPlansPage` | A whole `/plans` page — readiness banner, current subscription, and the grid. What `connect-sdk init` scaffolds. |
|
|
@@ -706,21 +709,20 @@ export function Shell({ children }: { children: React.ReactNode }) {
|
|
|
706
709
|
nav={[
|
|
707
710
|
{ label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
|
|
708
711
|
{ label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
|
|
712
|
+
]}
|
|
713
|
+
// Pinned to the foot of the rail: the routes you go to in order to
|
|
714
|
+
// change how the job works, not to do it.
|
|
715
|
+
secondaryNav={[
|
|
709
716
|
{
|
|
710
|
-
|
|
717
|
+
label: "Settings",
|
|
718
|
+
href: "/settings",
|
|
719
|
+
icon: <Settings className="size-5" />,
|
|
711
720
|
items: [
|
|
712
|
-
{
|
|
713
|
-
|
|
714
|
-
href: "/settings",
|
|
715
|
-
icon: <Settings className="size-5" />,
|
|
716
|
-
items: [
|
|
717
|
-
{ label: "Members", href: "/settings/members" },
|
|
718
|
-
{ label: "Billing", href: "/settings/billing" },
|
|
719
|
-
],
|
|
720
|
-
},
|
|
721
|
-
{ label: "Plans", href: "/plans", icon: <Receipt className="size-5" />, gateExempt: true },
|
|
721
|
+
{ label: "Members", href: "/settings/members" },
|
|
722
|
+
{ label: "Billing", href: "/settings/billing" },
|
|
722
723
|
],
|
|
723
724
|
},
|
|
725
|
+
{ label: "Plans", href: "/plans", icon: <Receipt className="size-5" />, gateExempt: true },
|
|
724
726
|
]}
|
|
725
727
|
>
|
|
726
728
|
{children}
|
|
@@ -729,10 +731,16 @@ export function Shell({ children }: { children: React.ReactNode }) {
|
|
|
729
731
|
}
|
|
730
732
|
```
|
|
731
733
|
|
|
732
|
-
That is the entire layout of
|
|
733
|
-
identical across the family — the lockup, the rail geometry, the
|
|
734
|
-
treatment, the app launcher, the account
|
|
735
|
-
here; only `name`, `icon` and
|
|
734
|
+
That is the entire layout of an application platform. Everything that should
|
|
735
|
+
look identical across the family — the lockup, the rail geometry, the
|
|
736
|
+
active-route treatment, the app launcher, the account row, the mobile drawer —
|
|
737
|
+
comes from here; only `name`, `icon` and the two navs differ per platform.
|
|
738
|
+
|
|
739
|
+
**There is no top bar.** The launcher and the account row live at the foot of
|
|
740
|
+
the rail, one press from wherever the pointer already is, and a page's own
|
|
741
|
+
heading belongs to the page — put it in `ConnectPageShell` from
|
|
742
|
+
`@cosmoops/connect-sdk/ui`. Below `md` a slim bar carries the drawer trigger
|
|
743
|
+
and nothing else.
|
|
736
744
|
|
|
737
745
|
**Routing is a prop.** The SDK ships into any React app and cannot import
|
|
738
746
|
`next/link`, so `renderLink` is how your router gets involved; the default is a
|
|
@@ -747,7 +755,7 @@ around `children` — an app that ships a shell and forgets the gate is an app
|
|
|
747
755
|
that lets a suspended workspace keep working. The chrome is never gated: the
|
|
748
756
|
sidebar, the lockup, the account menu and sign-out stay reachable in every
|
|
749
757
|
blocked state. Routes that resolve a block rather than needing one — your
|
|
750
|
-
pricing page — are flagged where they are declared:
|
|
758
|
+
pricing page — are flagged where they are declared, in either nav:
|
|
751
759
|
|
|
752
760
|
```tsx
|
|
753
761
|
{ label: "Plans", href: "/plans", gateExempt: true }
|
|
@@ -771,26 +779,38 @@ Connect, and `ai:chat` on the API key behind your bridge.
|
|
|
771
779
|
|
|
772
780
|
Other things worth knowing:
|
|
773
781
|
|
|
774
|
-
- **The rail
|
|
775
|
-
(`localStorage`).
|
|
776
|
-
|
|
777
|
-
|
|
782
|
+
- **The rail starts collapsed** and remembers the choice per browser
|
|
783
|
+
(`localStorage`). The icon rail is the resting state; expanding is the
|
|
784
|
+
deliberate act. Expanded, the collapse control sits at the header's trailing
|
|
785
|
+
edge; collapsed, hovering the lockup swaps it for the expand glyph — so the
|
|
786
|
+
rail spends no row on a permanent control, at the cost of the lockup no
|
|
787
|
+
longer being a link home while collapsed. Pass `defaultCollapsed={false}` to
|
|
788
|
+
open by default, drive it with `collapsed` + `onCollapsedChange`, or pin it
|
|
789
|
+
open with `collapsible={false}`.
|
|
790
|
+
- **The collapse animates width and nothing else.** Every row is laid out so
|
|
791
|
+
its glyph sits on the same horizontal centre in both states, and labels are
|
|
792
|
+
clipped rather than re-measured — so no row reflows and nothing drifts
|
|
793
|
+
sideways mid-transition.
|
|
794
|
+
- **Below `md` there is no rail**, only a drawer behind the mobile bar's menu
|
|
778
795
|
button — a 64px icon strip on a phone is a worse drawer, not a smaller one.
|
|
779
796
|
- **Children unfold only on the branch you are on.** A sidebar that opens
|
|
780
797
|
every branch at once is a sitemap.
|
|
781
798
|
- **No `isSignedIn` branch, in here or in yours.** The auth buttons render
|
|
782
|
-
nothing once a session exists, the account
|
|
799
|
+
nothing once a session exists, the account row renders nothing without one,
|
|
783
800
|
and the launcher renders nothing when signed out.
|
|
784
|
-
- `
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
- `theme` / `onThemeChange` pass straight through to `
|
|
801
|
+
- `secondaryNav`, `sidebarFooter` and `footer` are slots; `contentClassName`
|
|
802
|
+
replaces the content pane's default max-width and padding. (`title` and
|
|
803
|
+
`headerActions` are gone with the top bar — see the 0.12.0 changelog entry.)
|
|
804
|
+
- `theme` / `onThemeChange` pass straight through to `ConnectUserButton` — the
|
|
788
805
|
SDK has no theme provider of its own, deliberately.
|
|
789
806
|
|
|
790
|
-
`
|
|
791
|
-
likes
|
|
792
|
-
|
|
793
|
-
|
|
807
|
+
`ConnectSidebarNav` is the nav column alone, for an app with chrome it already
|
|
808
|
+
likes, and `ConnectSidebar` / `ConnectSidebarHeader` / `ConnectSidebarBody` /
|
|
809
|
+
`ConnectSidebarFooter` / `ConnectSidebarRow` are the rest of the rail — the
|
|
810
|
+
exact pieces `AppShell` composes, so chrome the props cannot express is a
|
|
811
|
+
rearrangement rather than a fork. `resolveActivePath` and `isNavItemActive` are
|
|
812
|
+
the same pure functions the rail highlights with, exported so a breadcrumb or a
|
|
813
|
+
document title derived from your nav cannot disagree with it.
|
|
794
814
|
|
|
795
815
|
### Logo
|
|
796
816
|
|
|
@@ -825,6 +845,56 @@ So the hand-off is a URL, and Connect renders it as a modal over the relevant pa
|
|
|
825
845
|
|
|
826
846
|
Full history: [CHANGELOG.md](./CHANGELOG.md). Latest first:
|
|
827
847
|
|
|
848
|
+
### 0.12.0 — breaking
|
|
849
|
+
|
|
850
|
+
The application shell is one column. **The top bar is gone**; the rail holds
|
|
851
|
+
everything it used to, and starts collapsed.
|
|
852
|
+
|
|
853
|
+
- **`title` and `headerActions` are removed** with the bar that held them. A
|
|
854
|
+
page's heading goes in `ConnectPageShell` (from `/ui`), next to that page's
|
|
855
|
+
own breadcrumbs and actions; an application-level control goes in
|
|
856
|
+
`sidebarFooter` as a `ConnectSidebarRow`.
|
|
857
|
+
- **`defaultCollapsed` now defaults to `true`.** Pass `false` for 0.11's
|
|
858
|
+
behaviour.
|
|
859
|
+
- **`ConnectSideNav` → `ConnectSidebarNav`**, **`ConnectUserMenu` →
|
|
860
|
+
`ConnectUserButton`**. Both old names remain as deprecated aliases.
|
|
861
|
+
`AppShell` keeps its name — the one export here without the `Connect`
|
|
862
|
+
prefix, as decided in 0.8.0.
|
|
863
|
+
- **New:** `secondaryNav` (a second nav group pinned to the foot of the rail),
|
|
864
|
+
`ConnectProfileModal` (the account modal with no trigger), and the rail's
|
|
865
|
+
own pieces — `ConnectSidebar`, `ConnectSidebarHeader`, `ConnectSidebarBody`,
|
|
866
|
+
`ConnectSidebarFooter`, `ConnectSidebarRow`, `ConnectShellProvider`,
|
|
867
|
+
`useConnectShell`, `sidebarRowClass`.
|
|
868
|
+
- **The workspace switcher moved into the rail's header** and is collapse-aware;
|
|
869
|
+
the account row sits at the foot with a circular avatar.
|
|
870
|
+
- **Fixed:** the closed Ask AI panel occupied 400px at `lg` and up on every page
|
|
871
|
+
with `askAi` on; sidebar contrast (the active row was **below WCAG AA** in
|
|
872
|
+
both schemes); the launcher and account rows looked permanently selected.
|
|
873
|
+
|
|
874
|
+
### 0.11.0 — breaking
|
|
875
|
+
|
|
876
|
+
- **`ConnectUser.workspaceId`, `workspaceName` and `workspaceType` are
|
|
877
|
+
nullable.** They are `null` for exactly one thing: a **Connect platform
|
|
878
|
+
staff** session, which belongs to no workspace. If you never enable staff
|
|
879
|
+
access — it is off by default — this is a type narrow and nothing else.
|
|
880
|
+
`?? ""` is the wrong fix: decide what your tenanting does with a session that
|
|
881
|
+
has no tenant.
|
|
882
|
+
- **Connect platform staff sign-in**, `platformAccess` on `exchangeCode()` and
|
|
883
|
+
`getUser()`, `<ConnectPlatformGate>` and `useConnectPlatformAccess()`.
|
|
884
|
+
|
|
885
|
+
### 0.10.0
|
|
886
|
+
|
|
887
|
+
- **`@cosmoops/connect-sdk/ui`** — `ConnectPageShell`, `ConnectSection`,
|
|
888
|
+
`ConnectEmptyState`, `ConnectStatusChip`, `ConnectSettingRow`. Deliberately
|
|
889
|
+
*not* a `"use client"` module, so a page shell does not become a client
|
|
890
|
+
boundary.
|
|
891
|
+
- `ConnectLinkButton`, `ConnectConfirmDialog`, `ConnectCursorPagination`,
|
|
892
|
+
`ConnectFilterTabs`, `ConnectAccountSessions` — built since 0.8 and reachable
|
|
893
|
+
from no entry point until now.
|
|
894
|
+
- `ConnectWorkspaceSwitcher`, `useConnectWorkspaces()`,
|
|
895
|
+
`useConnectWorkspaceMembers()`, and the `workspaces` resource on the server
|
|
896
|
+
client.
|
|
897
|
+
|
|
828
898
|
### 0.9.0
|
|
829
899
|
|
|
830
900
|
- **Ask AI**, mounted by `AppShell` by default — a chat panel grounded in the
|
package/dist/cjs/cli/doctor.js
CHANGED
|
@@ -162,8 +162,8 @@ function checkEnv(root) {
|
|
|
162
162
|
status: "fail",
|
|
163
163
|
title: `${name} set`,
|
|
164
164
|
detail: declaredEmpty
|
|
165
|
-
? `Declared but empty in .env.local. The value is in your application's
|
|
166
|
-
: `Not found in .env.local or the environment. Both values are in your application's
|
|
165
|
+
? `Declared but empty in .env.local. The value is in your application's Connect client section in Connect.`
|
|
166
|
+
: `Not found in .env.local or the environment. Both values are in your application's Connect client section in Connect.`,
|
|
167
167
|
};
|
|
168
168
|
});
|
|
169
169
|
}
|
|
@@ -176,7 +176,7 @@ function checkRedirectUri() {
|
|
|
176
176
|
return {
|
|
177
177
|
status: "warn",
|
|
178
178
|
title: "Redirect URI registered in Connect",
|
|
179
|
-
detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your application's
|
|
179
|
+
detail: "Not checkable from here. Confirm {your origin}/api/connect/callback is listed as a redirect URI in your application's Connect client section - including the deployed origin, not just http://localhost:3000.",
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
const MARKER = { pass: " ok ", warn: " note ", fail: " FAIL " };
|
package/dist/cjs/cli/init.js
CHANGED
|
@@ -44,7 +44,7 @@ const PLANS_PAGE = `"use client";
|
|
|
44
44
|
// with the other exports) whenever you want your own copy or layout.
|
|
45
45
|
export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
|
|
46
46
|
`;
|
|
47
|
-
const ENV_TEMPLATE = `# CosmoOps Connect - values come from your application's
|
|
47
|
+
const ENV_TEMPLATE = `# CosmoOps Connect - values come from your application's Connect client section.
|
|
48
48
|
CONNECT_API_KEY=
|
|
49
49
|
CONNECT_CLIENT_ID=
|
|
50
50
|
`;
|
|
@@ -111,7 +111,7 @@ function runInit(argv = []) {
|
|
|
111
111
|
console.log(` ${marker} ${(0, node_path_1.relative)(root, target)} - ${scaffold.description}`);
|
|
112
112
|
}
|
|
113
113
|
const env = ensureEnv(root);
|
|
114
|
-
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your application's
|
|
114
|
+
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the two values from your application's Connect client section`);
|
|
115
115
|
// The two steps a scaffolder must not do for you: editing an existing layout
|
|
116
116
|
// means parsing and rewriting JSX someone else wrote, and the redirect URI
|
|
117
117
|
// lives in Connect, not in this repo.
|
|
@@ -137,7 +137,7 @@ Three things left, all one-time:
|
|
|
137
137
|
Tailwind roots its content scan inside node_modules and skips your app.
|
|
138
138
|
|
|
139
139
|
3. Register {your origin}/api/connect/callback as a redirect URI in your
|
|
140
|
-
application's
|
|
140
|
+
application's Connect client section in Connect.
|
|
141
141
|
|
|
142
142
|
No next.config.ts changes needed - not even transpilePackages: dist/ is
|
|
143
143
|
already ES2022 with "use client" hoisted, so Turbopack and webpack consume it
|
|
@@ -40,12 +40,31 @@ function formatDate(iso) {
|
|
|
40
40
|
function useConnectAccessGate() {
|
|
41
41
|
const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
|
|
42
42
|
const { data, isLoading: entitlementsLoading } = (0, provider_js_1.useConnectEntitlements)();
|
|
43
|
-
if (sessionLoading
|
|
43
|
+
if (sessionLoading) {
|
|
44
44
|
return { state: "loading", blocker: null, paymentRetry: null };
|
|
45
45
|
}
|
|
46
46
|
if (!session?.user) {
|
|
47
47
|
return { state: "signed_out", blocker: null, paymentRetry: null };
|
|
48
48
|
}
|
|
49
|
+
// Connect platform staff pass straight through, and this check has to come
|
|
50
|
+
// *before* the entitlements read below or the gate would hard-block them.
|
|
51
|
+
//
|
|
52
|
+
// Every state this gate can report is a fact about a workspace - suspended,
|
|
53
|
+
// no plan, wrong audience, awaiting approval, payment due. A staff session
|
|
54
|
+
// has no workspace at all (see `SessionAccess.staff`), so it has no subscription
|
|
55
|
+
// either, and `subscription_missing` would send an operator to your plan
|
|
56
|
+
// picker to buy a plan for a workspace that does not exist. "Nothing is
|
|
57
|
+
// blocking this session" is the truthful answer, not a lenient one.
|
|
58
|
+
//
|
|
59
|
+
// The entitlements request is still issued - hooks cannot be called
|
|
60
|
+
// conditionally - and the bridge answers it `403 no_workspace`. That costs
|
|
61
|
+
// one failed request per staff page load, and its result is ignored here.
|
|
62
|
+
if (session.staff) {
|
|
63
|
+
return { state: "clear", blocker: null, paymentRetry: null };
|
|
64
|
+
}
|
|
65
|
+
if (entitlementsLoading) {
|
|
66
|
+
return { state: "loading", blocker: null, paymentRetry: null };
|
|
67
|
+
}
|
|
49
68
|
if (!data) {
|
|
50
69
|
// Signed in, not loading, yet nothing came back - a failed read (see
|
|
51
70
|
// `error` on `useConnectEntitlements`). Fail open rather than hard-block
|
|
@@ -12,7 +12,11 @@ const apps_grid_js_1 = require("./apps-grid.js");
|
|
|
12
12
|
const ask_ai_js_1 = require("./ask-ai.js");
|
|
13
13
|
const auth_buttons_js_1 = require("./auth-buttons.js");
|
|
14
14
|
const logo_js_1 = require("./logo.js");
|
|
15
|
+
const motion_js_1 = require("./motion.js");
|
|
16
|
+
const sidebar_js_1 = require("./sidebar.js");
|
|
15
17
|
const side_nav_js_1 = require("./side-nav.js");
|
|
18
|
+
const shell_context_js_1 = require("./shell-context.js");
|
|
19
|
+
const workspace_switcher_js_1 = require("./workspace-switcher.js");
|
|
16
20
|
const user_menu_js_1 = require("./user-menu.js");
|
|
17
21
|
const nav_js_1 = require("./nav.js");
|
|
18
22
|
const use_active_path_js_1 = require("../use-active-path.js");
|
|
@@ -37,7 +41,7 @@ function collapsedSnapshot() {
|
|
|
37
41
|
/**
|
|
38
42
|
* No preference on the server, which is the point: `useSyncExternalStore` uses
|
|
39
43
|
* this for the hydrating render and only then swaps in the real reading, so a
|
|
40
|
-
* returning user's
|
|
44
|
+
* returning user's rail state can never make the server's HTML and the
|
|
41
45
|
* client's first pass disagree. Reading storage during render instead is a
|
|
42
46
|
* hydration mismatch waiting for its first user.
|
|
43
47
|
*/
|
|
@@ -55,36 +59,21 @@ function subscribeCollapsed(onStoreChange) {
|
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
58
|
-
* The
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return renderLink({
|
|
62
|
-
href: homeHref,
|
|
63
|
-
className: `flex items-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${collapsed ? "justify-center" : ""}`,
|
|
64
|
-
// `ConnectLogo` names itself, but it does it with `role="img"` on a
|
|
65
|
-
// `<span>`, which the `<a>` wrapping it does not inherit as its own name.
|
|
66
|
-
"aria-label": `${name} home`,
|
|
67
|
-
children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon, collapsed: collapsed }),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* The application frame every CosmoOps application platform runs inside: a
|
|
72
|
-
* collapsible sidebar, a top bar carrying the app launcher and the account
|
|
73
|
-
* menu, and your pages in the middle.
|
|
62
|
+
* The application frame every CosmoOps application platform runs inside: one
|
|
63
|
+
* rail down the side holding the brand, the workspace, the navigation and the
|
|
64
|
+
* account, and your pages in the rest.
|
|
74
65
|
*
|
|
75
66
|
* This is the piece each partner was otherwise writing by hand, and writing
|
|
76
|
-
* slightly differently - which is how a family of applications stops looking
|
|
77
|
-
* one product. Everything that should be identical across platforms
|
|
78
|
-
*
|
|
79
|
-
* account menu, the access gate) lives here; everything that genuinely
|
|
80
|
-
* differs is a prop:
|
|
67
|
+
* slightly differently - which is how a family of applications stops looking
|
|
68
|
+
* like one product. Everything that should be identical across platforms
|
|
69
|
+
* lives here; everything that genuinely differs is a prop:
|
|
81
70
|
*
|
|
82
71
|
* ```tsx
|
|
83
72
|
* "use client";
|
|
84
73
|
* import Link from "next/link";
|
|
85
74
|
* import { usePathname } from "next/navigation";
|
|
86
75
|
* import { AppShell } from "@cosmoops/connect-sdk/client";
|
|
87
|
-
* import { LayoutDashboard, Truck } from "lucide-react";
|
|
76
|
+
* import { LayoutDashboard, Settings, Truck } from "lucide-react";
|
|
88
77
|
*
|
|
89
78
|
* export function Shell({ children }: { children: React.ReactNode }) {
|
|
90
79
|
* return (
|
|
@@ -95,7 +84,10 @@ function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
|
|
|
95
84
|
* renderLink={(props) => <Link {...props} />}
|
|
96
85
|
* nav={[
|
|
97
86
|
* { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
|
|
98
|
-
* { label: "
|
|
87
|
+
* { label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
|
|
88
|
+
* ]}
|
|
89
|
+
* secondaryNav={[
|
|
90
|
+
* { label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
|
|
99
91
|
* ]}
|
|
100
92
|
* >
|
|
101
93
|
* {children}
|
|
@@ -104,16 +96,36 @@ function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
|
|
|
104
96
|
* }
|
|
105
97
|
* ```
|
|
106
98
|
*
|
|
99
|
+
* **There is no top bar.** There used to be one, carrying a page title, the
|
|
100
|
+
* launcher and the account menu - and it was a second, half-width strip of
|
|
101
|
+
* chrome saying things the rail was better placed to say. The launcher and
|
|
102
|
+
* the account row moved into the rail's footer, where they are one press from
|
|
103
|
+
* wherever the pointer already is; the page title moved into the page, where
|
|
104
|
+
* `ConnectPageShell` renders it next to that page's own actions and
|
|
105
|
+
* breadcrumbs. What is left is a single column of chrome and a full-height
|
|
106
|
+
* content pane, which is what every application this shell is modelled on
|
|
107
|
+
* settled at.
|
|
108
|
+
*
|
|
109
|
+
* The one exception is below `md`, where a slim bar carries the drawer
|
|
110
|
+
* trigger. A phone has no rail to put it in, and a navigation you cannot open
|
|
111
|
+
* is not a smaller navigation.
|
|
112
|
+
*
|
|
113
|
+
* **The rail starts collapsed.** `defaultCollapsed` is `true`: the icon rail
|
|
114
|
+
* is the resting state, and expanding is the deliberate act. That is the right
|
|
115
|
+
* default because the rail's job is to be *there*, not to be read - a person
|
|
116
|
+
* learns five glyphs in a day and then wants the 200px back for their data.
|
|
117
|
+
* A stored preference from the user's last visit always wins over it.
|
|
118
|
+
*
|
|
107
119
|
* There is no `isSignedIn` branch anywhere in here, and there should be none
|
|
108
|
-
* in yours: the auth buttons render nothing once a session exists, the
|
|
109
|
-
*
|
|
120
|
+
* in yours: the auth buttons render nothing once a session exists, the account
|
|
121
|
+
* row renders nothing without one, and the launcher renders nothing when
|
|
110
122
|
* signed out. The swap happens inside the components.
|
|
111
123
|
*
|
|
112
124
|
* It renders no provider of its own - mount `ConnectProvider` above it, once,
|
|
113
125
|
* so a page with a shell, a plan table and a usage meter still makes one
|
|
114
126
|
* session read.
|
|
115
127
|
*/
|
|
116
|
-
function AppShell({ name, icon, homeHref = "/", nav, activePath, renderLink =
|
|
128
|
+
function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, renderLink = shell_context_js_1.defaultRenderLink, sidebarFooter, workspaceSwitcher, footer, defaultCollapsed = true, collapsed: controlledCollapsed, onCollapsedChange, collapsible = true, showAppsGrid = true, showAuthButtons = true, theme, onThemeChange, gate = true, gateExemptRoutes, askAi = true, className, contentClassName, children, }) {
|
|
117
129
|
const { session, isLoading } = (0, provider_js_1.useConnect)();
|
|
118
130
|
const pathname = (0, use_active_path_js_1.useActivePath)(activePath);
|
|
119
131
|
const [mobileOpen, setMobileOpen] = (0, react_1.useState)(false);
|
|
@@ -123,8 +135,7 @@ function AppShell({ name, icon, homeHref = "/", nav, activePath, renderLink = si
|
|
|
123
135
|
const stored = (0, react_1.useSyncExternalStore)(subscribeCollapsed, collapsedSnapshot, serverCollapsedSnapshot);
|
|
124
136
|
const isControlled = controlledCollapsed !== undefined;
|
|
125
137
|
const collapsed = collapsible ? (isControlled ? controlledCollapsed : (stored ?? defaultCollapsed)) : false;
|
|
126
|
-
const
|
|
127
|
-
const next = !collapsed;
|
|
138
|
+
const setCollapsed = (0, react_1.useCallback)((next) => {
|
|
128
139
|
onCollapsedChange?.(next);
|
|
129
140
|
if (isControlled)
|
|
130
141
|
return;
|
|
@@ -135,24 +146,47 @@ function AppShell({ name, icon, homeHref = "/", nav, activePath, renderLink = si
|
|
|
135
146
|
// Not persisted - but the event below still moves the rail for this session.
|
|
136
147
|
}
|
|
137
148
|
window.dispatchEvent(new Event(COLLAPSED_EVENT));
|
|
138
|
-
}, [
|
|
149
|
+
}, [isControlled, onCollapsedChange]);
|
|
139
150
|
const closeMobile = (0, react_1.useCallback)(() => setMobileOpen(false), []);
|
|
151
|
+
// Connect platform staff see a different set of routes - see
|
|
152
|
+
// `ConnectNavItem.audience`. Derived from the session rather than passed in,
|
|
153
|
+
// so a host app never has to thread "is this an operator" through its own
|
|
154
|
+
// layout to get the right rail.
|
|
155
|
+
const isStaff = session?.staff === true;
|
|
156
|
+
const visibleNav = (0, react_1.useMemo)(() => (0, nav_js_1.filterNavForAudience)(nav, isStaff), [nav, isStaff]);
|
|
157
|
+
const visibleSecondaryNav = (0, react_1.useMemo)(() => (secondaryNav ? (0, nav_js_1.filterNavForAudience)(secondaryNav, isStaff) : undefined), [secondaryNav, isStaff]);
|
|
140
158
|
const currentPath = (0, nav_js_1.normalizePath)(pathname);
|
|
141
|
-
|
|
159
|
+
// Computed from the *unfiltered* nav on purpose: `gateExempt` is a fact
|
|
160
|
+
// about a route, not about who can see it, and reading it from the filtered
|
|
161
|
+
// list would quietly re-gate a route the moment it became staff-only. Both
|
|
162
|
+
// navs contribute - a `gateExempt` "Plans" route is just as often in the
|
|
163
|
+
// secondary group as the primary one.
|
|
164
|
+
const exempt = [
|
|
165
|
+
...(0, nav_js_1.gateExemptPaths)(nav),
|
|
166
|
+
...(secondaryNav ? (0, nav_js_1.gateExemptPaths)(secondaryNav) : []),
|
|
167
|
+
...(gateExemptRoutes ?? []).map(nav_js_1.normalizePath),
|
|
168
|
+
];
|
|
142
169
|
const isExempt = exempt.some((path) => currentPath === path || currentPath.startsWith(`${path}/`));
|
|
143
170
|
// A signed-out visitor has no readiness to check and nothing to be blocked
|
|
144
171
|
// by; `ConnectAccessGate` passes them straight through, but mounting it at
|
|
145
172
|
// all costs an entitlements read that cannot succeed.
|
|
146
173
|
const signedOut = !isLoading && !session?.user;
|
|
147
174
|
const gated = gate && !isExempt && !signedOut;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
175
|
+
// Two shell contexts, one set of children. The drawer's is always expanded
|
|
176
|
+
// and closes itself on navigation; the rail's carries the real collapse
|
|
177
|
+
// state. Building them here rather than inside the sidebar is what lets the
|
|
178
|
+
// same `railContent` render in both places with no `mobile` prop threaded
|
|
179
|
+
// through every part.
|
|
180
|
+
const railShell = (0, react_1.useMemo)(() => ({ collapsed, collapsible, setCollapsed, renderLink, activePath }), [collapsed, collapsible, setCollapsed, renderLink, activePath]);
|
|
181
|
+
const drawerShell = (0, react_1.useMemo)(() => ({
|
|
182
|
+
collapsed: false,
|
|
183
|
+
collapsible: false,
|
|
184
|
+
setCollapsed: () => { },
|
|
185
|
+
renderLink,
|
|
186
|
+
activePath,
|
|
187
|
+
onNavigate: closeMobile,
|
|
188
|
+
}), [renderLink, activePath, closeMobile]);
|
|
189
|
+
const accountRow = signedOut ? (showAuthButtons && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm", fullWidth: true }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : ((0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
|
|
190
|
+
const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, {}) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
|
|
191
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0", children: (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col", children: railContent }) }) }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { className: "flex-1 overflow-y-auto", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-8 md:py-8", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
|
|
158
192
|
}
|
|
@@ -7,6 +7,7 @@ const react_1 = require("@heroui/react");
|
|
|
7
7
|
const lucide_react_1 = require("lucide-react");
|
|
8
8
|
const provider_js_1 = require("../provider.js");
|
|
9
9
|
const motion_js_1 = require("./motion.js");
|
|
10
|
+
const shell_context_js_1 = require("./shell-context.js");
|
|
10
11
|
function AppTile({ app }) {
|
|
11
12
|
const disabled = app.launchUrl === null;
|
|
12
13
|
const body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("span", { className: "relative flex size-12 items-center justify-center overflow-hidden rounded-xl bg-surface-secondary", children: [app.iconUrl ? (
|
|
@@ -33,13 +34,13 @@ function AppTile({ app }) {
|
|
|
33
34
|
* Renders nothing when signed out: a launcher with nothing to launch is just a
|
|
34
35
|
* button that opens an empty box.
|
|
35
36
|
*/
|
|
36
|
-
function ConnectAppsGrid({ className, label = "Switch app" }) {
|
|
37
|
+
function ConnectAppsGrid({ className, label = "Switch app", variant = "row" }) {
|
|
37
38
|
const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
|
|
38
39
|
const { data, isLoading, error } = (0, provider_js_1.useConnectApps)();
|
|
39
40
|
if (sessionLoading || !session?.user)
|
|
40
41
|
return null;
|
|
41
42
|
const apps = data?.apps ?? [];
|
|
42
|
-
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsx)(AppTile, { app: app }) }, app.applicationId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
|
|
43
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [variant === "row" ? ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": label, className: (0, shell_context_js_1.sidebarRowClass)({ className }), children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-5 shrink-0 items-center justify-center", children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: label })] })) : ((0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsx)(AppTile, { app: app }) }, app.applicationId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
|
|
43
44
|
}
|
|
44
45
|
function ConnectBrowseAllLink() {
|
|
45
46
|
const { appUrl } = (0, provider_js_1.useConnect)();
|
|
@@ -9,7 +9,26 @@ const lucide_react_1 = require("lucide-react");
|
|
|
9
9
|
const streamdown_1 = require("streamdown");
|
|
10
10
|
const provider_js_1 = require("../provider.js");
|
|
11
11
|
const ai_widgets_js_1 = require("./ai-widgets.js");
|
|
12
|
+
/**
|
|
13
|
+
* The panel's open width. The *inner* pane always carries it, so the content
|
|
14
|
+
* keeps its layout while the outer shell's width animates to zero around it.
|
|
15
|
+
*/
|
|
12
16
|
const PANEL_WIDTH = "w-full lg:w-[400px]";
|
|
17
|
+
/**
|
|
18
|
+
* The outer shell's width, which is the thing that animates.
|
|
19
|
+
*
|
|
20
|
+
* Two mutually exclusive branches rather than `PANEL_WIDTH` plus an
|
|
21
|
+
* `lg:w-0` override, because that override never applied: `lg:w-[400px]` and
|
|
22
|
+
* `lg:w-0` are the same utility at the same breakpoint, so the winner is
|
|
23
|
+
* whichever Tailwind emits later - and it emits arbitrary values after scale
|
|
24
|
+
* values. The closed panel therefore measured a full 400px at `lg` and up and
|
|
25
|
+
* sat there permanently, eating that much of the content pane on every page
|
|
26
|
+
* with `askAi` on. Only the `translate-x` hid it below `lg`, which is why this
|
|
27
|
+
* looked correct on a phone and wrong on a laptop.
|
|
28
|
+
*/
|
|
29
|
+
function panelShellWidth(isOpen) {
|
|
30
|
+
return isOpen ? PANEL_WIDTH : "w-full lg:w-0 lg:overflow-hidden lg:border-s-0";
|
|
31
|
+
}
|
|
13
32
|
/**
|
|
14
33
|
* `blurIn` over the default `fadeIn`: Gemini arrives in bursts rather than an
|
|
15
34
|
* even token drip, and a blur masks a batch landing at once where a plain
|
|
@@ -328,7 +347,7 @@ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placehold
|
|
|
328
347
|
setInput("");
|
|
329
348
|
void send(text);
|
|
330
349
|
};
|
|
331
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${
|
|
350
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${panelShellWidth(isOpen)} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width]`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
|
|
332
351
|
}
|
|
333
352
|
/** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
|
|
334
353
|
const LINK_SAFETY = { enabled: false };
|