Skip to content
Open
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
280 changes: 280 additions & 0 deletions skills/SKILL.md

Large diffs are not rendered by default.

225 changes: 225 additions & 0 deletions skills/examples/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import {
Box,
Flex,
Header,
Button,
Table,
TextInput,
EntityStatusBadge,
Menu,
IconButton,
Pagination,
Text,
} from '@contentful/f36-components';
// Navbar is a separate package — not in f36-components
import { Navbar } from '@contentful/f36-navbar';
import {
PlusIcon,
MagnifyingGlassIcon,
DotsThreeIcon,
PenNibIcon,
ImageSquareIcon,
SparkleIcon,
GlobeIcon,
GearSixIcon,
TreeStructureIcon,
TagIcon,
} from '@contentful/f36-icons';
import tokens from '@contentful/f36-tokens';

// Full app shell: Navbar (60px) + sidebar (280px) + content area
// Only include structural elements the design shows — see base-shell.md
function AppShellExample() {
return (
<Box
style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}
>
{/* Navbar from @contentful/f36-navbar — never hand-build with Flex/Box */}
{/* Requires global box-sizing: border-box or it renders 92px instead of 60px */}
<Navbar
mainNavigation={
<>
<Navbar.Item title="Content model" icon={<PenNibIcon />} />
<Navbar.Item title="Content" icon={<ImageSquareIcon />} isActive />
<Navbar.Item title="Experiences" icon={<SparkleIcon />} />
<Navbar.Item title="Media" icon={<GlobeIcon />} />
<Navbar.Item title="Apps">
<Navbar.MenuItem title="Installed apps" />
<Navbar.MenuItem title="Marketplace" />
</Navbar.Item>
</>
}
switcher={
<Navbar.Switcher
space="Acme Corp"
environment="Production"
envVariant="master"
/>
}
account={
<Navbar.Account
username="Conny Contentful"
initials="CC"
label="Account"
>
<Navbar.MenuItem title="Account settings" />
<Navbar.MenuDivider />
<Navbar.MenuItem title="Log out" />
</Navbar.Account>
}
/>

<Flex style={{ flex: 1 }}>
{/* Sidebar: 280px default, flexShrink: 0, top-left border-radius 12px */}
<Box
style={{
width: '280px',
flexShrink: 0,
backgroundColor: tokens.colorWhite,
borderRadius: '12px 0 0 0',
padding: '24px 12px 8px',
}}
>
{/* Section title: 12px, weight 600, gray700 */}
<Text
style={{
fontSize: tokens.fontSizeS,
fontWeight: tokens.fontWeightDemiBold,
color: tokens.gray700,
display: 'block',
height: '28px',
paddingLeft: tokens.spacingM,
}}
>
Content
</Text>

<Flex flexDirection="column" style={{ gap: tokens.spacing2Xs }}>
{/* Active nav item: blue100 background, 34px tall, 4px radius */}
<Flex
alignItems="center"
style={{
height: '34px',
padding: `${tokens.spacing2Xs} ${tokens.spacingS}`,
gap: tokens.spacingXs,
borderRadius: tokens.borderRadiusSmall,
backgroundColor: tokens.blue100,
cursor: 'pointer',
}}
>
<ImageSquareIcon size="small" />
<Text style={{ color: tokens.gray900 }}>Entries</Text>
</Flex>

<Flex
alignItems="center"
style={{
height: '34px',
padding: `${tokens.spacing2Xs} ${tokens.spacingS}`,
gap: tokens.spacingXs,
borderRadius: tokens.borderRadiusSmall,
cursor: 'pointer',
}}
>
<TreeStructureIcon size="small" />
<Text style={{ color: tokens.gray900 }}>Content types</Text>
</Flex>

<Flex
alignItems="center"
style={{
height: '34px',
padding: `${tokens.spacing2Xs} ${tokens.spacingS}`,
gap: tokens.spacingXs,
borderRadius: tokens.borderRadiusSmall,
cursor: 'pointer',
}}
>
<TagIcon size="small" />
<Text style={{ color: tokens.gray900 }}>Tags</Text>
</Flex>
</Flex>
</Box>

{/* Content area: flex 1, 12px top padding, 24px horizontal */}
<Box
style={{
flex: 1,
backgroundColor: tokens.colorWhite,
borderRadius: '12px 12px 0 0',
padding: `${tokens.spacingS} ${tokens.spacingL} 0`,
}}
>
{/* Header with title + primary action */}
<Header
title="Entries"
actions={
<Button variant="primary" size="small" startIcon={<PlusIcon />}>
Add entry
</Button>
}
filters={
<TextInput
type="search"
placeholder="Search entries"
icon={<MagnifyingGlassIcon />}
/>
}
/>

{/* Table: full width, no horizontal padding, 36px header rows, 56px body rows */}
<Table>
<Table.Head>
<Table.Row>
<Table.Cell>Name</Table.Cell>
<Table.Cell>Content type</Table.Cell>
<Table.Cell>Updated</Table.Cell>
<Table.Cell>Status</Table.Cell>
<Table.Cell width="60px" />
</Table.Row>
</Table.Head>
<Table.Body>
<Table.Row>
<Table.Cell>Homepage banner</Table.Cell>
<Table.Cell>Banner</Table.Cell>
<Table.Cell>2 hours ago</Table.Cell>
<Table.Cell>
<EntityStatusBadge entityStatus="published" />
</Table.Cell>
<Table.Cell>
<Menu>
<Menu.Trigger>
<IconButton
icon={<DotsThreeIcon />}
aria-label="Actions"
variant="transparent"
size="small"
/>
</Menu.Trigger>
<Menu.List>
<Menu.Item>Edit</Menu.Item>
<Menu.Item>Duplicate</Menu.Item>
<Menu.Divider />
<Menu.Item>Delete</Menu.Item>
</Menu.List>
</Menu>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>

<Flex justifyContent="center" marginTop="spacingL">
<Pagination
activePage={1}
onPageChange={() => {}}
totalItems={50}
itemsPerPage={20}
/>
</Flex>
</Box>
</Flex>
</Box>
);
}

export { AppShellExample };
60 changes: 60 additions & 0 deletions skills/examples/confirmation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Modal, Button, Paragraph } from '@contentful/f36-components';
import { useState } from 'react';

function DeleteContentTypeModal() {
const [isOpen, setIsOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);

async function handleDelete() {
setIsDeleting(true);
// ... perform deletion
setIsDeleting(false);
setIsOpen(false);
}

return (
<>
<Button variant="negative" size="small" onClick={() => setIsOpen(true)}>
Delete content type
</Button>

<Modal
isShown={isOpen}
onClose={() => setIsOpen(false)}
size="small"
shouldCloseOnOverlayClick={false}
>
<Modal.Header
title="Delete content type"
onClose={() => setIsOpen(false)}
/>
<Modal.Content>
<Paragraph>
This content type and all its fields will be permanently deleted.
This cannot be undone.
</Paragraph>
</Modal.Content>
<Modal.Controls>
{/* Cancel label MUST be "Never mind" for destructive confirmations — never "Cancel" */}
<Button
variant="secondary"
size="small"
onClick={() => setIsOpen(false)}
>
Never mind
</Button>
<Button
variant="negative"
size="small"
isLoading={isDeleting}
onClick={handleDelete}
>
Delete content type
</Button>
</Modal.Controls>
</Modal>
</>
);
}

export { DeleteContentTypeModal };
88 changes: 88 additions & 0 deletions skills/examples/detail-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
Layout,
Header,
Button,
Stack,
FormControl,
TextInput,
Textarea,
EntityStatusBadge,
Note,
Paragraph,
SectionHeading,
Box,
} from '@contentful/f36-components';

function EntryDetailPage() {
return (
<Layout
variant="wide"
header={
<Layout.Header>
<Header
title="Homepage banner"
breadcrumbs={[{ label: 'Content', href: '/content' }]}
withBackButton
dividerLine
actions={
<Stack spacing="spacingS">
<Button variant="secondary" size="small">
Save
</Button>
<Button variant="positive" size="small">
Publish
</Button>
</Stack>
}
/>
</Layout.Header>
}
rightSidebar={
<Layout.Sidebar>
<Stack flexDirection="column" spacing="spacingM" padding="spacingM">
<SectionHeading>Status</SectionHeading>
<EntityStatusBadge entityStatus="draft" />

<SectionHeading marginTop="spacingL">Metadata</SectionHeading>
<Paragraph>Created: Jan 15, 2025</Paragraph>
<Paragraph>Updated: 2 hours ago</Paragraph>
</Stack>
</Layout.Sidebar>
}
>
<Layout.Body>
<Note variant="warning" marginBottom="spacingL">
You have unsaved changes that will be lost if you navigate away.
</Note>

<Stack flexDirection="column" spacing="spacingM">
<FormControl isRequired>
<FormControl.Label>Title</FormControl.Label>
<TextInput defaultValue="Homepage banner" />
</FormControl>

<FormControl>
<FormControl.Label>Description</FormControl.Label>
<Textarea
rows={4}
defaultValue="Main promotional banner for the homepage."
/>
<FormControl.HelpText>
A short description displayed below the banner title.
</FormControl.HelpText>
</FormControl>

<FormControl isRequired isInvalid>
<FormControl.Label>Slug</FormControl.Label>
<TextInput defaultValue="" />
<FormControl.ValidationMessage>
This field is required. Add a value before publishing.
</FormControl.ValidationMessage>
</FormControl>
</Stack>
</Layout.Body>
</Layout>
);
}

export { EntryDetailPage };
Loading
Loading