@odori/cli 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{chunk-RXLB2CXH.js → chunk-RHG23EWW.js} +457 -241
  2. package/dist/cli.js +1 -1
  3. package/dist/index.d.ts +45 -5
  4. package/dist/index.js +3 -3
  5. package/dist/registry-snapshot-JEVXYGS2.js +4868 -0
  6. package/package.json +3 -3
  7. package/src/assets.ts +90 -0
  8. package/src/brand-file.ts +16 -4
  9. package/src/cli.ts +38 -13
  10. package/src/commands/add.ts +37 -1
  11. package/src/commands/dev.ts +32 -2
  12. package/src/commands/doctor.ts +47 -2
  13. package/src/commands/exportVideo.ts +6 -0
  14. package/src/commands/{still.ts → frame.ts} +23 -9
  15. package/src/commands/update.ts +58 -7
  16. package/src/discovery.ts +63 -2
  17. package/src/index.ts +1 -1
  18. package/src/jobs.ts +1 -1
  19. package/src/registry-snapshot.json +1529 -327
  20. package/src/registry-source.ts +37 -2
  21. package/src/render.ts +8 -1
  22. package/src/server.ts +7 -1
  23. package/studio/src/Studio.tsx +6 -22
  24. package/studio/src/components/ExportPanel.tsx +90 -6
  25. package/studio/src/components/Inspector.tsx +101 -1
  26. package/studio/src/components/Navigator.tsx +149 -0
  27. package/studio/src/components/Settings.tsx +109 -0
  28. package/studio/src/components/Transport.tsx +98 -55
  29. package/studio/src/components/ui.tsx +16 -1
  30. package/studio/src/lib/highlight.ts +85 -0
  31. package/studio/src/settings.ts +87 -0
  32. package/studio/src/studio.css +350 -8
  33. package/studio/src/views/BrandsView.tsx +18 -1
  34. package/studio/src/views/ComponentsView.tsx +191 -26
  35. package/studio/src/views/HomeView.tsx +7 -4
  36. package/studio/src/views/VideosView.tsx +33 -6
  37. package/studio/src/virtual.d.ts +4 -1
  38. package/dist/registry-snapshot-BDP6PVYB.js +0 -3559
@@ -153,6 +153,18 @@ body.render-mode {
153
153
  background-clip: content-box;
154
154
  }
155
155
 
156
+ /* Anything you click says so, once, rather than per control. */
157
+ button:not(:disabled),
158
+ [role="button"]:not([aria-disabled="true"]),
159
+ summary {
160
+ cursor: pointer;
161
+ }
162
+
163
+ button:disabled,
164
+ [role="button"][aria-disabled="true"] {
165
+ cursor: not-allowed;
166
+ }
167
+
156
168
  /* ---------- primitives ---------- */
157
169
 
158
170
  .button {
@@ -294,6 +306,66 @@ a.button {
294
306
  font-size: 11px;
295
307
  }
296
308
 
309
+ /* ---------- settings ---------- */
310
+
311
+ .settings {
312
+ position: relative;
313
+ }
314
+ /* Right-aligned: the trigger is the last thing in the header, so a menu
315
+ growing rightward from its left edge would leave the window. */
316
+ .settings-menu {
317
+ left: auto;
318
+ right: 0;
319
+ }
320
+ .menu-heading {
321
+ color: var(--subtle);
322
+ font-size: 11px;
323
+ letter-spacing: 0.04em;
324
+ margin: 6px 0 2px;
325
+ padding: 0 9px;
326
+ text-transform: uppercase;
327
+ }
328
+ .menu-heading:first-child {
329
+ margin-top: 2px;
330
+ }
331
+ .menu-hint {
332
+ color: var(--subtle);
333
+ font-size: 11px;
334
+ }
335
+ /* A tick in a reserved column, so choosing an option never reflows the row
336
+ or nudges the one under the cursor. */
337
+ .menu-item {
338
+ display: grid;
339
+ grid-template-columns: 1fr 14px;
340
+ }
341
+ .menu-item > span:first-child {
342
+ grid-column: 1;
343
+ }
344
+ .menu-hint {
345
+ grid-column: 1;
346
+ }
347
+ .menu-item[data-selected="true"]::after {
348
+ align-self: center;
349
+ color: var(--foreground);
350
+ content: "✓";
351
+ font-size: 11px;
352
+ grid-column: 2;
353
+ grid-row: 1 / span 2;
354
+ justify-self: end;
355
+ }
356
+ /* A stalled bar stops looking like a working one. */
357
+ .progress[data-stalled="true"] > span {
358
+ opacity: 0.5;
359
+ }
360
+
361
+ .menu-note {
362
+ border-top: 1px solid var(--border);
363
+ color: var(--subtle);
364
+ font-size: 11px;
365
+ margin: 4px 0 0;
366
+ padding: 7px 9px 3px;
367
+ }
368
+
297
369
  /* Matches the transport's icon buttons rather than the host select. */
298
370
  .rate {
299
371
  appearance: none;
@@ -485,19 +557,93 @@ select.input {
485
557
 
486
558
  .main {
487
559
  display: grid;
488
- /* A detail is the stage and the inspector; galleries and pages are one
489
- column. The inspector's column is a root variable so its drag handle can
490
- resize it without React re-laying anything out. */
491
- grid-template-columns: minmax(0, 1fr) clamp(240px, var(--inspector-width, 320px), 50vw);
560
+ /* A detail is a list, a stage, and an inspector; galleries and pages are one
561
+ column. The side columns size to their contents, so a panel that puts
562
+ itself away takes its column with it and the stage grows into the gap
563
+ without a rule per combination of what is open. The inspector's width is
564
+ a root variable so its drag handle can resize it without React re-laying
565
+ anything out. */
566
+ grid-template-columns: auto minmax(0, 1fr) auto;
492
567
  overflow: hidden;
493
568
  }
494
569
  .main[data-single="true"] {
495
570
  grid-template-columns: 1fr;
496
571
  }
497
572
 
573
+ /* ---------- navigator ---------- */
574
+
575
+ .navigator {
576
+ background: var(--panel);
577
+ border-right: 1px solid var(--border);
578
+ display: flex;
579
+ flex-direction: column;
580
+ overflow: hidden;
581
+ width: 232px;
582
+ }
583
+ .navigator-head {
584
+ align-items: center;
585
+ border-bottom: 1px solid var(--border);
586
+ display: flex;
587
+ gap: 6px;
588
+ padding: 10px;
589
+ }
590
+ .navigator-search {
591
+ height: 28px;
592
+ font-size: 12px;
593
+ min-width: 0;
594
+ }
595
+ .navigator-scroll {
596
+ overflow-y: auto;
597
+ padding: 10px;
598
+ scrollbar-gutter: stable;
599
+ }
600
+ .navigator-group {
601
+ color: var(--subtle);
602
+ font-size: 11px;
603
+ font-weight: 500;
604
+ letter-spacing: 0.04em;
605
+ margin: 10px 0 4px 2px;
606
+ text-transform: uppercase;
607
+ }
608
+ .navigator-scroll > div:first-child .navigator-group {
609
+ margin-top: 0;
610
+ }
611
+ .navigator-empty {
612
+ padding: 4px 2px;
613
+ }
614
+ .navigator-toggle,
615
+ .navigator-reopen {
616
+ align-items: center;
617
+ background: transparent;
618
+ border: 1px solid transparent;
619
+ border-radius: var(--radius-sm);
620
+ color: var(--muted);
621
+ cursor: pointer;
622
+ display: inline-flex;
623
+ flex: none;
624
+ height: 28px;
625
+ justify-content: center;
626
+ width: 28px;
627
+ }
628
+ .navigator-toggle:hover,
629
+ .navigator-reopen:hover {
630
+ background: var(--hover);
631
+ color: var(--foreground);
632
+ }
633
+ /* Away, the way back floats where the list was. */
634
+ .navigator-reopen {
635
+ background: var(--panel);
636
+ border-color: var(--border);
637
+ left: 10px;
638
+ position: fixed;
639
+ top: 58px;
640
+ z-index: 5;
641
+ }
642
+
498
643
  .inspector {
499
644
  background: var(--panel);
500
645
  border-left: 1px solid var(--border);
646
+ width: clamp(240px, var(--inspector-width, 320px), 50vw);
501
647
  container-type: inline-size;
502
648
  display: flex;
503
649
  overflow: hidden;
@@ -507,6 +653,11 @@ select.input {
507
653
  flex: 1;
508
654
  overflow-y: auto;
509
655
  padding: 16px;
656
+ /* The gutter is reserved whether or not it is used. Code is taller than the
657
+ facts it replaces, so switching to it summoned a scrollbar, and the ten
658
+ pixels that took came out of the content: the header's controls jumped
659
+ sideways every time you changed face. */
660
+ scrollbar-gutter: stable;
510
661
  }
511
662
 
512
663
 
@@ -526,6 +677,127 @@ select.input {
526
677
  text-overflow: ellipsis;
527
678
  white-space: nowrap;
528
679
  }
680
+ /* Two views of one selection, not two modes: the segmented control sits in
681
+ the pane's own header rather than above the whole app. */
682
+ .inspector-views {
683
+ background: var(--hover);
684
+ border-radius: var(--radius-sm);
685
+ display: flex;
686
+ flex: none;
687
+ gap: 2px;
688
+ margin-left: auto;
689
+ padding: 2px;
690
+ }
691
+ .inspector-views button {
692
+ background: transparent;
693
+ border: 0;
694
+ border-radius: calc(var(--radius-sm) - 1px);
695
+ color: var(--muted);
696
+ cursor: pointer;
697
+ font: inherit;
698
+ font-size: 11px;
699
+ padding: 3px 9px;
700
+ }
701
+ .inspector-views button:hover {
702
+ color: var(--foreground);
703
+ }
704
+ /* A lift off the track it sits in, and nothing else. The weight and the
705
+ border this used to add both changed the button's size, so selecting one
706
+ nudged the other sideways: emphasis has to come from colour alone, or the
707
+ control moves while you read it. */
708
+ .inspector-views button[data-active="true"] {
709
+ background: color-mix(in srgb, var(--foreground) 10%, var(--hover));
710
+ color: var(--foreground);
711
+ }
712
+
713
+ /* Which of a component's files is being read. */
714
+ .source-files {
715
+ display: flex;
716
+ flex-wrap: wrap;
717
+ gap: 6px;
718
+ margin-bottom: 10px;
719
+ }
720
+ .source-files button {
721
+ background: transparent;
722
+ border: 1px solid var(--border);
723
+ border-radius: 999px;
724
+ color: var(--muted);
725
+ font-family: var(--font-mono);
726
+ font-size: 11px;
727
+ padding: 3px 10px;
728
+ }
729
+ .source-files button:hover {
730
+ color: var(--foreground);
731
+ }
732
+ .source-files button[data-active="true"] {
733
+ background: var(--hover);
734
+ border-color: var(--border-strong);
735
+ color: var(--foreground);
736
+ }
737
+
738
+ /*
739
+ * Token colours, taken from the same GitHub themes the docs site highlights
740
+ * with, so a component's source reads the same in both places.
741
+ */
742
+ :root {
743
+ --token-comment: #6e7781;
744
+ --token-string: #0a3069;
745
+ --token-keyword: #cf222e;
746
+ --token-number: #0550ae;
747
+ --token-tag: #116329;
748
+ --token-attr: #953800;
749
+ --token-fn: #8250df;
750
+ }
751
+ :root[data-theme="dark"] {
752
+ --token-comment: #8b949e;
753
+ --token-string: #a5d6ff;
754
+ --token-keyword: #ff7b72;
755
+ --token-number: #79c0ff;
756
+ --token-tag: #7ee787;
757
+ --token-attr: #ffa657;
758
+ --token-fn: #d2a8ff;
759
+ }
760
+ @media (prefers-color-scheme: dark) {
761
+ :root:not([data-theme="light"]) {
762
+ --token-comment: #8b949e;
763
+ --token-string: #a5d6ff;
764
+ --token-keyword: #ff7b72;
765
+ --token-number: #79c0ff;
766
+ --token-tag: #7ee787;
767
+ --token-attr: #ffa657;
768
+ --token-fn: #d2a8ff;
769
+ }
770
+ }
771
+
772
+ .source [data-token="comment"] { color: var(--token-comment); font-style: italic; }
773
+ .source [data-token="string"] { color: var(--token-string); }
774
+ .source [data-token="keyword"] { color: var(--token-keyword); }
775
+ .source [data-token="number"] { color: var(--token-number); }
776
+ .source [data-token="tag"] { color: var(--token-tag); }
777
+ .source [data-token="attr"] { color: var(--token-attr); }
778
+ .source [data-token="fn"] { color: var(--token-fn); }
779
+ .source [data-token="punct"] { color: var(--muted); }
780
+
781
+ /*
782
+ * Code scrolls the full width of the pane. The inspector pads its content by
783
+ * sixteen, which is right for prose and wrong for a scrolling region: it left
784
+ * a gutter the code could not reach, so a long line appeared to stop short of
785
+ * an edge it had not reached. The negative margin gives the scroller the whole
786
+ * pane and the padding puts the text back where it was.
787
+ */
788
+ .source {
789
+ color: var(--foreground);
790
+ font-family: var(--font-mono);
791
+ font-size: 11px;
792
+ line-height: 1.65;
793
+ margin: 0 -16px;
794
+ overflow-x: auto;
795
+ overscroll-behavior-x: contain;
796
+ padding: 0 16px;
797
+ tab-size: 2;
798
+ white-space: pre;
799
+ }
800
+
529
801
  .inspector-toggle,
530
802
  .inspector-reopen {
531
803
  align-items: center;
@@ -556,9 +828,7 @@ select.input {
556
828
  }
557
829
 
558
830
  /* A collapsed inspector hands its column to the stage. */
559
- html[data-inspector="collapsed"] .main:not([data-single="true"]) {
560
- grid-template-columns: minmax(0, 1fr);
561
- }
831
+ /* Panels carry their own width now, so collapsing one needs no grid rule. */
562
832
  .inspector-resize {
563
833
  cursor: col-resize;
564
834
  /* Fully inside the pane: the inspector clips overflow, and a handle hung
@@ -630,6 +900,33 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
630
900
  }
631
901
  }
632
902
 
903
+ /* Filter the catalog by the video a component is actually used in. */
904
+ .catalog-filter {
905
+ display: flex;
906
+ flex-wrap: wrap;
907
+ gap: 6px;
908
+ padding: 16px 20px 0;
909
+ }
910
+ .catalog-filter button {
911
+ background: transparent;
912
+ border: 1px solid var(--border);
913
+ border-radius: 999px;
914
+ color: var(--muted);
915
+ cursor: pointer;
916
+ font: inherit;
917
+ font-size: 12px;
918
+ padding: 4px 12px;
919
+ }
920
+ .catalog-filter button:hover {
921
+ border-color: var(--border-strong);
922
+ color: var(--foreground);
923
+ }
924
+ .catalog-filter button[data-active="true"] {
925
+ background: var(--hover);
926
+ border-color: var(--border-strong);
927
+ color: var(--foreground);
928
+ }
929
+
633
930
  /* A gallery page or an explainer page: one scrolling column with a readable
634
931
  lead-in. */
635
932
  .page {
@@ -714,6 +1011,13 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
714
1011
 
715
1012
  .stage {
716
1013
  display: grid;
1014
+ /* The column has to be named. An implicit grid column is auto, which means
1015
+ max-content, so the transport's row of controls set the width and the
1016
+ timeline was laid out at its natural size and then clipped: in a pane this
1017
+ narrow the right half of every video sat under the inspector, unclickable,
1018
+ and the playhead walked off the edge on its way there. min-width on .stage
1019
+ does not help, because the overflow is the track inside it. */
1020
+ grid-template-columns: minmax(0, 1fr);
717
1021
  grid-template-rows: minmax(0, 1fr) auto;
718
1022
  min-width: 0;
719
1023
  overflow: hidden;
@@ -756,13 +1060,19 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
756
1060
  border-top: 1px solid var(--border);
757
1061
  display: grid;
758
1062
  gap: 10px;
1063
+ min-width: 0;
759
1064
  padding: 10px 14px 14px;
760
1065
  }
761
1066
 
762
1067
  .transport-row {
763
1068
  align-items: center;
764
1069
  display: flex;
1070
+ /* Wrapping is what lets the transport be narrower than its controls. Without
1071
+ it the row is a single min-content block and it is the row, not the
1072
+ timeline, that decides how wide the stage has to be. */
1073
+ flex-wrap: wrap;
765
1074
  gap: 8px;
1075
+ min-width: 0;
766
1076
  }
767
1077
 
768
1078
  .timecode {
@@ -777,8 +1087,13 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
777
1087
  }
778
1088
 
779
1089
  .timeline {
780
- cursor: pointer;
1090
+ /* Only the track is clickable, so only the track shows a pointer. */
781
1091
  display: grid;
1092
+ /* Named for the same reason .stage is: an implicit auto column would take
1093
+ its width from the ruler and the scene labels rather than from the space
1094
+ available, and push the track out from under the cursor. */
1095
+ grid-template-columns: minmax(0, 1fr);
1096
+ min-width: 0;
782
1097
  gap: 4px;
783
1098
  position: relative;
784
1099
  user-select: none;
@@ -792,6 +1107,8 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
792
1107
  justify-content: space-between;
793
1108
  }
794
1109
  .track {
1110
+ /* The track is a click target, not something to drag. */
1111
+ cursor: pointer;
795
1112
  background: var(--field);
796
1113
  border: 1px solid var(--border);
797
1114
  border-radius: var(--radius-sm);
@@ -903,6 +1220,21 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
903
1220
  width: 7.5px;
904
1221
  }
905
1222
 
1223
+ /* The frame under the cursor, which is a suggestion until it is clicked. It
1224
+ reads as the same instrument as the playhead, turned down. */
1225
+ .playhead[data-preview="true"],
1226
+ .playhead[data-preview="true"]::before {
1227
+ background: var(--subtle);
1228
+ }
1229
+ .playhead[data-preview="true"] {
1230
+ opacity: 0.75;
1231
+ }
1232
+ /* Muted rather than moved: the readout must not change width when it starts
1233
+ reporting the hovered frame, or the whole row shifts under the cursor. */
1234
+ .timecode[data-preview="true"] {
1235
+ color: var(--subtle);
1236
+ }
1237
+
906
1238
  /* ---------- inspector ---------- */
907
1239
 
908
1240
  .facts {
@@ -1263,6 +1595,16 @@ html[data-inspector="collapsed"] .main:not([data-single="true"]) {
1263
1595
  color: var(--foreground);
1264
1596
  }
1265
1597
 
1598
+ /* A drawer inside a family. Quieter than the family it sits under, and
1599
+ indented enough that the nesting reads without a rule to draw it. */
1600
+ .group-title {
1601
+ color: var(--muted);
1602
+ font-size: 12px;
1603
+ font-weight: 500;
1604
+ letter-spacing: -0.01em;
1605
+ margin: 2px 0 8px;
1606
+ }
1607
+
1266
1608
  /* ---------- home ---------- */
1267
1609
 
1268
1610
  .home {
@@ -13,6 +13,7 @@ import {CanvasStage} from "../components/CanvasStage";
13
13
  import {Transport} from "../components/Transport";
14
14
  import {Empty, Fact, SectionTitle, Separator} from "../components/ui";
15
15
  import {Inspector} from "../components/Inspector";
16
+ import {Navigator} from "../components/Navigator";
16
17
  import {useShortcuts} from "../shortcuts";
17
18
 
18
19
  /** Brands come from videos/**\/brands modules and from every resolved layout. */
@@ -101,9 +102,21 @@ export const BrandsView = ({
101
102
  }
102
103
 
103
104
  const layout = defineVideoLayout({extends: base, brand});
105
+ const brandFile = project.files.brands.find((item) => item.id === brand.name)?.file;
104
106
 
105
107
  return (
106
108
  <>
109
+ <Navigator
110
+ label="Brands"
111
+ selected={brand.name}
112
+ onSelect={onSelect}
113
+ items={brands.map((item) => ({
114
+ id: item.name,
115
+ title: item.name,
116
+ detail: `${Object.keys(item.audio.cues).length} cues · ${item.audio.targetLufs} LUFS`,
117
+ }))}
118
+ />
119
+
107
120
  <section className="stage">
108
121
  <CanvasStage width={layout.format.width} height={layout.format.height}>
109
122
  <OdoriRuntime
@@ -125,7 +138,11 @@ export const BrandsView = ({
125
138
  />
126
139
  </section>
127
140
 
128
- <Inspector title={brand.name} hint={project.files.brands.find((item) => item.id === brand.name)?.file}>
141
+ <Inspector
142
+ title={brand.name}
143
+ hint={brandFile}
144
+ source={brandFile}
145
+ >
129
146
  <SectionTitle>Preview with</SectionTitle>
130
147
  <ul className="list">
131
148
  {videos.map((video) => (