char-slop/chordpad.vibed.software

git clone https://git.t4t.associates/char-slop/chordpad.vibed.software

Charlotte Somallow midi import as chords040fec5

main
17.8 KiB963 linesraw
1:root {
2  color-scheme: dark;
3  font-family: "Inter Variable", Inter, system-ui, sans-serif;
4  font-size: 13px;
5
6  /* cool greys, against which the warm accents stand out; lines neutral */
7  --bg: oklch(19.9% 0.014 285.1);
8  --panel: oklch(23.1% 0.017 285);
9  --panel-2: oklch(27% 0.022 284.8);
10  --border: oklch(34.7% 0 0);
11  /* for the few lines that divide more than the rest */
12  --border-strong: oklch(46% 0 0);
13  /* around the track being edited */
14  --border-active: oklch(64% 0 0);
15  --text: oklch(95.4% 0.008 332.1);
16  --muted: oklch(63.9% 0.023 333.1);
17  --accent: oklch(82.4% 0.159 330.6);
18  --accent-mute: oklch(63.8% 0.13 331.4);
19  /* the accent turned halfway round the hue wheel, for melodies */
20  --accent-melody: oklch(82.4% 0.142 150.6);
21  /* the accent turned towards purple, and a touch more vivid: chord notes outside the key */
22  --accent-outside: oklch(82.4% 0.16 295.6);
23  /* the other keys on the key's axis: a minor third up, down, and a tritone away */
24  --accent-sharp: oklch(82.4% 0.127 40.6);
25  --accent-flat: oklch(82.4% 0.127 210.6);
26  --accent-inverse: oklch(82.4% 0.127 270.6);
27  /* the parallel modes: quieter than the axis, and running from the brightest mode (light and
28     warm) to the darkest (dim and cool), with the minor scales' variants off to the side in red */
29  --mode-lydian: oklch(84% 0.075 95);
30  --mode-ionian: oklch(80% 0.075 125);
31  --mode-mixolydian: oklch(76% 0.075 155);
32  --mode-dorian: oklch(72% 0.075 185);
33  --mode-aeolian: oklch(68% 0.075 215);
34  --mode-phrygian: oklch(64% 0.075 245);
35  --mode-locrian: oklch(60% 0.075 275);
36  --mode-harmonicMinor: oklch(70% 0.085 20);
37  --mode-melodicMinor: oklch(75% 0.085 45);
38
39  --row-off: oklch(20.8% 0.014 285.1);
40  --row-scale: oklch(24.4% 0.019 284.9);
41  --row-tonic: oklch(27.3% 0.036 301);
42  --line-row: oklch(0% 0 0 / 0.165);
43  --line-bar: oklch(100% 0 0 / 0.2);
44  --line-beat: oklch(100% 0 0 / 0.09);
45
46  --c: var(--muted);
47}
48
49/* in the key: nothing to point out */
50.on-home {
51  --c: var(--text);
52}
53/* from none of the key, its axis, or its parallel modes */
54.chromatic {
55  --c: var(--accent);
56}
57.on-sharp {
58  --c: var(--accent-sharp);
59}
60.on-flat {
61  --c: var(--accent-flat);
62}
63.on-inverse {
64  --c: var(--accent-inverse);
65}
66.from-lydian {
67  --c: var(--mode-lydian);
68}
69.from-ionian {
70  --c: var(--mode-ionian);
71}
72.from-mixolydian {
73  --c: var(--mode-mixolydian);
74}
75.from-dorian {
76  --c: var(--mode-dorian);
77}
78.from-aeolian {
79  --c: var(--mode-aeolian);
80}
81.from-phrygian {
82  --c: var(--mode-phrygian);
83}
84.from-locrian {
85  --c: var(--mode-locrian);
86}
87.from-harmonicMinor {
88  --c: var(--mode-harmonicMinor);
89}
90.from-melodicMinor {
91  --c: var(--mode-melodicMinor);
92}
93
94* {
95  box-sizing: border-box;
96}
97
98[hidden] {
99  display: none !important;
100}
101
102body {
103  margin: 0;
104  background: var(--bg);
105  color: var(--text);
106}
107
108button,
109input,
110select {
111  font: inherit;
112  color: inherit;
113  background: var(--panel-2);
114  border: 1px solid var(--border);
115  padding: 3px 8px;
116}
117button {
118  cursor: pointer;
119}
120button:hover:not(:disabled) {
121  border-color: var(--accent-mute);
122}
123button:disabled {
124  opacity: 0.4;
125  cursor: default;
126}
127input[type="checkbox"] {
128  padding: 0;
129  accent-color: var(--accent);
130}
131
132kbd,
133code {
134  font-family: inherit;
135  font-size: 11px;
136  background: var(--panel-2);
137  border: 1px solid var(--border);
138  padding: 0 4px;
139}
140
141.app {
142  display: grid;
143  grid-template-rows: auto 1fr auto auto auto;
144  height: 100vh;
145}
146
147/* the project on the left, the transport in the very middle, settings on the right */
148.toolbar {
149  display: grid;
150  grid-template-columns: 1fr auto 1fr;
151  gap: 8px 20px;
152  align-items: center;
153  padding: 8px 12px;
154  background: var(--panel);
155  border-bottom: 1px solid var(--border);
156
157  .tb-section {
158    display: flex;
159    flex-wrap: wrap;
160    gap: 8px 20px;
161    align-items: center;
162  }
163  .tb-right {
164    justify-content: end;
165  }
166
167  .tb-group {
168    display: flex;
169    gap: 6px;
170    align-items: center;
171  }
172  label {
173    display: flex;
174    gap: 6px;
175    align-items: center;
176    color: var(--muted);
177  }
178  .tb-projects {
179    max-width: 160px;
180  }
181  .tb-bpm {
182    width: 58px;
183  }
184  .tb-play {
185    min-width: 70px;
186    border-color: var(--accent);
187  }
188}
189
190/* no room for three columns: let it wrap */
191@media (max-width: 1080px) {
192  .toolbar {
193    display: flex;
194    flex-wrap: wrap;
195  }
196}
197
198.timeline {
199  overflow: auto;
200  user-select: none;
201  -webkit-user-select: none;
202  background: var(--bg);
203}
204
205.tl-content {
206  position: relative;
207  min-height: 100%;
208}
209.tl-rows {
210  display: grid;
211}
212
213.tl-corner,
214.ruler {
215  position: sticky;
216  top: 0;
217  height: 22px;
218  background-color: var(--panel);
219  border-bottom: 1px solid var(--border);
220  z-index: 3;
221}
222.tl-corner {
223  left: 0;
224  z-index: 4;
225  border-right: 1px solid var(--border);
226}
227.ruler {
228  cursor: text;
229
230  .loop-region {
231    position: absolute;
232    top: 0;
233    bottom: 0;
234    background: color-mix(in oklab, var(--accent) 22%, transparent);
235    border-inline: 2px solid var(--accent);
236    cursor: default;
237  }
238  .loop-region.off {
239    background: color-mix(in oklab, var(--muted) 12%, transparent);
240    border-color: var(--muted);
241  }
242  /* straddling the region's edges, to grab them by */
243  .loop-edge {
244    position: absolute;
245    top: 0;
246    bottom: 0;
247    width: 9px;
248    cursor: ew-resize;
249  }
250  .loop-edge.start {
251    left: -6px;
252  }
253  .loop-edge.end {
254    right: -6px;
255  }
256  span {
257    position: absolute;
258    top: 4px;
259    font-size: 11px;
260    color: var(--muted);
261    pointer-events: none;
262  }
263}
264
265.track-head {
266  position: sticky;
267  left: 0;
268  scroll-margin-top: 22px;
269  z-index: 2;
270  background: var(--panel);
271  border-right: 1px solid var(--border);
272  border-bottom: 1px solid var(--border);
273  padding: 6px 40px 6px 8px;
274  /* dragged by, to move the track */
275  cursor: grab;
276
277  &.active {
278    background: var(--panel-2);
279  }
280  &.active .th-name {
281    font-weight: 650;
282  }
283  .th-top {
284    display: flex;
285    gap: 4px;
286  }
287  .th-name {
288    flex: 1;
289    min-width: 0;
290    background: transparent;
291    border-color: transparent;
292    padding: 2px 4px;
293  }
294  .th-name:hover,
295  .th-name:focus {
296    border-color: var(--border);
297  }
298  .th-btn {
299    padding: 0 6px;
300    font-size: 11px;
301  }
302  .th-btn.on {
303    background: var(--accent);
304    color: var(--bg);
305  }
306  .th-bass {
307    display: flex;
308    align-items: center;
309    gap: 6px;
310    padding: 4px 4px 0;
311    color: var(--muted);
312    font-size: 11px;
313    input {
314      margin: 0;
315    }
316  }
317  .th-gain {
318    display: flex;
319    align-items: center;
320    gap: 6px;
321    padding: 4px 4px 0;
322    color: var(--muted);
323    font-size: 11px;
324    font-variant-numeric: tabular-nums;
325    input {
326      flex: 1;
327      min-width: 0;
328      margin: 0;
329      accent-color: var(--muted);
330    }
331    span {
332      width: 44px;
333      text-align: right;
334    }
335  }
336}
337
338.keys {
339  position: absolute;
340  right: 0;
341  bottom: 0;
342  width: 34px;
343  overflow: hidden;
344  cursor: ns-resize;
345
346  .keys-inner {
347    display: flex;
348    flex-direction: column;
349    will-change: translate;
350  }
351
352  .key {
353    flex: 0 0 10px;
354    background: oklch(88.5% 0.014 286.1);
355    border-bottom: 1px solid oklch(0% 0 0 / 0.2);
356    font-size: 8px;
357    line-height: 9px;
358    color: oklch(32.1% 0 0);
359    text-align: right;
360    padding-right: 2px;
361  }
362  .key.black {
363    background: oklch(28.9% 0.018 285.2);
364    margin-right: 12px;
365  }
366  .key.tonic {
367    box-shadow: inset 3px 0 var(--accent);
368  }
369}
370
371.lane {
372  position: relative;
373  border-bottom: 1px solid var(--border);
374}
375
376/* over the borders it shares with the tracks either side, rather than beside them */
377:is(.track-head, .lane).active::after {
378  content: "";
379  position: absolute;
380  inset: -1px 0;
381  z-index: 1;
382  border-block: 1px solid var(--border-active);
383  pointer-events: none;
384}
385
386.track-resize {
387  position: absolute;
388  left: 0;
389  right: 0;
390  bottom: 0;
391  height: 5px;
392  cursor: row-resize;
393
394  &:hover {
395    background: color-mix(in oklab, var(--accent) 50%, transparent);
396  }
397}
398/* keep the resize cursor while dragging over other things */
399body.resizing-track,
400body.resizing-track * {
401  cursor: row-resize !important;
402}
403body.resizing-loop,
404body.resizing-loop * {
405  cursor: ew-resize !important;
406}
407body.panning,
408body.panning *,
409body.reordering,
410body.reordering * {
411  cursor: grabbing !important;
412}
413.reorder-line {
414  position: fixed;
415  z-index: 10;
416  height: 2px;
417  background: var(--accent);
418  pointer-events: none;
419}
420
421.roll {
422  position: relative;
423  overflow: hidden;
424  cursor: crosshair;
425}
426.roll-inner {
427  position: relative;
428  /* moved through by transform, so its own layer: moving it then needn't paint it again */
429  will-change: translate;
430}
431
432.lane.notes {
433  --note: var(--accent-melody);
434}
435
436.note {
437  position: absolute;
438  overflow: hidden;
439  padding-left: 2px;
440  font-size: 9px;
441  font-weight: 600;
442  line-height: 9px;
443  white-space: nowrap;
444  color: oklch(0% 0 0 / 0.7);
445  background: var(--note, var(--accent));
446  box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.267);
447
448  &.selected {
449    background: oklch(100% 0 0);
450  }
451  &.outside {
452    --note: var(--accent-outside);
453  }
454  &.auto {
455    background: color-mix(in oklab, var(--note, var(--accent)) 45%, var(--bg));
456    color: oklch(100% 0 0 / 0.75);
457  }
458  /* written but not played: a bass left to another track */
459  &.silent {
460    background: none;
461    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--note, var(--accent)) 70%, transparent);
462    color: var(--muted);
463  }
464  &.ghost {
465    background: color-mix(in oklab, var(--accent-melody) 30%, transparent);
466    box-shadow: none;
467    pointer-events: none;
468  }
469}
470.roll > .note:not(.auto) {
471  cursor: grab;
472}
473
474.chord-col {
475  position: absolute;
476  top: 0;
477  bottom: 0;
478
479  /* chords take turns being shaded rather than getting lines: they always start on a grid line,
480     and a line of their own would only brighten it */
481  &.alt {
482    background: oklch(100% 0 0 / 0.02);
483  }
484  &.current {
485    background: oklch(100% 0 0 / 0.039);
486  }
487}
488
489.strip {
490  position: relative;
491  background: var(--panel);
492  border-bottom: 1px solid var(--border);
493  cursor: text;
494}
495
496.chord {
497  position: absolute;
498  top: 4px;
499  bottom: 4px;
500  padding: 3px 6px;
501  overflow: hidden;
502  white-space: nowrap;
503  cursor: grab;
504  background: color-mix(in oklab, var(--c) 16%, var(--panel-2));
505  border: 1px solid color-mix(in oklab, var(--c) 40%, transparent);
506  border-left: 2px solid var(--c);
507
508  &.current {
509    outline: 2px solid var(--text);
510    outline-offset: -1px;
511  }
512  &.selected {
513    background: color-mix(in oklab, var(--c) 34%, var(--panel-2));
514    border-color: var(--c);
515  }
516  &.unknown {
517    --c: oklch(51% 0 0);
518  }
519  &.pinned::after {
520    content: "";
521    position: absolute;
522    top: 4px;
523    right: 8px;
524    width: 5px;
525    height: 5px;
526    border-radius: 50%;
527    background: var(--text);
528  }
529
530  .chord-name {
531    font-weight: 600;
532    font-size: 14px;
533  }
534  .chord-roman {
535    font-size: 11px;
536    color: var(--muted);
537  }
538  .chord-tag {
539    margin-left: 6px;
540    color: var(--c);
541  }
542  .chord-handle {
543    position: absolute;
544    top: 0;
545    right: 0;
546    bottom: 0;
547    width: 6px;
548    cursor: ew-resize;
549  }
550}
551
552.tl-line {
553  position: absolute;
554  top: 0;
555  bottom: 0;
556  left: 0;
557  width: 2px;
558  margin-left: -1px;
559  pointer-events: none;
560  z-index: 1;
561}
562.tl-cursor {
563  background: color-mix(in oklab, var(--accent) 60%, transparent);
564}
565.tl-playhead {
566  background: var(--text);
567}
568
569.tl-add {
570  position: sticky;
571  left: 0;
572  display: flex;
573  gap: 6px;
574  padding: 8px;
575  z-index: 2;
576}
577
578.chord-editor {
579  position: absolute;
580  z-index: 5;
581  display: flex;
582  gap: 6px;
583  align-items: center;
584  padding: 6px 4px;
585  pointer-events: none;
586
587  input {
588    pointer-events: auto;
589    width: 110px;
590    font-weight: 600;
591    font-size: 14px;
592    border-color: var(--accent);
593    background: var(--bg);
594  }
595  input.invalid {
596    border-color: oklch(68.8% 0.201 24.1);
597    color: oklch(79% 0.121 20.1);
598  }
599  .chord-hint {
600    color: var(--muted);
601    background: var(--bg);
602    padding: 2px 6px;
603  }
604  .chord-hint:empty {
605    display: none;
606  }
607}
608
609.palette {
610  background: var(--panel);
611  border-top: 1px solid var(--border);
612  padding: 6px 12px 8px;
613
614  &[data-open="false"] {
615    padding-bottom: 6px;
616    header {
617      margin-bottom: 0;
618    }
619    .palette-body,
620    .help,
621    label {
622      display: none;
623    }
624  }
625  max-height: 34vh;
626  overflow: auto;
627
628  header {
629    display: flex;
630    gap: 16px;
631    align-items: center;
632    color: var(--muted);
633    margin-bottom: 6px;
634    label {
635      display: flex;
636      gap: 4px;
637      align-items: center;
638    }
639  }
640}
641
642.palette-body {
643  display: flex;
644  gap: 32px;
645  align-items: start;
646}
647
648.circle {
649  flex: none;
650  width: 260px;
651  height: 260px;
652
653  .circle-chord {
654    cursor: pointer;
655    path {
656      fill: transparent;
657      stroke: color-mix(in oklab, var(--border) 70%, transparent);
658    }
659    &:hover path {
660      fill: var(--panel-2);
661    }
662    &.here path {
663      fill: color-mix(in oklab, var(--c) 18%, var(--panel));
664      stroke: var(--c);
665    }
666    text {
667      text-anchor: middle;
668      pointer-events: none;
669    }
670    .name {
671      fill: var(--c);
672      font-size: 12px;
673      font-weight: 600;
674    }
675    .roman {
676      fill: var(--muted);
677      font-size: 8.5px;
678    }
679    /* less room on the inner ring */
680    &.minor .name {
681      font-size: 10.5px;
682    }
683  }
684}
685
686.palette-row {
687  display: grid;
688  grid-template-columns: 110px repeat(7, 104px);
689  border-top: 1px solid color-mix(in oklab, var(--border) 60%, transparent);
690
691  /* the key and its axis, then the modes */
692  &.on-inverse + .palette-row {
693    border-top-color: var(--border-strong);
694  }
695
696  .palette-mode {
697    display: grid;
698    align-content: center;
699    color: var(--c);
700    small {
701      color: var(--muted);
702      font-size: 10px;
703    }
704  }
705  &.on-home .palette-mode {
706    font-weight: 650;
707  }
708}
709
710.chip {
711  display: flex;
712  gap: 6px;
713  align-items: baseline;
714  padding: 4px 6px;
715  background: none;
716  border: none;
717
718  b {
719    color: var(--c);
720    font-weight: 600;
721  }
722  span {
723    color: var(--muted);
724    font-size: 11px;
725  }
726  &:hover {
727    background: var(--panel-2);
728  }
729  &.same {
730    opacity: 0.35;
731  }
732}
733
734.hints {
735  display: flex;
736  gap: 16px;
737  padding: 6px 12px;
738  color: var(--muted);
739  font-size: 11px;
740  border-top: 1px solid var(--border);
741  background: var(--panel);
742
743  .hint kbd + kbd {
744    margin-left: 2px;
745  }
746}
747
748.dialog {
749  background: var(--panel);
750  color: var(--text);
751  border: 1px solid var(--border);
752  padding: 16px;
753  width: min(560px, 90vw);
754
755  &::backdrop {
756    background: oklch(0% 0 0 / 0.5);
757  }
758  h2 {
759    margin: 0 0 12px;
760    font-size: 15px;
761  }
762}
763
764.dialog-actions {
765  display: flex;
766  justify-content: flex-end;
767  gap: 6px;
768  margin-top: 12px;
769}
770
771.tt-dialog {
772  .tt-options {
773    display: grid;
774    gap: 6px;
775    max-height: 60vh;
776    overflow: auto;
777  }
778  .tt-option {
779    display: grid;
780    grid-template-columns: 1fr auto;
781    gap: 2px 12px;
782    text-align: left;
783    padding: 8px 10px;
784  }
785  .tt-option:focus-visible {
786    outline: 2px solid var(--accent);
787    outline-offset: -2px;
788  }
789  .tt-detail {
790    color: var(--muted);
791  }
792  .tt-preview {
793    grid-column: 1 / -1;
794    color: var(--muted);
795    font-size: 11px;
796    white-space: nowrap;
797    overflow: hidden;
798    text-overflow: ellipsis;
799  }
800  .tt-preview:empty {
801    display: none;
802  }
803}
804
805.midi-dialog {
806  .midi-parts {
807    display: grid;
808    grid-template-columns: auto 1fr auto;
809    gap: 6px 12px;
810    align-items: baseline;
811    max-height: 60vh;
812    overflow: auto;
813  }
814  .midi-part {
815    display: contents;
816  }
817  .midi-detail {
818    color: var(--muted);
819  }
820}
821
822.disclosure {
823  background: none;
824  border: none;
825  padding: 0;
826  font-size: 11px;
827  white-space: nowrap;
828
829  /* a chevron, as on git.t4t.associates */
830  &::before {
831    content: "";
832    display: inline-block;
833    position: relative;
834    top: -0.17em;
835    width: 0.4em;
836    height: 0.4em;
837    margin-right: 0.6em;
838    border-right: 1.5px solid currentColor;
839    border-bottom: 1.5px solid currentColor;
840    transform: rotate(45deg);
841  }
842  &[aria-expanded="false"]::before {
843    top: -0.05em;
844    transform: rotate(-45deg);
845  }
846}
847
848.help {
849  display: inline-grid;
850  place-items: center;
851  width: 14px;
852  height: 14px;
853  border: 1px solid currentColor;
854  border-radius: 50%;
855  color: var(--muted);
856  font-size: 9px;
857  font-weight: 650;
858  cursor: help;
859
860  &:hover {
861    color: var(--accent);
862  }
863}
864.disclosure:hover:not(:disabled) {
865  color: var(--accent);
866}
867
868.chord-panel {
869  display: flex;
870  gap: 16px;
871  align-items: center;
872  padding: 6px 12px;
873  background: var(--panel);
874  border-top: 1px solid var(--border);
875
876  &[data-open="true"] .chord-panel-body {
877    min-height: 42px;
878  }
879  .chord-panel-body {
880    display: flex;
881    flex-wrap: wrap;
882    gap: 8px 20px;
883    /* the buttons in a line, under their labels */
884    align-items: end;
885  }
886
887  .chord-panel-empty {
888    align-self: center;
889    color: var(--muted);
890  }
891  .chord-panel-name {
892    display: grid;
893    min-width: 72px;
894    &:empty {
895      display: none;
896    }
897    b {
898      font-size: 16px;
899    }
900    span {
901      color: var(--muted);
902      font-size: 11px;
903    }
904  }
905  .chord-panel-group {
906    display: grid;
907    gap: 3px;
908  }
909  .chord-panel-label {
910    color: var(--muted);
911    font-size: 11px;
912  }
913  .chord-panel-buttons {
914    display: flex;
915    gap: 4px;
916  }
917  .chord-panel-action {
918    display: flex;
919    gap: 6px;
920    align-items: baseline;
921    padding: 3px 8px;
922    b {
923      font-weight: 650;
924    }
925  }
926}
927
928.select-box {
929  position: fixed;
930  z-index: 10;
931  pointer-events: none;
932  border: 1px solid var(--accent-melody);
933  background: color-mix(in oklab, var(--accent-melody) 15%, transparent);
934}
935
936.menu {
937  /* undo the centring popovers get by default, so it can sit under its button */
938  inset: auto;
939  margin: 0;
940  padding: 4px;
941  min-width: 170px;
942  color: var(--text);
943  background: var(--panel-2);
944  border: 1px solid var(--border);
945  box-shadow: 0 8px 24px oklch(0% 0 0 / 0.4);
946
947  button {
948    display: block;
949    width: 100%;
950    text-align: left;
951    background: none;
952    border: none;
953    padding: 5px 10px;
954  }
955  button:hover {
956    background: color-mix(in oklab, var(--accent) 18%, transparent);
957  }
958  hr {
959    border: none;
960    border-top: 1px solid var(--border);
961    margin: 4px 2px;
962  }
963}