diff --git a/app/components/royal-table.hbs b/app/components/royal-table.hbs
new file mode 100644
index 0000000..e35b5d4
--- /dev/null
+++ b/app/components/royal-table.hbs
@@ -0,0 +1,25 @@
+
+ {{#if @caption}}
+
+ {{@caption}}
+
+ {{/if}}
+
+ {{yield
+ (component
+ 'royal-table/row'
+ place='head'
+ onClickHead=@onClickHead
+ onSort=@onSort
+ sortingProp=@sortingProp
+ sortingIconComponent=@sortingIconComponent
+ parent=this
+ )
+ }}
+
+
+ {{#each @data as |item index|}}
+ {{yield (component 'royal-table/row' place='body' data=item parent=this) item index}}
+ {{/each}}
+
+
diff --git a/app/components/royal-table.js b/app/components/royal-table.js
new file mode 100644
index 0000000..c495a4d
--- /dev/null
+++ b/app/components/royal-table.js
@@ -0,0 +1,17 @@
+import Component from '@glimmer/component';
+
+export default class RoyalTableComponent extends Component {
+ columns = [];
+
+ registerColumn(column) {
+ if (!this.columns.includes(column)) {
+ this.columns.push(column);
+ }
+ }
+
+ unregisterColumn(column) {
+ if (this.columns.includes(column)) {
+ this.columns = this.columns.filter((c) => c !== column);
+ }
+ }
+}
diff --git a/app/components/royal-table/column.hbs b/app/components/royal-table/column.hbs
new file mode 100644
index 0000000..b9650b3
--- /dev/null
+++ b/app/components/royal-table/column.hbs
@@ -0,0 +1,46 @@
+{{#if this.isHead}}
+ {{! template-lint-disable no-invalid-interactive }}
+
+ {{#if @sortable}}
+ {{! TODO: add ensure-safe-component }}
+ {{#let
+ (if
+ @sortingIconComponent
+ (component @sortingIconComponent)
+ (component 'royal-table/column/sorting-icon')
+ ) as |SortingIcon|
+ }}
+
+ {{/let}}
+ {{/if}}
+ {{#if (has-block 'name')}}
+ {{yield to='name'}}
+ {{else}}
+ {{@name}}
+ {{/if}}
+ |
+{{else if this.isBody}}
+
+ {{#if (has-block)}}
+ {{yield}}
+ {{else if @bodyComponent}}
+ {{#let (component @bodyComponent) as |BodyComponent|}}
+
+ {{/let}}
+ {{else}}
+ {{get @data @prop}}
+ {{/if}}
+ |
+{{/if}}
diff --git a/app/components/royal-table/column.js b/app/components/royal-table/column.js
new file mode 100644
index 0000000..4b83ae6
--- /dev/null
+++ b/app/components/royal-table/column.js
@@ -0,0 +1,51 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+export default class RoyalTableColumnComponent extends Component {
+ constructor() {
+ super(...arguments);
+
+ if (this.isHead) {
+ this.args?.parent?.registerColumn(this);
+ }
+ }
+
+ willDestroy() {
+ super.willDestroy(...arguments);
+ this.args?.parent?.unregisterColumn(this);
+ }
+
+ get isHead() {
+ return this.args.place === 'head';
+ }
+
+ get isBody() {
+ return this.args.place === 'body';
+ }
+
+ get isSorted() {
+ return this.args.sortingProp && (this.isAscSorted || this.isDescSorted);
+ }
+
+ get isAscSorted() {
+ return this.args.sortingProp === `${this.args.prop}:asc`;
+ }
+
+ get isDescSorted() {
+ return this.args.sortingProp === `${this.args.prop}:desc`;
+ }
+
+ @action
+ handleClickHead(e) {
+ if (typeof this.args.onClickHead === 'function') {
+ this.args.onClickHead(this.args.prop, this, e);
+ }
+ }
+
+ @action
+ handleSort(e) {
+ if (this.args.sortable && typeof this.args.onSort === 'function') {
+ this.args.onSort(this.args.prop, this, e);
+ }
+ }
+}
diff --git a/app/components/royal-table/column/sorting-icon.hbs b/app/components/royal-table/column/sorting-icon.hbs
new file mode 100644
index 0000000..8a56eb0
--- /dev/null
+++ b/app/components/royal-table/column/sorting-icon.hbs
@@ -0,0 +1,11 @@
+
+ {{#if @sortable}}
+ {{#if @isAscSorted}}
+ 🠗
+ {{else if @isDescSorted}}
+ 🠕
+ {{else}}
+ ⬍
+ {{/if}}
+ {{/if}}
+
diff --git a/app/components/royal-table/row.hbs b/app/components/royal-table/row.hbs
new file mode 100644
index 0000000..09f098c
--- /dev/null
+++ b/app/components/royal-table/row.hbs
@@ -0,0 +1,23 @@
+
+ {{yield
+ (component
+ 'royal-table/column'
+ place=@place
+ prop=@prop
+ sortable=@sortable
+ onClickHead=@onClickHead
+ onSort=@onSort
+ sortingProp=@sortingProp
+ sortingIconComponent=@sortingIconComponent
+ data=@data
+ parent=@parent
+ )
+ }}
+
+{{#if (has-block 'expandedView')}}
+ {{#if @data.expanded}}
+
+ | {{yield to="expandedView"}} |
+
+ {{/if}}
+{{/if}}
diff --git a/app/components/royal-table/row.js b/app/components/royal-table/row.js
new file mode 100644
index 0000000..5bf02c7
--- /dev/null
+++ b/app/components/royal-table/row.js
@@ -0,0 +1,3 @@
+import Component from '@glimmer/component';
+
+export default class RoyalTableRowComponent extends Component {}