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

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,181 @@
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-xs);
89
+ }
90
+
91
+ /* Group sections */
92
+ .sidebar__group {
93
+ padding-block-end: var(--space-xs);
94
+ }
95
+
96
+ .sidebar__group-label {
97
+ display: block;
98
+ padding: var(--space-xs) var(--space-l);
99
+ font-size: var(--font-size-xs);
100
+ font-weight: 600;
101
+ letter-spacing: 0.05em;
102
+ text-transform: uppercase;
103
+ color: var(--color-outline-variant);
104
+ user-select: none;
105
+ }
106
+
107
+ .sidebar__list {
108
+ list-style: none;
109
+ margin: 0;
110
+ padding: 0;
111
+ }
112
+
113
+ .sidebar__list-item a {
114
+ display: block;
115
+ padding: var(--space-xs) var(--space-l);
116
+ text-decoration: none;
117
+ color: var(--color-on-offset);
118
+ border-inline-start: var(--border-width-thickest) solid transparent;
119
+
120
+ &:hover {
121
+ color: var(--color-primary-on-background);
122
+ background-color: var(--color-offset-variant);
123
+ }
124
+ }
125
+
126
+ .sidebar__list-item:has(a[aria-current="true"]) a {
127
+ color: var(--color-primary-on-background);
128
+ background-color: var(--color-offset-variant);
129
+ border-inline-start-color: var(--color-primary-variant);
130
+ font-weight: 600;
131
+ }
132
+
133
+ /* Secondary list in footer */
134
+ .sidebar__list--secondary {
135
+ margin: 0;
136
+ padding: 0;
137
+ }
138
+
139
+ /* Footer: compact, not sticky */
140
+ .sidebar__footer {
141
+ flex-shrink: 0;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: space-between;
145
+ padding: var(--space-s) var(--space-l);
146
+ border-block-start: var(--border-hairline);
147
+ }
148
+
149
+ .sidebar__footer .sidebar__list--secondary {
150
+ display: flex;
151
+ gap: var(--space-s);
152
+ }
153
+
154
+ .sidebar__list-item--inline {
155
+ display: inline-flex;
156
+ }
157
+
158
+ .sidebar__list-item--inline a {
159
+ padding: 0;
160
+ border-inline-start: 0;
161
+ }
162
+
163
+ .sidebar__logo {
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ /* Backdrop (mobile only) */
168
+ .sidebar-backdrop {
169
+ display: none;
170
+ position: fixed;
171
+ inset: 0;
172
+ background-color: hsl(0 0% 0% / 0.4);
173
+ z-index: 199;
174
+ opacity: 0;
175
+ transition: opacity 0.25s ease;
176
+ }
177
+
178
+ .sidebar-backdrop--visible {
179
+ display: block;
180
+ opacity: 1;
181
+ }
@@ -0,0 +1,154 @@
1
+ {% from "../logo/macro.njk" import logo with context %}
2
+ {% set items = opts.navigation.items %}
3
+ {% set renames = { "Reader": "Microsub" } %}
4
+ <sidebar-nav class="{{ classes("sidebar", opts) }}" aria-label="Main">
5
+ <div class="sidebar__header">
6
+ <a class="sidebar__title u-url" href="{{ opts.url or "/" }}">
7
+ <span class="p-name">{{ opts.name }}</span>
8
+ </a>
9
+ <button class="sidebar__close" type="button" aria-label="Close menu">
10
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
11
+ <path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
12
+ </svg>
13
+ </button>
14
+ </div>
15
+ <nav class="sidebar__nav">
16
+ {# --- PUBLISH --- #}
17
+ <div class="sidebar__group">
18
+ <span class="sidebar__group-label">Publish</span>
19
+ <ul class="sidebar__list" role="list">
20
+ {% for item in items %}{% if item.href and item.text %}
21
+ {% if "/posts" in item.href or "/files" in item.href %}
22
+ <li class="sidebar__list-item">
23
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
24
+ {{- item.text | safe -}}
25
+ </a>
26
+ </li>
27
+ {% endif %}
28
+ {% endif %}{% endfor %}
29
+ </ul>
30
+ </div>
31
+
32
+ {# --- READ & ENGAGE --- #}
33
+ <div class="sidebar__group">
34
+ <span class="sidebar__group-label">Read & Engage</span>
35
+ <ul class="sidebar__list" role="list">
36
+ {% for item in items %}{% if item.href and item.text %}
37
+ {% if "/microsub" in item.href or "/webmention" in item.href %}
38
+ <li class="sidebar__list-item">
39
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
40
+ {%- if renames[item.text] %}{{ renames[item.text] }}{% else %}{{ item.text | safe }}{% endif -%}
41
+ </a>
42
+ </li>
43
+ {% endif %}
44
+ {% endif %}{% endfor %}
45
+ </ul>
46
+ </div>
47
+
48
+ {# --- CURATE --- #}
49
+ <div class="sidebar__group">
50
+ <span class="sidebar__group-label">Curate</span>
51
+ <ul class="sidebar__list" role="list">
52
+ {% for item in items %}{% if item.href and item.text %}
53
+ {% if "/blogroll" in item.href or "/podroll" in item.href %}
54
+ <li class="sidebar__list-item">
55
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
56
+ {{- item.text | safe -}}
57
+ </a>
58
+ </li>
59
+ {% endif %}
60
+ {% endif %}{% endfor %}
61
+ </ul>
62
+ </div>
63
+
64
+ {# --- BUILD --- #}
65
+ <div class="sidebar__group">
66
+ <span class="sidebar__group-label">Build</span>
67
+ <ul class="sidebar__list" role="list">
68
+ {% for item in items %}{% if item.href and item.text %}
69
+ {% if "/homepage" in item.href or "/cv" in item.href %}
70
+ <li class="sidebar__list-item">
71
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
72
+ {{- item.text | safe -}}
73
+ </a>
74
+ </li>
75
+ {% endif %}
76
+ {% endif %}{% endfor %}
77
+ </ul>
78
+ </div>
79
+
80
+ {# --- ACTIVITY --- #}
81
+ <div class="sidebar__group">
82
+ <span class="sidebar__group-label">Activity</span>
83
+ <ul class="sidebar__list" role="list">
84
+ {% for item in items %}{% if item.href and item.text %}
85
+ {% if "/github" in item.href or "/lastfm" in item.href or "/funkwhale" in item.href or "/youtube" in item.href or "/rss" in item.href %}
86
+ <li class="sidebar__list-item">
87
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
88
+ {{- item.text | safe -}}
89
+ </a>
90
+ </li>
91
+ {% endif %}
92
+ {% endif %}{% endfor %}
93
+ </ul>
94
+ </div>
95
+
96
+ {# --- UNCATEGORIZED (catch-all for future plugins) --- #}
97
+ {% set knownPaths = ["/posts", "/files", "/microsub", "/webmention", "/blogroll", "/podroll", "/homepage", "/cv", "/github", "/lastfm", "/funkwhale", "/youtube", "/rss"] %}
98
+ {% set hasOther = [] %}
99
+ {% for item in items %}{% if item.href and item.text %}
100
+ {% set matched = false %}
101
+ {% for kp in knownPaths %}
102
+ {% if kp in item.href %}{% set matched = true %}{% endif %}
103
+ {% endfor %}
104
+ {% if not matched %}{% set hasOther = hasOther.concat([item]) %}{% endif %}
105
+ {% endif %}{% endfor %}
106
+ {% if hasOther | length > 0 %}
107
+ <div class="sidebar__group">
108
+ <span class="sidebar__group-label">Other</span>
109
+ <ul class="sidebar__list" role="list">
110
+ {% for item in hasOther %}
111
+ <li class="sidebar__list-item">
112
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
113
+ {{- item.text | safe -}}
114
+ </a>
115
+ </li>
116
+ {% endfor %}
117
+ </ul>
118
+ </div>
119
+ {% endif %}
120
+
121
+ {# --- SYSTEM (hardcoded core pages) --- #}
122
+ <div class="sidebar__group">
123
+ <span class="sidebar__group-label">System</span>
124
+ <ul class="sidebar__list" role="list">
125
+ <li class="sidebar__list-item">
126
+ <a href="{{ opts.url }}/status">Status</a>
127
+ </li>
128
+ <li class="sidebar__list-item">
129
+ <a href="{{ opts.url }}/plugins">Plugins</a>
130
+ </li>
131
+ </ul>
132
+ </div>
133
+ </nav>
134
+ <div class="sidebar__footer">
135
+ {% if opts.secondaryNavigation.items.length > 0 %}
136
+ <ul class="sidebar__list sidebar__list--secondary" role="list">
137
+ {% for item in opts.secondaryNavigation.items %}{% if item.href and item.text %}
138
+ <li class="sidebar__list-item sidebar__list-item--inline">
139
+ <a href="{{ item.href }}"{{- attributes(item.attributes) }}>
140
+ {{- item.text | safe -}}
141
+ </a>
142
+ </li>
143
+ {% endif %}{% endfor %}
144
+ </ul>
145
+ {% endif %}
146
+ {{ logo({
147
+ classes: "h-x-app sidebar__logo",
148
+ href: "https://getindiekit.com",
149
+ src: opts.logoSrc,
150
+ alt: "Indiekit",
151
+ size: 14
152
+ }) }}
153
+ </div>
154
+ </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.27",
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");