QA pass: layout, create-flow, and About books #68

Merged
an.di merged 38 commits from adilallo/fix/CR-129-create-rule-button into main 2026-08-26 15:46:56 +00:00
13 changed files with 88 additions and 60 deletions
Showing only changes of commit 712437edc7 - Show all commits
@@ -388,6 +388,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => { const handleSelectFieldType = useCallback((ft: AddCustomFieldType) => {
setEditingBlockId(null); setEditingBlockId(null);
setAddFieldExpanded(false);
resetFieldTypeDrafts(); resetFieldTypeDrafts();
setFieldTypeModal(ft); setFieldTypeModal(ft);
fieldModalSnapshotRef.current = JSON.stringify({ fieldModalSnapshotRef.current = JSON.stringify({
@@ -531,6 +532,7 @@ const CustomMethodCardWizardContainer = memo<CustomMethodCardWizardProps>(
}); });
setFieldTypeModal(null); setFieldTypeModal(null);
setEditingBlockId(null); setEditingBlockId(null);
setAddFieldExpanded(false);
fieldModalSnapshotRef.current = null; fieldModalSnapshotRef.current = null;
}, [ }, [
badgeBlockTitle, badgeBlockTitle,
@@ -90,7 +90,7 @@ function CustomMethodCardWizardViewComponent({
/> />
) : null} ) : null}
{!fieldTypeModal && wizardStep === 3 ? ( {!fieldTypeModal && wizardStep === 3 ? (
<div className="flex w-full flex-col gap-4 pt-1"> <div className="flex w-full flex-col gap-6">
{draftFieldBlocks.length > 0 ? ( {draftFieldBlocks.length > 0 ? (
<CustomMethodCardWizardBlocksList <CustomMethodCardWizardBlocksList
blocks={draftFieldBlocks} blocks={draftFieldBlocks}
@@ -43,7 +43,7 @@ function CustomMethodCardWizardBlocksListViewComponent({
}: CustomMethodCardWizardBlocksListViewProps) { }: CustomMethodCardWizardBlocksListViewProps) {
return ( return (
<ul <ul
className="flex list-none flex-col gap-2 p-0 pt-1" className="flex list-none flex-col gap-2 p-0"
aria-label={listLabel} aria-label={listLabel}
> >
{blocks.map((block, index) => { {blocks.map((block, index) => {
@@ -53,11 +53,9 @@ function CustomMethodCardWizardFieldBodiesViewComponent({
maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS} maxLength={CUSTOM_METHOD_CARD_WIZARD_MAX_FIELD_CHARS}
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<InputLabel <label className="text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]">
label={copy.text.placeholderLabel} {copy.text.placeholderLabel}
size="s" </label>
palette="default"
/>
<TextArea <TextArea
formHeader={false} formHeader={false}
appearance="embedded" appearance="embedded"
+1 -1
View File
@@ -28,7 +28,7 @@ function VerticalComponent({
"data-testid": dataTestId, "data-testid": dataTestId,
}: VerticalProps) { }: VerticalProps) {
const base = const base =
"box-border flex w-[90px] shrink-0 cursor-pointer flex-col items-center gap-[var(--spacing-scale-008)] rounded-[var(--spacing-scale-004)] border border-solid border-[var(--color-border-default-brand-primary)] bg-transparent px-[var(--spacing-scale-008)] py-[var(--spacing-scale-012)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] disabled:cursor-not-allowed disabled:opacity-60"; "box-border flex w-full min-w-[90px] cursor-pointer flex-col items-center gap-[var(--spacing-scale-008)] rounded-[var(--spacing-scale-004)] border border-solid border-[var(--color-border-default-brand-primary)] bg-transparent px-[var(--spacing-scale-008)] py-[var(--spacing-scale-012)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] disabled:cursor-not-allowed disabled:opacity-60";
return ( return (
<button <button
@@ -7,7 +7,7 @@ import type { AddCustomFieldProps, AddCustomFieldType } from "./AddCustomField.t
/** /**
* Figma: "Add Custom Field" control — Community Rule System (`20235:12994`). * Figma: "Add Custom Field" control — Community Rule System (`20235:12994`).
* Collapsed CTA expands to a 2×2 field-type picker (per-type modals deferred). * Collapsed CTA expands to a four-tile field-type picker.
*/ */
const AddCustomFieldContainer = memo<AddCustomFieldProps>( const AddCustomFieldContainer = memo<AddCustomFieldProps>(
({ active, onPressAdd, onSelectFieldType, className = "" }) => { ({ active, onPressAdd, onSelectFieldType, className = "" }) => {
@@ -11,7 +11,7 @@ export const ADD_CUSTOM_FIELD_TYPE_ICONS = {
} as const satisfies Record<AddCustomFieldType, IconName>; } as const satisfies Record<AddCustomFieldType, IconName>;
export interface AddCustomFieldProps { export interface AddCustomFieldProps {
/** When true, show the 2×2 field-type grid; when false, show the primary CTA. */ /** When true, show the four field-type tiles; when false, show the primary CTA. */
active: boolean; active: boolean;
onPressAdd?: () => void; onPressAdd?: () => void;
onSelectFieldType?: (type: AddCustomFieldType) => void; onSelectFieldType?: (type: AddCustomFieldType) => void;
@@ -38,15 +38,6 @@ function FieldTypeButton({
); );
} }
/**
* Stable block height for collapsed vs expanded so the Create dialog (`top-1/2 -translate-y-1/2`)
* does not shrink and re-center when toggling `active`.
*
* - Collapsed CTA: `py-12` (48+48) + inner row (`py-3` + 20px icon/line) ≈ 140px border-box.
* - Expanded: inner `p-4` (32) + Vertical tile (py 12+12, gap 8, 32px icon, 18px label) ≈ 114px — shorter without this floor.
*/
const ADD_CUSTOM_FIELD_SHELL_MIN_H_PX = 140;
function AddCustomFieldViewComponent({ function AddCustomFieldViewComponent({
active, active,
onPressAdd, onPressAdd,
@@ -55,17 +46,12 @@ function AddCustomFieldViewComponent({
fieldTypeLabels, fieldTypeLabels,
className, className,
}: AddCustomFieldViewProps) { }: AddCustomFieldViewProps) {
const shellStyle = {
minHeight: ADD_CUSTOM_FIELD_SHELL_MIN_H_PX,
} as const;
if (!active) { if (!active) {
return ( return (
<button <button
type="button" type="button"
onClick={onPressAdd} onClick={onPressAdd}
style={shellStyle} className={`flex h-[88px] w-full shrink-0 cursor-pointer items-center justify-center rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] px-6 text-medium-label text-[var(--color-content-default-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] ${className ?? ""}`.trim()}
className={`flex w-full shrink-0 cursor-pointer items-center justify-center rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] px-6 py-12 text-medium-label text-[var(--color-content-default-primary)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] ${className ?? ""}`.trim()}
> >
<span className="flex items-center gap-[var(--spacing-scale-006)] rounded-full px-4 py-3"> <span className="flex items-center gap-[var(--spacing-scale-006)] rounded-full px-4 py-3">
<svg <svg
@@ -89,35 +75,33 @@ function AddCustomFieldViewComponent({
); );
} }
const expandedShellClasses = ["flex w-full shrink-0 flex-col", className ?? ""] const expandedShellClasses = ["flex w-full shrink-0", className ?? ""]
.join(" ") .join(" ")
.trim(); .trim();
return ( return (
<div className={expandedShellClasses} style={shellStyle}> <div className={expandedShellClasses}>
<div className="flex w-full flex-col gap-3 rounded-[var(--measures-radius-medium,8px)] bg-[var(--color-surface-default-secondary)] p-4"> <div className="grid w-full grid-cols-4 gap-3">
<div className="flex w-full flex-row flex-nowrap justify-center gap-3 overflow-x-auto max-sm:justify-start"> <FieldTypeButton
<FieldTypeButton type="text"
type="text" label={fieldTypeLabels.text}
label={fieldTypeLabels.text} onSelect={onSelectFieldType}
onSelect={onSelectFieldType} />
/> <FieldTypeButton
<FieldTypeButton type="badges"
type="badges" label={fieldTypeLabels.badges}
label={fieldTypeLabels.badges} onSelect={onSelectFieldType}
onSelect={onSelectFieldType} />
/> <FieldTypeButton
<FieldTypeButton type="upload"
type="upload" label={fieldTypeLabels.upload}
label={fieldTypeLabels.upload} onSelect={onSelectFieldType}
onSelect={onSelectFieldType} />
/> <FieldTypeButton
<FieldTypeButton type="proportion"
type="proportion" label={fieldTypeLabels.proportion}
label={fieldTypeLabels.proportion} onSelect={onSelectFieldType}
onSelect={onSelectFieldType} />
/>
</div>
</div> </div>
</div> </div>
); );
+1 -1
View File
@@ -59,7 +59,7 @@ export function CreateView({
{headerContent !== undefined ? ( {headerContent !== undefined ? (
<div className="shrink-0">{headerContent}</div> <div className="shrink-0">{headerContent}</div>
) : title || description ? ( ) : title || description ? (
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0"> <div className="bg-[var(--color-surface-default-primary)] px-[24px] shrink-0">
<ContentLockup <ContentLockup
title={title} title={title}
description={description} description={description}
@@ -24,7 +24,7 @@
"dragHandleAriaLabel": "Drag to reorder this field" "dragHandleAriaLabel": "Drag to reorder this field"
}, },
"footer": { "footer": {
"finalize": "Finalize" "finalize": "Finalize policy"
}, },
"editModal": { "editModal": {
"noCustomFieldsYet": "No custom fields yet.", "noCustomFieldsYet": "No custom fields yet.",
@@ -36,7 +36,7 @@
"clearFileLabel": "Clear file" "clearFileLabel": "Clear file"
}, },
"addCustomField": { "addCustomField": {
"cta": "Add", "cta": "Add Custom Field",
"fieldTypes": { "fieldTypes": {
"text": "Text", "text": "Text",
"badges": "Badges", "badges": "Badges",
@@ -442,7 +442,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
expect( expect(
screen.getByRole("button", { name: "Core Principle & Scope" }), screen.getByRole("button", { name: "Core Principle & Scope" }),
).toBeInTheDocument(); ).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Finalize" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => { await waitFor(() => {
expect(latest.customMethodCardMetaById?.[customId]?.label).toBe( expect(latest.customMethodCardMetaById?.[customId]?.label).toBe(
@@ -493,7 +493,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
}); });
fireEvent.click(screen.getByRole("button", { name: "Next" })); fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" })); fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Finalize" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => { await waitFor(() => {
expect(latest.customMethodCardMetaById?.signal?.label).toBe( expect(latest.customMethodCardMetaById?.signal?.label).toBe(
@@ -546,7 +546,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
fireEvent.dragStart(rows[0], { dataTransfer }); fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[2], { dataTransfer }); fireEvent.drop(rows[2], { dataTransfer });
fireEvent.click(screen.getByRole("button", { name: "Finalize" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -235,7 +235,7 @@ describe("CoreValuesSelectScreen", () => {
fireEvent.dragStart(rows[0], { dataTransfer }); fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[1], { dataTransfer }); fireEvent.drop(rows[1], { dataTransfer });
fireEvent.click(screen.getByRole("button", { name: "Finalize" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -288,9 +288,53 @@ describe("CoreValuesSelectScreen", () => {
target: { value: "A community-authored value." }, target: { value: "A community-authored value." },
}); });
fireEvent.click(screen.getByRole("button", { name: "Next" })); fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(await screen.findByRole("button", { name: "Finalize" })); expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Add Custom Field" }),
).toBeInTheDocument();
fireEvent.click(await screen.findByRole("button", { name: "Finalize policy" }));
} }
it("collapses the field-type picker after adding a custom field", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByRole("button", { name: "Add value" }));
fireEvent.change(await screen.findByPlaceholderText("Policy name"), {
target: { value: CUSTOM_LABEL },
});
fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.change(
await screen.findByPlaceholderText("Policy description"),
{ target: { value: "A community-authored value." } },
);
fireEvent.click(screen.getByRole("button", { name: "Next" }));
expect(
await screen.findByText("Custom policy details"),
).toBeInTheDocument();
const wizard = screen.getByRole("dialog");
fireEvent.click(
within(wizard).getByRole("button", { name: "Add Custom Field" }),
);
fireEvent.click(within(wizard).getByRole("button", { name: "Text" }));
fireEvent.change(
await screen.findByPlaceholderText("Add your text block title"),
{ target: { value: "Policy title" } },
);
fireEvent.click(screen.getByRole("button", { name: "Add field" }));
expect(
await within(wizard).findByRole("button", { name: "Add Custom Field" }),
).toBeInTheDocument();
expect(
within(wizard).queryByRole("button", { name: "Text" }),
).not.toBeInTheDocument();
expect(
within(wizard).getByRole("button", { name: "Policy title" }),
).toBeInTheDocument();
});
it("opens the empty custom-policy wizard from Add value", async () => { it("opens the empty custom-policy wizard from Add value", async () => {
renderWithProviders(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByRole("button", { name: "Add value" })); fireEvent.click(screen.getByRole("button", { name: "Add value" }));
+2 -2
View File
@@ -829,7 +829,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
fireEvent.change(nameInput, { target: { value: "Custom Signal header" } }); fireEvent.change(nameInput, { target: { value: "Custom Signal header" } });
fireEvent.click(screen.getByRole("button", { name: "Next" })); fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Next" })); fireEvent.click(screen.getByRole("button", { name: "Next" }));
fireEvent.click(screen.getByRole("button", { name: "Finalize" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => { await waitFor(() => {
expect(latest.customMethodCardMetaById?.signal?.label).toBe( expect(latest.customMethodCardMetaById?.signal?.label).toBe(
@@ -883,7 +883,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
fireEvent.dragStart(rows[0], { dataTransfer }); fireEvent.dragStart(rows[0], { dataTransfer });
fireEvent.drop(rows[2], { dataTransfer }); fireEvent.drop(rows[2], { dataTransfer });
fireEvent.click(screen.getByRole("button", { name: "Finalize" })); fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
await waitFor(() => { await waitFor(() => {
expect( expect(