@aquera/nile-elements 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +16 -0
  2. package/demo/nxtgen-classes.css +2 -2
  3. package/demo/nxtgen-properties.css +2 -2
  4. package/demo/nxtgen-utilities.css +4 -4
  5. package/demo/nxtgen.css +2 -2
  6. package/dist/index.cjs.js +1 -1
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.js +408 -271
  9. package/dist/nile-empty-state/nile-empty-state.cjs.js +1 -1
  10. package/dist/nile-empty-state/nile-empty-state.cjs.js.map +1 -1
  11. package/dist/nile-empty-state/nile-empty-state.css.cjs.js +1 -1
  12. package/dist/nile-empty-state/nile-empty-state.css.cjs.js.map +1 -1
  13. package/dist/nile-empty-state/nile-empty-state.css.esm.js +129 -6
  14. package/dist/nile-empty-state/nile-empty-state.esm.js +45 -31
  15. package/dist/nile-pagination/nile-pagination.cjs.js +1 -1
  16. package/dist/nile-pagination/nile-pagination.cjs.js.map +1 -1
  17. package/dist/nile-pagination/nile-pagination.esm.js +25 -25
  18. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js +1 -1
  19. package/dist/nile-wysiwyg-editor/engine/create-state.esm.js +1 -1
  20. package/dist/nile-wysiwyg-editor/engine/index.cjs.js +1 -1
  21. package/dist/nile-wysiwyg-editor/engine/index.esm.js +1 -1
  22. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js +1 -1
  23. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js.map +1 -1
  24. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.esm.js +1 -1
  25. package/dist/nile-wysiwyg-editor/index.cjs.js +1 -1
  26. package/dist/nile-wysiwyg-editor/index.esm.js +1 -1
  27. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js +1 -1
  28. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +1 -1
  29. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js +1 -1
  30. package/dist/nile-wysiwyg-editor/ui/code-block-menu.esm.js +1 -1
  31. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js +1 -1
  32. package/dist/nile-wysiwyg-editor/ui/source-view.esm.js +1 -1
  33. package/dist/src/nile-empty-state/nile-empty-state.css.js +127 -4
  34. package/dist/src/nile-empty-state/nile-empty-state.css.js.map +1 -1
  35. package/dist/src/nile-empty-state/nile-empty-state.d.ts +35 -5
  36. package/dist/src/nile-empty-state/nile-empty-state.js +159 -42
  37. package/dist/src/nile-empty-state/nile-empty-state.js.map +1 -1
  38. package/dist/src/nile-empty-state/nile-empty-state.test.d.ts +1 -0
  39. package/dist/src/nile-empty-state/nile-empty-state.test.js +186 -15
  40. package/dist/src/nile-empty-state/nile-empty-state.test.js.map +1 -1
  41. package/dist/src/nile-pagination/nile-pagination.js +5 -5
  42. package/dist/src/nile-pagination/nile-pagination.js.map +1 -1
  43. package/dist/src/version.js +1 -1
  44. package/dist/src/version.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/dist/xml-09119317.cjs.js +2 -0
  47. package/dist/xml-09119317.cjs.js.map +1 -0
  48. package/dist/{xml-25c4108a.esm.js → xml-9bf771b3.esm.js} +1 -1
  49. package/package.json +1 -1
  50. package/src/nile-empty-state/nile-empty-state.css.ts +127 -4
  51. package/src/nile-empty-state/nile-empty-state.test.ts +192 -15
  52. package/src/nile-empty-state/nile-empty-state.ts +195 -34
  53. package/src/nile-pagination/nile-pagination.ts +5 -5
  54. package/vscode-html-custom-data.json +79 -16
  55. package/dist/xml-90fd974e.cjs.js +0 -2
  56. package/dist/xml-90fd974e.cjs.js.map +0 -1
@@ -1,5 +1,9 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
2
2
  import './nile-empty-state';
3
+ // nile-glyph lives in its own package and is NOT registered by nile-elements —
4
+ // the component documents it as a @dependency. Register it here or every glyph
5
+ // assertion below would be querying an element that never upgrades.
6
+ import '@aquera/nile-glyph/nile-glyph';
3
7
  import type { NileEmptyState } from './nile-empty-state';
4
8
 
5
9
  describe('NileEmptyState', () => {
@@ -10,8 +14,7 @@ describe('NileEmptyState', () => {
10
14
  it('5. variant defaults tonal', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.variant).to.equal('tonal'); });
11
15
  it('6. text default', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.text).to.equal('Empty State'); });
12
16
  it('7. subText default', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.subText).to.equal('No Data'); });
13
- it('8. grayscale defaults false', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.grayscale).to.be.false; });
14
- it('9. svgIconSize defaults 40', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.svgIconSize).to.equal(40); });
17
+ it('9. svgIconSize unset by default', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.svgIconSize).to.be.undefined; });
15
18
  it('10. container part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="container"]')).to.exist; });
16
19
  it('11. body part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="body"]')).to.exist; });
17
20
  it('12. text part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="text"]')).to.exist; });
@@ -27,9 +30,9 @@ describe('NileEmptyState', () => {
27
30
  it('22. flat body', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__body--flat')).to.exist; });
28
31
  it('23. text displayed', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state text="Nothing here"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__text')!.textContent).to.contain('Nothing here'); });
29
32
  it('24. subText displayed', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state sub-text="Try again"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__subtext')!.textContent).to.contain('Try again'); });
30
- it('25. nile-icon in tonal', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('nile-icon')).to.exist; });
31
- it('26. nile-icon in content', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('nile-icon')).to.exist; });
32
- it('27. nile-icon in flat', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('nile-icon')).to.exist; });
33
+ it('25. nile-glyph in tonal', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('nile-glyph')).to.exist; });
34
+ it('26. nile-glyph in content', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('nile-glyph')).to.exist; });
35
+ it('27. nile-glyph in flat', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('nile-glyph')).to.exist; });
33
36
  it('28. slot', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('slot')).to.exist; });
34
37
  it('29. slotted content', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state><button>Action</button></nile-empty-state>`); expect(el.querySelector('button')).to.exist; });
35
38
  it('30. dynamic text', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.text = 'New Text'; await el.updateComplete; expect(el.shadowRoot!.querySelector('.empty-state__text')!.textContent).to.contain('New Text'); });
@@ -62,26 +65,25 @@ describe('NileEmptyState', () => {
62
65
  it('57. localName', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.localName).to.equal('nile-empty-state'); });
63
66
  it('58. namespaceURI', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.namespaceURI).to.equal('http://www.w3.org/1999/xhtml'); });
64
67
  it('59. ownerDocument', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.ownerDocument).to.equal(document); });
65
- it('60. icon default', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.icon).to.contain('nile-icon-error'); });
68
+ it('60. icon default', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.icon).to.equal('ng-x-circle'); });
66
69
  it('61. custom icon', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state icon="star"></nile-empty-state>`); expect(el.icon).to.equal('star'); });
67
70
  it('62. actions class', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state-actions')).to.exist; });
68
71
  it('63. empty-state__body class', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__body')).to.exist; });
69
72
  it('64. text-section class', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty__state__text-section')).to.exist; });
70
73
  it('65. sub-text attr', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state sub-text="Nope"></nile-empty-state>`); expect(el.subText).to.equal('Nope'); });
71
- it('66. tonal-body part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="tonal-body"]')).to.exist; });
74
+ it('66. tonal-body part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part~="tonal-body"]')).to.exist; });
72
75
  it('67. content-image part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="content-image"]')).to.exist; });
73
76
  it('68. flat-body part', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="flat-body"]')).to.exist; });
74
77
  it('69. no tonal in flat', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__body--tonal')).to.be.null; });
75
78
  it('70. no content in tonal', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__body--content')).to.be.null; });
76
79
  it('71. no flat in content', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__body--flat')).to.be.null; });
77
- it('72. icon-wrapper part content', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="icon-wrapper"]')).to.exist; });
80
+ it('72. icon-wrapper part content', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part~="icon-wrapper"]')).to.exist; });
78
81
  it('73. classList add', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.classList.add('z'); expect(el.classList.contains('z')).to.be.true; });
79
82
  it('74. dataset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state data-idx="0"></nile-empty-state>`); expect(el.dataset.idx).to.equal('0'); });
80
83
  it('75. getBoundingClientRect', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.getBoundingClientRect()).to.exist; });
81
84
  it('76. size type string', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(typeof el.size).to.equal('string'); });
82
85
  it('77. variant type string', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(typeof el.variant).to.equal('string'); });
83
- it('78. grayscale type boolean', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(typeof el.grayscale).to.equal('boolean'); });
84
- it('79. svgIconSize type number', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(typeof el.svgIconSize).to.equal('number'); });
86
+ it('79. svgIconSize type number once set', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state .svgIconSize=${60}></nile-empty-state>`); expect(typeof el.svgIconSize).to.equal('number'); });
85
87
  it('80. createElement', async () => { const el = document.createElement('nile-empty-state') as NileEmptyState; document.body.appendChild(el); await el.updateComplete; expect(el.shadowRoot).to.not.be.null; document.body.removeChild(el); });
86
88
  it('81. aria-label', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state aria-label="Empty"></nile-empty-state>`); expect(el.getAttribute('aria-label')).to.equal('Empty'); });
87
89
  it('82. role', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state role="status"></nile-empty-state>`); expect(el.getAttribute('role')).to.equal('status'); });
@@ -98,9 +100,184 @@ describe('NileEmptyState', () => {
98
100
  it('93. dispatchCustomEvent', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); let d: any; el.addEventListener('t', ((e: CustomEvent) => { d = e.detail; }) as EventListener); el.dispatchEvent(new CustomEvent('t', { detail: 'x' })); expect(d).to.equal('x'); });
99
101
  it('94. scrollIntoView', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.scrollIntoView).to.be.a('function'); });
100
102
  it('95. focus method', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.focus).to.be.a('function'); });
101
- it('96. dynamic icon', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); el.icon = 'star'; await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-icon'); expect(icon!.getAttribute('name')).to.equal('star'); });
102
- it('97. svgIconUrl renders custom', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal" svgIconUrl="/path/icon.svg"></nile-empty-state>`); await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-icon'); expect(icon!.getAttribute('customsvgpath')).to.exist; });
103
- it('98. svgIconSize applies', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal" svgIconUrl="/p.svg" .svgIconSize=${60}></nile-empty-state>`); await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-icon'); expect(icon!.getAttribute('size')).to.equal('60'); });
103
+ it('96. dynamic icon', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); el.icon = 'star'; await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-glyph'); expect(icon!.getAttribute('name')).to.equal('star'); });
104
+ it('97. svgIconUrl renders custom', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal" svgIconUrl="/path/icon.svg"></nile-empty-state>`); await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-glyph'); expect(icon!.getAttribute('customsvgpath')).to.exist; });
105
+ it('98. svgIconSize applies', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal" svgIconUrl="/p.svg" .svgIconSize=${60}></nile-empty-state>`); await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-glyph'); expect(icon!.getAttribute('size')).to.equal('60'); });
104
106
  it('99. shadow childNodes', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.childNodes.length).to.be.greaterThan(0); });
105
- it('100. full integration', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state text="Empty" sub-text="Nothing" variant="content" size="lg" icon="star" class="e" id="e1"><button>Retry</button></nile-empty-state>`); expect(el.text).to.equal('Empty'); expect(el.subText).to.equal('Nothing'); expect(el.variant).to.equal('content'); expect(el.size).to.equal('lg'); expect(el.icon).to.equal('star'); expect(el.id).to.equal('e1'); expect(el.querySelector('button')).to.exist; expect(el.shadowRoot!.querySelector('.empty-state--lg')).to.exist; });
107
+ it('100. legacy full integration', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state text="Empty" sub-text="Nothing" variant="content" size="lg" icon="star" class="e" id="e1"><button>Retry</button></nile-empty-state>`); expect(el.text).to.equal('Empty'); expect(el.subText).to.equal('Nothing'); expect(el.variant).to.equal('content'); expect(el.size).to.equal('lg'); expect(el.icon).to.equal('star'); expect(el.id).to.equal('e1'); expect(el.querySelector('button')).to.exist; expect(el.shadowRoot!.querySelector('.empty-state--lg')).to.exist; });
108
+
109
+ // ── status presets ────────────────────────────────────────────────────────
110
+
111
+ const container = (el: NileEmptyState) => el.shadowRoot!.querySelector('[part="container"]')!;
112
+ const glyph = (el: NileEmptyState) => el.shadowRoot!.querySelector('nile-glyph')!;
113
+
114
+ it('101. status unset by default', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.status).to.be.undefined; });
115
+ it('102. status=empty copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect(el.text).to.equal('No data yet'); expect(el.subText).to.contain('Rows appear here'); });
116
+ it('103. status=empty icon', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect(el.icon).to.equal('ng-database'); });
117
+ it('104. status=no-results copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="no-results"></nile-empty-state>`); expect(el.text).to.equal('No matches'); expect(el.icon).to.equal('ng-search-lg'); });
118
+ it('105. status=not-found copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="not-found"></nile-empty-state>`); expect(el.text).to.contain(`couldn't find that`); expect(el.icon).to.equal('ng-file-06'); });
119
+ it('106. status=no-access copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="no-access"></nile-empty-state>`); expect(el.text).to.contain(`don't have access`); expect(el.icon).to.equal('ng-lock'); });
120
+ it('107. status=error copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); expect(el.text).to.equal('Something went wrong'); expect(el.icon).to.equal('ng-alert-circle'); });
121
+ it('108. status=offline copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="offline"></nile-empty-state>`); expect(el.text).to.equal('Connection lost'); expect(el.icon).to.equal('ng-cloud-off'); });
122
+ it('109. preset icon reaches the glyph', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect(glyph(el).getAttribute('name')).to.equal('ng-database'); });
123
+ it('110. preset renders the copy', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="no-results"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('.empty-state__text')!.textContent).to.contain('No matches'); });
124
+ it('111. status reflects', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); expect(el.getAttribute('status')).to.equal('error'); });
125
+ it('112. status change reruns the preset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); el.status = 'offline'; await el.updateComplete; expect(el.text).to.equal('Connection lost'); });
126
+
127
+ // ── per-property overrides beat the preset ────────────────────────────────
128
+
129
+ it('113. text overrides preset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty" text="Mine"></nile-empty-state>`); expect(el.text).to.equal('Mine'); expect(el.subText).to.contain('Rows appear here'); });
130
+ it('114. sub-text overrides preset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty" sub-text="Mine"></nile-empty-state>`); expect(el.subText).to.equal('Mine'); expect(el.text).to.equal('No data yet'); });
131
+ it('115. icon overrides preset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty" icon="star"></nile-empty-state>`); expect(el.icon).to.equal('star'); });
132
+ it('116. override survives a status change', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty" text="Mine"></nile-empty-state>`); el.status = 'error'; await el.updateComplete; expect(el.text).to.equal('Mine'); });
133
+
134
+ // ── tone ──────────────────────────────────────────────────────────────────
135
+
136
+ it('117. tone defaults neutral without status', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.tone).to.equal('neutral'); });
137
+ it('118. tone defaults from status', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="no-access"></nile-empty-state>`); expect(el.tone).to.equal('warning'); });
138
+ it('119. error status is error tone', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); expect(el.tone).to.equal('error'); });
139
+ it('120. tone overridable', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="no-access" tone="neutral"></nile-empty-state>`); expect(el.tone).to.equal('neutral'); });
140
+ it('121. tone class on container', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); expect(container(el).classList.contains('empty-state--tone-error')).to.be.true; });
141
+ it('122. neutral tone omits glyph color', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect(glyph(el).getAttribute('color')).to.be.null; });
142
+ it('123. warning tone sets glyph color', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="not-found"></nile-empty-state>`); expect(glyph(el).getAttribute('color')).to.contain('fg-warning-primary'); });
143
+
144
+ // ── ARIA ──────────────────────────────────────────────────────────────────
145
+
146
+ it('124. no live region without status', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(container(el).getAttribute('role')).to.be.null; expect(container(el).getAttribute('aria-live')).to.be.null; });
147
+ it('125. status gets role=status/polite', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect(container(el).getAttribute('role')).to.equal('status'); expect(container(el).getAttribute('aria-live')).to.equal('polite'); });
148
+ it('126. error gets role=alert/assertive', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); expect(container(el).getAttribute('role')).to.equal('alert'); expect(container(el).getAttribute('aria-live')).to.equal('assertive'); });
149
+ it('127. offline gets role=alert', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="offline"></nile-empty-state>`); expect(container(el).getAttribute('role')).to.equal('alert'); });
150
+ it('128. no-access does not interrupt', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="no-access"></nile-empty-state>`); expect(container(el).getAttribute('aria-live')).to.equal('polite'); });
151
+
152
+ // ── layout ────────────────────────────────────────────────────────────────
153
+
154
+ it('129. layout defaults stack', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.layout).to.equal('stack'); expect(container(el).classList.contains('empty-state--stack')).to.be.true; });
155
+ it('130. layout=inline class', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline"></nile-empty-state>`); expect(container(el).classList.contains('empty-state--inline')).to.be.true; });
156
+ it('131. layout=page class', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="page"></nile-empty-state>`); expect(container(el).classList.contains('empty-state--page')).to.be.true; });
157
+ it('132. layout reflects', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="page"></nile-empty-state>`); expect(el.getAttribute('layout')).to.equal('page'); });
158
+ it('133. inline shrinks the glyph to 20', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline"></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('20'); });
159
+ it('134. stack keeps the 40px tonal glyph', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('40'); });
160
+ it('135. inline drops the 512px ceiling', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline"></nile-empty-state>`); expect(getComputedStyle(container(el)).maxWidth).to.equal('none'); });
161
+ it('136. stack keeps the 512px ceiling', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(getComputedStyle(container(el)).maxWidth).to.equal('512px'); });
162
+ it('137. dynamic layout', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.layout = 'inline'; await el.updateComplete; expect(container(el).classList.contains('empty-state--inline')).to.be.true; });
163
+
164
+ // ── code ──────────────────────────────────────────────────────────────────
165
+
166
+ it('138. no code part when unset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="code"]')).to.be.null; });
167
+ it('139. code renders', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state code="Error 404"></nile-empty-state>`); expect(el.shadowRoot!.querySelector('[part="code"]')!.textContent).to.contain('Error 404'); });
168
+ it('140. code sits above the title', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state code="404"></nile-empty-state>`); const section = el.shadowRoot!.querySelector('[part="text-section"]')!; expect(section.firstElementChild!.getAttribute('part')).to.equal('code'); });
169
+ it('141. dynamic code', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.code = '500'; await el.updateComplete; expect(el.shadowRoot!.querySelector('[part="code"]')).to.exist; });
170
+
171
+ // ── bordered ──────────────────────────────────────────────────────────────
172
+
173
+ it('147. bordered defaults false', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(el.bordered).to.be.false; expect(container(el).classList.contains('empty-state--bordered')).to.be.false; });
174
+ it('148. bordered class', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state bordered></nile-empty-state>`); expect(container(el).classList.contains('empty-state--bordered')).to.be.true; });
175
+ // The --ng-* sheet isn't loaded here, so the token has to be supplied or the
176
+ // whole `border` shorthand is invalid at computed-value time and drops out.
177
+ it('149. bordered draws a dashed edge', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state bordered style="--ng-colors-border-primary:#D5D7DA"></nile-empty-state>`); expect(getComputedStyle(container(el)).borderTopStyle).to.equal('dashed'); });
178
+ it('150. bordered reflects', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state bordered></nile-empty-state>`); expect(el.hasAttribute('bordered')).to.be.true; });
179
+
180
+ // --bordered and --inline both set padding at one-class specificity, so on their
181
+ // own the winner would be whichever rule comes last in the stylesheet. The explicit
182
+ // .empty-state--inline.empty-state--bordered rule settles it; these pin that intent.
183
+ const SPACING = '--ng-spacing-xl:20px;--ng-spacing-lg:16px;--ng-spacing-4xl:96px;--ng-spacing-3xl:64px';
184
+ it('150b. bordered stack uses the roomy padding', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state bordered style=${SPACING}></nile-empty-state>`); const cs = getComputedStyle(container(el)); expect(cs.paddingTop).to.equal('96px'); expect(cs.paddingLeft).to.equal('64px'); });
185
+ it('150c. bordered inline keeps the tight padding', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" bordered style=${SPACING}></nile-empty-state>`); const cs = getComputedStyle(container(el)); expect(cs.paddingTop).to.equal('20px'); expect(cs.paddingLeft).to.equal('16px'); });
186
+ it('150d. bordered does not change inline padding', async () => { const plain = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" style=${SPACING}></nile-empty-state>`); const edged = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" bordered style=${SPACING}></nile-empty-state>`); expect(getComputedStyle(container(edged)).padding).to.equal(getComputedStyle(container(plain)).padding); });
187
+ it('150e. bordered inline still draws the dashed edge', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" bordered style="--ng-colors-border-primary:#D5D7DA"></nile-empty-state>`); expect(getComputedStyle(container(el)).borderTopStyle).to.equal('dashed'); });
188
+
189
+ // ── regressions the specimen sheet flagged ────────────────────────────────
190
+
191
+ it('151. content variant no longer forces white', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); expect(glyph(el).getAttribute('color')).to.not.equal('white'); });
192
+ // Was a literal `white`, which punched a white chip into any dark page. Feed
193
+ // the theme token a sentinel: if the chip picks it up, it is no longer fixed.
194
+ it('152. flat chip background tracks the theme token', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat" style="--ng-colors-bg-primary:rgb(1, 2, 3)"></nile-empty-state>`); const chip = el.shadowRoot!.querySelector('.nile-flat-type-icon')!; expect(getComputedStyle(chip).backgroundColor).to.equal('rgb(1, 2, 3)'); });
195
+ it('153. svgIconUrl still wins over a preset icon', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error" svgIconUrl="/p.svg"></nile-empty-state>`); expect(glyph(el).getAttribute('customsvgpath')).to.exist; expect(glyph(el).getAttribute('name')).to.be.null; });
196
+ it('154. svgIconColor beats the tone colour', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error" svgIconUrl="/p.svg" svgIconColor="#123456"></nile-empty-state>`); expect(glyph(el).getAttribute('color')).to.equal('#123456'); });
197
+ // ── the preset glyphs must actually exist ─────────────────────────────────
198
+
199
+ // nile-glyph strips a name's dashes, looks it up, and silently renders nothing
200
+ // when it misses — so a bad preset name is invisible rather than loud. These
201
+ // assert the glyph ACTUALLY DRAWS, which is stronger than checking the name
202
+ // against a barrel: it exercises the same resolution path the browser takes,
203
+ // and it cannot drift from it.
204
+ const STATUSES = ['empty', 'no-results', 'not-found', 'no-access', 'error', 'offline'] as const;
205
+
206
+ /** The <svg> nile-glyph drew, once it has resolved the name. */
207
+ const drawnSvg = async (el: NileEmptyState): Promise<SVGElement> => {
208
+ const g = glyph(el);
209
+ await waitUntil(
210
+ () => !!g.shadowRoot?.querySelector('svg'),
211
+ `glyph "${el.icon}" never drew — the name does not resolve in @aquera/nile-glyph`,
212
+ );
213
+ return g.shadowRoot!.querySelector('svg')!;
214
+ };
215
+
216
+ STATUSES.forEach((status, i) => {
217
+ it(`${155 + i}. status=${status} names a glyph that actually draws`, async () => {
218
+ const el = await fixture<NileEmptyState>(html`<nile-empty-state status="${status}"></nile-empty-state>`);
219
+ expect(await drawnSvg(el)).to.exist;
220
+ });
221
+ });
222
+
223
+ // nile-glyph resolves the var expression and reflects the resolved value back,
224
+ // so these assert the paint method that actually reaches the SVG. Every glyph the
225
+ // component draws is stroked now — preset or not, either theme.
226
+ it('161. preset glyphs are stroked, not filled', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect(glyph(el).getAttribute('method')).to.equal('stroke'); });
227
+ it('162. preset glyph artwork is an outline', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); expect((await drawnSvg(el)).getAttribute('fill')).to.equal('none'); });
228
+ it('163. a call site with no status is stroked too', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); expect(glyph(el).getAttribute('method')).to.equal('stroke'); });
229
+ it('163b. content variant strokes a preset glyph', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content" status="empty"></nile-empty-state>`); expect(glyph(el).getAttribute('method')).to.equal('stroke'); });
230
+ it('163c. content variant strokes a caller icon as well', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content" icon="star"></nile-empty-state>`); expect(glyph(el).getAttribute('method')).to.equal('stroke'); });
231
+ it('164. overriding the preset icon does not change the paint method', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty" icon="star"></nile-empty-state>`); expect(glyph(el).getAttribute('method')).to.equal('stroke'); });
232
+
233
+ // ── nile-glyph + ::part(icon-chrome) ──────────────────────────────────────
234
+ //
235
+ // The component draws through nile-glyph, not nile-icon: nile-icon resolves
236
+ // against a vendored subset and renders nothing when a name misses it, which
237
+ // is what held the presets on near-miss names.
238
+
239
+ const SHAPES: ReadonlyArray<readonly [string, string]> = [
240
+ ['variant', 'tonal'], ['variant', 'flat'], ['variant', 'content'], ['layout', 'inline'],
241
+ ];
242
+ const partsOf = (el: NileEmptyState, sel: string) =>
243
+ (el.shadowRoot!.querySelector(sel)!.getAttribute('part') ?? '').split(/\s+/);
244
+
245
+ it('166. never renders a nile-icon in any shape', async () => { for (const [name, value] of SHAPES) { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.setAttribute(name, value); await el.updateComplete; expect(el.shadowRoot!.querySelector('nile-icon'), `${name}="${value}"`).to.be.null; } });
246
+ it('167. renders exactly one glyph per shape', async () => { for (const [name, value] of SHAPES) { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.setAttribute(name, value); await el.updateComplete; expect(el.shadowRoot!.querySelectorAll('[part="body"] nile-glyph').length, `${name}="${value}"`).to.equal(1); } });
247
+ it('169. every shape exposes exactly one icon-chrome element', async () => { for (const [name, value] of SHAPES) { const el = await fixture<NileEmptyState>(html`<nile-empty-state></nile-empty-state>`); el.setAttribute(name, value); await el.updateComplete; expect(el.shadowRoot!.querySelectorAll('[part~="icon-chrome"]').length, `${name}="${value}"`).to.equal(1); } });
248
+ it('170. tonal disc carries icon-chrome alongside tonal-body', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal"></nile-empty-state>`); const p = partsOf(el, '.empty-state__body--tonal'); expect(p).to.include('icon-chrome'); expect(p).to.include('tonal-body'); });
249
+ it('171. flat tile carries icon-chrome, its wrapper does not', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(partsOf(el, '.nile-flat-type-icon')).to.include('icon-chrome'); expect(partsOf(el, '.empty-state__body--flat')).to.not.include('icon-chrome'); });
250
+ it('172. content icon box carries icon-chrome alongside icon-wrapper', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content"></nile-empty-state>`); const p = partsOf(el, '.empty-state__img__icon'); expect(p).to.include('icon-chrome'); expect(p).to.include('icon-wrapper'); });
251
+ it('173. inline exposes a bare glyph box, no variant chrome', async () => { for (const v of ['tonal', 'flat', 'content'] as const) { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" variant=${v}></nile-empty-state>`); const p = partsOf(el, '.empty-state__body--inline'); expect(p, v).to.include('inline-icon'); expect(p, v).to.include('icon-chrome'); expect(el.shadowRoot!.querySelector('.empty-state__body--tonal'), v).to.be.null; expect(el.shadowRoot!.querySelector('.nile-flat-type-icon'), v).to.be.null; expect(el.shadowRoot!.querySelector('.empty-state__img__icon'), v).to.be.null; } });
252
+ it('174. inline still honours the status preset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" status="no-results"></nile-empty-state>`); expect(glyph(el).getAttribute('name')).to.equal('ng-search-lg'); expect(container(el).getAttribute('role')).to.equal('status'); });
253
+ it('175. the inert nile-icon variant attribute is gone from the flat tile', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat"></nile-empty-state>`); expect(glyph(el).hasAttribute('variant')).to.be.false; });
254
+
255
+ // ── svgIconColor beats the tone, so chrome and glyph can differ ───────────
256
+
257
+ it('176. svgIconColor colours a catalogue glyph', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state icon="ng-search-lg" svgIconColor="#6941C6"></nile-empty-state>`); expect(glyph(el).getAttribute('color')).to.equal('#6941C6'); });
258
+ it('177. svgIconColor beats the tone colour', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error" svgIconColor="#6941C6"></nile-empty-state>`); expect(el.tone).to.equal('error'); expect(glyph(el).getAttribute('color')).to.equal('#6941C6'); });
259
+ it('178. without svgIconColor the tone still decides', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); expect(glyph(el).getAttribute('color')).to.contain('--ng-colors-fg-error-primary'); });
260
+ it('179. svgIconColor applies in every shape', async () => { for (const [name, value] of SHAPES) { const el = await fixture<NileEmptyState>(html`<nile-empty-state svgIconColor="#6941C6"></nile-empty-state>`); el.setAttribute(name, value); await el.updateComplete; expect(glyph(el).getAttribute('color'), `${name}="${value}"`).to.equal('#6941C6'); } });
261
+
262
+ // The resolving accessors must report a resolved oldValue: Lit reads the new value
263
+ // back through the getter, so capturing the raw backing field would compare an
264
+ // undefined against a resolved value.
265
+ it('180. assigning the already-resolved value schedules nothing', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); el.icon = el.icon; expect((el as any).isUpdatePending).to.be.false; });
266
+ it('181. same for text and tone', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="error"></nile-empty-state>`); el.text = el.text; el.tone = el.tone; expect((el as any).isUpdatePending).to.be.false; });
267
+ it('182. a real change still schedules', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); el.icon = 'star'; expect((el as any).isUpdatePending).to.be.true; await el.updateComplete; expect(glyph(el).getAttribute('name')).to.equal('star'); });
268
+ it('183. changedProperties reports the resolved old value', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty"></nile-empty-state>`); let seen: unknown = 'unset'; (el as any).willUpdate = (c: Map<string, unknown>) => { if (c.has('icon')) seen = c.get('icon'); }; el.icon = 'star'; await el.updateComplete; expect(seen).to.equal('ng-database'); });
269
+ it('184. removing the attribute falls back to the preset', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="empty" icon="star"></nile-empty-state>`); el.removeAttribute('icon'); await el.updateComplete; expect(el.icon).to.equal('ng-database'); });
270
+
271
+ // svgIconSize overrides the layout-driven size rather than replacing it, so a
272
+ // custom SVG tracks the shape it is drawn in unless the caller says otherwise.
273
+ it('185. custom SVG follows the inline size', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" svgIconUrl="/p.svg"></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('20'); });
274
+ it('186. custom SVG follows the tonal size', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="tonal" svgIconUrl="/p.svg"></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('40'); });
275
+ it('187. custom SVG follows the flat size', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat" svgIconUrl="/p.svg"></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('24'); });
276
+ it('188. custom SVG follows the flat lg size', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="flat" size="lg" svgIconUrl="/p.svg"></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('28'); });
277
+ it('189. custom SVG follows the content size', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state variant="content" svgIconUrl="/p.svg"></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('24'); });
278
+ it('190. svgIconSize overrides the layout size', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" svgIconUrl="/p.svg" .svgIconSize=${60}></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('60'); });
279
+ it('191. svgIconSize does not affect a catalogue glyph', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state layout="inline" .svgIconSize=${60}></nile-empty-state>`); expect(glyph(el).getAttribute('size')).to.equal('20'); });
280
+
281
+
282
+ it('165. full new-API integration', async () => { const el = await fixture<NileEmptyState>(html`<nile-empty-state status="not-found" layout="page" size="lg" code="Error 404" bordered><button>Go back</button></nile-empty-state>`); expect(el.text).to.contain(`couldn't find that`); expect(el.tone).to.equal('warning'); expect(container(el).getAttribute('role')).to.equal('status'); expect(container(el).classList.contains('empty-state--page')).to.be.true; expect(container(el).classList.contains('empty-state--bordered')).to.be.true; expect(el.shadowRoot!.querySelector('[part="code"]')!.textContent).to.contain('Error 404'); expect(el.querySelector('button')).to.exist; });
106
283
  });
@@ -6,11 +6,74 @@
6
6
  */
7
7
 
8
8
  import { html, nothing, CSSResultArray, TemplateResult } from 'lit';
9
- import { customElement, property, state } from 'lit/decorators.js';
9
+ import { customElement, property } from 'lit/decorators.js';
10
10
  import { classMap } from 'lit/directives/class-map.js';
11
11
  import { styles } from './nile-empty-state.css';
12
12
  import NileElement from '../internal/nile-element';
13
13
  import { ifDefined } from 'lit/directives/if-defined.js';
14
+
15
+ /** What happened. Selects an icon, tone, default copy and ARIA role. */
16
+ export type NileEmptyStateStatus =
17
+ | 'empty'
18
+ | 'no-results'
19
+ | 'not-found'
20
+ | 'no-access'
21
+ | 'error'
22
+ | 'offline';
23
+
24
+ /** How loud the state looks. Defaults from `status`, always overridable. */
25
+ export type NileEmptyStateTone = 'neutral' | 'info' | 'warning' | 'error';
26
+
27
+ /** What shape the state occupies. */
28
+ export type NileEmptyStateLayout = 'stack' | 'inline' | 'page';
29
+
30
+ interface StatusPreset {
31
+ icon: string;
32
+ tone: NileEmptyStateTone;
33
+ text: string;
34
+ subText: string;
35
+ role: 'status' | 'alert';
36
+ live: 'polite' | 'assertive';
37
+ }
38
+
39
+ const STATUS_PRESETS: Record<NileEmptyStateStatus, StatusPreset> = {
40
+ 'empty': {
41
+ icon: 'ng-database', tone: 'neutral', role: 'status', live: 'polite',
42
+ text: 'No data yet', subText: `Rows appear here once there's something to show.`,
43
+ },
44
+ 'no-results': {
45
+ icon: 'ng-search-lg', tone: 'neutral', role: 'status', live: 'polite',
46
+ text: 'No matches', subText: 'Try removing a filter or broadening your search.',
47
+ },
48
+ 'not-found': {
49
+ icon: 'ng-file-06', tone: 'warning', role: 'status', live: 'polite',
50
+ text: `We couldn't find that`, subText: 'It may have been moved or deleted.',
51
+ },
52
+ 'no-access': {
53
+ icon: 'ng-lock', tone: 'warning', role: 'status', live: 'polite',
54
+ text: `You don't have access`, subText: 'Ask an admin to grant you access to this resource.',
55
+ },
56
+ 'error': {
57
+ icon: 'ng-alert-circle', tone: 'error', role: 'alert', live: 'assertive',
58
+ text: 'Something went wrong', subText: `We couldn't load this. Try again.`,
59
+ },
60
+ 'offline': {
61
+ icon: 'ng-cloud-off', tone: 'error', role: 'alert', live: 'assertive',
62
+ text: 'Connection lost', subText: 'Check your connection and try again.',
63
+ },
64
+ };
65
+
66
+ const TONE_GLYPH_COLOR: Record<NileEmptyStateTone, string | undefined> = {
67
+ neutral: undefined,
68
+ info: 'var(--nile-colors-primary-600, var(--ng-colors-fg-brand-primary-600))',
69
+ warning: 'var(--nile-colors-yellow-700, var(--ng-colors-fg-warning-primary))',
70
+ error: 'var(--nile-colors-red-700, var(--ng-colors-fg-error-primary))',
71
+ };
72
+
73
+ const DEFAULT_ICON = 'ng-x-circle';
74
+ const DEFAULT_TEXT = 'Empty State';
75
+ const DEFAULT_SUB_TEXT = 'No Data';
76
+
14
77
  /**
15
78
  * Nile empty-state component.
16
79
  *
@@ -28,78 +91,175 @@ export class NileEmptyState extends NileElement {
28
91
  return [styles];
29
92
  }
30
93
 
31
- @property() size: 'sm' | 'md' | 'lg' = 'md';
94
+ @property({ type: String, attribute: true }) size: 'sm' | 'md' | 'lg' = 'md';
32
95
 
33
- @property() variant:
96
+ @property({ type: String, attribute: true }) variant:
34
97
  | 'flat'
35
98
  | 'content'
36
99
  | 'tonal' = 'tonal';
37
100
 
38
- @property() icon: string = 'var(--nile-icon-error, var(--ng-icon-x-circle))';
39
- @property({type:Boolean}) grayscale: boolean = false;
101
+ @property({ type: String, attribute: true, reflect: true }) status?: NileEmptyStateStatus;
102
+ @property({ type: String, attribute: true, reflect: true }) layout: NileEmptyStateLayout = 'stack';
103
+ @property({ type: String, attribute: true }) code?: string;
104
+ @property({ type: Boolean, attribute: true, reflect: true }) bordered: boolean = false;
105
+ @property({ type: String, attribute: true }) svgIconUrl: string;
106
+ /**
107
+ * Overrides the layout-driven glyph size for a custom `svgIconUrl`. Left unset,
108
+ * the SVG matches whatever size the current variant/layout draws at.
109
+ */
110
+ @property({ type: Number, attribute: true }) svgIconSize?: number;
111
+ @property({ type: String, attribute: true }) svgIconColor: string;
112
+
113
+ private _icon?: string;
114
+ private _text?: string;
115
+ private _subText?: string;
116
+ private _tone?: NileEmptyStateTone;
117
+ @property({ type: String, attribute: true })
118
+ get icon(): string {
119
+ return this._icon ?? this.preset?.icon ?? DEFAULT_ICON;
120
+ }
121
+
122
+ set icon(value: string | null) {
123
+ const old = this.icon;
124
+ this._icon = value ?? undefined;
125
+ this.requestUpdate('icon', old);
126
+ }
127
+
128
+ /** Headline. Resolves through the `status` preset to `DEFAULT_TEXT`. */
129
+ @property({ type: String, attribute: true })
130
+ get text(): string {
131
+ return this._text ?? this.preset?.text ?? DEFAULT_TEXT;
132
+ }
40
133
 
41
- @property() text: String = 'Empty State';
42
- @property({attribute:'sub-text'}) subText: String = 'No Data';
43
- @property() svgIconUrl: string;
44
- @property({type: Number}) svgIconSize: number = 40;
45
- @property({type: String}) svgIconColor: string;
134
+ set text(value: string | null) {
135
+ const old = this.text;
136
+ this._text = value ?? undefined;
137
+ this.requestUpdate('text', old);
138
+ }
139
+
140
+ /** Supporting line. Resolves through the `status` preset to `DEFAULT_SUB_TEXT`. */
141
+ @property({ type: String, attribute: 'sub-text' })
142
+ get subText(): string {
143
+ return this._subText ?? this.preset?.subText ?? DEFAULT_SUB_TEXT;
144
+ }
145
+
146
+ set subText(value: string | null) {
147
+ const old = this.subText;
148
+ this._subText = value ?? undefined;
149
+ this.requestUpdate('subText', old);
150
+ }
151
+
152
+ @property({ type: String, attribute: true, reflect: true })
153
+ get tone(): NileEmptyStateTone {
154
+ return this._tone ?? this.preset?.tone ?? 'neutral';
155
+ }
156
+
157
+ set tone(value: NileEmptyStateTone | null) {
158
+ const old = this.tone;
159
+ this._tone = value ?? undefined;
160
+ this.requestUpdate('tone', old);
161
+ }
46
162
 
47
163
  /* #endregion */
48
164
 
49
165
  /* #region Methods */
50
166
 
167
+ /** The preset backing the current `status`, or undefined when none is set. */
168
+ private get preset(): StatusPreset | undefined {
169
+ return this.status ? STATUS_PRESETS[this.status] : undefined;
170
+ }
171
+
172
+ private get glyphColor(): string | undefined {
173
+ return this.svgIconColor ?? TONE_GLYPH_COLOR[this.tone];
174
+ }
175
+
176
+ /** ARIA wiring */
177
+ private get liveRegion(): { role: string; live: string } | undefined {
178
+ const preset = this.preset;
179
+ if (!preset) return undefined;
180
+ return { role: preset.role, live: preset.live };
181
+ }
182
+
183
+ private renderGlyph(size: number | string): TemplateResult {
184
+ const color = this.glyphColor;
185
+ return this.svgIconUrl ? html`
186
+ <nile-glyph
187
+ customSvgPath="${this.svgIconUrl}"
188
+ size="${this.svgIconSize ?? size}"
189
+ method="stroke"
190
+ color="${ifDefined(color)}"
191
+ noFill
192
+ ></nile-glyph>
193
+ ` : html`
194
+ <nile-glyph
195
+ name="${this.icon}"
196
+ method="stroke"
197
+ size="${size}"
198
+ color="${ifDefined(color)}"
199
+ ></nile-glyph>
200
+ `;
201
+ }
202
+
51
203
  /**
52
204
  * Render method
53
205
  * @slot This is a slot test
54
206
  */
55
207
  public render(): TemplateResult {
208
+ const live = this.liveRegion;
209
+ const inline = this.layout === 'inline';
210
+
56
211
  return html`
57
212
  <div part="container" class="${classMap({
58
213
  'empty-state': true,
59
214
  'empty-state--sm': this.size == 'sm',
60
215
  'empty-state--md': this.size == 'md',
61
- 'empty-state--lg': this.size == 'lg'
62
- })}">
216
+ 'empty-state--lg': this.size == 'lg',
217
+ 'empty-state--stack': this.layout === 'stack',
218
+ 'empty-state--inline': inline,
219
+ 'empty-state--page': this.layout === 'page',
220
+ 'empty-state--bordered': this.bordered,
221
+ [`empty-state--tone-${this.tone}`]: true,
222
+ })}"
223
+ role="${ifDefined(live?.role)}"
224
+ aria-live="${ifDefined(live?.live)}"
225
+ >
63
226
  <div part="body" class="${classMap({ 'empty-state__body': true })}">
64
227
 
65
- ${this.variant == 'content' ? html`
228
+ ${inline ? html`
229
+ <span part="inline-icon icon-chrome" class="empty-state__body--inline">
230
+ ${this.renderGlyph(20)}
231
+ </span>` : nothing
232
+ }
233
+
234
+ ${!inline && this.variant == 'content' ? html`
66
235
  <div part="content-image" class="empty-state__body--content">
67
236
  ${!this.icon && !this.svgIconUrl ? nothing : html`
68
- <div part="icon-wrapper" class="empty-state__img__icon">
69
- ${this.svgIconUrl ? html`
70
- <nile-icon customSvgPath="${this.svgIconUrl}" size="${this.svgIconSize}" color="${ifDefined(this.svgIconColor)}" noFill></nile-icon>
71
- <!-- <nile-icon customSvgPath="${this.svgIconUrl}" size="${this.svgIconSize}" color="${this.svgIconColor ? this.svgIconColor : '#ddd'}"></nile-icon> -->
72
- ` : html`
73
- <nile-icon name="${this.icon}" color="white" size="${this.size == 'sm' ? 24 : this.size == 'md' ? 24 : 28}"></nile-icon>
74
- `}
237
+ <div part="icon-wrapper icon-chrome" class="empty-state__img__icon">
238
+ ${
239
+ this.renderGlyph(this.size == 'lg' ? 28 : 24)}
75
240
  </div>`
76
241
  }
77
242
  </div>`: nothing
78
243
  }
79
244
 
80
- ${this.variant == 'tonal' ? html`
81
- <div part="tonal-body" class="empty-state__body--tonal">
82
- ${this.svgIconUrl ? html`
83
- <nile-icon customSvgPath="${this.svgIconUrl}" size="${this.svgIconSize}" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" color="${ifDefined(this.svgIconColor)}" noFill></nile-icon>
84
- ` : html`
85
- <nile-icon name="${this.icon}" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" size="40"></nile-icon>
86
- `}
245
+ ${!inline && this.variant == 'tonal' ? html`
246
+ <div part="tonal-body icon-chrome" class="empty-state__body--tonal">
247
+ ${this.renderGlyph(40)}
87
248
  </div>`: nothing
88
249
  }
89
250
 
90
- ${this.variant == 'flat' ? html`
251
+ ${!inline && this.variant == 'flat' ? html`
91
252
  <div part="flat-body" class="empty-state__body--flat">
92
- <span class="nile-flat-type-icon">
93
- ${this.svgIconUrl ? html`
94
- <nile-icon customSvgPath="${this.svgIconUrl}" size="${this.svgIconSize}" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" variant="secondary" color="${ifDefined(this.svgIconColor)}" noFill></nile-icon>
95
- ` : html`
96
- <nile-icon name="${this.icon}" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" size="${this.size == 'sm' ? 24 : this.size == 'md' ? 24 : 28}" variant="secondary"></nile-icon>
97
- `}
253
+ <span part="icon-chrome" class="nile-flat-type-icon">
254
+ ${this.renderGlyph(this.size == 'lg' ? 28 : 24)}
98
255
  </span>
99
256
  </div>`: nothing
100
257
  }
101
258
 
102
259
  <div part="text-section" class="empty__state__text-section">
260
+ ${this.code ? html`
261
+ <div part="code" class="empty-state__code">${this.code}</div>
262
+ ` : nothing}
103
263
  <div part="text" class="empty-state__text">${this.text}</div>
104
264
  <div part="subtitle" class="empty-state__subtext">${this.subText}</div>
105
265
  </div>
@@ -108,6 +268,7 @@ export class NileEmptyState extends NileElement {
108
268
  <div part="actions" class="empty-state-actions">
109
269
  <slot></slot>
110
270
  </div>
271
+
111
272
  </div>
112
273
  `;
113
274
  }