@multiplatform.one/frappe-ui 7.53.0 → 7.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cjs/FrappeListView.cjs +13 -28
  2. package/dist/cjs/FrappeListView.native.js +15 -30
  3. package/dist/cjs/FrappeListView.native.js.map +1 -1
  4. package/dist/cjs/record/RecordHero.cjs +1 -1
  5. package/dist/cjs/record/RecordHero.native.js +1 -1
  6. package/dist/cjs/record/RecordHero.native.js.map +1 -1
  7. package/dist/cjs/record/RecordPage.cjs +1 -1
  8. package/dist/cjs/record/RecordPage.native.js +1 -1
  9. package/dist/esm/FrappeListView.mjs +14 -29
  10. package/dist/esm/FrappeListView.mjs.map +1 -1
  11. package/dist/esm/FrappeListView.native.js +16 -31
  12. package/dist/esm/FrappeListView.native.js.map +1 -1
  13. package/dist/esm/record/RecordHero.mjs +1 -1
  14. package/dist/esm/record/RecordHero.mjs.map +1 -1
  15. package/dist/esm/record/RecordHero.native.js +1 -1
  16. package/dist/esm/record/RecordHero.native.js.map +1 -1
  17. package/dist/esm/record/RecordPage.mjs +1 -1
  18. package/dist/esm/record/RecordPage.native.js +1 -1
  19. package/dist/jsx/FrappeListView.mjs +14 -29
  20. package/dist/jsx/FrappeListView.mjs.map +1 -1
  21. package/dist/jsx/FrappeListView.native.js +15 -30
  22. package/dist/jsx/FrappeListView.native.js.map +1 -1
  23. package/dist/jsx/record/RecordHero.mjs +1 -1
  24. package/dist/jsx/record/RecordHero.mjs.map +1 -1
  25. package/dist/jsx/record/RecordHero.native.js +1 -1
  26. package/dist/jsx/record/RecordHero.native.js.map +1 -1
  27. package/dist/jsx/record/RecordPage.mjs +1 -1
  28. package/dist/jsx/record/RecordPage.native.js +1 -1
  29. package/package.json +16 -16
  30. package/src/FrappeListView.controls.spec.tsx +7 -0
  31. package/src/FrappeListView.spec.tsx +8 -3
  32. package/src/FrappeListView.tsx +16 -19
  33. package/src/frappeRoutes.spec.tsx +3 -0
  34. package/src/record/RecordHero.tsx +1 -1
  35. package/src/record/RecordPage.tsx +1 -1
  36. package/src/schemaForm/SchemaForm.spec.tsx +16 -4
@@ -1,6 +1,7 @@
1
1
  // FrappeListView's list controls (MPO-411): sorts, a facet on a connection
2
2
  // doctype, a range, favorites, a search mapping and the URL state that a
3
3
  // list page (pokemon's roster) used to hand-wire around the view.
4
+ import { dismissAllToasts } from "@multiplatform.one/components";
4
5
  import { Order } from "@multiplatform.one/frappe";
5
6
  import { renderWithProviders } from "@multiplatform.one/test-utils";
6
7
  import { act, cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
@@ -47,8 +48,14 @@ afterAll(() => {
47
48
  globalThis.fetch = realFetch;
48
49
  });
49
50
 
51
+ // A favorite toggle shows a toast that outlives its test in the module
52
+ // store. Remounted by a later test, its close timer calls tamagui's hide,
53
+ // whose 100 ms follow-up fired after happy-dom was gone ("window is not
54
+ // defined", two unhandled errors that fail the file). Unmount first, then
55
+ // clear the store, so no toast reaches the next test.
50
56
  afterEach(() => {
51
57
  cleanup();
58
+ dismissAllToasts();
52
59
  vi.restoreAllMocks();
53
60
  });
54
61
 
@@ -164,15 +164,20 @@ describe("FrappeListView refresh press floor (LC-54 amendment)", () => {
164
164
  const midWeightAtom = /(?:^|\s)(?:_fw-|_fow-|_fontWeight-)(?:500|600)(?:\s|$)/;
165
165
 
166
166
  describe("FrappeListView design-law (avatar + type)", () => {
167
- it("list-row avatars are nested and follow nestedControl, not a 40px literal", async () => {
167
+ // LC-155 ROW-ANATOMY: a list row's leading avatar fills ListRow's `$true`
168
+ // square media slot. It replaced the nestedControl (29px) avatar.
169
+ it("list-row avatars fill the $true media slot (LC-155), not a 40px literal", async () => {
168
170
  const { container } = renderListView();
169
171
  const avatar = await waitFor(() => {
170
172
  const node = container.querySelector("[data-avatar-px]");
171
173
  expect(node).not.toBeNull();
172
174
  return node as HTMLElement;
173
175
  });
174
- expect(avatar.getAttribute("data-avatar-nested")).toBe("true");
175
- expect(Number(avatar.getAttribute("data-avatar-px"))).toBe(29);
176
+ const slot = avatar.closest('[data-list-slot="media"]');
177
+ expect(slot).not.toBeNull();
178
+ expect(avatar.getAttribute("data-avatar-nested")).toBeNull();
179
+ const px = Number(getVariableValue(getSize("$true" as SizeTokens)));
180
+ expect(Number(avatar.getAttribute("data-avatar-px"))).toBe(px);
176
181
  expect(Number(avatar.getAttribute("data-avatar-px"))).not.toBe(40);
177
182
  });
178
183
 
@@ -21,11 +21,11 @@ import {
21
21
  Avatar,
22
22
  EmptyState,
23
23
  ListPageLayout,
24
+ ListRow,
24
25
  PageHeader,
25
26
  SkipLink,
26
27
  Tooltip,
27
28
  ViewSwitcher,
28
- componentColors,
29
29
  showToast,
30
30
  type DropdownMenuEntry,
31
31
  type ImageGridProps,
@@ -489,22 +489,19 @@ function FrappeListViewBody<TDoc extends { name: string; doctype: string }>({
489
489
  const imageUrl = imageFieldName ? resolveImageUrl(row[imageFieldName], finalBaseURL) : "";
490
490
 
491
491
  const titleName = typeof title === "string" ? title : undefined;
492
+ // LC-155: the MPO ListRow owns the anatomy (headline, one meta line,
493
+ // a $true media slot).
492
494
  return (
493
- <XStack alignItems="center" flex={1} minWidth={0} width="100%" {...knobProps.gap}>
494
- {imageUrl ? (
495
- <Avatar nested circular src={imageUrl} name={titleName} alt={titleName ?? ""} />
496
- ) : null}
497
- <YStack flex={1} minWidth={0}>
498
- <Text
499
- {...knobProps.body}
500
- color={componentColors.text.primary}
501
- numberOfLines={1}
502
- {...({ "data-list-slot": "title" } as Record<string, unknown>)}
503
- >
504
- {title}
505
- </Text>
506
- {subtitleParts.length > 0 && (
507
- <Text {...knobProps.body} color={knobProps.textAccentColor} numberOfLines={1}>
495
+ <ListRow
496
+ media={
497
+ imageUrl ? (
498
+ <Avatar circular size="$true" src={imageUrl} name={titleName} alt={titleName ?? ""} />
499
+ ) : undefined
500
+ }
501
+ title={title}
502
+ subtitle={
503
+ subtitleParts.length > 0 ? (
504
+ <Text {...knobProps.body} {...knobProps.textRole.meta} numberOfLines={1}>
508
505
  {subtitleParts.map((part, index) => (
509
506
  <React.Fragment key={index}>
510
507
  {index > 0 ? " · " : null}
@@ -512,9 +509,9 @@ function FrappeListViewBody<TDoc extends { name: string; doctype: string }>({
512
509
  </React.Fragment>
513
510
  ))}
514
511
  </Text>
515
- )}
516
- </YStack>
517
- </XStack>
512
+ ) : undefined
513
+ }
514
+ />
518
515
  );
519
516
  },
520
517
  [effectiveDisplayFields, imageFieldName, finalBaseURL, knobProps],
@@ -284,6 +284,9 @@ describe("frappeRouteTarget", () => {
284
284
  describe("FrappeListView with a route", () => {
285
285
  it("is the doctype's list page: its rows, a row opens the record, Add opens a new one", async () => {
286
286
  const { container, pushed } = renderAt("/moves", "list");
287
+ // The file's first list render pays the cold imports before its read
288
+ // goes out; time the row wait from the read, as the guest case does.
289
+ await movesListed;
287
290
  await waitFor(() => expect(screen.getByText("tackle")).toBeTruthy());
288
291
  expect(screen.getByText("vine-whip")).toBeTruthy();
289
292
  expect(screen.getByRole("heading", { name: doctype })).toBeTruthy();
@@ -80,7 +80,7 @@ export function RecordVitals({ tiles }: { tiles: readonly RecordVitalTile[] }) {
80
80
  testID={`record-vital-${tile.id}`}
81
81
  data-record-metric={tile.id}
82
82
  >
83
- <Text {...knobProps.label} color="$color11" numberOfLines={1}>
83
+ <Text {...knobProps.label} {...knobProps.textRole.label} numberOfLines={1}>
84
84
  {tile.label}
85
85
  </Text>
86
86
  <TamaguiSizableText size="$6" {...knobProps.heading} color="$color" numberOfLines={1}>
@@ -360,7 +360,7 @@ export function RecordPage<TDoc extends { name: string; doctype: string }>({
360
360
  </XStack>
361
361
  <RecordLargeTitle title={title} />
362
362
  {look?.subtitle?.(doc) ? (
363
- <Text {...nested.body} color="$color11">
363
+ <Text {...nested.body} color="$textMuted">
364
364
  {look.subtitle(doc)}
365
365
  </Text>
366
366
  ) : null}
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { fireEvent, screen } from "@testing-library/react";
5
5
  import { renderWithProviders } from "@multiplatform.one/test-utils";
6
+ import { hairlineBorderWidth, Preset } from "@multiplatform.one/theme";
6
7
  import { describe, expect, it, vi } from "vitest";
7
8
  import type { FieldSchema } from "../fieldRegistry";
8
9
  import { SchemaForm, type FormSchema } from "./index";
@@ -79,12 +80,15 @@ describe("SchemaForm design-law (MPO-60 / R10)", () => {
79
80
  expect(input.disabled || input.getAttribute("aria-disabled") === "true").toBe(true);
80
81
  });
81
82
 
82
- it("default section is SF-CARD: no hardcoded 1px border", () => {
83
+ // LC-151 replaced the borderless content card: a top-level card's edge is
84
+ // cardSurface's theme hairline, which follows the borderWidth knob, and the
85
+ // section hardcodes no border of its own.
86
+ it("default section is SF-CARD: the cardSurface hairline edge (LC-151), no hardcoded border", () => {
83
87
  const schema: FormSchema = {
84
88
  fields: identityFields,
85
89
  sections: [{ label: "Identity", columns: [{ fields: identityFields }] }],
86
90
  };
87
- const { container } = renderWithProviders(
91
+ const { container, unmount } = renderWithProviders(
88
92
  <SchemaForm schema={schema} doc={{}} setValue={vi.fn()} />,
89
93
  );
90
94
  const card = container.querySelector("[data-mpo-schema-form-card]") as HTMLElement;
@@ -92,8 +96,16 @@ describe("SchemaForm design-law (MPO-60 / R10)", () => {
92
96
  expect(card.getAttribute("data-mpo-schema-form-borderless")).toBeNull();
93
97
  const style = card.getAttribute("style") ?? "";
94
98
  expect(style).not.toMatch(/border-width:\s*1px/i);
95
- const computed = getComputedStyle(card);
96
- expect(computed.borderWidth === "1px" && computed.borderStyle === "solid").toBe(false);
99
+ expect(getComputedStyle(card).borderWidth).toBe(`${hairlineBorderWidth}px`);
100
+ unmount();
101
+
102
+ const flat = renderWithProviders(
103
+ <Preset overrides={{ borderWidth: "none" }}>
104
+ <SchemaForm schema={schema} doc={{}} setValue={vi.fn()} />
105
+ </Preset>,
106
+ );
107
+ const edgeless = flat.container.querySelector("[data-mpo-schema-form-card]") as HTMLElement;
108
+ expect(getComputedStyle(edgeless).borderWidth).toBe("0px");
97
109
  });
98
110
 
99
111
  it("hideBorder is SF-TRANSPARENT (borderless marker, no card fill)", () => {