Skip to content

Commit 9dd2a49

Browse files
AppFrame component (#2147)
* Add AppFrame component * Please stylelint * Update src/layout/app-frame.scss Co-authored-by: Katie Langerman <[email protected]> * Use tokens * Add changeset Co-authored-by: Katie Langerman <[email protected]>
1 parent ee85a4b commit 9dd2a49

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

.changeset/lovely-rivers-bow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/css": minor
3+
---
4+
5+
Add AppFrame component
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React from 'react'
2+
import clsx from 'clsx'
3+
//import { AppHeaderTemplate as AppHeader } from '../App/AppHeader.stories'
4+
5+
export default {
6+
title: 'Components/Layout/AppFrame',
7+
parameters: {
8+
layout: 'fullscreen'
9+
},
10+
11+
excludeStories: ['AppFrameTemplate'],
12+
argTypes: {
13+
14+
// Debug
15+
16+
_debug: {
17+
control: {
18+
type: 'boolean',
19+
}
20+
},
21+
22+
a11yNavItems: {
23+
type: 'array',
24+
},
25+
26+
// Children
27+
28+
headerChildren: {
29+
description: 'creates a slot for header children',
30+
table: {
31+
category: 'HTML'
32+
}
33+
},
34+
subheaderChildren: {
35+
description: 'creates a slot for subheader children',
36+
table: {
37+
category: 'HTML'
38+
}
39+
},
40+
bodyChildren: {
41+
description: 'creates a slot for body children',
42+
table: {
43+
category: 'HTML'
44+
}
45+
},
46+
footerChildren: {
47+
description: 'creates a slot for footer children',
48+
table: {
49+
category: 'HTML'
50+
}
51+
},
52+
},
53+
}
54+
55+
export const AppFrameTemplate = ({
56+
_debug,
57+
a11yNavItems,
58+
headerChildren,
59+
subheaderChildren,
60+
bodyChildren,
61+
footerChildren,
62+
}) => {
63+
64+
// Default values
65+
a11yNavItems = a11yNavItems ?? [
66+
{url: '#start-of-content', label: 'Skip to content'},
67+
{url: '/', label: 'GitHub homepage'},
68+
];
69+
70+
return (
71+
<>
72+
<div className={clsx('AppFrame')}>
73+
74+
<div className={clsx('AppFrame-a11yNav')}>
75+
{a11yNavItems.map(link => (
76+
<a className={clsx('AppFrame-a11yLink')} href={link.url}>{link.label}</a>
77+
))}
78+
</div>
79+
80+
<div className={clsx('AppFrame-main')}>
81+
82+
<div className={clsx('AppFrame-header-wrapper')}>
83+
84+
<div className={clsx('AppFrame-header')}>
85+
{headerChildren}
86+
</div>
87+
88+
<div id="start-of-content"></div>
89+
90+
{subheaderChildren && (
91+
<div className={clsx('AppFrame-subheader')}>
92+
{subheaderChildren}
93+
</div>
94+
)}
95+
96+
</div>
97+
<div className={clsx('AppFrame-body')}>
98+
{bodyChildren}
99+
</div>
100+
</div>
101+
<div className={clsx('AppFrame-footer')}>
102+
{footerChildren}
103+
</div>
104+
</div>
105+
106+
{_debug && (
107+
<>
108+
<style type="text/css">{`
109+
.AppFrame {
110+
111+
}
112+
.AppFrame-header,
113+
.AppFrame-subheader,
114+
.AppFrame-body,
115+
.AppFrame-footer {
116+
padding: 16px;
117+
}
118+
.AppFrame-header {
119+
background: pink;
120+
}
121+
.AppFrame-subheader {
122+
background: lightblue;
123+
}
124+
.AppFrame-footer {
125+
background: pink;
126+
}
127+
`}</style>
128+
</>
129+
)}
130+
</>
131+
);
132+
};
133+
134+
export const Playground = AppFrameTemplate.bind({})
135+
136+
Playground.args = {
137+
_debug: true,
138+
headerChildren: "Header slot",
139+
subheaderChildren: "Subheader slot",
140+
bodyChildren: "Body slot",
141+
footerChildren: "Footer slot",
142+
};

src/layout/app-frame.scss

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// stylelint-disable max-nesting-depth
2+
// stylelint-disable primer/spacing
3+
// stylelint-disable primer/borders
4+
5+
.AppFrame {
6+
7+
// AppFrame structure
8+
// ===================
9+
//
10+
// .AppFrame
11+
// ├─ .AppFrame-a11yNav
12+
// │ ├─ .AppFrame-a11yLink
13+
//
14+
// ├─ .AppFrame-main
15+
// │ ├─ .AppFrame-header-wrapper
16+
// │ │ ├─ .AppFrame-header
17+
// │ │ ├─ .AppFrame-subheader
18+
// │ │
19+
// │ ├─ #start-of-content
20+
// │ ├─ .AppFrame-body
21+
// │ ├─ .AppFrame-footer
22+
23+
// Accessibility navigation
24+
25+
.AppFrame-a11yNav {
26+
position: absolute;
27+
z-index: 1000;
28+
display: flex;
29+
width: 100%;
30+
padding: var(--base-size-16, 16px);
31+
background: var(--color-canvas-inset);
32+
padding-block-end: calc(var(--base-size-16, 16px) - var(--primer-borderWidth-thin, 1px));
33+
isolation: isolate;
34+
align-items: center;
35+
gap: var(--base-size-8, 8px);
36+
37+
&:not(:focus-within) {
38+
width: 1px;
39+
height: 1px;
40+
padding: 0;
41+
margin: -1px;
42+
overflow: hidden;
43+
clip: rect(1px, 1px, 1px, 1px);
44+
border: 0;
45+
}
46+
47+
&:focus-within {
48+
top: 0;
49+
left: 0;
50+
51+
// Narrow viewport
52+
@media (max-width: #{map-get($breakpoints, 'md') - 0.02px}) {
53+
justify-content: center;
54+
}
55+
}
56+
}
57+
58+
.AppFrame-a11yLink {
59+
transition: none;
60+
61+
&:not(:focus) {
62+
display: block;
63+
width: var(--base-size-8, 8px);
64+
height: var(--base-size-8, 8px);
65+
overflow: hidden;
66+
text-indent: var(--base-size-128, 128px);
67+
pointer-events: none;
68+
background: var(--color-border-default);
69+
border-radius: var(--primer-borderRadius-full, 100vh);
70+
}
71+
72+
&:focus {
73+
z-index: 20;
74+
display: grid;
75+
width: auto;
76+
height: auto;
77+
min-height: var(--primer-control-medium-size, 32px);
78+
padding: 0 var(--primer-control-medium-paddingInline-spacious, 16px);
79+
overflow: auto;
80+
color: var(--color-fg-on-emphasis);
81+
background: var(--color-accent-emphasis);
82+
border-radius: var(--primer-borderRadius-full, 100vh);
83+
align-items: center;
84+
85+
@media (pointer: coarse) {
86+
&::after {
87+
@include minTouchTarget(var(--primer-control-minTarget-coarse, 44px));
88+
}
89+
}
90+
91+
@media (prefers-reduced-motion: no-preference) {
92+
animation: AppFrame-a11yLink-focus 200ms ease-out;
93+
}
94+
95+
@keyframes AppFrame-a11yLink-focus {
96+
0% {
97+
color: var(--color-accent-emphasis);
98+
transform: scale(0.3, 0.25);
99+
}
100+
101+
50% {
102+
color: var(--color-accent-emphasis);
103+
transform: scale(1, 1);
104+
}
105+
106+
55% {
107+
color: var(--color-fg-on-emphasis);
108+
}
109+
110+
100% {
111+
transform: scaleX(1);
112+
}
113+
}
114+
}
115+
}
116+
117+
.AppFrame-main {
118+
display: flex;
119+
min-height: 100vh;
120+
flex-direction: column;
121+
122+
@supports (height: 100dvh) {
123+
min-height: 100dvh;
124+
}
125+
}
126+
127+
.AppFrame-header-wrapper {
128+
position: relative;
129+
height: min-content;
130+
overflow: visible;
131+
132+
.AppFrame-header {
133+
position: sticky;
134+
top: 0;
135+
z-index: 1;
136+
}
137+
}
138+
139+
.AppFrame-header {
140+
flex: 0 0 auto;
141+
}
142+
143+
.AppFrame-subheader {
144+
flex: 0 0 auto;
145+
}
146+
147+
.AppFrame-body {
148+
flex: 1 0;
149+
height: 100%;
150+
}
151+
152+
.AppFrame-footer {
153+
flex: 0 0 auto;
154+
}
155+
}

src/layout/index.scss

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import '../support/index.scss';
2+
@import './app-frame.scss';
23
@import './mixins.scss';
34
@import './container.scss';
45
@import './grid.scss';

0 commit comments

Comments
 (0)