diff --git a/images/icons/info.svg b/images/icons/info.svg
new file mode 100644
index 00000000..634faae0
--- /dev/null
+++ b/images/icons/info.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/images/icons/pin.svg b/images/icons/pin.svg
new file mode 100644
index 00000000..f2a17513
--- /dev/null
+++ b/images/icons/pin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/icons/Info.tsx b/src/components/icons/Info.tsx
new file mode 100644
index 00000000..1274904c
--- /dev/null
+++ b/src/components/icons/Info.tsx
@@ -0,0 +1,26 @@
+// This file was auto-generated using scripts/generate-icons.js
+import type { JSX } from 'preact';
+
+export type InfoIconProps = JSX.SVGAttributes;
+
+/**
+ * Icon generated from info.svg
+ */
+export default function InfoIcon(props: InfoIconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/icons/Pin.tsx b/src/components/icons/Pin.tsx
new file mode 100644
index 00000000..d337cc09
--- /dev/null
+++ b/src/components/icons/Pin.tsx
@@ -0,0 +1,32 @@
+// This file was auto-generated using scripts/generate-icons.js
+import type { JSX } from 'preact';
+
+export type PinIconProps = JSX.SVGAttributes;
+
+/**
+ * Icon generated from pin.svg
+ */
+export default function PinIcon(props: PinIconProps) {
+ return (
+
+ );
+}
diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts
index 818fc660..d888b4ce 100644
--- a/src/components/icons/index.ts
+++ b/src/components/icons/index.ts
@@ -60,6 +60,7 @@ export { default as HideFilledIcon } from './HideFilled';
export { default as HighlightIcon } from './Highlight';
export { default as ImageIcon } from './Image';
export { default as ImageFilledIcon } from './ImageFilled';
+export { default as InfoIcon } from './Info';
export { default as LeaveIcon } from './Leave';
export { default as LeaveFilledIcon } from './LeaveFilled';
export { default as LinkIcon } from './Link';
@@ -75,6 +76,7 @@ export { default as MenuCollapseIcon } from './MenuCollapse';
export { default as MenuExpandIcon } from './MenuExpand';
export { default as NoteIcon } from './Note';
export { default as NoteFilledIcon } from './NoteFilled';
+export { default as PinIcon } from './Pin';
export { default as PlusIcon } from './Plus';
export { default as PointerDownIcon } from './PointerDown';
export { default as PointerUpIcon } from './PointerUp';
diff --git a/src/pattern-library/components/PlaygroundApp.tsx b/src/pattern-library/components/PlaygroundApp.tsx
index 772bdc3b..71da20b3 100644
--- a/src/pattern-library/components/PlaygroundApp.tsx
+++ b/src/pattern-library/components/PlaygroundApp.tsx
@@ -153,6 +153,8 @@ export default function PlaygroundApp({
>
);
+ const prototypeRoutes = getRoutes('prototype');
+
const groupKeys = Object.keys(componentGroups) as Array<
keyof typeof componentGroups
>;
@@ -194,6 +196,17 @@ export default function PlaygroundApp({
);
})}
+ {prototypeRoutes.length > 0 && (
+ <>
+ Prototypes
+
+ {prototypeRoutes.map(route => (
+
+ ))}
+
+ >
+ )}
+
{extraRoutes.length > 0 && (
<>
{extraRoutesTitle}
diff --git a/src/pattern-library/components/patterns/prototype/.gitkeep b/src/pattern-library/components/patterns/prototype/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pattern-library/components/patterns/prototype/FakeAnnotationPublishControl.tsx b/src/pattern-library/components/patterns/prototype/FakeAnnotationPublishControl.tsx
new file mode 100644
index 00000000..aaa27899
--- /dev/null
+++ b/src/pattern-library/components/patterns/prototype/FakeAnnotationPublishControl.tsx
@@ -0,0 +1,139 @@
+import classnames from 'classnames';
+
+import {
+ Button,
+ CancelIcon,
+ GlobeIcon,
+ GroupsIcon,
+ InfoIcon,
+ LockIcon,
+ MenuExpandIcon,
+} from '../../../..';
+import Menu from './Menu';
+import MenuItem from './MenuItem';
+
+export type AnnotationPublishControlProps = {
+ /** The group this annotation or draft would publish to */
+ group: string;
+
+ /**
+ * Should the save button be disabled? Hint: it will be if the annotation has
+ * no content
+ */
+ isDisabled?: boolean;
+
+ /** Annotation or draft is "Only Me" */
+ isPrivate?: boolean;
+
+ noSharing?: boolean;
+
+ noSharingMessage?: string;
+
+ /** Callback for cancel button click */
+ onCancel?: () => void;
+
+ /** Callback for save button click */
+ onSave?: () => void;
+};
+
+/**
+ * Render a compound control button for publishing (saving) an annotation:
+ * - Save the annotation — left side of button
+ * - Choose sharing/privacy option - drop-down menu on right side of button
+ *
+ * @param {AnnotationPublishControlProps} props
+ */
+function AnnotationPublishControl({
+ group,
+ isDisabled = false,
+ isPrivate = false,
+ noSharing = false,
+ noSharingMessage = "Why can't I share this annotation?",
+ onCancel = () => {},
+ onSave = () => {},
+}: AnnotationPublishControlProps) {
+ const menuLabel = (
+
+
+
+ );
+
+ return (
+
+
+
+ {/* This wrapper div is necessary because of peculiarities with
+ Safari: see https://github.com/hypothesis/client/issues/2302 */}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default AnnotationPublishControl;
diff --git a/src/pattern-library/components/patterns/prototype/Menu.tsx b/src/pattern-library/components/patterns/prototype/Menu.tsx
new file mode 100644
index 00000000..2e7d8d9c
--- /dev/null
+++ b/src/pattern-library/components/patterns/prototype/Menu.tsx
@@ -0,0 +1,258 @@
+import classnames from 'classnames';
+import type { ComponentChildren } from 'preact';
+import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
+
+import { useElementShouldClose } from '../../../../';
+import { MenuExpandIcon } from '../../../../';
+import MenuArrow from './MenuArrow';
+
+/**
+ * Flag indicating whether the next click event on the menu's toggle button
+ * should be ignored, because the action it would trigger has already been
+ * triggered by a preceding "mousedown" event.
+ */
+let ignoreNextClick = false;
+
+export type MenuProps = {
+ /**
+ * Whether the menu content is aligned with the left (default) or right edges
+ * of the toggle element.
+ */
+ align?: 'left' | 'right';
+
+ /**
+ * Additional CSS class for the arrow caret at the edge of the menu content
+ * that "points" toward the menu's toggle button. This can be used to adjust
+ * the position of that caret respective to the toggle button.
+ */
+ arrowClass?: string;
+
+ /**
+ * Label element or string for the toggle button that hides and shows the menu
+ */
+ label: ComponentChildren;
+
+ /** Menu content, typically `MenuSection` and `MenuItem` components */
+ children: ComponentChildren;
+
+ /**
+ * Whether the menu elements should be positioned relative to the Menu
+ * container. When `false`, the consumer is responsible for positioning.
+ */
+ containerPositioned?: boolean;
+
+ /** Additional CSS classes to apply to the Menu */
+ contentClass?: string;
+
+ /**
+ * Whether the menu is open when initially rendered. Ignored if `open` is
+ * present.
+ */
+ defaultOpen?: boolean;
+
+ /** Whether to render an (arrow) indicator next to the Menu label */
+ menuIndicator?: boolean;
+
+ /** Callback when the Menu is opened or closed. */
+ onOpenChanged?: (open: boolean) => void;
+
+ /**
+ * Whether the Menu is currently open, when the Menu is being used as a
+ * controlled component. In these cases, an `onOpenChanged` handler should
+ * be provided to respond to the user opening or closing the menu.
+ */
+ open?: boolean;
+
+ /**
+ * A title for the menu. This is important for accessibility if the menu's
+ * toggle button has only an icon as a label.
+ */
+ title: string;
+};
+
+const noop = () => {};
+
+/**
+ * A drop-down menu.
+ *
+ * Menus consist of a button which toggles whether the menu is open, an
+ * an arrow indicating the state of the menu and content when is shown when
+ * the menu is open. The children of the menu component are rendered as the
+ * content of the menu when open. Typically this consists of a list of
+ * `MenuSection` and/or `MenuItem` components.
+ *
+ * @example
+ *
+ */
+export default function Menu({
+ align = 'left',
+ arrowClass = '',
+ children,
+ containerPositioned = true,
+ contentClass,
+ defaultOpen = false,
+ label,
+ open,
+ onOpenChanged,
+ menuIndicator = true,
+ title,
+}: MenuProps) {
+ let [isOpen, setOpen]: [boolean, (open: boolean) => void] =
+ useState(defaultOpen);
+ if (typeof open === 'boolean') {
+ isOpen = open;
+ setOpen = onOpenChanged || noop;
+ }
+
+ // Notify parent when menu is opened or closed.
+ const wasOpen = useRef(isOpen);
+ useEffect(() => {
+ if (typeof onOpenChanged === 'function' && wasOpen.current !== isOpen) {
+ wasOpen.current = isOpen;
+ onOpenChanged(isOpen);
+ }
+ }, [isOpen, onOpenChanged]);
+
+ /**
+ * Toggle menu when user presses toggle button. The menu is shown on mouse
+ * press for a more responsive/native feel but also handles a click event for
+ * activation via other input methods.
+ */
+ const toggleMenu = (event: Event) => {
+ // If the menu was opened on press, don't close it again on the subsequent
+ // mouse up ("click") event.
+ if (event.type === 'mousedown') {
+ ignoreNextClick = true;
+ } else if (event.type === 'click' && ignoreNextClick) {
+ // Ignore "click" event triggered from the mouse up action.
+ ignoreNextClick = false;
+ event.stopPropagation();
+ event.preventDefault();
+ return;
+ }
+
+ setOpen(!isOpen);
+ };
+ const closeMenu = useCallback(() => setOpen(false), [setOpen]);
+
+ // Set up an effect which adds document-level event handlers when the menu
+ // is open and removes them when the menu is closed or removed.
+ //
+ // These handlers close the menu when the user taps or clicks outside the
+ // menu or presses Escape.
+ const menuRef = useRef(null);
+
+ // Menu element should close via `closeMenu` whenever it's open and there
+ // are user interactions outside of it (e.g. clicks) in the document
+ useElementShouldClose(menuRef, isOpen, closeMenu);
+
+ const stopPropagation = (e: Event) => e.stopPropagation();
+
+ // It should also close if the user presses a key which activates menu items.
+ const handleMenuKeyDown = (event: KeyboardEvent) => {
+ const key = event.key;
+ if (key === 'Enter' || key === ' ') {
+ // The browser will not open the link if the link element is removed
+ // from within the keypress event that triggers it. Add a little
+ // delay to work around that.
+ setTimeout(() => {
+ closeMenu();
+ });
+ }
+ };
+
+ const containerStyle = {
+ position: containerPositioned ? 'relative' : 'static',
+ };
+
+ return (
+ // See https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
+
+
+ {isOpen && (
+ <>
+
+
+ {children}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/pattern-library/components/patterns/prototype/MenuArrow.tsx b/src/pattern-library/components/patterns/prototype/MenuArrow.tsx
new file mode 100644
index 00000000..0473f4c0
--- /dev/null
+++ b/src/pattern-library/components/patterns/prototype/MenuArrow.tsx
@@ -0,0 +1,32 @@
+import classnames from 'classnames';
+
+import { PointerDownIcon, PointerUpIcon } from '../../../../';
+
+type MenuArrowProps = {
+ classes?: string;
+ direction?: 'up' | 'down';
+};
+
+/**
+ * Render a white-filled "pointer" arrow for use in menus and menu-like
+ * elements
+ *
+ * This will set up absolute positioning for this arrow, but the vertical and
+ * horizontal positioning will need to be tuned in the component using the
+ * arrow by adding additional utility classes (`classes` prop here).
+ */
+export default function MenuArrow({
+ classes,
+ direction = 'up',
+}: MenuArrowProps) {
+ const Icon = direction === 'up' ? PointerUpIcon : PointerDownIcon;
+ return (
+
+ );
+}
diff --git a/src/pattern-library/components/patterns/prototype/MenuItem.tsx b/src/pattern-library/components/patterns/prototype/MenuItem.tsx
new file mode 100644
index 00000000..33ae100b
--- /dev/null
+++ b/src/pattern-library/components/patterns/prototype/MenuItem.tsx
@@ -0,0 +1,299 @@
+import classnames from 'classnames';
+import type { ComponentChildren, Ref } from 'preact';
+import { useEffect, useRef } from 'preact/hooks';
+
+import { CaretUpIcon, MenuExpandIcon } from '../../../../';
+import type { IconComponent } from '../../../../';
+
+type SubmenuToggleProps = {
+ title: string;
+ isExpanded: boolean;
+ onToggleSubmenu?: (e: Event) => void;
+};
+
+function SubmenuToggle({
+ title,
+ isExpanded,
+ onToggleSubmenu,
+}: SubmenuToggleProps) {
+ // FIXME: Use `MenuCollapseIcon` instead of `CaretUpIcon` once size
+ // disparities are addressed
+ const Icon = isExpanded ? CaretUpIcon : MenuExpandIcon;
+ return (
+
inside of the menu item itself
+ // but we have a non-standard mechanism with the toggle control
+ // requiring an onClick event nested inside a "menuitemradio|menuitem".
+ // Therefore, a static element with a role="none" is necessary here.
+ role="none"
+ className={classnames(
+ // Center content in a 40px square. The entire element is clickable
+ 'flex flex-col items-center justify-center w-10 h-10',
+ 'text-grey-6 bg-grey-1',
+ // Clip the background (color) such that it only shows within the
+ // content box, which is a 24px rounded square formed by the large
+ // borders
+ 'bg-clip-content border-[8px] border-transparent rounded-xl',
+ // When the menu item is hovered AND this element is hovered, darken
+ // the text color so it is clear that the toggle is the hovered element
+ 'group-hover:hover:text-grey-8',
+ {
+ // When the submenu is expanded, this element always has a darker
+ // background color regardless of hover state.
+ 'bg-grey-4': isExpanded,
+ // When the parent menu item is hovered, it gets a darker background.
+ // Make the toggle background darker also.
+ 'group-hover:bg-grey-3': !isExpanded,
+ }
+ )}
+ onClick={onToggleSubmenu}
+ title={title}
+ >
+
+
+ );
+}
+
+export type MenuItemProps = {
+ /**
+ * URL of the external link to open when this item is clicked. Either the
+ * `href` or an `onClick` callback should be supplied.
+ */
+ href?: string;
+
+ /**
+ * Icon to render for this item. This will show to the left of the item label
+ * unless this is a submenu item, in which case it goes on the right. Ignored
+ * if this is not a submenu item and `leftChannelContent` is also provided.
+ */
+ icon?: IconComponent;
+
+ /**
+ * Dim the label to indicate that this item is not currently available. The
+ * `onClick` callback will still be invoked when this item is clicked and the
+ * submenu, if any, can still be toggled.
+ */
+ isDisabled?: boolean;
+
+ /** Indicates that the submenu associated with this item is currently open */
+ isExpanded?: boolean;
+
+ /**
+ * Display an indicator to show that this menu item represents something which
+ * is currently selected/active/focused.
+ */
+ isSelected?: boolean;
+
+ /**
+ * True if this item is part of a submenu, in which case it is rendered with a
+ * different style (shaded background)
+ */
+ isSubmenuItem?: boolean;
+
+ /**
+ * If present, display a button to toggle the sub-menu associated with this
+ * item and indicate the current state; `true` if the submenu is visible.
+ * Note. Omit this prop, or set it to null, if there is no `submenu`.
+ */
+ isSubmenuVisible?: boolean;
+
+ label: ComponentChildren;
+
+ /**
+ * Optional content to render into a left channel. This accommodates small
+ * non-icon images or spacing and will supersede any provided icon if this
+ * is not a submenu item.
+ */
+ leftChannelContent?: ComponentChildren;
+
+ onClick?: (e: Event) => void;
+ onToggleSubmenu?: (e: Event) => void;
+ /**
+ * Contents of the submenu for this item. This is typically a list of
+ * `MenuItem` components with the `isSubmenuItem` prop set to `true`, but can
+ * include other content as well. The submenu is only rendered if
+ * `isSubmenuVisible` is `true`.
+ */
+ submenu?: ComponentChildren;
+};
+
+/**
+ * An item in a dropdown menu.
+ *
+ * Dropdown menu items display an icon, a label and can optionally have a submenu
+ * associated with them.
+ *
+ * When clicked, menu items either open an external link, if the `href` prop
+ * is provided, or perform a custom action via the `onClick` callback.
+ *
+ * The icon can either be an external SVG image, referenced by URL, or the
+ * name of an icon registered in the application.
+ *
+ * For items that have submenus, the `MenuItem` will call the `renderSubmenu`
+ * prop to render the content of the submenu, when the submenu is visible.
+ * Note that the `submenu` is not supported for link (`href`) items.
+ */
+export default function MenuItem({
+ href,
+ icon: Icon,
+ isDisabled,
+ isExpanded,
+ isSelected,
+ isSubmenuItem,
+ isSubmenuVisible,
+ label,
+ leftChannelContent,
+ onClick,
+ onToggleSubmenu,
+ submenu,
+}: MenuItemProps) {
+ const menuItemRef = useRef(null);
+
+ let focusTimer: number | undefined;
+
+ // menuItem can be either a link or a button
+ let menuItem;
+ const hasSubmenuVisible = typeof isSubmenuVisible === 'boolean';
+ const isRadioButtonType = typeof isSelected === 'boolean';
+
+ useEffect(() => {
+ return () => {
+ // unmount
+ clearTimeout(focusTimer);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ switch (event.key) {
+ case 'ArrowRight':
+ if (onToggleSubmenu) {
+ event.stopPropagation();
+ event.preventDefault();
+ onToggleSubmenu(event);
+ }
+ break;
+ case 'Enter':
+ case ' ':
+ if (onClick) {
+ // Let event propagate so the menu closes
+ onClick(event);
+ }
+ }
+ };
+
+ const renderedIcon = Icon ? : null;
+ const leftIcon = !isSubmenuItem ? renderedIcon : null;
+ const rightIcon = isSubmenuItem ? renderedIcon : null;
+
+ const hasLeftChannel = leftChannelContent || isSubmenuItem || !!leftIcon;
+ const hasRightContent = !!rightIcon;
+
+ const menuItemContent = (
+ <>
+ {hasLeftChannel && (
+
+ {leftChannelContent ?? leftIcon}
+
+ )}
+
+ {label}
+
+ {hasRightContent && (
+
+ {rightIcon}
+
+ )}
+ {hasSubmenuVisible && (
+
+ )}
+ >
+ );
+
+ const wrapperClasses = classnames(
+ 'focus-visible-ring ring-inset',
+ 'w-full min-w-[150px] flex items-center select-none',
+ 'border-b',
+ // Set this container as a "group" so that children may style based on its
+ // layout state.
+ // See https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
+ 'group',
+ {
+ 'min-h-[30px] font-normal': isSubmenuItem,
+ 'min-h-[40px] font-medium': !isSubmenuItem,
+ 'bg-grey-1 hover:bg-grey-3': isSubmenuItem || isExpanded,
+ 'bg-white hover:bg-grey-1': !isSubmenuItem && !isExpanded && !isDisabled,
+ 'bg-grey-0': isDisabled,
+ // visual "padding" on the right is part of SubmenuToggle when rendered,
+ // but when not rendering a SubmenuToggle, we need to add some padding here
+ 'pr-1': !hasSubmenuVisible,
+ },
+ {
+ // When the item is selected, show a left border to indicate it
+ 'border-l-[4px] border-l-brand': isSelected,
+ // Add equivalent padding to border size when not selected. This instead
+ // of a transparent left border to make focus ring cover the full
+ // menu item. Otherwise the focus ring will be inset on the left too far.
+ 'pl-[4px]': !isSelected,
+ 'border-b-grey-3': isExpanded,
+ 'border-b-transparent': !isExpanded,
+ 'text-color-text-light': isDisabled,
+ 'text-color-text': !isDisabled,
+ }
+ );
+
+ if (href) {
+ // The menu item is a link
+ menuItem = (
+ }
+ className={wrapperClasses}
+ data-testid="menu-item"
+ href={href}
+ target="_blank"
+ tabIndex={-1}
+ rel="noopener noreferrer"
+ role="menuitem"
+ onKeyDown={onKeyDown}
+ >
+ {menuItemContent}
+
+ );
+ } else {
+ // The menu item is a clickable button or radio button.
+ // In either case there may be an optional submenu.
+ menuItem = (
+
+ Give instructors the ability to create and manage{' '}
+ course-shared content that all
+ course participants can see regardless of section group membership.
+ Separately, provide instructors the ability to{' '}
+ pin content such that it shows up
+ at the top of sidebar on every tab.
+
+
+ On first launch, or when editing settings for an assignment in a
+ copied course, give the instructor the option to{' '}
+
+ copy all of the course-shared annotations they created in the
+ source assignment
+ {' '}
+ to the copied assignment.
+
+ >
+ }
+ >
+
+
With this approach context, there are three implied projects:
+
+
+
Course-shared content
+
Pinned content
+
Annotation re-use for copied course assignments
+
+
+
+ Pinned content is independent functionality and could be
+ deferred if desired. Annotation re-use depends on{' '}
+ course-shared content.
+
+ >
+ }
+ />
+
+ An instructor may create top-level annotations that are visible to
+ everyone in the course, regardless of which section group they
+ belong to. An instructor may edit an annotation and move it into or
+ out of this “all-participants” group.
+
+ }
+ >
+
+
+
+ We might be able to extend the existing annotation-publish
+ control. The annotation-publish control is available when creating{' '}
+ or editing an annotation.
+
+
+
+
+
+
+
+
+
+
+
+ It may be difficult technically and logically to deal with moving
+ annotations between course-shared and section-group-only once they
+ have replies. If that is the case, we could restrict moving an
+ annotation once it has replies.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Content visible to all course participants should be “merged into”
+ the annotation threads for the active group (section/reading group
+ as indicated by the group selector in the top bar), but it should be
+ easy to distinguish which annotation threads are shared to all
+ participants.
+
+
+
+ It should be easy to visually distinguish which annotations in the
+ sidebar are shared.
+
+
+
+
+ Annotations
+ Page Notes
+ Orphans
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A top-level annotation can be “pinned” by authorized users, which
+ makes the annotation(s) show up at the top of the sidebar above the
+ annotation-type tabs at all times. This feature could help with the
+ use case of instructors wanting to put certain annotations front and
+ center, or provide instructions or prompts for the assignment as a
+ whole.{' '}
+
+ }
+ >
+
+
+ Pinning is a “toggle” type of functionality. We could restrict its
+ availability to top-level annotations.
+
+
+
+ As pinning is a toggling function, we could add an additional icon
+ to the annotation footer.
+
+
+
+
+
+
+
+
+
+
+
+ Pinned content could be shown above all other content,
+ including tabs (i.e. a pinned Page Note would also show up on the
+ Annotations tab, but above the tabs). This could satisfy use cases
+ relating to creating prompts or instructions for assignments, or for
+ otherwise showing certain instructor content at the top.
+
+
+
An annotation may be both shared and pinned.
+
+
+
+
+
+
+ Annotations
+ Page Notes
+ Orphans
+
+
+
+
+
+
+
+
+
+
+
+
+ An instructor copies a course. Afterwards, they have the option to{' '}
+
+ copy over their own annotation content that was shared to all
+ participants
+ {' '}
+ on an assignment-by-assignment basis. They could then edit or remove
+ any that they want to change or delete.
+
+ }
+ >
+
+
+
+ Continue}
+ >
+
+ It looks like this assignment was copied from another course.
+ You can re-use your shared content in this assignment.
+
+
+ Copy shared annotations to this assignment
+
+
+
+ Only your annotations shared with all course participants
+ will be copied. Replies are not copied. You can edit or
+ remove individual copied annotations at any time.
+
+