Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/ui-carousel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"typecheck": "npm run panda && tsc --noEmit"
},
"peerDependencies": {
"@microbit/ui": "^0.4.0",
"@microbit/ui": "^0.5.0",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit weird but OK, we'll have to release ui-v0.5.0 first then ui-patterns-0.7.0.

"@pandacss/dev": "^1.11.4",
"react": "^18.3.1",
"react-aria": "^3.50.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/ui-patterns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ blocks.
pass a narrowed set where landing a user in a language silently would be
wrong (e.g. incomplete native-app translations).

- **Project components** — the building blocks of a "your projects" page:
`ProjectCard`, the selection toolbar, search and sort controls, the
name and confirm dialogs, and `useProjectActions` for the rename,
duplicate and delete flows behind them. Page layout and the grid stay
app-side; see the "Projects/Page example" story for a composition.

- **Error views** — the full-page fallbacks every app carries.
`UnexpectedErrorPage` says an unexpected error occurred, links to the
app's support site, offers a reload, and shows an optional error
Expand Down
108 changes: 108 additions & 0 deletions packages/ui-patterns/lang/ui.en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"ui-patterns.clear-action": {
"defaultMessage": "Clear",
"description": "Clears a search box or a selection"
},
"ui-patterns.copied-feedback": {
"defaultMessage": "Copied",
"description": "Accessible name of the copy button briefly after copying the error reference"
Expand All @@ -7,6 +11,34 @@
"defaultMessage": "Copy error reference",
"description": "Accessible name of the button that copies the error reference"
},
"ui-patterns.delete-project-action": {
"defaultMessage": "{count, plural, one {Delete} other {Delete {count} projects}}",
"description": "Delete action for one project or a selection of projects"
},
"ui-patterns.delete-project-confirm-heading": {
"defaultMessage": "Confirm delete project",
"description": "Heading of the dialog confirming deletion of one project"
},
"ui-patterns.delete-project-confirm-text": {
"defaultMessage": "Are you sure you want to delete the project \"{project}\"?",
"description": "Body of the dialog confirming deletion of one project"
},
"ui-patterns.delete-projects-confirm-heading": {
"defaultMessage": "Confirm delete projects",
"description": "Heading of the dialog confirming deletion of several projects"
},
"ui-patterns.delete-projects-confirm-text": {
"defaultMessage": "Are you sure you want to delete {numProjects} projects?",
"description": "Body of the dialog confirming deletion of several projects"
},
"ui-patterns.duplicate-project-action": {
"defaultMessage": "Duplicate",
"description": "Duplicate project action"
},
"ui-patterns.duplicate-project-heading": {
"defaultMessage": "Duplicate project",
"description": "Heading of the dialog naming a duplicated project"
},
"ui-patterns.error-reference": {
"defaultMessage": "Error reference: {id}",
"description": "Shown on the unexpected error page. The id identifies the error report and can be quoted in support requests."
Expand Down Expand Up @@ -47,6 +79,14 @@
"defaultMessage": "Language not fully supported",
"description": "Language support toast notification title"
},
"ui-patterns.name-label": {
"defaultMessage": "Name",
"description": "Label of the project name field"
},
"ui-patterns.name-project-heading": {
"defaultMessage": "Name your project",
"description": "Default heading of the project name dialog"
},
"ui-patterns.not-found-home-link": {
"defaultMessage": "Go to home page",
"description": "Link text on the page not found page"
Expand All @@ -55,18 +95,86 @@
"defaultMessage": "Page not found",
"description": "Title of the page shown for an unknown URL"
},
"ui-patterns.open-project-action": {
"defaultMessage": "Open",
"description": "Open project action"
},
"ui-patterns.play-video": {
"defaultMessage": "Play video: {title}",
"description": "Accessible label for the play button shown over a video preview image; title is the video's title"
},
"ui-patterns.project-menu-label": {
"defaultMessage": "{name} actions menu",
"description": "Accessible name of a project card's actions menu button"
},
"ui-patterns.project-name-not-empty": {
"defaultMessage": "The project name cannot be empty",
"description": "Validation message for the project name field"
},
"ui-patterns.reload-action": {
"defaultMessage": "Click to reload the page",
"description": "Button on the unexpected error page"
},
"ui-patterns.rename-project-action": {
"defaultMessage": "Rename",
"description": "Rename project action"
},
"ui-patterns.rename-project-heading": {
"defaultMessage": "Rename project",
"description": "Heading of the dialog renaming a project"
},
"ui-patterns.search": {
"defaultMessage": "Search",
"description": "Label and placeholder of the projects search box"
},
"ui-patterns.select-project-action": {
"defaultMessage": "Select {name}",
"description": "Accessible label of a project card's selection checkbox"
},
"ui-patterns.selection-actions-group": {
"defaultMessage": "Selection actions",
"description": "Accessible name of the toolbar acting on selected projects"
},
"ui-patterns.skip-to-toolbar-action": {
"defaultMessage": "Skip to toolbar",
"description": "Hidden button on a selected project card that moves focus to the selection toolbar"
},
"ui-patterns.sort-option-last-modified": {
"defaultMessage": "Last modified",
"description": "Sort projects by last modified"
},
"ui-patterns.sort-option-name": {
"defaultMessage": "Name",
"description": "Sort projects by name"
},
"ui-patterns.sort-option-relevance": {
"defaultMessage": "Relevance",
"description": "Shown in the sort control while searching; results are ranked by relevance"
},
"ui-patterns.sort-order-ascending-label": {
"defaultMessage": "Ascending order",
"description": "Accessible name of the sort direction button when ascending"
},
"ui-patterns.sort-order-descending-label": {
"defaultMessage": "Descending order",
"description": "Accessible name of the sort direction button when descending"
},
"ui-patterns.sort-projects-label": {
"defaultMessage": "Sort projects",
"description": "Accessible name of the sort field select"
},
"ui-patterns.support-request": {
"defaultMessage": "Please consider <link>raising a support request</link>.",
"description": "Text on the unexpected error page. The link opens the support site."
},
"ui-patterns.timestamp-now": {
"defaultMessage": "now",
"description": "Relative time shown for a project modified this second"
},
"ui-patterns.timestamp-seconds": {
"defaultMessage": "a few seconds ago",
"description": "Relative time shown for a project modified under a minute ago"
},
"ui-patterns.unexpected-error-title": {
"defaultMessage": "An unexpected error occurred",
"description": "Title of the full-page view shown for an unrecoverable error"
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-patterns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@formatjs/intl-localematcher": "^0.8.0"
},
"peerDependencies": {
"@microbit/ui": "^0.4.0",
"@microbit/ui": "^0.5.0",
"@pandacss/dev": "^1.11.4",
"react": "^18.3.1",
"react-icons": "^4.12.0 || ^5.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/ui-patterns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export * from "./NotFoundPage";
export * from "./UnexpectedErrorPage";
export * from "./YoutubeVideoEmbed";
export * from "./get-default-language";
export * from "./projects";
export * from "./languages";
export { uiPatternsMessage } from "./messages";
145 changes: 145 additions & 0 deletions packages/ui-patterns/src/projects/NameProjectDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* (c) 2026, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalFooter,
ModalHeader,
TextField,
} from "@microbit/ui";
import {
FocusEvent,
FormEvent,
ReactNode,
RefObject,
useCallback,
useState,
} from "react";
import { FormattedMessage } from "react-intl";
import { uiMessage } from "@microbit/ui/messages";
import { uiPatternsMessage } from "../messages";

export interface NameProjectDialogProps {
isOpen: boolean;
onClose: () => void;
onSave: (name: string) => void;
/** The name to start from: the current one, or a default for a new project. */
initialName: string;
/** Defaults to "Name your project". */
heading?: ReactNode;
/**
* The verb for what saving does ("Create", "Rename", "Confirm and save").
* Required: there is always a better label than "Confirm".
*/
confirmText: ReactNode;
/** Shown under the field, e.g. what the name is used for. */
helperText?: ReactNode;
finalFocusRef?: RefObject<HTMLElement>;
onCloseComplete?: () => void;
}

export const isValidProjectName = (name: string): boolean =>
name.trim().length > 0;

/**
* Asks for a project name. The field starts with the initial name selected
* so typing replaces it. Submit with Enter or the confirm button; both are
* disabled while the name is blank.
*/
export const NameProjectDialog = ({
isOpen,
onClose,
onSave,
initialName,
heading,
confirmText,
helperText,
finalFocusRef,
onCloseComplete,
}: NameProjectDialogProps) => {
const [name, setName] = useState(initialName);
// Start afresh from the initial name each time the dialog opens. Adjusting
// state during render, as React recommends for state derived from a prop
// change, rather than an effect that would flash the stale name.
const [wasOpen, setWasOpen] = useState(isOpen);
if (isOpen !== wasOpen) {
setWasOpen(isOpen);
if (isOpen) {
setName(initialName);
}
}
const isValid = isValidProjectName(name);

const handleFocus = useCallback((event: FocusEvent<HTMLInputElement>) => {
event.target.setSelectionRange(0, event.target.value.length);
}, []);
const handleSave = useCallback(() => {
if (isValid) {
onSave(name.trim());
}
}, [isValid, name, onSave]);
const handleSubmit = useCallback(
(e: FormEvent) => {
e.preventDefault();
handleSave();
},
[handleSave],
);

return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="md"
finalFocusRef={finalFocusRef}
onCloseComplete={onCloseComplete}
>
<ModalHeader>
{heading ?? (
<FormattedMessage
{...uiPatternsMessage("ui-patterns.name-project-heading")}
/>
)}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<form onSubmit={handleSubmit}>
<TextField
label={
<FormattedMessage
{...uiPatternsMessage("ui-patterns.name-label")}
/>
}
value={name}
onChange={setName}
onFocus={handleFocus}
autoFocus
autoComplete="off"
isRequired
isInvalid={!isValid}
helperText={helperText}
helperTextCss={{ color: "gray.700" }}
errorMessage={
<FormattedMessage
{...uiPatternsMessage("ui-patterns.project-name-not-empty")}
/>
}
/>
</form>
</ModalBody>
<ModalFooter css={{ gap: 3 }}>
<Button onPress={onClose}>
<FormattedMessage {...uiMessage("ui.cancel-action")} />
</Button>
<Button variant="primary" onPress={handleSave} isDisabled={!isValid}>
{confirmText}
</Button>
</ModalFooter>
</Modal>
);
};
Loading