diff --git a/CHANGELOG.md b/CHANGELOG.md index c5382ad..75a8f3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,19 @@ ## [Unreleased] +### Added + +- Navbar component snippet and classes. + ### Changed - All three default themes now have distinct status colors. Status UIs can now distinguish caution, error, info, and success states without token overrides. - `dust` theme background lightened slightly to improve contrast ratios with the updated color palette. +### Breaking changes + +- Tabs are now a formal tabset based on the ` @@ -380,19 +392,21 @@

Telemetry

-

Navigation Tabs

+

Tabs

-

Page links with native current-page state.

+

Switch between views on a page

- +
+ + +
- +
+ Incident content here. +
+
@@ -543,28 +557,76 @@

State Matrix

diff --git a/packages/core-css/src/components/navigation.css b/packages/core-css/src/components/navigation.css new file mode 100644 index 0000000..1eeec27 --- /dev/null +++ b/packages/core-css/src/components/navigation.css @@ -0,0 +1,65 @@ +.elum-navbar { + align-items: center; + justify-content: space-between; + display: flex; + overflow-x: auto; + overflow-y: hidden; + border-bottom: var(--elum-border-width) solid var(--elum-color-border); + padding: var(--elum-space-3) var(--elum-space-4); + margin-bottom: 1.5rem; + position: sticky; + top: 0; + background-color: var(--elum-color-bg); + z-index: 999; +} + +.elum-navbar-brand { + align-items: center; + color: var(--elum-navbar-brand-color, var(--elum-color-accent)); + display: inline-flex; + font-size: var(--elum-navbar-brand-size, var(--elum-text-sm)); + font-weight: 700; + letter-spacing: 0.08em; + line-height: var(--elum-lh-tight); + padding: 0 var(--elum-space-2); + text-transform: uppercase; +} + +.elum-navlinks { + align-items: center; + display: flex; +} + +.elum-navlink { + display: inline-flex; + align-items: center; + color: var(--elum-navlink-color, var(--elum-color-accent)); + text-decoration: none; + font-size: var(--elum-navlink-size, var(--elum-text-sm)); + font-weight: 700; + letter-spacing: 0.08em; + line-height: var(--elum-lh-tight); + text-transform: uppercase; +} + +.elum-navlink:not(:last-child)::after { + display: inline; + color: var(--elum-color-muted); + content: "|"; + margin: 0 var(--elum-space-1); + font-weight: 500; + line-height: 0; +} + +.elum-navlink:hover { + color: var(--elum-color-fg); +} + +.elum-navlink:focus-visible { + outline: var(--elum-color-focus) solid var(--elum-border-width); + outline-offset: calc(var(--elum-border-width) * 2); +} + +.elum-navlink[aria-current="page"] { + color: var(--elum-color-fg); +} diff --git a/packages/core-css/src/components/tabs.css b/packages/core-css/src/components/tabs.css index 89dbcfe..248f43b 100644 --- a/packages/core-css/src/components/tabs.css +++ b/packages/core-css/src/components/tabs.css @@ -13,8 +13,12 @@ } .elum-tab { + appearance: none; + background: transparent; + border: 0; border-bottom: var(--elum-border-width) solid var(--elum-color-border); color: var(--elum-color-muted); + cursor: pointer; display: inline-flex; padding: var(--elum-space-1) var(--elum-space-2); text-decoration: none; @@ -29,7 +33,7 @@ outline-offset: calc(var(--elum-border-width) * -1); } -.elum-tab[aria-current="page"] { +.elum-tab[aria-selected="true"] { background: var(--elum-color-bg); border: var(--elum-border-width) solid var(--elum-color-border); border-bottom: 0; diff --git a/packages/core-css/src/index.css b/packages/core-css/src/index.css index fccdb20..10f19b1 100644 --- a/packages/core-css/src/index.css +++ b/packages/core-css/src/index.css @@ -9,3 +9,4 @@ @import "./components/tabs.css"; @import "./components/toolbar.css"; @import "./components/query.css"; +@import "./components/navigation.css"; diff --git a/packages/core-patterns/snippets/index.html b/packages/core-patterns/snippets/index.html index d2f8e1a..eca6850 100644 --- a/packages/core-patterns/snippets/index.html +++ b/packages/core-patterns/snippets/index.html @@ -1,4 +1,13 @@
+ +

Card Title

@@ -180,10 +189,13 @@

Card Title

- +
+ + +
+ +
Incident content here.
+
diff --git a/tests/core-css-contract.test.mjs b/tests/core-css-contract.test.mjs index 9d94158..b4e2b07 100644 --- a/tests/core-css-contract.test.mjs +++ b/tests/core-css-contract.test.mjs @@ -14,6 +14,7 @@ const DATA_CSS = readFileSync("packages/core-css/src/components/data.css", "utf8 const TABS_CSS = readFileSync("packages/core-css/src/components/tabs.css", "utf8"); const TOOLBAR_CSS = readFileSync("packages/core-css/src/components/toolbar.css", "utf8"); const QUERY_CSS = readFileSync("packages/core-css/src/components/query.css", "utf8"); +const NAVIGATION_CSS = readFileSync("packages/core-css/src/components/navigation.css", "utf8"); const INDEX_CSS = readFileSync("packages/core-css/src/index.css", "utf8"); const INDEX_CSS_PATH = "packages/core-css/src/index.css"; const PUBLIC_CSS = [ @@ -28,6 +29,7 @@ const PUBLIC_CSS = [ TABS_CSS, TOOLBAR_CSS, QUERY_CSS, + NAVIGATION_CSS, ].join("\n"); const THEMES = ["dust", "iron", "neon"]; @@ -72,7 +74,7 @@ function contrastRatio(firstColor, secondColor) { } test("core CSS entrypoint imports the public bundles", () => { - for (const bundle of ["tokens", "base", "button", "form", "card", "feedback", "telemetry", "data", "tabs", "toolbar", "query"]) { + for (const bundle of ["tokens", "base", "button", "form", "card", "feedback", "telemetry", "data", "tabs", "toolbar", "query", "navigation"]) { assert.match(INDEX_CSS, new RegExp(`@import ".+${bundle}\\.css";`)); } }); @@ -124,6 +126,10 @@ test("component CSS exposes current public class surfaces", () => { "elum-badge", "elum-status-label", "elum-system-bar", + "elum-navbar", + "elum-navbar-brand", + "elum-navlinks", + "elum-navlink", "elum-tabs", "elum-tab", "elum-toolbar", @@ -209,5 +215,16 @@ test("table and row APIs expose responsive state hooks", () => { test("tabs scroll horizontally and show focus", () => { assert.match(TABS_CSS, /\.elum-tabs\s*{[^}]*overflow-x:\s*auto;/s); assert.match(TABS_CSS, /\.elum-tab:focus-visible\s*{[^}]*outline:/s); - assert.doesNotMatch(TABS_CSS, /\.elum-tab\[aria-current="page"\]\s*{[^}]*box-shadow:/s); + assert.doesNotMatch(TABS_CSS, /\.elum-tab\[aria-selected="true"\]\s*{[^}]*box-shadow:/s); +}); + +test("tab control resets native button styling", () => { + assert.match(TABS_CSS, /\.elum-tab\s*{[^}]*appearance:\s*none;/s); + assert.match(TABS_CSS, /\.elum-tab\s*{[^}]*background:\s*transparent;/s); +}); + +test("navbar sticks to the top and navlinks show focus", () => { + assert.match(NAVIGATION_CSS, /\.elum-navbar\s*{[^}]*position:\s*sticky;/s); + assert.match(NAVIGATION_CSS, /\.elum-navlink:focus-visible\s*{[^}]*outline:/s); + assert.match(NAVIGATION_CSS, /\.elum-navlink\[aria-current="page"\]/); }); diff --git a/tests/docs-contract.test.mjs b/tests/docs-contract.test.mjs index 569f09d..a896efb 100644 --- a/tests/docs-contract.test.mjs +++ b/tests/docs-contract.test.mjs @@ -37,7 +37,8 @@ test("component docs cover the public component set", () => { "Badge", "Status Label", "System Bar", - "Navigation Tabs", + "Navbar", + "Tabs", "Toolbar", "Query Row", "Pagination", diff --git a/tests/playground.test.mjs b/tests/playground.test.mjs index 181ae44..06b405f 100644 --- a/tests/playground.test.mjs +++ b/tests/playground.test.mjs @@ -81,15 +81,18 @@ test("playground and snippets expose toolbar groups semantically", () => { } }); -test("playground and snippets expose navigation tabs semantically", () => { +test("playground and snippets expose the in-page tabset pattern", () => { for (const file of [PLAYGROUND_PATH, SNIPPETS_PATH]) { const html = readFileSync(file, "utf8"); assert.match(html, classToken("elum-tabs")); assert.match(html, classToken("elum-tab")); - assert.match(html, /aria-current="page"/); - assert.doesNotMatch(html, /role="tablist"/); - assert.doesNotMatch(html, /role="tab"/); + assert.match(html, /role="tablist"/); + assert.match(html, /role="tab"/); + assert.match(html, /role="tabpanel"/); + assert.match(html, /aria-selected="(?:true|false)"/); + assert.match(html, /aria-controls="[^"]+"/); + assert.doesNotMatch(html, /class="elum-tab"[^>]*aria-current="page"/); } });