moontide-mcp 2.1.0 → 2.2.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.
@@ -353,6 +353,21 @@ export function playingSection(reading, cycle) {
353
353
  return { index: last, cycleIn: reading.sections[last].length, progress: 1 };
354
354
  }
355
355
 
356
+ /**
357
+ * The names of the sections that play in the whole cycles `from` to `to` (both included), in playing order: a section that
358
+ * plays twice in a row is listed once, and one that comes back after another section is listed again. A range across the end
359
+ * of the song goes on from its start (the rule of playingSection). A range is at most MAX_RANGE_CYCLES cycles long, so
360
+ * walking it cycle by cycle is cheap.
361
+ */
362
+ export function sectionsCrossed(reading, from, to) {
363
+ const names = [];
364
+ for (let cycle = from; cycle <= to; cycle++) {
365
+ const name = String(reading.sections[playingSection(reading, cycle).index].name);
366
+ if (names[names.length - 1] !== name) names.push(name);
367
+ }
368
+ return names;
369
+ }
370
+
356
371
  /**
357
372
  * The harmony the Jam view's Declared line shows while `section` (an entry of a read song) plays, in the shape of a
358
373
  * reading's `declared`: the section's own text or the piece's, `{ state: 'none' }` when neither is declared, and for a
@@ -16,7 +16,7 @@ const MAX_INDENT_LEVELS = 4;
16
16
  const LIST_NOTE_ID = 'song-cell-list-note';
17
17
 
18
18
  /** True for the two keyboard gestures that mean a right-click: the Menu key, and F10 with Shift (and no other modifier). */
19
- function isAskKey(event) {
19
+ export function isAskKey(event) {
20
20
  if (event.altKey || event.metaKey || event.ctrlKey) return false;
21
21
  if (event.key === 'ContextMenu') return !event.shiftKey;
22
22
  return event.key === 'F10' && event.shiftKey === true;
package/page/structure.js CHANGED
@@ -120,7 +120,8 @@ export const SONG_COPY = {
120
120
  differs: 'What plays differs from the declaration at the start of section {section}, track {track}.',
121
121
  differsMore: 'What plays differs from the declaration at the start of section {section}, track {track} (and {count} more places).',
122
122
  differsMoreOne: 'What plays differs from the declaration at the start of section {section}, track {track} (and 1 more place).',
123
- checkFailed: 'The page could not check what plays against the declaration.',
123
+ checkFailed: 'The page could not check what plays against the declaration: the check broke off.',
124
+ checkTooSlow: 'The page could not check what plays against the declaration: the check took too long.',
124
125
  // Under a fixed variation: whether its notes are in the scale of the section's harmony (words only, nothing is changed).
125
126
  fitFits: 'fits',
126
127
  fitNot: 'does not fit ({notes})',
package/page/style.css CHANGED
@@ -456,34 +456,6 @@ body {
456
456
  font-size: 0.8125rem;
457
457
  }
458
458
 
459
- /* The status of the latest request to Claude. No display rule: #top-bar > span styles it and the browser's [hidden]
460
- works unaided, so nothing to say takes no width. It sits before the Session button in the markup, so it is right of
461
- the Layout group. It holds the long sentence and a short form: the short one is for fit step 3 and is aria-hidden. */
462
- #ask-status {
463
- flex: none;
464
- }
465
-
466
- #ask-status .ask-short {
467
- display: none;
468
- }
469
-
470
- /* Fit step 3 and 4: the short form shows, and the long sentence is visually hidden but kept in the accessibility tree
471
- (never display: none), the recipe of #change-status. */
472
- #top-bar[data-fit="3"] #ask-status .ask-short,
473
- #top-bar[data-fit="4"] #ask-status .ask-short {
474
- display: inline;
475
- }
476
-
477
- #top-bar[data-fit="3"] #ask-status .ask-long,
478
- #top-bar[data-fit="4"] #ask-status .ask-long {
479
- position: absolute;
480
- width: 1px;
481
- height: 1px;
482
- overflow: hidden;
483
- clip-path: inset(50%);
484
- white-space: nowrap;
485
- }
486
-
487
459
  /* The cycle and beat beside the tempo, quiet decoration (Label size, mono, muted). The hidden attribute
488
460
  hides it while nothing plays, so it has no display rule. Fit step 1 drops it with the wordmark (.bar-deco). */
489
461
  #top-bar #cycle-beat {
@@ -494,9 +466,7 @@ body {
494
466
  }
495
467
 
496
468
  /* Whether page requests reach Claude live. No display rule: #top-bar > span
497
- styles it and the browser's [hidden] works unaided. After #ask-status in the markup, same order,
498
- so it sits right of the request status and left of the motion switch, and the request status
499
- appearing never moves it. */
469
+ styles it and the browser's [hidden] works unaided. */
500
470
  #page-requests {
501
471
  flex-shrink: 0;
502
472
  }
@@ -1015,6 +985,300 @@ body {
1015
985
  font-weight: 400;
1016
986
  }
1017
987
 
988
+ /* The Requests chip, before the Session button: the same box as every control of the bar (1.75rem high, 0.75rem at the
989
+ sides), so it lines up with its neighbour. It is hidden while no request was sent, and the count in its pill is the
990
+ requests still in flight. Neutral colors only: the accent means live now and red belongs to the failed-change banner.
991
+ No transition and no animation: everything changes in one frame. */
992
+ #requests-toggle {
993
+ flex: none;
994
+ display: inline-flex;
995
+ align-items: center;
996
+ gap: 0.25rem;
997
+ box-sizing: border-box;
998
+ min-width: 1.75rem;
999
+ min-height: 1.75rem;
1000
+ padding: 0 0.75rem;
1001
+ background: transparent;
1002
+ border: 1px solid transparent;
1003
+ border-radius: 0.5rem;
1004
+ color: var(--text-muted);
1005
+ font-family: inherit;
1006
+ font-size: 0.9375rem;
1007
+ font-weight: 400;
1008
+ line-height: 1.5;
1009
+ white-space: nowrap;
1010
+ cursor: pointer;
1011
+ }
1012
+
1013
+ #requests-toggle[hidden] {
1014
+ display: none;
1015
+ }
1016
+
1017
+ #requests-toggle[data-inflight="true"] {
1018
+ color: var(--text);
1019
+ }
1020
+
1021
+ /* The word is the text color on hover, never muted on the fill. */
1022
+ #requests-toggle:hover {
1023
+ background: var(--surface-2);
1024
+ color: var(--text);
1025
+ }
1026
+
1027
+ #requests-toggle[aria-expanded="true"] {
1028
+ color: var(--text);
1029
+ border-color: var(--border);
1030
+ }
1031
+
1032
+ #requests-toggle:focus-visible {
1033
+ outline: 1px solid var(--text-muted);
1034
+ outline-offset: 1px;
1035
+ }
1036
+
1037
+ .rq-count {
1038
+ background: var(--surface-2);
1039
+ color: var(--text);
1040
+ border-radius: 1rem;
1041
+ padding: 0 0.5rem;
1042
+ font-family: var(--font-mono);
1043
+ font-size: 0.8125rem;
1044
+ font-weight: 400;
1045
+ }
1046
+
1047
+ #requests-toggle[data-inflight="true"] .rq-count {
1048
+ font-weight: 600;
1049
+ }
1050
+
1051
+ .rq-count[hidden] {
1052
+ display: none;
1053
+ }
1054
+
1055
+ #top-bar[data-fit="4"] #requests-toggle {
1056
+ padding: 0 0.5rem;
1057
+ }
1058
+
1059
+ /* The panel under the bar's right end: an overlay that pushes nothing, under the Ask box and the popovers (1000) and
1060
+ the return banner (900), level with the Session drawer (800), which it never shares the screen with. One row per
1061
+ request, newest first; it scrolls inside itself. */
1062
+ #requests-panel {
1063
+ position: absolute;
1064
+ top: 100%;
1065
+ right: 1rem;
1066
+ margin-top: 0.5rem;
1067
+ z-index: 800;
1068
+ box-sizing: border-box;
1069
+ width: 26rem;
1070
+ max-width: calc(100vw - 2rem);
1071
+ max-height: 20rem;
1072
+ overflow-y: auto;
1073
+ padding: 0.5rem;
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ gap: 0.25rem;
1077
+ background: var(--secondary);
1078
+ border: 1px solid var(--text-muted);
1079
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.45);
1080
+ font-size: 0.8125rem;
1081
+ line-height: 1.5;
1082
+ }
1083
+
1084
+ #requests-panel[hidden] {
1085
+ display: none;
1086
+ }
1087
+
1088
+ #requests-panel:focus {
1089
+ outline: none;
1090
+ }
1091
+
1092
+ #requests-panel:focus-visible {
1093
+ outline: 1px solid var(--text-muted);
1094
+ outline-offset: -1px;
1095
+ }
1096
+
1097
+ .rq-list,
1098
+ .rq-recent-body {
1099
+ margin: 0;
1100
+ padding: 0;
1101
+ list-style: none;
1102
+ display: flex;
1103
+ flex-direction: column;
1104
+ gap: 0.25rem;
1105
+ }
1106
+
1107
+ .rq-recent-body[hidden] {
1108
+ display: none;
1109
+ }
1110
+
1111
+ .rq-recent[hidden] {
1112
+ display: none;
1113
+ }
1114
+
1115
+ /* A row is two lines: the start of the request and its time, then a shape and the words of the state. The hover and
1116
+ the open state change the border, never the fill, so muted text is never placed on the lighter surface. */
1117
+ .rq-row {
1118
+ display: grid;
1119
+ grid-template-columns: 0.5rem 1fr auto;
1120
+ column-gap: 0.5rem;
1121
+ align-items: center;
1122
+ width: 100%;
1123
+ min-width: 1.75rem;
1124
+ min-height: 3rem;
1125
+ box-sizing: border-box;
1126
+ padding: 0.5rem 1rem;
1127
+ background: transparent;
1128
+ border: 1px solid transparent;
1129
+ color: var(--text);
1130
+ font-family: inherit;
1131
+ font-size: 0.8125rem;
1132
+ font-weight: 400;
1133
+ line-height: 1.5;
1134
+ text-align: left;
1135
+ cursor: pointer;
1136
+ }
1137
+
1138
+ .rq-row:hover {
1139
+ border-color: var(--border);
1140
+ }
1141
+
1142
+ .rq-row[aria-expanded="true"] {
1143
+ border-color: var(--border);
1144
+ }
1145
+
1146
+ .rq-row:focus-visible,
1147
+ .rq-recent-head:focus-visible {
1148
+ outline: 1px solid var(--text-muted);
1149
+ outline-offset: -1px;
1150
+ }
1151
+
1152
+ .rq-text {
1153
+ grid-column: 1 / 3;
1154
+ min-width: 0;
1155
+ overflow: hidden;
1156
+ text-overflow: ellipsis;
1157
+ white-space: nowrap;
1158
+ }
1159
+
1160
+ .rq-time {
1161
+ grid-column: 3;
1162
+ font-family: var(--font-mono);
1163
+ color: var(--text-muted);
1164
+ white-space: nowrap;
1165
+ }
1166
+
1167
+ /* The shape backs the words up and nothing more: a ring while it goes on, a disc when it is done, a dashed ring when
1168
+ it ended without a change, a square when it did not work. Text colors only. */
1169
+ .rq-mark {
1170
+ grid-column: 1;
1171
+ grid-row: 2;
1172
+ width: 0.5rem;
1173
+ height: 0.5rem;
1174
+ box-sizing: border-box;
1175
+ border-radius: 50%;
1176
+ border: 1px solid var(--text-muted);
1177
+ }
1178
+
1179
+ .rq-mark[data-shape="done"] {
1180
+ background: var(--text);
1181
+ border-color: var(--text);
1182
+ }
1183
+
1184
+ .rq-mark[data-shape="ended"] {
1185
+ border-style: dashed;
1186
+ }
1187
+
1188
+ .rq-mark[data-shape="bad"] {
1189
+ background: var(--text);
1190
+ border-color: var(--text);
1191
+ border-radius: 0;
1192
+ }
1193
+
1194
+ .rq-state {
1195
+ grid-column: 2 / 4;
1196
+ grid-row: 2;
1197
+ min-width: 0;
1198
+ overflow: hidden;
1199
+ text-overflow: ellipsis;
1200
+ white-space: nowrap;
1201
+ }
1202
+
1203
+ .rq-item[data-finished="true"] .rq-text {
1204
+ color: var(--text-muted);
1205
+ }
1206
+
1207
+ .rq-detail {
1208
+ box-sizing: border-box;
1209
+ margin: 0.25rem 0 0;
1210
+ padding: 0.5rem;
1211
+ max-height: 8rem;
1212
+ overflow-y: auto;
1213
+ background: var(--dominant);
1214
+ border: 1px solid var(--border);
1215
+ overflow-wrap: anywhere;
1216
+ }
1217
+
1218
+ .rq-detail[hidden] {
1219
+ display: none;
1220
+ }
1221
+
1222
+ .rq-detail p {
1223
+ margin: 0 0 0.5rem;
1224
+ }
1225
+
1226
+ .rq-detail p:last-child {
1227
+ margin-bottom: 0;
1228
+ }
1229
+
1230
+ .rq-detail .rq-about,
1231
+ .rq-detail .rq-basis {
1232
+ color: var(--text-muted);
1233
+ }
1234
+
1235
+ .rq-detail .rq-reason {
1236
+ font-family: var(--font-mono);
1237
+ white-space: pre-wrap;
1238
+ }
1239
+
1240
+ .rq-recent-head {
1241
+ display: flex;
1242
+ align-items: center;
1243
+ gap: 0.5rem;
1244
+ width: 100%;
1245
+ min-width: 2rem;
1246
+ min-height: 2rem;
1247
+ box-sizing: border-box;
1248
+ padding: 0 1rem;
1249
+ background: transparent;
1250
+ border: 1px solid transparent;
1251
+ color: var(--text-muted);
1252
+ font-family: inherit;
1253
+ font-size: 0.8125rem;
1254
+ font-weight: 400;
1255
+ line-height: 1.5;
1256
+ text-align: left;
1257
+ cursor: pointer;
1258
+ }
1259
+
1260
+ .rq-recent-head:hover {
1261
+ color: var(--text);
1262
+ border-color: var(--border);
1263
+ }
1264
+
1265
+ .rq-recent .rq-item .rq-state {
1266
+ color: var(--text-muted);
1267
+ }
1268
+
1269
+ /* The spoken line of the Requests list: visually hidden, kept in the accessibility tree, always present and never
1270
+ [hidden] (the recipe of #change-status). */
1271
+ #requests-live {
1272
+ position: absolute;
1273
+ width: 1px;
1274
+ height: 1px;
1275
+ overflow: hidden;
1276
+ clip-path: inset(50%);
1277
+ white-space: nowrap;
1278
+ font-size: 0.8125rem;
1279
+ line-height: 1.5;
1280
+ }
1281
+
1018
1282
  /* The strip: one muted line under the bar, only while the drawer is closed and a condition holds. */
1019
1283
  #session-strip {
1020
1284
  display: flex;
@@ -4533,6 +4797,100 @@ button.mx-name:focus-visible {
4533
4797
  text-overflow: ellipsis;
4534
4798
  }
4535
4799
 
4800
+ /* The line that says why a scale is in use when several fit: a bold lead word and one plain sentence under the scale
4801
+ name. It is no warning, so it wears the text colour only, and it is hidden for a scale that was simply heard. */
4802
+ #jam-reason {
4803
+ margin: 0;
4804
+ font-size: 1rem;
4805
+ font-weight: 400;
4806
+ line-height: 1.5;
4807
+ color: var(--text);
4808
+ overflow-wrap: anywhere;
4809
+ }
4810
+
4811
+ #jam-reason[hidden] {
4812
+ display: none;
4813
+ }
4814
+
4815
+ #jam-reason-lead {
4816
+ font-weight: 600;
4817
+ }
4818
+
4819
+ /* The scales that fit, under the reason line: a quiet caption, then one item per scale. While one can be chosen each item is a
4820
+ button and the one in use is pressed (a lighter fill and an underline, no weight change, so nothing shifts); when none can
4821
+ be chosen the items are plain text with no hover and no pointer. Never cut: an item keeps its whole name and the row wraps
4822
+ between items. Text colours only, no Jam hue: a list of scales is no warning. */
4823
+ #jam-candidates {
4824
+ display: flex;
4825
+ flex-direction: column;
4826
+ gap: 0.25rem;
4827
+ }
4828
+
4829
+ #jam-candidates[hidden] {
4830
+ display: none;
4831
+ }
4832
+
4833
+ #jam-candidates-label,
4834
+ #jam-candidates-more {
4835
+ font-size: 0.8125rem;
4836
+ font-weight: 400;
4837
+ line-height: 1.5;
4838
+ color: var(--text-muted);
4839
+ }
4840
+
4841
+ #jam-candidates-more[hidden] {
4842
+ display: none;
4843
+ }
4844
+
4845
+ #jam-candidate-list {
4846
+ display: flex;
4847
+ flex-direction: row;
4848
+ flex-wrap: wrap;
4849
+ gap: 0.5rem;
4850
+ }
4851
+
4852
+ .jam-candidate {
4853
+ box-sizing: border-box;
4854
+ min-height: 1.75rem;
4855
+ padding: 0 0.5rem;
4856
+ display: inline-flex;
4857
+ align-items: center;
4858
+ border: 1px solid var(--border);
4859
+ border-radius: 0;
4860
+ background: transparent;
4861
+ color: var(--text-muted);
4862
+ font: inherit;
4863
+ font-size: 0.8125rem;
4864
+ font-weight: 400;
4865
+ line-height: 1.5;
4866
+ white-space: nowrap;
4867
+ }
4868
+
4869
+ button.jam-candidate {
4870
+ cursor: pointer;
4871
+ }
4872
+
4873
+ button.jam-candidate:hover {
4874
+ color: var(--text);
4875
+ border-color: var(--text-muted);
4876
+ }
4877
+
4878
+ button.jam-candidate[aria-pressed="true"] {
4879
+ color: var(--text);
4880
+ background: var(--surface-2);
4881
+ border-color: var(--text-muted);
4882
+ box-shadow: inset 0 -0.125rem 0 var(--text);
4883
+ }
4884
+
4885
+ button.jam-candidate:focus-visible {
4886
+ outline: 0.125rem solid var(--text);
4887
+ outline-offset: 0.25rem;
4888
+ }
4889
+
4890
+ span.jam-candidate {
4891
+ color: var(--text);
4892
+ }
4893
+
4536
4894
  /* The declared harmony: a quiet line above the scale block and the label before the scale name.
4537
4895
  Both are hidden without a declaration. Two plain facts: no accent, no mark of agreement or difference. */
4538
4896
  #jam-declared {