diff --git a/addons/rose/addon/components/rose/footer/brand/index.hbs b/addons/rose/addon/components/rose/footer/brand/index.hbs
new file mode 100644
index 000000000..27b91417f
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/brand/index.hbs
@@ -0,0 +1,12 @@
+
+
+ {{#if @logo}}
+ {{inline-svg @logo}}
+ {{/if}}
+
+
diff --git a/addons/rose/addon/components/rose/footer/copyright/index.hbs b/addons/rose/addon/components/rose/footer/copyright/index.hbs
new file mode 100644
index 000000000..47e582dcb
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/copyright/index.hbs
@@ -0,0 +1,11 @@
+
diff --git a/addons/rose/addon/components/rose/footer/copyright/index.js b/addons/rose/addon/components/rose/footer/copyright/index.js
new file mode 100644
index 000000000..a9068f750
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/copyright/index.js
@@ -0,0 +1,8 @@
+import Component from '@glimmer/component';
+
+export default class RoseFooterCopyrightComponent extends Component {
+
+ // =attributes
+ currentYear = new Date().getFullYear();
+
+}
diff --git a/addons/rose/addon/components/rose/footer/index.hbs b/addons/rose/addon/components/rose/footer/index.hbs
new file mode 100644
index 000000000..bbe5ecc72
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/index.hbs
@@ -0,0 +1,10 @@
+
diff --git a/addons/rose/addon/components/rose/footer/index.stories.mdx b/addons/rose/addon/components/rose/footer/index.stories.mdx
new file mode 100644
index 000000000..608bc1068
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/index.stories.mdx
@@ -0,0 +1,40 @@
+import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
+import { hbs } from 'ember-cli-htmlbars';
+import { action } from '@storybook/addon-actions';
+import { text } from '@storybook/addon-knobs';
+
+
+
+# Footer
+
+A footer component that allows four distinct sections to be configured: brand, nav, and text.
+
+```html
+
+
+ Text
+
+ Section
+ Section
+
+
+```
+
+
+ {{
+ template: hbs`
+
+
+ {{footerText}}
+
+ Section
+ Section
+
+
+ `,
+ context: {
+ brandName: text('brandName', 'Brand Name'),
+ footerText: text('footerText', 'Text'),
+ },
+ }}
+
diff --git a/addons/rose/addon/components/rose/footer/nav/index.hbs b/addons/rose/addon/components/rose/footer/nav/index.hbs
new file mode 100644
index 000000000..c03ac88c9
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/nav/index.hbs
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/addons/rose/addon/components/rose/footer/nav/link/index.hbs b/addons/rose/addon/components/rose/footer/nav/link/index.hbs
new file mode 100644
index 000000000..bebaacc29
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/nav/link/index.hbs
@@ -0,0 +1,20 @@
+{{#if @href}}
+
+{{/if}}
+
+{{#if @route}}
+
+{{/if}}
diff --git a/addons/rose/addon/components/rose/footer/product/index.hbs b/addons/rose/addon/components/rose/footer/product/index.hbs
new file mode 100644
index 000000000..6d3436db1
--- /dev/null
+++ b/addons/rose/addon/components/rose/footer/product/index.hbs
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/addons/rose/addon/components/rose/layout/global/index.hbs b/addons/rose/addon/components/rose/layout/global/index.hbs
index bfa2c8e75..373f70531 100644
--- a/addons/rose/addon/components/rose/layout/global/index.hbs
+++ b/addons/rose/addon/components/rose/layout/global/index.hbs
@@ -2,5 +2,6 @@
{{yield (hash
header=(component "rose/anonymous" tagName="header" class="rose-layout-global-header")
body=(component "rose/anonymous" class="rose-layout-global-body")
+ footer=(component "rose/anonymous" class="rose-layout-global-footer")
)}}
diff --git a/addons/rose/addon/components/rose/layout/global/index.stories.mdx b/addons/rose/addon/components/rose/layout/global/index.stories.mdx
index 9b8e34ea4..22cb6b8e6 100644
--- a/addons/rose/addon/components/rose/layout/global/index.stories.mdx
+++ b/addons/rose/addon/components/rose/layout/global/index.stories.mdx
@@ -1,29 +1,29 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
-import { action } from '@storybook/addon-actions';
-import { text, boolean, select } from '@storybook/addon-knobs';
# Layout|Global
-A top-level layout/positioning component that allows two distinct sections to
-be configured: header and body.
+A top-level layout/positioning component that allows three distinct sections to
+be configured: header, body, and footer.
-```htmlbars
+```html
+
```
{{
template: hbs`
-
- Header
- Body
-
+
+ Header
+ Body
+ Footer
+
`,
}}
diff --git a/addons/rose/addon/styles/rose/components/_index.scss b/addons/rose/addon/styles/rose/components/_index.scss
index 19a9d3de6..6e6ef8490 100644
--- a/addons/rose/addon/styles/rose/components/_index.scss
+++ b/addons/rose/addon/styles/rose/components/_index.scss
@@ -2,6 +2,7 @@
@import 'dropdown';
@import 'form';
@import 'header';
+@import 'footer';
@import 'layout';
@import 'icon';
@import 'list';
diff --git a/addons/rose/addon/styles/rose/components/footer/_brand.scss b/addons/rose/addon/styles/rose/components/footer/_brand.scss
new file mode 100644
index 000000000..26e5f2983
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/footer/_brand.scss
@@ -0,0 +1,14 @@
+@use '../../variables/sizing';
+
+.rose-footer-brand {
+ display: flex;
+ align-items: center;
+ color: var(--ui-gray-starker-1);
+ margin: sizing.rems(xs);
+
+ svg {
+ $logoSize: sizing.rems(m);
+ display: inline-block;
+ width: $logoSize;
+ }
+}
diff --git a/addons/rose/addon/styles/rose/components/footer/_index.scss b/addons/rose/addon/styles/rose/components/footer/_index.scss
new file mode 100644
index 000000000..9632c4e0c
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/footer/_index.scss
@@ -0,0 +1,17 @@
+@use '../../variables/sizing';
+@use '../../variables/media';
+@use '../../utilities/type';
+
+@import 'brand';
+@import 'nav';
+@import 'text';
+@import 'link';
+
+.rose-footer {
+ @include type.type(xs);
+
+ display: flex;
+ justify-content: center;
+ color: var(--ui-gray-starker-1);
+ height: sizing.rems(xxl) + sizing.rems(xxs);
+}
diff --git a/addons/rose/addon/styles/rose/components/footer/_link.scss b/addons/rose/addon/styles/rose/components/footer/_link.scss
new file mode 100644
index 000000000..dc35db4c0
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/footer/_link.scss
@@ -0,0 +1,9 @@
+@use '../../variables/sizing';
+
+.rose-footer-nav-link {
+ display: inline-flex;
+ align-items: center;
+ text-decoration: underline;
+ color: var(--ui-gray-starker-1);
+ margin: 0 sizing.rems(xs);
+}
diff --git a/addons/rose/addon/styles/rose/components/footer/_nav.scss b/addons/rose/addon/styles/rose/components/footer/_nav.scss
new file mode 100644
index 000000000..bd23dde4a
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/footer/_nav.scss
@@ -0,0 +1,9 @@
+@use '../../variables/sizing';
+
+.rose-footer-nav {
+ grid-area: nav;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ margin-right: sizing.rems(s);
+}
diff --git a/addons/rose/addon/styles/rose/components/footer/_text.scss b/addons/rose/addon/styles/rose/components/footer/_text.scss
new file mode 100644
index 000000000..9e7b29b8b
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/footer/_text.scss
@@ -0,0 +1,7 @@
+@use '../../variables/sizing';
+
+.rose-footer-text {
+ display: inline-flex;
+ align-items: center;
+ margin: 0 sizing.rems(xs);
+}
diff --git a/addons/rose/app/components/rose/footer.js b/addons/rose/app/components/rose/footer.js
new file mode 100644
index 000000000..ccac33104
--- /dev/null
+++ b/addons/rose/app/components/rose/footer.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/footer';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/footer/brand.js b/addons/rose/app/components/rose/footer/brand.js
new file mode 100644
index 000000000..995d715ba
--- /dev/null
+++ b/addons/rose/app/components/rose/footer/brand.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/footer/brand';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/footer/copyright.js b/addons/rose/app/components/rose/footer/copyright.js
new file mode 100644
index 000000000..d46304fc7
--- /dev/null
+++ b/addons/rose/app/components/rose/footer/copyright.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/footer/copyright';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/footer/nav.js b/addons/rose/app/components/rose/footer/nav.js
new file mode 100644
index 000000000..3ca032af2
--- /dev/null
+++ b/addons/rose/app/components/rose/footer/nav.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/footer/nav';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/footer/nav/link.js b/addons/rose/app/components/rose/footer/nav/link.js
new file mode 100644
index 000000000..e38b27530
--- /dev/null
+++ b/addons/rose/app/components/rose/footer/nav/link.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/footer/nav/link';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/footer/product.js b/addons/rose/app/components/rose/footer/product.js
new file mode 100644
index 000000000..7193b5690
--- /dev/null
+++ b/addons/rose/app/components/rose/footer/product.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/footer/product';
\ No newline at end of file
diff --git a/addons/rose/tests/integration/components/rose/footer-test.js b/addons/rose/tests/integration/components/rose/footer-test.js
new file mode 100644
index 000000000..4a43f0d4f
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/footer-test.js
@@ -0,0 +1,42 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/footer', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ assert.expect(4);
+ await render(hbs``);
+ assert.ok(find('.rose-footer'));
+ assert.notOk(find('.rose-footer-nav'));
+ assert.notOk(find('.rose-footer-text'));
+ assert.notOk(find('.rose-footer-brand'));
+ });
+
+ test('it renders with brand', async function (assert) {
+ assert.expect(1);
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-footer-brand'));
+ });
+
+ test('it renders with nav', async function (assert) {
+ assert.expect(1);
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-footer-nav'));
+ });
+
+ test('it renders with text', async function (assert) {
+ assert.expect(1);
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-footer-text'));
+ });
+
+});
diff --git a/addons/rose/tests/integration/components/rose/footer/nav-test.js b/addons/rose/tests/integration/components/rose/footer/nav-test.js
new file mode 100644
index 000000000..782602f44
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/footer/nav-test.js
@@ -0,0 +1,24 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, findAll } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/footer/nav', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ assert.expect(1);
+ await render(hbs``);
+ assert.ok(find('.rose-footer-nav'));
+ });
+
+ test('it renders nav external link elements', async function (assert) {
+ assert.expect(1);
+ await render(hbs`
+
+
+ `);
+ assert.equal(findAll('a').length, 2);
+ });
+
+});
diff --git a/addons/rose/tests/integration/components/rose/footer/nav/link-test.js b/addons/rose/tests/integration/components/rose/footer/nav/link-test.js
new file mode 100644
index 000000000..d5405db01
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/footer/nav/link-test.js
@@ -0,0 +1,22 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/footer/nav/link', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders with @href', async function (assert) {
+ assert.expect(2);
+ await render(hbs``);
+ assert.ok(find('a'));
+ assert.ok(find('.rose-footer-nav-link'));
+ });
+
+ test('it renders with @route', async function (assert) {
+ assert.expect(2);
+ await render(hbs``);
+ assert.ok(find('a'));
+ assert.ok(find('.rose-footer-nav-link'));
+ });
+});
diff --git a/addons/rose/tests/integration/components/rose/footer/text-test.js b/addons/rose/tests/integration/components/rose/footer/text-test.js
new file mode 100644
index 000000000..d3cbfacff
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/footer/text-test.js
@@ -0,0 +1,24 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/footer/text', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ assert.expect(1);
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-footer-text'));
+ });
+
+ test('it renders content', async function (assert) {
+ assert.expect(1);
+ await render(hbs`
+ content
+ `);
+ assert.equal(find('.rose-footer-text').textContent.trim(), 'content');
+ });
+});
diff --git a/addons/rose/tests/integration/components/rose/layout/global-test.js b/addons/rose/tests/integration/components/rose/layout/global-test.js
index 2e6b91039..aa87f074b 100644
--- a/addons/rose/tests/integration/components/rose/layout/global-test.js
+++ b/addons/rose/tests/integration/components/rose/layout/global-test.js
@@ -30,4 +30,11 @@ module('Integration | Component | rose/layout/global', function (hooks) {
`);
assert.ok(find('.rose-layout-global-body'));
});
+
+ test('it renders with footer', async function (assert) {
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-layout-global-footer'));
+ });
});
diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs
index d63eee931..a40b1dcba 100644
--- a/ui/core/app/templates/application.hbs
+++ b/ui/core/app/templates/application.hbs
@@ -37,6 +37,17 @@
{{outlet}}
+
+
+
+ {{t "hashicorp.copyright"}}
+ {{t "hashicorp.product"}}
+
+ {{t "hashicorp.documentation.title"}}
+
+
+
+
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index a86a36263..6755b8976 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -17,4 +17,11 @@ titles:
global: Global
actions: Actions
descriptions:
- empty-set: There are no items to display yet. You may be able to add items or try back later.
\ No newline at end of file
+ empty-set: There are no items to display yet. You may be able to add items or try back later.
+hashicorp:
+ title: HashiCorp
+ copyright: © 2020 HashiCorp
+ product: Boundary v0.1
+ documentation:
+ title: Documentation
+ link: https://boundaryproject.io