@nysds/playground 0.1.0 → 0.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.
- package/README.md +20 -10
- package/dist/assets/{index-LO1tomgR.css → index-BuPgmhhR.css} +1 -1
- package/dist/assets/{index-bBtGCGL_.js → index-EthD3ZNJ.js} +370 -368
- package/dist/index.html +115 -141
- package/index.html +113 -139
- package/package.json +1 -1
- package/presets/16-themes.json +2 -2
- package/src/app.css +155 -111
- package/src/deck-model.ts +47 -0
- package/src/decks.test.ts +55 -0
- package/src/main.ts +91 -27
- package/src/present.ts +9 -0
- package/src/slide-list.ts +276 -0
package/presets/16-themes.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Agency themes",
|
|
3
|
-
"description": "The same button, alert, and card under different agency themes.
|
|
3
|
+
"description": "The same button, alert, and card under different agency themes. A nys-theme-* class on any element themes everything inside it.",
|
|
4
4
|
"html": "<nys-select id=\"theme-select\" label=\"Select an agency theme\" class=\"nys-margin-b-300\">\n <option value=\"default\" label=\"Default (NY.gov)\" selected></option>\n <option value=\"admin\" label=\"Administration\"></option>\n <option value=\"health\" label=\"Health\"></option>\n <option value=\"transportation\" label=\"Transportation\"></option>\n <option value=\"environment\" label=\"Environment\"></option>\n</nys-select>\n\n<div id=\"theme-wrapper\">\n <div class=\"nys-display-flex nys-flex-column nys-flex-gap-300\">\n <nys-button label=\"Primary action\"></nys-button>\n <nys-alert type=\"info\" heading=\"Themed alert\" text=\"This alert's color follows the selected theme.\"></nys-alert>\n <nys-card heading=\"Themed card\" description=\"Card accents follow the theme color too.\"></nys-card>\n </div>\n</div>\n",
|
|
5
5
|
"css": "",
|
|
6
|
-
"js": "const themeSelect = document.getElementById('theme-select');\nconst themeWrapper = document.getElementById('theme-wrapper');\n\nthemeSelect.addEventListener('nys-change', (e) => {\n themeWrapper.
|
|
6
|
+
"js": "const themeSelect = document.getElementById('theme-select');\nconst themeWrapper = document.getElementById('theme-wrapper');\n\nthemeSelect.addEventListener('nys-change', (e) => {\n // The wrapper carries one nys-theme-* class at a time. No class means the\n // default NY.gov theme.\n themeWrapper.className = e.detail.value === 'default' ? '' : `nys-theme-${e.detail.value}`;\n});\n"
|
|
7
7
|
}
|
package/src/app.css
CHANGED
|
@@ -141,19 +141,18 @@ body {
|
|
|
141
141
|
color: var(--pg-muted);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
/*
|
|
145
|
-
button on one line, so stack them
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
144
|
+
/* Below about 800px the wordmark and "Playground" no longer fit beside the
|
|
145
|
+
settings button on one line, so stack them, drop the divider, and let the
|
|
146
|
+
wordmark scale down with the window. Its natural width at 48px tall is
|
|
147
|
+
437px. */
|
|
148
|
+
@media (max-width: 800px) {
|
|
150
149
|
.home__title {
|
|
151
150
|
flex-direction: column;
|
|
152
151
|
align-items: flex-start;
|
|
153
152
|
gap: 4px;
|
|
154
153
|
}
|
|
155
154
|
.home__logo {
|
|
156
|
-
width: min(100%,
|
|
155
|
+
width: min(100%, 437px);
|
|
157
156
|
height: auto;
|
|
158
157
|
}
|
|
159
158
|
.home__title-text {
|
|
@@ -163,6 +162,12 @@ body {
|
|
|
163
162
|
}
|
|
164
163
|
}
|
|
165
164
|
|
|
165
|
+
@media (max-width: 640px) {
|
|
166
|
+
.home {
|
|
167
|
+
padding-inline: 16px;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
166
171
|
.home__lede {
|
|
167
172
|
margin: 16px 0 24px;
|
|
168
173
|
max-width: 60ch;
|
|
@@ -682,20 +687,6 @@ nys-toggle[hidden] {
|
|
|
682
687
|
min-height: 0;
|
|
683
688
|
}
|
|
684
689
|
|
|
685
|
-
.split__divider {
|
|
686
|
-
flex: 0 0 var(--pg-divider-width);
|
|
687
|
-
cursor: col-resize;
|
|
688
|
-
background: var(--pg-border);
|
|
689
|
-
background-clip: content-box;
|
|
690
|
-
border-inline: 4px solid var(--pg-surface);
|
|
691
|
-
touch-action: none;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
.split__divider:focus-visible {
|
|
695
|
-
outline: 2px solid var(--nys-color-focus, #004dd1);
|
|
696
|
-
outline-offset: 1px;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
690
|
/* Presentation mode ---------------------------------------------------- */
|
|
700
691
|
|
|
701
692
|
.app--present .toolbar,
|
|
@@ -714,9 +705,8 @@ nys-toggle[hidden] {
|
|
|
714
705
|
}
|
|
715
706
|
|
|
716
707
|
/*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* ellipses before anything can overlap it.
|
|
708
|
+
* Two columns: the title side shrinks into its ellipsis, and the controls
|
|
709
|
+
* take the room they need.
|
|
720
710
|
*/
|
|
721
711
|
.caption {
|
|
722
712
|
/* Sits above the editor drawer (z-index 5) so tooltips that pop upward
|
|
@@ -724,7 +714,7 @@ nys-toggle[hidden] {
|
|
|
724
714
|
position: relative;
|
|
725
715
|
z-index: 10;
|
|
726
716
|
display: grid;
|
|
727
|
-
grid-template-columns: minmax(0, 1fr) auto
|
|
717
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
728
718
|
align-items: center;
|
|
729
719
|
gap: 12px;
|
|
730
720
|
padding: 8px 16px;
|
|
@@ -751,45 +741,31 @@ nys-toggle[hidden] {
|
|
|
751
741
|
min-width: 0;
|
|
752
742
|
}
|
|
753
743
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
744
|
+
/*
|
|
745
|
+
* Spaced by visible edges, not by box. A ghost circle button shows only its
|
|
746
|
+
* 24px glyph inside a 40px box, so ghost boxes sit 4px apart and the
|
|
747
|
+
* outlined arrows carry a wider margin: every visible edge then sits 20px
|
|
748
|
+
* from the next, and hovered ghost circles still clear each other. Margins
|
|
749
|
+
* rather than `gap`, because the zero-width tooltips between the buttons
|
|
750
|
+
* would each take a gap of their own.
|
|
751
|
+
*/
|
|
759
752
|
.caption__right {
|
|
760
753
|
display: flex;
|
|
761
754
|
align-items: center;
|
|
762
755
|
justify-self: end;
|
|
763
|
-
gap: 8px;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
.caption__actions {
|
|
767
|
-
display: flex;
|
|
768
|
-
align-items: center;
|
|
769
|
-
gap: 4px;
|
|
770
756
|
}
|
|
771
757
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
.caption__home {
|
|
775
|
-
display: inline-flex;
|
|
776
|
-
flex: 0 0 auto;
|
|
777
|
-
align-items: center;
|
|
778
|
-
justify-content: center;
|
|
779
|
-
width: 40px;
|
|
780
|
-
height: 40px;
|
|
781
|
-
color: var(--nys-color-text-reverse);
|
|
782
|
-
border-radius: 50%;
|
|
758
|
+
.caption__right > nys-button {
|
|
759
|
+
margin-inline: 2px;
|
|
783
760
|
}
|
|
784
761
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
762
|
+
#prev-button,
|
|
763
|
+
#next-button {
|
|
764
|
+
margin-inline: 10px;
|
|
788
765
|
}
|
|
789
766
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
height: 100%;
|
|
767
|
+
#next-button {
|
|
768
|
+
margin-inline-end: 0;
|
|
793
769
|
}
|
|
794
770
|
|
|
795
771
|
.caption__notes[hidden] {
|
|
@@ -800,16 +776,25 @@ nys-toggle[hidden] {
|
|
|
800
776
|
min-width: 0;
|
|
801
777
|
}
|
|
802
778
|
|
|
779
|
+
/* A flex row, so the title or picker gives way and the counter keeps its
|
|
780
|
+
place at the end. */
|
|
803
781
|
.caption__title {
|
|
782
|
+
display: flex;
|
|
783
|
+
align-items: center;
|
|
784
|
+
min-width: 0;
|
|
804
785
|
margin: 0;
|
|
805
|
-
overflow: hidden;
|
|
806
786
|
font-size: 1.0625rem;
|
|
807
787
|
font-weight: 600;
|
|
808
788
|
line-height: 1.3;
|
|
809
|
-
text-overflow: ellipsis;
|
|
810
789
|
white-space: nowrap;
|
|
811
790
|
}
|
|
812
791
|
|
|
792
|
+
.caption__title > .present-only {
|
|
793
|
+
min-width: 0;
|
|
794
|
+
overflow: hidden;
|
|
795
|
+
text-overflow: ellipsis;
|
|
796
|
+
}
|
|
797
|
+
|
|
813
798
|
.caption__group {
|
|
814
799
|
color: var(--nys-color-text-reverse-weak);
|
|
815
800
|
font-weight: 400;
|
|
@@ -818,13 +803,20 @@ nys-toggle[hidden] {
|
|
|
818
803
|
/* The slide title doubles as the slide picker while editing. */
|
|
819
804
|
.caption__picker {
|
|
820
805
|
display: inline-flex;
|
|
806
|
+
flex: 0 1 auto;
|
|
821
807
|
align-items: center;
|
|
822
808
|
gap: 2px;
|
|
823
|
-
|
|
809
|
+
min-width: 0;
|
|
824
810
|
}
|
|
825
811
|
|
|
812
|
+
/* A select sizes to its longest option; `field-sizing` sizes it to the
|
|
813
|
+
chosen one instead where the browser supports it, so the chevron hugs
|
|
814
|
+
the title. Either way it can shrink and clip. */
|
|
826
815
|
.caption__select {
|
|
816
|
+
min-width: 0;
|
|
827
817
|
max-width: 100%;
|
|
818
|
+
field-sizing: content;
|
|
819
|
+
text-overflow: ellipsis;
|
|
828
820
|
padding: 2px 2px 2px 4px;
|
|
829
821
|
border: none;
|
|
830
822
|
border-radius: 4px;
|
|
@@ -875,8 +867,9 @@ nys-toggle[hidden] {
|
|
|
875
867
|
display: none;
|
|
876
868
|
}
|
|
877
869
|
|
|
870
|
+
/* The counter rides with the title, set off by a bullet. */
|
|
878
871
|
.caption__count {
|
|
879
|
-
margin: 0;
|
|
872
|
+
margin: 0 0 0 8px;
|
|
880
873
|
font-variant-numeric: tabular-nums;
|
|
881
874
|
font-size: 0.9375rem;
|
|
882
875
|
font-weight: 400;
|
|
@@ -884,64 +877,23 @@ nys-toggle[hidden] {
|
|
|
884
877
|
white-space: nowrap;
|
|
885
878
|
}
|
|
886
879
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
display: none;
|
|
890
|
-
margin-inline-start: 8px;
|
|
880
|
+
.caption__count:not(:empty)::before {
|
|
881
|
+
content: "\2022 ";
|
|
891
882
|
}
|
|
892
883
|
|
|
893
884
|
/*
|
|
894
|
-
* Narrow screens: two rows. The
|
|
895
|
-
*
|
|
896
|
-
* mark has nowhere to go, so it steps aside.
|
|
885
|
+
* Narrow screens: two rows. The title, counter, and slide settings take row
|
|
886
|
+
* one, and the action buttons spread across row two.
|
|
897
887
|
*/
|
|
898
888
|
@media (max-width: 720px) {
|
|
899
889
|
.caption {
|
|
900
|
-
grid-template-columns:
|
|
901
|
-
|
|
902
|
-
gap: 4px 8px;
|
|
890
|
+
grid-template-columns: minmax(0, 1fr);
|
|
891
|
+
gap: 6px;
|
|
903
892
|
}
|
|
904
893
|
|
|
905
|
-
.caption__left,
|
|
906
894
|
.caption__right {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
.caption__center {
|
|
911
|
-
display: none;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
#prev-button {
|
|
915
|
-
grid-column: 1;
|
|
916
|
-
grid-row: 1 / 3;
|
|
917
|
-
align-self: center;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
.caption__headline {
|
|
921
|
-
grid-column: 2;
|
|
922
|
-
grid-row: 1;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
.caption__actions {
|
|
926
|
-
grid-column: 2;
|
|
927
|
-
grid-row: 2;
|
|
928
|
-
flex-wrap: wrap;
|
|
929
|
-
justify-content: center;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
#next-button {
|
|
933
|
-
grid-column: 3;
|
|
934
|
-
grid-row: 1 / 3;
|
|
935
|
-
align-self: center;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/* The counter reads better beside the title than alone on the icon row. */
|
|
939
|
-
.caption__right > .caption__count {
|
|
940
|
-
display: none;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
.caption__count--inline {
|
|
944
|
-
display: inline;
|
|
895
|
+
justify-self: stretch;
|
|
896
|
+
justify-content: space-between;
|
|
945
897
|
}
|
|
946
898
|
}
|
|
947
899
|
|
|
@@ -1019,6 +971,99 @@ nys-toggle[hidden] {
|
|
|
1019
971
|
align-self: flex-start;
|
|
1020
972
|
}
|
|
1021
973
|
|
|
974
|
+
/* Read by screen readers, invisible to everyone else. */
|
|
975
|
+
.visually-hidden {
|
|
976
|
+
position: absolute;
|
|
977
|
+
width: 1px;
|
|
978
|
+
height: 1px;
|
|
979
|
+
margin: -1px;
|
|
980
|
+
padding: 0;
|
|
981
|
+
overflow: hidden;
|
|
982
|
+
clip: rect(0 0 0 0);
|
|
983
|
+
white-space: nowrap;
|
|
984
|
+
border: 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/* The slide list in Deck settings ------------------------------------ */
|
|
988
|
+
|
|
989
|
+
/* The modal body clips at its edges, and a focus ring reaches 4px past a
|
|
990
|
+
button, so the list keeps that much room on each side. */
|
|
991
|
+
.slide-list {
|
|
992
|
+
display: flex;
|
|
993
|
+
flex-direction: column;
|
|
994
|
+
gap: 4px;
|
|
995
|
+
margin: 0;
|
|
996
|
+
padding: 0 4px;
|
|
997
|
+
list-style: none;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.slide-row {
|
|
1001
|
+
position: relative;
|
|
1002
|
+
display: flex;
|
|
1003
|
+
flex-wrap: wrap;
|
|
1004
|
+
align-items: center;
|
|
1005
|
+
gap: 8px;
|
|
1006
|
+
padding: 4px 0;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.slide-row--dragging {
|
|
1010
|
+
opacity: 0.4;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* A line above or below the row the drag would land beside. */
|
|
1014
|
+
.slide-row--drop-before::before,
|
|
1015
|
+
.slide-row--drop-after::after {
|
|
1016
|
+
content: "";
|
|
1017
|
+
position: absolute;
|
|
1018
|
+
inset-inline: 0;
|
|
1019
|
+
height: 2px;
|
|
1020
|
+
background: var(--nys-color-theme, #154973);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.slide-row--drop-before::before {
|
|
1024
|
+
top: -3px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.slide-row--drop-after::after {
|
|
1028
|
+
bottom: -3px;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* The button inside sets the pointer cursor itself. Once a drag starts, the
|
|
1032
|
+
browser draws the drag cursor and no CSS reaches it. */
|
|
1033
|
+
.slide-row__handle {
|
|
1034
|
+
flex: 0 0 auto;
|
|
1035
|
+
touch-action: none;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.slide-row__position {
|
|
1039
|
+
flex: 0 0 1.5em;
|
|
1040
|
+
font-variant-numeric: tabular-nums;
|
|
1041
|
+
color: var(--pg-muted);
|
|
1042
|
+
text-align: end;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.slide-row__title {
|
|
1046
|
+
flex: 1 1 200px;
|
|
1047
|
+
min-width: 0;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.slide-row__group {
|
|
1051
|
+
flex: 0 1 200px;
|
|
1052
|
+
min-width: 0;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.slide-row__actions {
|
|
1056
|
+
display: flex;
|
|
1057
|
+
flex: 0 0 auto;
|
|
1058
|
+
align-items: center;
|
|
1059
|
+
margin-inline-start: auto;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.slide-row--removed .slide-row__title,
|
|
1063
|
+
.slide-row--removed .slide-row__group {
|
|
1064
|
+
text-decoration: line-through;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1022
1067
|
/* The base CSS field is the same CodeMirror editor as the CSS pane, boxed
|
|
1023
1068
|
like a design system text area so it sits with the other fields. */
|
|
1024
1069
|
.settings__code .settings__legend {
|
|
@@ -1046,6 +1091,9 @@ nys-toggle[hidden] {
|
|
|
1046
1091
|
|
|
1047
1092
|
/* Toast ---------------------------------------------------------------- */
|
|
1048
1093
|
|
|
1094
|
+
/* The toast host is a live region that stays in the page. Empty, it has no
|
|
1095
|
+
size, so it needs no hiding: `display: none` would take the region out of
|
|
1096
|
+
the accessibility tree between toasts. */
|
|
1049
1097
|
.toast {
|
|
1050
1098
|
position: fixed;
|
|
1051
1099
|
inset-block-end: 24px;
|
|
@@ -1054,10 +1102,6 @@ nys-toggle[hidden] {
|
|
|
1054
1102
|
max-width: 320px;
|
|
1055
1103
|
}
|
|
1056
1104
|
|
|
1057
|
-
.toast[hidden] {
|
|
1058
|
-
display: none;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
1105
|
/* Narrow screens ------------------------------------------------------- */
|
|
1062
1106
|
|
|
1063
1107
|
@media (max-width: 720px) {
|
package/src/deck-model.ts
CHANGED
|
@@ -35,6 +35,26 @@ export interface StoredDeck {
|
|
|
35
35
|
/** The markup a brand new slide starts from. */
|
|
36
36
|
export const BLANK_SLIDE_HTML = '<nys-button label="Excelsior"></nys-button>\n';
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether a slide holds anything worth a warning before it is deleted: code
|
|
40
|
+
* in any editor beyond the starter button, or a setting someone changed from
|
|
41
|
+
* what a new slide gets.
|
|
42
|
+
*/
|
|
43
|
+
export function hasContent(slide: Slide): boolean {
|
|
44
|
+
const html = slide.html.trim();
|
|
45
|
+
return (
|
|
46
|
+
(html !== '' && html !== BLANK_SLIDE_HTML.trim()) ||
|
|
47
|
+
slide.css.trim() !== '' ||
|
|
48
|
+
slide.js.trim() !== '' ||
|
|
49
|
+
!/^Slide \d+$/.test(slide.title.trim()) ||
|
|
50
|
+
slide.group.trim() !== '' ||
|
|
51
|
+
slide.description.trim() !== '' ||
|
|
52
|
+
slide.notes.trim() !== '' ||
|
|
53
|
+
slide.version !== 'latest' ||
|
|
54
|
+
(slide.editors?.length ?? 0) > 0
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
/** Turns a title into a slug that is safe in a URL and a filename. */
|
|
39
59
|
export function slugify(title: string): string {
|
|
40
60
|
const slug = title
|
|
@@ -278,6 +298,33 @@ export function staleStarters(
|
|
|
278
298
|
});
|
|
279
299
|
}
|
|
280
300
|
|
|
301
|
+
/** One row of the slide list in Deck settings. */
|
|
302
|
+
export interface SlideEdit {
|
|
303
|
+
id: string;
|
|
304
|
+
title: string;
|
|
305
|
+
group: string;
|
|
306
|
+
/** Marked for removal; the slide goes when the edits are applied. */
|
|
307
|
+
removed?: boolean;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Applies the slide list's edits: the rows' order, titles, and groups, with
|
|
312
|
+
* removed rows dropped. Code and every other field come from the slide as it
|
|
313
|
+
* is. A blank title keeps the old one, and the result is never empty.
|
|
314
|
+
*/
|
|
315
|
+
export function arrangeSlides(slides: readonly Slide[], edits: readonly SlideEdit[]): Slide[] {
|
|
316
|
+
const byId = new Map(slides.map((slide) => [slide.id, slide]));
|
|
317
|
+
const result: Slide[] = [];
|
|
318
|
+
for (const edit of edits) {
|
|
319
|
+
const slide = byId.get(edit.id);
|
|
320
|
+
if (!slide || edit.removed) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
result.push({...slide, title: edit.title.trim() || slide.title, group: edit.group.trim()});
|
|
324
|
+
}
|
|
325
|
+
return result.length > 0 ? result : slides.map((slide) => ({...slide}));
|
|
326
|
+
}
|
|
327
|
+
|
|
281
328
|
/** Formats an ISO timestamp as "today", "3 days ago", and so on. */
|
|
282
329
|
export function relativeTime(iso: string, now = new Date()): string {
|
|
283
330
|
const then = new Date(iso).getTime();
|
package/src/decks.test.ts
CHANGED
|
@@ -9,7 +9,9 @@ import assert from 'node:assert/strict';
|
|
|
9
9
|
import test from 'node:test';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
arrangeSlides,
|
|
12
13
|
copyOfDeck,
|
|
14
|
+
hasContent,
|
|
13
15
|
makeDeck,
|
|
14
16
|
makeSlide,
|
|
15
17
|
missingStarters,
|
|
@@ -286,3 +288,56 @@ test('staleStarters finds untouched copies seeded under a retired key', () => {
|
|
|
286
288
|
);
|
|
287
289
|
assert.deepEqual(staleStarters([untouched], retired, []), []);
|
|
288
290
|
});
|
|
291
|
+
|
|
292
|
+
test('arrangeSlides reorders, renames, regroups, and drops removed slides', () => {
|
|
293
|
+
const slides = [
|
|
294
|
+
makeSlide({id: 'a', title: 'A', html: 'a'}),
|
|
295
|
+
makeSlide({id: 'b', title: 'B', html: 'b', group: 'One'}),
|
|
296
|
+
makeSlide({id: 'c', title: 'C', html: 'c'}),
|
|
297
|
+
];
|
|
298
|
+
const result = arrangeSlides(slides, [
|
|
299
|
+
{id: 'c', title: 'C renamed', group: ' Two '},
|
|
300
|
+
{id: 'b', title: 'B', group: 'One', removed: true},
|
|
301
|
+
{id: 'a', title: ' ', group: ''},
|
|
302
|
+
]);
|
|
303
|
+
assert.deepEqual(
|
|
304
|
+
result.map((slide) => [slide.id, slide.title, slide.group, slide.html]),
|
|
305
|
+
[
|
|
306
|
+
['c', 'C renamed', 'Two', 'c'],
|
|
307
|
+
['a', 'A', '', 'a'],
|
|
308
|
+
],
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('arrangeSlides ignores unknown rows and never returns an empty deck', () => {
|
|
313
|
+
const slides = [makeSlide({id: 'a', title: 'A', html: 'a'})];
|
|
314
|
+
assert.deepEqual(
|
|
315
|
+
arrangeSlides(slides, [{id: 'ghost', title: 'Ghost', group: ''}]).map((s) => s.id),
|
|
316
|
+
['a'],
|
|
317
|
+
);
|
|
318
|
+
assert.deepEqual(
|
|
319
|
+
arrangeSlides(slides, [{id: 'a', title: 'A', group: '', removed: true}]).map((s) => s.id),
|
|
320
|
+
['a'],
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('hasContent is false for a slide as Add slide or New deck made it', () => {
|
|
325
|
+
assert.equal(hasContent(makeSlide({id: 'slide-3', title: 'Slide 3'})), false);
|
|
326
|
+
assert.equal(
|
|
327
|
+
hasContent(makeSlide({id: 'slide-1', title: 'Slide 1', html: '<nys-button label="Excelsior"></nys-button>\n'})),
|
|
328
|
+
false,
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('hasContent is true once anything was typed or set', () => {
|
|
333
|
+
const blank = {id: 'slide-3', title: 'Slide 3'};
|
|
334
|
+
assert.equal(hasContent(makeSlide({...blank, html: '<p>Hi</p>'})), true);
|
|
335
|
+
assert.equal(hasContent(makeSlide({...blank, css: 'p{}'})), true);
|
|
336
|
+
assert.equal(hasContent(makeSlide({...blank, js: 'x()'})), true);
|
|
337
|
+
assert.equal(hasContent(makeSlide({...blank, title: 'Button'})), true);
|
|
338
|
+
assert.equal(hasContent(makeSlide({...blank, group: 'One'})), true);
|
|
339
|
+
assert.equal(hasContent(makeSlide({...blank, description: 'd'})), true);
|
|
340
|
+
assert.equal(hasContent(makeSlide({...blank, notes: 'n'})), true);
|
|
341
|
+
assert.equal(hasContent(makeSlide({...blank, version: '1.21.0'})), true);
|
|
342
|
+
assert.equal(hasContent(makeSlide({...blank, editors: ['html']})), true);
|
|
343
|
+
});
|