@frontpanel/css 0.3.0 → 0.5.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/components.css CHANGED
@@ -29,6 +29,19 @@
29
29
  }
30
30
  }
31
31
 
32
+ /* A menu dropping open from its button: a short slide down as it fades in. */
33
+ @keyframes fp-menu-drop {
34
+ from {
35
+ opacity: 0;
36
+ translate: 0 -4px;
37
+ }
38
+
39
+ to {
40
+ opacity: 1;
41
+ translate: 0 0;
42
+ }
43
+ }
44
+
32
45
  /** Redraws an LCD line left to right in four steps, as when its value changes. */
33
46
  .fp-lcd-redraw {
34
47
  animation: fp-lcd-redraw 160ms steps(4, end);
@@ -306,6 +319,34 @@
306
319
  }
307
320
  }
308
321
 
322
+ /**
323
+ * Chrome buttons joined side by side into one bezel, as on a transport row: fp-chrome-btn children
324
+ * share their seams, and only the outer corners stay round. Give it `role="group"` and a name.
325
+ */
326
+ .fp-button-group {
327
+ display: inline-flex;
328
+ align-items: stretch;
329
+
330
+ /* The 1 px edge round each button falls in the gap, so neighbours share one seam. */
331
+ gap: 1px;
332
+
333
+ & > .fp-chrome-btn:not(:first-child) {
334
+ border-start-start-radius: 0;
335
+ border-end-start-radius: 0;
336
+ }
337
+
338
+ & > .fp-chrome-btn:not(:last-child) {
339
+ border-start-end-radius: 0;
340
+ border-end-end-radius: 0;
341
+ }
342
+
343
+ /* A focused button's ring goes over its neighbours, not under them. */
344
+ & > .fp-chrome-btn:focus-visible {
345
+ position: relative;
346
+ z-index: 1;
347
+ }
348
+ }
349
+
309
350
  /**
310
351
  * Molded-plastic cap. The cap colour and its label colour come from the fp-cap-* classes
311
352
  * (`--fp-cap`, `--fp-cap-ink`); a bare `fp-cap` is white plastic with a dark label. Selected caps
@@ -1005,6 +1046,413 @@
1005
1046
  }
1006
1047
  }
1007
1048
 
1049
+ /**
1050
+ * A row of latching piano keys in a dark well, after the key row on an old cassette deck. Its
1051
+ * children alternate: an fp-silk label printed over the key, then the fp-key. Give it
1052
+ * `role="radiogroup"` round radio keys, or `role="group"` round checkbox keys, and a name; one
1053
+ * checkbox key needs no role. With `data-orientation="vertical"` the keys lie on their side,
1054
+ * stacked, each label before its key.
1055
+ */
1056
+ .fp-keys {
1057
+ display: inline-grid;
1058
+ grid-auto-flow: column;
1059
+
1060
+ /* Every key in a row is one width: the widest label's, or the key's own. */
1061
+ grid-auto-columns: 1fr;
1062
+ grid-template-rows: auto auto;
1063
+ row-gap: 0.5rem;
1064
+ justify-items: center;
1065
+ align-items: end;
1066
+
1067
+ /* A label, with its LED over it when the light is above the key. */
1068
+ & > .fp-silk {
1069
+ display: inline-flex;
1070
+ flex-direction: column;
1071
+ align-items: center;
1072
+ gap: 0.375rem;
1073
+ padding-inline: 0.25rem;
1074
+ white-space: nowrap;
1075
+ }
1076
+
1077
+ &[data-orientation="vertical"] {
1078
+ grid-auto-flow: row;
1079
+ grid-auto-columns: auto;
1080
+ grid-auto-rows: 1fr;
1081
+ grid-template-columns: auto auto;
1082
+ grid-template-rows: none;
1083
+ column-gap: 0.75rem;
1084
+ row-gap: 0;
1085
+ justify-items: stretch;
1086
+ align-items: center;
1087
+
1088
+ & > .fp-silk {
1089
+ flex-direction: row;
1090
+ justify-content: flex-end;
1091
+ padding-inline: 0;
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * A piano key in an fp-keys row: knurled plastic, silver or dark by skin, with a strip at its foot,
1098
+ * standing in its stretch of the well. Put it on a button with `role="radio"` or `role="checkbox"` and
1099
+ * `aria-checked`; checked, it latches part way down, smaller and darker under the well's lip, and
1100
+ * a press sinks it deeper. The strip is an LED in the key's `fp-led-*` colour (green), lit while
1101
+ * latched; with `data-strip="plain"` it is cream plastic, for keys whose LED sits over the label,
1102
+ * or that have none.
1103
+ */
1104
+ .fp-key {
1105
+ /* How far the key is shaded, latched or held down. */
1106
+ --fp-key-shade: 0;
1107
+
1108
+ /* The well round the key: a narrow seam between keys, wider at the ends of the row. */
1109
+ --fp-key-start: 2px;
1110
+ --fp-key-end: 2px;
1111
+
1112
+ /* The well's edge, drawn on the outside of the row only, so neighbours show no seam. */
1113
+ --fp-key-edge-start: 0 0 transparent;
1114
+ --fp-key-edge-end: 0 0 transparent;
1115
+ position: relative;
1116
+ justify-self: stretch;
1117
+ min-width: calc(3.5rem + 4px);
1118
+ min-height: calc(4.75rem + 14px);
1119
+ padding: 0;
1120
+ border: 0;
1121
+ border-radius: 0;
1122
+ background-color: var(--fp-color-key-well);
1123
+ box-shadow:
1124
+ inset 0 3px 5px rgb(0 0 0 / 0.8),
1125
+ 0 -1px 0 var(--fp-color-edge),
1126
+ 0 1px 0 var(--fp-color-edge),
1127
+ 0 2px 0 var(--fp-color-bevel-hi),
1128
+ var(--fp-key-edge-start),
1129
+ var(--fp-key-edge-end);
1130
+
1131
+ @media (pointer: fine) {
1132
+ min-width: calc(3rem + 4px);
1133
+ min-height: calc(4.125rem + 14px);
1134
+ }
1135
+
1136
+ &:nth-child(1 of .fp-key) {
1137
+ --fp-key-start: 4px;
1138
+ --fp-key-edge-start: -1px 0 0 var(--fp-color-edge);
1139
+ border-start-start-radius: 6px;
1140
+ border-end-start-radius: 6px;
1141
+ }
1142
+
1143
+ &:nth-last-child(1 of .fp-key) {
1144
+ --fp-key-end: 4px;
1145
+ --fp-key-edge-end: 1px 0 0 var(--fp-color-edge);
1146
+ border-start-end-radius: 6px;
1147
+ border-end-end-radius: 6px;
1148
+ }
1149
+
1150
+ /*
1151
+ * The key itself: a glossy frame lit from the top left, round a knurled face, a fine diamond grid
1152
+ * light on one diagonal and shaded on the other. It stands 4px below the well's top, with room
1153
+ * under it to travel down.
1154
+ */
1155
+ &::before {
1156
+ content: "";
1157
+ position: absolute;
1158
+ inset-block: 4px 10px;
1159
+ inset-inline: var(--fp-key-start) var(--fp-key-end);
1160
+ border-radius: 5px;
1161
+ background:
1162
+ linear-gradient(var(--fp-color-key-knurl-lo), transparent 2px) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1163
+ repeating-linear-gradient(45deg, var(--fp-color-key-knurl-hi) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1164
+ repeating-linear-gradient(-45deg, var(--fp-color-key-knurl-lo) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1165
+ linear-gradient(var(--fp-color-key-hi), color-mix(in srgb, var(--fp-color-key-hi) 50%, var(--fp-color-key))) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1166
+ linear-gradient(160deg, var(--fp-color-key-hi) 0%, var(--fp-color-key) 35%, var(--fp-color-key-lo) 100%);
1167
+ box-shadow:
1168
+ inset 1px 1px 0 rgb(255 255 255 / 0.28),
1169
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1170
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1171
+ 0 3px 0 rgb(0 0 0 / 0.9);
1172
+ transform-origin: 50% 100%;
1173
+ transition-property: translate, scale, filter, box-shadow;
1174
+ transition-duration: 70ms;
1175
+ transition-timing-function: linear;
1176
+ }
1177
+
1178
+ /*
1179
+ * The strip at the key's foot: an LED lens, dark in its colour until the key latches, set in a
1180
+ * rim of the well's colour, so a lit strip stands out on a pale key too.
1181
+ */
1182
+ &::after {
1183
+ content: "";
1184
+ position: absolute;
1185
+ inset-block-end: 15px;
1186
+ inset-inline: calc(var(--fp-key-start) + 5px) calc(var(--fp-key-end) + 5px);
1187
+ height: 7px;
1188
+ border-radius: 2px;
1189
+ background: color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 32%, black);
1190
+ box-shadow:
1191
+ inset 0 1px 1px rgb(0 0 0 / 0.6),
1192
+ 0 0 0 1.5px var(--fp-color-key-well);
1193
+
1194
+ /* It moves with the key: scaled from the key's foot, 5px below its own. */
1195
+ transform-origin: 50% calc(100% + 5px);
1196
+ transition-property: translate, scale, filter;
1197
+ transition-duration: 70ms;
1198
+ transition-timing-function: linear;
1199
+ }
1200
+
1201
+ @media (prefers-reduced-motion: reduce) {
1202
+ &::before,
1203
+ &::after {
1204
+ transition-duration: 0ms;
1205
+ }
1206
+ }
1207
+
1208
+ /* A plain strip: cream plastic, shaded with the key. */
1209
+ &[data-strip="plain"]::after {
1210
+ height: 8px;
1211
+ border-radius: 1px;
1212
+ background: linear-gradient(
1213
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, white),
1214
+ var(--fp-color-key-strip) 60%,
1215
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, black)
1216
+ );
1217
+ box-shadow: 0 0 0 1.5px var(--fp-color-key-well);
1218
+ filter: brightness(calc(1 - var(--fp-key-shade)));
1219
+ }
1220
+
1221
+ &:hover:not(:disabled)::before {
1222
+ filter: brightness(1.1);
1223
+ }
1224
+
1225
+ /* Latched: well down, a little smaller and darker, the well's lip shading its top edge. */
1226
+ &[aria-checked="true"] {
1227
+ --fp-key-shade: 0.24;
1228
+
1229
+ &::before {
1230
+ translate: 0 6px;
1231
+ scale: 0.96;
1232
+ box-shadow:
1233
+ inset 0 6px 5px -2px rgb(0 0 0 / 0.9),
1234
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1235
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1236
+ 0 0 0 transparent;
1237
+ }
1238
+
1239
+ &::after {
1240
+ translate: 0 6px;
1241
+ scale: 0.96;
1242
+ }
1243
+ }
1244
+
1245
+ /* A latched LED strip lights, brightest in the middle, and spills its glow up over the face. */
1246
+ &[aria-checked="true"]:not([data-strip="plain"])::after {
1247
+ background: radial-gradient(
1248
+ ellipse 60% 120% at 50% 50%,
1249
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, white),
1250
+ var(--fp-led, var(--fp-color-led-on))
1251
+ );
1252
+ box-shadow:
1253
+ 0 0 0 1.5px var(--fp-color-key-well),
1254
+ 0 0 6px 1px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 70%, transparent),
1255
+ 0 -5px 12px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 35%, transparent);
1256
+ }
1257
+
1258
+ /* Held under a finger: past the latch, deeper and darker still. */
1259
+ &:active:not(:disabled) {
1260
+ --fp-key-shade: 0.36;
1261
+
1262
+ &::before {
1263
+ translate: 0 9px;
1264
+ scale: 0.94;
1265
+ box-shadow:
1266
+ inset 0 7px 5px -2px rgb(0 0 0 / 0.95),
1267
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1268
+ 0 0 0 transparent;
1269
+ }
1270
+
1271
+ &::after {
1272
+ translate: 0 9px;
1273
+ scale: 0.94;
1274
+ }
1275
+ }
1276
+
1277
+ /*
1278
+ * The ring goes round the key, in the dark well, where the skin's own ring could vanish: its
1279
+ * own colour, with the usual glow outside it. Over its neighbours, not under them.
1280
+ */
1281
+ &:focus-visible {
1282
+ z-index: 1;
1283
+ outline: none;
1284
+ }
1285
+
1286
+ &:focus-visible::before {
1287
+ outline: 2px solid var(--fp-color-key-ring);
1288
+ outline-offset: 2px;
1289
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
1290
+ }
1291
+
1292
+ /* The hover filter outranks the focus rule, so a hovered key keeps its glow here. */
1293
+ &:focus-visible:hover:not(:disabled)::before {
1294
+ filter: brightness(1.1) drop-shadow(0 0 3px var(--fp-color-focus));
1295
+ }
1296
+
1297
+ /* Disabled, the key stays up and fades; its stretch of the well stays dark. */
1298
+ &:disabled::before,
1299
+ &:disabled::after {
1300
+ opacity: 0.45;
1301
+ }
1302
+
1303
+ /*
1304
+ * On its side, in a vertical fp-keys: the well runs top to bottom, the strip is at the key's
1305
+ * right end, and a latched key slides right, toward it, the well's left lip shading its near edge.
1306
+ */
1307
+ [data-orientation="vertical"] > & {
1308
+ --fp-key-edge-start: 0 0 transparent;
1309
+ --fp-key-edge-end: 0 0 transparent;
1310
+ min-width: calc(4.75rem + 14px);
1311
+ min-height: calc(3.5rem + 4px);
1312
+ border-radius: 0;
1313
+ box-shadow:
1314
+ inset 3px 0 5px rgb(0 0 0 / 0.8),
1315
+ -1px 0 0 var(--fp-color-edge),
1316
+ 1px 0 0 var(--fp-color-edge),
1317
+ 1px 1px 0 var(--fp-color-bevel-hi),
1318
+ var(--fp-key-edge-start),
1319
+ var(--fp-key-edge-end);
1320
+
1321
+ @media (pointer: fine) {
1322
+ min-width: calc(4.125rem + 14px);
1323
+ min-height: calc(3rem + 4px);
1324
+ }
1325
+
1326
+ &:nth-child(1 of .fp-key) {
1327
+ --fp-key-edge-start: 0 -1px 0 var(--fp-color-edge);
1328
+ border-radius: 6px 6px 0 0;
1329
+ }
1330
+
1331
+ &:nth-last-child(1 of .fp-key) {
1332
+ --fp-key-edge-end: 0 1px 0 var(--fp-color-edge), 0 2px 0 var(--fp-color-bevel-hi);
1333
+ border-radius: 0 0 6px 6px;
1334
+ }
1335
+
1336
+ &:nth-child(1 of .fp-key):nth-last-child(1 of .fp-key) {
1337
+ border-radius: 6px;
1338
+ }
1339
+
1340
+ &::before {
1341
+ inset: var(--fp-key-start) 10px var(--fp-key-end) 4px;
1342
+ background:
1343
+ linear-gradient(90deg, rgb(0 0 0 / 0.5), transparent 2px) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1344
+ repeating-linear-gradient(45deg, rgb(255 255 255 / 0.11) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1345
+ repeating-linear-gradient(-45deg, rgb(0 0 0 / 0.45) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1346
+ linear-gradient(var(--fp-color-key-hi), color-mix(in srgb, var(--fp-color-key-hi) 50%, var(--fp-color-key))) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1347
+ linear-gradient(160deg, var(--fp-color-key-hi) 0%, var(--fp-color-key) 35%, var(--fp-color-key-lo) 100%);
1348
+ box-shadow:
1349
+ inset 1px 1px 0 rgb(255 255 255 / 0.28),
1350
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1351
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1352
+ 3px 0 0 rgb(0 0 0 / 0.9);
1353
+ transform-origin: 100% 50%;
1354
+ }
1355
+
1356
+ &::after {
1357
+ inset: calc(var(--fp-key-start) + 5px) 15px calc(var(--fp-key-end) + 5px) auto;
1358
+ width: 7px;
1359
+ height: auto;
1360
+ transform-origin: calc(100% + 5px) 50%;
1361
+ }
1362
+
1363
+ &[data-strip="plain"]::after {
1364
+ width: 8px;
1365
+ background: linear-gradient(
1366
+ 90deg,
1367
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, white),
1368
+ var(--fp-color-key-strip) 60%,
1369
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, black)
1370
+ );
1371
+ }
1372
+
1373
+ &[aria-checked="true"] {
1374
+ &::before {
1375
+ translate: 6px 0;
1376
+ box-shadow:
1377
+ inset 6px 0 5px -2px rgb(0 0 0 / 0.9),
1378
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1379
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1380
+ 0 0 0 transparent;
1381
+ }
1382
+
1383
+ &::after {
1384
+ translate: 6px 0;
1385
+ }
1386
+ }
1387
+
1388
+ &[aria-checked="true"]:not([data-strip="plain"])::after {
1389
+ background: radial-gradient(
1390
+ ellipse 120% 60% at 50% 50%,
1391
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, white),
1392
+ var(--fp-led, var(--fp-color-led-on))
1393
+ );
1394
+ box-shadow:
1395
+ 0 0 0 1.5px var(--fp-color-key-well),
1396
+ 0 0 6px 1px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 70%, transparent),
1397
+ -5px 0 12px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 35%, transparent);
1398
+ }
1399
+
1400
+ &:active:not(:disabled) {
1401
+ &::before {
1402
+ translate: 9px 0;
1403
+ box-shadow:
1404
+ inset 7px 0 5px -2px rgb(0 0 0 / 0.95),
1405
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1406
+ 0 0 0 transparent;
1407
+ }
1408
+
1409
+ &::after {
1410
+ translate: 9px 0;
1411
+ }
1412
+ }
1413
+ }
1414
+
1415
+ /*
1416
+ * Forced colours drop the plastic, the well and the light: each key and its strip keep an outline,
1417
+ * and a latched key opts out to fill in the system's selected colour, its strip in the selected
1418
+ * text colour, so it reads apart from the rest by fill as well as by its travel. The selectors
1419
+ * match the rules they override, the vertical ones too.
1420
+ */
1421
+ @media (forced-colors: active) {
1422
+ &::before,
1423
+ &::after {
1424
+ outline: 1px solid ButtonText;
1425
+ }
1426
+
1427
+ &[aria-checked="true"]::before,
1428
+ [data-orientation="vertical"] > &[aria-checked="true"]::before {
1429
+ forced-color-adjust: none;
1430
+ background: SelectedItem;
1431
+ box-shadow: none;
1432
+ }
1433
+
1434
+ &[aria-checked="true"]::after,
1435
+ &[aria-checked="true"]:not([data-strip="plain"])::after,
1436
+ [data-orientation="vertical"] > &[aria-checked="true"]::after,
1437
+ [data-orientation="vertical"] > &[aria-checked="true"]:not([data-strip="plain"])::after {
1438
+ forced-color-adjust: none;
1439
+ background: SelectedItemText;
1440
+ box-shadow: none;
1441
+ filter: none;
1442
+ }
1443
+
1444
+ &:focus-visible::before {
1445
+ outline: 2px solid CanvasText;
1446
+ }
1447
+
1448
+ &:disabled::before,
1449
+ &:disabled::after {
1450
+ opacity: 1;
1451
+ outline-color: GrayText;
1452
+ }
1453
+ }
1454
+ }
1455
+
1008
1456
  /**
1009
1457
  * Level meter: a strip of LED segments in a dark housing, lit green, then amber, then red. Set
1010
1458
  * `--fp-meter-level` (0 to 1, in whole segments), `--fp-meter-segments` (12), and where amber and
@@ -1369,6 +1817,130 @@
1369
1817
  padding: 0.75rem;
1370
1818
  }
1371
1819
 
1820
+ /**
1821
+ * Faceplate menu: a small brushed-chrome plate that drops from the button that opens it, holding
1822
+ * fp-menu-item rows and fp-menu-separator grooves. Placing it is up to you, or to Reka's
1823
+ * DropdownMenu, which also sets the `data-state="open"` it drops in on.
1824
+ */
1825
+ .fp-menu {
1826
+ color-scheme: var(--fp-scheme);
1827
+ z-index: 50;
1828
+ box-sizing: border-box;
1829
+ min-width: 10rem;
1830
+ overflow-y: auto;
1831
+ padding: 0.25rem;
1832
+ border-radius: 6px;
1833
+ color: var(--fp-color-ink);
1834
+ background-image: linear-gradient(
1835
+ 180deg,
1836
+ color-mix(in srgb, var(--fp-color-chrome-hi) 60%, var(--fp-color-chrome-100)) 0%,
1837
+ var(--fp-color-chrome-100) 40%,
1838
+ var(--fp-color-chrome-300) 100%
1839
+ );
1840
+ box-shadow:
1841
+ 0 0 0 1px var(--fp-color-edge),
1842
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
1843
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
1844
+ 2px 3px 0 var(--fp-color-cast),
1845
+ 0 12px 18px -8px var(--fp-color-cast);
1846
+
1847
+ /* The plate takes focus when a pointer opens it; its rows show where the keys are. */
1848
+ outline: none;
1849
+
1850
+ &[data-state="open"] {
1851
+ animation: fp-menu-drop 90ms ease-out;
1852
+ }
1853
+
1854
+ @media (prefers-reduced-motion: reduce) {
1855
+ &[data-state="open"] {
1856
+ animation: none;
1857
+ }
1858
+ }
1859
+
1860
+ /* Forced colours drop the chrome and its shadows: an outline keeps the plate's edge. */
1861
+ @media (forced-colors: active) {
1862
+ outline: 1px solid CanvasText;
1863
+ }
1864
+ }
1865
+
1866
+ /**
1867
+ * A row on an fp-menu: a silkscreened label, with an fp-icon before it if you like. The highlighted
1868
+ * row presses into the plate; Reka sets `data-highlighted` on the row under the pointer or the
1869
+ * arrow keys, and `data-disabled` on one that can't be chosen. 44 px tall, 36 px with a mouse,
1870
+ * trackpad or pen.
1871
+ */
1872
+ .fp-menu-item {
1873
+ display: flex;
1874
+ align-items: center;
1875
+ gap: 0.5rem;
1876
+ min-height: 2.75rem;
1877
+ padding-inline: 0.625rem;
1878
+ border-radius: 4px;
1879
+ color: var(--fp-color-ink);
1880
+ font-family: var(--fp-font-display);
1881
+ font-size: 11px;
1882
+ line-height: 1.3;
1883
+ letter-spacing: 0.08em;
1884
+ text-transform: uppercase;
1885
+ text-decoration: none;
1886
+ white-space: nowrap;
1887
+ cursor: default;
1888
+ user-select: none;
1889
+ outline: none;
1890
+
1891
+ @media (pointer: fine) {
1892
+ min-height: 2.25rem;
1893
+ }
1894
+
1895
+ &[data-highlighted] {
1896
+ background-color: var(--fp-color-chrome-300);
1897
+ box-shadow:
1898
+ inset 1px 1px 3px var(--fp-color-bevel-lo),
1899
+ inset -1px -1px 0 var(--fp-color-bevel-hi);
1900
+ }
1901
+
1902
+ /* The keyboard's row also gets the ring every focused control has, inside the row. */
1903
+ &:focus-visible {
1904
+ outline: 2px solid var(--fp-color-focus-ring);
1905
+ outline-offset: -2px;
1906
+ }
1907
+
1908
+ &[data-disabled] {
1909
+ opacity: 0.45;
1910
+ }
1911
+
1912
+ /*
1913
+ * Forced colours drop the pressed-in shading: the system's highlight colours show the highlighted
1914
+ * row, and GrayText, not a fade, shows a disabled one.
1915
+ */
1916
+ @media (forced-colors: active) {
1917
+ &[data-highlighted] {
1918
+ forced-color-adjust: none;
1919
+ background: Highlight;
1920
+ box-shadow: none;
1921
+ color: HighlightText;
1922
+
1923
+ /* Opted out, its focus ring would keep the skin's colour: the system's text colour shows on any ground. */
1924
+ &:focus-visible {
1925
+ outline-color: HighlightText;
1926
+ }
1927
+ }
1928
+
1929
+ &[data-disabled] {
1930
+ opacity: 1;
1931
+ color: GrayText;
1932
+ }
1933
+ }
1934
+ }
1935
+
1936
+ /** A groove between groups of rows on an fp-menu, cut into the plate: shaded above, lit below. */
1937
+ .fp-menu-separator {
1938
+ height: 0;
1939
+ margin: 0.25rem 0.375rem;
1940
+ border-top: 1px solid var(--fp-color-bevel-lo);
1941
+ border-bottom: 1px solid var(--fp-color-bevel-hi);
1942
+ }
1943
+
1372
1944
  /** Recessed well inside a matte panel, for secondary content or an empty slot. */
1373
1945
  .fp-well {
1374
1946
  color-scheme: var(--fp-scheme);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frontpanel/css",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "Early-2000s hardware skin as plain CSS: liquid-chrome bezels, gel buttons, LCDs and LEDs. Tokens, classes and an optional Tailwind 4 entry.",
6
6
  "author": "Dulan Katukurundage",
7
7
  "license": "MIT",
package/tailwind.css CHANGED
@@ -32,6 +32,14 @@
32
32
  --color-fp-led-amber: var(--fp-color-led-amber);
33
33
  --color-fp-led-red: var(--fp-color-led-red);
34
34
  --color-fp-led-off: var(--fp-color-led-off);
35
+ --color-fp-key-hi: var(--fp-color-key-hi);
36
+ --color-fp-key: var(--fp-color-key);
37
+ --color-fp-key-lo: var(--fp-color-key-lo);
38
+ --color-fp-key-well: var(--fp-color-key-well);
39
+ --color-fp-key-knurl-hi: var(--fp-color-key-knurl-hi);
40
+ --color-fp-key-knurl-lo: var(--fp-color-key-knurl-lo);
41
+ --color-fp-key-strip: var(--fp-color-key-strip);
42
+ --color-fp-key-ring: var(--fp-color-key-ring);
35
43
  --color-fp-cap: var(--fp-color-cap);
36
44
  --color-fp-cap-ink: var(--fp-color-cap-ink);
37
45
  --color-fp-cap-blue: var(--fp-color-cap-blue);
@@ -86,6 +94,19 @@
86
94
  opacity: 1;
87
95
  }
88
96
  }
97
+
98
+ /* A menu dropping open from its button: a short slide down as it fades in. */
99
+ @keyframes fp-menu-drop {
100
+ from {
101
+ opacity: 0;
102
+ translate: 0 -4px;
103
+ }
104
+
105
+ to {
106
+ opacity: 1;
107
+ translate: 0 0;
108
+ }
109
+ }
89
110
  }
90
111
 
91
112
  /** Redraws an LCD line left to right in four steps, as when its value changes. */
@@ -365,6 +386,34 @@
365
386
  }
366
387
  }
367
388
 
389
+ /**
390
+ * Chrome buttons joined side by side into one bezel, as on a transport row: fp-chrome-btn children
391
+ * share their seams, and only the outer corners stay round. Give it `role="group"` and a name.
392
+ */
393
+ @utility fp-button-group {
394
+ display: inline-flex;
395
+ align-items: stretch;
396
+
397
+ /* The 1 px edge round each button falls in the gap, so neighbours share one seam. */
398
+ gap: 1px;
399
+
400
+ & > .fp-chrome-btn:not(:first-child) {
401
+ border-start-start-radius: 0;
402
+ border-end-start-radius: 0;
403
+ }
404
+
405
+ & > .fp-chrome-btn:not(:last-child) {
406
+ border-start-end-radius: 0;
407
+ border-end-end-radius: 0;
408
+ }
409
+
410
+ /* A focused button's ring goes over its neighbours, not under them. */
411
+ & > .fp-chrome-btn:focus-visible {
412
+ position: relative;
413
+ z-index: 1;
414
+ }
415
+ }
416
+
368
417
  /**
369
418
  * Molded-plastic cap. The cap colour and its label colour come from the fp-cap-* classes
370
419
  * (`--fp-cap`, `--fp-cap-ink`); a bare `fp-cap` is white plastic with a dark label. Selected caps
@@ -1064,6 +1113,413 @@
1064
1113
  }
1065
1114
  }
1066
1115
 
1116
+ /**
1117
+ * A row of latching piano keys in a dark well, after the key row on an old cassette deck. Its
1118
+ * children alternate: an fp-silk label printed over the key, then the fp-key. Give it
1119
+ * `role="radiogroup"` round radio keys, or `role="group"` round checkbox keys, and a name; one
1120
+ * checkbox key needs no role. With `data-orientation="vertical"` the keys lie on their side,
1121
+ * stacked, each label before its key.
1122
+ */
1123
+ @utility fp-keys {
1124
+ display: inline-grid;
1125
+ grid-auto-flow: column;
1126
+
1127
+ /* Every key in a row is one width: the widest label's, or the key's own. */
1128
+ grid-auto-columns: 1fr;
1129
+ grid-template-rows: auto auto;
1130
+ row-gap: 0.5rem;
1131
+ justify-items: center;
1132
+ align-items: end;
1133
+
1134
+ /* A label, with its LED over it when the light is above the key. */
1135
+ & > .fp-silk {
1136
+ display: inline-flex;
1137
+ flex-direction: column;
1138
+ align-items: center;
1139
+ gap: 0.375rem;
1140
+ padding-inline: 0.25rem;
1141
+ white-space: nowrap;
1142
+ }
1143
+
1144
+ &[data-orientation="vertical"] {
1145
+ grid-auto-flow: row;
1146
+ grid-auto-columns: auto;
1147
+ grid-auto-rows: 1fr;
1148
+ grid-template-columns: auto auto;
1149
+ grid-template-rows: none;
1150
+ column-gap: 0.75rem;
1151
+ row-gap: 0;
1152
+ justify-items: stretch;
1153
+ align-items: center;
1154
+
1155
+ & > .fp-silk {
1156
+ flex-direction: row;
1157
+ justify-content: flex-end;
1158
+ padding-inline: 0;
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * A piano key in an fp-keys row: knurled plastic, silver or dark by skin, with a strip at its foot,
1165
+ * standing in its stretch of the well. Put it on a button with `role="radio"` or `role="checkbox"` and
1166
+ * `aria-checked`; checked, it latches part way down, smaller and darker under the well's lip, and
1167
+ * a press sinks it deeper. The strip is an LED in the key's `fp-led-*` colour (green), lit while
1168
+ * latched; with `data-strip="plain"` it is cream plastic, for keys whose LED sits over the label,
1169
+ * or that have none.
1170
+ */
1171
+ @utility fp-key {
1172
+ /* How far the key is shaded, latched or held down. */
1173
+ --fp-key-shade: 0;
1174
+
1175
+ /* The well round the key: a narrow seam between keys, wider at the ends of the row. */
1176
+ --fp-key-start: 2px;
1177
+ --fp-key-end: 2px;
1178
+
1179
+ /* The well's edge, drawn on the outside of the row only, so neighbours show no seam. */
1180
+ --fp-key-edge-start: 0 0 transparent;
1181
+ --fp-key-edge-end: 0 0 transparent;
1182
+ position: relative;
1183
+ justify-self: stretch;
1184
+ min-width: calc(3.5rem + 4px);
1185
+ min-height: calc(4.75rem + 14px);
1186
+ padding: 0;
1187
+ border: 0;
1188
+ border-radius: 0;
1189
+ background-color: var(--fp-color-key-well);
1190
+ box-shadow:
1191
+ inset 0 3px 5px rgb(0 0 0 / 0.8),
1192
+ 0 -1px 0 var(--fp-color-edge),
1193
+ 0 1px 0 var(--fp-color-edge),
1194
+ 0 2px 0 var(--fp-color-bevel-hi),
1195
+ var(--fp-key-edge-start),
1196
+ var(--fp-key-edge-end);
1197
+
1198
+ @media (pointer: fine) {
1199
+ min-width: calc(3rem + 4px);
1200
+ min-height: calc(4.125rem + 14px);
1201
+ }
1202
+
1203
+ &:nth-child(1 of .fp-key) {
1204
+ --fp-key-start: 4px;
1205
+ --fp-key-edge-start: -1px 0 0 var(--fp-color-edge);
1206
+ border-start-start-radius: 6px;
1207
+ border-end-start-radius: 6px;
1208
+ }
1209
+
1210
+ &:nth-last-child(1 of .fp-key) {
1211
+ --fp-key-end: 4px;
1212
+ --fp-key-edge-end: 1px 0 0 var(--fp-color-edge);
1213
+ border-start-end-radius: 6px;
1214
+ border-end-end-radius: 6px;
1215
+ }
1216
+
1217
+ /*
1218
+ * The key itself: a glossy frame lit from the top left, round a knurled face, a fine diamond grid
1219
+ * light on one diagonal and shaded on the other. It stands 4px below the well's top, with room
1220
+ * under it to travel down.
1221
+ */
1222
+ &::before {
1223
+ content: "";
1224
+ position: absolute;
1225
+ inset-block: 4px 10px;
1226
+ inset-inline: var(--fp-key-start) var(--fp-key-end);
1227
+ border-radius: 5px;
1228
+ background:
1229
+ linear-gradient(var(--fp-color-key-knurl-lo), transparent 2px) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1230
+ repeating-linear-gradient(45deg, var(--fp-color-key-knurl-hi) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1231
+ repeating-linear-gradient(-45deg, var(--fp-color-key-knurl-lo) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1232
+ linear-gradient(var(--fp-color-key-hi), color-mix(in srgb, var(--fp-color-key-hi) 50%, var(--fp-color-key))) 5px 5px / calc(100% - 10px) calc(100% - 20px) no-repeat,
1233
+ linear-gradient(160deg, var(--fp-color-key-hi) 0%, var(--fp-color-key) 35%, var(--fp-color-key-lo) 100%);
1234
+ box-shadow:
1235
+ inset 1px 1px 0 rgb(255 255 255 / 0.28),
1236
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1237
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1238
+ 0 3px 0 rgb(0 0 0 / 0.9);
1239
+ transform-origin: 50% 100%;
1240
+ transition-property: translate, scale, filter, box-shadow;
1241
+ transition-duration: 70ms;
1242
+ transition-timing-function: linear;
1243
+ }
1244
+
1245
+ /*
1246
+ * The strip at the key's foot: an LED lens, dark in its colour until the key latches, set in a
1247
+ * rim of the well's colour, so a lit strip stands out on a pale key too.
1248
+ */
1249
+ &::after {
1250
+ content: "";
1251
+ position: absolute;
1252
+ inset-block-end: 15px;
1253
+ inset-inline: calc(var(--fp-key-start) + 5px) calc(var(--fp-key-end) + 5px);
1254
+ height: 7px;
1255
+ border-radius: 2px;
1256
+ background: color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 32%, black);
1257
+ box-shadow:
1258
+ inset 0 1px 1px rgb(0 0 0 / 0.6),
1259
+ 0 0 0 1.5px var(--fp-color-key-well);
1260
+
1261
+ /* It moves with the key: scaled from the key's foot, 5px below its own. */
1262
+ transform-origin: 50% calc(100% + 5px);
1263
+ transition-property: translate, scale, filter;
1264
+ transition-duration: 70ms;
1265
+ transition-timing-function: linear;
1266
+ }
1267
+
1268
+ @media (prefers-reduced-motion: reduce) {
1269
+ &::before,
1270
+ &::after {
1271
+ transition-duration: 0ms;
1272
+ }
1273
+ }
1274
+
1275
+ /* A plain strip: cream plastic, shaded with the key. */
1276
+ &[data-strip="plain"]::after {
1277
+ height: 8px;
1278
+ border-radius: 1px;
1279
+ background: linear-gradient(
1280
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, white),
1281
+ var(--fp-color-key-strip) 60%,
1282
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, black)
1283
+ );
1284
+ box-shadow: 0 0 0 1.5px var(--fp-color-key-well);
1285
+ filter: brightness(calc(1 - var(--fp-key-shade)));
1286
+ }
1287
+
1288
+ &:hover:not(:disabled)::before {
1289
+ filter: brightness(1.1);
1290
+ }
1291
+
1292
+ /* Latched: well down, a little smaller and darker, the well's lip shading its top edge. */
1293
+ &[aria-checked="true"] {
1294
+ --fp-key-shade: 0.24;
1295
+
1296
+ &::before {
1297
+ translate: 0 6px;
1298
+ scale: 0.96;
1299
+ box-shadow:
1300
+ inset 0 6px 5px -2px rgb(0 0 0 / 0.9),
1301
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1302
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1303
+ 0 0 0 transparent;
1304
+ }
1305
+
1306
+ &::after {
1307
+ translate: 0 6px;
1308
+ scale: 0.96;
1309
+ }
1310
+ }
1311
+
1312
+ /* A latched LED strip lights, brightest in the middle, and spills its glow up over the face. */
1313
+ &[aria-checked="true"]:not([data-strip="plain"])::after {
1314
+ background: radial-gradient(
1315
+ ellipse 60% 120% at 50% 50%,
1316
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, white),
1317
+ var(--fp-led, var(--fp-color-led-on))
1318
+ );
1319
+ box-shadow:
1320
+ 0 0 0 1.5px var(--fp-color-key-well),
1321
+ 0 0 6px 1px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 70%, transparent),
1322
+ 0 -5px 12px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 35%, transparent);
1323
+ }
1324
+
1325
+ /* Held under a finger: past the latch, deeper and darker still. */
1326
+ &:active:not(:disabled) {
1327
+ --fp-key-shade: 0.36;
1328
+
1329
+ &::before {
1330
+ translate: 0 9px;
1331
+ scale: 0.94;
1332
+ box-shadow:
1333
+ inset 0 7px 5px -2px rgb(0 0 0 / 0.95),
1334
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1335
+ 0 0 0 transparent;
1336
+ }
1337
+
1338
+ &::after {
1339
+ translate: 0 9px;
1340
+ scale: 0.94;
1341
+ }
1342
+ }
1343
+
1344
+ /*
1345
+ * The ring goes round the key, in the dark well, where the skin's own ring could vanish: its
1346
+ * own colour, with the usual glow outside it. Over its neighbours, not under them.
1347
+ */
1348
+ &:focus-visible {
1349
+ z-index: 1;
1350
+ outline: none;
1351
+ }
1352
+
1353
+ &:focus-visible::before {
1354
+ outline: 2px solid var(--fp-color-key-ring);
1355
+ outline-offset: 2px;
1356
+ filter: drop-shadow(0 0 3px var(--fp-color-focus));
1357
+ }
1358
+
1359
+ /* The hover filter outranks the focus rule, so a hovered key keeps its glow here. */
1360
+ &:focus-visible:hover:not(:disabled)::before {
1361
+ filter: brightness(1.1) drop-shadow(0 0 3px var(--fp-color-focus));
1362
+ }
1363
+
1364
+ /* Disabled, the key stays up and fades; its stretch of the well stays dark. */
1365
+ &:disabled::before,
1366
+ &:disabled::after {
1367
+ opacity: 0.45;
1368
+ }
1369
+
1370
+ /*
1371
+ * On its side, in a vertical fp-keys: the well runs top to bottom, the strip is at the key's
1372
+ * right end, and a latched key slides right, toward it, the well's left lip shading its near edge.
1373
+ */
1374
+ [data-orientation="vertical"] > & {
1375
+ --fp-key-edge-start: 0 0 transparent;
1376
+ --fp-key-edge-end: 0 0 transparent;
1377
+ min-width: calc(4.75rem + 14px);
1378
+ min-height: calc(3.5rem + 4px);
1379
+ border-radius: 0;
1380
+ box-shadow:
1381
+ inset 3px 0 5px rgb(0 0 0 / 0.8),
1382
+ -1px 0 0 var(--fp-color-edge),
1383
+ 1px 0 0 var(--fp-color-edge),
1384
+ 1px 1px 0 var(--fp-color-bevel-hi),
1385
+ var(--fp-key-edge-start),
1386
+ var(--fp-key-edge-end);
1387
+
1388
+ @media (pointer: fine) {
1389
+ min-width: calc(4.125rem + 14px);
1390
+ min-height: calc(3rem + 4px);
1391
+ }
1392
+
1393
+ &:nth-child(1 of .fp-key) {
1394
+ --fp-key-edge-start: 0 -1px 0 var(--fp-color-edge);
1395
+ border-radius: 6px 6px 0 0;
1396
+ }
1397
+
1398
+ &:nth-last-child(1 of .fp-key) {
1399
+ --fp-key-edge-end: 0 1px 0 var(--fp-color-edge), 0 2px 0 var(--fp-color-bevel-hi);
1400
+ border-radius: 0 0 6px 6px;
1401
+ }
1402
+
1403
+ &:nth-child(1 of .fp-key):nth-last-child(1 of .fp-key) {
1404
+ border-radius: 6px;
1405
+ }
1406
+
1407
+ &::before {
1408
+ inset: var(--fp-key-start) 10px var(--fp-key-end) 4px;
1409
+ background:
1410
+ linear-gradient(90deg, rgb(0 0 0 / 0.5), transparent 2px) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1411
+ repeating-linear-gradient(45deg, rgb(255 255 255 / 0.11) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1412
+ repeating-linear-gradient(-45deg, rgb(0 0 0 / 0.45) 0 1px, transparent 1px 3.5px) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1413
+ linear-gradient(var(--fp-color-key-hi), color-mix(in srgb, var(--fp-color-key-hi) 50%, var(--fp-color-key))) 5px 5px / calc(100% - 20px) calc(100% - 10px) no-repeat,
1414
+ linear-gradient(160deg, var(--fp-color-key-hi) 0%, var(--fp-color-key) 35%, var(--fp-color-key-lo) 100%);
1415
+ box-shadow:
1416
+ inset 1px 1px 0 rgb(255 255 255 / 0.28),
1417
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1418
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1419
+ 3px 0 0 rgb(0 0 0 / 0.9);
1420
+ transform-origin: 100% 50%;
1421
+ }
1422
+
1423
+ &::after {
1424
+ inset: calc(var(--fp-key-start) + 5px) 15px calc(var(--fp-key-end) + 5px) auto;
1425
+ width: 7px;
1426
+ height: auto;
1427
+ transform-origin: calc(100% + 5px) 50%;
1428
+ }
1429
+
1430
+ &[data-strip="plain"]::after {
1431
+ width: 8px;
1432
+ background: linear-gradient(
1433
+ 90deg,
1434
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, white),
1435
+ var(--fp-color-key-strip) 60%,
1436
+ color-mix(in srgb, var(--fp-color-key-strip) 80%, black)
1437
+ );
1438
+ }
1439
+
1440
+ &[aria-checked="true"] {
1441
+ &::before {
1442
+ translate: 6px 0;
1443
+ box-shadow:
1444
+ inset 6px 0 5px -2px rgb(0 0 0 / 0.9),
1445
+ inset -1px -1px 0 rgb(0 0 0 / 0.6),
1446
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1447
+ 0 0 0 transparent;
1448
+ }
1449
+
1450
+ &::after {
1451
+ translate: 6px 0;
1452
+ }
1453
+ }
1454
+
1455
+ &[aria-checked="true"]:not([data-strip="plain"])::after {
1456
+ background: radial-gradient(
1457
+ ellipse 120% 60% at 50% 50%,
1458
+ color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 65%, white),
1459
+ var(--fp-led, var(--fp-color-led-on))
1460
+ );
1461
+ box-shadow:
1462
+ 0 0 0 1.5px var(--fp-color-key-well),
1463
+ 0 0 6px 1px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 70%, transparent),
1464
+ -5px 0 12px color-mix(in srgb, var(--fp-led, var(--fp-color-led-on)) 35%, transparent);
1465
+ }
1466
+
1467
+ &:active:not(:disabled) {
1468
+ &::before {
1469
+ translate: 9px 0;
1470
+ box-shadow:
1471
+ inset 7px 0 5px -2px rgb(0 0 0 / 0.95),
1472
+ inset 0 0 0 99px rgb(0 0 0 / var(--fp-key-shade)),
1473
+ 0 0 0 transparent;
1474
+ }
1475
+
1476
+ &::after {
1477
+ translate: 9px 0;
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ /*
1483
+ * Forced colours drop the plastic, the well and the light: each key and its strip keep an outline,
1484
+ * and a latched key opts out to fill in the system's selected colour, its strip in the selected
1485
+ * text colour, so it reads apart from the rest by fill as well as by its travel. The selectors
1486
+ * match the rules they override, the vertical ones too.
1487
+ */
1488
+ @media (forced-colors: active) {
1489
+ &::before,
1490
+ &::after {
1491
+ outline: 1px solid ButtonText;
1492
+ }
1493
+
1494
+ &[aria-checked="true"]::before,
1495
+ [data-orientation="vertical"] > &[aria-checked="true"]::before {
1496
+ forced-color-adjust: none;
1497
+ background: SelectedItem;
1498
+ box-shadow: none;
1499
+ }
1500
+
1501
+ &[aria-checked="true"]::after,
1502
+ &[aria-checked="true"]:not([data-strip="plain"])::after,
1503
+ [data-orientation="vertical"] > &[aria-checked="true"]::after,
1504
+ [data-orientation="vertical"] > &[aria-checked="true"]:not([data-strip="plain"])::after {
1505
+ forced-color-adjust: none;
1506
+ background: SelectedItemText;
1507
+ box-shadow: none;
1508
+ filter: none;
1509
+ }
1510
+
1511
+ &:focus-visible::before {
1512
+ outline: 2px solid CanvasText;
1513
+ }
1514
+
1515
+ &:disabled::before,
1516
+ &:disabled::after {
1517
+ opacity: 1;
1518
+ outline-color: GrayText;
1519
+ }
1520
+ }
1521
+ }
1522
+
1067
1523
  /**
1068
1524
  * Level meter: a strip of LED segments in a dark housing, lit green, then amber, then red. Set
1069
1525
  * `--fp-meter-level` (0 to 1, in whole segments), `--fp-meter-segments` (12), and where amber and
@@ -1428,6 +1884,130 @@
1428
1884
  padding: 0.75rem;
1429
1885
  }
1430
1886
 
1887
+ /**
1888
+ * Faceplate menu: a small brushed-chrome plate that drops from the button that opens it, holding
1889
+ * fp-menu-item rows and fp-menu-separator grooves. Placing it is up to you, or to Reka's
1890
+ * DropdownMenu, which also sets the `data-state="open"` it drops in on.
1891
+ */
1892
+ @utility fp-menu {
1893
+ color-scheme: var(--fp-scheme);
1894
+ z-index: 50;
1895
+ box-sizing: border-box;
1896
+ min-width: 10rem;
1897
+ overflow-y: auto;
1898
+ padding: 0.25rem;
1899
+ border-radius: 6px;
1900
+ color: var(--fp-color-ink);
1901
+ background-image: linear-gradient(
1902
+ 180deg,
1903
+ color-mix(in srgb, var(--fp-color-chrome-hi) 60%, var(--fp-color-chrome-100)) 0%,
1904
+ var(--fp-color-chrome-100) 40%,
1905
+ var(--fp-color-chrome-300) 100%
1906
+ );
1907
+ box-shadow:
1908
+ 0 0 0 1px var(--fp-color-edge),
1909
+ inset 1px 1px 0 var(--fp-color-bevel-hi),
1910
+ inset -1px -1px 0 var(--fp-color-bevel-lo),
1911
+ 2px 3px 0 var(--fp-color-cast),
1912
+ 0 12px 18px -8px var(--fp-color-cast);
1913
+
1914
+ /* The plate takes focus when a pointer opens it; its rows show where the keys are. */
1915
+ outline: none;
1916
+
1917
+ &[data-state="open"] {
1918
+ animation: fp-menu-drop 90ms ease-out;
1919
+ }
1920
+
1921
+ @media (prefers-reduced-motion: reduce) {
1922
+ &[data-state="open"] {
1923
+ animation: none;
1924
+ }
1925
+ }
1926
+
1927
+ /* Forced colours drop the chrome and its shadows: an outline keeps the plate's edge. */
1928
+ @media (forced-colors: active) {
1929
+ outline: 1px solid CanvasText;
1930
+ }
1931
+ }
1932
+
1933
+ /**
1934
+ * A row on an fp-menu: a silkscreened label, with an fp-icon before it if you like. The highlighted
1935
+ * row presses into the plate; Reka sets `data-highlighted` on the row under the pointer or the
1936
+ * arrow keys, and `data-disabled` on one that can't be chosen. 44 px tall, 36 px with a mouse,
1937
+ * trackpad or pen.
1938
+ */
1939
+ @utility fp-menu-item {
1940
+ display: flex;
1941
+ align-items: center;
1942
+ gap: 0.5rem;
1943
+ min-height: 2.75rem;
1944
+ padding-inline: 0.625rem;
1945
+ border-radius: 4px;
1946
+ color: var(--fp-color-ink);
1947
+ font-family: var(--fp-font-display);
1948
+ font-size: 11px;
1949
+ line-height: 1.3;
1950
+ letter-spacing: 0.08em;
1951
+ text-transform: uppercase;
1952
+ text-decoration: none;
1953
+ white-space: nowrap;
1954
+ cursor: default;
1955
+ user-select: none;
1956
+ outline: none;
1957
+
1958
+ @media (pointer: fine) {
1959
+ min-height: 2.25rem;
1960
+ }
1961
+
1962
+ &[data-highlighted] {
1963
+ background-color: var(--fp-color-chrome-300);
1964
+ box-shadow:
1965
+ inset 1px 1px 3px var(--fp-color-bevel-lo),
1966
+ inset -1px -1px 0 var(--fp-color-bevel-hi);
1967
+ }
1968
+
1969
+ /* The keyboard's row also gets the ring every focused control has, inside the row. */
1970
+ &:focus-visible {
1971
+ outline: 2px solid var(--fp-color-focus-ring);
1972
+ outline-offset: -2px;
1973
+ }
1974
+
1975
+ &[data-disabled] {
1976
+ opacity: 0.45;
1977
+ }
1978
+
1979
+ /*
1980
+ * Forced colours drop the pressed-in shading: the system's highlight colours show the highlighted
1981
+ * row, and GrayText, not a fade, shows a disabled one.
1982
+ */
1983
+ @media (forced-colors: active) {
1984
+ &[data-highlighted] {
1985
+ forced-color-adjust: none;
1986
+ background: Highlight;
1987
+ box-shadow: none;
1988
+ color: HighlightText;
1989
+
1990
+ /* Opted out, its focus ring would keep the skin's colour: the system's text colour shows on any ground. */
1991
+ &:focus-visible {
1992
+ outline-color: HighlightText;
1993
+ }
1994
+ }
1995
+
1996
+ &[data-disabled] {
1997
+ opacity: 1;
1998
+ color: GrayText;
1999
+ }
2000
+ }
2001
+ }
2002
+
2003
+ /** A groove between groups of rows on an fp-menu, cut into the plate: shaded above, lit below. */
2004
+ @utility fp-menu-separator {
2005
+ height: 0;
2006
+ margin: 0.25rem 0.375rem;
2007
+ border-top: 1px solid var(--fp-color-bevel-lo);
2008
+ border-bottom: 1px solid var(--fp-color-bevel-hi);
2009
+ }
2010
+
1431
2011
  /** Recessed well inside a matte panel, for secondary content or an empty slot. */
1432
2012
  @utility fp-well {
1433
2013
  color-scheme: var(--fp-scheme);
package/tokens.css CHANGED
@@ -32,6 +32,14 @@
32
32
  --fp-color-led-amber: #ffb000;
33
33
  --fp-color-led-red: #ff4a3d;
34
34
  --fp-color-led-off: #1f252e;
35
+ --fp-color-key-hi: #e8ecf2;
36
+ --fp-color-key: #b9c2cf;
37
+ --fp-color-key-lo: #7d8898;
38
+ --fp-color-key-well: #3a4250;
39
+ --fp-color-key-knurl-hi: rgb(255 255 255 / 0.3);
40
+ --fp-color-key-knurl-lo: rgb(0 0 0 / 0.2);
41
+ --fp-color-key-strip: #ece5d3;
42
+ --fp-color-key-ring: #8fbaff;
35
43
  --fp-color-cap: #f2f4f7;
36
44
  --fp-color-cap-ink: #2a303b;
37
45
  --fp-color-cap-blue: #2f6bc4;
@@ -89,6 +97,13 @@
89
97
  --fp-color-cap: #d5dae1;
90
98
  --fp-color-cap-graphite: #5a6373;
91
99
  --fp-color-focus-ring: #4fd2ff;
100
+ --fp-color-key-hi: #5a5e65;
101
+ --fp-color-key: #34373c;
102
+ --fp-color-key-lo: #222428;
103
+ --fp-color-key-well: #0b0c0e;
104
+ --fp-color-key-knurl-hi: rgb(255 255 255 / 0.11);
105
+ --fp-color-key-knurl-lo: rgb(0 0 0 / 0.45);
106
+ --fp-color-key-ring: #4fd2ff;
92
107
  --fp-color-select: #e7eaf0;
93
108
  --fp-color-error-text: #ff8a80;
94
109
  --fp-color-backdrop: #0d1016;
@@ -127,6 +142,13 @@
127
142
  --fp-color-cap: #d5dae1;
128
143
  --fp-color-cap-graphite: #5a6373;
129
144
  --fp-color-focus-ring: #4fd2ff;
145
+ --fp-color-key-hi: #5a5e65;
146
+ --fp-color-key: #34373c;
147
+ --fp-color-key-lo: #222428;
148
+ --fp-color-key-well: #0b0c0e;
149
+ --fp-color-key-knurl-hi: rgb(255 255 255 / 0.11);
150
+ --fp-color-key-knurl-lo: rgb(0 0 0 / 0.45);
151
+ --fp-color-key-ring: #4fd2ff;
130
152
  --fp-color-select: #e7eaf0;
131
153
  --fp-color-error-text: #ff8a80;
132
154
  --fp-color-backdrop: #0d1016;