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..f44dc7b2654 --- /dev/null +++ b/packages/react/src/stories/NoodlesPage.stories.module.css @@ -0,0 +1,382 @@ +.ThemePortals { + position: absolute; + top: 0; + left: 0; + width: 100%; +} + +.Page { + position: relative; + display: flex; + flex-direction: column; + height: 100dvh; + min-height: 480px; +} + +.Surface { + flex: 1; + min-height: 0; +} + +.Workspace { + height: 100%; + min-height: 0; + 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); + padding-bottom: var(--base-size-8); +} + +.Brand, +.Account { + display: flex; + align-items: center; + gap: var(--base-size-8); + min-height: var(--control-large-size); +} + +.Brand { + justify-content: space-between; + min-height: calc(var(--control-large-size) + var(--base-size-8)); + padding-inline: var(--base-size-8) var(--base-size-4); +} + +.Wordmark { + display: flex; + align-items: center; + gap: var(--base-size-8); + font-family: var(--fontStack-monospace); + font-size: var(--NoodlesPage--wordmark-fontSize, inherit); + line-height: var(--NoodlesPage--wordmark-lineHeight, inherit); + color: var(--fgColor-muted); +} + +.Project { + position: relative; + flex-shrink: 0; + height: var(--control-large-size); + margin-top: var(--base-size-6); + margin-bottom: var(--base-size-4); + border-radius: var(--borderRadius-medium); +} + +.Project:where(:hover, :focus-within) { + background-color: var(--NoodlesPage--project-bgColor-hover, var(--button-invisible-bgColor-hover)); +} + +.NoodleCover { + display: block; + width: var(--base-size-16); + height: var(--base-size-24); + background: linear-gradient( + 135deg, + var(--bgColor-emphasis), + color-mix(in srgb, var(--bgColor-done-emphasis) 25%, var(--bgColor-emphasis)), + color-mix(in srgb, var(--bgColor-accent-emphasis) 35%, var(--bgColor-emphasis)) + ); + border: var(--borderWidth-thin) solid var(--borderColor-muted); + border-radius: var(--base-size-2); +} + +.CoverChevron { + display: flex; + color: var(--fgColor-onEmphasis); +} + +.Navigation { + flex: 1; + min-height: 0; + overflow: auto; +} + +.SidebarFooter { + display: grid; + flex-shrink: 0; + margin-top: var(--base-size-16); + font-size: var(--text-body-size-small); + line-height: var(--base-size-20); +} + +.Account { + position: relative; + min-height: var(--control-small-size); + margin-top: calc(var(--base-size-16) + var(--borderWidth-thin)); + gap: 0; + + &::before { + position: absolute; + top: calc(-1 * (var(--base-size-8) + var(--borderWidth-thin))); + inset-inline: var(--base-size-8); + height: var(--borderWidth-thin); + content: ''; + background: var(--borderColor-default); + } +} + +.Avatar { + display: grid; + width: var(--base-size-20); + height: var(--base-size-20); + place-items: center; + font-size: calc(var(--text-body-size-small) - var(--base-size-4)); + color: var(--fgColor-onEmphasis); + background: var(--bgColor-emphasis); + border-radius: var(--borderRadius-full); +} + +.PagePanel { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + min-height: 0; + overflow: hidden; + background: var(--bgColor-default); + border-radius: var(--borderRadius-large); +} + +.Header { + display: flex; + flex-shrink: 0; + align-items: center; + gap: var(--base-size-4); + min-height: var(--NoodlesPage--header-minHeight, var(--control-large-size)); + padding: var(--base-size-4) var(--base-size-8); + font-size: var(--text-body-size-small); +} + +.Breadcrumb { + display: flex; + align-items: center; + gap: var(--base-size-4); + min-width: 0; +} + +.CurrentTitle { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.HeaderActions, +.SecondaryActions { + display: flex; + align-items: center; + gap: var(--base-size-4); +} + +.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: var(--NoodlesPage--document-width, 768px); + margin-inline: auto; + padding: var(--base-size-64) var(--base-size-40) var(--base-size-128); + font-size: var(--text-body-size-medium); + line-height: var(--text-body-lineHeight-medium); + + p { + margin: var(--base-size-12) 0 0; + } + + ul { + display: grid; + gap: var(--base-size-16); + padding-left: var(--base-size-24); + margin: var(--base-size-12) 0 0; + } + + li { + padding-left: var(--base-size-4); + } + + li::marker { + color: var(--fgColor-muted); + } +} + +.PagePanel:where([data-wide='true']) .Document { + max-width: 1120px; +} + +.Introduction, +.Section { + position: relative; + margin-top: var(--base-size-32); +} + +.BlockHandle { + position: absolute; + top: 50%; + left: calc(-1 * var(--base-size-24)); + color: var(--fgColor-muted); + opacity: 0; +} + +.Section:hover .BlockHandle { + opacity: 1; +} + +.EditorFooter { + display: flex; + flex-shrink: 0; + align-items: center; + gap: var(--base-size-4); + min-height: var(--control-medium-size); + padding-inline: var(--base-size-8); + font-size: var(--text-body-size-small); +} + +.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); + + form { + display: grid; + gap: var(--base-size-12); + } + + 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; + flex-wrap: nowrap; + } + + .SidebarRegion { + display: none; + } + + .CommentsRegion { + flex-shrink: 0; + width: 100%; + height: min(320px, 40dvh); + + > [data-component='PageLayout.Pane'] { + width: 100%; + } + } + + .Comments { + max-height: 40dvh; + 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; + } + + .Sidebar { + padding-right: 0; + } + + .Project, + .SidebarFooter { + display: none; + } + + .Header { + flex-wrap: wrap; + } + + .Breadcrumb { + flex: 1; + max-width: 100%; + } + + .HeaderActions { + margin-left: 0; + } + + .Document { + padding: var(--base-size-32) var(--base-size-24) var(--base-size-64); + } + + .BlockHandle { + display: none; + } +} diff --git a/packages/react/src/stories/NoodlesPage.stories.tsx b/packages/react/src/stories/NoodlesPage.stories.tsx new file mode 100644 index 00000000000..95096b9c7e1 --- /dev/null +++ b/packages/react/src/stories/NoodlesPage.stories.tsx @@ -0,0 +1,772 @@ +import {useId, useMemo, useRef, useState} from 'react' +import {clsx} from 'clsx' +import type {Meta, StoryObj} from '@storybook/react-vite' +import { + ArrowDownIcon, + ArrowUpIcon, + BookIcon, + ChevronDownIcon, + CodeSquareIcon, + CommentDiscussionIcon, + CommentIcon, + CopyIcon, + GearIcon, + GrabberIcon, + HomeIcon, + KebabHorizontalIcon, + LinkIcon, + PaperclipIcon, + PencilIcon, + PlusIcon, + ScreenFullIcon, + SidebarCollapseIcon, + SidebarExpandIcon, + SyncIcon, + ZapIcon, +} from '@primer/octicons-react' +import {ActionList} from '../ActionList' +import {ActionMenu} from '../ActionMenu' +import {Button, IconButton} from '../Button' +import CounterLabel from '../CounterLabel' +import {Dialog} from '../Dialog' +import FormControl from '../FormControl' +import Heading from '../Heading' +import {NavList} from '../NavList' +import {PageLayout} from '../PageLayout' +import {PortalContext, registerPortalRoot} from '../Portal' +import Textarea from '../Textarea' +import TextInput from '../TextInput' +import { + noodlesPages, + noodlesProjectCover, + noodlesProjectPages, + noodlesReports, + noodlesStatusSections, +} from './NoodlesPage.fixtures' +import type {NoodlesPageFixture} from './NoodlesPage.fixtures' +import classes from './NoodlesPage.stories.module.css' +import theme from './NoodlesPage.theme.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. ' + + 'A standalone Noodles-inspired workspace with a fixed, page-scoped Noodles theme. NoodlesPage.theme.module.css owns theme tokens and component overrides, including the transparent sidebar divider; NoodlesPage.stories.module.css owns the story layout, scrolling and responsive regions. Five optional ActionList and Button styling hooks provide its label typography and button typography; existing semantic tokens handle the remaining colors and selection styling. Menus and dialogs inherit the same theme through a registered portal container. Navigation, page creation, comments, attachments, reading width, focus mode and Markdown preview work locally with original fictional report fixtures. No account connection, remote writes or persistence. Reloading resets the 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 NoodleCover() { + return