@rmdes/indiekit-frontend 1.0.0-beta.25 → 1.0.0-beta.26

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.
@@ -1,14 +1,32 @@
1
1
  .app {
2
2
  background-color: var(--color-offset);
3
+ font-family: var(--font-family-sans);
4
+
5
+ /* Mobile: stacked column layout */
3
6
  display: flex;
4
7
  flex-direction: column;
5
- font-family: var(--font-family-sans);
6
8
  justify-content: space-between;
9
+
10
+ /* Desktop: sidebar + content grid */
11
+ @media (width >= 48rem) {
12
+ display: grid;
13
+ grid-template-columns: 15rem 1fr;
14
+ grid-template-rows: 1fr auto;
15
+ grid-template-areas:
16
+ "sidebar main"
17
+ "sidebar footer";
18
+ min-block-size: 100dvh;
19
+ }
7
20
  }
8
21
 
9
22
  .app--minimalui {
10
23
  --container-max-inline-size: 36rem;
11
24
 
25
+ /* Always flex column for minimalui, never grid */
26
+ display: flex;
27
+ flex-direction: column;
28
+ justify-content: space-between;
29
+
12
30
  & .header,
13
31
  & .footer {
14
32
  border: 0;
@@ -10,6 +10,13 @@
10
10
  justify-content: flex-end;
11
11
  }
12
12
 
13
+ /* Hide footer on desktop when sidebar is active */
14
+ @media (width >= 48rem) {
15
+ body:not(.app--minimalui) .footer {
16
+ display: none;
17
+ }
18
+ }
19
+
13
20
  .footer__container {
14
21
  align-items: center;
15
22
  display: flex;
@@ -18,6 +18,13 @@
18
18
  display: flex;
19
19
  }
20
20
 
21
+ /* Hide header on desktop (sidebar replaces it) */
22
+ @media (width >= 48rem) {
23
+ body:not(.app--minimalui) .header {
24
+ display: none;
25
+ }
26
+ }
27
+
21
28
  body:not(.app--minimalui) .header {
22
29
  backdrop-filter: blur(16px);
23
30
  background-color: var(--header-background-color);
@@ -58,3 +65,24 @@ body:not(.app--minimalui) .header {
58
65
  padding: var(--space-s);
59
66
  }
60
67
  }
68
+
69
+ .header__hamburger {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ padding: var(--space-s);
74
+ color: var(--color-on-offset);
75
+ background: none;
76
+ border: 0;
77
+ border-radius: var(--border-radius-small);
78
+ cursor: pointer;
79
+
80
+ &:hover {
81
+ color: var(--color-primary-on-background);
82
+ background-color: var(--color-offset-variant);
83
+ }
84
+
85
+ @media (width >= 48rem) {
86
+ display: none;
87
+ }
88
+ }
@@ -6,6 +6,10 @@
6
6
  <span class="p-name">{{ opts.name }}</span>
7
7
  </a>
8
8
  </div>
9
- {{ navigation(opts.navigation) | indent(4) if opts.navigation.items.length > 0 }}
9
+ <button class="header__hamburger" type="button" aria-label="Open menu">
10
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
11
+ <path d="M3 6h18M3 12h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
12
+ </svg>
13
+ </button>
10
14
  </div>
11
- </header>
15
+ </header>
@@ -3,6 +3,7 @@
3
3
  background-color: var(--color-background);
4
4
  color: var(--color-on-background);
5
5
  flex: 1;
6
+ grid-area: main;
6
7
  }
7
8
 
8
9
  .main__container {
@@ -0,0 +1,57 @@
1
+ export const SidebarComponent = class extends HTMLElement {
2
+ connectedCallback() {
3
+ this.closeButton = this.querySelector(".sidebar__close");
4
+ this.backdrop = document.querySelector(".sidebar-backdrop");
5
+ this.hamburger = document.querySelector(".header__hamburger");
6
+
7
+ if (this.closeButton) {
8
+ this.closeButton.addEventListener("click", () => this.close());
9
+ }
10
+
11
+ if (this.backdrop) {
12
+ this.backdrop.addEventListener("click", () => this.close());
13
+ }
14
+
15
+ if (this.hamburger) {
16
+ this.hamburger.addEventListener("click", () => this.open());
17
+ }
18
+
19
+ document.addEventListener("keydown", (event) => {
20
+ if (event.key === "Escape" && this.classList.contains("sidebar--open")) {
21
+ this.close();
22
+ }
23
+ });
24
+
25
+ this.mediaQuery = window.matchMedia("(width >= 48rem)");
26
+ this.mediaQuery.addEventListener("change", (event) => {
27
+ if (event.matches && this.classList.contains("sidebar--open")) {
28
+ this.close();
29
+ }
30
+ });
31
+ }
32
+
33
+ open() {
34
+ this.classList.add("sidebar--open");
35
+
36
+ if (this.backdrop) {
37
+ this.backdrop.classList.add("sidebar-backdrop--visible");
38
+ }
39
+
40
+ const firstLink = this.querySelector(".sidebar__list-item a");
41
+ if (firstLink) {
42
+ firstLink.focus();
43
+ }
44
+ }
45
+
46
+ close() {
47
+ this.classList.remove("sidebar--open");
48
+
49
+ if (this.backdrop) {
50
+ this.backdrop.classList.remove("sidebar-backdrop--visible");
51
+ }
52
+
53
+ if (this.hamburger) {
54
+ this.hamburger.focus();
55
+ }
56
+ }
57
+ };
@@ -0,0 +1,3 @@
1
+ {% macro sidebar(opts) %}
2
+ {%- include "./template.njk" -%}
3
+ {% endmacro %}
@@ -0,0 +1,150 @@
1
+ /* Sidebar */
2
+ .sidebar {
3
+ --sidebar-inline-size: 15rem;
4
+ --anchor-color: var(--color-on-offset);
5
+ --anchor-color-hover: var(--color-primary-on-background);
6
+ --anchor-decoration-color: transparent;
7
+
8
+ background-color: var(--color-offset);
9
+ color: var(--color-on-offset);
10
+ display: flex;
11
+ flex-direction: column;
12
+ font: var(--font-caption);
13
+ }
14
+
15
+ /* Desktop: persistent sidebar */
16
+ @media (width >= 48rem) {
17
+ .sidebar {
18
+ grid-area: sidebar;
19
+ position: sticky;
20
+ inset-block-start: 0;
21
+ block-size: 100dvh;
22
+ border-inline-end: var(--border-hairline);
23
+ z-index: 10;
24
+ }
25
+ }
26
+
27
+ /* Mobile: off-canvas drawer */
28
+ @media (width < 48rem) {
29
+ .sidebar {
30
+ position: fixed;
31
+ inset-block: 0;
32
+ inset-inline-start: 0;
33
+ inline-size: min(var(--sidebar-inline-size), 80vw);
34
+ z-index: 200;
35
+ transform: translateX(-100%);
36
+ transition: transform 0.25s ease;
37
+ box-shadow: none;
38
+ }
39
+
40
+ .sidebar--open {
41
+ transform: translateX(0);
42
+ box-shadow: 4px 0 16px var(--color-shadow);
43
+ }
44
+ }
45
+
46
+ /* Header: site name + close button */
47
+ .sidebar__header {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ padding: var(--space-m) var(--space-l);
52
+ border-block-end: var(--border-hairline);
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ .sidebar__title {
57
+ font-weight: 600;
58
+ font-size: var(--font-size-m);
59
+ line-height: var(--line-height-tight);
60
+ text-decoration: none;
61
+ }
62
+
63
+ .sidebar__close {
64
+ display: none;
65
+ align-items: center;
66
+ justify-content: center;
67
+ padding: var(--space-xs);
68
+ color: var(--color-on-offset);
69
+ background: none;
70
+ border: 0;
71
+ border-radius: var(--border-radius-small);
72
+ cursor: pointer;
73
+
74
+ &:hover {
75
+ color: var(--color-primary-on-background);
76
+ background-color: var(--color-offset-variant);
77
+ }
78
+
79
+ @media (width < 48rem) {
80
+ display: inline-flex;
81
+ }
82
+ }
83
+
84
+ /* Nav section: scrollable list */
85
+ .sidebar__nav {
86
+ flex: 1;
87
+ overflow-y: auto;
88
+ padding-block: var(--space-s);
89
+ }
90
+
91
+ .sidebar__list {
92
+ list-style: none;
93
+ margin: 0;
94
+ padding: 0;
95
+ }
96
+
97
+ .sidebar__list-item a {
98
+ display: block;
99
+ padding: var(--space-s) var(--space-l);
100
+ text-decoration: none;
101
+ color: var(--color-on-offset);
102
+ border-inline-start: var(--border-width-thickest) solid transparent;
103
+
104
+ &:hover {
105
+ color: var(--color-primary-on-background);
106
+ background-color: var(--color-offset-variant);
107
+ }
108
+ }
109
+
110
+ .sidebar__list-item:has(a[aria-current="true"]) a {
111
+ color: var(--color-primary-on-background);
112
+ background-color: var(--color-offset-variant);
113
+ border-inline-start-color: var(--color-primary-variant);
114
+ font-weight: 600;
115
+ }
116
+
117
+ /* Secondary list: smaller separator */
118
+ .sidebar__list--secondary {
119
+ border-block-start: var(--border-hairline);
120
+ padding-block-start: var(--space-s);
121
+ margin-block-start: var(--space-s);
122
+ }
123
+
124
+ /* Footer: secondary nav + logo */
125
+ .sidebar__footer {
126
+ flex-shrink: 0;
127
+ padding: var(--space-m) var(--space-l);
128
+ border-block-start: var(--border-hairline);
129
+ }
130
+
131
+ .sidebar__logo {
132
+ display: block;
133
+ margin-block-start: var(--space-m);
134
+ }
135
+
136
+ /* Backdrop (mobile only) */
137
+ .sidebar-backdrop {
138
+ display: none;
139
+ position: fixed;
140
+ inset: 0;
141
+ background-color: hsl(0 0% 0% / 0.4);
142
+ z-index: 199;
143
+ opacity: 0;
144
+ transition: opacity 0.25s ease;
145
+ }
146
+
147
+ .sidebar-backdrop--visible {
148
+ display: block;
149
+ opacity: 1;
150
+ }
@@ -0,0 +1,43 @@
1
+ {% from "../logo/macro.njk" import logo with context %}
2
+ <sidebar-nav class="{{ classes("sidebar", opts) }}" aria-label="Main">
3
+ <div class="sidebar__header">
4
+ <a class="sidebar__title u-url" href="{{ opts.url or "/" }}">
5
+ <span class="p-name">{{ opts.name }}</span>
6
+ </a>
7
+ <button class="sidebar__close" type="button" aria-label="Close menu">
8
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
9
+ <path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
10
+ </svg>
11
+ </button>
12
+ </div>
13
+ <nav class="sidebar__nav">
14
+ <ul class="sidebar__list" role="list">
15
+ {% for item in opts.navigation.items %}{% if item.href and item.text %}
16
+ <li class="sidebar__list-item">
17
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
18
+ {{- item.text | safe -}}
19
+ </a>
20
+ </li>
21
+ {% endif %}{% endfor %}
22
+ </ul>
23
+ </nav>
24
+ <div class="sidebar__footer">
25
+ {% if opts.secondaryNavigation.items.length > 0 %}
26
+ <ul class="sidebar__list sidebar__list--secondary" role="list">
27
+ {% for item in opts.secondaryNavigation.items %}{% if item.href and item.text %}
28
+ <li class="sidebar__list-item">
29
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
30
+ {{- item.text | safe -}}
31
+ </a>
32
+ </li>
33
+ {% endif %}{% endfor %}
34
+ </ul>
35
+ {% endif %}
36
+ {{ logo({
37
+ classes: "h-x-app sidebar__logo",
38
+ href: "https://getindiekit.com",
39
+ src: opts.logoSrc,
40
+ alt: "Indiekit"
41
+ }) | indent(4) }}
42
+ </div>
43
+ </sidebar-nav>
@@ -31,6 +31,7 @@
31
31
  {% from "textarea/macro.njk" import textarea with context %}
32
32
  {% from "user/macro.njk" import user with context %}
33
33
  {% from "warning-text/macro.njk" import warningText with context %}
34
+ {% from "sidebar/macro.njk" import sidebar with context %}
34
35
  {% from "widget/macro.njk" import widget with context %}
35
36
  {% set appClasses = "app" + (" " + appClasses if appClasses) + (" app--minimalui" if minimalui) %}
36
37
  {% set mainClasses = "main" + (" " + mainClasses if mainClasses) %}
@@ -65,6 +66,21 @@
65
66
  <script>document.body.classList.add("js-enabled")</script>
66
67
  {{ skipLink() }}
67
68
 
69
+ {% if not minimalui %}
70
+ {{ sidebar({
71
+ url: application.url,
72
+ name: application.name,
73
+ navigation: {
74
+ items: application.navigation | rejectattr("secondary")
75
+ },
76
+ secondaryNavigation: {
77
+ items: application.navigation | selectattr("secondary")
78
+ },
79
+ logoSrc: application.url + assetPath | default("/assets") + "/icon.svg"
80
+ }) }}
81
+ <div class="sidebar-backdrop" aria-hidden="true"></div>
82
+ {% endif %}
83
+
68
84
  {% block header %}
69
85
  {{ header({
70
86
  url: application.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-frontend",
3
- "version": "1.0.0-beta.25",
3
+ "version": "1.0.0-beta.26",
4
4
  "description": "Frontend components for Indiekit (fork with floating toolbar)",
5
5
  "keywords": [
6
6
  "express",
package/scripts/app.js CHANGED
@@ -7,6 +7,7 @@ import { GeoInputFieldComponent } from "../components/geo-input/index.js";
7
7
  import { NotificationBannerComponent } from "../components/notification-banner/index.js";
8
8
  import { RadiosFieldComponent } from "../components/radios/index.js";
9
9
  import { SharePreviewComponent } from "../components/share-preview/index.js";
10
+ import { SidebarComponent } from "../components/sidebar/index.js";
10
11
  import { TagInputFieldComponent } from "../components/tag-input/index.js";
11
12
  import { TextareaFieldComponent } from "../components/textarea/index.js";
12
13
 
@@ -19,5 +20,6 @@ customElements.define("geo-input-field", GeoInputFieldComponent);
19
20
  customElements.define("notification-banner", NotificationBannerComponent);
20
21
  customElements.define("radios-field", RadiosFieldComponent);
21
22
  customElements.define("share-preview", SharePreviewComponent);
23
+ customElements.define("sidebar-nav", SidebarComponent);
22
24
  customElements.define("tag-input-field", TagInputFieldComponent);
23
25
  customElements.define("textarea-field", TextareaFieldComponent);
package/styles/app.css CHANGED
@@ -54,6 +54,7 @@
54
54
  @import url("../components/section/styles.css");
55
55
  @import url("../components/select/styles.css");
56
56
  @import url("../components/share-preview/styles.css");
57
+ @import url("../components/sidebar/styles.css");
57
58
  @import url("../components/skip-link/styles.css");
58
59
  @import url("../components/summary/styles.css");
59
60
  @import url("../components/tag/styles.css");