maverick-wave 5.29.0 → 5.31.0

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 (144) hide show
  1. package/README.md +2 -2
  2. package/maverick-wave.min.css +2 -2
  3. package/package.json +8 -2
  4. package/src/scss/components/_avatars.scss +35 -3
  5. package/src/scss/components/_dropdown.scss +24 -1
  6. package/src/scss/components/_lang-switch.scss +4 -8
  7. package/src/scss/components/_modals.scss +33 -5
  8. package/.claude/settings.local.json +0 -43
  9. package/.claude/skills/mw-maverick-wave/SKILL.md +0 -532
  10. package/.claude/skills/mw-maverick-wave/examples/angular-form.md +0 -279
  11. package/.claude/skills/mw-maverick-wave/examples/angular-list-page.md +0 -309
  12. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +0 -538
  13. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +0 -530
  14. package/.claude/skills/mw-maverick-wave/references/components.md +0 -2169
  15. package/.claude/skills/mw-maverick-wave/references/forms.md +0 -358
  16. package/.claude/skills/mw-maverick-wave/references/javascript.md +0 -164
  17. package/.claude/skills/mw-maverick-wave/references/layout.md +0 -764
  18. package/.claude/skills/mw-maverick-wave/references/theming.md +0 -528
  19. package/.github/workflows/ci.yml +0 -41
  20. package/.idea/codeStyles/Project.xml +0 -59
  21. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  22. package/.idea/maverick-wave.iml +0 -12
  23. package/.idea/modules.xml +0 -8
  24. package/.idea/prettier.xml +0 -6
  25. package/.idea/vcs.xml +0 -6
  26. package/.prettierignore +0 -8
  27. package/.prettierrc.json +0 -7
  28. package/CHANGELOG.md +0 -950
  29. package/CLAUDE.md +0 -60
  30. package/gulpfile.js +0 -167
  31. package/index.html +0 -1730
  32. package/release.sh +0 -31
  33. package/scripts/verify.js +0 -252
  34. package/src/assets/favicon/apple-touch-icon.png +0 -0
  35. package/src/assets/favicon/favicon-96x96.png +0 -0
  36. package/src/assets/favicon/favicon.svg +0 -1
  37. package/src/assets/favicon/site.webmanifest +0 -37
  38. package/src/assets/favicon/web-app-manifest-192x192.png +0 -0
  39. package/src/assets/favicon/web-app-manifest-512x512.png +0 -0
  40. package/src/assets/favicon.ico +0 -0
  41. package/src/assets/header-logo.svg +0 -24
  42. package/src/assets/images/gallery-city.svg +0 -49
  43. package/src/assets/images/gallery-desert.svg +0 -25
  44. package/src/assets/images/gallery-forest.svg +0 -46
  45. package/src/assets/images/gallery-lake.svg +0 -26
  46. package/src/assets/images/gallery-mountain.svg +0 -19
  47. package/src/assets/images/gallery-ocean.svg +0 -26
  48. package/src/assets/images/gallery-oldtown.svg +0 -41
  49. package/src/assets/images/gallery-snow.svg +0 -39
  50. package/src/assets/images/photo-balloon.svg +0 -35
  51. package/src/assets/images/photo-forest-path.svg +0 -31
  52. package/src/assets/images/photo-lighthouse.svg +0 -42
  53. package/src/assets/images/photo-palm.svg +0 -27
  54. package/src/assets/images/photo-peak.svg +0 -27
  55. package/src/assets/images/photo-tower.svg +0 -34
  56. package/src/assets/images/photo-waterfall.svg +0 -32
  57. package/src/assets/images/slider-field.svg +0 -124
  58. package/src/assets/images/slider-forest.svg +0 -33
  59. package/src/assets/images/slider-lake.svg +0 -35
  60. package/src/assets/images/slider-mountains.svg +0 -29
  61. package/src/assets/images/slider-trees.svg +0 -41
  62. package/src/assets/images/story-aurora.svg +0 -36
  63. package/src/assets/images/story-beach.svg +0 -34
  64. package/src/assets/images/story-stars.svg +0 -35
  65. package/src/assets/images/tile-analytics.svg +0 -22
  66. package/src/assets/images/tile-cloud.svg +0 -19
  67. package/src/assets/images/tile-ecommerce.svg +0 -14
  68. package/src/assets/images/tile-enterprise.svg +0 -22
  69. package/src/assets/images/tile-marketing.svg +0 -16
  70. package/src/assets/media/demo-chime.wav +0 -0
  71. package/src/assets/media/story-sunrise.jpg +0 -0
  72. package/src/assets/media/story-sunrise.mp4 +0 -0
  73. package/src/partials/accordions-container.html +0 -179
  74. package/src/partials/alerts-container.html +0 -151
  75. package/src/partials/announcement-container.html +0 -67
  76. package/src/partials/avatars-container.html +0 -229
  77. package/src/partials/badges-container.html +0 -114
  78. package/src/partials/blog-posts-container.html +0 -158
  79. package/src/partials/breadcrumbs-container.html +0 -126
  80. package/src/partials/button-bar-container.html +0 -175
  81. package/src/partials/buttons-container.html +0 -272
  82. package/src/partials/calendar-container.html +0 -203
  83. package/src/partials/cards-container.html +0 -727
  84. package/src/partials/code-container.html +0 -194
  85. package/src/partials/colors-container.html +0 -263
  86. package/src/partials/coming-soon-container.html +0 -10
  87. package/src/partials/contact-container.html +0 -85
  88. package/src/partials/content-slider-container.html +0 -36
  89. package/src/partials/divider-container.html +0 -70
  90. package/src/partials/documentation-container.html +0 -204
  91. package/src/partials/dropdown-container.html +0 -119
  92. package/src/partials/empty-state-container.html +0 -79
  93. package/src/partials/feed-container.html +0 -506
  94. package/src/partials/footer-container.html +0 -113
  95. package/src/partials/form-container.html +0 -262
  96. package/src/partials/form-elements-container.html +0 -616
  97. package/src/partials/form-field-container.html +0 -166
  98. package/src/partials/gallery-container.html +0 -62
  99. package/src/partials/get-started-container.html +0 -423
  100. package/src/partials/grid-container.html +0 -191
  101. package/src/partials/header-container.html +0 -69
  102. package/src/partials/header-utilities-container.html +0 -331
  103. package/src/partials/highlights-container.html +0 -71
  104. package/src/partials/history-container.html +0 -67
  105. package/src/partials/home-container.html +0 -31
  106. package/src/partials/html-lists-container.html +0 -63
  107. package/src/partials/info-container.html +0 -104
  108. package/src/partials/input-group-container.html +0 -299
  109. package/src/partials/item-lists-container.html +0 -368
  110. package/src/partials/kanban-container.html +0 -459
  111. package/src/partials/kbd-container.html +0 -63
  112. package/src/partials/leader-row-container.html +0 -139
  113. package/src/partials/login-container.html +0 -65
  114. package/src/partials/lunch-menu-container.html +0 -341
  115. package/src/partials/media-container.html +0 -27
  116. package/src/partials/meta-info-container.html +0 -21
  117. package/src/partials/modals-container.html +0 -199
  118. package/src/partials/mosaic-container.html +0 -173
  119. package/src/partials/page-header-container.html +0 -78
  120. package/src/partials/pagination-container.html +0 -64
  121. package/src/partials/palette-container.html +0 -480
  122. package/src/partials/panels-container.html +0 -126
  123. package/src/partials/parallax-container.html +0 -147
  124. package/src/partials/portrait-gallery-container.html +0 -175
  125. package/src/partials/preview-container.html +0 -136
  126. package/src/partials/pricing-container.html +0 -523
  127. package/src/partials/progress-container.html +0 -230
  128. package/src/partials/prose-container.html +0 -43
  129. package/src/partials/ratings-container.html +0 -104
  130. package/src/partials/section-head-container.html +0 -77
  131. package/src/partials/segmented-container.html +0 -66
  132. package/src/partials/skeleton-container.html +0 -87
  133. package/src/partials/spinners-container.html +0 -96
  134. package/src/partials/stepper-container.html +0 -111
  135. package/src/partials/stories-container.html +0 -182
  136. package/src/partials/tables-container.html +0 -309
  137. package/src/partials/tabs-container.html +0 -290
  138. package/src/partials/tags-container.html +0 -242
  139. package/src/partials/techstack-bucket-container.html +0 -187
  140. package/src/partials/testimonials-container.html +0 -88
  141. package/src/partials/tiles-container.html +0 -247
  142. package/src/partials/timelines-container.html +0 -196
  143. package/src/partials/typography-container.html +0 -74
  144. package/src/partials/utilities-container.html +0 -553
@@ -1,2169 +0,0 @@
1
- # Components
2
-
3
- Colour variants are always one of `primary`, `secondary`, `success`, `warning`,
4
- `danger`, `info` - but not every component offers all six. The lists below are
5
- exhaustive: what is not named does not exist.
6
-
7
- ## Buttons
8
-
9
- ```html
10
- <button type="button" class="mw-btn mw-btn-primary">Save</button>
11
- <button type="button" class="mw-btn mw-btn-primary">
12
- <i class="fas fa-save"></i> Save
13
- </button>
14
- <a href="#" class="mw-btn mw-btn-outline">Link that looks like a button</a>
15
- ```
16
-
17
- - Variants: `mw-btn-primary`, `mw-btn-secondary`, `mw-btn-danger`,
18
- `mw-btn-success`, `mw-btn-outline`, `mw-btn-ghost`, `mw-btn-ghost-danger`,
19
- `mw-btn-link`, `mw-btn-link-muted`
20
- - Shapes: `mw-btn-icon` (square, for an icon-only button), `mw-btn-block`
21
- (full width)
22
- - Sizes: `mw-btn-sm`, `mw-btn-lg`
23
- - `mw-btn` is `inline-flex` with a gap - icons need no wrapper and no extra class
24
- - `disabled` gets `opacity: .6` and `not-allowed`; there is no disabled class
25
- - `mw-active` gives the pressed/selected look on the coloured variants
26
- - Every variant dips 1px on `:active`, and the solid ones invert their top
27
- highlight into an inner shadow. On touch that press is the only feedback there
28
- is, so do not override it away
29
- - **A link inside a sentence is `mw-link`, not `mw-btn mw-btn-link`.** The base
30
- class carries a control height, so in running text it lifts that one line to
31
- 2.125rem while the lines around it stay where they are. `mw-p-0` does not help
32
- - the height does it, not the padding. Keep `mw-btn` only where the link
33
- really is a button in a row of buttons and has to line up with them
34
- - On a coarse pointer or below 768px `mw-btn` grows to a 2.75rem minimum height
35
- and `mw-btn-sm` to 2.5rem, on their own
36
-
37
- ```html
38
- <!-- a button that happens to look like a link -->
39
- <button type="button" class="mw-btn mw-btn-link">Read Article</button>
40
-
41
- <!-- a link inside a sentence -->
42
- <p>Built with <a href="https://astro.build" class="mw-link">Astro</a>.</p>
43
- ```
44
-
45
- ### Text links
46
-
47
- `mw-link` and `mw-link-muted` are the inline links, added in 4.13.0. Same look
48
- as the two button variants - no box, underline on hover - but without
49
- `inline-flex` and without the control height, which is what makes them sit on
50
- the line instead of above it. `font` and the focus ring are set explicitly, so
51
- a `<button>` in the middle of a paragraph works as well as an `<a>`.
52
-
53
- ```html
54
- <p>
55
- Built with <a href="https://astro.build" class="mw-link">Astro</a> and written
56
- up in the
57
- <a href="#get-started" class="mw-link-muted">Get Started</a> section.
58
- </p>
59
- ```
60
-
61
- ### Three emphasis levels
62
-
63
- `mw-btn-primary` (or any solid colour) **fills**, `mw-btn-outline` **draws a
64
- line**, `mw-btn-ghost` does **neither** until you point at it. Ghost is what a
65
- toolbar, a card's own actions or a row of icon buttons wants - five outlined
66
- boxes in a row fight the content they sit on.
67
-
68
- ```html
69
- <button type="button" class="mw-btn mw-btn-ghost">Rename</button>
70
- <button type="button" class="mw-btn mw-btn-ghost mw-btn-ghost-danger">
71
- Remove
72
- </button>
73
- ```
74
-
75
- Ghost is not `mw-btn-plain`: it keeps the button's shape, padding and hit area
76
- and only leaves them unpainted at rest. `mw-btn-plain` has no look at all.
77
-
78
- ### Icon-only buttons
79
-
80
- ```html
81
- <button
82
- type="button"
83
- class="mw-btn mw-btn-ghost mw-btn-icon"
84
- aria-label="Duplicate"
85
- >
86
- <i class="fas fa-copy"></i>
87
- </button>
88
- ```
89
-
90
- `mw-btn-icon` makes the button square and follows the size modifier next to it
91
- (`mw-btn-sm` / `mw-btn-lg`), growing to 2.75rem on a coarse pointer. It always
92
- needs an `aria-label` - there is no text to read out. Combine it with any
93
- variant; ghost is usually the right one.
94
-
95
- ### `mw-btn-plain` - a button with no button in it
96
-
97
- For everywhere a `<button>` is the right element and a button is the wrong look:
98
- a calendar day, a time in a request row, a settings tab, a modal backdrop.
99
-
100
- ```html
101
- <button type="button" class="mw-btn-plain">14:30</button>
102
- ```
103
-
104
- It clears `appearance`, background, border, padding and margin, inherits font
105
- and colour, and keeps exactly two things: the pointer and the focus ring. Used
106
- on its own - `mw-btn` is neither needed nor wanted next to it.
107
-
108
- Not the same as `mw-btn-link`, which is a button dressed as a link and brings
109
- its own colour and control height, nor as `mw-link`, which is the link inside a
110
- sentence.
111
-
112
- **Destructive actions use `mw-btn-danger`.** `mw-btn-secondary` is a brand
113
- colour, not a semantic one.
114
-
115
- ### Mini buttons
116
-
117
- Round 18px icon buttons - remove a row, close an alert, drop a tag.
118
-
119
- ```html
120
- <button type="button" class="mw-btn-mini mw-btn-mini-danger">
121
- <i class="fas fa-times"></i>
122
- </button>
123
- ```
124
-
125
- Variants: `mw-btn-mini-primary`, `-secondary`, `-danger`, `-success`. Plain
126
- `mw-btn-mini` is the neutral one. Works on `<a>` as well.
127
-
128
- ### Button bar
129
-
130
- Row of equally treated buttons, centered by default.
131
-
132
- ```html
133
- <div class="mw-button-bar mw-button-bar-primary">
134
- <button class="mw-btn">Previous</button>
135
- <button class="mw-btn">Overview</button>
136
- <button class="mw-btn">Next</button>
137
- </div>
138
- ```
139
-
140
- - Colouring: `mw-button-bar-primary`, `-secondary`, `-outline` style the bare
141
- `mw-btn` children, so the buttons carry no variant class themselves.
142
- `mw-button-bar-nav` mixes them for prev/center/next navigation.
143
- - Alignment: `mw-button-bar-left`, `-right`, `-center` (which is also the
144
- default), `mw-button-bar-between` pushes first and last apart.
145
- - Sizes: `mw-button-bar-sm`, `mw-button-bar-lg`.
146
- - Below 576px it stacks and gives every button the full width. Right for a row of
147
- independent actions - wrong for a switch, see below.
148
-
149
- ### `mw-actions-note` - one line above a row of actions
150
-
151
- ```html
152
- <div class="mw-modal-footer">
153
- <p class="mw-actions-note">This action cannot be undone.</p>
154
- <button class="mw-btn mw-btn-outline">Cancel</button>
155
- <button class="mw-btn mw-btn-danger">Delete</button>
156
- </div>
157
- ```
158
-
159
- Right-aligned, muted, one size smaller. For what the buttons apply to, why one
160
- of them is disabled, or that something cannot be taken back.
161
-
162
- - Works in `mw-modal-footer`, `mw-card-footer`, `mw-form-actions` and
163
- `mw-panel-footer`. It is a child of the action row, not a wrapper around it:
164
- the line takes the full width and pushes the buttons onto the row below.
165
- - Only rows that actually carry a note start wrapping, so adding one changes
166
- nothing anywhere else.
167
- - Inside `mw-form-actions` the alignment variants steer it as well -
168
- `mw-form-actions-left` makes the note left-aligned too.
169
- - `mw-form-actions-hint` is the old, form-only name for the same thing. Still
170
- styled, but use `mw-actions-note`: the hint name is wrong the moment the line
171
- sits in a modal.
172
-
173
- ### Segmented control
174
-
175
- Two or three positions of **one** switch: income/expense, offer/quote, a period
176
- picker.
177
-
178
- ```html
179
- <div class="mw-segmented">
180
- <button type="button" class="mw-segmented-item mw-active">Income</button>
181
- <button type="button" class="mw-segmented-item">Expense</button>
182
- </div>
183
- ```
184
-
185
- - Stays horizontal at every width and splits the row into equal shares - which
186
- is the whole point. Stacked in a `mw-button-bar`, two positions of a switch
187
- look like two buttons you could press both of.
188
- - Active position: `mw-active` on the item.
189
- - `mw-segmented-secondary` switches the active fill to the secondary colour,
190
- `mw-segmented-auto` shrinks the control to its content instead of filling the
191
- row - capped at `100%`, so labels wider than a 320px screen truncate instead
192
- of pushing the row off it. `disabled` works on an item.
193
- - Items reach a 2.5rem minimum height on a coarse pointer.
194
-
195
- ## Cards
196
-
197
- ```html
198
- <div class="mw-card">
199
- <div class="mw-card-img">
200
- <img src="cover.jpg" alt="" />
201
- </div>
202
- <div class="mw-card-body">
203
- <h3 class="mw-card-title">Title</h3>
204
- <hr class="mw-card-title-divider" />
205
- <p class="mw-card-subtitle">Subtitle in muted italics</p>
206
- <p class="mw-card-text">Body copy.</p>
207
- <div class="mw-card-footer">
208
- <button class="mw-btn mw-btn-primary">Open</button>
209
- <span class="mw-text-muted">12 items</span>
210
- </div>
211
- </div>
212
- </div>
213
- ```
214
-
215
- - `mw-card` is the shell; the hover lift is built in and has no modifier class.
216
- It is a flex column by default, so `mw-card-footer` sticks to the bottom and
217
- cards in a grid line up without any extra class.
218
- - `mw-card-simple` is the flat variant with even padding for arbitrary content.
219
- Use it when you only need a padded surface - it stays a block container, so
220
- inline children keep flowing side by side.
221
- - Image height: 210px, `mw-card-lg` 340px, `mw-card-xl` 480px (260px/340px below
222
- `sm`). Set `--mw-card-img-height` on the card for any other height.
223
- `mw-card-img-contain` shows the whole image instead of cropping it.
224
- - `mw-card-footer` pushes its last child to the right - with a single child that
225
- means it sits right, not left. Everything is vertically centered, so plain text
226
- lines up with a button next to it. Actions that no longer fit next to each
227
- other wrap.
228
- - The card measures **itself**, not the window - a card in a three-column grid
229
- is just as narrow on a 1200px desktop as it is on a phone, and looks the same
230
- in both. Every card declares `container: mw-card / inline-size` and takes
231
- `width: 100%`, so it fills its slot instead of sizing to its content. Give a
232
- card an explicit width if you need it to hug its content.
233
- - What steps on the card's own width: the footer stacks to full-width children
234
- below 360px, and the title, the subtitle and the body padding step up at
235
- 420px. The image height is the one that still follows the window, because it
236
- is declared on the card itself and a container cannot query itself.
237
- - `mw-tile` works the same way, and so do `mw-stepper`, both timelines, the
238
- gallery wrapper and the image slider: the step labels shrink and the two-sided
239
- timeline collapses to one side once the component is narrow, whatever the
240
- window does. One catch on tiles: a tile
241
- clips its overflow, and being a container makes it the containing block for
242
- `position: fixed`, so a `data-tooltip` **inside** a tile is cut off at its
243
- edge. Cards do not clip and are unaffected.
244
- - `mw-card-badge` (top right corner) and `mw-card-ribbon` (diagonal banner) are
245
- absolutely positioned overlays; colour them with `mw-card-addon-primary`,
246
- `-secondary`, `-success`, `-warning`, `-danger`, `-info`.
247
- - A card does not clip its content, so a tooltip or dropdown inside it can reach
248
- outside. The exception is `mw-card-ribbon`: that banner has to be cut off at
249
- the edge, so a card with one as its **direct child** switches to
250
- `overflow: hidden` and clips everything else too. A ribbon deeper inside - on
251
- a card nested in another card - does not make the outer one clip.
252
-
253
- ```html
254
- <div class="mw-card">
255
- <div class="mw-card-badge mw-card-addon-success">New</div>
256
- ...
257
- </div>
258
- ```
259
-
260
- **Feature frame** - wraps a card to lift one option out of a grid of equals,
261
- for the "most booked" plan in a pricing row:
262
-
263
- ```html
264
- <div class="mw-card-feature mw-card-feature-secondary">
265
- <p class="mw-card-feature-label">Most booked</p>
266
- <div class="mw-card">
267
- <div class="mw-card-body">...</div>
268
- </div>
269
- </div>
270
- ```
271
-
272
- - Unlike badge and ribbon this is a **wrapper around** the card, not an overlay
273
- inside it. The card itself stays untouched - same width, same height, same
274
- hover as its neighbours - and the frame plus its label bar grow outwards into
275
- the grid gap.
276
- - Colours are `mw-card-feature-secondary` and `mw-card-feature-info`; primary is
277
- the default and needs no class. This is a separate set from the
278
- `mw-card-addon-*` classes that colour badge and ribbon - the frame only comes
279
- in those three.
280
- - The label is a single line. It is set in uppercase at `2xs` and truncates with
281
- an ellipsis rather than wrapping, because the 22px bar height is baked into
282
- the frame's negative margins.
283
- - The frame reaches 29px above the card and 7px below it, so it sticks out of
284
- its grid cell. Any `mw-grid-*` that directly contains one therefore gets
285
- `row-gap: 56px` automatically, which leaves 20px between two stacked frames.
286
- CSS has no per-row gap, so this applies to every row of that grid - expect
287
- wrapped rows without a frame to sit further apart than usual. That is also why
288
- the bar is kept low: each extra pixel is charged to every row.
289
- - The space **above the first row** is not covered - the frame reaches into
290
- whatever sits there. Give the container the room (`mw-mt-10` on the grid, or
291
- `mw-mb-10` on the heading above it).
292
- - In a layout that is not a `mw-grid-*` - your own flex column, an Angular host
293
- with its own grid - nothing raises the gap for you. Leave at least 36px
294
- between stacked items yourself.
295
-
296
- **Stack card** - icon + title header with a gradient rule, for feature or
297
- tech-stack grids:
298
-
299
- ```html
300
- <div class="mw-card mw-card-stack">
301
- <div class="mw-card-stack-header">
302
- <div class="mw-card-stack-icon"><i class="fas fa-server"></i></div>
303
- <h5 class="mw-card-stack-title">Backend</h5>
304
- </div>
305
- <div class="mw-card-stack-body">
306
- <div class="mw-tags"><span class="mw-tags-item">Kotlin</span></div>
307
- <p class="mw-card-stack-text">Description.</p>
308
- </div>
309
- </div>
310
- ```
311
-
312
- ## Pricing
313
-
314
- Two pieces that are almost always needed together: `mw-price`, the figure on its
315
- own, and `mw-offer`, the card it is sold from.
316
-
317
- ### Price
318
-
319
- One baseline row - what it cost before, what it costs now, the unit, a discount
320
- tag:
321
-
322
- ```html
323
- <p class="mw-price">
324
- <span class="mw-price-original">
325
- <span class="mw-sr-only">Regular price:</span>159 Euro
326
- </span>
327
- <span class="mw-sr-only">Now:</span>
328
- <span class="mw-price-amount"
329
- >129<span class="mw-price-fraction">,90</span></span
330
- >
331
- <span class="mw-price-currency">Euro</span>
332
- <span class="mw-price-period">/ month</span>
333
- <span class="mw-tag mw-tag-secondary">-20%</span>
334
- </p>
335
- <p class="mw-price-note">Billed yearly, cancel any time.</p>
336
- ```
337
-
338
- - Everything in the row is sized in `em` off `--mw-price-size`, so
339
- `mw-price-xs` (1.3rem), `mw-price-sm` (1.5rem) and `mw-price-lg` (3rem) move
340
- the whole row at once - cents, unit and struck original included. Set the
341
- token yourself for any other size.
342
- - `mw-price-original` is muted **and** struck, and it takes its own line
343
- **above** the new price. Muted alone reads as a footnote; the line is what
344
- says this is no longer the price. The rule is drawn explicitly, because the
345
- browser default hairline disappears at this size against a muted tone.
346
- - Stacked is the default because inline the row runs long - "199 Euro 159 Euro
347
- -20 %" - and the first thing to wrap away when the width gives out is the
348
- discount tag, the one part that was there to catch the eye. `mw-price-inline`
349
- puts it back on one line for a wide card or a table row.
350
- - `mw-price-fraction` goes **inside** the amount and sits one step down on the
351
- baseline, not raised to the cap height - raised cents are a discounter's idiom
352
- and make a service price read cheaper than it is.
353
- - `mw-price-currency` and `mw-price-period` stop shrinking at 0.8rem, and
354
- `mw-price-original` at 0.9rem. At `mw-price-xs` the `em` chain would put them
355
- under 10px, which is where the floor earns its keep.
356
- - `mw-price-word` for "On request" or "Free" - bold, so it holds the same slot in
357
- a row of cards, but set well below a figure. Its line box is scaled back up to
358
- the amount's, so a word and a number in neighbouring cards land on one line.
359
- - `mw-price-note` is a **sibling** of the row, not a child - inside it, it would
360
- inherit the price size and join the baseline.
361
- - A `mw-tag` or `mw-badge` placed directly in the row is centred against the
362
- digits rather than hung off their baseline.
363
-
364
- | Class | What it does |
365
- | ------------------- | ---------------------------------------------- |
366
- | `mw-price-xs/sm/lg` | Retunes `--mw-price-size`, everything follows |
367
- | `mw-price-inline` | Old price in front of the new one, on one line |
368
- | `mw-price-center` | Centres the row |
369
- | `mw-price-plain` | Body colour instead of the brand |
370
-
371
- A struck-through price is a visual convention a screen reader does not pass on -
372
- `<s>` is announced by almost none of them. Wherever an old and a new price stand
373
- next to each other, name them with `mw-sr-only`, as above. Without it the reader
374
- hears two prices and no way to tell which one is charged.
375
-
376
- ### Offer card
377
-
378
- `mw-offer` goes on a `mw-card` and does the one thing the card cannot: it pushes
379
- the price to the bottom of the body, so the prices in a row line up even though
380
- the feature lists have different lengths.
381
-
382
- ```html
383
- <article class="mw-card mw-offer">
384
- <div class="mw-card-body">
385
- <h3 class="mw-card-title">Studio</h3>
386
- <hr class="mw-card-title-divider" />
387
- <p class="mw-card-text">Description.</p>
388
- <ul class="mw-list mw-list-check">
389
- <li>10 projects</li>
390
- </ul>
391
- <p class="mw-price mw-price-sm">…</p>
392
- <p class="mw-price-note">Per month, billed yearly.</p>
393
- <div class="mw-card-footer">
394
- <button class="mw-btn mw-btn-primary">Choose</button>
395
- </div>
396
- </div>
397
- </article>
398
- ```
399
-
400
- - Bottom-aligned, note included: a card that carries a `mw-price-note` and one
401
- that does not will **not** have their prices on the same line. Give every card
402
- in a row a note, or none of them.
403
- - To lift one plan out of the row, wrap it in `mw-card-feature` - the frame is
404
- the card's, not the offer's.
405
- - A price dropped into a `mw-card-footer` instead needs no modifier: it lines up
406
- with the button beside it on its own, and `mw-price-sm` keeps it from
407
- outweighing the action. That is the layout for a dated event, where the price
408
- is a detail rather than the offer.
409
- - Such a footer keeps its row once the card gets narrow, and gives the action
410
- half of it. A footer normally stacks its children to full width down there, so
411
- that two buttons do not squeeze - but a price is a label, not a second action,
412
- and a full-bleed button under a left-aligned price reads as a banner rather
413
- than as the end of a card.
414
-
415
- **Head band** - name and price in a solid bar at the top, for when the price is
416
- the thing being compared and the feature list is only its justification:
417
-
418
- ```html
419
- <article class="mw-card mw-offer mw-card-addon-secondary">
420
- <div class="mw-offer-head">
421
- <p class="mw-offer-name">Studio</p>
422
- <p class="mw-price mw-price-sm">…</p>
423
- </div>
424
- <div class="mw-card-body">…</div>
425
- </article>
426
- ```
427
-
428
- The band reads `mw-card-addon-*` for its colour - the same set the card badge and
429
- the ribbon use, primary without one. The price inherits the band's ink, and the
430
- card drops its top corner arc, which the band already owns.
431
-
432
- ### Billing cycle
433
-
434
- A subscription sold on several terms, where the longer one is cheaper. The
435
- switch is a `mw-segmented` **above** the grid, not one per card - three cards
436
- with three switches are three states the visitor has to keep in their head.
437
-
438
- ```html
439
- <div
440
- class="mw-segmented mw-segmented-auto"
441
- role="group"
442
- aria-label="Billing cycle"
443
- >
444
- <button type="button" class="mw-segmented-item mw-active" aria-pressed="true">
445
- Monthly
446
- </button>
447
- <button type="button" class="mw-segmented-item" aria-pressed="false">
448
- 6 months <span class="mw-tag mw-tag-secondary">-10%</span>
449
- </button>
450
- <button type="button" class="mw-segmented-item" aria-pressed="false">
451
- Yearly <span class="mw-tag mw-tag-secondary">-20%</span>
452
- </button>
453
- </div>
454
- ```
455
-
456
- - The headline figure stays **per month** on every term. "288 Euro" next to
457
- "30 Euro" is not a comparison a reader can make in their head; the amount that
458
- actually leaves the account belongs in the `mw-price-note` below
459
- ("288 Euro charged once for the year, cancel monthly").
460
- - The full monthly rate goes in `mw-price-original`, the saving in a `mw-tag`
461
- next to it. Both carry `hidden` on the monthly term - the framework resets
462
- `[hidden]` to `display: none !important`, so it beats a component's own
463
- `display` and an `el.hidden = true` or an Angular `[hidden]` binding works on
464
- a `mw-tag` too.
465
- - Wrap the price in `aria-live="polite"` and put `aria-pressed` on the switch
466
- items. The figure changes without the page moving, which a screen reader
467
- otherwise never hears.
468
- - Drop trailing zero cents: "24 Euro", not "24,00 Euro". `mw-price-amount` sets
469
- tabular figures, so the row does not jump when the term changes.
470
- - A tag inside the **active** item drops its colour and outlines itself in the
471
- item's ink - the colour variants mix their background from the page, and an
472
- orange pill on the blue fill comes out purple. Inactive items keep the colour,
473
- which is what makes a visitor click them.
474
-
475
- The switching itself is the application's job - the framework ships the look,
476
- not the arithmetic. The showcase wires it in a few lines at the bottom of
477
- `index.html`.
478
-
479
- ## Panels
480
-
481
- A bordered box with a header rule - the "titled section" of an application.
482
-
483
- ```html
484
- <div class="mw-panel mw-panel-primary">
485
- <h4 class="mw-panel-header">Filters</h4>
486
- <div class="mw-panel-body">...</div>
487
- <div class="mw-panel-footer">
488
- <button class="mw-btn mw-btn-outline">Reset</button>
489
- <button class="mw-btn mw-btn-primary">Apply</button>
490
- </div>
491
- </div>
492
- ```
493
-
494
- - Header colour: `mw-panel-primary`, `mw-panel-secondary`; without them the
495
- header is neutral. Title and action sit at opposite ends and wrap to two rows
496
- when the panel gets too narrow for both.
497
- - Height: `mw-panel-scrollable` (500px), `mw-panel-max-height-sm` (300px),
498
- `-md` (500px), `-lg` (700px) - the body scrolls, header and footer stay.
499
- - Panels in a grid row end up equally tall on their own - a grid item stretches.
500
- A height variant opts out with `align-self: start`.
501
-
502
- ## Tiles
503
-
504
- Compact info blocks. The look is on the class, the click is on the element: put
505
- `mw-tile` on an `<a>` or a `<button>` and it gets the hand cursor, the focus ring
506
- and the tap handling. On a `<div>` it stays a plain surface - no cursor, no ring,
507
- because there would be nothing to reach by keyboard.
508
-
509
- ```html
510
- <div class="mw-tiles mw-tiles-2col">
511
- <a href="/billing" class="mw-tile">
512
- <div class="mw-tile-img"><img src="icon.png" alt="" /></div>
513
- <h3 class="mw-tile-header">Billing</h3>
514
- <div class="mw-tile-body">
515
- <p>Invoices and payments</p>
516
- </div>
517
- <div class="mw-tile-footer">Updated 2026-01-02</div>
518
- </a>
519
- </div>
520
- ```
521
-
522
- Container: `mw-tiles` (1 / 2 at `sm` / 3 at `md`), `mw-tiles-2col` (1 / 2 at
523
- `sm`), `mw-tiles-4col` (1 / 2 at `sm` / 4 at `lg`) - the same ramps `mw-grid-3`,
524
- `-2` and `-4` use, and the full container width like any other grid.
525
- Tile sizes: `mw-tile-sm`, `mw-tile-lg`.
526
-
527
- ## Accordion
528
-
529
- ```html
530
- <div class="mw-accordion">
531
- <div class="mw-accordion-item">
532
- <button
533
- type="button"
534
- class="mw-accordion-header mw-active"
535
- aria-expanded="true"
536
- aria-controls="faq-1"
537
- >
538
- <span>Question</span>
539
- <i class="fas fa-chevron-down mw-accordion-icon"></i>
540
- </button>
541
- <div class="mw-accordion-content mw-active" id="faq-1">
542
- <div class="mw-accordion-content-inner">Answer</div>
543
- </div>
544
- </div>
545
- </div>
546
- ```
547
-
548
- The header is a `<button>` - it is the control that opens the panel, and a
549
- `<div>` is not focusable. A heading is not allowed inside a button, so the
550
- question is a `<span>` and the header carries the type itself; a nested `h3`
551
- still renders the same, for markup written before 4.12.
552
-
553
- Open state = `mw-active` on header **and** content (bare `active` still works
554
- but is deprecated). The icon rotates via the **header** state - putting the
555
- class on the icon instead does nothing. Content taller than 500px scrolls.
556
- The `mw-accordion-content-inner` wrapper is required and not decoration: the
557
- panel opens by animating a grid row and that wrapper is the row, so content
558
- placed straight into `mw-accordion-content` never collapses when closed.
559
- Toggling is JS - see `references/javascript.md`; the shipped script keeps
560
- `aria-expanded` in step when the header is a button.
561
-
562
- ## Tabs
563
-
564
- ```html
565
- <div class="mw-tabs">
566
- <div class="mw-tabs-nav">
567
- <button type="button" class="mw-tabs-nav-item mw-active" data-tab="tab1">
568
- Details
569
- </button>
570
- <button type="button" class="mw-tabs-nav-item" data-tab="tab2">
571
- History
572
- </button>
573
- </div>
574
- <div class="mw-tabs-content">
575
- <div class="mw-tabs-panel mw-active" id="tab1">...</div>
576
- <div class="mw-tabs-panel" id="tab2">...</div>
577
- </div>
578
- </div>
579
- ```
580
-
581
- - Variants: `mw-tabs-vertical` (nav on the left, horizontal again below `sm`),
582
- `mw-tabs-pills`.
583
- - `data-tab` matches the panel `id` - that pairing is only needed for the
584
- shipped JS. In a SPA, bind `mw-active` yourself and drop the attribute.
585
- - The shipped JS turns the strip into a real tablist: `role`, `aria-selected`,
586
- `aria-controls`, `aria-labelledby`, a roving `tabindex` and arrow/Home/End
587
- keys. Without it, that is yours to add - see `references/javascript.md`.
588
- - The nav scrolls horizontally instead of wrapping, and says so: a shadow shows
589
- on whichever side still has tabs behind it and disappears once that end is
590
- reached. Pure CSS, no scroll listener. The fade colour comes from
591
- `--mw-scroll-hint-cover`, which is preset to the page background and
592
- re-pointed to the card background inside `mw-card`, `mw-panel`, `mw-modal`,
593
- `mw-tile` and `mw-calendar` - override it if the strip sits on some other
594
- surface.
595
- - Tab items reach a 2.75rem minimum height on a coarse pointer.
596
-
597
- ## Modal
598
-
599
- ```html
600
- <dialog id="delete-modal" class="mw-modal mw-modal-sm" closedby="any">
601
- <div class="mw-modal-header">
602
- <h4 class="mw-modal-title">Delete invoice</h4>
603
- <button class="mw-modal-close" type="button" aria-label="Close">
604
- &#120299;
605
- </button>
606
- </div>
607
- <div class="mw-modal-body">
608
- <p>Invoice 2026-0042 will be removed.</p>
609
- </div>
610
- <div class="mw-modal-footer">
611
- <p class="mw-actions-note">This action cannot be undone.</p>
612
- <button class="mw-modal-close mw-btn mw-btn-outline">Cancel</button>
613
- <button class="mw-btn mw-btn-danger">Delete</button>
614
- </div>
615
- </dialog>
616
- ```
617
-
618
- - Escape, the focus trap, `inert` on the page behind it and the body scroll lock
619
- all come from the element. There is no overlay wrapper and no backdrop div.
620
- - Opened with `showModal()`, closed with `close()`. `main.js` wires the close
621
- buttons and exposes `mwOpenModal(id)` / `mwCloseModal(id)`; a trigger can also
622
- carry `data-mw-modal="delete-modal"` and needs no script at all.
623
- - `closedby="any"` dismisses it on a backdrop click; where that attribute is not
624
- understood yet the script handles the click instead.
625
- - Sizes: `mw-modal-sm` 370px, default 520px, `mw-modal-lg` 720px,
626
- `mw-modal-xl` 960px. Height is capped at 80-92dvh, the body scrolls. Below
627
- 576px it becomes a bottom sheet.
628
- - `mw-modal-close` is the hook, not a look: on the header X it styles the button,
629
- on a footer action it only closes. Combine it with `mw-btn mw-btn-primary` and
630
- the button keeps its own paint.
631
- - `mw-modal-body` takes a `mw-form` directly - the form brings the field gaps,
632
- the body brings the padding.
633
- - `mw-actions-note` is the muted line above the buttons - see below.
634
- - Name it with `aria-labelledby` pointing at the `mw-modal-title`; `role` and
635
- `aria-modal` come from `showModal()`. Without `autofocus` the browser focuses
636
- the first focusable element, usually the close button.
637
-
638
- Angular: `[open]="isOpen()"` does **not** work - the attribute opens a non-modal
639
- dialog with no top layer, backdrop or focus trap. It takes a directive calling
640
- `showModal()`, see `examples/angular-services.md`.
641
-
642
- ### The older overlay div
643
-
644
- Still styled, for markup that predates the `<dialog>` above. It cannot trap
645
- focus or make the page inert, so it is not what to write now.
646
-
647
- ```html
648
- <div id="delete-modal" class="mw-modal-overlay">
649
- <div class="mw-modal mw-modal-sm">…</div>
650
- <div class="mw-modal-backdrop"></div>
651
- </div>
652
- ```
653
-
654
- - `display: none` until `mw-modal-open` is added to the overlay - that class is
655
- the whole open/close mechanism, in Angular
656
- `[class.mw-modal-open]="isOpen()"`.
657
- - Body scroll lock comes from `body:has(.mw-modal-open)`.
658
- - `mw-modal-backdrop` is the click-to-close surface; put the handler on it.
659
-
660
- ## Alerts & toasts
661
-
662
- ```html
663
- <div class="mw-alert mw-alert-success">
664
- <div class="mw-alert-icon"><i class="fas fa-check-circle"></i></div>
665
- <div class="mw-alert-content">
666
- <span class="mw-alert-title">Saved</span>
667
- <p>The invoice was created.</p>
668
- </div>
669
- <button type="button" class="mw-btn-mini mw-btn-mini-danger mw-alert-close">
670
- <i class="fas fa-times"></i>
671
- </button>
672
- </div>
673
- ```
674
-
675
- Variants: `mw-alert-primary`, `-secondary`, `-success`, `-warning`, `-danger`,
676
- `-info`. `mw-alert-title` and the close button are optional. Dismissal runs in
677
- two steps: `mw-alert-closing` fades the alert out over `--mw-duration-base`, then
678
- `mw-alert-closed` hides it (`display: none`) - in a SPA prefer removing it from
679
- the list instead.
680
-
681
- **Toasts** are alerts inside a fixed stack:
682
-
683
- ```html
684
- <div class="mw-toast-stack mw-toast-stack-top-right">
685
- <div class="mw-alert mw-alert-success">...</div>
686
- </div>
687
- ```
688
-
689
- Positions: `mw-toast-stack-top-right` (also the default without a position
690
- class), `-top-center`, `-bottom-right`. Width is capped at
691
- `min(380px, 100vw - 2rem)`, clicks pass through everywhere except on a toast,
692
- the entry animation respects `prefers-reduced-motion`. Auto-dismiss is the
693
- application's job.
694
-
695
- ## Announcement
696
-
697
- The ribbon glued under the fixed header - a discount, a launch note, a
698
- maintenance window. One per page.
699
-
700
- ```html
701
- <aside class="mw-announcement mw-announcement-secondary">
702
- <a class="mw-announcement-content" href="#pricing">
703
- <span class="mw-announcement-highlight">-20 %</span>
704
- <span>Summer discount on all coachings until 06.09.</span>
705
- </a>
706
- </aside>
707
- ```
708
-
709
- Positioned fixed below the header and one z-index layer under it, and it stays
710
- there - the page scrolls beneath it. Variants: `mw-announcement-secondary`,
711
- `-success`, `-warning`, `-danger`, `-info` (primary is the default).
712
- `mw-announcement-highlight` is the pill in front of the text; the content
713
- element is usually an `<a>` but a plain `<div>` works.
714
- `mw-announcement-static` puts the ribbon into the document flow instead - it
715
- then scrolls with the content and shifts no anchor offset.
716
-
717
- The height lives in `--mw-announcement-height` and drives the anchor scroll
718
- offset (`scroll-padding-top`) automatically - a taller ribbon raises the token
719
- instead of fighting the offset.
720
-
721
- On a page whose header carries `mw-header-reveal` the ribbon leaves and arrives
722
- with it, without a class of its own - both cover the height of the pair, so they
723
- come in as one block. See `references/layout.md`.
724
-
725
- ## Empty state
726
-
727
- ```html
728
- <div class="mw-empty-state mw-empty-state-warning mw-empty-state-sm">
729
- <div class="mw-empty-state-icon"><i class="fas fa-inbox"></i></div>
730
- <p class="mw-empty-state-title">No results</p>
731
- <p class="mw-empty-state-desc">Try a different filter.</p>
732
- <button class="mw-btn mw-btn-outline">Clear filters</button>
733
- </div>
734
- ```
735
-
736
- Variants: `mw-empty-state-primary`, `-success`, `-warning`, `-danger`. Size:
737
- `mw-empty-state-sm`.
738
-
739
- ## Spinners
740
-
741
- ```html
742
- <div class="mw-spinner-container">
743
- <div class="mw-spinner-border mw-spinner-primary"></div>
744
- </div>
745
- ```
746
-
747
- - Shapes (one is mandatory): `mw-spinner-border`, `mw-spinner-dual-ring`,
748
- `mw-spinner-dots` (needs three empty `<div>` children).
749
- - Sizes `mw-spinner-sm`, `mw-spinner-lg`; colours `mw-spinner-primary`,
750
- `mw-spinner-secondary`.
751
- - `mw-spinner-container` centers in the available space.
752
- `mw-spinner-container-overlay` lays a dimmed, blurred scrim over the parent -
753
- the parent needs `position: relative`.
754
-
755
- ## Skeleton
756
-
757
- ```html
758
- <div class="mw-skeleton-card">
759
- <div class="mw-skeleton-row">
760
- <span class="mw-skeleton mw-skeleton-circle"></span>
761
- <div class="mw-skeleton-body">
762
- <span class="mw-skeleton mw-skeleton-text" style="width: 55%"></span>
763
- <span class="mw-skeleton mw-skeleton-text"></span>
764
- </div>
765
- </div>
766
- <span class="mw-skeleton mw-skeleton-rect"></span>
767
- <span class="mw-skeleton mw-skeleton-title"></span>
768
- <span class="mw-skeleton mw-skeleton-text"></span>
769
- </div>
770
- ```
771
-
772
- Shapes: `mw-skeleton-title`, `-text`, `-circle`, `-rect` (+ `-rect-sm`,
773
- `-rect-lg`). Widths are set inline when a line should look ragged.
774
-
775
- ## Tables
776
-
777
- ```html
778
- <div class="mw-table-responsive">
779
- <table class="mw-table mw-table-cards">
780
- <thead>
781
- <tr>
782
- <th>Name</th>
783
- <th>Status</th>
784
- <th class="mw-text-numeric">Amount</th>
785
- </tr>
786
- </thead>
787
- <tbody>
788
- <tr>
789
- <td data-label="Name">Jane Doe</td>
790
- <td data-label="Status">
791
- <span class="mw-tag mw-tag-success">Paid</span>
792
- </td>
793
- <td data-label="Amount" class="mw-text-numeric">1.204,50</td>
794
- </tr>
795
- </tbody>
796
- </table>
797
- </div>
798
- ```
799
-
800
- | Class | Effect |
801
- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
802
- | `mw-table` | Base: zebra rows, row hover, primary-coloured header |
803
- | `mw-table-subtle` | Quiet grey header with an accent rule - for data-heavy lists |
804
- | `mw-table-compact` | Less padding, smaller type |
805
- | `mw-table-hover` | Stronger row hover |
806
- | `mw-table-cards` | Below `md` every row becomes a card; keep `<thead>` (hidden via CSS) and give each cell a `data-label` |
807
- | `mw-table-responsive` | Wrapper, horizontal scroll |
808
- | `mw-table-responsive-hint` | On top of the wrapper: a soft right edge showing there are more columns. Opt-in - see below |
809
- | `mw-table-responsive-scroll` | Wrapper with a height cap (`--mw-table-scroll-height`, 400px / 260px below `sm`) and vertical scroll |
810
- | `mw-table-sticky-head` | Header stays put while the body scrolls - only works inside a height-limited wrapper, i.e. `mw-table-responsive-scroll` |
811
-
812
- `mw-table-responsive-hint` is opt-in, unlike the automatic hint on a tab bar,
813
- and works differently for a reason: a table paints its own opaque surface, so
814
- the gradient trick used on `mw-tabs-nav` would sit _behind_ the rows and never
815
- show. A mask sits in front and does show - but a mask has no `local` attachment,
816
- so it cannot fade away once the last column is reached. Put the class on the
817
- tables you know overflow, leave it off the ones that fit.
818
-
819
- ```html
820
- <div class="mw-table-responsive-scroll" style="--mw-table-scroll-height: 250px">
821
- <table class="mw-table mw-table-subtle mw-table-sticky-head">
822
- ...
823
- </table>
824
- </div>
825
- ```
826
-
827
- ## Kanban
828
-
829
- A board is a grid of equally wide lanes; a ticket is a plain `mw-card` with
830
- `mw-kanban-card` on top. Everything except the counters and the composer is CSS.
831
-
832
- The board puts as many lanes in a row as fit at 260px each and wraps the rest
833
- onto the next. It measures its own width for that, so a board inside a panel or
834
- a half-width column wraps on a desktop too.
835
-
836
- ```html
837
- <div class="mw-kanban" style="--mw-kanban-column-min-height: 390px">
838
- <div class="mw-kanban-column">
839
- <div class="mw-kanban-column-header">
840
- <h5 class="mw-kanban-title">In Progress</h5>
841
- <span class="mw-kanban-count">2</span>
842
- <button
843
- type="button"
844
- class="mw-btn mw-btn-secondary mw-btn-sm mw-kanban-add"
845
- aria-label="Add ticket to In Progress"
846
- >
847
- <i class="fas fa-plus"></i>
848
- </button>
849
- </div>
850
-
851
- <div class="mw-kanban-column-body">
852
- <article class="mw-card mw-kanban-card">
853
- <div class="mw-card-ribbon mw-card-addon-danger">High</div>
854
- <p class="mw-kanban-card-title">Sticky table header jitters</p>
855
- <p class="mw-kanban-card-text">
856
- On Safari the header shifts by a pixel.
857
- </p>
858
- <div class="mw-kanban-card-footer">
859
- <span class="mw-kanban-card-id">MW-102</span>
860
- <div class="mw-kanban-card-actions">
861
- <div class="mw-avatar mw-avatar-xs mw-avatar-initials">jd</div>
862
- <button
863
- type="button"
864
- class="mw-kanban-action"
865
- aria-label="Move MW-102 one lane left"
866
- >
867
- <i class="fas fa-chevron-left"></i>
868
- </button>
869
- <button
870
- type="button"
871
- class="mw-kanban-action"
872
- aria-label="Move MW-102 one lane right"
873
- >
874
- <i class="fas fa-chevron-right"></i>
875
- </button>
876
- <button
877
- type="button"
878
- class="mw-kanban-action"
879
- aria-label="Edit MW-102"
880
- >
881
- <i class="fas fa-pen"></i>
882
- </button>
883
- <button
884
- type="button"
885
- class="mw-kanban-action mw-kanban-action-danger"
886
- aria-label="Delete MW-102"
887
- >
888
- <i class="fas fa-trash"></i>
889
- </button>
890
- </div>
891
- </div>
892
- </article>
893
-
894
- <div class="mw-kanban-empty">No tickets</div>
895
- </div>
896
- </div>
897
- ...
898
- </div>
899
- ```
900
-
901
- | Class | Role |
902
- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
903
- | `mw-kanban` | Board. Grid, one column per lane, same width for all of them |
904
- | `mw-kanban-plain` | Board without its own surface or padding - for a board that already sits on a panel |
905
- | `mw-kanban-compact` | Tighter padding, description clamped to 2 lines instead of 4, lane floor 90px |
906
- | `mw-kanban-column` | Lane: dashed border, flex column |
907
- | `mw-kanban-column-header` | Title + counter + add button in one row |
908
- | `mw-kanban-title` | Lane title, uppercase, truncates |
909
- | `mw-kanban-count` | Ticket counter pill |
910
- | `mw-kanban-add` | Sits **on** `mw-btn` - only trims it to the header line height |
911
- | `mw-kanban-column-body` | Ticket stack; fills the lane so the empty state stays centred |
912
- | `mw-kanban-card` | Ticket. Needs `mw-card` next to it |
913
- | `mw-kanban-card-title` | Ticket title |
914
- | `mw-kanban-card-text` | Description, clamped to 4 lines (2 on a compact board) |
915
- | `mw-kanban-card-footer` | Rule + key on the left, avatar and actions on the right |
916
- | `mw-kanban-card-id` | Ticket key, monospaced so equal-length keys line up across cards |
917
- | `mw-kanban-card-actions` | Right-hand group; resets the avatar margin |
918
- | `mw-kanban-action` | 26px square icon button (32px below `md`, hit area grown to 44px), `mw-kanban-action-danger` turns the hover red |
919
- | `mw-kanban-empty` | Placeholder; hides itself as soon as the lane holds a ticket or an open composer |
920
- | `mw-kanban-card-in` | One-shot entry animation for a freshly created ticket |
921
-
922
- - **Priority is the regular `mw-card-ribbon`** and brings its own colour through
923
- `mw-card-addon-danger|warning|info`. It has to be a **direct child** of the
924
- card - that is what reserves the space next to the title. No ribbon means no
925
- priority; there is no separate priority class.
926
- - Lanes are as tall as the fullest one, with `--mw-kanban-column-min-height` as
927
- the floor (120px, 90px compact). Raise it per board so a board that starts out
928
- empty still reads as a board.
929
- - Board surface and lane border are `--mw-kanban-background` and
930
- `--mw-kanban-lane-border` - set them on the board, or drop the surface with
931
- `mw-kanban-plain`.
932
- - Responsive: below `lg` the lanes reflow into two columns, below `sm` into one.
933
- - The actions are ordinary buttons - which of them exist is your decision. Give
934
- every one an `aria-label` naming the ticket; the icon alone has no accessible
935
- name.
936
-
937
- ### Composer
938
-
939
- The inline form for creating and editing a ticket. One per board, moved into the
940
- lane it is needed in.
941
-
942
- ```html
943
- <form class="mw-kanban-composer mw-active">
944
- <input type="text" class="mw-input mw-input-sm mw-kanban-composer-title" />
945
- <textarea class="mw-textarea mw-kanban-composer-text" rows="2"></textarea>
946
- <div class="mw-kanban-composer-row">
947
- <select class="mw-select mw-select-sm mw-kanban-composer-priority">
948
- ...
949
- </select>
950
- <select class="mw-select mw-select-sm mw-kanban-composer-assignee">
951
- ...
952
- </select>
953
- </div>
954
- <div class="mw-kanban-composer-actions">
955
- <button type="button" class="mw-btn mw-btn-outline mw-btn-sm">
956
- Cancel
957
- </button>
958
- <button type="submit" class="mw-btn mw-btn-primary mw-btn-sm">Save</button>
959
- </div>
960
- </form>
961
- ```
962
-
963
- - **`mw-kanban-composer` is `display: none` until it also carries `mw-active`.**
964
- Rendering it conditionally is not enough - the class has to be there too, or
965
- the form stays invisible.
966
- - `mw-kanban-composer-row` puts priority and assignee side by side and stacks
967
- them once the lane gets too narrow; `mw-kanban-composer-actions` is the button
968
- row and gives its buttons the same 80px floor as a modal footer.
969
- - The `-title`, `-text`, `-priority`, `-assignee` and `-cancel` classes carry no
970
- styling of their own beyond a min-height on the textarea - they are the hooks
971
- the shipped JS queries. In a SPA you bind the controls yourself and can drop
972
- them.
973
- - `mw-kanban-editing` hides the ticket the composer is currently replacing.
974
-
975
- ## Calendar
976
-
977
- A card-shaped surface with a header and a seven column grid. A month is six
978
- rows, a week is one - same classes, same cell size, the only difference is how
979
- many cells you render. Which page is on screen is application state: the shipped
980
- JS renders it from `data-calendar`, an Angular app renders the same markup
981
- itself.
982
-
983
- ```html
984
- <div
985
- class="mw-calendar"
986
- data-calendar="month"
987
- data-calendar-markers='{"2026-08-19": ["success", "warning"]}'
988
- >
989
- <div class="mw-calendar-header">
990
- <button
991
- type="button"
992
- class="mw-btn mw-btn-outline mw-btn-sm mw-calendar-nav"
993
- data-calendar-nav="-1"
994
- aria-label="Previous month"
995
- >
996
- <i class="fas fa-chevron-left"></i>
997
- </button>
998
-
999
- <div class="mw-calendar-title" aria-live="polite">August 2026</div>
1000
-
1001
- <button
1002
- type="button"
1003
- class="mw-btn mw-btn-outline mw-btn-sm mw-calendar-nav"
1004
- data-calendar-nav="1"
1005
- aria-label="Next month"
1006
- >
1007
- <i class="fas fa-chevron-right"></i>
1008
- </button>
1009
- </div>
1010
-
1011
- <div class="mw-calendar-grid">
1012
- <div class="mw-calendar-weekday" aria-hidden="true">Mon</div>
1013
- ...
1014
- <div class="mw-calendar-weekday mw-calendar-weekend" aria-hidden="true">
1015
- Sat
1016
- </div>
1017
-
1018
- <button
1019
- type="button"
1020
- class="mw-calendar-day mw-calendar-adjacent"
1021
- aria-label="Monday, 27 July 2026"
1022
- >
1023
- <span class="mw-calendar-date">27</span>
1024
- </button>
1025
-
1026
- <button
1027
- type="button"
1028
- class="mw-calendar-day mw-calendar-today"
1029
- aria-current="date"
1030
- aria-pressed="false"
1031
- aria-label="Wednesday, 19 August 2026"
1032
- >
1033
- <span class="mw-calendar-date">19</span>
1034
- <span class="mw-calendar-dots">
1035
- <span class="mw-calendar-dot mw-calendar-dot-success"></span>
1036
- <span class="mw-calendar-dot mw-calendar-dot-warning"></span>
1037
- </span>
1038
- </button>
1039
- ...
1040
- </div>
1041
-
1042
- <div class="mw-calendar-legend">
1043
- <span class="mw-calendar-legend-item">
1044
- <span class="mw-calendar-dot mw-calendar-dot-success"></span>
1045
- Slots free
1046
- </span>
1047
- ...
1048
- </div>
1049
- </div>
1050
- ```
1051
-
1052
- | Class | Role |
1053
- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
1054
- | `mw-calendar` | Surface. Same border, radius and shadow as a card, without the hover lift |
1055
- | `mw-calendar-plain` | Drops the surface - for a calendar already sitting on a card or panel |
1056
- | `mw-calendar-compact` | Date picker density: dots hidden, 29px cells, tighter header and labels |
1057
- | `mw-calendar-header` | Arrow, title, arrow |
1058
- | `mw-calendar-nav` | Sits **on** `mw-btn` - only squares it off around the chevron |
1059
- | `mw-calendar-title` | Takes the space between the arrows, stays optically centred |
1060
- | `mw-calendar-grid` | The seven column grid; weekday labels and day cells are its only children |
1061
- | `mw-calendar-weekday` | Column label. Decorative - the day buttons carry the weekday themselves |
1062
- | `mw-calendar-day` | One day. A `button`, so `:disabled` gives you an unavailable day |
1063
- | `mw-calendar-date` | The number |
1064
- | `mw-calendar-adjacent` | Day of the neighbouring month - muted, still readable |
1065
- | `mw-calendar-weekend` | Saturday/Sunday. Tinted cell; on the label it turns the text secondary |
1066
- | `mw-calendar-today` | Outlined in the secondary colour, bold |
1067
- | `mw-selected` | Filled with the primary colour |
1068
- | `mw-calendar-dots` | Dot row, absolutely placed at the bottom of the cell |
1069
- | `mw-calendar-dot` | 5px dot. Colour comes from `--mw-calendar-dot`; the `-primary`, `-secondary`, `-success`, `-warning`, `-danger`, `-info` classes are presets for it |
1070
- | `mw-calendar-legend` | Rule plus a row of dot/label pairs below the grid |
1071
- | `mw-calendar-legend-item` | One dot/label pair |
1072
-
1073
- - **Today is outlined in secondary, the picked day is filled with primary.** Two
1074
- different colours on purpose: an outline and a fill in the same colour read as
1075
- two states of the same thing. Do not give one day both.
1076
- - The dots are positioned absolutely, so a day without any keeps the exact same
1077
- height and its number sits on the same line as every other. Two or three per
1078
- day stay readable, more do not. A dot on a picked day keeps its status colour;
1079
- only the untoned one flips to the accent text colour.
1080
- - **What a dot means belongs to the caller.** The same calendar reads "request
1081
- pending / booked / time offered" in a scheduling app and something entirely
1082
- different on a public booking page, so the colour is a custom property, not a
1083
- fixed set: `style="--mw-calendar-dot: #7a4fd4"` on the dot, or on the cell to
1084
- colour all of its dots. The six tone classes are named presets for exactly
1085
- that property - use them when they fit, ignore them when they do not.
1086
- - **Render the days spilling in from the neighbouring month.** An empty first
1087
- row reads like a broken calendar, not like a short month. `mw-calendar-adjacent`
1088
- is what pushes them back.
1089
- - A month gets six rows even when five would do, so the calendar - and
1090
- everything under it - keeps its height while you page through.
1091
- - Cell height is fixed (38px, 44px on a coarse pointer, 29px compact), width
1092
- follows the container. No aspect ratio: a full-width calendar would otherwise
1093
- grow rows several hundred pixels tall. A calendar therefore wants a column,
1094
- not the full page width - give it one, or cap it.
1095
- - **A week has no class of its own.** Render seven cells instead of forty-two
1096
- and you have one; everything else is identical.
1097
- - `mw-calendar-compact` hides the dots and shortens the cell - the dots are what
1098
- a full cell needs its height for, so the two go together. It costs about a
1099
- third of the calendar height and leaves a plain date picker; drop the legend
1100
- there, it has nothing left to explain. 29px is below a comfortable tap target,
1101
- so keep it for pointer-first surfaces. Its grid gap drops to 2px as well, which
1102
- puts the space back into the cells.
1103
- - Give every day an `aria-label` with the full date - the bare number is not an
1104
- accessible name. The weekday labels are `aria-hidden`, they would only repeat
1105
- it. With a `button` per day, keep one tab stop for the grid (roving
1106
- `tabindex`) instead of 42.
1107
-
1108
- ## Tags
1109
-
1110
- Two forms, picked by count - see also the pitfall list in `SKILL.md`.
1111
-
1112
- **Single chip** (table cells, status columns) - carries its colour itself:
1113
-
1114
- ```html
1115
- <span class="mw-tag mw-tag-success">Paid</span>
1116
- ```
1117
-
1118
- Variants: `mw-tag-primary`, `-secondary`, `-success`, `-info`, `-warning`,
1119
- `-danger`, `-muted`, `-neutral`. Size: `mw-tag-lg`.
1120
-
1121
- `mw-tag-muted` and `mw-tag-neutral` are not the same and the difference carries
1122
- meaning. **Muted** means the label steps back - draft, archived, a shortcoming
1123
- ("no e-invoice"). **Neutral** is an identifier with no judgement attached and
1124
- keeps the normal text colour - a customer number, a document type, a price on a
1125
- category ("XRechnung" is a property, not a verdict). Both are also available on
1126
- the list container: `mw-tags-muted`, `mw-tags-neutral`.
1127
-
1128
- **List** - the container carries the colour for all its items:
1129
-
1130
- ```html
1131
- <div class="mw-tags mw-tags-primary">
1132
- <span class="mw-tags-item"
1133
- ><i class="fas fa-check mw-tags-icon"></i>Angular</span
1134
- >
1135
- <span class="mw-tags-item mw-tags-removable">
1136
- Kotlin
1137
- <button type="button" class="mw-btn-mini mw-btn-mini-danger mw-tags-remove">
1138
- <i class="fas fa-times"></i>
1139
- </button>
1140
- </span>
1141
- </div>
1142
- ```
1143
-
1144
- Container variants: `mw-tags-primary`, `-secondary`, `-success`, `-info`,
1145
- `-warning`, `-danger`, `-muted`, size `mw-tags-lg`. `mw-tags-remove` only
1146
- positions the button - its look comes from `mw-btn-mini` plus a colour variant.
1147
-
1148
- ## Badges
1149
-
1150
- A count or a status, not a label. See the pitfall list in `SKILL.md` for the
1151
- badge-versus-tag rule: a tag names something and sits in a row of its own kind,
1152
- a badge carries a number or a state and usually sits _on_ something.
1153
-
1154
- ```html
1155
- <span class="mw-badge mw-badge-danger">99+</span>
1156
- ```
1157
-
1158
- Colours: `mw-badge-primary` (the default), `-secondary`, `-success`, `-warning`,
1159
- `-danger`, `-info`, `-muted`, plus `mw-badge-outline`. Sizes: `mw-badge-sm`,
1160
- `mw-badge-lg`.
1161
-
1162
- Round on one character, a pill from two on - a `min-width` the padding grows
1163
- past. The digits are `tabular-nums`, so a counter ticking 9 to 10 does not shift
1164
- what sits beside it.
1165
-
1166
- **On an icon or a button:**
1167
-
1168
- ```html
1169
- <span class="mw-badge-anchor">
1170
- <button class="mw-btn mw-btn-outline">
1171
- <i class="fas fa-inbox"></i> Inbox
1172
- </button>
1173
- <span class="mw-badge mw-badge-danger mw-badge-float">5</span>
1174
- </span>
1175
- ```
1176
-
1177
- `mw-badge-anchor` on the thing being badged, `mw-badge-float` on the badge. It
1178
- hangs half off the corner and carries a ring in the surface colour, so it reads
1179
- as a separate object rather than a blob on the icon. The ring follows the
1180
- surface automatically inside a card, panel, modal, tile or the header.
1181
-
1182
- **Status:**
1183
-
1184
- ```html
1185
- <span class="mw-badge-status mw-badge-status-success">
1186
- <span class="mw-badge-dot"></span> Online
1187
- </span>
1188
- ```
1189
-
1190
- `mw-badge-status-{primary|secondary|success|warning|danger|info|muted}` colours
1191
- the dot inside it. A bare `mw-badge-dot-{colour}` works on its own too. Add
1192
- `mw-badge-pulse` for something actually running - a job, an open connection: a
1193
- ring grows out and fades while the dot underneath stays put, so a column of them
1194
- in a table stays readable. Not for decoration.
1195
-
1196
- ## Dropdown
1197
-
1198
- Built on `<details>` / `<summary>`. That is where the keyboard handling, the
1199
- focus behaviour and the open state a screen reader can see come from, and it
1200
- works with JavaScript switched off. The framework script only adds Escape and
1201
- click-outside.
1202
-
1203
- ```html
1204
- <details class="mw-dropdown">
1205
- <summary class="mw-btn mw-btn-primary">
1206
- Actions <i class="fas fa-chevron-down mw-dropdown-caret"></i>
1207
- </summary>
1208
- <div class="mw-dropdown-menu">
1209
- <div class="mw-dropdown-label">Manage</div>
1210
- <button class="mw-dropdown-item"><i class="fas fa-pen"></i> Edit</button>
1211
- <button class="mw-dropdown-item mw-active">Duplicate</button>
1212
- <button class="mw-dropdown-item" disabled>Move</button>
1213
- <hr class="mw-dropdown-divider" />
1214
- <button class="mw-dropdown-item mw-dropdown-item-danger">
1215
- <i class="fas fa-trash"></i> Delete
1216
- </button>
1217
- </div>
1218
- </details>
1219
- ```
1220
-
1221
- Parts: `mw-dropdown-menu`, `-item`, `-item-danger`, `-divider`, `-label`,
1222
- `-caret` (rotates with the open state on its own). Alignment:
1223
- `mw-dropdown-end` anchors the menu to the trigger's right edge,
1224
- `mw-dropdown-up` opens it upward.
1225
-
1226
- `mw-active` (or `aria-checked="true"`) marks the current choice, for a menu that
1227
- picks rather than acts. Icons inside items keep one column, so labels line up
1228
- whether or not every item has one.
1229
-
1230
- Where the browser supports anchor positioning the menu is `position: fixed`,
1231
- anchored to its trigger and flipped into whichever side has room, so it escapes
1232
- a clipping ancestor by itself. Everywhere else it is absolutely positioned and
1233
- is clipped by any ancestor that hides its overflow. The framework's own containers lift that clip while a menu is open; on
1234
- your own it is `:has(.mw-dropdown[open]) { overflow: visible }`.
1235
-
1236
- On a coarse pointer the rows grow to 2.75rem and the menu takes at least the
1237
- trigger's full width.
1238
-
1239
- ## Language switcher
1240
-
1241
- A `mw-dropdown` with a quieter trigger: no fill and no border until it is
1242
- hovered, so a header bar does not turn into a row of competing buttons.
1243
-
1244
- ```html
1245
- <details class="mw-dropdown mw-dropdown-end mw-lang-switch">
1246
- <summary aria-label="Language: Deutsch">
1247
- <span class="mw-flag mw-flag-de"></span>
1248
- <span class="mw-lang-switch-code">de</span>
1249
- <i class="fas fa-chevron-down mw-dropdown-caret"></i>
1250
- </summary>
1251
- <div class="mw-dropdown-menu">
1252
- <button
1253
- type="button"
1254
- class="mw-dropdown-item mw-active"
1255
- aria-current="true"
1256
- data-mw-lang="de"
1257
- >
1258
- <span class="mw-flag mw-flag-de"></span>
1259
- <span class="mw-lang-switch-name" lang="de">Deutsch</span>
1260
- <i class="fas fa-check mw-lang-switch-check"></i>
1261
- </button>
1262
- <button type="button" class="mw-dropdown-item" data-mw-lang="en">
1263
- <span class="mw-flag mw-flag-gb"></span>
1264
- <span class="mw-lang-switch-name" lang="en">English</span>
1265
- <i class="fas fa-check mw-lang-switch-check"></i>
1266
- </button>
1267
- </div>
1268
- </details>
1269
- ```
1270
-
1271
- The two-letter code carries the state, not the flag: a flag is a country and
1272
- never a language - no country stands for English. Write each language name in
1273
- its own language and put `lang="fr"` on it, so a screen reader pronounces it
1274
- instead of spelling it out.
1275
-
1276
- Parts: `mw-lang-switch-code`, `-name`, `-check` (the tick, shown by `mw-active`
1277
- on the item and kept in the layout while hidden). `mw-lang-switch-inverted` is
1278
- for a dark bar built without `mw-header`; inside `mw-header` those colours apply
1279
- on their own.
1280
-
1281
- Flags: `mw-flag` plus `mw-flag-<iso>` for de, at, ch, gb, us, ie, fr, it, es,
1282
- pt, nl, be, pl, hu, ro, bg, ua, se, dk, no, fi, tr and jp - CSS gradients, all
1283
- in the same 4:3 box whatever the real ratio. Never an emoji flag: Windows ships
1284
- no glyphs for them. Anything outside the set goes as an `<img>` or an inline
1285
- `<svg>` inside `mw-flag`.
1286
-
1287
- ## Keyboard keys
1288
-
1289
- `<kbd>` is styled directly, so a shortcut in prose needs no class:
1290
-
1291
- ```html
1292
- <p>
1293
- Press
1294
- <span class="mw-kbd-group"
1295
- ><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd></span
1296
- >
1297
- to open the palette, <kbd>Esc</kbd> to close.
1298
- </p>
1299
- ```
1300
-
1301
- `mw-kbd` is the same look for a `<span>` when the markup is not yours to change.
1302
- `mw-kbd-group` wraps a combination so the pluses are spaced and the whole thing
1303
- cannot wrap apart. `mw-kbd-pressed` shows a key mid-press - the raised bottom
1304
- edge drops and the key sits 1px lower.
1305
-
1306
- ## Info badges & counters
1307
-
1308
- ```html
1309
- <span class="mw-info mw-info-primary">Active</span>
1310
-
1311
- <a href="/invoices" class="mw-info mw-info-counter mw-info-success">
1312
- <div class="mw-info-value">150</div>
1313
- <div class="mw-info-label">Invoices</div>
1314
- </a>
1315
-
1316
- <span class="mw-info-mini mw-info-mini-danger"
1317
- ><i class="fas fa-times"></i
1318
- ></span>
1319
- ```
1320
-
1321
- - `mw-info`: sizes `mw-info-sm`, `-lg`, `-xl`; colours `mw-info-primary`,
1322
- `-secondary`, `-success`, `-warning`, `-danger`, `-info` (tinted background +
1323
- a 6px left border).
1324
- - `mw-info-counter` turns it into a big number over a small label
1325
- (`mw-info-value`, `mw-info-label`).
1326
- - The cursor is `default` because a figure is usually not clickable. On `<a>`
1327
- and `<button>` it turns into a pointer automatically; on anything else use
1328
- `mw-info-clickable`.
1329
- - `mw-info-block` is the same surface carrying a paragraph: 68 characters
1330
- wide, left-aligned, and it does not lift on hover. It sits at the left edge
1331
- like everything else with running text in it - `mw-info-block-center` puts it
1332
- back in the middle of its container.
1333
- - `mw-info-mini` is the 20px status dot: sizes `-sm`, `-lg`, `-xl`, colours
1334
- `-primary`, `-secondary`, `-success`, `-warning`, `-danger`, `-info`.
1335
-
1336
- ## Progress
1337
-
1338
- ```html
1339
- <div class="mw-progress-container">
1340
- <div class="mw-progress-barinfo">
1341
- <span class="mw-progress-label">Completion</span>
1342
- <span class="mw-progress-value mw-progress-percent">75</span>
1343
- </div>
1344
- <div class="mw-progress-bar">
1345
- <div class="mw-progress-fill mw-progress-primary" style="width: 75%"></div>
1346
- </div>
1347
- </div>
1348
- ```
1349
-
1350
- - The value element only prints what you put in it; `mw-progress-percent`
1351
- appends `%`, `mw-progress-numeric` appends `/10`. Neither computes anything.
1352
- - The fill width is set by you (`style="width: 75%"`). The shipped JS instead
1353
- reads `data-value="75"` and animates it into view - in a SPA bind the style.
1354
- - Colours: `mw-progress-primary`, `-secondary`, `-success`, `-warning`,
1355
- `-danger`, `-info` (default fill is the secondary colour).
1356
- - Sizes on the container: `mw-progress-sm`, `mw-progress-lg`.
1357
- - `mw-progress-hide-info` on the info row hides the labels while keeping the
1358
- layout; `mw-progress-inline-label` on the container puts the text inside the
1359
- bar.
1360
-
1361
- ## Rating
1362
-
1363
- ```html
1364
- <div class="mw-rating-container">
1365
- <p class="mw-rating-info">Product rating</p>
1366
- <div class="mw-rating mw-rating-lg" data-rating="4">
1367
- <i class="mw-rating-icon fas fa-thumbs-up" aria-hidden="true"></i>
1368
- <!-- five icons total -->
1369
- </div>
1370
- </div>
1371
- ```
1372
-
1373
- `data-rating` is `0`-`5` and highlights the first N icons - pure CSS, any icon
1374
- works. Sizes: `mw-rating-sm`, `mw-rating-lg`.
1375
-
1376
- ## Meta header
1377
-
1378
- Small icon + text counters, e.g. under a page title.
1379
-
1380
- ```html
1381
- <div class="mw-meta-header">
1382
- <div class="mw-meta-item">
1383
- <i class="fas fa-list"></i><span>Total: 14</span>
1384
- </div>
1385
- <div class="mw-meta-item">
1386
- <i class="fas fa-pen"></i><span>Drafts: 3</span>
1387
- </div>
1388
- </div>
1389
- ```
1390
-
1391
- ## Stepper
1392
-
1393
- ```html
1394
- <div class="mw-stepper">
1395
- <div class="mw-stepper-step">
1396
- <div class="mw-stepper-indicator mw-done"><i class="fas fa-check"></i></div>
1397
- <span class="mw-stepper-label mw-done">Account</span>
1398
- </div>
1399
- <div class="mw-stepper-connector mw-done"></div>
1400
- <div class="mw-stepper-step">
1401
- <div class="mw-stepper-indicator mw-active">2</div>
1402
- <span class="mw-stepper-label mw-active">Details</span>
1403
- </div>
1404
- <div class="mw-stepper-connector"></div>
1405
- <div class="mw-stepper-step">
1406
- <div class="mw-stepper-indicator">3</div>
1407
- <span class="mw-stepper-label">Review</span>
1408
- </div>
1409
- </div>
1410
- ```
1411
-
1412
- State classes here are prefixed: `mw-done`, `mw-active`. The horizontal variant
1413
- needs the explicit `mw-stepper-connector` elements between the steps; the
1414
- vertical one (`mw-stepper-vertical`) draws the line itself and takes
1415
- `mw-stepper-content` with `mw-stepper-content-title` / `-desc` instead of a
1416
- label:
1417
-
1418
- ```html
1419
- <div class="mw-stepper-vertical">
1420
- <div class="mw-stepper-step mw-done">
1421
- <div class="mw-stepper-indicator mw-done"><i class="fas fa-check"></i></div>
1422
- <div class="mw-stepper-content">
1423
- <p class="mw-stepper-content-title">Dependencies installed</p>
1424
- <p class="mw-stepper-content-desc">All packages resolved.</p>
1425
- </div>
1426
- </div>
1427
- </div>
1428
- ```
1429
-
1430
- ## Timelines
1431
-
1432
- Both are chronicles (CV, changelog), not schedulable time axes.
1433
-
1434
- ```html
1435
- <div class="mw-timeline-big">
1436
- <div class="mw-timeline-big-step mw-active">
1437
- <div class="mw-timeline-big-date-container">
1438
- <div class="mw-timeline-big-date-main">01/2023 - today</div>
1439
- <div class="mw-timeline-big-date-sub">(3 years)</div>
1440
- </div>
1441
- <div class="mw-timeline-big-content">
1442
- <div class="mw-card">...</div>
1443
- <div class="mw-timeline-big-event">
1444
- <span class="mw-timeline-big-event-date">05/2023</span>
1445
- <span class="mw-timeline-big-event-title">Certification</span>
1446
- </div>
1447
- </div>
1448
- </div>
1449
- </div>
1450
-
1451
- <div class="mw-timeline-simple">
1452
- <div class="mw-timeline-simple-step mw-active">
1453
- <div class="mw-timeline-simple-date">2024-03-22</div>
1454
- <div class="mw-timeline-simple-content">
1455
- <div class="mw-card mw-card-simple">...</div>
1456
- </div>
1457
- </div>
1458
- </div>
1459
- ```
1460
-
1461
- The current step gets `mw-active` (bare `active` still works but is
1462
- deprecated). Any card fits into the content wrapper.
1463
-
1464
- ## Avatars
1465
-
1466
- ```html
1467
- <div class="mw-avatar mw-avatar-lg mw-avatar-primary">
1468
- <img src="me.jpg" alt="" />
1469
- </div>
1470
-
1471
- <div class="mw-avatar mw-avatar-initials mw-avatar-xs">MW</div>
1472
- ```
1473
-
1474
- - Sizes: `mw-avatar-xs` 32, `-sm` 64, default 96, `-lg` 144, `-xl` 210 px.
1475
- - Shape: `mw-avatar-square`.
1476
- - `mw-avatar-landscape` (3:2) and `mw-avatar-portrait` (9:16) for a photo that
1477
- is not square - meant for `-lg` and `-xl`, with 20px corners that
1478
- `mw-avatar-square` still sharpens. A landscape avatar keeps the height of its
1479
- size step, a portrait one the width; on `-lg` a portrait is 2:3 instead, as
1480
- tall as the 9:16 one and wider.
1481
- - Every avatar carries the accent arc on its top-right corner, inside the frame
1482
- (`--mw-corner-accent`). It stays as it is on hover.
1483
- - Colours `mw-avatar-primary`, `-secondary`, `-success`, `-warning`, `-danger`,
1484
- `-info` tint the **border**. On `mw-avatar-initials` the same class also tints
1485
- the **surface**, so both can be combined.
1486
- - `mw-avatar-initials` centers text and scales the font with the size class -
1487
- the standard display for a signed-in user without a picture.
1488
- - `mw-avatar-group` overlaps its avatars; `mw-avatar-group-sm` / `-lg` change
1489
- the overlap.
1490
- - Hover on an avatar **with an image** zooms the picture and sweeps a light
1491
- across it. Nothing to switch on. It is skipped on `mw-avatar-initials`, on
1492
- touch, under `prefers-reduced-motion` and under `mw-hover-static`.
1493
-
1494
- ## Date stamp
1495
-
1496
- The date on a dated card - an event, a changelog entry, a day on a menu. It
1497
- answers to the **card**, not the viewport: narrow, it is a muted line above the
1498
- title; once the card is wide enough that a single column would leave its right
1499
- half empty, the same markup becomes a column on the left.
1500
-
1501
- ```html
1502
- <article class="mw-card mw-offer">
1503
- <div class="mw-card-body">
1504
- <p class="mw-date-stamp mw-date-stamp-today">
1505
- <span class="mw-date-stamp-day">Fri</span>
1506
- <span class="mw-date-stamp-date">18.09.</span>
1507
- </p>
1508
- <div class="mw-date-stamp-body">
1509
- <h4 class="mw-text-base mw-text-bold mw-mb-2">Baked fish fillet</h4>
1510
- <p class="mw-text-sm mw-text-muted mw-mb-0">In beer batter.</p>
1511
- </div>
1512
- <p class="mw-price mw-price-xs">
1513
- <span class="mw-price-amount">11.90</span>
1514
- <span class="mw-price-currency">Euro</span>
1515
- </p>
1516
- </div>
1517
- </article>
1518
- ```
1519
-
1520
- - `mw-date-stamp-body` wraps everything that is not the stamp or the price. It
1521
- is `display: contents` on a narrow card, so the body still sees the title and
1522
- the price as its own children - which is what `mw-offer` selects on. On a wide
1523
- card it becomes the middle column.
1524
- - The price is a **sibling** of the wrapper, not inside it. Give the card one
1525
- and it gets a third column at the right edge; leave it out and the card is two
1526
- columns. With more than one dish per card, put the price on a
1527
- `mw-leader-row` with the dish instead - an empty third track would still
1528
- charge the card a gap.
1529
- - Write the day as `Fri`, not `FRI` - the uppercase is `text-transform`, so a
1530
- screen reader still reads a word rather than three letters.
1531
- - Day column width, day font size, the column gap and the card's block padding
1532
- all ramp with the **card**, from 420px to 900px wide, via
1533
- `fluid-container()`. A narrow card in a wide window gets narrow-card sizing -
1534
- which is the point of putting the switch in a container query at all.
1535
- - `mw-date-stamp-today` drops the muting for a **fill**: on a wide card it spans
1536
- the full card height, rounded to the card's own left corners and cut square
1537
- against the content. A text colour would compete with the price, which is
1538
- already the brand colour.
1539
- - Cards in a stack want `mw-grid-even` next to `mw-grid-1`, or the card with the
1540
- longer description ends up taller than the rest of the week.
1541
-
1542
- ## Contact
1543
-
1544
- How to reach someone - postal address, phone, mail. It belongs on an
1545
- `<address>`, and undoing that element's italics is most of what the component
1546
- does; the rest is a label column wide enough that the values line up.
1547
-
1548
- ```html
1549
- <address class="mw-contact">
1550
- <p class="mw-text-bold">Gasthaus Lamm</p>
1551
- <p>Marktplatz 3<br />72622 Musterhausen</p>
1552
- <p class="mw-contact-row">
1553
- <span class="mw-contact-label">Phone</span>
1554
- <a class="mw-link" href="tel:+497123456789">07123 456789</a>
1555
- </p>
1556
- </address>
1557
- ```
1558
-
1559
- - `<address>` is for the contact details of its nearest `<article>` or of the
1560
- page - not for any postal address that happens to be on it.
1561
- - The label column is 5rem and fixed. An auto column is as wide as its own word,
1562
- so "Phone" and "Email" would start their values in different places.
1563
- - Wrap phone and mail in `tel:` and `mailto:` links and mark them `mw-link` -
1564
- in a block of plain lines an unstyled link reads as text.
1565
- - Opening hours are **not** part of this. A day, its times and a line between
1566
- them is a `mw-leader-row` in a `<dl>`, one row per day, and a note for the
1567
- rest.
1568
-
1569
- ## Item lists
1570
-
1571
- Layout containers for arbitrary children - the children need no classes.
1572
-
1573
- | Class | Look |
1574
- | ------------------------- | ------------------------------------------------------------- |
1575
- | `mw-item-list` | Plain flex column with gaps |
1576
- | `mw-item-list-horizontal` | Row, wrapping; column below `sm` |
1577
- | `mw-item-list-cards` | Wider gaps, for cards |
1578
- | `mw-item-list-compact` | Bordered card-surface box, divided rows, hover indent (nests) |
1579
- | `mw-item-list-menu` | Elevated menu card, pointer cursor, press feedback |
1580
- | `mw-item-list-slide` | Cards sliding sideways with an accent border on hover |
1581
- | `mw-item-list-scroll` | Scroll box, 300px tall |
1582
-
1583
- Modifiers are additive - keep the base class and add the modifier:
1584
- `mw-item-list-scroll` + `-sm` (200px), `-lg` (400px) or `-xl` (500px), and
1585
- `mw-item-list-slide` + `-secondary` for the secondary accent.
1586
-
1587
- ```html
1588
- <div class="mw-item-list-compact">
1589
- <div>Quick settings</div>
1590
- <div>
1591
- Privacy
1592
- <div class="mw-item-list-compact">
1593
- <div>Cookies</div>
1594
- </div>
1595
- </div>
1596
- </div>
1597
- ```
1598
-
1599
- **Checkbox lists** are the one variant with required inner markup - a `<ul>` of
1600
- `<li>` each holding a `mw-checkbox`:
1601
-
1602
- ```html
1603
- <ul class="mw-item-list-checkbox">
1604
- <li class="mw-selected">
1605
- <label class="mw-checkbox">
1606
- <input type="checkbox" checked />
1607
- <span class="mw-checkbox-box"></span>
1608
- <div class="mw-checkbox-content">
1609
- <h4 class="mw-checkbox-header">Task management</h4>
1610
- <p class="mw-checkbox-label">Track your daily tasks</p>
1611
- </div>
1612
- </label>
1613
- </li>
1614
- </ul>
1615
- ```
1616
-
1617
- Add `mw-item-list-checkbox-compact`, `-large` or `-secondary` alongside the
1618
- base class; `mw-item-list-checkbox-scroll` is a standalone class that replaces
1619
- it. `mw-selected` on the `<li>` is the selected-row highlight - bind it to the
1620
- checkbox state yourself in a SPA. The `mw-checkbox-content` block is optional;
1621
- a bare `mw-checkbox-label` renders a single-line row.
1622
-
1623
- The `mw-checkbox` inside is the ordinary form-element checkbox, so
1624
- `mw-checkbox-disabled` and the colour variants (`mw-checkbox-success` and the
1625
- rest, see `references/forms.md`) work here too. Row height is a list concern -
1626
- use the list's own `-compact` / `-large` modifiers rather than
1627
- `mw-checkbox-sm` / `-lg`, which sit on the wrong element to reach the box.
1628
-
1629
- ## Leader row
1630
-
1631
- A label on the left, its value on the right, a dotted line bridging the gap -
1632
- the row a menu, a table of contents and an invoice all draw. The markup is two
1633
- spans; the line is the row's own `::before`, so it stays out of the
1634
- accessibility tree and no filler element needs `aria-hidden`.
1635
-
1636
- ```html
1637
- <p class="mw-leader-row mw-leader-row-top">
1638
- <span class="mw-leader-row-label mw-text-bold">Roast pork with dumpling</span>
1639
- <span class="mw-leader-row-value mw-text-bold mw-text-primary">17.50</span>
1640
- </p>
1641
- ```
1642
-
1643
- - Label and value is what `<dl>` is for, and since HTML 5.2 a `<div>` may group
1644
- a `<dt>` and its `<dd>` inside one - so the semantic version costs no extra
1645
- element over the `<p>`. Put `mw-leader-row` on that `<div>`. Use it where the
1646
- pair **is** the content; where every row also carries a description, a `<p>`
1647
- stays simpler.
1648
- - `mw-leader-row-top` keeps the value beside the **first** line of a wrapping
1649
- label instead of the last. On a menu that is where the price belongs - next to
1650
- the dish, not somewhere down in its description.
1651
- - A second `mw-leader-row-value` needs no extra class. Both sit after the
1652
- leader, with a mark in the leader's colour between them - "11.30 - 14.00 |
1653
- 17.00 - 23.00". Without it the pair reads as one long range.
1654
- - Values are set in tabular figures and never break, so a column of them lines
1655
- up. The label wraps between its words and stops at its longest one; past that
1656
- the whole row wraps, with the values dropping under the label. Nothing shrinks
1657
- to one letter per line.
1658
- - Keep `mw-price` out of a leader row: it sets its own baseline and the leader
1659
- would no longer meet it. Plain text in the value span, or the price in a
1660
- column of its own.
1661
-
1662
- `mw-leader-row-dashed` and `mw-leader-row-solid` swap the line style; a single
1663
- row retunes with `--mw-leader-row-color`, `--mw-leader-row-style` and
1664
- `--mw-leader-row-width`.
1665
-
1666
- ## HTML lists
1667
-
1668
- `mw-list` is the base (spacing for `li`), combined with one of:
1669
- `mw-list-unstyled`, `mw-list-inline`, `mw-list-link` (underlined anchors),
1670
- `mw-list-dot` (coloured markers), `mw-list-check` (✓ prefix).
1671
-
1672
- ```html
1673
- <ul class="mw-list mw-list-check">
1674
- <li>Done</li>
1675
- </ul>
1676
- ```
1677
-
1678
- ## Breadcrumbs
1679
-
1680
- ```html
1681
- <nav class="mw-breadcrumbs mw-breadcrumbs-collapse" aria-label="Breadcrumb">
1682
- <ol class="mw-breadcrumbs-list">
1683
- <li class="mw-breadcrumbs-item">
1684
- <a href="/"><i class="fas fa-home"></i></a>
1685
- </li>
1686
- <li class="mw-breadcrumbs-item">
1687
- <a href="/invoices"><span>Invoices</span></a>
1688
- </li>
1689
- <li class="mw-breadcrumbs-item mw-breadcrumbs-current">
1690
- <a href="#" class="mw-breadcrumbs-active"><span>2026-001</span></a>
1691
- </li>
1692
- </ol>
1693
- </nav>
1694
- ```
1695
-
1696
- The last item takes `mw-breadcrumbs-current` on the `li` and
1697
- `mw-breadcrumbs-active` on the anchor. `mw-breadcrumbs-sm` is the small variant,
1698
- `mw-breadcrumbs-collapse` hides middle items on narrow screens.
1699
-
1700
- ## Pagination
1701
-
1702
- A titled content frame with prev/next controls - not a page-number list. Same
1703
- surface and header as `mw-calendar`; only the body differs, and the body is
1704
- yours.
1705
-
1706
- ```html
1707
- <div class="mw-pagination">
1708
- <div class="mw-pagination-header">
1709
- <button
1710
- type="button"
1711
- class="mw-btn mw-btn-outline mw-btn-sm mw-pagination-nav"
1712
- aria-label="Previous week"
1713
- >
1714
- <i class="fas fa-chevron-left"></i>
1715
- </button>
1716
-
1717
- <div class="mw-pagination-title" aria-live="polite">Week 10</div>
1718
-
1719
- <button
1720
- type="button"
1721
- class="mw-btn mw-btn-outline mw-btn-sm mw-pagination-nav"
1722
- aria-label="Next week"
1723
- >
1724
- <i class="fas fa-chevron-right"></i>
1725
- </button>
1726
- </div>
1727
-
1728
- <div class="mw-pagination-content">...</div>
1729
- </div>
1730
- ```
1731
-
1732
- - `mw-pagination-nav` sits **on** `mw-btn` - it only squares the button off
1733
- around the chevron. The arrows carry no accessible name of their own, so give
1734
- each one an `aria-label`; `aria-live="polite"` on the title is what tells a
1735
- screen reader where you landed.
1736
- - `mw-pagination-loading` goes on `mw-pagination-content` (not on the
1737
- container) and dims it while the next page is on its way.
1738
- - `mw-pagination-slide-left` / `-slide-right` are the directional transitions,
1739
- also on the content.
1740
- - The body has a 200px floor so a short page does not collapse the frame. Paging
1741
- is application state - the framework ships no JavaScript for it.
1742
-
1743
- ## Divider
1744
-
1745
- ```html
1746
- <hr class="mw-divider mw-divider-dashed" />
1747
- <div class="mw-divider-text"><span>or</span></div>
1748
- <div class="mw-divider-vertical"></div>
1749
- ```
1750
-
1751
- Variants: `mw-divider-thick`, `mw-divider-dashed`, `mw-divider-primary`,
1752
- `mw-divider-secondary`. `mw-divider-text` is a labelled rule (the colour
1753
- variants apply to it too), `mw-divider-vertical` needs a flex row with a height.
1754
-
1755
- ## Gallery & image slider
1756
-
1757
- ```html
1758
- <div class="mw-gallery-container">
1759
- <div class="mw-gallery">
1760
- <button type="button" class="mw-gallery-navi mw-gallery-navi-prev">
1761
- &#10094;
1762
- </button>
1763
- <div class="mw-gallery-track">
1764
- <div class="mw-gallery-slide" data-desc="Mountains">
1765
- <img src="1.jpg" alt="" />
1766
- </div>
1767
- </div>
1768
- <button type="button" class="mw-gallery-navi mw-gallery-navi-next">
1769
- &#10095;
1770
- </button>
1771
- </div>
1772
- <div class="mw-gallery-desc">Mountains</div>
1773
- <div class="mw-gallery-dots"></div>
1774
- </div>
1775
- ```
1776
-
1777
- The dots (`mw-gallery-dot`, active one gets `mw-active`) are generated by the
1778
- shipped JS, as is the track transform. In a SPA, render the dots and set
1779
- `transform: translateX(...)` yourself. Swipe is single-finger and decided by the
1780
- dominant axis, so a page scroll that drifts sideways does not page the gallery.
1781
-
1782
- `mw-gallery-container` is a query container (`mw-gallery`), and the caption size,
1783
- the dot size and the 4:3 crop follow _its_ width - 688px and 512px - not the
1784
- viewport. A gallery in a half-width column therefore takes its narrow layout
1785
- there. Keep the wrapper: a container query only reaches descendants, and the
1786
- caption and the dots are siblings of `mw-gallery`.
1787
-
1788
- `mw-image-slider` swaps a base image against overlays through indexed buttons:
1789
-
1790
- ```html
1791
- <div class="mw-image-slider">
1792
- <div class="mw-image-slider-base" data-index="0">
1793
- <img class="mw-image-slider-base-image" src="base.jpg" alt="" />
1794
- </div>
1795
- <div class="mw-image-slider-overlay">
1796
- <div class="mw-image-slider-overlay-image mw-active" data-index="1">
1797
- <img src="a.jpg" alt="" />
1798
- </div>
1799
- </div>
1800
- <div class="mw-image-slider-controls-grid-3">
1801
- <button class="mw-btn mw-btn-primary mw-active" data-index="0">Base</button>
1802
- </div>
1803
- </div>
1804
- ```
1805
-
1806
- Control grids: `mw-image-slider-controls-grid-2`, `-grid-3`. Visible overlay and
1807
- current button carry `active`. `mw-image-slider` is its own query container
1808
- (`mw-slider`): the three heights (250 / 320 / 370px) and the control columns
1809
- switch at 420px and 560px of slider width, because the slider caps at 600px and
1810
- past a tablet the viewport says nothing about the room it has.
1811
-
1812
- ## Mosaic
1813
-
1814
- Photos of any shape in one grid; a tap opens the lightbox.
1815
-
1816
- ```html
1817
- <div class="mw-mosaic">
1818
- <figure class="mw-mosaic-item mw-mosaic-big">
1819
- <a href="ridge-full.jpg">
1820
- <img
1821
- src="ridge.jpg"
1822
- alt="Mountain range at sunrise"
1823
- width="800"
1824
- height="450"
1825
- loading="lazy"
1826
- />
1827
- </a>
1828
- <figcaption>First light on the ridge.</figcaption>
1829
- </figure>
1830
- <figure class="mw-mosaic-item">…</figure>
1831
- </div>
1832
- ```
1833
-
1834
- - Two columns on a phone, four at most, from the width of the box it sits in.
1835
- - The script shapes each tile from the photo's proportions - the `width` and
1836
- `height` attributes, else the natural size once loaded: `mw-mosaic-wide` from
1837
- 1.3:1, `mw-mosaic-tall` from 1:1.25, square otherwise. A shape class set by
1838
- hand wins; `mw-mosaic-big` (2×2) is only ever set by hand. Without the script
1839
- every tile is square.
1840
- - `grid-auto-flow: dense` fills the holes and moves tiles away from their source
1841
- order. `reading-flow: grid-rows` makes Tab follow the eye where the browser
1842
- has it (Chromium so far).
1843
- - Gaps: `mw-mosaic-gap-sm`, `-gap-lg`.
1844
- - The tile crops, the lightbox does not. The `figcaption` is hidden in the tile
1845
- and shown in the lightbox.
1846
-
1847
- ### Lightbox
1848
-
1849
- A click on a link that holds an image - inside `mw-mosaic` or any element with
1850
- `data-mw-lightbox` - opens the photo full screen instead of following the link.
1851
- The script builds the viewer once, a `<dialog class="mw-lightbox">`, with one
1852
- slide per image of the group in reading order, the full image from the link's
1853
- `href` and the caption copied from the `figcaption`. Swipe, arrow keys or the
1854
- arrows page through it; Escape, the cross or a click on the stage beside the
1855
- photo close it, and focus returns to the tile of the last photo viewed. A click
1856
- with a modifier key stays a link.
1857
-
1858
- Without `main.js`, build it yourself: a `<dialog class="mw-lightbox">` holding
1859
- `mw-lightbox-bar` (with `mw-lightbox-prev`, `mw-lightbox-count`,
1860
- `mw-lightbox-next` and `mw-lightbox-close`, all `mw-overlay-btn`) and
1861
- `mw-lightbox-track` with one `<figure class="mw-lightbox-slide">` per photo. The
1862
- track is a scroll-snap strip; the page scroll is locked while the dialog is open.
1863
-
1864
- ## Portrait gallery
1865
-
1866
- ```html
1867
- <div class="mw-portrait-gallery">
1868
- <div
1869
- class="mw-portrait-gallery-track"
1870
- tabindex="0"
1871
- aria-label="Portrait photos"
1872
- >
1873
- <figure class="mw-portrait-gallery-slide">
1874
- <img
1875
- src="cape.jpg"
1876
- alt="Lighthouse on a cliff at dusk"
1877
- width="600"
1878
- height="800"
1879
- loading="lazy"
1880
- />
1881
- <figcaption>
1882
- <strong>North Cape</strong>
1883
- The lamp comes on just after the sun goes.
1884
- </figcaption>
1885
- </figure>
1886
- </div>
1887
- </div>
1888
- ```
1889
-
1890
- - Upright 3:4 slides in a native scroll-snap strip, the next one peeking in.
1891
- One slide below 560px of gallery width, two and a quarter from there, three
1892
- from 880px - a container query, so a gallery in a narrow column stays at one.
1893
- The height is capped at 78svh, which a landscape phone needs.
1894
- - `mw-portrait-gallery-tall`: 9:16, for stories, reels and screenshots. A
1895
- `<video>` works in place of an `<img>`.
1896
- - Gaps: `mw-portrait-gallery-gap-sm`, `-gap-lg`.
1897
- - The script adds the arrows (`mw-portrait-gallery-prev` / `-next`, for a mouse
1898
- only) and the dots (`mw-portrait-gallery-dots` / `-dot`, below 560px, the
1899
- current one `mw-active`).
1900
- - A photo more than 1.4× off the slide's shape is shown whole over a blurred
1901
- copy of itself instead of cropped: the script sets `mw-fit-contain` on the
1902
- slide and prepends a `mw-fit-backdrop` image, and checks again on every
1903
- resize. `mw-fit-contain` set by hand forces it, `mw-fit-cover` on the slide
1904
- turns it off.
1905
-
1906
- ## Photo feed
1907
-
1908
- The three-column grid from a phone's photo app, in a framed box that scrolls.
1909
-
1910
- ```html
1911
- <div class="mw-feed">
1912
- <div class="mw-feed-item">
1913
- <button type="button" popovertarget="post-cape">
1914
- <img
1915
- src="cape.jpg"
1916
- alt="Lighthouse on a cliff at dusk"
1917
- width="600"
1918
- height="800"
1919
- loading="lazy"
1920
- />
1921
- </button>
1922
- <figure class="mw-feed-post" id="post-cape" popover>
1923
- <button
1924
- type="button"
1925
- class="mw-overlay-btn mw-feed-post-close"
1926
- popovertarget="post-cape"
1927
- popovertargetaction="hide"
1928
- aria-label="Close"
1929
- >
1930
- &#120299;
1931
- </button>
1932
- <img
1933
- src="cape.jpg"
1934
- alt="Lighthouse on a cliff at dusk"
1935
- width="600"
1936
- height="800"
1937
- loading="lazy"
1938
- />
1939
- <figcaption>
1940
- <p>Stood there until the lamp came on.</p>
1941
- <time datetime="2026-08-12">12 August 2026</time>
1942
- </figcaption>
1943
- </figure>
1944
- </div>
1945
- </div>
1946
- ```
1947
-
1948
- - 3:4 tiles, 2px apart; `mw-feed-square` for square ones.
1949
- - Past three rows the box scrolls, with a strip of the next row in view so it
1950
- reads as scrollable. `mw-feed-rows-2` shows two rows, `mw-feed-plain` drops
1951
- the frame.
1952
- - No script: the tile is a button with `popovertarget`, the post a `popover`.
1953
- In Angular that is `[attr.popovertarget]` and a matching `[id]`. A tile can be
1954
- an `<a>` to a page of its own instead.
1955
- - `time` and `small` in the caption are set as a muted line. The page scroll is
1956
- locked while a post is open.
1957
-
1958
- ## Story archive
1959
-
1960
- ```html
1961
- <div class="mw-stories" aria-label="Story archive">
1962
- <button type="button" class="mw-story" popovertarget="story-coast">
1963
- <span class="mw-story-cover"><img src="beach.jpg" alt="" /></span>
1964
- <span class="mw-story-label">Coast</span>
1965
- </button>
1966
- <div class="mw-story-reel" id="story-coast" popover>
1967
- <div class="mw-story-reel-head">
1968
- <span>Coast</span>
1969
- <label class="mw-overlay-btn mw-story-reel-pause"
1970
- ><input type="checkbox" aria-label="Pause"
1971
- /></label>
1972
- <button
1973
- type="button"
1974
- class="mw-overlay-btn"
1975
- popovertarget="story-coast"
1976
- popovertargetaction="hide"
1977
- aria-label="Close"
1978
- >
1979
- &#120299;
1980
- </button>
1981
- </div>
1982
- <img src="beach.jpg" alt="Beach at sunset" loading="lazy" />
1983
- <video
1984
- src="sunrise.mp4"
1985
- poster="sunrise.jpg"
1986
- autoplay
1987
- muted
1988
- loop
1989
- playsinline
1990
- ></video>
1991
- </div>
1992
- </div>
1993
- ```
1994
-
1995
- - The row scrolls sideways, with a shadow on the side that has more behind it;
1996
- a mouse gets a thin scrollbar.
1997
- - Playback is CSS: every frame shows for five seconds, a segmented bar in the
1998
- head counts them off, and holding the story (`:active`) or ticking the pause
1999
- checkbox stops it. Full screen on a phone, phone-shaped (9:16) from `sm` up.
2000
- - Twelve frames at most - the timing is written out for twelve. Later frames
2001
- are hidden and the story ends after the twelfth.
2002
- - Under `prefers-reduced-motion` nothing plays: the frames become a vertical
2003
- strip to swipe, and the bar follows the scroll where the browser has scroll
2004
- timelines.
2005
- - The script adds tap zones (`mw-story-reel-prev` on the left 30%, `-next` on
2006
- the rest), arrow keys, focus on the close button when the story opens, a video
2007
- that starts when its frame comes up and stops with the story, and closes the
2008
- story at its end.
2009
-
2010
- ## Overlay button
2011
-
2012
- `mw-overlay-btn` is the round 44px button that sits on a photo - dark and
2013
- translucent in both themes, with light ink and an inline `<svg>` sized for it.
2014
- `:disabled` hides it. The lightbox, the portrait gallery, the feed post and the
2015
- story reel use it for their close and arrow buttons.
2016
-
2017
- ## Blog post
2018
-
2019
- ```html
2020
- <article class="mw-blog-post">
2021
- <h2 class="mw-blog-post-title">Title</h2>
2022
- <div class="mw-blog-post-header">
2023
- <div class="mw-blog-post-header-icon"><i class="fas fa-code"></i></div>
2024
- <div class="mw-blog-post-meta">
2025
- <div class="mw-blog-post-meta-item">
2026
- <i class="far fa-calendar"></i><span>2026-04-10</span>
2027
- </div>
2028
- </div>
2029
- </div>
2030
- <img class="mw-blog-post-featured-image" src="cover.jpg" alt="" />
2031
- <div class="mw-blog-post-content-markdown">
2032
- <!-- raw h3/p/ul/blockquote/pre from a markdown renderer -->
2033
- </div>
2034
- <div class="mw-blog-post-footer">
2035
- <button class="mw-btn mw-blog-post-footer-button">
2036
- <i class="fas fa-arrow-left"></i><span>Previous</span>
2037
- </button>
2038
- </div>
2039
- </article>
2040
- ```
2041
-
2042
- `mw-blog-post-content-markdown` styles unclassed HTML - the target for rendered
2043
- markdown. `mw-blog-post-content` is the same wrapper for hand-written markup.
2044
-
2045
- ## Testimonial
2046
-
2047
- A quote card with its source - a voice about the product. A `figure`, because
2048
- quote and attribution belong together:
2049
-
2050
- ```html
2051
- <figure class="mw-testimonial">
2052
- <blockquote>
2053
- The workshop picked me up after a long day of talking.
2054
- </blockquote>
2055
- <figcaption class="mw-testimonial-source">
2056
- Anna M. <span class="mw-testimonial-detail">&middot; voice workshop</span>
2057
- <time class="mw-testimonial-date" datetime="2026-03">March 2026</time>
2058
- </figcaption>
2059
- </figure>
2060
- ```
2061
-
2062
- The card carries the surface, so inside it the blockquote drops most of its
2063
- tint and keeps only the edge and the quotation marks. `mw-testimonial-source`
2064
- uses `margin-top: auto` - in a grid of equal-height cards every source line
2065
- sits on its card's bottom edge. `mw-testimonial-featured` sets the quote a
2066
- size larger, for the one voice lifted above the grid.
2067
-
2068
- `mw-testimonial-date` is optional: a `<time>` on the right edge of the source
2069
- line, month and year - a full date is review-platform optics, a bare year
2070
- reads as a footnote. It drops to its own line, still right-aligned, when the
2071
- source line runs out of room. Leave it out unless the voices are kept fresh: a
2072
- dated quote ages, and three from years ago say more about the page than about
2073
- the product. The `datetime` attribute is what Schema.org's `datePublished`
2074
- would read.
2075
-
2076
- For a wall of testimonials of unequal length, put them in `mw-columns-2` or
2077
- `mw-columns-3` (`references/layout.md`) instead of a grid.
2078
-
2079
- ## Prose
2080
-
2081
- Long-form running text - a legal page, terms, a plain article. One class on
2082
- the container, bare `h2`/`h3`/`p`/`ul` inside:
2083
-
2084
- ```html
2085
- <div class="mw-card mw-card-simple mw-prose">
2086
- <h2>1. Scope</h2>
2087
- <p>...</p>
2088
- <h2>2. Contact</h2>
2089
- <p>...</p>
2090
- </div>
2091
- ```
2092
-
2093
- Headings are deliberately small (`lg`/`md`, primary ink) - in a document they
2094
- structure the reading rather than compete with the page title. First and last
2095
- child lose their outer margins, so the container's own padding rules.
2096
-
2097
- ## Media
2098
-
2099
- Self-hosted audio and video. The native controls are the player -
2100
- `accent-color` already brands them - the wrapper only makes the element sit
2101
- right:
2102
-
2103
- ```html
2104
- <div class="mw-media">
2105
- <video src="/media/intro.mp4" controls preload="metadata"></video>
2106
- <p class="mw-media-caption">Two minutes about how a session works.</p>
2107
- </div>
2108
- ```
2109
-
2110
- `audio` and `video` become full-width blocks; a video also gets the surface
2111
- silhouette and a resting shadow. Works with `<audio controls>` the same way.
2112
-
2113
- ## Code & terminal
2114
-
2115
- ```html
2116
- <div class="mw-code-block">
2117
- <div class="mw-code-block-header">
2118
- <span class="mw-code-block-filename">main.kt</span>
2119
- <span class="mw-code-block-language">Kotlin</span>
2120
- </div>
2121
- <div class="mw-code-container">
2122
- <pre><code>fun main() {}</code></pre>
2123
- </div>
2124
- </div>
2125
-
2126
- <div class="mw-terminal">
2127
- <span class="mw-terminal-line">npm install maverick-wave</span>
2128
- <span class="mw-terminal-line mw-terminal-output">added 1 package</span>
2129
- </div>
2130
- ```
2131
-
2132
- Token classes for manual highlighting: `mw-code-keyword`, `mw-code-function`,
2133
- `mw-code-string`, `mw-code-number`, `mw-code-comment`, `mw-code-operator`. There
2134
- is no highlighting engine - wrap spans yourself or plug in Prism/highlight.js.
2135
- `mw-code-nowrap` disables wrapping. `<code>` on its own is styled as inline code.
2136
-
2137
- ## Tech stack bucket
2138
-
2139
- ```html
2140
- <div class="mw-techstack-bucket mw-techstack-bucket-dashed">
2141
- <a href="https://angular.dev" class="mw-techstack-item">
2142
- <img class="mw-techstack-logo" src="angular.svg" alt="Angular" />
2143
- <div class="mw-techstack-info">
2144
- <span class="mw-techstack-name">Angular</span>
2145
- <span class="mw-techstack-desc">Frontend</span>
2146
- </div>
2147
- </a>
2148
- <a
2149
- href="#"
2150
- class="mw-techstack-item mw-techstack-item-sm"
2151
- data-tooltip="Docker"
2152
- >
2153
- <img class="mw-techstack-logo" src="docker.svg" alt="Docker" />
2154
- </a>
2155
- </div>
2156
- ```
2157
-
2158
- `mw-techstack-item-sm` is the logo-only chip; the info block is optional. On a
2159
- coarse pointer the chip itself grows from 32px to 2.5rem - it often carries a
2160
- `data-tooltip`, which owns the pseudo-element a grown hit area would need.
2161
-
2162
- ## Coming soon
2163
-
2164
- ```html
2165
- <div class="mw-coming-soon">
2166
- <i class="mw-coming-soon-icon fas fa-tools"></i>
2167
- <div class="mw-coming-soon-text">Coming soon</div>
2168
- </div>
2169
- ```