diff --git a/addons/rose/addon/components/rose/list/key-value.js b/addons/rose/addon/components/rose/list/key-value.js
new file mode 100644
index 000000000..0d173250c
--- /dev/null
+++ b/addons/rose/addon/components/rose/list/key-value.js
@@ -0,0 +1,7 @@
+import Component from '@ember/component';
+import layout from '../../../templates/components/rose/list/key-value';
+
+export default Component.extend({
+ layout,
+ tagName: ''
+});
diff --git a/addons/rose/addon/components/rose/list/key-value.stories.mdx b/addons/rose/addon/components/rose/list/key-value.stories.mdx
new file mode 100644
index 000000000..b37e554ff
--- /dev/null
+++ b/addons/rose/addon/components/rose/list/key-value.stories.mdx
@@ -0,0 +1,103 @@
+import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
+import { hbs } from 'ember-cli-htmlbars';
+import { action } from '@storybook/addon-actions';
+
+
+
+# Key/Value List
+
+A simple key/value list with support for embedded form fields and buttons.
+
+
+ {{
+ template: hbs`
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+
+
+ Key
+
+
+
+
+
+ Add
+
+
+
+
+
+
+ Key
+
+
+
+
+
+ Remove
+
+
+
+
+ `,
+ }}
+
+
+```html
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+ {{!-- In actual use, form fields should be yielded from an outer form component --}}
+
+
+
+ Key
+
+
+
+
+
+ Add
+
+
+
+
+
+
+
+ Key
+
+
+
+
+
+ Remove
+
+
+
+
+
+```
diff --git a/addons/rose/addon/components/rose/list/key-value/item.js b/addons/rose/addon/components/rose/list/key-value/item.js
new file mode 100644
index 000000000..291e1b9da
--- /dev/null
+++ b/addons/rose/addon/components/rose/list/key-value/item.js
@@ -0,0 +1,8 @@
+import Component from '@ember/component';
+import layout from '../../../../templates/components/rose/list/key-value/item';
+import ComponentAutoId from '../../../../mixins/component-auto-id';
+
+export default Component.extend(ComponentAutoId, {
+ layout,
+ tagName: ''
+});
diff --git a/addons/rose/addon/styles/rose/components/_index.scss b/addons/rose/addon/styles/rose/components/_index.scss
index dd35e9d22..19a9d3de6 100644
--- a/addons/rose/addon/styles/rose/components/_index.scss
+++ b/addons/rose/addon/styles/rose/components/_index.scss
@@ -4,6 +4,7 @@
@import 'header';
@import 'layout';
@import 'icon';
+@import 'list';
@import 'notification';
@import 'card';
@import 'cards';
diff --git a/addons/rose/addon/styles/rose/components/form/_input.scss b/addons/rose/addon/styles/rose/components/form/_input.scss
index e5be517eb..4b898f4ef 100644
--- a/addons/rose/addon/styles/rose/components/form/_input.scss
+++ b/addons/rose/addon/styles/rose/components/form/_input.scss
@@ -9,6 +9,7 @@
@include type.type(m);
display: inline-block;
box-sizing: border-box;
+ width: 100%;
margin-bottom: sizing.rems(xs);
padding: sizing.rems(xs);
color: var(--stark);
diff --git a/addons/rose/addon/styles/rose/components/form/_select.scss b/addons/rose/addon/styles/rose/components/form/_select.scss
index d5d4a499a..4e1902da0 100644
--- a/addons/rose/addon/styles/rose/components/form/_select.scss
+++ b/addons/rose/addon/styles/rose/components/form/_select.scss
@@ -13,6 +13,7 @@
vertical-align: middle;
display: inline-block;
box-sizing: border-box;
+ width: 100%;
margin-bottom: sizing.rems(xs);
padding: sizing.rems(xs) sizing.rems(xxl) sizing.rems(xs) sizing.rems(xs);
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
diff --git a/addons/rose/addon/styles/rose/components/list/_index.scss b/addons/rose/addon/styles/rose/components/list/_index.scss
new file mode 100644
index 000000000..f52188987
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/list/_index.scss
@@ -0,0 +1 @@
+@import 'key-value';
diff --git a/addons/rose/addon/styles/rose/components/list/_key-value.scss b/addons/rose/addon/styles/rose/components/list/_key-value.scss
new file mode 100644
index 000000000..a6b54eb24
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/list/_key-value.scss
@@ -0,0 +1,45 @@
+@use '../../variables/sizing';
+@use '../../variables/typography';
+@use '../../utilities/type';
+
+.rose-list-key-value-item {
+ display: flex;
+ border-bottom: 1px solid var(--ui-gray-subtler-3);
+ @include type.type(s);
+ $lineHeight: 36 / 14;
+ line-height: $lineHeight;
+ padding: (sizing.rems(s) / 2) 0;
+
+ .rose-list-key-value-item-cell {
+ display: flex;
+ align-items: center;
+ margin-right: sizing.rems(m);
+
+ // Eliminate top/bottom margin from children
+ > * {
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+
+ &:last-child {
+ &,
+ & > * {
+ margin-right: 0;
+ }
+ }
+
+ // First cell (key) is approximately 25%
+ &:nth-child(1) {
+ width: 25%;
+ font-weight: typography.font-weight(semibold);
+ }
+
+ // Second cell (value) is approximately 75%
+ &:nth-child(2) {
+ flex-grow: 1;
+ }
+
+ // Remaining cells will take up as little space as necessary to
+ // suit their content.
+ }
+}
diff --git a/addons/rose/addon/templates/components/rose/list/key-value.hbs b/addons/rose/addon/templates/components/rose/list/key-value.hbs
new file mode 100644
index 000000000..2cd7a3d44
--- /dev/null
+++ b/addons/rose/addon/templates/components/rose/list/key-value.hbs
@@ -0,0 +1,5 @@
+
+ {{yield (hash
+ item=(component 'rose/list/key-value/item')
+ )}}
+
diff --git a/addons/rose/addon/templates/components/rose/list/key-value/item.hbs b/addons/rose/addon/templates/components/rose/list/key-value/item.hbs
new file mode 100644
index 000000000..a4585afaa
--- /dev/null
+++ b/addons/rose/addon/templates/components/rose/list/key-value/item.hbs
@@ -0,0 +1,13 @@
+
+ {{yield (hash
+ key=(component 'rose/anonymous'
+ id=(concat "item-key-" this.id)
+ tagName='div'
+ classNames='rose-list-key-value-item-cell'
+ )
+ cell=(component 'rose/anonymous'
+ tagName='div'
+ classNames='rose-list-key-value-item-cell'
+ )
+ )}}
+
diff --git a/addons/rose/app/components/rose/list/key-value.js b/addons/rose/app/components/rose/list/key-value.js
new file mode 100644
index 000000000..c2016a1d1
--- /dev/null
+++ b/addons/rose/app/components/rose/list/key-value.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/list/key-value';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/list/key-value/item.js b/addons/rose/app/components/rose/list/key-value/item.js
new file mode 100644
index 000000000..842352028
--- /dev/null
+++ b/addons/rose/app/components/rose/list/key-value/item.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/list/key-value/item';
\ No newline at end of file
diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs
index 95220225a..2974ba49c 100644
--- a/addons/rose/tests/dummy/app/templates/index.hbs
+++ b/addons/rose/tests/dummy/app/templates/index.hbs
@@ -1,21 +1,6 @@
-Default
-Default Outline
-Default with Icon
-
-Light
-Dark
-
-Informational
-Informational Outline
-
-Alert
-Alert Outline
-
-Error
-Error Outline
-
-Success
-Success Outline
-
-Warning
-Warning Outline
+
+
+ Key
+ Value
+
+
diff --git a/addons/rose/tests/dummy/app/templates/playground.hbs b/addons/rose/tests/dummy/app/templates/playground.hbs
index cf74a1e48..e9ace3c02 100644
--- a/addons/rose/tests/dummy/app/templates/playground.hbs
+++ b/addons/rose/tests/dummy/app/templates/playground.hbs
@@ -33,7 +33,7 @@
-
+
Item Name
@@ -44,7 +44,51 @@
- Body content
+
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+
+
+
+ Key
+
+
+
+
+
+ Add
+
+
+
+
+
+
+
+ Key
+
+
+
+
+
+ Remove
+
+
+
+
+
+
diff --git a/addons/rose/tests/integration/components/rose/list/key-value-test.js b/addons/rose/tests/integration/components/rose/list/key-value-test.js
new file mode 100644
index 000000000..086bf9b64
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/list/key-value-test.js
@@ -0,0 +1,30 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, findAll } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | rose/list/key-value', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function(assert) {
+ assert.expect(2);
+ await render(hbs`
+
+
+ Key
+ Value
+
+
+ Key
+ Value
+
+
+ `);
+ const itemLabelledBy = find('.rose-list-key-value-item:first-child').getAttribute('aria-labelledby');
+ assert.equal(findAll('.rose-list-key-value-item').length, 2, 'it has two items');
+ assert.ok(
+ find(`.rose-list-key-value-item:first-child > .rose-list-key-value-item-cell:first-child#${itemLabelledBy}`),
+ 'The key cell has an ID associated with the `aria-labelledby` attribute of its parent item element'
+ );
+ });
+});