diff --git a/app/components/primer/beta/page_layout.html.erb b/app/components/primer/beta/page_layout.html.erb
new file mode 100644
index 0000000000..4e261ecfdd
--- /dev/null
+++ b/app/components/primer/beta/page_layout.html.erb
@@ -0,0 +1,15 @@
+<%= render Primer::BaseComponent.new(**@system_arguments) do %>
+ <%= render(Primer::BaseComponent.new(tag: :div, classes: "PageLayout-wrapper #{@wrapper_sizing_class}")) do %>
+ <%= header_region %>
+ <%= render(Primer::BaseComponent.new(tag: :div, classes: "PageLayout-columns")) do %>
+ <% if pane_region.render_first? %>
+ <%= pane_region %>
+ <%= content_region %>
+ <% else %>
+ <%= content_region %>
+ <%= pane_region %>
+ <% end %>
+ <% end %>
+ <%= footer_region %>
+ <% end %>
+<% end %>
diff --git a/app/components/primer/beta/page_layout.rb b/app/components/primer/beta/page_layout.rb
new file mode 100644
index 0000000000..f4a5e3d5cb
--- /dev/null
+++ b/app/components/primer/beta/page_layout.rb
@@ -0,0 +1,504 @@
+# frozen_string_literal: true
+
+module Primer
+ module Beta
+ # `PageLayout` provides foundational patterns for responsive pages. `PageLayout` can be used for simple two-column pages, or it can be nested to provide flexible 3-column experiences.
+ #
+ # On smaller screens, `PageLayout` uses vertically stacked rows to display content. `PageLayout` is responsible for determining the arrangement of the main regions that compose a page.
+ # This means anything after the global and local headers (i.e. repo or org headers), and anything before the global footer.
+ #
+ # `PageLayout` controls the page spacings, supports header and footer regions, provides different styles of panes, and handles responsive strategies.
+ #
+ # `PageLayout` flows as both column, when there's enough horizontal space to render both `Content` and `Pane` side-by-side (on a desktop of tablet device, per instance);
+ # or it flows as a row, when `Content` and `Pane` are stacked vertically (e.g. on a mobile device).
+ # `PageLayout` should always work in any screen size.
+ #
+ # @accessibility
+ # Keyboard navigation follows the markup order. Decide carefully how the focus order should be be by deciding whether
+ # `content_region` or `pane_region` comes first in code. This is determined by the `position` argrument to the `pane_region` slot.
+ class PageLayout < Primer::Component
+ status :beta
+
+ WRAPPER_SIZING_DEFAULT = :fluid
+ WRAPPER_SIZING_MAPPINGS = {
+ WRAPPER_SIZING_DEFAULT => "",
+ :md => "container-md",
+ :lg => "container-lg",
+ :xl => "container-xl"
+ }.freeze
+ WRAPPER_SIZING_OPTIONS = WRAPPER_SIZING_MAPPINGS.keys.freeze
+
+ OUTER_SPACING_DEFAULT = :normal
+ OUTER_SPACING_MAPPINGS = {
+ OUTER_SPACING_DEFAULT => "PageLayout--outerSpacing-normal",
+ :condensed => "PageLayout--outerSpacing-condensed"
+ }.freeze
+ OUTER_SPACING_OPTIONS = OUTER_SPACING_MAPPINGS.keys.freeze
+
+ COLUMN_GAP_DEFAULT = :normal
+ COLUMN_GAP_MAPPINGS = {
+ COLUMN_GAP_DEFAULT => "PageLayout--columnGap-normal",
+ :condensed => "PageLayout--columnGap-condensed"
+ }.freeze
+ COLUMN_GAP_OPTIONS = COLUMN_GAP_MAPPINGS.keys.freeze
+
+ ROW_GAP_DEFAULT = :normal
+ ROW_GAP_MAPPINGS = {
+ ROW_GAP_DEFAULT => "PageLayout--rowGap-normal",
+ :condensed => "PageLayout--rowGap-condensed"
+ }.freeze
+ ROW_GAP_OPTIONS = ROW_GAP_MAPPINGS.keys.freeze
+
+ RESPONSIVE_VARIANT_DEFAULT = :stack_regions
+ RESPONSIVE_VARIANT_MAPPINGS = {
+ RESPONSIVE_VARIANT_DEFAULT => "PageLayout--responsive-stackRegions",
+ :separate_regions => "PageLayout--responsive-separateRegions"
+ }.freeze
+ RESPONSIVE_VARIANT_OPTIONS = RESPONSIVE_VARIANT_MAPPINGS.keys.freeze
+
+ PRIMARY_REGION_DEFAULT = :content
+ PRIMARY_REGION_MAPPINGS = {
+ PRIMARY_REGION_DEFAULT => "PageLayout--responsive-primary-content",
+ :pane => "PageLayout--responsive-primary-pane"
+ }.freeze
+ PRIMARY_REGION_OPTIONS = PRIMARY_REGION_MAPPINGS.keys.freeze
+
+ HEADER_DIVIDER_NARROW_DEFAULT = :inherit
+ HEADER_DIVIDER_NARROW_MAPPINGS = {
+ HEADER_DIVIDER_NARROW_DEFAULT => "PageLayout-region--dividerNarrow-line-after",
+ :none => "",
+ :line => "PageLayout-region--dividerNarrow-line-after",
+ :filled => "PageLayout-region--dividerNarrow-filled-after"
+ }.freeze
+ HEADER_DIVIDER_NARROW_OPTIONS = HEADER_DIVIDER_NARROW_MAPPINGS.keys.freeze
+
+ FOOTER_DIVIDER_NARROW_DEFAULT = :inherit
+ FOOTER_DIVIDER_NARROW_MAPPINGS = {
+ FOOTER_DIVIDER_NARROW_DEFAULT => "PageLayout-region--dividerNarrow-line-before",
+ :none => "",
+ :line => "PageLayout-region--dividerNarrow-line-before",
+ :filled => "PageLayout-region--dividerNarrow-filled-before"
+ }.freeze
+ FOOTER_DIVIDER_NARROW_OPTIONS = FOOTER_DIVIDER_NARROW_MAPPINGS.keys.freeze
+
+ # The layout's content.
+ #
+ # @param width [Symbol] <%= one_of(Primer::Beta::PageLayout::Content::WIDTH_OPTIONS) %>
+ # @param tag [Symbol] <%= one_of(Primer::Beta::PageLayout::Content::TAG_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ renders_one :content_region, "Primer::Beta::PageLayout::Content"
+
+ # The layout's header.
+ #
+ # @param divider [Boolean] Whether to show a header divider
+ # @param divider_narrow [Symbol] Whether to show a divider below the `header` region if in responsive mode. <%= one_of(Primer::Beta::PageLayout::HEADER_DIVIDER_NARROW_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ renders_one :header_region, lambda { |divider: false, divider_narrow: :line, **header_system_arguments|
+ header_system_arguments[:classes] = class_names(
+ header_system_arguments[:classes],
+ { "PageLayout-header--hasDivider" => divider },
+ { HEADER_DIVIDER_NARROW_MAPPINGS[fetch_or_fallback(HEADER_DIVIDER_NARROW_OPTIONS, divider_narrow, HEADER_DIVIDER_NARROW_DEFAULT)] => divider },
+ "PageLayout-header",
+ "PageLayout-region"
+ )
+
+ Primer::BaseComponent.new(tag: :div, **header_system_arguments)
+ }
+
+ # The layout's footer.
+ #
+ # @param divider [Boolean] Whether to show a footer divider
+ # @param divider_narrow [Symbol] Whether to show a divider below the `footer` region if in responsive mode. <%= one_of(Primer::Beta::PageLayout::FOOTER_DIVIDER_NARROW_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ renders_one :footer_region, lambda { |divider: false, divider_narrow: FOOTER_DIVIDER_NARROW_DEFAULT, **footer_system_arguments|
+ # These classes have to be set in the parent `Layout` element, so we modify its system arguments.
+
+ footer_system_arguments[:classes] = class_names(
+ footer_system_arguments[:classes],
+ { "PageLayout-footer--hasDivider" => divider },
+ { FOOTER_DIVIDER_NARROW_MAPPINGS[fetch_or_fallback(FOOTER_DIVIDER_NARROW_OPTIONS, divider_narrow, FOOTER_DIVIDER_NARROW_DEFAULT)] => divider },
+ "PageLayout-footer",
+ "PageLayout-region"
+ )
+
+ Primer::BaseComponent.new(tag: :div, **footer_system_arguments)
+ }
+
+ # The layout's pane.
+ #
+ # @param width [Symbol] <%= one_of(Primer::Beta::PageLayout::Pane::WIDTH_OPTIONS) %>
+ # @param position [Symbol] Pane placement when `Layout` is in column modes. <%= one_of(Primer::Beta::PageLayout::Pane::POSITION_OPTIONS) %>
+ # @param position_narrow [Symbol] Pane placement when `Layout` is in column modes. <%= one_of(Primer::Beta::PageLayout::Pane::POSITION_NARROW_OPTIONS) %>
+ # @param divider [Boolean] Whether to show a pane line divider.
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ renders_one :pane_region, lambda { |
+ width: Pane::WIDTH_DEFAULT,
+ position: Pane::POSITION_DEFAULT,
+ position_narrow: Pane::POSITION_NARROW_DEFAULT,
+ divider: false,
+ **pane_system_arguments
+ |
+ position_narrow = position if position_narrow == Pane::POSITION_NARROW_DEFAULT
+ # These classes have to be set in the parent `Layout` element, so we modify its system arguments.
+ @system_arguments[:classes] = class_names(
+ @system_arguments[:classes],
+ Pane::POSITION_MAPPINGS[fetch_or_fallback(Pane::POSITION_OPTIONS, position, Pane::POSITION_DEFAULT)],
+ Pane::WIDTH_MAPPINGS[fetch_or_fallback(Pane::WIDTH_OPTIONS, width, Pane::WIDTH_DEFAULT)],
+ { Pane::POSITION_NARROW_MAPPINGS[fetch_or_fallback(Pane::POSITION_NARROW_OPTIONS, position_narrow, Pane::POSITION_NARROW_DEFAULT)] => @responsive_variant == :stack_regions },
+ { "PageLayout--hasPaneDivider" => divider }
+ )
+
+ Pane.new(divider: divider, position: position, **pane_system_arguments)
+ }
+
+ # @example Default
+ #
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Header and footer
+ #
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.header_region(border: true) { "Header" } %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% c.footer_region(border: true) { "Footer" } %>
+ # <% end %>
+ #
+ # @example Wrapper sizing
+ #
+ # @description
+ # When `:fluid` the layout will be set to full width. When the other sizing options are used the layout will be centered with corresponding widths.
+ #
+ # - `:fluid`: full width
+ # - `:md`: max-width: 768px
+ # - `:lg`: max-width: 1012px
+ # - `:xl`: max-width: 1280px
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :fluid)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :md)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :lg)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :xl)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Outer spacing
+ #
+ # @description
+ # Sets wrapper margins surrounding the component to distance itself from the viewport edges.
+ #
+ # - `:condensed` keeps the margin at 16px.
+ # - `:normal`` sets the margin to 16px, and to 24px on lg breakpoints and above.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(outer_spacing: :condensed)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(outer_spacing: :normal)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Column gap
+ #
+ # @description
+ # Sets the gap between columns to distance them from each other.
+ #
+ # - `:condensed` keeps the gap always at 16px.
+ # - `:normal` sets the gap to 16px, and to 24px on lg breakpoints and above.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(column_gap: :condensed)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(column_gap: :normal)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Row gap
+ #
+ # @description
+ # Sets the gap below the header and above the footer.
+ #
+ # - `:condensed` keeps the gap always at 16px.
+ # - `:normal` sets the gap to 16px, and to 24px on lg breakpoints and above.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(row_gap: :condensed)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(row_gap: :normal)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Pane widths
+ #
+ # @description
+ # Sets the pane width. The width is predetermined according to the breakpoint instead of it being percentage-based.
+ #
+ # - `default`:
+ # - `narrow`:
+ # - `wide`:
+ #
+ # When flowing as a row, `Pane` takes the full width.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(width: :default, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(width: :narrow, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(width: :wide, border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Pane position
+ #
+ # @description
+ # Use `start` for panes that manipulate local navigation, while right-aligned `end` is useful for metadata and other auxiliary information.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(position: :start, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new( mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(position: :end, border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Pane resposive position
+ #
+ # @description
+ # Defines the position of the pane in the responsive layout.
+ #
+ # - `:start` puts the pane above content
+ # - `:end` puts it below content.
+ # - `:inherit` uses the same value from `position`
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(position_narrow: :inherit, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(position_narrow: :start, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(position_narrow: :end, border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Header
+ #
+ # @description
+ # You can add an optional header to the layout and have spacing and positioning taken care of for you.
+ # You can optionally add a divider to the header.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.header_region(border: true) { "Header" } %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.header_region(divider: true, border: true) { "Header" } %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Footer
+ #
+ # @description
+ # You can add an optional footer to the layout and have spacing and positioning taken care of for you.
+ # You can optionally add a divider to the footer.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% c.footer_region(border: true) { "Header" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% c.footer_region(divider: true, border: true) { "Header" } %>
+ # <% end %>
+ #
+ # @param wrapper_sizing [Symbol] Define the maximum width of the component. `:fluid` sets it to full-width. Other values center Layout horizontally. <%= one_of(Primer::Beta::PageLayout::WRAPPER_SIZING_OPTIONS) %>
+ # @param outer_spacing [Symbol] Sets wrapper margins surrounding the component to distance itself from the viewport edges. <%= one_of(Primer::Beta::PageLayout::OUTER_SPACING_OPTIONS) %>
+ # @param column_gap [Symbol] Sets gap between columns. <%= one_of(Primer::Beta::PageLayout::COLUMN_GAP_OPTIONS) %>
+ # @param row_gap [Symbol] Sets the gap below the header and above the footer. <%= one_of(Primer::Beta::PageLayout::ROW_GAP_OPTIONS) %>
+ # @param responsive_variant [Symbol] Defines how the layout component adapts to smaller viewports. `:stack_regions` presents the content in a vertical flow, with pane and content vertically arranged. `:separate_regions` presents pane and content as different pages on smaller viewports.
+ # @param primary_region [Symbol] When `responsive_variant` is set to `:separate_regions`, defines which region appears first on small viewports. `:content` is default.
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ def initialize(
+ wrapper_sizing: WRAPPER_SIZING_DEFAULT,
+ outer_spacing: OUTER_SPACING_DEFAULT,
+ column_gap: COLUMN_GAP_DEFAULT,
+ row_gap: ROW_GAP_DEFAULT,
+ responsive_variant: RESPONSIVE_VARIANT_DEFAULT,
+ primary_region: PRIMARY_REGION_DEFAULT,
+ **system_arguments
+ )
+
+ @wrapper_sizing_class = WRAPPER_SIZING_MAPPINGS[fetch_or_fallback(WRAPPER_SIZING_OPTIONS, wrapper_sizing, WRAPPER_SIZING_DEFAULT)]
+ @responsive_variant = responsive_variant
+ @system_arguments = system_arguments
+ @system_arguments[:tag] = :div
+ @system_arguments[:classes] = class_names(
+ "PageLayout",
+ OUTER_SPACING_MAPPINGS[fetch_or_fallback(OUTER_SPACING_OPTIONS, outer_spacing, OUTER_SPACING_DEFAULT)],
+ COLUMN_GAP_MAPPINGS[fetch_or_fallback(COLUMN_GAP_OPTIONS, column_gap, COLUMN_GAP_DEFAULT)],
+ ROW_GAP_MAPPINGS[fetch_or_fallback(ROW_GAP_OPTIONS, row_gap, ROW_GAP_DEFAULT)],
+ RESPONSIVE_VARIANT_MAPPINGS[fetch_or_fallback(RESPONSIVE_VARIANT_OPTIONS, @responsive_variant, RESPONSIVE_VARIANT_DEFAULT)],
+ { PRIMARY_REGION_MAPPINGS[fetch_or_fallback(PRIMARY_REGION_OPTIONS, primary_region, PRIMARY_REGION_DEFAULT)] => @responsive_variant == :separate_regions },
+ system_arguments[:classes]
+ )
+ end
+
+ def render?
+ content_region.present? && pane_region.present?
+ end
+
+ # The layout's content.
+ class Content < Primer::Component
+ status :beta
+
+ WIDTH_DEFAULT = :fluid
+ WIDTH_OPTIONS = [WIDTH_DEFAULT, :md, :lg, :xl].freeze
+
+ TAG_DEFAULT = :div
+ TAG_OPTIONS = [TAG_DEFAULT, :main].freeze
+
+ # @param width [Symbol] <%= one_of(Primer::Beta::PageLayout::Content::WIDTH_OPTIONS) %>
+ # @param tag [Symbol] <%= one_of(Primer::Beta::PageLayout::Content::TAG_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ def initialize(tag: TAG_DEFAULT, width: WIDTH_DEFAULT, **system_arguments)
+ @width = fetch_or_fallback(WIDTH_OPTIONS, width, WIDTH_DEFAULT)
+
+ @system_arguments = system_arguments
+ @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, TAG_DEFAULT)
+ @system_arguments[:classes] = class_names(
+ "PageLayout-region",
+ "PageLayout-content",
+ system_arguments[:classes]
+ )
+ end
+
+ def call
+ render(Primer::BaseComponent.new(**@system_arguments)) do
+ if @width == :fluid
+ content
+ else
+ render(Primer::BaseComponent.new(tag: :div, classes: "PageLayout-content-centered-#{@width}")) do
+ render(Primer::BaseComponent.new(tag: :div, container: @width)) do
+ content
+ end
+ end
+ end
+ end
+ end
+ end
+
+ # The layout's pane content. This is a secondary, smaller region that is paired with the `Content` region.
+ class Pane < Primer::Component
+ status :beta
+
+ POSITION_DEFAULT = :start
+ POSITION_MAPPINGS = {
+ POSITION_DEFAULT => "PageLayout--panePos-start",
+ :end => "PageLayout--panePos-end"
+ }.freeze
+ POSITION_OPTIONS = POSITION_MAPPINGS.keys.freeze
+
+ WIDTH_DEFAULT = :default
+ WIDTH_MAPPINGS = {
+ WIDTH_DEFAULT => "",
+ :narrow => "PageLayout--paneWidth-narrow",
+ :wide => "PageLayout--paneWidth-wide"
+ }.freeze
+ WIDTH_OPTIONS = WIDTH_MAPPINGS.keys.freeze
+
+ POSITION_NARROW_DEFAULT = :inherit
+ POSITION_NARROW_MAPPINGS = {
+ POSITION_NARROW_DEFAULT => "",
+ :start => "PageLayout--responsive-panePos-start",
+ :end => "PageLayout--responsive-panePos-end"
+ }.freeze
+ POSITION_NARROW_OPTIONS = POSITION_NARROW_MAPPINGS.keys.freeze
+
+ DIVIDER_DEFAULT = :start
+ DIVIDER_MAPPINGS = {
+ DIVIDER_DEFAULT => "PageLayout--panePos-start",
+ :end => "PageLayout--panePos-start"
+ }.freeze
+ DIVIDER_OPTIONS = DIVIDER_MAPPINGS.keys.freeze
+
+ DIVIDER_NARROW_DEFAULT = :inherit
+ DIVIDER_NARROW_MAPPINGS = {
+ { end: DIVIDER_NARROW_DEFAULT } => "PageLayout-region--dividerNarrow-line-before",
+ { end: :none } => "PageLayout-region--dividerNarrow-none-before",
+ { end: :line } => "PageLayout-region--dividerNarrow-line-before",
+ { end: :filled } => "PageLayout-region--dividerNarrow-filled-before",
+ { start: DIVIDER_NARROW_DEFAULT } => "PageLayout-region--dividerNarrow-line-after",
+ { start: :none } => "PageLayout-region--dividerNarrow-none-after",
+ { start: :line } => "PageLayout-region--dividerNarrow-line-after",
+ { start: :filled } => "PageLayout-region--dividerNarrow-filled-after"
+ }.freeze
+ DIVIDER_NARROW_USER_OPTIONS = [DIVIDER_NARROW_DEFAULT, :none, :line, :filled].freeze
+
+ TAG_DEFAULT = :div
+ TAG_OPTIONS = [TAG_DEFAULT, :aside, :nav, :section].freeze
+
+ # @param divider [Boolean]
+ # @param divider_narrow [Symbol] <%= one_of(Primer::Beta::PageLayout::Pane::DIVIDER_NARROW_OPTIONS) %>
+ # @param position [Symbol] <%= one_of(Primer::Beta::PageLayout::Pane::POSITION_OPTIONS) %>
+ # @param tag [Symbol] <%= one_of(Primer::Beta::PageLayout::Pane::TAG_OPTIONS) %>
+ def initialize(divider: false, divider_narrow: DIVIDER_NARROW_DEFAULT, position: POSITION_DEFAULT, tag: TAG_DEFAULT, **system_arguments)
+ @system_arguments = system_arguments
+ @position = position
+
+ @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, TAG_DEFAULT)
+ @system_arguments[:classes] = class_names(
+ "PageLayout-region",
+ "PageLayout-pane",
+ { DIVIDER_NARROW_MAPPINGS[{ position => fetch_or_fallback(DIVIDER_NARROW_USER_OPTIONS, divider_narrow, DIVIDER_NARROW_DEFAULT) }] => divider },
+ @system_arguments[:classes]
+ )
+ end
+
+ def render_first?
+ @position == :start
+ end
+
+ def call
+ render(Primer::BaseComponent.new(**@system_arguments)) { content }
+ end
+ end
+ end
+ end
+end
diff --git a/app/components/primer/beta/split_page_layout.html.erb b/app/components/primer/beta/split_page_layout.html.erb
new file mode 100644
index 0000000000..e6e39137b9
--- /dev/null
+++ b/app/components/primer/beta/split_page_layout.html.erb
@@ -0,0 +1,8 @@
+<%= render Primer::BaseComponent.new(**@system_arguments) do %>
+ <%= render(Primer::BaseComponent.new(tag: :div, classes: "PageLayout-wrapper")) do %>
+ <%= render(Primer::BaseComponent.new(tag: :div, classes: "PageLayout-columns")) do %>
+ <%= pane_region %>
+ <%= content_region %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/components/primer/beta/split_page_layout.rb b/app/components/primer/beta/split_page_layout.rb
new file mode 100644
index 0000000000..ec919174a6
--- /dev/null
+++ b/app/components/primer/beta/split_page_layout.rb
@@ -0,0 +1,210 @@
+# frozen_string_literal: true
+
+module Primer
+ module Beta
+ # In the `SplitPageLayout`, changes in the Pane region are reflected in the `Content` region. This is also known as a "List/Detail" or "Master/Detail" pattern.
+ #
+ # On larger screens, the user sees both regions side by side, with the `Pane` region appearing flushed to the left.
+ #
+ # On smaller screens, the user only sees one of `Pane` or `Content` regions at a time.
+ # Pages may decide if it's more important to show the `Pane` region or the `Content` region first by the `:primary_region` property.
+ #
+ # @accessibility
+ # Keyboard navigation follows the markup order. In the case of the `SplitPageLayout`, the `Pane` region is the first region, and the `Content` region is the second.
+ class SplitPageLayout < Primer::Component
+ status :beta
+
+ PANE_TAG_DEFAULT = :div
+ PANE_TAG_OPTIONS = [PANE_TAG_DEFAULT, :aside, :nav, :section].freeze
+
+ PANE_WIDTH_DEFAULT = :default
+ PANE_WIDTH_MAPPINGS = {
+ PANE_WIDTH_DEFAULT => "",
+ :narrow => "PageLayout--paneWidth-narrow",
+ :wide => "PageLayout--paneWidth-wide"
+ }.freeze
+ PANE_WIDTH_OPTIONS = PANE_WIDTH_MAPPINGS.keys.freeze
+
+ INNER_SPACING_DEFAULT = :normal
+ INNER_SPACING_MAPPINGS = {
+ normal: "PageLayout--innerSpacing-normal",
+ condensed: "PageLayout--innerSpacing-condensed"
+ }.freeze
+ INNER_SPACING_OPTIONS = INNER_SPACING_MAPPINGS.keys.freeze
+
+ PRIMARY_REGION_DEFAULT = :content
+ PRIMARY_REGION_MAPPINGS = {
+ PRIMARY_REGION_DEFAULT => "PageLayout--responsive-primary-content",
+ :pane => "PageLayout--responsive-primary-pane"
+ }.freeze
+ PRIMARY_REGION_OPTIONS = PRIMARY_REGION_MAPPINGS.keys.freeze
+
+ # The layout's content.
+ #
+ # @param width [Symbol] <%= one_of(Primer::Beta::SplitPageLayout::Content::WIDTH_OPTIONS) %>
+ # @param tag [Symbol] <%= one_of(Primer::Beta::SplitPageLayout::Content::TAG_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ renders_one :content_region, "Primer::Beta::SplitPageLayout::Content"
+
+ # The layout's pane.
+ #
+ # @param width [Symbol] <%= one_of(Primer::Beta::SplitPageLayout::PANE_WIDTH_OPTIONS) %>
+ # @param tag [Symbol] <%= one_of(Primer::Beta::SplitPageLayout::PANE_TAG_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ renders_one :pane_region, lambda { |width: PANE_WIDTH_DEFAULT, tag: PANE_TAG_DEFAULT, **system_arguments|
+ @pane_system_arguments = system_arguments
+ @pane_system_arguments[:tag] = fetch_or_fallback(PANE_TAG_OPTIONS, tag, PANE_TAG_DEFAULT)
+ @pane_system_arguments[:classes] = class_names(
+ @pane_system_arguments[:classes],
+ "PageLayout-region",
+ "PageLayout-pane"
+ )
+
+ # These classes have to be set in the parent element, so we modify its system arguments.
+ @system_arguments[:classes] = class_names(
+ @system_arguments[:classes],
+ PANE_WIDTH_MAPPINGS[fetch_or_fallback(PANE_WIDTH_OPTIONS, width, PANE_WIDTH_DEFAULT)]
+ )
+
+ Primer::BaseComponent.new(**@pane_system_arguments)
+ }
+
+ # @example Default
+ #
+ # <%= render(Primer::Beta::SplitPageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Inner spacing
+ #
+ # @description
+ # Sets padding to regions individually.
+ #
+ # - `:condensed` keeps the margin at 16px.
+ # - `:normal` sets the margin to 16px, and to 24px on lg breakpoints and above.
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(inner_spacing: :condensed)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(inner_spacing: :normal)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Responsive primary region
+ #
+ # @description
+ # When `responsive_variant` is set to `:separate_regions`, defines which region appears first on small viewports. `:content` is default.
+ #
+ # - `:content`
+ # - `:pane`
+ #
+ # @code
+ # <%= render(Primer::Beta::PageLayout.new(resposive_primary_region: :content)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::PageLayout.new(primary_region: :pane)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(border: true) { "Pane" } %>
+ # <% end %>
+ #
+ # @example Pane widths
+ #
+ # @description
+ # Sets the pane width. The width is predetermined according to the breakpoint instead of it being percentage-based.
+ #
+ # - `default`:
+ # - `narrow`:
+ # - `wide`:
+ #
+ # When flowing as a row, `Pane` takes the full width.
+ #
+ # @code
+ # <%= render(Primer::Beta::SplitPageLayout.new) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(width: :default, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::SplitPageLayout.new(mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(width: :narrow, border: true) { "Pane" } %>
+ # <% end %>
+ # <%= render(Primer::Beta::SplitPageLayout.new(mt: 5)) do |c| %>
+ # <% c.content_region(border: true) { "Content" } %>
+ # <% c.pane_region(width: :wide, border: true) { "Pane" } %>
+ # <% end %>
+ #
+ #
+ # @param inner_spacing [Symbol] Sets padding to regions individually. <%= one_of(Primer::Beta::SplitPageLayout::INNER_SPACING_OPTIONS) %>
+ # @param primary_region [Symbol] When `responsive_variant` is set to `:separate_regions`, defines which region appears first on small viewports. `:content` is default. <%= one_of(Primer::Beta::SplitPageLayout::PRIMARY_REGION_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ def initialize(
+ inner_spacing: INNER_SPACING_DEFAULT,
+ primary_region: PRIMARY_REGION_DEFAULT,
+ **system_arguments
+ )
+
+ @system_arguments = system_arguments
+ @system_arguments[:tag] = :div
+ @system_arguments[:classes] = class_names(
+ "PageLayout",
+ INNER_SPACING_MAPPINGS[fetch_or_fallback(INNER_SPACING_OPTIONS, inner_spacing, INNER_SPACING_DEFAULT)],
+ PRIMARY_REGION_MAPPINGS[fetch_or_fallback(PRIMARY_REGION_OPTIONS, primary_region, PRIMARY_REGION_DEFAULT)],
+ "PageLayout--responsive-separateRegions",
+ "PageLayout--columnGap-none",
+ "PageLayout--rowGap-none",
+ "PageLayout--panePos-start",
+ "PageLayout--hasPaneDivider",
+ system_arguments[:classes]
+ )
+ end
+
+ def render?
+ content_region.present? && pane_region.present?
+ end
+
+ # The layout's content.
+ class Content < Primer::Component
+ status :beta
+
+ WIDTH_DEFAULT = :fluid
+ WIDTH_OPTIONS = [WIDTH_DEFAULT, :md, :lg, :xl].freeze
+
+ TAG_DEFAULT = :div
+ TAG_OPTIONS = [TAG_DEFAULT, :main].freeze
+
+ # @param width [Symbol] <%= one_of(Primer::Beta::SplitPageLayout::Content::WIDTH_OPTIONS) %>
+ # @param tag [Symbol] <%= one_of(Primer::Beta::SplitPageLayout::Content::TAG_OPTIONS) %>
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
+ def initialize(tag: TAG_DEFAULT, width: WIDTH_DEFAULT, **system_arguments)
+ @width = fetch_or_fallback(WIDTH_OPTIONS, width, WIDTH_DEFAULT)
+
+ @system_arguments = system_arguments
+ @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, TAG_DEFAULT)
+ @system_arguments[:classes] = class_names(
+ "PageLayout-region",
+ "PageLayout-content",
+ system_arguments[:classes]
+ )
+ end
+
+ def call
+ render(Primer::BaseComponent.new(**@system_arguments)) do
+ if @width == :fluid
+ content
+ else
+ render(Primer::BaseComponent.new(tag: :div, classes: "PageLayout-content-centered-#{@width}")) do
+ render(Primer::BaseComponent.new(tag: :div, container: @width)) do
+ content
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/docs/content/components/beta/pagelayout.md b/docs/content/components/beta/pagelayout.md
new file mode 100644
index 0000000000..75229edff3
--- /dev/null
+++ b/docs/content/components/beta/pagelayout.md
@@ -0,0 +1,284 @@
+---
+title: PageLayout
+componentId: page_layout
+status: Beta
+source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/page_layout.rb
+storybook: https://primer.style/view-components/stories/?path=/story/primer-beta-page-layout
+---
+
+import Example from '../../../src/@primer/gatsby-theme-doctocat/components/example'
+
+
+
+`PageLayout` provides foundational patterns for responsive pages. `PageLayout` can be used for simple two-column pages, or it can be nested to provide flexible 3-column experiences.
+
+ On smaller screens, `PageLayout` uses vertically stacked rows to display content. `PageLayout` is responsible for determining the arrangement of the main regions that compose a page.
+This means anything after the global and local headers (i.e. repo or org headers), and anything before the global footer.
+
+ `PageLayout` controls the page spacings, supports header and footer regions, provides different styles of panes, and handles responsive strategies.
+
+`PageLayout` flows as both column, when there's enough horizontal space to render both `Content` and `Pane` side-by-side (on a desktop of tablet device, per instance);
+or it flows as a row, when `Content` and `Pane` are stacked vertically (e.g. on a mobile device).
+`PageLayout` should always work in any screen size.
+
+## Accessibility
+
+Keyboard navigation follows the markup order. Decide carefully how the focus order should be be by deciding whether
+`content_region` or `pane_region` comes first in code. This is determined by the `position` argrument to the `pane_region` slot.
+
+## Arguments
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `wrapper_sizing` | `Symbol` | `:fluid` | Define the maximum width of the component. `:fluid` sets it to full-width. Other values center Layout horizontally. One of `:fluid`, `:lg`, `:md`, or `:xl`. |
+| `outer_spacing` | `Symbol` | `:normal` | Sets wrapper margins surrounding the component to distance itself from the viewport edges. One of `:condensed` and `:normal`. |
+| `column_gap` | `Symbol` | `:normal` | Sets gap between columns. One of `:condensed` and `:normal`. |
+| `row_gap` | `Symbol` | `:normal` | Sets the gap below the header and above the footer. One of `:condensed` and `:normal`. |
+| `responsive_variant` | `Symbol` | `:stack_regions` | Defines how the layout component adapts to smaller viewports. `:stack_regions` presents the content in a vertical flow, with pane and content vertically arranged. `:separate_regions` presents pane and content as different pages on smaller viewports. |
+| `primary_region` | `Symbol` | `:content` | When `responsive_variant` is set to `:separate_regions`, defines which region appears first on small viewports. `:content` is default. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+## Slots
+
+### `Content_region`
+
+The layout's content.
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `width` | `Symbol` | N/A | One of `:fluid`, `:lg`, `:md`, or `:xl`. |
+| `tag` | `Symbol` | N/A | One of `:div` and `:main`. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+### `Header_region`
+
+The layout's header.
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `divider` | `Boolean` | N/A | Whether to show a header divider |
+| `divider_narrow` | `Symbol` | N/A | Whether to show a divider below the `header` region if in responsive mode. One of `:filled`, `:inherit`, `:line`, or `:none`. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+### `Footer_region`
+
+The layout's footer.
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `divider` | `Boolean` | N/A | Whether to show a footer divider |
+| `divider_narrow` | `Symbol` | N/A | Whether to show a divider below the `footer` region if in responsive mode. One of `:filled`, `:inherit`, `:line`, or `:none`. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+### `Pane_region`
+
+The layout's pane.
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `width` | `Symbol` | N/A | One of `:default`, `:narrow`, or `:wide`. |
+| `position` | `Symbol` | N/A | Pane placement when `Layout` is in column modes. One of `:end` and `:start`. |
+| `position_narrow` | `Symbol` | N/A | Pane placement when `Layout` is in column modes. One of `:end`, `:inherit`, or `:start`. |
+| `divider` | `Boolean` | N/A | Whether to show a pane line divider. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+## Examples
+
+### Default
+
+
+
+```erb
+
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Header and footer
+
+
+
+```erb
+
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.header_region(border: true) { "Header" } %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+ <% c.footer_region(border: true) { "Footer" } %>
+<% end %>
+```
+
+### Wrapper sizing
+
+When `:fluid` the layout will be set to full width. When the other sizing options are used the layout will be centered with corresponding widths. - `:fluid`: full width - `:md`: max-width: 768px - `:lg`: max-width: 1012px - `:xl`: max-width: 1280px
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :fluid)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :md)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :lg)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(wrapper_sizing: :xl)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Outer spacing
+
+Sets wrapper margins surrounding the component to distance itself from the viewport edges. - `:condensed` keeps the margin at 16px. - `:normal`` sets the margin to 16px, and to 24px on lg breakpoints and above.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(outer_spacing: :condensed)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(outer_spacing: :normal)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Column gap
+
+Sets the gap between columns to distance them from each other. - `:condensed` keeps the gap always at 16px. - `:normal` sets the gap to 16px, and to 24px on lg breakpoints and above.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(column_gap: :condensed)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(column_gap: :normal)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Row gap
+
+Sets the gap below the header and above the footer. - `:condensed` keeps the gap always at 16px. - `:normal` sets the gap to 16px, and to 24px on lg breakpoints and above.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(row_gap: :condensed)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(row_gap: :normal)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Pane widths
+
+Sets the pane width. The width is predetermined according to the breakpoint instead of it being percentage-based. - `default`: - `narrow`: - `wide`: When flowing as a row, `Pane` takes the full width.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(width: :default, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(width: :narrow, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(width: :wide, border: true) { "Pane" } %>
+<% end %>
+```
+
+### Pane position
+
+Use `start` for panes that manipulate local navigation, while right-aligned `end` is useful for metadata and other auxiliary information.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(position: :start, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new( mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(position: :end, border: true) { "Pane" } %>
+<% end %>
+```
+
+### Pane resposive position
+
+Defines the position of the pane in the responsive layout. - `:start` puts the pane above content - `:end` puts it below content. - `:inherit` uses the same value from `position`
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(position_narrow: :inherit, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(position_narrow: :start, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(position_narrow: :end, border: true) { "Pane" } %>
+<% end %>
+```
+
+### Header
+
+You can add an optional header to the layout and have spacing and positioning taken care of for you. You can optionally add a divider to the header.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.header_region(border: true) { "Header" } %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.header_region(divider: true, border: true) { "Header" } %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Footer
+
+You can add an optional footer to the layout and have spacing and positioning taken care of for you. You can optionally add a divider to the footer.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+ <% c.footer_region(border: true) { "Header" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+ <% c.footer_region(divider: true, border: true) { "Header" } %>
+<% end %>
+```
diff --git a/docs/content/components/beta/splitpagelayout.md b/docs/content/components/beta/splitpagelayout.md
new file mode 100644
index 0000000000..5e0fc8d0fc
--- /dev/null
+++ b/docs/content/components/beta/splitpagelayout.md
@@ -0,0 +1,121 @@
+---
+title: SplitPageLayout
+componentId: split_page_layout
+status: Beta
+source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/split_page_layout.rb
+storybook: https://primer.style/view-components/stories/?path=/story/primer-beta-split-page-layout
+---
+
+import Example from '../../../src/@primer/gatsby-theme-doctocat/components/example'
+
+
+
+In the `SplitPageLayout`, changes in the Pane region are reflected in the `Content` region. This is also known as a "List/Detail" or "Master/Detail" pattern.
+
+On larger screens, the user sees both regions side by side, with the `Pane` region appearing flushed to the left.
+
+On smaller screens, the user only sees one of `Pane` or `Content` regions at a time.
+Pages may decide if it's more important to show the `Pane` region or the `Content` region first by the `:primary_region` property.
+
+## Accessibility
+
+Keyboard navigation follows the markup order. In the case of the `SplitPageLayout`, the `Pane` region is the first region, and the `Content` region is the second.
+
+## Arguments
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `inner_spacing` | `Symbol` | `:normal` | Sets padding to regions individually. One of `:condensed` and `:normal`. |
+| `primary_region` | `Symbol` | `:content` | When `responsive_variant` is set to `:separate_regions`, defines which region appears first on small viewports. `:content` is default. One of `:content` and `:pane`. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+## Slots
+
+### `Content_region`
+
+The layout's content.
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `width` | `Symbol` | N/A | One of `:fluid`, `:lg`, `:md`, or `:xl`. |
+| `tag` | `Symbol` | N/A | One of `:div` and `:main`. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+### `Pane_region`
+
+The layout's pane.
+
+| Name | Type | Default | Description |
+| :- | :- | :- | :- |
+| `width` | `Symbol` | N/A | One of `:default`, `:narrow`, or `:wide`. |
+| `tag` | `Symbol` | N/A | One of `:aside`, `:div`, `:nav`, or `:section`. |
+| `system_arguments` | `Hash` | N/A | [System arguments](/system-arguments) |
+
+## Examples
+
+### Default
+
+
+
+```erb
+
+<%= render(Primer::Beta::SplitPageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Inner spacing
+
+Sets padding to regions individually. - `:condensed` keeps the margin at 16px. - `:normal` sets the margin to 16px, and to 24px on lg breakpoints and above.
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(inner_spacing: :condensed)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(inner_spacing: :normal)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Responsive primary region
+
+When `responsive_variant` is set to `:separate_regions`, defines which region appears first on small viewports. `:content` is default. - `:content` - `:pane`
+
+
+
+```erb
+<%= render(Primer::Beta::PageLayout.new(resposive_primary_region: :content)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::PageLayout.new(primary_region: :pane)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(border: true) { "Pane" } %>
+<% end %>
+```
+
+### Pane widths
+
+Sets the pane width. The width is predetermined according to the breakpoint instead of it being percentage-based. - `default`: - `narrow`: - `wide`: When flowing as a row, `Pane` takes the full width.
+
+
+
+```erb
+<%= render(Primer::Beta::SplitPageLayout.new) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(width: :default, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::SplitPageLayout.new(mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(width: :narrow, border: true) { "Pane" } %>
+<% end %>
+<%= render(Primer::Beta::SplitPageLayout.new(mt: 5)) do |c| %>
+ <% c.content_region(border: true) { "Content" } %>
+ <% c.pane_region(width: :wide, border: true) { "Pane" } %>
+<% end %>
+```
diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
index a7e75db06c..bdee1dead2 100644
--- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml
+++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
@@ -81,12 +81,16 @@
url: "/components/octicon"
- title: OcticonSymbols
url: "/components/octiconsymbols"
+ - title: PageLayout
+ url: "/components/pagelayout"
- title: Popover
url: "/components/popover"
- title: ProgressBar
url: "/components/progressbar"
- title: Spinner
url: "/components/spinner"
+ - title: SplitPageLayout
+ url: "/components/splitpagelayout"
- title: State
url: "/components/state"
- title: Subhead
diff --git a/lib/tasks/docs.rake b/lib/tasks/docs.rake
index da068ff1a2..077c7c32ab 100644
--- a/lib/tasks/docs.rake
+++ b/lib/tasks/docs.rake
@@ -29,6 +29,8 @@ namespace :docs do
# Rails controller for rendering arbitrary ERB
view_context = ApplicationController.new.tap { |c| c.request = ActionDispatch::TestRequest.create }.view_context
components = [
+ Primer::Beta::PageLayout,
+ Primer::Beta::SplitPageLayout,
Primer::Alpha::Layout,
Primer::HellipButton,
Primer::Alpha::BorderBox::Header,
diff --git a/package.json b/package.json
index 7cc01984fb..60f48f9918 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
},
"devDependencies": {
"@github/prettier-config": "0.0.4",
- "@primer/css": "^19.0.0",
+ "@primer/css": "^19.2.0",
"@primer/primitives": "^7.1.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"@rollup/plugin-typescript": "^8.2.0",
diff --git a/static/arguments.yml b/static/arguments.yml
index 20b9c4284b..5928e21b06 100644
--- a/static/arguments.yml
+++ b/static/arguments.yml
@@ -279,6 +279,61 @@
type: Hash
default: N/A
description: "[System arguments](/system-arguments)"
+- component: PageLayout
+ source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/page_layout.rb
+ parameters:
+ - name: wrapper_sizing
+ type: Symbol
+ default: "`:fluid`"
+ description: Define the maximum width of the component. `:fluid` sets it to full-width.
+ Other values center Layout horizontally. One of `:fluid`, `:lg`, `:md`, or `:xl`.
+ - name: outer_spacing
+ type: Symbol
+ default: "`:normal`"
+ description: Sets wrapper margins surrounding the component to distance itself
+ from the viewport edges. One of `:condensed` and `:normal`.
+ - name: column_gap
+ type: Symbol
+ default: "`:normal`"
+ description: Sets gap between columns. One of `:condensed` and `:normal`.
+ - name: row_gap
+ type: Symbol
+ default: "`:normal`"
+ description: Sets the gap below the header and above the footer. One of `:condensed`
+ and `:normal`.
+ - name: responsive_variant
+ type: Symbol
+ default: "`:stack_regions`"
+ description: Defines how the layout component adapts to smaller viewports. `:stack_regions`
+ presents the content in a vertical flow, with pane and content vertically arranged.
+ `:separate_regions` presents pane and content as different pages on smaller
+ viewports.
+ - name: primary_region
+ type: Symbol
+ default: "`:content`"
+ description: When `responsive_variant` is set to `:separate_regions`, defines
+ which region appears first on small viewports. `:content` is default.
+ - name: system_arguments
+ type: Hash
+ default: N/A
+ description: "[System arguments](/system-arguments)"
+- component: SplitPageLayout
+ source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/split_page_layout.rb
+ parameters:
+ - name: inner_spacing
+ type: Symbol
+ default: "`:normal`"
+ description: Sets padding to regions individually. One of `:condensed` and `:normal`.
+ - name: primary_region
+ type: Symbol
+ default: "`:content`"
+ description: When `responsive_variant` is set to `:separate_regions`, defines
+ which region appears first on small viewports. `:content` is default. One of
+ `:content` and `:pane`.
+ - name: system_arguments
+ type: Hash
+ default: N/A
+ description: "[System arguments](/system-arguments)"
- component: Text
source: https://github.com/primer/view_components/tree/main/app/components/primer/beta/text.rb
parameters:
diff --git a/static/audited_at.json b/static/audited_at.json
index 360fa1bbf2..32facb2b55 100644
--- a/static/audited_at.json
+++ b/static/audited_at.json
@@ -18,6 +18,11 @@
"Primer::Beta::Blankslate": "",
"Primer::Beta::Breadcrumbs": "",
"Primer::Beta::Breadcrumbs::Item": "",
+ "Primer::Beta::PageLayout": "",
+ "Primer::Beta::PageLayout::Content": "",
+ "Primer::Beta::PageLayout::Pane": "",
+ "Primer::Beta::SplitPageLayout": "",
+ "Primer::Beta::SplitPageLayout::Content": "",
"Primer::Beta::Text": "",
"Primer::Beta::Truncate": "",
"Primer::Beta::Truncate::TruncateText": "",
diff --git a/static/classes.yml b/static/classes.yml
index 1a46c93a28..8646ac74b5 100644
--- a/static/classes.yml
+++ b/static/classes.yml
@@ -49,6 +49,37 @@
- ".Link--muted"
- ".Link--primary"
- ".Link--secondary"
+- ".PageLayout"
+- ".PageLayout--columnGap-condensed"
+- ".PageLayout--columnGap-none"
+- ".PageLayout--columnGap-normal"
+- ".PageLayout--hasPaneDivider"
+- ".PageLayout--innerSpacing-normal"
+- ".PageLayout--outerSpacing-condensed"
+- ".PageLayout--outerSpacing-normal"
+- ".PageLayout--panePos-end"
+- ".PageLayout--panePos-start"
+- ".PageLayout--paneWidth-narrow"
+- ".PageLayout--paneWidth-wide"
+- ".PageLayout--responsive-panePos-end"
+- ".PageLayout--responsive-panePos-start"
+- ".PageLayout--responsive-primary-content"
+- ".PageLayout--responsive-separateRegions"
+- ".PageLayout--responsive-stackRegions"
+- ".PageLayout--rowGap-condensed"
+- ".PageLayout--rowGap-none"
+- ".PageLayout--rowGap-normal"
+- ".PageLayout-columns"
+- ".PageLayout-content"
+- ".PageLayout-footer"
+- ".PageLayout-footer--hasDivider"
+- ".PageLayout-header"
+- ".PageLayout-header--hasDivider"
+- ".PageLayout-pane"
+- ".PageLayout-region"
+- ".PageLayout-region--dividerNarrow-line-after"
+- ".PageLayout-region--dividerNarrow-line-before"
+- ".PageLayout-wrapper"
- ".Popover"
- ".Popover-message"
- ".Popover-message--large"
diff --git a/static/constants.json b/static/constants.json
index 59af9dcfc7..907f73cbda 100644
--- a/static/constants.json
+++ b/static/constants.json
@@ -249,6 +249,227 @@
},
"Primer::Beta::Breadcrumbs::Item": {
},
+ "Primer::Beta::PageLayout": {
+ "COLUMN_GAP_DEFAULT": "normal",
+ "COLUMN_GAP_MAPPINGS": {
+ "normal": "PageLayout--columnGap-normal",
+ "condensed": "PageLayout--columnGap-condensed"
+ },
+ "COLUMN_GAP_OPTIONS": [
+ "normal",
+ "condensed"
+ ],
+ "Content": "Primer::Beta::PageLayout::Content",
+ "FOOTER_DIVIDER_NARROW_DEFAULT": "inherit",
+ "FOOTER_DIVIDER_NARROW_MAPPINGS": {
+ "inherit": "PageLayout-region--dividerNarrow-line-before",
+ "none": "",
+ "line": "PageLayout-region--dividerNarrow-line-before",
+ "filled": "PageLayout-region--dividerNarrow-filled-before"
+ },
+ "FOOTER_DIVIDER_NARROW_OPTIONS": [
+ "inherit",
+ "none",
+ "line",
+ "filled"
+ ],
+ "HEADER_DIVIDER_NARROW_DEFAULT": "inherit",
+ "HEADER_DIVIDER_NARROW_MAPPINGS": {
+ "inherit": "PageLayout-region--dividerNarrow-line-after",
+ "none": "",
+ "line": "PageLayout-region--dividerNarrow-line-after",
+ "filled": "PageLayout-region--dividerNarrow-filled-after"
+ },
+ "HEADER_DIVIDER_NARROW_OPTIONS": [
+ "inherit",
+ "none",
+ "line",
+ "filled"
+ ],
+ "OUTER_SPACING_DEFAULT": "normal",
+ "OUTER_SPACING_MAPPINGS": {
+ "normal": "PageLayout--outerSpacing-normal",
+ "condensed": "PageLayout--outerSpacing-condensed"
+ },
+ "OUTER_SPACING_OPTIONS": [
+ "normal",
+ "condensed"
+ ],
+ "PRIMARY_REGION_DEFAULT": "content",
+ "PRIMARY_REGION_MAPPINGS": {
+ "content": "PageLayout--responsive-primary-content",
+ "pane": "PageLayout--responsive-primary-pane"
+ },
+ "PRIMARY_REGION_OPTIONS": [
+ "content",
+ "pane"
+ ],
+ "Pane": "Primer::Beta::PageLayout::Pane",
+ "RESPONSIVE_VARIANT_DEFAULT": "stack_regions",
+ "RESPONSIVE_VARIANT_MAPPINGS": {
+ "stack_regions": "PageLayout--responsive-stackRegions",
+ "separate_regions": "PageLayout--responsive-separateRegions"
+ },
+ "RESPONSIVE_VARIANT_OPTIONS": [
+ "stack_regions",
+ "separate_regions"
+ ],
+ "ROW_GAP_DEFAULT": "normal",
+ "ROW_GAP_MAPPINGS": {
+ "normal": "PageLayout--rowGap-normal",
+ "condensed": "PageLayout--rowGap-condensed"
+ },
+ "ROW_GAP_OPTIONS": [
+ "normal",
+ "condensed"
+ ],
+ "WRAPPER_SIZING_DEFAULT": "fluid",
+ "WRAPPER_SIZING_MAPPINGS": {
+ "fluid": "",
+ "md": "container-md",
+ "lg": "container-lg",
+ "xl": "container-xl"
+ },
+ "WRAPPER_SIZING_OPTIONS": [
+ "fluid",
+ "md",
+ "lg",
+ "xl"
+ ]
+ },
+ "Primer::Beta::PageLayout::Content": {
+ "TAG_DEFAULT": "div",
+ "TAG_OPTIONS": [
+ "div",
+ "main"
+ ],
+ "WIDTH_DEFAULT": "fluid",
+ "WIDTH_OPTIONS": [
+ "fluid",
+ "md",
+ "lg",
+ "xl"
+ ]
+ },
+ "Primer::Beta::PageLayout::Pane": {
+ "DIVIDER_DEFAULT": "start",
+ "DIVIDER_MAPPINGS": {
+ "start": "PageLayout--panePos-start",
+ "end": "PageLayout--panePos-start"
+ },
+ "DIVIDER_NARROW_DEFAULT": "inherit",
+ "DIVIDER_NARROW_MAPPINGS": {
+ "{:end=>:inherit}": "PageLayout-region--dividerNarrow-line-before",
+ "{:end=>:none}": "PageLayout-region--dividerNarrow-none-before",
+ "{:end=>:line}": "PageLayout-region--dividerNarrow-line-before",
+ "{:end=>:filled}": "PageLayout-region--dividerNarrow-filled-before",
+ "{:start=>:inherit}": "PageLayout-region--dividerNarrow-line-after",
+ "{:start=>:none}": "PageLayout-region--dividerNarrow-none-after",
+ "{:start=>:line}": "PageLayout-region--dividerNarrow-line-after",
+ "{:start=>:filled}": "PageLayout-region--dividerNarrow-filled-after"
+ },
+ "DIVIDER_NARROW_USER_OPTIONS": [
+ "inherit",
+ "none",
+ "line",
+ "filled"
+ ],
+ "DIVIDER_OPTIONS": [
+ "start",
+ "end"
+ ],
+ "POSITION_DEFAULT": "start",
+ "POSITION_MAPPINGS": {
+ "start": "PageLayout--panePos-start",
+ "end": "PageLayout--panePos-end"
+ },
+ "POSITION_NARROW_DEFAULT": "inherit",
+ "POSITION_NARROW_MAPPINGS": {
+ "inherit": "",
+ "start": "PageLayout--responsive-panePos-start",
+ "end": "PageLayout--responsive-panePos-end"
+ },
+ "POSITION_NARROW_OPTIONS": [
+ "inherit",
+ "start",
+ "end"
+ ],
+ "POSITION_OPTIONS": [
+ "start",
+ "end"
+ ],
+ "TAG_DEFAULT": "div",
+ "TAG_OPTIONS": [
+ "div",
+ "aside",
+ "nav",
+ "section"
+ ],
+ "WIDTH_DEFAULT": "default",
+ "WIDTH_MAPPINGS": {
+ "default": "",
+ "narrow": "PageLayout--paneWidth-narrow",
+ "wide": "PageLayout--paneWidth-wide"
+ },
+ "WIDTH_OPTIONS": [
+ "default",
+ "narrow",
+ "wide"
+ ]
+ },
+ "Primer::Beta::SplitPageLayout": {
+ "Content": "Primer::Beta::SplitPageLayout::Content",
+ "INNER_SPACING_DEFAULT": "normal",
+ "INNER_SPACING_MAPPINGS": {
+ "normal": "PageLayout--innerSpacing-normal",
+ "condensed": "PageLayout--innerSpacing-condensed"
+ },
+ "INNER_SPACING_OPTIONS": [
+ "normal",
+ "condensed"
+ ],
+ "PANE_TAG_DEFAULT": "div",
+ "PANE_TAG_OPTIONS": [
+ "div",
+ "aside",
+ "nav",
+ "section"
+ ],
+ "PANE_WIDTH_DEFAULT": "default",
+ "PANE_WIDTH_MAPPINGS": {
+ "default": "",
+ "narrow": "PageLayout--paneWidth-narrow",
+ "wide": "PageLayout--paneWidth-wide"
+ },
+ "PANE_WIDTH_OPTIONS": [
+ "default",
+ "narrow",
+ "wide"
+ ],
+ "PRIMARY_REGION_DEFAULT": "content",
+ "PRIMARY_REGION_MAPPINGS": {
+ "content": "PageLayout--responsive-primary-content",
+ "pane": "PageLayout--responsive-primary-pane"
+ },
+ "PRIMARY_REGION_OPTIONS": [
+ "content",
+ "pane"
+ ]
+ },
+ "Primer::Beta::SplitPageLayout::Content": {
+ "TAG_DEFAULT": "div",
+ "TAG_OPTIONS": [
+ "div",
+ "main"
+ ],
+ "WIDTH_DEFAULT": "fluid",
+ "WIDTH_OPTIONS": [
+ "fluid",
+ "md",
+ "lg",
+ "xl"
+ ]
+ },
"Primer::Beta::Text": {
"DEFAULT_TAG": "span"
},
diff --git a/static/statuses.json b/static/statuses.json
index d170c9414a..0f4ce1359e 100644
--- a/static/statuses.json
+++ b/static/statuses.json
@@ -18,6 +18,11 @@
"Primer::Beta::Blankslate": "beta",
"Primer::Beta::Breadcrumbs": "beta",
"Primer::Beta::Breadcrumbs::Item": "alpha",
+ "Primer::Beta::PageLayout": "beta",
+ "Primer::Beta::PageLayout::Content": "beta",
+ "Primer::Beta::PageLayout::Pane": "beta",
+ "Primer::Beta::SplitPageLayout": "beta",
+ "Primer::Beta::SplitPageLayout::Content": "beta",
"Primer::Beta::Text": "beta",
"Primer::Beta::Truncate": "beta",
"Primer::Beta::Truncate::TruncateText": "alpha",
diff --git a/stories/primer/beta/page_layout_stories.rb b/stories/primer/beta/page_layout_stories.rb
new file mode 100644
index 0000000000..11d763b80c
--- /dev/null
+++ b/stories/primer/beta/page_layout_stories.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "primer/beta/page_layout"
+
+class Primer::Beta::PageLayoutStories < ViewComponent::Storybook::Stories
+ layout "storybook_preview"
+
+ story(:page_layout) do
+ controls do
+ select(:wrapper_sizing, Primer::Beta::PageLayout::WRAPPER_SIZING_OPTIONS, Primer::Beta::PageLayout::WRAPPER_SIZING_DEFAULT)
+ select(:outer_spacing, Primer::Beta::PageLayout::OUTER_SPACING_OPTIONS, Primer::Beta::PageLayout::OUTER_SPACING_DEFAULT)
+ select(:column_gap, Primer::Beta::PageLayout::COLUMN_GAP_OPTIONS, Primer::Beta::PageLayout::COLUMN_GAP_DEFAULT)
+ select(:row_gap, Primer::Beta::PageLayout::ROW_GAP_OPTIONS, Primer::Beta::PageLayout::ROW_GAP_DEFAULT)
+ select(:primary_region, Primer::Beta::PageLayout::PRIMARY_REGION_OPTIONS, Primer::Beta::PageLayout::PRIMARY_REGION_DEFAULT)
+ select(:responsive_variant, Primer::Beta::PageLayout::RESPONSIVE_VARIANT_OPTIONS, Primer::Beta::PageLayout::RESPONSIVE_VARIANT_DEFAULT)
+ end
+
+ content do |c|
+ c.content_region(border: true) do
+ "Main region"
+ end
+ c.pane_region(border: true) do
+ "Pane region"
+ end
+ end
+ end
+
+ story(:page_layout_with_header_and_footer) do
+ controls do
+ select(:wrapper_sizing, Primer::Beta::PageLayout::WRAPPER_SIZING_OPTIONS, Primer::Beta::PageLayout::WRAPPER_SIZING_DEFAULT)
+ select(:outer_spacing, Primer::Beta::PageLayout::OUTER_SPACING_OPTIONS, Primer::Beta::PageLayout::OUTER_SPACING_DEFAULT)
+ select(:column_gap, Primer::Beta::PageLayout::COLUMN_GAP_OPTIONS, Primer::Beta::PageLayout::COLUMN_GAP_DEFAULT)
+ select(:row_gap, Primer::Beta::PageLayout::ROW_GAP_OPTIONS, Primer::Beta::PageLayout::ROW_GAP_DEFAULT)
+ select(:primary_region, Primer::Beta::PageLayout::PRIMARY_REGION_OPTIONS, Primer::Beta::PageLayout::PRIMARY_REGION_DEFAULT)
+ select(:responsive_variant, Primer::Beta::PageLayout::RESPONSIVE_VARIANT_OPTIONS, Primer::Beta::PageLayout::RESPONSIVE_VARIANT_DEFAULT)
+ end
+
+ content do |c|
+ c.header_region(border: true) do
+ "Header region"
+ end
+ c.content_region(border: true) do
+ "Content region"
+ end
+ c.pane_region(border: true) do
+ "Pane region"
+ end
+ c.footer_region(border: true) do
+ "Footer region"
+ end
+ end
+ end
+end
diff --git a/stories/primer/beta/split_page_layout_stories.rb b/stories/primer/beta/split_page_layout_stories.rb
new file mode 100644
index 0000000000..335941da78
--- /dev/null
+++ b/stories/primer/beta/split_page_layout_stories.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "primer/beta/split_page_layout"
+
+class Primer::Beta::SplitPageLayoutStories < ViewComponent::Storybook::Stories
+ layout "storybook_preview"
+
+ story(:page_layout) do
+ controls do
+ select(:inner_spacing, Primer::Beta::SplitPageLayout::INNER_SPACING_OPTIONS, Primer::Beta::SplitPageLayout::INNER_SPACING_DEFAULT)
+ select(:primary_region, Primer::Beta::SplitPageLayout::PRIMARY_REGION_OPTIONS, Primer::Beta::SplitPageLayout::PRIMARY_REGION_DEFAULT)
+ end
+
+ content do |c|
+ c.content_region(border: true) do
+ "Content region"
+ end
+ c.pane_region(border: true) do
+ "Pane region"
+ end
+ end
+ end
+end
diff --git a/test/components/component_test.rb b/test/components/component_test.rb
index afa2359404..446ba53c5a 100644
--- a/test/components/component_test.rb
+++ b/test/components/component_test.rb
@@ -7,6 +7,14 @@ class PrimerComponentTest < Minitest::Test
# Components with any arguments necessary to make them render
COMPONENTS_WITH_ARGS = [
+ [Primer::Beta::PageLayout, {}, proc { |component|
+ component.content_region(tag: :div) { "Foo" }
+ component.pane_region(tag: :div) { "Bar" }
+ }],
+ [Primer::Beta::SplitPageLayout, {}, proc { |component|
+ component.content_region(tag: :div) { "Foo" }
+ component.pane_region(tag: :div) { "Bar" }
+ }],
[Primer::Alpha::Layout, {}, proc { |component|
component.main(tag: :div) { "Foo" }
component.sidebar(tag: :div) { "Bar" }
diff --git a/test/components/primer/beta/page_layout_test.rb b/test/components/primer/beta/page_layout_test.rb
new file mode 100644
index 0000000000..b82ba2db30
--- /dev/null
+++ b/test/components/primer/beta/page_layout_test.rb
@@ -0,0 +1,424 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class PrimerBetaPageLayoutTest < Minitest::Test
+ include Primer::ComponentTestHelpers
+
+ def test_doesnt_render_without_both_slots
+ render_inline(Primer::Beta::PageLayout.new)
+ refute_component_rendered
+
+ render_inline(Primer::Beta::PageLayout.new) { |c| c.content_region { "Content" } }
+ refute_component_rendered
+
+ render_inline(Primer::Beta::PageLayout.new) { |c| c.pane_region { "Pane" } }
+ refute_component_rendered
+ end
+
+ def test_renders_layout
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-columns") do
+ assert_selector("div.PageLayout-region.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-region.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_optionally_renders_header_and_footer
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.header_region { "Header" }
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ c.footer_region { "Footer" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-header.PageLayout-region", text: "Header")
+ assert_selector("div.PageLayout-columns") do
+ assert_selector("div.PageLayout-region.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-region.PageLayout-pane", text: "Pane")
+ end
+ assert_selector("div.PageLayout-footer.PageLayout-region", text: "Footer")
+ end
+ end
+
+ def test_renders_layout_with_correct_default_classes
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ expected_classes = [
+ "PageLayout",
+ "PageLayout--responsive-stackRegions",
+ "PageLayout--outerSpacing-normal",
+ "PageLayout--columnGap-normal",
+ "PageLayout--rowGap-normal",
+ "PageLayout--panePos-start",
+ "PageLayout--responsive-panePos-start"
+ ].join(".")
+ assert_selector("div.#{expected_classes}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+
+ def test_wrapper_sizing
+ Primer::Beta::PageLayout::WRAPPER_SIZING_OPTIONS.each do |size|
+ render_inline(Primer::Beta::PageLayout.new(wrapper_sizing: size)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-wrapper#{size == :fluid ? '' : ".container-#{size}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+ end
+
+ def test_outer_spacing
+ Primer::Beta::PageLayout::OUTER_SPACING_OPTIONS.each do |size|
+ render_inline(Primer::Beta::PageLayout.new(outer_spacing: size)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ size_class = Primer::Beta::PageLayout::OUTER_SPACING_MAPPINGS[size]
+ assert_selector("div.PageLayout#{size_class.empty? ? '' : ".#{size_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_column_gap
+ Primer::Beta::PageLayout::COLUMN_GAP_OPTIONS.each do |size|
+ render_inline(Primer::Beta::PageLayout.new(column_gap: size)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ size_class = Primer::Beta::PageLayout::COLUMN_GAP_MAPPINGS[size]
+ assert_selector("div.PageLayout#{size_class.empty? ? '' : ".#{size_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_row_gap
+ Primer::Beta::PageLayout::ROW_GAP_OPTIONS.each do |size|
+ render_inline(Primer::Beta::PageLayout.new(row_gap: size)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ size_class = Primer::Beta::PageLayout::ROW_GAP_MAPPINGS[size]
+ assert_selector("div.PageLayout#{size_class.empty? ? '' : ".#{size_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_responsive_variant
+ Primer::Beta::PageLayout::RESPONSIVE_VARIANT_OPTIONS.each do |variant|
+ render_inline(Primer::Beta::PageLayout.new(responsive_variant: variant)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ variant_class = Primer::Beta::PageLayout::RESPONSIVE_VARIANT_MAPPINGS[variant]
+ assert_selector("div.PageLayout#{variant_class.empty? ? '' : ".#{variant_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_primary_region
+ Primer::Beta::PageLayout::PRIMARY_REGION_OPTIONS.each do |region|
+ render_inline(Primer::Beta::PageLayout.new(responsive_variant: :separate_regions, primary_region: region)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ region_class = Primer::Beta::PageLayout::PRIMARY_REGION_MAPPINGS[region]
+ assert_selector("div.PageLayout#{region_class.empty? ? '' : ".#{region_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_primary_region_not_set_when_stack_regions
+ render_inline(Primer::Beta::PageLayout.new(responsive_variant: :stack_regions)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ refute_selector("div.PageLayout--responsive-primary-pane")
+ refute_selector("div.PageLayout--responsive-primary-content")
+ end
+
+ def test_pane_position
+ Primer::Beta::PageLayout::Pane::POSITION_OPTIONS.each do |position|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: position) { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout--panePos-#{position}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+ end
+
+ def test_pane_position_renders_pane_first
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: :start) { "Pane" }
+ end
+
+ assert_match(/PageLayout-pane.*PageLayout-content/m, @rendered_component)
+ end
+
+ def test_pane_position_renders_pane_last
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: :end) { "Pane" }
+ end
+
+ assert_match(/PageLayout-content.*PageLayout-pane/m, @rendered_component)
+ end
+
+ def test_stack_regions_variant_with_pane_position_narrow
+ Primer::Beta::PageLayout::Pane::POSITION_OPTIONS.each do |position|
+ Primer::Beta::PageLayout::Pane::POSITION_NARROW_OPTIONS.each do |position_narrow|
+ render_inline(Primer::Beta::PageLayout.new(responsive_variant: :stack_regions)) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: position, position_narrow: position_narrow) { "Pane" }
+ end
+
+ position_narrow = position if position_narrow == :inherit
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout--panePos-#{position}.PageLayout--responsive-panePos-#{position_narrow}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+ end
+ end
+
+ def test_variant_pane_position_not_set_when_separate_regions
+ render_inline(Primer::Beta::PageLayout.new(responsive_variant: :separate_regions)) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: :start) { "Pane" }
+ end
+
+ refute_selector("div.PageLayout--responsive-panePos-end")
+ refute_selector("div.PageLayout--responsive-panePos-start")
+ end
+
+ def test_pane_width
+ Primer::Beta::PageLayout::Pane::WIDTH_OPTIONS.each do |size|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(width: size) { "Pane" }
+ end
+
+ width_class = Primer::Beta::PageLayout::Pane::WIDTH_MAPPINGS[size]
+ assert_selector("div.PageLayout") do
+ assert_selector("div#{width_class.empty? ? '' : ".#{width_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+ end
+
+ def test_pane_divider_present_when_set
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(divider: true) { "Pane" }
+ end
+
+ assert_selector("div.PageLayout.PageLayout--hasPaneDivider") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane.PageLayout-region--dividerNarrow-line-after", text: "Pane")
+ end
+ end
+
+ def test_pane_divider_absent_when_not_set
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ refute_selector("div.PageLayout.PageLayout--hasPaneDivider")
+ end
+
+ def test_pane_divider_narrow
+ Primer::Beta::PageLayout::Pane::POSITION_OPTIONS.each do |position|
+ Primer::Beta::PageLayout::Pane::DIVIDER_NARROW_USER_OPTIONS.each do |type|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: position, divider: true, divider_narrow: type) { "Pane" }
+ end
+
+ type_class = Primer::Beta::PageLayout::Pane::DIVIDER_NARROW_MAPPINGS[{ position => type }]
+ assert_selector("div.PageLayout.PageLayout--hasPaneDivider") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane.#{type_class}", text: "Pane")
+ end
+ end
+ end
+ end
+
+ def test_pane_divider_narrow_not_applied_when_no_divider
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(position: :start, divider: false, divider_narrow: :line) { "Pane" }
+ end
+
+ type_class = Primer::Beta::PageLayout::Pane::DIVIDER_NARROW_MAPPINGS[{ start: :line }]
+ refute_selector("div.PageLayout-pane.#{type_class}", text: "Pane")
+ end
+
+ def test_pane_tags
+ Primer::Beta::PageLayout::Pane::TAG_OPTIONS.each do |tag|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(tag: tag) { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("#{tag}.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_main_width
+ Primer::Beta::PageLayout::Content::WIDTH_OPTIONS.each do |width|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region(width: width) { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout-columns") do
+ assert_selector("div.PageLayout-content") do
+ if width == :fluid
+ assert_text("Content")
+ else
+ assert_selector("div.PageLayout-content-centered-#{width}") do
+ assert_selector("div.container-#{width}", text: "Content")
+ end
+ end
+ end
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_main_tags
+ Primer::Beta::PageLayout::Content::TAG_OPTIONS.each do |tag|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region(tag: tag) { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("#{tag}.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_header_divider_present_when_set
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.header_region(divider: true) { "Header" }
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout-header.PageLayout-header--hasDivider.PageLayout-region--dividerNarrow-line-after")
+ end
+
+ def test_header_divider_not_present_when_not_set
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.header_region { "Header" }
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ refute_selector("div.PageLayout-header.PageLayout-header--hasDivider")
+ end
+
+ def test_header_responsive_divider
+ Primer::Beta::PageLayout::HEADER_DIVIDER_NARROW_OPTIONS.each do |opt|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.header_region(divider: true, divider_narrow: opt) { "Header" }
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ divider_class = Primer::Beta::PageLayout::HEADER_DIVIDER_NARROW_MAPPINGS[opt]
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-header#{divider_class.empty? ? '' : ".#{divider_class}"}", text: "Header")
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_footer_divider_present_when_set
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ c.footer_region(divider: true) { "Footer" }
+ end
+
+ assert_selector("div.PageLayout-footer.PageLayout-footer--hasDivider.PageLayout-region--dividerNarrow-line-before")
+ end
+
+ def test_footer_divider_not_present_when_not_set
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ c.footer_region { "Footer" }
+ end
+
+ refute_selector("div.PageLayout-footer.PageLayout-footer--hasDivider.PageLayout-region--dividerNarrow-line-before")
+ end
+
+ def test_footer_responsive_divider
+ Primer::Beta::PageLayout::FOOTER_DIVIDER_NARROW_OPTIONS.each do |opt|
+ render_inline(Primer::Beta::PageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ c.footer_region(divider: true, divider_narrow: opt) { "Footer" }
+ end
+
+ divider_class = Primer::Beta::PageLayout::FOOTER_DIVIDER_NARROW_MAPPINGS[opt]
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-footer#{divider_class.empty? ? '' : ".#{divider_class}"}", text: "Footer")
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+end
diff --git a/test/components/primer/beta/split_page_layout_test.rb b/test/components/primer/beta/split_page_layout_test.rb
new file mode 100644
index 0000000000..1b6fb39c4d
--- /dev/null
+++ b/test/components/primer/beta/split_page_layout_test.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class PrimerBetaSplitPageLayoutTest < Minitest::Test
+ include Primer::ComponentTestHelpers
+
+ def test_doesnt_render_without_both_slots
+ render_inline(Primer::Beta::SplitPageLayout.new)
+ refute_component_rendered
+
+ render_inline(Primer::Beta::SplitPageLayout.new) { |c| c.content_region { "Content" } }
+ refute_component_rendered
+
+ render_inline(Primer::Beta::SplitPageLayout.new) { |c| c.pane_region { "Pane" } }
+ refute_component_rendered
+ end
+
+ def test_renders_layout
+ render_inline(Primer::Beta::SplitPageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+
+ def test_renders_layout_with_correct_default_classes
+ render_inline(Primer::Beta::SplitPageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ expected_classes = [
+ "PageLayout",
+ "PageLayout--innerSpacing-normal",
+ "PageLayout--columnGap-none",
+ "PageLayout--rowGap-none",
+ "PageLayout--panePos-start",
+ "PageLayout--hasPaneDivider",
+ "PageLayout--responsive-separateRegions",
+ "PageLayout--responsive-primary-content"
+ ].join(".")
+ assert_selector("div.#{expected_classes}") do
+ assert_selector("div.PageLayout-wrapper") do
+ assert_selector("div.PageLayout-columns") do
+ assert_selector("div.PageLayout-region.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-region.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+ end
+
+ def test_primary_region
+ Primer::Beta::SplitPageLayout::PRIMARY_REGION_OPTIONS.each do |region|
+ render_inline(Primer::Beta::SplitPageLayout.new(primary_region: region)) do |c|
+ c.content_region { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ region_class = Primer::Beta::SplitPageLayout::PRIMARY_REGION_MAPPINGS[region]
+ assert_selector("div.PageLayout#{region_class.empty? ? '' : ".#{region_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_pane_width
+ Primer::Beta::SplitPageLayout::PANE_WIDTH_OPTIONS.each do |size|
+ render_inline(Primer::Beta::SplitPageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(width: size) { "Pane" }
+ end
+
+ width_class = Primer::Beta::SplitPageLayout::PANE_WIDTH_MAPPINGS[size]
+ assert_selector("div.PageLayout") do
+ assert_selector("div#{width_class.empty? ? '' : ".#{width_class}"}") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+ end
+
+ def test_pane_tags
+ Primer::Beta::SplitPageLayout::PANE_TAG_OPTIONS.each do |tag|
+ render_inline(Primer::Beta::SplitPageLayout.new) do |c|
+ c.content_region { "Content" }
+ c.pane_region(tag: tag) { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("div.PageLayout-content", text: "Content")
+ assert_selector("#{tag}.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_content_width
+ Primer::Beta::SplitPageLayout::Content::WIDTH_OPTIONS.each do |width|
+ render_inline(Primer::Beta::SplitPageLayout.new) do |c|
+ c.content_region(width: width) { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout-columns") do
+ assert_selector("div.PageLayout-content") do
+ if width == :fluid
+ assert_text("Content")
+ else
+ assert_selector("div.PageLayout-content-centered-#{width}") do
+ assert_selector("div.container-#{width}", text: "Content")
+ end
+ end
+ end
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+
+ def test_content_tags
+ Primer::Beta::SplitPageLayout::Content::TAG_OPTIONS.each do |tag|
+ render_inline(Primer::Beta::SplitPageLayout.new) do |c|
+ c.content_region(tag: tag) { "Content" }
+ c.pane_region { "Pane" }
+ end
+
+ assert_selector("div.PageLayout") do
+ assert_selector("#{tag}.PageLayout-content", text: "Content")
+ assert_selector("div.PageLayout-pane", text: "Pane")
+ end
+ end
+ end
+end
diff --git a/test/components/stories_test.rb b/test/components/stories_test.rb
index 2c03d2023f..96dd4879e7 100644
--- a/test/components/stories_test.rb
+++ b/test/components/stories_test.rb
@@ -9,6 +9,9 @@ class AllComponentsHaveStoriesTest < Minitest::Test
Primer::FlexItemComponent,
Primer::OcticonSymbolsComponent,
Primer::Beta::Breadcrumbs::Item,
+ Primer::Beta::SplitPageLayout::Content,
+ Primer::Beta::PageLayout::Pane,
+ Primer::Beta::PageLayout::Content,
Primer::Alpha::Layout::Main,
Primer::Alpha::Layout::Sidebar
].freeze
diff --git a/yarn.lock b/yarn.lock
index b37c653dcf..2c93cb0603 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -102,18 +102,23 @@
"@nodelib/fs.scandir" "2.1.4"
fastq "^1.6.0"
-"@primer/css@^19.0.0":
- version "19.0.0"
- resolved "https://registry.yarnpkg.com/@primer/css/-/css-19.0.0.tgz#c03e9d9365e37074f3555a98bdea0e705e1f5bf3"
- integrity sha512-w3/DoQlWBjyyo0scneGkcVHyaIBFjODbmWZoJ1qYHe+eLufPOKNc5Tf/OlxqiYQxGCF8vVb3f/i30z4KfzJzzA==
+"@primer/css@^19.2.0":
+ version "19.2.0"
+ resolved "https://registry.yarnpkg.com/@primer/css/-/css-19.2.0.tgz#afd1821c46da5a1c675f8e74a38fa566434c868b"
+ integrity sha512-20fJIGZy/s+LqDfqR1qqGS34LZTg3co4XbeL+Lv45LBqsVQ+ULwURNU6y3JBb78THLbish9V4TccPw59dj1YOQ==
dependencies:
- "@primer/primitives" "^7.1.0"
+ "@primer/primitives" "^7.4.0"
"@primer/primitives@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.1.0.tgz#944151afb8b0e8ffd33c1cfdc4f873b5dd46cbe5"
integrity sha512-XVM535ieY+ohh82oMCZZwlebsnNtJORR5vpiUk/OLT1KX7pr2aRiSbitFwZ3umo0dw/L5tr6ebSJjRCXLu8UJg==
+"@primer/primitives@^7.4.0":
+ version "7.4.0"
+ resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.4.0.tgz#75df54a80233a432b687c0e3010e4be6bd60a82d"
+ integrity sha512-gD6yHXN7YKox/bdUNgxhoSS/WXZVaORK1r4dOAyTrdoPrLV/ucIfRInPyVcTF+Mqr0zcTFJtiMtuA5Y8CSyOEg==
+
"@rollup/plugin-node-resolve@^11.2.0":
version "11.2.0"
resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.0.tgz"