diff --git a/.changeset/noodles-component-typography.md b/.changeset/noodles-component-typography.md new file mode 100644 index 00000000000..2a8abf33b18 --- /dev/null +++ b/.changeset/noodles-component-typography.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +ActionList, Button: Support scoped label and button typography customization while preserving default styling. diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 7a4a9065c0a..44e1df2c411 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -221,7 +221,7 @@ outline: 2px solid transparent; & .ItemLabel { - font-weight: var(--base-text-weight-semibold); + font-weight: var(--ActionList--label-fontWeight-selected, var(--base-text-weight-semibold)); color: var(--control-fgColor-rest); } @@ -578,7 +578,7 @@ background: var(--control-transparent-bgColor-selected); & .ItemLabel { - font-weight: var(--base-text-weight-semibold); + font-weight: var(--ActionList--label-fontWeight-selected, var(--base-text-weight-semibold)); } & .ActionListSubContent::before, @@ -708,12 +708,12 @@ span wrapping svg or text */ /* text */ .ItemLabel { position: relative; - font-size: var(--text-body-size-medium); + font-size: var(--ActionList--label-fontSize-medium, var(--text-body-size-medium)); font-weight: var(--base-text-weight-normal); /* stylelint-disable-next-line primer/typography */ line-height: 20px; /* temporary until we fix line-height rounding in primitives */ - color: var(--fgColor-default); + color: var(--ActionList--label-fgColor, var(--fgColor-default)); grid-area: label; /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated */ word-break: break-word; diff --git a/packages/react/src/Button/ButtonBase.module.css b/packages/react/src/Button/ButtonBase.module.css index 278b4979314..3e508c19a75 100644 --- a/packages/react/src/Button/ButtonBase.module.css +++ b/packages/react/src/Button/ButtonBase.module.css @@ -6,8 +6,8 @@ /* stylelint-disable-next-line primer/spacing */ padding: 0 var(--control-medium-paddingInline-normal); font-family: inherit; - font-size: var(--text-body-size-medium); - font-weight: var(--base-text-weight-medium); + font-size: var(--Button--fontSize-medium, var(--text-body-size-medium)); + font-weight: var(--Button--fontWeight, var(--base-text-weight-medium)); color: var(--button-default-fgColor-rest); text-align: center; text-decoration: none; diff --git a/packages/react/src/stories/NoodlesPage.fixtures.ts b/packages/react/src/stories/NoodlesPage.fixtures.ts new file mode 100644 index 00000000000..569a9d0fc74 --- /dev/null +++ b/packages/react/src/stories/NoodlesPage.fixtures.ts @@ -0,0 +1,87 @@ +export interface NoodlesPageFixture { + id: string + title: string + count?: number + parentId?: string +} + +export interface NoodlesSectionFixture { + heading: string + paragraph?: string + items?: readonly string[] +} + +export const noodlesProjectCover: NoodlesPageFixture = { + id: 'project-cover', + title: 'Octoinvaders \u00b7 Ink Shield', +} + +export const noodlesProjectPages: readonly NoodlesPageFixture[] = [ + {id: 'onboarding', title: 'First-use onboarding', parentId: 'project-cover'}, + {id: 'redirect', title: 'Redirect timing', parentId: 'project-cover'}, + {id: 'pacing', title: 'Wave-two pacing', parentId: 'project-cover'}, + {id: 'accessible-feedback', title: 'Accessible shield feedback', parentId: 'project-cover'}, + {id: 'guardrails', title: 'Rollout guardrails', parentId: 'project-cover'}, +] + +export const noodlesPages: readonly NoodlesPageFixture[] = [ + {id: 'status-updates', title: 'Status updates', count: 12}, + {id: 'standups', title: 'Standups', count: 60}, + {id: 'prototype', title: 'Ink Shield interactive prototype'}, + {id: 'visual-direction', title: 'Octoinvaders visual direction'}, + {id: 'inspiration', title: 'Arcade inspiration'}, + {id: 'feature-brief', title: 'Feature brief'}, + {id: 'mechanics', title: 'Mechanics specification'}, + {id: 'waves', title: 'Wave and character design'}, + {id: 'art-audio', title: 'Art and audio direction'}, + {id: 'dashboard', title: 'Kusto playtest dashboard'}, + {id: 'delivery', title: 'Ink Shield delivery board'}, + {id: 'telemetry', title: 'Telemetry and experiment plan'}, + {id: 'checklist', title: 'Playtest and launch checklist'}, +] + +export const noodlesReports = [ + 'October 2, 2026', + 'September 25, 2026', + 'September 18, 2026', + 'September 11, 2026', + 'September 4, 2026', + 'August 28, 2026', + 'August 21, 2026', + 'August 14, 2026', + 'August 7, 2026', + 'July 31, 2026', + 'July 24, 2026', + 'July 17, 2026', +] as const + +// Original demo copy, not content copied from the authenticated reference. +export const noodlesStatusSections: readonly NoodlesSectionFixture[] = [ + { + heading: '1/ What got done this week? What was the outcome?', + items: [ + 'Finished the last input and reporting improvements, then practiced switching the shield off without stopping a session.', + 'Prepared the delivery board, owner rotation, and the small-cohort release checklist.', + 'The fictional playtest suggests clearer shield guidance and steadier pacing. We will verify these observations with a new cohort.', + ], + }, + { + heading: '2/ What is the plan for next week?', + items: [ + 'Invite the next internal group and review the experience together each day.', + 'Expand the audience only after the team reviews the results and signs off on the release.', + ], + }, + { + heading: '3/ What risks, decisions, or blockers need attention?', + paragraph: + 'These are fictional planning notes, not production measurements. A wider release needs a fresh playtest and an accessibility review.', + }, + { + heading: '4/ What help do you need?', + items: [ + 'A second reviewer for the keyboard and controller flows.', + 'A release partner to confirm ownership of the monitoring checklist.', + ], + }, +] diff --git a/packages/react/src/stories/NoodlesPage.stories.module.css b/packages/react/src/stories/NoodlesPage.stories.module.css new file mode 100644 index 00000000000..239afa9421d --- /dev/null +++ b/packages/react/src/stories/NoodlesPage.stories.module.css @@ -0,0 +1,219 @@ +.Workspace { + height: 100dvh; + min-height: 480px; + padding: var(--base-size-8); + color: var(--fgColor-default); + background: var(--bgColor-muted); +} + +.Layout { + height: 100%; +} + +/* Keep PageLayout's flex wrappers shrinkable so the document and navigation scroll independently. */ +.Layout, +.Layout > div, +.Layout > div > div, +.ContentRegion, +.ContentRegion > div { + min-height: 0; + flex-wrap: nowrap; +} + +.ContentRegion > div { + display: flex; + flex-direction: column; +} + +.Sidebar { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding-right: var(--base-size-8); +} + +.Brand { + min-height: calc(var(--control-large-size) + var(--base-size-8)); +} + +.Project { + flex-shrink: 0; + margin-block: var(--base-size-4); +} + +.ProjectCoverLink { + flex: 1; + min-width: 0; +} + +.Navigation { + flex: 1; + min-height: 0; + overflow: auto; +} + +.SidebarFooter { + padding-top: var(--base-size-12); +} + +.Account { + margin-top: var(--base-size-12); + border-top: var(--borderWidth-thin) solid var(--borderColor-default); +} + +.PagePanel { + flex: 1; + min-width: 0; + min-height: 0; + overflow: hidden; + background: var(--bgColor-default); + border-radius: var(--borderRadius-large); +} + +.Header { + flex-shrink: 0; + min-height: var(--control-large-size); +} + +.Breadcrumb { + min-width: 0; +} + +.HeaderActions { + flex-shrink: 0; + margin-left: auto; +} + +.HeaderDivider { + height: var(--base-size-16); + margin-inline: var(--base-size-4); + border-left: var(--borderWidth-thin) solid var(--borderColor-default); +} + +.ScrollArea { + flex: 1; + min-height: 0; + overflow: auto; + scrollbar-gutter: stable; +} + +.Document { + box-sizing: border-box; + width: 100%; + max-width: 768px; + margin-inline: auto; + padding: var(--base-size-64) var(--base-size-40) var(--base-size-128); + overflow-wrap: anywhere; + + :where(p, ul) { + margin: 0; + } +} + +.PagePanel:where([data-wide='true']) .Document { + max-width: 1120px; +} + +.EditorFooter { + flex-shrink: 0; + min-height: var(--control-medium-size); +} + +.Notice { + flex: 1; + min-width: 0; + overflow: hidden; + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.Muted { + color: var(--fgColor-muted); +} + +.Comments { + height: 100%; + min-width: 0; + overflow: auto; + padding: var(--base-size-16); + border-left: var(--borderWidth-thin) solid var(--borderColor-default); + + li { + overflow-wrap: anywhere; + } +} + +.CommentsRegion > [data-component='PageLayout.Pane'] { + height: 100%; + min-height: 0; +} + +@media (max-width: 1100px) { + .SecondaryActions { + display: none; + } + + .Workspace:where([data-comments='true']) { + .Layout > div > div { + flex-direction: column; + } + + .SidebarRegion { + display: none; + } + + .CommentsRegion { + flex-shrink: 0; + width: 100%; + height: min(320px, 40dvh); + + > [data-component='PageLayout.Pane'] { + width: 100%; + } + } + + .Comments { + border-top: var(--borderWidth-thin) solid var(--borderColor-default); + border-left: 0; + } + } +} + +@media (max-width: 767px) { + .Layout { + flex-direction: column; + } + + .SidebarRegion { + width: 100%; + height: 30dvh; + + > [data-component='PageLayout.VerticalDivider'] { + display: none; + } + } + + .Sidebar { + padding-right: 0; + } + + .Project, + .SidebarFooter { + display: none; + } + + .Breadcrumb { + flex-basis: 100%; + } + + .HeaderActions { + margin-left: 0; + } + + .Document { + padding: var(--base-size-32) var(--base-size-24) var(--base-size-64); + } +} diff --git a/packages/react/src/stories/NoodlesPage.stories.tsx b/packages/react/src/stories/NoodlesPage.stories.tsx new file mode 100644 index 00000000000..681c11e2e83 --- /dev/null +++ b/packages/react/src/stories/NoodlesPage.stories.tsx @@ -0,0 +1,750 @@ +import {useId, useRef, useState} from 'react' +import type {Meta, StoryObj} from '@storybook/react-vite' +import { + ArrowDownIcon, + ArrowUpIcon, + BookIcon, + ChevronDownIcon, + CodeSquareIcon, + CommentDiscussionIcon, + CommentIcon, + GearIcon, + HomeIcon, + KebabHorizontalIcon, + LinkIcon, + PaperclipIcon, + PencilIcon, + PersonIcon, + PlusIcon, + ScreenFullIcon, + SidebarCollapseIcon, + SidebarExpandIcon, + SyncIcon, + ZapIcon, +} from '@primer/octicons-react' +import {ActionList} from '../ActionList' +import {ActionMenu} from '../ActionMenu' +import {Button, IconButton} from '../Button' +import {Dialog} from '../Dialog' +import FormControl from '../FormControl' +import Heading from '../Heading' +import {NavList} from '../NavList' +import {PageLayout} from '../PageLayout' +import {Stack} from '../Stack' +import Textarea from '../Textarea' +import TextInput from '../TextInput' +import Truncate from '../Truncate' +import { + noodlesPages, + noodlesProjectCover, + noodlesProjectPages, + noodlesReports, + noodlesStatusSections, +} from './NoodlesPage.fixtures' +import type {NoodlesPageFixture} from './NoodlesPage.fixtures' +import {NoodlesThemedPage} from './NoodlesPage.themed' +import classes from './NoodlesPage.stories.module.css' + +const meta = { + title: 'Examples/Noodles page', + globals: {colorScheme: 'light'}, + parameters: { + layout: 'fullscreen', + controls: {disable: true}, + docs: { + description: { + component: + 'PageLayout provides the sidebar, document and comments regions. The native resizable sidebar supports dragging and keyboard resizing from 240px to 480px, preserves its width through navigation toggles and focus mode, and resets to 312px on double-click or reload. ' + + 'Two standalone versions of a Noodles-inspired workspace: Primer noodles uses default Primer appearance, while Noodles noodles uses the fixed, page-scoped Noodles theme. Both use the same original fictional fixtures and support local navigation, page creation, comments, attachments, reading width, focus mode and Markdown preview. No appearance-playground controls, account connection, remote writes or persistence. Reloading resets each demo.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj +type UtilityDialog = 'agent' | 'settings' | 'attachments' | null +type EditDialog = 'new' | 'rename' | null +const initialPages = [noodlesProjectCover, ...noodlesProjectPages, ...noodlesPages] + +function NoodlesPage() { + const [pages, setPages] = useState([...initialPages]) + const [selectedId, setSelectedId] = useState('status-updates') + const [reportIndex, setReportIndex] = useState(0) + const [sidebarVisible, setSidebarVisible] = useState(true) + const [sidebarWidth, setSidebarWidth] = useState(312) + const [focusMode, setFocusMode] = useState(false) + const [wide, setWide] = useState(false) + const [markdown, setMarkdown] = useState(false) + const [commentsOpen, setCommentsOpen] = useState(false) + const [notesByPage, setNotesByPage] = useState>({}) + const [noteDraft, setNoteDraft] = useState('') + const [utilityDialog, setUtilityDialog] = useState(null) + const [editDialog, setEditDialog] = useState(null) + const [draftTitle, setDraftTitle] = useState('') + const [titleError, setTitleError] = useState(false) + const [attachmentsByPage, setAttachmentsByPage] = useState>({}) + const [deliveryGroup, setDeliveryGroup] = useState('Third slice') + const [notice, setNotice] = useState('') + const [reportTitles, setReportTitles] = useState>({}) + const id = useId() + const nextId = useRef(0) + const titleRef = useRef(null) + const selected = pages.find(page => page.id === selectedId) + if (!selected) throw new Error(`Unknown local Noodles page: ${selectedId}`) + const reportDate = noodlesReports.find((_, index) => index === reportIndex) + if (!reportDate) throw new Error(`Unknown local Noodles report: ${reportIndex}`) + const isReport = selected.id === 'status-updates' + const pageKey = isReport ? reportDate : selected.id + const notes = notesByPage[pageKey] ?? [] + const attachments = attachmentsByPage[pageKey] ?? [] + const title = isReport ? (reportTitles[reportDate] ?? reportDate) : selected.title + const markdownPreview = [ + `# ${title}`, + '', + isReport ? `## Week ${12 - reportIndex}: Rollout readiness` : `## ${selected.title}`, + '', + 'Demo notes for the Octoinvaders Ink Shield team.', + ...noodlesStatusSections.flatMap(section => [ + '', + `## ${section.heading}`, + '', + ...(section.items?.map(item => `- ${item}`) ?? [section.paragraph]), + ]), + ].join('\n') + + function openEditor(mode: EditDialog) { + setDraftTitle(mode === 'rename' ? title : '') + setTitleError(false) + setEditDialog(mode) + } + + function addPage(pageTitle: string, parentId?: string) { + const page = {id: `${id}-page-${nextId.current++}`, title: pageTitle, parentId} + setPages(current => [...current, page]) + setSelectedId(page.id) + setNotice(`Created "${pageTitle}" locally. Nothing was saved remotely.`) + } + + function reset() { + setPages([...initialPages]) + setSelectedId('status-updates') + setReportIndex(0) + setReportTitles({}) + setNotesByPage({}) + setAttachmentsByPage({}) + setNoteDraft('') + setMarkdown(false) + setNotice('Local pages, comments and attachments reset.') + } + + function openCover() { + setSelectedId('project-cover') + setMarkdown(false) + } + + function renderPage(page: NoodlesPageFixture) { + return ( + { + event.preventDefault() + setSelectedId(page.id) + setMarkdown(false) + }} + > + {page.title} + {page.count !== undefined && {page.count}} + + ) + } + + return ( +
+ + {sidebarVisible && !focusMode && ( + + + + )} + + + + {(!sidebarVisible || focusMode) && ( + { + setSidebarVisible(true) + setFocusMode(false) + }} + /> + )} + {selected.id === 'project-cover' && ( + + )} + + + + {isReport ? ( + + + + {title} + + + + + {noodlesReports.map((report, index) => ( + setReportIndex(index)} + > + {report} + + ))} + + + + ) : ( + + {title} + + )} + + + + + + + openEditor('rename')}>Rename page + addPage(`${title} (copy)`, selected.parentId)}> + Duplicate page + + + Reset local changes + + + + openEditor('new')} + /> + + + setCommentsOpen(value => !value)} + /> + + setUtilityDialog('attachments')} + /> + setUtilityDialog('agent')} + /> + setUtilityDialog('agent')} + /> + + setFocusMode(value => !value)} + /> + + setReportIndex(index => index + 1)} + /> + setReportIndex(index => index - 1)} + /> + + +
+
+ {markdown ? ( +