create-ailk 0.11.3 → 0.11.4
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/package.json +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +81 -6
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +145 -23
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +31 -5
- package/templates/apps/web/app/[locale]/(authed)/account/for-you-labels.ts +2 -2
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +5 -4
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +64 -40
- package/templates/apps/web/messages/en.json +1 -0
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +33 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"tar": "^7.5.21"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@working-theory/schema": "0.
|
|
31
|
-
"@working-theory/templates": "0.
|
|
30
|
+
"@working-theory/schema": "0.32.0",
|
|
31
|
+
"@working-theory/templates": "0.32.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
* by side); Log out signs out and routes home.
|
|
10
10
|
* 3. ? opens Contact support (→ /account/support) and, only when the site
|
|
11
11
|
* configures a help URL, Help center.
|
|
12
|
-
* 4. Below md the tab bar reads avatar · Inbox · Help
|
|
12
|
+
* 4. Below md the tab bar reads avatar · Inbox · For you · Help, with no
|
|
13
|
+
* count on any tab (#7469, DESIGN.md §2b as amended 2026-10-01); For
|
|
14
|
+
* you is `?tab=for-you` of the Inbox route, and the frame hands the
|
|
15
|
+
* tab to the page (PhoneTabProvider).
|
|
13
16
|
* 5. The For-you items are the aside; none → no aside.
|
|
14
17
|
* 6. No site chrome (Nav, Footer, ThemeToggle) inside the frame.
|
|
15
18
|
*/
|
|
16
19
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
20
|
+
import type { ReactElement } from "react";
|
|
17
21
|
|
|
18
22
|
jest.mock("next-intl/server", () => {
|
|
19
23
|
const msgs: unknown = require("../../../../../messages/en.json");
|
|
@@ -69,14 +73,23 @@ jest.mock("better-auth/react", () => ({
|
|
|
69
73
|
const mockPush = jest.fn();
|
|
70
74
|
const mockRefresh = jest.fn();
|
|
71
75
|
let mockPathname = "/account";
|
|
76
|
+
let mockSearch = "";
|
|
72
77
|
jest.mock("next/navigation", () => ({
|
|
73
78
|
__esModule: true,
|
|
74
79
|
useRouter: () => ({ push: mockPush, refresh: mockRefresh }),
|
|
75
80
|
usePathname: () => mockPathname,
|
|
81
|
+
useSearchParams: () => new URLSearchParams(mockSearch),
|
|
76
82
|
}));
|
|
77
83
|
|
|
84
|
+
import { usePhoneTab } from "@working-theory/templates/account";
|
|
85
|
+
|
|
78
86
|
import AccountLayout from "../layout";
|
|
79
87
|
|
|
88
|
+
/** A page that shows the phone tab the frame hands it. */
|
|
89
|
+
function PhoneTabProbe(): ReactElement {
|
|
90
|
+
return <p data-testid="phone-tab-probe">{usePhoneTab()}</p>;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
const SESSION = {
|
|
81
94
|
user: {
|
|
82
95
|
id: "user_1",
|
|
@@ -107,7 +120,12 @@ function openByPointer(trigger: HTMLElement): void {
|
|
|
107
120
|
|
|
108
121
|
async function renderLayout(locale = "en"): Promise<void> {
|
|
109
122
|
const element = await AccountLayout({
|
|
110
|
-
children:
|
|
123
|
+
children: (
|
|
124
|
+
<>
|
|
125
|
+
<p data-testid="account-child">page</p>
|
|
126
|
+
<PhoneTabProbe />
|
|
127
|
+
</>
|
|
128
|
+
),
|
|
111
129
|
params: Promise.resolve({ locale }),
|
|
112
130
|
});
|
|
113
131
|
render(element);
|
|
@@ -118,6 +136,7 @@ const ORIGINAL_HELP_URL = process.env.AILK_HELP_CENTER_URL;
|
|
|
118
136
|
beforeEach(() => {
|
|
119
137
|
window.localStorage.clear();
|
|
120
138
|
mockPathname = "/account";
|
|
139
|
+
mockSearch = "";
|
|
121
140
|
delete process.env.AILK_HELP_CENTER_URL;
|
|
122
141
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
123
142
|
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: [] });
|
|
@@ -241,22 +260,78 @@ describe("the help menu (D13)", () => {
|
|
|
241
260
|
});
|
|
242
261
|
});
|
|
243
262
|
|
|
244
|
-
describe("the phone frame (D13)", () => {
|
|
245
|
-
it("the tab bar reads avatar · Inbox · Help, and Help opens the same menu", async () => {
|
|
263
|
+
describe("the phone frame (D13; #7469 — the For you tab)", () => {
|
|
264
|
+
it("the tab bar reads avatar · Inbox · For you · Help, and Help opens the same menu", async () => {
|
|
246
265
|
await renderLayout();
|
|
247
266
|
const bar = screen.getByTestId("bottom-tab-bar");
|
|
248
267
|
const cells = Array.from(bar.children);
|
|
249
|
-
expect(cells).toHaveLength(
|
|
268
|
+
expect(cells).toHaveLength(4);
|
|
250
269
|
expect(cells[0]).toBe(screen.getByTestId("bottom-tab-bar-leading"));
|
|
251
270
|
expect(cells[1]).toHaveTextContent("Inbox");
|
|
271
|
+
expect(cells[1]).toHaveAttribute("href", "/account");
|
|
252
272
|
expect(cells[1]).toHaveAttribute("aria-current", "page");
|
|
253
|
-
expect(cells[2]).
|
|
273
|
+
expect(cells[2]).toHaveTextContent("For you");
|
|
274
|
+
expect(cells[2]).toHaveAttribute("href", "/account?tab=for-you");
|
|
275
|
+
expect(cells[2]).not.toHaveAttribute("aria-current");
|
|
276
|
+
expect(cells[3]).toBe(screen.getByTestId("bottom-tab-bar-trailing"));
|
|
254
277
|
openByPointer(screen.getByTestId("frame-bottom-bar-help"));
|
|
255
278
|
expect(
|
|
256
279
|
within(screen.getByTestId("signed-in-shell-help-menu")).getByText("Contact support"),
|
|
257
280
|
).toBeInTheDocument();
|
|
258
281
|
});
|
|
259
282
|
|
|
283
|
+
it("no tab carries a count, even with For-you items waiting", async () => {
|
|
284
|
+
mockGetSession.mockResolvedValue({
|
|
285
|
+
user: { ...SESSION.user, name: null, emailVerified: false },
|
|
286
|
+
});
|
|
287
|
+
await renderLayout();
|
|
288
|
+
const bar = screen.getByTestId("bottom-tab-bar");
|
|
289
|
+
const tabs = within(bar).getAllByRole("link");
|
|
290
|
+
expect(tabs.map((tab) => tab.textContent)).toEqual(["Inbox", "For you"]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("a non-default locale prefixes both tabs", async () => {
|
|
294
|
+
mockPathname = "/fr/account";
|
|
295
|
+
await renderLayout("fr");
|
|
296
|
+
const tabs = within(screen.getByTestId("bottom-tab-bar")).getAllByRole("link");
|
|
297
|
+
expect(tabs.map((tab) => tab.getAttribute("href"))).toEqual([
|
|
298
|
+
"/fr/account",
|
|
299
|
+
"/fr/account?tab=for-you",
|
|
300
|
+
]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("on ?tab=for-you, For you is selected, Inbox is not, and the page is handed the tab", async () => {
|
|
304
|
+
mockSearch = "tab=for-you";
|
|
305
|
+
await renderLayout();
|
|
306
|
+
const [inbox, forYou] = within(screen.getByTestId("bottom-tab-bar")).getAllByRole("link");
|
|
307
|
+
expect(forYou).toHaveAttribute("aria-current", "page");
|
|
308
|
+
expect(inbox).not.toHaveAttribute("aria-current");
|
|
309
|
+
expect(screen.getByTestId("phone-tab-probe")).toHaveTextContent("for-you");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("on the Inbox the page is handed the inbox tab", async () => {
|
|
313
|
+
await renderLayout();
|
|
314
|
+
expect(screen.getByTestId("phone-tab-probe")).toHaveTextContent("inbox");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("an unknown ?tab= is the Inbox", async () => {
|
|
318
|
+
mockSearch = "tab=library";
|
|
319
|
+
await renderLayout();
|
|
320
|
+
const [inbox] = within(screen.getByTestId("bottom-tab-bar")).getAllByRole("link");
|
|
321
|
+
expect(inbox).toHaveAttribute("aria-current", "page");
|
|
322
|
+
expect(screen.getByTestId("phone-tab-probe")).toHaveTextContent("inbox");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("an Inbox item page keeps the Inbox tab selected, whatever ?tab= says", async () => {
|
|
326
|
+
mockPathname = "/account/inbox/purchase/fc_1";
|
|
327
|
+
mockSearch = "tab=for-you";
|
|
328
|
+
await renderLayout();
|
|
329
|
+
const [inbox, forYou] = within(screen.getByTestId("bottom-tab-bar")).getAllByRole("link");
|
|
330
|
+
expect(inbox).toHaveAttribute("aria-current", "page");
|
|
331
|
+
expect(forYou).not.toHaveAttribute("aria-current");
|
|
332
|
+
expect(screen.getByTestId("phone-tab-probe")).toHaveTextContent("inbox");
|
|
333
|
+
});
|
|
334
|
+
|
|
260
335
|
it("the phone top bar carries the site logo, linking home", async () => {
|
|
261
336
|
await renderLayout();
|
|
262
337
|
const logos = screen.getAllByTestId("account-site-logo");
|
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
* 2. `result` and `purchase` rows sit under Recents and All, newest first,
|
|
9
9
|
* each linking to its item page.
|
|
10
10
|
* 3. `email-unverified` and `payment-due` are For-you items (the phone
|
|
11
|
-
*
|
|
11
|
+
* For you tab), never Inbox rows.
|
|
12
12
|
* 4. A member with no name gets an "Add your name" For-you item (needs-you,
|
|
13
13
|
* an Open action) linking to User settings › General.
|
|
14
14
|
* 5. A failed read renders an error row with Retry — never an empty Inbox.
|
|
15
|
+
* 6. #7469 (DESIGN.md §2b as amended 2026-10-01) — the route is the
|
|
16
|
+
* phone's two main tabs. The Inbox tab is the filter tabs and the list
|
|
17
|
+
* only: no For-you stack, no bottom sheet, no large title. The For you
|
|
18
|
+
* tab lists every For-you item in one inset card, and ✕ dismisses one.
|
|
19
|
+
* At `md`+ only the Inbox panel shows (the aside holds For you).
|
|
15
20
|
*/
|
|
16
21
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
17
22
|
|
|
@@ -73,6 +78,8 @@ jest.mock("next/navigation", () => ({
|
|
|
73
78
|
usePathname: () => "/account",
|
|
74
79
|
}));
|
|
75
80
|
|
|
81
|
+
import { PhoneTabProvider } from "@working-theory/templates/account";
|
|
82
|
+
|
|
76
83
|
import { accountExperimentKeys } from "../../../../../lib/account-config";
|
|
77
84
|
import Page from "../page";
|
|
78
85
|
|
|
@@ -114,12 +121,24 @@ const NO_SUBSCRIPTION = {
|
|
|
114
121
|
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
|
|
115
122
|
};
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
/** Renders the page on a phone tab — the frame's PhoneTabProvider. */
|
|
125
|
+
async function renderPage(locale = "en", tab = "inbox"): Promise<void> {
|
|
118
126
|
const element = await Page({ params: Promise.resolve({ locale }) });
|
|
119
|
-
render(element);
|
|
127
|
+
render(<PhoneTabProvider tab={tab}>{element}</PhoneTabProvider>);
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
const UNVERIFIED_AND_PAST_DUE = (): void => {
|
|
131
|
+
mockGetSession.mockResolvedValue({
|
|
132
|
+
user: { ...SESSION.user, emailVerified: false },
|
|
133
|
+
});
|
|
134
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
135
|
+
ok: true,
|
|
136
|
+
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
122
140
|
beforeEach(() => {
|
|
141
|
+
window.localStorage.clear();
|
|
123
142
|
accountExperimentKeys.length = 0;
|
|
124
143
|
mockGetSession.mockReset().mockResolvedValue(SESSION);
|
|
125
144
|
mockSignupMe.mockReset().mockResolvedValue({ ok: true, body: SIGNUP });
|
|
@@ -195,18 +214,11 @@ describe("the Inbox — D6a tabs (AC-7)", () => {
|
|
|
195
214
|
});
|
|
196
215
|
|
|
197
216
|
describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
198
|
-
it("email-unverified and payment-due are For
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
ok: true,
|
|
204
|
-
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
205
|
-
});
|
|
206
|
-
await renderPage();
|
|
207
|
-
const forYou = screen.getByTestId("inbox-for-you");
|
|
208
|
-
expect(forYou).toHaveClass("md:hidden");
|
|
209
|
-
expect(within(forYou).getByTestId("for-you-stack-count")).toHaveTextContent("2");
|
|
217
|
+
it("email-unverified and payment-due are For you tab items, never Inbox rows", async () => {
|
|
218
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
219
|
+
await renderPage("en", "for-you");
|
|
220
|
+
const forYou = screen.getByTestId("inbox-panel-for-you");
|
|
221
|
+
expect(within(forYou).getAllByTestId("for-you-item")).toHaveLength(2);
|
|
210
222
|
const list = screen.getByTestId("inbox-list");
|
|
211
223
|
expect(within(list).queryByText("Email not verified")).not.toBeInTheDocument();
|
|
212
224
|
expect(
|
|
@@ -214,15 +226,10 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
|
214
226
|
).not.toBeInTheDocument();
|
|
215
227
|
});
|
|
216
228
|
|
|
217
|
-
it("nothing for you → no For you section", async () => {
|
|
218
|
-
await renderPage();
|
|
219
|
-
expect(screen.queryByTestId("inbox-for-you")).not.toBeInTheDocument();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
229
|
it("a member with no name sees 'Add your name' (needs-you, Open → User settings › General)", async () => {
|
|
223
230
|
mockGetSession.mockResolvedValue({ user: { ...SESSION.user, name: "" } });
|
|
224
|
-
await renderPage();
|
|
225
|
-
const forYou = screen.getByTestId("inbox-for-you");
|
|
231
|
+
await renderPage("en", "for-you");
|
|
232
|
+
const forYou = screen.getByTestId("inbox-panel-for-you");
|
|
226
233
|
const item = within(forYou).getByTestId("for-you-item");
|
|
227
234
|
expect(item).toHaveAttribute("data-item-id", "add-name");
|
|
228
235
|
expect(item).toHaveTextContent("Add your name");
|
|
@@ -235,6 +242,101 @@ describe("For-you items, not Inbox rows (AC-7, D9 as amended)", () => {
|
|
|
235
242
|
});
|
|
236
243
|
});
|
|
237
244
|
|
|
245
|
+
describe("the phone Inbox tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
246
|
+
it("holds the filter tabs and the list only — no For-you stack and no bottom sheet", async () => {
|
|
247
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
248
|
+
await renderPage();
|
|
249
|
+
const inbox = screen.getByTestId("inbox-panel-inbox");
|
|
250
|
+
expect(within(inbox).getByTestId("inbox-list")).toBeInTheDocument();
|
|
251
|
+
expect(screen.queryByTestId("for-you-stack")).not.toBeInTheDocument();
|
|
252
|
+
expect(screen.queryByTestId("for-you-stack-count")).not.toBeInTheDocument();
|
|
253
|
+
expect(screen.queryByTestId("for-you-sheet")).not.toBeInTheDocument();
|
|
254
|
+
expect(screen.queryByTestId("inbox-for-you")).not.toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("shows on the phone and at md+, while the For you panel stays hidden", async () => {
|
|
258
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
259
|
+
await renderPage();
|
|
260
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("hidden");
|
|
261
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toHaveClass("md:hidden");
|
|
262
|
+
expect(screen.getByTestId("inbox-panel-for-you")).toHaveClass("hidden");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("the phone has no large title or lead — both stay at md+", async () => {
|
|
266
|
+
await renderPage();
|
|
267
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveClass("max-md:sr-only");
|
|
268
|
+
expect(screen.getByRole("heading", { level: 1 })).not.toHaveClass("hidden");
|
|
269
|
+
expect(screen.getByText("What changed, and what's coming.")).toHaveClass("hidden", "md:block");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("the list sits in a bordered card, inset by the canvas on every side", async () => {
|
|
273
|
+
accountExperimentKeys.push("ailk-founder-diagnostic");
|
|
274
|
+
await renderPage();
|
|
275
|
+
// The shell's main slot insets the canvas (p-4 below md); the page adds
|
|
276
|
+
// no full-bleed escape, and the list is RowGroup's bordered box.
|
|
277
|
+
const page = screen.getByTestId("account-inbox");
|
|
278
|
+
expect(page.className).not.toMatch(/-mx-|w-screen/);
|
|
279
|
+
expect(screen.getByTestId("inbox-panel-inbox").className ?? "").not.toMatch(/-mx-|w-screen/);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("the phone For you tab (#7469, DESIGN.md §2b · §5.15)", () => {
|
|
284
|
+
it("lists every item with its type tag, title, Open or View, and ✕", async () => {
|
|
285
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
286
|
+
mockGetSession.mockResolvedValue({
|
|
287
|
+
user: { ...SESSION.user, name: "", emailVerified: false },
|
|
288
|
+
});
|
|
289
|
+
await renderPage("en", "for-you");
|
|
290
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
291
|
+
// Phone-only: shown below md, hidden at md+ where the aside holds it.
|
|
292
|
+
expect(panel).toHaveClass("md:hidden");
|
|
293
|
+
expect(panel).not.toHaveClass("hidden");
|
|
294
|
+
// The Inbox panel is hidden on the phone but still shows at md+.
|
|
295
|
+
expect(screen.getByTestId("inbox-panel-inbox")).toHaveClass("hidden", "md:block");
|
|
296
|
+
const card = within(panel).getByTestId("for-you-list-card");
|
|
297
|
+
const items = within(card).getAllByTestId("for-you-item");
|
|
298
|
+
expect(items).toHaveLength(3);
|
|
299
|
+
for (const item of items) {
|
|
300
|
+
expect(within(item).getByTestId("for-you-tag")).toBeInTheDocument();
|
|
301
|
+
expect(
|
|
302
|
+
within(item).queryByTestId("for-you-open") ?? within(item).queryByTestId("for-you-view"),
|
|
303
|
+
).not.toBeNull();
|
|
304
|
+
expect(within(item).getByTestId("for-you-dismiss")).toHaveAccessibleName(/^Dismiss/);
|
|
305
|
+
}
|
|
306
|
+
expect(within(panel).getByText("Add your name")).toBeInTheDocument();
|
|
307
|
+
expect(within(panel).getByText("Email not verified")).toBeInTheDocument();
|
|
308
|
+
expect(within(panel).getByText("Your payment didn't go through")).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("✕ dismisses an item — it leaves the list", async () => {
|
|
312
|
+
UNVERIFIED_AND_PAST_DUE();
|
|
313
|
+
await renderPage("en", "for-you");
|
|
314
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
315
|
+
const [first] = within(panel).getAllByTestId("for-you-item");
|
|
316
|
+
const id = first!.getAttribute("data-item-id");
|
|
317
|
+
fireEvent.click(within(first!).getByTestId("for-you-dismiss"));
|
|
318
|
+
const left = within(panel).getAllByTestId("for-you-item");
|
|
319
|
+
expect(left).toHaveLength(1);
|
|
320
|
+
expect(left.map((item) => item.getAttribute("data-item-id"))).not.toContain(id);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("the For you tab keeps the page's h1 (visually hidden on the phone)", async () => {
|
|
324
|
+
await renderPage("en", "for-you");
|
|
325
|
+
const h1 = screen.getByRole("heading", { level: 1 });
|
|
326
|
+
expect(h1).toHaveTextContent("Inbox");
|
|
327
|
+
expect(screen.getByTestId("inbox-panel-inbox")).not.toContainElement(h1);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("with nothing waiting the tab shows its empty line, not a blank page", async () => {
|
|
331
|
+
await renderPage("en", "for-you");
|
|
332
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
333
|
+
expect(within(panel).queryByTestId("for-you-item")).not.toBeInTheDocument();
|
|
334
|
+
expect(within(panel).getByTestId("for-you-empty")).toHaveTextContent(
|
|
335
|
+
"Nothing here right now.",
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
238
340
|
describe("Hub unavailable (D12)", () => {
|
|
239
341
|
it("a failed read renders an error row with Retry, never an empty Inbox", async () => {
|
|
240
342
|
mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
@@ -250,6 +352,26 @@ describe("Hub unavailable (D12)", () => {
|
|
|
250
352
|
it("a failed For-you read also shows the error row", async () => {
|
|
251
353
|
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
|
|
252
354
|
await renderPage();
|
|
253
|
-
expect(
|
|
355
|
+
expect(
|
|
356
|
+
within(screen.getByTestId("inbox-panel-inbox")).getByTestId("inbox-error"),
|
|
357
|
+
).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("on the phone For you tab, a failed For-you read shows the error row above the list (#7469)", async () => {
|
|
361
|
+
mockSubscriptionMe.mockResolvedValue({ ok: false, status: 502, body: {} });
|
|
362
|
+
await renderPage("en", "for-you");
|
|
363
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
364
|
+
expect(within(panel).getByTestId("inbox-error")).toHaveTextContent(
|
|
365
|
+
"We couldn't load everything in your Inbox.",
|
|
366
|
+
);
|
|
367
|
+
// Never a silently empty list (D12): the empty line gives way to the error row.
|
|
368
|
+
expect(within(panel).queryByTestId("for-you-empty")).not.toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("a failed Inbox-kind read leaves the For you tab without an error row", async () => {
|
|
372
|
+
mockCheckoutsMe.mockResolvedValue({ ok: false, status: 500, body: {} });
|
|
373
|
+
await renderPage("en", "for-you");
|
|
374
|
+
const panel = screen.getByTestId("inbox-panel-for-you");
|
|
375
|
+
expect(within(panel).queryByTestId("inbox-error")).not.toBeInTheDocument();
|
|
254
376
|
});
|
|
255
377
|
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* #7418), with the same `failed` flag.
|
|
18
18
|
*
|
|
19
19
|
* `cache()` dedupes the reads within one request: the layout's aside and the
|
|
20
|
-
* Inbox's phone
|
|
20
|
+
* Inbox's phone For you tab render the same items from one set of API calls.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type {
|
|
@@ -9,10 +9,18 @@
|
|
|
9
9
|
* `createAuthClient()` at module scope is the `sign-in-gate.tsx` idiom. Log
|
|
10
10
|
* out ends the session, then navigates home and refreshes so every RSC under
|
|
11
11
|
* the `(authed)` gate re-reads the (now absent) session.
|
|
12
|
+
*
|
|
13
|
+
* #7469 (DESIGN.md §2b as amended 2026-10-01, matching the Admin's §5.15) —
|
|
14
|
+
* below `md` the bar reads avatar · Inbox · For you · Help, with no count
|
|
15
|
+
* on any tab. For you is a page of the Inbox route (`?tab=for-you`): the
|
|
16
|
+
* frame reads the URL and hands the tab to the page (`PhoneTabProvider`),
|
|
17
|
+
* which shows one panel per tab. At `md`+ nothing changes — For you stays
|
|
18
|
+
* the aside.
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
import {
|
|
15
22
|
ForYouPanel,
|
|
23
|
+
PhoneTabProvider,
|
|
16
24
|
useForYouDismissed,
|
|
17
25
|
type ForYouItem,
|
|
18
26
|
type ForYouLabels,
|
|
@@ -25,18 +33,23 @@ import {
|
|
|
25
33
|
import { localeHref } from "@working-theory/ui";
|
|
26
34
|
import { Link } from "@working-theory/ui/primitives";
|
|
27
35
|
import { createAuthClient } from "better-auth/react";
|
|
28
|
-
import { Inbox, Settings } from "lucide-react";
|
|
29
|
-
import { usePathname, useRouter } from "next/navigation";
|
|
36
|
+
import { Inbox, Settings, Sparkles } from "lucide-react";
|
|
37
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
30
38
|
import type { ReactElement, ReactNode } from "react";
|
|
31
39
|
|
|
32
40
|
import { ACCOUNT_PERSIST_KEY } from "./for-you-labels";
|
|
33
41
|
|
|
34
42
|
const authClient = createAuthClient();
|
|
35
43
|
|
|
44
|
+
/** The phone For you tab's `?tab=` value (#7469). */
|
|
45
|
+
const FOR_YOU_TAB = "for-you";
|
|
46
|
+
|
|
36
47
|
export interface AccountFrameLabels extends SignedInShellLabels {
|
|
37
48
|
/** The logo link's accessible name. */
|
|
38
49
|
home: string;
|
|
39
50
|
inbox: string;
|
|
51
|
+
/** The phone's For you tab label (#7469). */
|
|
52
|
+
forYou: string;
|
|
40
53
|
settings: string;
|
|
41
54
|
}
|
|
42
55
|
|
|
@@ -62,12 +75,16 @@ export function AccountFrame({
|
|
|
62
75
|
}: AccountFrameProps): ReactElement {
|
|
63
76
|
const router = useRouter();
|
|
64
77
|
const pathname = usePathname();
|
|
78
|
+
const searchParams = useSearchParams();
|
|
65
79
|
const [dismissed] = useForYouDismissed(ACCOUNT_PERSIST_KEY);
|
|
66
80
|
|
|
67
81
|
const homeHref = localeHref(locale, "/");
|
|
68
82
|
const inboxHref = localeHref(locale, "/account");
|
|
69
83
|
// The Inbox row stays selected on an item's own page (/account/inbox/…).
|
|
70
84
|
const onInbox = pathname === inboxHref || pathname.startsWith(`${inboxHref}/inbox/`);
|
|
85
|
+
// #7469 — the phone tab: For you only on the Inbox route itself.
|
|
86
|
+
const phoneTab =
|
|
87
|
+
pathname === inboxHref && searchParams?.get("tab") === FOR_YOU_TAB ? FOR_YOU_TAB : "inbox";
|
|
71
88
|
|
|
72
89
|
const onSignOut = (): void => {
|
|
73
90
|
void (async () => {
|
|
@@ -82,7 +99,7 @@ export function AccountFrame({
|
|
|
82
99
|
})();
|
|
83
100
|
};
|
|
84
101
|
|
|
85
|
-
const { home, inbox, settings, ...shellLabels } = labels;
|
|
102
|
+
const { home, inbox, forYou: forYouLabel, settings, ...shellLabels } = labels;
|
|
86
103
|
|
|
87
104
|
// D13 — the logo only: no box, no name, no popover; it links home. The
|
|
88
105
|
// same link is the phone top bar's leading cell.
|
|
@@ -105,7 +122,16 @@ export function AccountFrame({
|
|
|
105
122
|
persistKey={ACCOUNT_PERSIST_KEY}
|
|
106
123
|
locale={locale}
|
|
107
124
|
navItems={[{ labelKey: "inbox", label: inbox, path: "/account", icon: Inbox }]}
|
|
108
|
-
tabItems={[
|
|
125
|
+
tabItems={[
|
|
126
|
+
{ key: "inbox", label: inbox, href: inboxHref, icon: Inbox, isActive: onInbox && phoneTab === "inbox" },
|
|
127
|
+
{
|
|
128
|
+
key: FOR_YOU_TAB,
|
|
129
|
+
label: forYouLabel,
|
|
130
|
+
href: `${inboxHref}?tab=${FOR_YOU_TAB}`,
|
|
131
|
+
icon: Sparkles,
|
|
132
|
+
isActive: phoneTab === FOR_YOU_TAB,
|
|
133
|
+
},
|
|
134
|
+
]}
|
|
109
135
|
{...(onInbox ? { activePath: inboxHref } : {})}
|
|
110
136
|
user={user}
|
|
111
137
|
userMenuItems={[
|
|
@@ -137,7 +163,7 @@ export function AccountFrame({
|
|
|
137
163
|
: {})}
|
|
138
164
|
labels={shellLabels}
|
|
139
165
|
>
|
|
140
|
-
{children}
|
|
166
|
+
<PhoneTabProvider tab={phoneTab}>{children}</PhoneTabProvider>
|
|
141
167
|
</SignedInShell>
|
|
142
168
|
);
|
|
143
169
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* for-you-labels.ts — the For-you panel's strings from the `account`
|
|
3
3
|
* namespace (#7430, spec §5.2 i18n), and the frame's persist key. Shared by
|
|
4
|
-
* the layout's aside and the Inbox's phone
|
|
4
|
+
* the layout's aside and the Inbox's phone For you tab.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { AccountTranslate, ForYouLabels } from "@working-theory/templates/account";
|
|
@@ -10,7 +10,7 @@ import type { AccountTranslate, ForYouLabels } from "@working-theory/templates/a
|
|
|
10
10
|
* The account frame's `persistKey`: the shell's remembered collapse
|
|
11
11
|
* (`ailk:shell:account:*`) and the For-you dismissals
|
|
12
12
|
* (`ailk:account:foryou:dismissed`), shared by the aside and the Inbox's
|
|
13
|
-
* phone
|
|
13
|
+
* phone For you tab so a dismissal holds on both.
|
|
14
14
|
*/
|
|
15
15
|
export const ACCOUNT_PERSIST_KEY = "account";
|
|
16
16
|
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* home; Inbox as the rail's only row; a footer of the avatar and ? alone;
|
|
8
8
|
* the card avatar menu (name, then Settings and Log out); the ? help menu
|
|
9
9
|
* (Contact support, and Help center when `AILK_HELP_CENTER_URL` is set);
|
|
10
|
-
* and below `md` a tab bar reading avatar · Inbox ·
|
|
11
|
-
* renders no marketing frame and no consent Banner on
|
|
12
|
-
* `isAccountPath`).
|
|
10
|
+
* and below `md` a tab bar reading avatar · Inbox · For you · Help (#7469).
|
|
11
|
+
* The locale layout renders no marketing frame and no consent Banner on
|
|
12
|
+
* these paths (D4, `isAccountPath`).
|
|
13
13
|
*
|
|
14
14
|
* The aside is the For-you panel, fed by `accountForYouSources` plus the
|
|
15
15
|
* area's own "Add your name" item (`loadForYou`, deduped per request with
|
|
16
|
-
* the Inbox's phone
|
|
16
|
+
* the Inbox's phone For you tab); with no items there is no aside.
|
|
17
17
|
*
|
|
18
18
|
* The `(authed)` layout above owns the signed-out gate, so a missing session
|
|
19
19
|
* here is a race (it expired between the two layouts): the page renders
|
|
@@ -67,6 +67,7 @@ export default async function AccountLayout({
|
|
|
67
67
|
labels={{
|
|
68
68
|
home: t("frame.home"),
|
|
69
69
|
inbox: t("frame.inbox"),
|
|
70
|
+
forYou: t("frame.forYou"),
|
|
70
71
|
settings: t("frame.settings"),
|
|
71
72
|
signOut: t("frame.signOut"),
|
|
72
73
|
help: t("frame.help"),
|
|
@@ -17,8 +17,18 @@
|
|
|
17
17
|
*
|
|
18
18
|
* What needs the person is never an Inbox row: `email-unverified`,
|
|
19
19
|
* `payment-due` and "Add your name" are For-you items (D9 as amended). From
|
|
20
|
-
* `md` up they are the layout's aside
|
|
21
|
-
*
|
|
20
|
+
* `md` up they are the layout's aside.
|
|
21
|
+
*
|
|
22
|
+
* #7469 (DESIGN.md §2b as amended 2026-10-01, matching the Admin's §5.15) —
|
|
23
|
+
* below `md` the route is the phone's two main tabs, one `PhoneTabPanel`
|
|
24
|
+
* each (the frame hands the tab over from `?tab=`):
|
|
25
|
+
* - Inbox — the filter tabs and the list only, in RowGroup's inset card.
|
|
26
|
+
* No For-you stack and no bottom sheet, and no lead on the phone.
|
|
27
|
+
* - For you — every For-you item as rows of one inset card, each with its
|
|
28
|
+
* type tag, title, Open or View, and ✕; the empty line when none waits,
|
|
29
|
+
* and D12's error row above the list when a For-you source failed.
|
|
30
|
+
* The h1 sits above both panels, visually hidden below `md` (the phone has
|
|
31
|
+
* no large title). At `md`+ only the Inbox panel shows, lead included.
|
|
22
32
|
*
|
|
23
33
|
* D12 — any read that fails (a kind or a For-you source) renders an error
|
|
24
34
|
* row with Retry above the list; the list then holds what DID load, never a
|
|
@@ -26,8 +36,9 @@
|
|
|
26
36
|
*/
|
|
27
37
|
|
|
28
38
|
import {
|
|
29
|
-
|
|
39
|
+
ForYouPanel,
|
|
30
40
|
InboxList,
|
|
41
|
+
PhoneTabPanel,
|
|
31
42
|
type InboxListRow,
|
|
32
43
|
} from "@working-theory/templates/account";
|
|
33
44
|
import { Heading, Text } from "@working-theory/ui/primitives";
|
|
@@ -96,52 +107,65 @@ export default async function AccountInboxPage({ params }: Props): Promise<React
|
|
|
96
107
|
const failed = inbox.failed || forYou.failed;
|
|
97
108
|
|
|
98
109
|
return (
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
// The phone's top bar already clears the canvas — no top inset there.
|
|
111
|
+
<div className="max-w-3xl py-8 max-md:pt-0" data-testid="account-inbox">
|
|
112
|
+
{/* The page's one h1, on both phone tabs; visually hidden on the phone,
|
|
113
|
+
which has no large title (§5.15). */}
|
|
114
|
+
<Heading level={1} visual={2} className="max-md:sr-only">
|
|
101
115
|
{t("inbox.title")}
|
|
102
116
|
</Heading>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
<PhoneTabPanel tabs={["inbox"]} desktop data-testid="inbox-panel-inbox">
|
|
118
|
+
<Text as="p" variant="body-sm" tone="muted" className="mt-block-tight hidden md:block">
|
|
119
|
+
{t("inbox.lead")}
|
|
120
|
+
</Text>
|
|
121
|
+
|
|
122
|
+
{failed ? (
|
|
123
|
+
<ErrorRow
|
|
124
|
+
message={t("inbox.error")}
|
|
125
|
+
retryLabel={t("inbox.retry")}
|
|
126
|
+
className="mt-block-loose"
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
|
|
130
|
+
<InboxList
|
|
131
|
+
tabs={[
|
|
132
|
+
{ id: "recents", label: t("inbox.tabs.recents"), count: recents.length, rows: recents },
|
|
133
|
+
{ id: "suggested", label: t("inbox.tabs.suggested"), count: 0, rows: [] },
|
|
134
|
+
{
|
|
135
|
+
id: "scheduled",
|
|
136
|
+
label: t("inbox.tabs.scheduled"),
|
|
137
|
+
count: scheduled.length,
|
|
138
|
+
rows: scheduled,
|
|
139
|
+
},
|
|
140
|
+
{ id: "all", label: t("inbox.tabs.all"), count: all.length, rows: all },
|
|
141
|
+
]}
|
|
142
|
+
defaultTab="recents"
|
|
143
|
+
{...(failed ? {} : { emptyLine: t("inbox.empty") })}
|
|
144
|
+
/>
|
|
145
|
+
</PhoneTabPanel>
|
|
106
146
|
|
|
107
|
-
{/*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
|
|
112
|
-
<
|
|
147
|
+
{/* #7469 — the phone For you tab: every item, rows of one inset card.
|
|
148
|
+
At `md`+ the same items are the layout's aside. D12 here too: a
|
|
149
|
+
failed source shows the error row, and never the empty line alone. */}
|
|
150
|
+
<PhoneTabPanel tabs={["for-you"]} data-testid="inbox-panel-for-you">
|
|
151
|
+
{forYou.failed ? (
|
|
152
|
+
<ErrorRow
|
|
153
|
+
message={t("inbox.error")}
|
|
154
|
+
retryLabel={t("inbox.retry")}
|
|
155
|
+
className="mb-block-loose"
|
|
156
|
+
/>
|
|
157
|
+
) : null}
|
|
158
|
+
{forYou.failed && forYou.items.length === 0 ? null : (
|
|
159
|
+
<ForYouPanel
|
|
160
|
+
variant="list"
|
|
113
161
|
items={forYou.items}
|
|
114
162
|
persistKey={ACCOUNT_PERSIST_KEY}
|
|
115
163
|
labels={forYouLabels(translate)}
|
|
116
164
|
locale={locale}
|
|
117
165
|
now={forYou.now}
|
|
118
166
|
/>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{failed ? (
|
|
123
|
-
<ErrorRow
|
|
124
|
-
message={t("inbox.error")}
|
|
125
|
-
retryLabel={t("inbox.retry")}
|
|
126
|
-
className="mt-block-loose"
|
|
127
|
-
/>
|
|
128
|
-
) : null}
|
|
129
|
-
|
|
130
|
-
<InboxList
|
|
131
|
-
tabs={[
|
|
132
|
-
{ id: "recents", label: t("inbox.tabs.recents"), count: recents.length, rows: recents },
|
|
133
|
-
{ id: "suggested", label: t("inbox.tabs.suggested"), count: 0, rows: [] },
|
|
134
|
-
{
|
|
135
|
-
id: "scheduled",
|
|
136
|
-
label: t("inbox.tabs.scheduled"),
|
|
137
|
-
count: scheduled.length,
|
|
138
|
-
rows: scheduled,
|
|
139
|
-
},
|
|
140
|
-
{ id: "all", label: t("inbox.tabs.all"), count: all.length, rows: all },
|
|
141
|
-
]}
|
|
142
|
-
defaultTab="recents"
|
|
143
|
-
{...(failed ? {} : { emptyLine: t("inbox.empty") })}
|
|
144
|
-
/>
|
|
167
|
+
)}
|
|
168
|
+
</PhoneTabPanel>
|
|
145
169
|
</div>
|
|
146
170
|
);
|
|
147
171
|
}
|
package/versions.json
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@working-theory/admin-ui": "0.
|
|
3
|
-
"@working-theory/aeo": "0.
|
|
4
|
-
"@working-theory/agent-context": "0.
|
|
2
|
+
"@working-theory/admin-ui": "0.32.0",
|
|
3
|
+
"@working-theory/aeo": "0.32.0",
|
|
4
|
+
"@working-theory/agent-context": "0.32.0",
|
|
5
5
|
"@working-theory/agent-contract": "0.1.0",
|
|
6
6
|
"@working-theory/agent-memory": "0.0.0",
|
|
7
7
|
"@working-theory/ai": "0.1.0",
|
|
8
|
-
"@working-theory/analytics": "0.
|
|
9
|
-
"@working-theory/api-client": "0.
|
|
8
|
+
"@working-theory/analytics": "0.32.0",
|
|
9
|
+
"@working-theory/api-client": "0.32.0",
|
|
10
10
|
"@working-theory/cda-filesystem": "0.0.0",
|
|
11
|
-
"@working-theory/cli": "0.
|
|
12
|
-
"@working-theory/common": "0.
|
|
11
|
+
"@working-theory/cli": "0.32.0",
|
|
12
|
+
"@working-theory/common": "0.32.0",
|
|
13
13
|
"@working-theory/consent": "0.1.0",
|
|
14
|
-
"@working-theory/content-adapters": "0.
|
|
15
|
-
"@working-theory/content-projection": "0.
|
|
16
|
-
"@working-theory/email": "0.
|
|
14
|
+
"@working-theory/content-adapters": "0.32.0",
|
|
15
|
+
"@working-theory/content-projection": "0.32.0",
|
|
16
|
+
"@working-theory/email": "0.32.0",
|
|
17
17
|
"@working-theory/env": "0.0.0",
|
|
18
|
-
"@working-theory/eslint-config": "0.
|
|
19
|
-
"@working-theory/fumadocs-schema": "0.
|
|
18
|
+
"@working-theory/eslint-config": "0.32.0",
|
|
19
|
+
"@working-theory/fumadocs-schema": "0.32.0",
|
|
20
20
|
"@working-theory/ids": "0.8.0",
|
|
21
|
-
"@working-theory/jsonld": "0.
|
|
22
|
-
"@working-theory/license": "0.
|
|
23
|
-
"@working-theory/metadata": "0.
|
|
24
|
-
"@working-theory/observability": "0.
|
|
25
|
-
"@working-theory/page-renderer": "0.
|
|
21
|
+
"@working-theory/jsonld": "0.32.0",
|
|
22
|
+
"@working-theory/license": "0.32.0",
|
|
23
|
+
"@working-theory/metadata": "0.32.0",
|
|
24
|
+
"@working-theory/observability": "0.32.0",
|
|
25
|
+
"@working-theory/page-renderer": "0.32.0",
|
|
26
26
|
"@working-theory/plugin-mcp": "0.1.1",
|
|
27
|
-
"@working-theory/schema": "0.
|
|
28
|
-
"@working-theory/schema-pack-registry": "0.
|
|
29
|
-
"@working-theory/schema-validator": "0.
|
|
30
|
-
"@working-theory/sections-registry": "0.
|
|
31
|
-
"@working-theory/site-config": "0.
|
|
32
|
-
"@working-theory/stripe-client": "0.
|
|
33
|
-
"@working-theory/templates": "0.
|
|
34
|
-
"@working-theory/theme": "0.
|
|
35
|
-
"@working-theory/ui": "0.
|
|
36
|
-
"@working-theory/validation": "0.
|
|
37
|
-
"@working-theory/web-i18n": "0.
|
|
27
|
+
"@working-theory/schema": "0.32.0",
|
|
28
|
+
"@working-theory/schema-pack-registry": "0.32.0",
|
|
29
|
+
"@working-theory/schema-validator": "0.32.0",
|
|
30
|
+
"@working-theory/sections-registry": "0.32.0",
|
|
31
|
+
"@working-theory/site-config": "0.32.0",
|
|
32
|
+
"@working-theory/stripe-client": "0.32.0",
|
|
33
|
+
"@working-theory/templates": "0.32.0",
|
|
34
|
+
"@working-theory/theme": "0.32.0",
|
|
35
|
+
"@working-theory/ui": "0.32.0",
|
|
36
|
+
"@working-theory/validation": "0.32.0",
|
|
37
|
+
"@working-theory/web-i18n": "0.32.0",
|
|
38
38
|
"@working-theory/billing-client": "0.0.0",
|
|
39
39
|
"@working-theory/branding": "0.0.0",
|
|
40
40
|
"@working-theory/license-client": "0.0.0",
|
|
41
41
|
"@working-theory/license-manifest": "0.0.0",
|
|
42
42
|
"@working-theory/partner-session": "0.0.0",
|
|
43
43
|
"@working-theory/preview-protocol": "0.0.0",
|
|
44
|
-
"@working-theory/ui-commerce": "0.23.
|
|
45
|
-
"@working-theory/ui-docs-blog": "0.23.
|
|
46
|
-
"@working-theory/ui-scheduling": "0.23.
|
|
47
|
-
"@working-theory/ui-signal-collectors": "0.24.
|
|
44
|
+
"@working-theory/ui-commerce": "0.23.9",
|
|
45
|
+
"@working-theory/ui-docs-blog": "0.23.9",
|
|
46
|
+
"@working-theory/ui-scheduling": "0.23.9",
|
|
47
|
+
"@working-theory/ui-signal-collectors": "0.24.7",
|
|
48
48
|
"@working-theory/web": "0.0.0",
|
|
49
49
|
"@working-theory/api": "1.0.0",
|
|
50
50
|
"@working-theory/mcp": "0.0.0",
|
|
51
|
-
"@working-theory/database": "0.
|
|
51
|
+
"@working-theory/database": "0.32.0"
|
|
52
52
|
}
|