diff --git a/.changeset/dull-geckos-work.md b/.changeset/dull-geckos-work.md new file mode 100644 index 0000000000..e8df3deb1c --- /dev/null +++ b/.changeset/dull-geckos-work.md @@ -0,0 +1,5 @@ +--- +"@primer/view-components": patch +--- + +Move `navigation` styles to PVC diff --git a/.playwright/screenshots/previews.test.ts-snapshots/primer/alpha/tab_nav/default.png b/.playwright/screenshots/previews.test.ts-snapshots/primer/alpha/tab_nav/default.png new file mode 100644 index 0000000000..23bcbcdf0c Binary files /dev/null and b/.playwright/screenshots/previews.test.ts-snapshots/primer/alpha/tab_nav/default.png differ diff --git a/.playwright/screenshots/previews.test.ts-snapshots/primer/alpha/tab_nav/focused.png b/.playwright/screenshots/previews.test.ts-snapshots/primer/alpha/tab_nav/focused.png new file mode 100644 index 0000000000..d0e1c9bc98 Binary files /dev/null and b/.playwright/screenshots/previews.test.ts-snapshots/primer/alpha/tab_nav/focused.png differ diff --git a/app/components/primer/alpha/tab_nav.pcss b/app/components/primer/alpha/tab_nav.pcss new file mode 100644 index 0000000000..7d615baa3c --- /dev/null +++ b/app/components/primer/alpha/tab_nav.pcss @@ -0,0 +1,100 @@ +/* tabnav */ + +/* Outer wrapper */ +.tabnav { + margin-top: 0; + margin-bottom: var(--primer-stack-gap-normal, 16px); + border-bottom: var(--primer-borderWidth-thin, 1px) solid var(--color-border-default); +} + +.tabnav-tabs { + display: flex; + margin-bottom: calc(var(--primer-borderWidth-thin, 1px) * -1); + overflow: auto; +} + +.tabnav-tab { + display: inline-block; + flex-shrink: 0; + padding: var(--base-size-8, 8px) var(--primer-control-medium-paddingInline-spacious, 16px); + font-size: var(--primer-text-body-size-medium, 14px); + line-height: 23px; + color: var(--color-fg-muted); + text-decoration: none; + background-color: transparent; + border: var(--primer-borderWidth-thin, 1px) solid transparent; + border-bottom: 0; + transition: color 0.2s cubic-bezier(0.3, 0, 0.5, 1); + + &.selected, + &[aria-selected='true'], + &[aria-current]:not([aria-current='false']) { + color: var(--color-fg-default); + background-color: var(--color-canvas-default); /* cover bottom border */ + border-color: var(--color-border-default); + border-radius: var(--primer-borderRadius-medium, 6px) var(--primer-borderRadius-medium, 6px) 0 0; + + & .octicon { + color: inherit; + } + } + + &:hover { + color: var(--color-fg-default); + text-decoration: none; + transition-duration: 0.1s; + } + + &:focus, + &:focus-visible { + border-radius: var(--primer-borderRadius-medium, 6px) var(--primer-borderRadius-medium, 6px) 0 0 !important; + outline-offset: -6px; + } + + &:active { + color: var(--color-fg-muted); + } + + & .octicon { + margin-right: var(--primer-control-small-gap, 4px); + color: var(--color-fg-muted); + } + + & .Counter { + margin-left: var(--primer-control-small-gap, 4px); + color: inherit; + } +} + +/* Tabnav extras +** +** Tabnav extras are non-tab elements that sit in the tabnav. Usually they're +** inline text or links. */ + +.tabnav-extra { + display: inline-block; + padding-top: 10px; + margin-left: 10px; + font-size: var(--primer-text-body-size-small, 12px); + color: var(--color-fg-muted); + + & > .octicon { + margin-right: 2px; + } +} + +/* When tabnav-extra are anchors +** stylelint-disable-next-line selector-no-qualifying-type */ +a.tabnav-extra:hover { + color: var(--color-accent-fg); + text-decoration: none; +} + +/* Tabnav buttons +** +** For when there are multiple buttons, space them out appropriately. Requires +** the buttons to be floated or inline-block. */ + +.tabnav-btn { + margin-left: var(--primer-controlStack-medium-gap-condensed, 8px); +} diff --git a/app/components/primer/alpha/underline_nav.pcss b/app/components/primer/alpha/underline_nav.pcss new file mode 100644 index 0000000000..70abf4edfe --- /dev/null +++ b/app/components/primer/alpha/underline_nav.pcss @@ -0,0 +1,133 @@ +/* UnderlineNav */ + +.UnderlineNav { + display: flex; + min-height: var(--base-size-48, 48px); + overflow-x: auto; + overflow-y: hidden; + box-shadow: inset 0 -1px 0 var(--color-border-muted); + -webkit-overflow-scrolling: auto; + justify-content: space-between; + + & .Counter { + margin-left: var(--primer-control-medium-gap, 8px); + color: var(--color-fg-default); + background-color: var(--color-neutral-muted); + } + + & .Counter--primary { + color: var(--color-fg-on-emphasis); + background-color: var(--color-neutral-emphasis); + } +} + +.UnderlineNav-body { + display: flex; + align-items: center; + gap: var(--primer-control-medium-gap, 8px); + list-style: none; +} + +.UnderlineNav-item { + position: relative; + display: flex; + padding: 0 var(--primer-control-medium-paddingInline-condensed, 8px); + font-size: var(--primer-text-body-size-medium, 14px); + line-height: 30px; + color: var(--color-fg-default); + text-align: center; + white-space: nowrap; + cursor: pointer; + background-color: transparent; + border: 0; + border-radius: var(--primer-borderRadius-medium, 6px); + align-items: center; + + &:hover, + &:focus, + &:focus-visible { + color: var(--color-fg-default); + text-decoration: none; + border-bottom-color: var(--color-neutral-muted); + outline-offset: -2px; + transition: border-bottom-color 0.12s ease-out; + } + + /* renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected */ + & [data-content]::before { + display: block; + height: 0; + font-weight: var(--base-text-weight-semibold, 600); + visibility: hidden; + content: attr(data-content); + } + + /* increase touch target area */ + &::before { + @mixin minTouchTarget 48px; + } + + /* hover state was "sticking" on mobile after click */ + @media (pointer: fine) { + &:hover { + color: var(--color-fg-default); + text-decoration: none; + background: var(--color-action-list-item-default-hover-bg); + transition: background 0.12s ease-out; + } + } + + &.selected, + &[role='tab'][aria-selected='true'], + &[aria-current]:not([aria-current='false']) { + font-weight: var(--base-text-weight-semibold, 600); + color: var(--color-fg-default); + border-bottom-color: var(--color-primer-border-active); + + /* current/selected underline */ + &::after { + position: absolute; + right: 50%; + bottom: calc(50% - 25px); /* 48px total height / 2 (24px) + 1px */ + width: 100%; + height: 2px; + content: ''; + background: var(--color-primer-border-active); + border-radius: var(--primer-borderRadius-medium, 6px); + transform: translate(50%, -50%); + } + } +} + +.UnderlineNav--right { + justify-content: flex-end; + + & .UnderlineNav-actions { + flex: 1 1 auto; + } +} + +.UnderlineNav-actions { + align-self: center; +} + +.UnderlineNav--full { + display: block; + + /* required for underline to align with additional wrapper element */ + & .UnderlineNav-body { + min-height: var(--base-size-48, 48px); + } +} + +.UnderlineNav-octicon { + display: inline !important; + margin-right: var(--primer-control-medium-gap, 8px); + color: var(--color-fg-muted); + fill: var(--color-fg-muted); +} + +.UnderlineNav-container { + display: flex; + justify-content: space-between; +} diff --git a/app/components/primer/menu_component.pcss b/app/components/primer/menu_component.pcss new file mode 100644 index 0000000000..fcfc59b5af --- /dev/null +++ b/app/components/primer/menu_component.pcss @@ -0,0 +1,119 @@ +/* menu */ + +/* A menu on the side of a page, defaults to left side. e.g. github.com/about */ + +.menu { + margin-bottom: var(--primer-stack-gap-normal, 16px); + list-style: none; + background-color: var(--color-canvas-default); + border: var(--primer-borderWidth-thin, 1px) solid var(--color-border-default); + border-radius: var(--primer-borderRadius-medium, 6px); +} + +.menu-item { + position: relative; + display: block; + padding: var(--primer-control-medium-paddingInline-condensed, 8px) var(--primer-control-medium-paddingInline-spacious, 16px); + color: var(--color-fg-default); + border-bottom: var(--primer-borderWidth-thin, 1px) solid var(--color-border-muted); + + &:first-child { + border-top: 0; + border-top-left-radius: var(--primer-borderRadius-medium, 6px); + border-top-right-radius: var(--primer-borderRadius-medium, 6px); + + &::before { + border-top-left-radius: var(--primer-borderRadius-medium, 6px); + } + } + + &:last-child { + border-bottom: 0; + border-bottom-right-radius: var(--primer-borderRadius-medium, 6px); + border-bottom-left-radius: var(--primer-borderRadius-medium, 6px); + + &::before { + border-bottom-left-radius: var(--primer-borderRadius-medium, 6px); + } + } + + &:hover { + text-decoration: none; + background-color: var(--color-neutral-subtle); + } + + &:active { + background-color: var(--color-canvas-subtle); + } + + &.selected, + &[aria-selected='true'], + &[aria-current]:not([aria-current='false']) { + cursor: default; + background-color: var(--color-menu-bg-active); + + &::before { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 2px; + content: ''; + background-color: var(--color-primer-border-active); + } + } + + & .octicon { + width: 16px; + margin-right: var(--primer-control-medium-gap, 8px); + color: var(--color-fg-muted); + text-align: center; + } + + & .Counter { + float: right; + margin-left: var(--primer-control-small-gap, 4px); + } + + & .menu-warning { + float: right; + color: var(--color-attention-fg); + } + + & .avatar { + float: left; + margin-right: var(--primer-control-small-gap, 4px); + } + + &.alert { + & .Counter { + color: var(--color-danger-fg); + } + } +} + +.menu-heading { + display: block; + padding: var(--primer-control-medium-paddingInline-condensed, 8px) var(--primer-control-medium-paddingInline-spacious, 16px); + margin-top: 0; + margin-bottom: 0; + font-size: inherit; + font-weight: var(--base-text-weight-semibold, 600); + color: var(--color-fg-default); + border-bottom: var(--primer-borderWidth-thin, 1px) solid var(--color-border-muted); + + &:hover { + text-decoration: none; + } + + &:first-child { + border-top-left-radius: var(--primer-borderRadius-medium, 6px); + border-top-right-radius: var(--primer-borderRadius-medium, 6px); + } + + &:last-child { + border-bottom: 0; + border-bottom-right-radius: var(--primer-borderRadius-medium, 6px); + border-bottom-left-radius: var(--primer-borderRadius-medium, 6px); + } +} diff --git a/app/components/primer/primer.pcss b/app/components/primer/primer.pcss index 7af700d492..2665c102fc 100644 --- a/app/components/primer/primer.pcss +++ b/app/components/primer/primer.pcss @@ -2,7 +2,9 @@ @import "./alpha/action_list.pcss"; @import "./alpha/auto_complete.pcss"; @import "./alpha/banner.pcss"; +@import "./alpha/tab_nav.pcss"; @import "./alpha/toggle_switch.pcss"; +@import "./alpha/underline_nav.pcss"; @import "./alpha/segmented_control.pcss"; @import "./beta/avatar.pcss"; @import "./beta/avatar_stack.pcss"; @@ -15,6 +17,7 @@ @import "./beta/progress_bar.pcss"; @import "./beta/truncate.pcss"; @import "./dropdown.pcss"; +@import "./menu_component.pcss"; @import "./popover_component.pcss"; @import "./state_component.pcss"; @import "./subhead_component.pcss"; diff --git a/demo/app/assets/stylesheets/application.css b/demo/app/assets/stylesheets/application.css index 3d125a854c..6ee5103efd 100644 --- a/demo/app/assets/stylesheets/application.css +++ b/demo/app/assets/stylesheets/application.css @@ -9,7 +9,6 @@ *= require @primer/css/dist/forms.css *= require @primer/css/dist/layout.css *= require @primer/css/dist/links.css - *= require @primer/css/dist/navigation.css *= require @primer/css/dist/overlay.css *= require @primer/css/dist/utilities.css *= require @primer/css/dist/markdown.css diff --git a/previews/primer/alpha/tab_nav_preview.rb b/previews/primer/alpha/tab_nav_preview.rb new file mode 100644 index 0000000000..cd47d7899d --- /dev/null +++ b/previews/primer/alpha/tab_nav_preview.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Primer + module Alpha + # @label TabNav + class TabNavPreview < ViewComponent::Preview + # @label Playground + # + # @param number_of_tabs [Integer] number + # @param with_icons [Boolean] toggle + # @param with_counters [Boolean] toggle + def playground(number_of_tabs: 3, with_icons: false, with_counters: false) + render(Primer::Alpha::TabNav.new(label: "label")) do |c| + Array.new(number_of_tabs || 3) do |i| + c.with_tab(selected: i.zero?, href: "##{i + 1}") do |t| + t.icon(icon: :star) if with_icons + t.text { "Tab #{i + 1}" } + t.counter(count: 10) if with_counters + end + end + end + end + + # @label Default + def default + render(Primer::Alpha::TabNav.new(label: "Default")) do |c| + c.with_tab(selected: true, href: "#") { "Tab 1" } + c.with_tab(href: "#") { "Tab 2" } + c.with_tab(href: "#") { "Tab 3" } + end + end + + # @label With icons and counters + def with_icons_and_counters + render(Primer::Alpha::TabNav.new(label: "With icons and counters")) do |c| + c.with_tab(href: "#1", selected: true) do |t| + t.icon(icon: :star) + t.text { "Stars" } + t.counter(count: 10) + end + c.with_tab(href: "#2") do |t| + t.icon(icon: :heart) + t.text { "Sponsors" } + t.counter(count: 14) + end + c.with_tab(href: "#3") do |t| + t.icon(icon: :bookmark) + t.text { "Bookmarks" } + t.counter(count: 7) + end + end + end + end + end +end diff --git a/test/css/component_selector_use_test.rb b/test/css/component_selector_use_test.rb index 7ef500ddad..c14c53a91d 100644 --- a/test/css/component_selector_use_test.rb +++ b/test/css/component_selector_use_test.rb @@ -6,6 +6,7 @@ # rubocop:disable Style/WordArray IGNORED_SELECTORS = { :global => ["preview-wrap"], + Primer::Alpha::TabNav => ["octicon", "octicon-bookmark", "octicon-heart", "octicon-star"], Primer::Alpha::TabPanels => ["tabnav", "tabnav-tab", "tabnav-tabs"], Primer::Beta::Button => ["Button--medium", "octicon", "octicon-search", "Button--invisible-noVisuals"], Primer::Dropdown => ["btn", "details-overlay", "details-reset", "dropdown-menu-se", "octicon", "octicon-triangle-down"], diff --git a/test/css/component_specific_selectors_test.rb b/test/css/component_specific_selectors_test.rb index 5d5accfa2b..97dd616c0c 100644 --- a/test/css/component_specific_selectors_test.rb +++ b/test/css/component_specific_selectors_test.rb @@ -34,9 +34,21 @@ class ComponentSpecificSelectorsTest < Minitest::Test Primer::Alpha::Banner => [ ".Banner .Banner-close" ], + Primer::Alpha::TabNav => [ + ".tabnav-tab.selected", + ".tabnav-extra", + ".tabnav-btn" + ], Primer::Alpha::SegmentedControl => [ ".Button-withTooltip" ], + Primer::Alpha::UnderlineNav => [ + ".UnderlineNav .Counter--primary", + ".UnderlineNav-item.selected", + ".UnderlineNav--right", + ".UnderlineNav--full", + ".UnderlineNav-container" + ], Primer::Beta::Button => [ "summary.Button", ".Button-content--alignStart",