maverick-wave 5.6.0 → 5.8.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.
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
@@ -121,40 +124,50 @@
121
124
 
122
125
  /* Palette switcher, showcase only. The framework derives every tone from
123
126
  the root colors, so the panel writes nothing but those. */
124
- .palette-fab {
125
- position: fixed;
126
- right: 18px;
127
- bottom: 18px;
128
- z-index: 400;
129
- width: 52px;
130
- height: 52px;
131
- border-radius: 50%;
132
- border: 1px solid var(--mw-border-accent);
127
+ .palette-toggle {
128
+ display: none;
129
+ align-items: center;
130
+ justify-content: center;
131
+ width: 50px;
132
+ height: 40px;
133
+ flex-shrink: 0;
134
+ border: none;
135
+ border-radius: calc(10px * var(--mw-radius-scale, 1));
133
136
  background: var(--mw-primary-color);
134
137
  color: var(--mw-primary-accent-text-color);
135
- box-shadow: var(--mw-elevation-4);
136
- font-size: 1.25rem;
138
+ font-size: 1.1rem;
137
139
  cursor: pointer;
138
140
  transition: var(--mw-transition);
139
141
  }
140
142
 
141
- .palette-fab:hover {
143
+ /* Below sm the panel would cover the very thing it is changing */
144
+ @media (min-width: 576px) {
145
+ .palette-toggle {
146
+ display: flex;
147
+ }
148
+ }
149
+
150
+ .palette-toggle:hover {
142
151
  background: var(--mw-primary-color-hover);
143
152
  }
144
153
 
145
- .palette-fab:active {
146
- transform: scale(0.92);
154
+ .palette-toggle:active {
155
+ transform: scale(0.94);
147
156
  }
148
157
 
149
158
  .palette-panel {
150
159
  position: fixed;
151
- right: 18px;
152
- bottom: 82px;
160
+ top: calc(var(--mw-header-height) + 8px);
161
+ /* Flush with the container edge, so it lines up with the button in the
162
+ header rather than sitting further out */
163
+ right: max(18px, calc((100% - var(--mw-container-width)) / 2));
153
164
  z-index: 400;
154
- width: 360px;
165
+ /* Wide enough for three palettes in a row; the min() is for the narrow
166
+ end, where the panel would otherwise reach past the viewport */
167
+ width: min(440px, calc(100% - 24px));
155
168
  /* The variant list makes the panel taller than a laptop in landscape -
156
- the top of it would be behind the browser chrome without this */
157
- max-height: calc(100dvh - 120px);
169
+ it takes what is left under the header and scrolls the rest */
170
+ max-height: calc(100dvh - var(--mw-header-height) - 24px);
158
171
  overflow-y: auto;
159
172
  overscroll-behavior: contain;
160
173
  padding: 18px;
@@ -162,6 +175,26 @@
162
175
  border: 1px solid var(--mw-border);
163
176
  background: var(--mw-card-background);
164
177
  box-shadow: var(--mw-elevation-5);
178
+ transform: translateY(0);
179
+ opacity: 1;
180
+ /* display is in the list so allow-discrete keeps the panel mounted
181
+ through the fade out - `hidden` is the state the script writes */
182
+ transition-property: opacity, transform, display;
183
+ transition-duration: var(--mw-duration-base);
184
+ transition-timing-function: var(--mw-ease-out);
185
+ transition-behavior: allow-discrete;
186
+ }
187
+
188
+ @starting-style {
189
+ .palette-panel {
190
+ opacity: 0;
191
+ transform: translateY(-12px);
192
+ }
193
+ }
194
+
195
+ .palette-panel[hidden] {
196
+ opacity: 0;
197
+ transform: translateY(-12px);
165
198
  }
166
199
 
167
200
  .palette-heading {
@@ -178,7 +211,7 @@
178
211
 
179
212
  .palette-grid {
180
213
  display: grid;
181
- grid-template-columns: 1fr 1fr;
214
+ grid-template-columns: repeat(3, 1fr);
182
215
  gap: 6px;
183
216
  }
184
217
 
@@ -288,7 +321,7 @@
288
321
 
289
322
  .variant-row select {
290
323
  flex-shrink: 0;
291
- max-width: 110px;
324
+ max-width: 135px;
292
325
  padding: 3px 6px;
293
326
  font-size: 0.75rem;
294
327
  color: var(--mw-text-color);
@@ -336,8 +369,18 @@
336
369
  background: var(--mw-primary-accent-text-color);
337
370
  }
338
371
 
339
- .variant-setup {
372
+ .variant-setup-label {
373
+ display: block;
340
374
  margin-top: 14px;
375
+ margin-bottom: 5px;
376
+ font-size: 0.7rem;
377
+ color: var(--mw-text-muted-color);
378
+ }
379
+
380
+ .variant-setup {
381
+ display: block;
382
+ width: 100%;
383
+ resize: vertical;
341
384
  padding: 10px;
342
385
  border-radius: 8px;
343
386
  border: 1px solid var(--mw-border);
@@ -349,25 +392,9 @@
349
392
  white-space: pre-wrap;
350
393
  word-break: break-word;
351
394
  }
352
-
353
- @media (max-width: 480px) {
354
- .palette-fab {
355
- right: 12px;
356
- bottom: 12px;
357
- width: 46px;
358
- height: 46px;
359
- }
360
-
361
- .palette-panel {
362
- right: 12px;
363
- left: 12px;
364
- bottom: 68px;
365
- width: auto;
366
- }
367
- }
368
395
  </style>
369
396
 
370
- <script src="maverick-wave.min.js"></script>
397
+ <script src="maverick-wave.min.js?v={{VERSION}}"></script>
371
398
  </head>
372
399
 
373
400
  <body>
@@ -895,15 +922,22 @@
895
922
  // root element repaints the page - bar, ink, borders and tints included.
896
923
  // Nothing is persisted - a palette is for looking at, a reload is the way back.
897
924
  document.addEventListener('DOMContentLoaded', () => {
898
- const fab = document.getElementById('palette-fab');
925
+ const toggleBtn = document.getElementById('palette-toggle');
899
926
  const panel = document.getElementById('palette-panel');
900
927
  const primaryInput = document.getElementById('palette-primary');
901
928
  const secondaryInput = document.getElementById('palette-secondary');
902
929
  const items = [...document.querySelectorAll('.palette-item')];
903
- if (!fab || !panel) return;
930
+ if (!toggleBtn || !panel) return;
904
931
 
905
932
  const root = document.documentElement;
906
933
 
934
+ // The values the markup ships with, so the setup box below can tell a
935
+ // chosen colour from the built-in one and print only the difference
936
+ const BUILT_IN = {
937
+ primary: primaryInput.value,
938
+ secondary: secondaryInput.value,
939
+ };
940
+
907
941
  const DARK_INK = '#131925';
908
942
  const LIGHT_INK = '#f2f6fc';
909
943
 
@@ -939,6 +973,7 @@
939
973
  void root.offsetHeight;
940
974
  root.classList.remove('mw-theme-switching');
941
975
  window.mwRefreshColorSwatches?.();
976
+ window.mwRefreshSetup?.();
942
977
  };
943
978
 
944
979
  const paint = (name, color) => {
@@ -980,10 +1015,10 @@
980
1015
 
981
1016
  const toggle = (open) => {
982
1017
  panel.hidden = !open;
983
- fab.setAttribute('aria-expanded', String(open));
1018
+ toggleBtn.setAttribute('aria-expanded', String(open));
984
1019
  };
985
1020
 
986
- fab.addEventListener('click', (event) => {
1021
+ toggleBtn.addEventListener('click', (event) => {
987
1022
  event.stopPropagation();
988
1023
  toggle(panel.hidden);
989
1024
  });
@@ -993,6 +1028,79 @@
993
1028
  document.addEventListener('keydown', (event) => {
994
1029
  if (event.key === 'Escape') toggle(false);
995
1030
  });
1031
+
1032
+ // The seam to the variants panel below, which prints and reads the whole
1033
+ // setup - the palette is half of it. Only inline values count as chosen:
1034
+ // anything still coming from the stylesheet is the built-in one.
1035
+ const PAGE_GROUNDS = {
1036
+ dark: '--mw-dark-page-background',
1037
+ light: '--mw-light-page-background',
1038
+ };
1039
+
1040
+ window.mwPalette = {
1041
+ read: () => ({
1042
+ primary: root.style.getPropertyValue('--mw-primary-color') || null,
1043
+ secondary:
1044
+ root.style.getPropertyValue('--mw-secondary-color') || null,
1045
+ // The label colour ink() measured for each fill. It has to travel
1046
+ // with the setup: a project that only copies the two brand colours
1047
+ // keeps the stock accent text, and on a light brand tone that is an
1048
+ // unreadable button. write() recomputes it, so pasting it back is
1049
+ // harmless.
1050
+ primaryInk:
1051
+ root.style.getPropertyValue('--mw-primary-accent-text-color') ||
1052
+ null,
1053
+ secondaryInk:
1054
+ root.style.getPropertyValue('--mw-secondary-accent-text-color') ||
1055
+ null,
1056
+ dark: root.style.getPropertyValue(PAGE_GROUNDS.dark) || null,
1057
+ light: root.style.getPropertyValue(PAGE_GROUNDS.light) || null,
1058
+ }),
1059
+
1060
+ write: ({ primary, secondary, dark, light }) => {
1061
+ const current = window.mwPalette.read();
1062
+ const same =
1063
+ current.primary === primary &&
1064
+ current.secondary === secondary &&
1065
+ current.dark === dark &&
1066
+ current.light === light;
1067
+ // Typing into the setup box fires per keystroke, and settle() forces
1068
+ // a reflow - so a run that changes nothing has to cost nothing
1069
+ if (same) return;
1070
+
1071
+ [
1072
+ ['primary', primary, primaryInput],
1073
+ ['secondary', secondary, secondaryInput],
1074
+ ].forEach(([name, value, input]) => {
1075
+ if (value) {
1076
+ paint(name, value);
1077
+ input.value = value;
1078
+ } else {
1079
+ root.style.removeProperty(`--mw-${name}-color`);
1080
+ root.style.removeProperty(`--mw-${name}-accent-text-color`);
1081
+ input.value = BUILT_IN[name];
1082
+ }
1083
+ });
1084
+
1085
+ Object.entries(PAGE_GROUNDS).forEach(([key, prop]) => {
1086
+ const value = key === 'dark' ? dark : light;
1087
+ if (value) root.style.setProperty(prop, value);
1088
+ else root.style.removeProperty(prop);
1089
+ });
1090
+
1091
+ const shownPrimary = primary || BUILT_IN.primary;
1092
+ const shownSecondary = secondary || BUILT_IN.secondary;
1093
+ items.forEach((item) =>
1094
+ item.classList.toggle(
1095
+ 'mw-active',
1096
+ item.dataset.primary === shownPrimary &&
1097
+ item.dataset.secondary === shownSecondary
1098
+ )
1099
+ );
1100
+
1101
+ settle();
1102
+ },
1103
+ };
996
1104
  });
997
1105
 
998
1106
  // just for the showcase: the variant switches. Each one is a class on <html>
@@ -1014,13 +1122,36 @@
1014
1122
  : input.checked;
1015
1123
 
1016
1124
  const render = () => {
1125
+ // Never while it is being typed in - the box is an input as well as a
1126
+ // readout, and rewriting it under the cursor would eat the paste
1127
+ if (document.activeElement === setup) return;
1128
+
1017
1129
  const classes = [];
1018
1130
  const notes = [];
1019
1131
  const props = [];
1020
1132
 
1133
+ // The palette is part of the setup, and it is printed first because a
1134
+ // project sets its brand colours before it retunes anything else
1135
+ const palette = window.mwPalette?.read() ?? {};
1136
+ [
1137
+ ['--mw-primary-color', palette.primary],
1138
+ ['--mw-primary-accent-text-color', palette.primaryInk],
1139
+ ['--mw-secondary-color', palette.secondary],
1140
+ ['--mw-secondary-accent-text-color', palette.secondaryInk],
1141
+ ['--mw-dark-page-background', palette.dark],
1142
+ ['--mw-light-page-background', palette.light],
1143
+ ].forEach(([prop, value]) => {
1144
+ if (value) props.push(` ${prop}: ${value};`);
1145
+ });
1146
+
1021
1147
  controls.forEach((control) => {
1022
1148
  if (control.type !== 'checkbox') {
1023
- if (control.value) {
1149
+ // A select whose options are class names picks one of a set the
1150
+ // markup cannot hold at the same time - a shape is one or none
1151
+ if (!control.value) return;
1152
+ if (control.hasAttribute('data-variant-classes')) {
1153
+ classes.push(control.value);
1154
+ } else {
1024
1155
  props.push(
1025
1156
  ` ${control.dataset.variantProp}: ${control.value};`
1026
1157
  );
@@ -1047,16 +1178,22 @@
1047
1178
  lines.push(...notes);
1048
1179
  if (props.length) lines.push(':root {', ...props, '}');
1049
1180
 
1050
- setup.textContent = lines.length
1181
+ setup.value = lines.length
1051
1182
  ? lines.join('\n')
1052
1183
  : 'Default setup - nothing to change.';
1053
1184
  };
1054
1185
 
1186
+ const optionClasses = (control) =>
1187
+ [...control.options].map((option) => option.value).filter(Boolean);
1188
+
1055
1189
  const apply = (control) => {
1056
1190
  if (control.type === 'checkbox') {
1057
1191
  const target = control.dataset.variantTarget;
1058
1192
  const el = target ? document.querySelector(target) : root;
1059
1193
  el?.classList.toggle(control.dataset.variantClass, isOn(control));
1194
+ } else if (control.hasAttribute('data-variant-classes')) {
1195
+ root.classList.remove(...optionClasses(control));
1196
+ if (control.value) root.classList.add(control.value);
1060
1197
  } else if (control.value) {
1061
1198
  root.style.setProperty(control.dataset.variantProp, control.value);
1062
1199
  } else {
@@ -1064,6 +1201,61 @@
1064
1201
  }
1065
1202
  };
1066
1203
 
1204
+ // The box read backwards: someone else's setup pasted in drives the
1205
+ // switches, which drive the page. Anything the text does not mention
1206
+ // falls back to the default, so an empty box is a reset.
1207
+ const load = (text) => {
1208
+ const named = new Set(
1209
+ (text.match(/class\s*=\s*"([^"]*)"/)?.[1] || '')
1210
+ .trim()
1211
+ .split(/\s+/)
1212
+ .filter(Boolean)
1213
+ );
1214
+ // `.mw-header: no mw-header-reveal` - a class the markup ships and the
1215
+ // setup takes away, which is the deviation worth reading
1216
+ const dropped = new Set(
1217
+ [...text.matchAll(/:\s*no\s+([\w-]+)/g)].map((m) => m[1])
1218
+ );
1219
+ const props = new Map(
1220
+ [...text.matchAll(/(--[\w-]+)\s*:\s*([^;\n}]+)/g)].map((m) => [
1221
+ m[1],
1222
+ m[2].trim(),
1223
+ ])
1224
+ );
1225
+
1226
+ window.mwPalette?.write({
1227
+ primary: props.get('--mw-primary-color') ?? null,
1228
+ secondary: props.get('--mw-secondary-color') ?? null,
1229
+ dark: props.get('--mw-dark-page-background') ?? null,
1230
+ light: props.get('--mw-light-page-background') ?? null,
1231
+ });
1232
+
1233
+ controls.forEach((control) => {
1234
+ if (control.type === 'checkbox') {
1235
+ const name = control.dataset.variantClass;
1236
+ const on = control.dataset.variantTarget
1237
+ ? !dropped.has(name)
1238
+ : named.has(name);
1239
+ control.checked = control.hasAttribute('data-variant-invert')
1240
+ ? !on
1241
+ : on;
1242
+ } else if (control.hasAttribute('data-variant-classes')) {
1243
+ control.value =
1244
+ optionClasses(control).find((name) => named.has(name)) ?? '';
1245
+ } else {
1246
+ control.value = props.get(control.dataset.variantProp) ?? '';
1247
+ }
1248
+ apply(control);
1249
+ });
1250
+ };
1251
+
1252
+ // Picking a palette repaints the page, and the box has to say so
1253
+ window.mwRefreshSetup = render;
1254
+
1255
+ setup.addEventListener('input', () => load(setup.value));
1256
+ // Normalises what was pasted back into the shape render() writes
1257
+ setup.addEventListener('blur', render);
1258
+
1067
1259
  controls.forEach((control) =>
1068
1260
  control.addEventListener('change', () => {
1069
1261
  apply(control);