maverick-wave 5.5.0 → 5.7.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 (39) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +16 -13
  2. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +2 -2
  3. package/.claude/skills/mw-maverick-wave/references/components.md +5 -2
  4. package/.claude/skills/mw-maverick-wave/references/forms.md +5 -5
  5. package/.claude/skills/mw-maverick-wave/references/theming.md +41 -0
  6. package/CHANGELOG.md +17 -0
  7. package/README.md +30 -5
  8. package/index.html +359 -4
  9. package/maverick-wave.min.css +5 -5
  10. package/package.json +2 -2
  11. package/src/partials/history-container.html +1 -1
  12. package/src/partials/palette-container.html +210 -0
  13. package/src/partials/typography-container.html +8 -9
  14. package/src/partials/utilities-container.html +48 -0
  15. package/src/scss/abstracts/_functions.scss +11 -2
  16. package/src/scss/abstracts/_mixins.scss +35 -3
  17. package/src/scss/abstracts/_variables.scss +8 -8
  18. package/src/scss/base/_base.scss +3 -0
  19. package/src/scss/components/_avatars.scss +33 -1
  20. package/src/scss/components/_blog-post.scss +2 -2
  21. package/src/scss/components/_button-bar.scss +1 -1
  22. package/src/scss/components/_buttons.scss +1 -1
  23. package/src/scss/components/_cards.scss +13 -22
  24. package/src/scss/components/_coming-soon.scss +1 -1
  25. package/src/scss/components/_info.scss +2 -2
  26. package/src/scss/components/_lists.scss +1 -1
  27. package/src/scss/components/_panels.scss +1 -1
  28. package/src/scss/components/_pricing.scss +1 -1
  29. package/src/scss/components/_techstack-bucket.scss +1 -1
  30. package/src/scss/components/_tiles.scss +1 -1
  31. package/src/scss/form-elements/_input.scss +1 -1
  32. package/src/scss/form-elements/_select.scss +1 -1
  33. package/src/scss/layout/_footer.scss +1 -1
  34. package/src/scss/layout/_header.scss +9 -1
  35. package/src/scss/layout/_main.scss +31 -2
  36. package/src/scss/utilities/_index.scss +1 -0
  37. package/src/scss/utilities/_reveal.scss +9 -2
  38. package/src/scss/utilities/_touch-targets.scss +1 -1
  39. package/src/scss/utilities/_variants.scss +120 -0
package/index.html CHANGED
@@ -21,7 +21,10 @@
21
21
  @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css')
22
22
  layer(vendor);
23
23
  </style>
24
- <link rel="stylesheet" href="maverick-wave.min.css" />
24
+ <!-- ?v= so a release actually reaches a browser that has the last one
25
+ cached: the filename never changes, and without it a stale stylesheet
26
+ survives a normal reload -->
27
+ <link rel="stylesheet" href="maverick-wave.min.css?v={{VERSION}}" />
25
28
  <!-- Favicons -->
26
29
  <link rel="icon" type="image/svg+xml" href="favicon/favicon.svg" />
27
30
  <link
@@ -151,7 +154,12 @@
151
154
  right: 18px;
152
155
  bottom: 82px;
153
156
  z-index: 400;
154
- width: 300px;
157
+ width: 360px;
158
+ /* The variant list makes the panel taller than a laptop in landscape -
159
+ the top of it would be behind the browser chrome without this */
160
+ max-height: calc(100dvh - 120px);
161
+ overflow-y: auto;
162
+ overscroll-behavior: contain;
155
163
  padding: 18px;
156
164
  border-radius: 12px;
157
165
  border: 1px solid var(--mw-border);
@@ -241,7 +249,121 @@
241
249
  cursor: pointer;
242
250
  }
243
251
 
244
- @media (max-width: 576px) {
252
+ /* Single accent means the secondary picker writes a token nothing reads any
253
+ more - left live it is a knob that does nothing */
254
+ :root.mw-accent-single .palette-field:has(#palette-secondary) {
255
+ opacity: 0.4;
256
+ pointer-events: none;
257
+ }
258
+
259
+ .palette-heading-split {
260
+ margin-top: 18px;
261
+ padding-top: 14px;
262
+ border-top: 1px solid var(--mw-border);
263
+ }
264
+
265
+ .variant-row {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: space-between;
269
+ gap: 12px;
270
+ padding-block: 9px;
271
+ font-size: 0.8rem;
272
+ color: var(--mw-text-color);
273
+ cursor: pointer;
274
+ }
275
+
276
+ .variant-label {
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 2px;
280
+ }
281
+
282
+ .variant-label small {
283
+ font-size: 0.68rem;
284
+ line-height: 1.35;
285
+ color: var(--mw-text-muted-color);
286
+ }
287
+
288
+ .variant-row + .variant-row {
289
+ border-top: 1px solid var(--mw-border);
290
+ }
291
+
292
+ .variant-row select {
293
+ flex-shrink: 0;
294
+ max-width: 110px;
295
+ padding: 3px 6px;
296
+ font-size: 0.75rem;
297
+ color: var(--mw-text-color);
298
+ border: 1px solid var(--mw-border);
299
+ border-radius: 6px;
300
+ background: transparent;
301
+ cursor: pointer;
302
+ }
303
+
304
+ /* Drawn by hand rather than reusing mw-toggle: a framework switch would
305
+ reshape itself along with the variants it is there to control */
306
+ .variant-row input[type='checkbox'] {
307
+ appearance: none;
308
+ flex-shrink: 0;
309
+ position: relative;
310
+ width: 34px;
311
+ height: 18px;
312
+ margin: 0;
313
+ border: 1px solid var(--mw-border);
314
+ border-radius: 9px;
315
+ background: var(--mw-gray-background);
316
+ cursor: pointer;
317
+ transition: var(--mw-transition);
318
+ }
319
+
320
+ .variant-row input[type='checkbox']::after {
321
+ content: '';
322
+ position: absolute;
323
+ top: 2px;
324
+ left: 2px;
325
+ width: 12px;
326
+ height: 12px;
327
+ border-radius: 50%;
328
+ background: var(--mw-text-muted-color);
329
+ transition: var(--mw-transition);
330
+ }
331
+
332
+ .variant-row input[type='checkbox']:checked {
333
+ background: var(--mw-primary-color);
334
+ border-color: var(--mw-primary-color);
335
+ }
336
+
337
+ .variant-row input[type='checkbox']:checked::after {
338
+ left: 18px;
339
+ background: var(--mw-primary-accent-text-color);
340
+ }
341
+
342
+ .variant-setup-label {
343
+ display: block;
344
+ margin-top: 14px;
345
+ margin-bottom: 5px;
346
+ font-size: 0.7rem;
347
+ color: var(--mw-text-muted-color);
348
+ }
349
+
350
+ .variant-setup {
351
+ display: block;
352
+ width: 100%;
353
+ resize: vertical;
354
+ padding: 10px;
355
+ border-radius: 8px;
356
+ border: 1px solid var(--mw-border);
357
+ background: var(--mw-gray-background);
358
+ color: var(--mw-text-muted-color);
359
+ font-family: var(--mw-font-family-mono);
360
+ font-size: 0.68rem;
361
+ line-height: 1.5;
362
+ white-space: pre-wrap;
363
+ word-break: break-word;
364
+ }
365
+
366
+ @media (max-width: 480px) {
245
367
  .palette-fab {
246
368
  right: 12px;
247
369
  bottom: 12px;
@@ -258,7 +380,7 @@
258
380
  }
259
381
  </style>
260
382
 
261
- <script src="maverick-wave.min.js"></script>
383
+ <script src="maverick-wave.min.js?v={{VERSION}}"></script>
262
384
  </head>
263
385
 
264
386
  <body>
@@ -795,6 +917,13 @@
795
917
 
796
918
  const root = document.documentElement;
797
919
 
920
+ // The values the markup ships with, so the setup box below can tell a
921
+ // chosen colour from the built-in one and print only the difference
922
+ const BUILT_IN = {
923
+ primary: primaryInput.value,
924
+ secondary: secondaryInput.value,
925
+ };
926
+
798
927
  const DARK_INK = '#131925';
799
928
  const LIGHT_INK = '#f2f6fc';
800
929
 
@@ -830,6 +959,7 @@
830
959
  void root.offsetHeight;
831
960
  root.classList.remove('mw-theme-switching');
832
961
  window.mwRefreshColorSwatches?.();
962
+ window.mwRefreshSetup?.();
833
963
  };
834
964
 
835
965
  const paint = (name, color) => {
@@ -884,6 +1014,231 @@
884
1014
  document.addEventListener('keydown', (event) => {
885
1015
  if (event.key === 'Escape') toggle(false);
886
1016
  });
1017
+
1018
+ // The seam to the variants panel below, which prints and reads the whole
1019
+ // setup - the palette is half of it. Only inline values count as chosen:
1020
+ // anything still coming from the stylesheet is the built-in one.
1021
+ const PAGE_GROUNDS = {
1022
+ dark: '--mw-dark-page-background',
1023
+ light: '--mw-light-page-background',
1024
+ };
1025
+
1026
+ window.mwPalette = {
1027
+ read: () => ({
1028
+ primary: root.style.getPropertyValue('--mw-primary-color') || null,
1029
+ secondary:
1030
+ root.style.getPropertyValue('--mw-secondary-color') || null,
1031
+ // The label colour ink() measured for each fill. It has to travel
1032
+ // with the setup: a project that only copies the two brand colours
1033
+ // keeps the stock accent text, and on a light brand tone that is an
1034
+ // unreadable button. write() recomputes it, so pasting it back is
1035
+ // harmless.
1036
+ primaryInk:
1037
+ root.style.getPropertyValue('--mw-primary-accent-text-color') ||
1038
+ null,
1039
+ secondaryInk:
1040
+ root.style.getPropertyValue('--mw-secondary-accent-text-color') ||
1041
+ null,
1042
+ dark: root.style.getPropertyValue(PAGE_GROUNDS.dark) || null,
1043
+ light: root.style.getPropertyValue(PAGE_GROUNDS.light) || null,
1044
+ }),
1045
+
1046
+ write: ({ primary, secondary, dark, light }) => {
1047
+ const current = window.mwPalette.read();
1048
+ const same =
1049
+ current.primary === primary &&
1050
+ current.secondary === secondary &&
1051
+ current.dark === dark &&
1052
+ current.light === light;
1053
+ // Typing into the setup box fires per keystroke, and settle() forces
1054
+ // a reflow - so a run that changes nothing has to cost nothing
1055
+ if (same) return;
1056
+
1057
+ [
1058
+ ['primary', primary, primaryInput],
1059
+ ['secondary', secondary, secondaryInput],
1060
+ ].forEach(([name, value, input]) => {
1061
+ if (value) {
1062
+ paint(name, value);
1063
+ input.value = value;
1064
+ } else {
1065
+ root.style.removeProperty(`--mw-${name}-color`);
1066
+ root.style.removeProperty(`--mw-${name}-accent-text-color`);
1067
+ input.value = BUILT_IN[name];
1068
+ }
1069
+ });
1070
+
1071
+ Object.entries(PAGE_GROUNDS).forEach(([key, prop]) => {
1072
+ const value = key === 'dark' ? dark : light;
1073
+ if (value) root.style.setProperty(prop, value);
1074
+ else root.style.removeProperty(prop);
1075
+ });
1076
+
1077
+ const shownPrimary = primary || BUILT_IN.primary;
1078
+ const shownSecondary = secondary || BUILT_IN.secondary;
1079
+ items.forEach((item) =>
1080
+ item.classList.toggle(
1081
+ 'mw-active',
1082
+ item.dataset.primary === shownPrimary &&
1083
+ item.dataset.secondary === shownSecondary
1084
+ )
1085
+ );
1086
+
1087
+ settle();
1088
+ },
1089
+ };
1090
+ });
1091
+
1092
+ // just for the showcase: the variant switches. Each one is a class on <html>
1093
+ // or a single custom property, never a walk over the DOM - markup that
1094
+ // arrives later (a modal, a toast, a kanban card) is covered by the same
1095
+ // class that was already set. The readout at the bottom of the panel is the
1096
+ // whole setup a project would carry to get the look it is currently seeing.
1097
+ document.addEventListener('DOMContentLoaded', () => {
1098
+ const list = document.getElementById('variant-list');
1099
+ const setup = document.getElementById('variant-setup');
1100
+ if (!list || !setup) return;
1101
+
1102
+ const root = document.documentElement;
1103
+ const controls = [...list.querySelectorAll('input, select')];
1104
+
1105
+ const isOn = (input) =>
1106
+ input.hasAttribute('data-variant-invert')
1107
+ ? !input.checked
1108
+ : input.checked;
1109
+
1110
+ const render = () => {
1111
+ // Never while it is being typed in - the box is an input as well as a
1112
+ // readout, and rewriting it under the cursor would eat the paste
1113
+ if (document.activeElement === setup) return;
1114
+
1115
+ const classes = [];
1116
+ const notes = [];
1117
+ const props = [];
1118
+
1119
+ // The palette is part of the setup, and it is printed first because a
1120
+ // project sets its brand colours before it retunes anything else
1121
+ const palette = window.mwPalette?.read() ?? {};
1122
+ [
1123
+ ['--mw-primary-color', palette.primary],
1124
+ ['--mw-primary-accent-text-color', palette.primaryInk],
1125
+ ['--mw-secondary-color', palette.secondary],
1126
+ ['--mw-secondary-accent-text-color', palette.secondaryInk],
1127
+ ['--mw-dark-page-background', palette.dark],
1128
+ ['--mw-light-page-background', palette.light],
1129
+ ].forEach(([prop, value]) => {
1130
+ if (value) props.push(` ${prop}: ${value};`);
1131
+ });
1132
+
1133
+ controls.forEach((control) => {
1134
+ if (control.type !== 'checkbox') {
1135
+ if (control.value) {
1136
+ props.push(
1137
+ ` ${control.dataset.variantProp}: ${control.value};`
1138
+ );
1139
+ }
1140
+ return;
1141
+ }
1142
+
1143
+ // A targeted class is one the markup already ships, so its absence is
1144
+ // the deviation worth naming - the rest are classes to add
1145
+ const target = control.dataset.variantTarget;
1146
+ if (target) {
1147
+ if (!isOn(control)) {
1148
+ notes.push(target + ': no ' + control.dataset.variantClass);
1149
+ }
1150
+ } else if (isOn(control)) {
1151
+ classes.push(control.dataset.variantClass);
1152
+ }
1153
+ });
1154
+
1155
+ const lines = [];
1156
+ if (classes.length) {
1157
+ lines.push('html class="' + classes.join(' ') + '"');
1158
+ }
1159
+ lines.push(...notes);
1160
+ if (props.length) lines.push(':root {', ...props, '}');
1161
+
1162
+ setup.value = lines.length
1163
+ ? lines.join('\n')
1164
+ : 'Default setup - nothing to change.';
1165
+ };
1166
+
1167
+ const apply = (control) => {
1168
+ if (control.type === 'checkbox') {
1169
+ const target = control.dataset.variantTarget;
1170
+ const el = target ? document.querySelector(target) : root;
1171
+ el?.classList.toggle(control.dataset.variantClass, isOn(control));
1172
+ } else if (control.value) {
1173
+ root.style.setProperty(control.dataset.variantProp, control.value);
1174
+ } else {
1175
+ root.style.removeProperty(control.dataset.variantProp);
1176
+ }
1177
+ };
1178
+
1179
+ // The box read backwards: someone else's setup pasted in drives the
1180
+ // switches, which drive the page. Anything the text does not mention
1181
+ // falls back to the default, so an empty box is a reset.
1182
+ const load = (text) => {
1183
+ const named = new Set(
1184
+ (text.match(/class\s*=\s*"([^"]*)"/)?.[1] || '')
1185
+ .trim()
1186
+ .split(/\s+/)
1187
+ .filter(Boolean)
1188
+ );
1189
+ // `.mw-header: no mw-header-reveal` - a class the markup ships and the
1190
+ // setup takes away, which is the deviation worth reading
1191
+ const dropped = new Set(
1192
+ [...text.matchAll(/:\s*no\s+([\w-]+)/g)].map((m) => m[1])
1193
+ );
1194
+ const props = new Map(
1195
+ [...text.matchAll(/(--[\w-]+)\s*:\s*([^;\n}]+)/g)].map((m) => [
1196
+ m[1],
1197
+ m[2].trim(),
1198
+ ])
1199
+ );
1200
+
1201
+ window.mwPalette?.write({
1202
+ primary: props.get('--mw-primary-color') ?? null,
1203
+ secondary: props.get('--mw-secondary-color') ?? null,
1204
+ dark: props.get('--mw-dark-page-background') ?? null,
1205
+ light: props.get('--mw-light-page-background') ?? null,
1206
+ });
1207
+
1208
+ controls.forEach((control) => {
1209
+ if (control.type === 'checkbox') {
1210
+ const name = control.dataset.variantClass;
1211
+ const on = control.dataset.variantTarget
1212
+ ? !dropped.has(name)
1213
+ : named.has(name);
1214
+ control.checked = control.hasAttribute('data-variant-invert')
1215
+ ? !on
1216
+ : on;
1217
+ } else {
1218
+ control.value = props.get(control.dataset.variantProp) ?? '';
1219
+ }
1220
+ apply(control);
1221
+ });
1222
+ };
1223
+
1224
+ // Picking a palette repaints the page, and the box has to say so
1225
+ window.mwRefreshSetup = render;
1226
+
1227
+ setup.addEventListener('input', () => load(setup.value));
1228
+ // Normalises what was pasted back into the shape render() writes
1229
+ setup.addEventListener('blur', render);
1230
+
1231
+ controls.forEach((control) =>
1232
+ control.addEventListener('change', () => {
1233
+ apply(control);
1234
+ render();
1235
+ })
1236
+ );
1237
+
1238
+ // Applied on load too, so a browser that restored a checked box after a
1239
+ // reload does not leave the page and the panel disagreeing
1240
+ controls.forEach(apply);
1241
+ render();
887
1242
  });
888
1243
 
889
1244
  // for the modals - the framework handles both shapes, div and <dialog>