Add reset and styling

Change-Id: I5f18d0c07c4da3ee03dccb5c06b9510a0fc62772
diff --git a/.dockerignore b/.dockerignore
index 52f7ccb..a1cf873 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,8 @@
 !mapper/
 !matcher/
 !mappings/
+!testdata/
+testdata/sandbox
 !/LICENSE
 !.dockerignore
 !go.mod
diff --git a/cmd/koralmapper/main_test.go b/cmd/koralmapper/main_test.go
index b4dfc02..c53cd54 100644
--- a/cmd/koralmapper/main_test.go
+++ b/cmd/koralmapper/main_test.go
@@ -1948,8 +1948,8 @@
 	assert.Contains(t, htmlContent, "anno-mapper")
 	assert.Contains(t, htmlContent, `data-id="anno-mapper"`)
 	assert.Contains(t, htmlContent, `data-type="annotation"`)
-	assert.Contains(t, htmlContent, `value="opennlp"`)
-	assert.Contains(t, htmlContent, `value="upos"`)
+	assert.Contains(t, htmlContent, `placeholder="opennlp"`)
+	assert.Contains(t, htmlContent, `placeholder="upos"`)
 	assert.Contains(t, htmlContent, "Annotation mapping")
 
 	// Corpus mapping entries
@@ -1964,6 +1964,10 @@
 	assert.Contains(t, htmlContent, `class="response-fieldA"`)
 	assert.Contains(t, htmlContent, `class="response-fieldB"`)
 	assert.Contains(t, htmlContent, "Corpus mapping")
+
+	// Reset button
+	assert.Contains(t, htmlContent, `id="reset-btn"`)
+	assert.Contains(t, htmlContent, "Reset all")
 }
 
 func TestConfigPageAnnotationMappingHasFoundryInputs(t *testing.T) {
@@ -2068,8 +2072,8 @@
 	assert.Contains(t, htmlContent, `class="request-fieldB"`)
 	assert.Contains(t, htmlContent, `class="response-fieldA"`)
 	assert.Contains(t, htmlContent, `class="response-fieldB"`)
-	assert.Contains(t, htmlContent, `value="genre"`)
-	assert.Contains(t, htmlContent, `value="topic"`)
+	assert.Contains(t, htmlContent, `placeholder="genre"`)
+	assert.Contains(t, htmlContent, `placeholder="topic"`)
 }
 
 func TestConfigPageBackwardCompatibility(t *testing.T) {
@@ -2169,6 +2173,89 @@
 	assert.Equal(t, "First corpus", data.CorpusMappings[0].Description)
 }
 
+func TestConfigPageDefaultsAsPlaceholdersOnly(t *testing.T) {
+	lists := []tmconfig.MappingList{
+		{
+			ID:       "anno-mapper",
+			FoundryA: "opennlp",
+			LayerA:   "p",
+			FoundryB: "upos",
+			LayerB:   "pos",
+			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
+		},
+		{
+			ID:     "corpus-mapper",
+			Type:   "corpus",
+			FieldA: "genre",
+			FieldB: "topic",
+			Mappings: []tmconfig.MappingRule{
+				"textClass=science <> textClass=akademisch",
+			},
+		},
+	}
+	m, err := mapper.NewMapper(lists)
+	require.NoError(t, err)
+
+	mockConfig := &tmconfig.MappingConfig{Lists: lists}
+	tmconfig.ApplyDefaults(mockConfig)
+
+	app := fiber.New()
+	setupRoutes(app, m, mockConfig)
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	resp, err := app.Test(req)
+	require.NoError(t, err)
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	require.NoError(t, err)
+	htmlContent := string(body)
+
+	// Placeholders are present
+	assert.Contains(t, htmlContent, `placeholder="opennlp"`)
+	assert.Contains(t, htmlContent, `placeholder="upos"`)
+	assert.Contains(t, htmlContent, `placeholder="genre"`)
+	assert.Contains(t, htmlContent, `placeholder="topic"`)
+
+	// Value attributes must NOT appear on mapping inputs (defaults shown as
+	// placeholders only). We check that the combined string value="opennlp"
+	// etc. is absent.
+	assert.NotContains(t, htmlContent, `value="opennlp"`)
+	assert.NotContains(t, htmlContent, `value="upos"`)
+	assert.NotContains(t, htmlContent, `value="genre"`)
+	assert.NotContains(t, htmlContent, `value="topic"`)
+}
+
+func TestConfigPageHasResetButton(t *testing.T) {
+	lists := []tmconfig.MappingList{
+		{
+			ID:       "test-mapper",
+			Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
+		},
+	}
+	m, err := mapper.NewMapper(lists)
+	require.NoError(t, err)
+
+	mockConfig := &tmconfig.MappingConfig{Lists: lists}
+	tmconfig.ApplyDefaults(mockConfig)
+
+	app := fiber.New()
+	setupRoutes(app, m, mockConfig)
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	resp, err := app.Test(req)
+	require.NoError(t, err)
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	require.NoError(t, err)
+	htmlContent := string(body)
+
+	assert.Contains(t, htmlContent, `id="reset-btn"`)
+	assert.Contains(t, htmlContent, "Reset all")
+	assert.Contains(t, htmlContent, `type="button"`)
+}
+
 func TestConfigPagePreservesOrderOfMappings(t *testing.T) {
 	lists := []tmconfig.MappingList{
 		{
diff --git a/cmd/koralmapper/static/config.html b/cmd/koralmapper/static/config.html
index ef682f5..ac22063 100644
--- a/cmd/koralmapper/static/config.html
+++ b/cmd/koralmapper/static/config.html
@@ -25,9 +25,9 @@
                     <input type="checkbox" id="check-{{.ID}}-{{$section.Mode}}" class="checkbox {{$section.CheckboxClass}}" name="{{$section.CheckboxName}}">
                     <label for="check-{{.ID}}-{{$section.Mode}}"><span></span><span class="data-mode">query</span> <strong>{{.ID}}</strong></label>
                     <div class="mapping-fields {{$section.FieldsClass}}">
-                        <input type="text" class="{{$section.Mode}}-foundryA" value="{{.FoundryA}}" placeholder="{{.FoundryA}}" size="8">/<input type="text" class="{{$section.Mode}}-layerA" value="{{.LayerA}}" placeholder="{{.LayerA}}" size="4">
+                        <input type="text" class="{{$section.Mode}}-foundryA" placeholder="{{.FoundryA}}" size="8">/<input type="text" class="{{$section.Mode}}-layerA" placeholder="{{.LayerA}}" size="4">
                         <button type="button" class="{{$section.ArrowClass}}" data-dir="{{$section.ArrowDirection}}">{{$section.ArrowLabel}}</button>
-                        <input type="text" class="{{$section.Mode}}-foundryB" value="{{.FoundryB}}" placeholder="{{.FoundryB}}" size="8">/<input type="text" class="{{$section.Mode}}-layerB" value="{{.LayerB}}" placeholder="{{.LayerB}}" size="4">
+                        <input type="text" class="{{$section.Mode}}-foundryB" placeholder="{{.FoundryB}}" size="8">/<input type="text" class="{{$section.Mode}}-layerB" placeholder="{{.LayerB}}" size="4">
                     </div>
                 </div>
             </div>
@@ -39,9 +39,9 @@
                     <input type="checkbox" id="check-{{.ID}}-{{$section.Mode}}" class="checkbox {{$section.CheckboxClass}}" name="{{$section.CheckboxName}}">
                     <label for="check-{{.ID}}-{{$section.Mode}}"><span></span><span class="data-mode">corpus</span> <strong>{{.ID}}</strong></label>
                     <div class="mapping-fields {{$section.FieldsClass}}">
-                        <input type="text" class="{{$section.Mode}}-fieldA" value="{{.FieldA}}" placeholder="{{.FieldA}}" size="10">
+                        <input type="text" class="{{$section.Mode}}-fieldA" placeholder="{{.FieldA}}" size="10">
                         <button type="button" class="{{$section.ArrowClass}}" data-dir="{{$section.ArrowDirection}}">{{$section.ArrowLabel}}</button>
-                        <input type="text" class="{{$section.Mode}}-fieldB" value="{{.FieldB}}" placeholder="{{.FieldB}}" size="10">
+                        <input type="text" class="{{$section.Mode}}-fieldB" placeholder="{{.FieldB}}" size="10">
                     </div>
                 </div>
             </div>
@@ -50,7 +50,7 @@
         </section>
         {{end}}
 
-        <section>
+        <section id="mapping-info">
           <dl>
             {{range .AnnotationMappings}}
             <dt>{{.ID}}:</dt>
@@ -61,6 +61,8 @@
             {{if .Description}}<dd>{{.Description}}</dd>{{end}}
             {{end}}
           </dl>
+
+          <button type="button" id="reset-btn">Reset all</button>
         </section>
 
         <section class="mapping-section" id="pipe-info">
@@ -73,7 +75,7 @@
     </section>
 
         <footer class="version">
-            <p><tt>v{{.Version}} - {{.Date}} {{.Hash}}</tt></p>
+            <p><tt>{{.Version}} - {{.Date}} {{.Hash}}</tt></p>
         </footer>
     </div>
     <script src="/static/config.js"></script>
diff --git a/cmd/koralmapper/static/config.js b/cmd/koralmapper/static/config.js
index a98aa61..9f6a92c 100644
--- a/cmd/koralmapper/static/config.js
+++ b/cmd/koralmapper/static/config.js
@@ -38,6 +38,10 @@
     document.cookie = cookieName + "=" + value + "; path=/; SameSite=Lax; max-age=31536000";
   }
 
+  function deleteCookie() {
+    document.cookie = cookieName + "=; path=/; SameSite=Lax; max-age=0";
+  }
+
   // Form state
 
   function reverseDir(dir) {
@@ -320,5 +324,36 @@
     })(arrows[i]);
   }
 
+  // Reset button
+
+  function resetForm() {
+    for (var i = 0; i < checkboxes.length; i++) {
+      checkboxes[i].checked = false;
+    }
+
+    for (var i = 0; i < textInputs.length; i++) {
+      textInputs[i].value = "";
+    }
+
+    var requestArrows = container.querySelectorAll(".request-dir-arrow");
+    for (var i = 0; i < requestArrows.length; i++) {
+      requestArrows[i].dataset.dir = "atob";
+      requestArrows[i].textContent = "\u2192";
+    }
+    var responseArrows = container.querySelectorAll(".response-dir-arrow");
+    for (var i = 0; i < responseArrows.length; i++) {
+      responseArrows[i].dataset.dir = "btoa";
+      responseArrows[i].textContent = "\u2190";
+    }
+
+    deleteCookie();
+    registerPipes();
+  }
+
+  var resetBtn = container.querySelector("#reset-btn");
+  if (resetBtn) {
+    resetBtn.addEventListener("click", resetForm);
+  }
+
   registerPipes();
 })();
diff --git a/cmd/koralmapper/static/style.css b/cmd/koralmapper/static/style.css
index 4fd7230..d639d93 100644
--- a/cmd/koralmapper/static/style.css
+++ b/cmd/koralmapper/static/style.css
@@ -17,14 +17,30 @@
     margin:.2em 0;
 }
 
+#mapping-info {
+    display: flex;
+    flex-direction: row;
+    align-content: space-between; 
+    align-items:flex-start;
+}
+
+#mapping-info > dl {
+    width: 100%;
+}
+
+#mapping-info > button {
+    white-space: nowrap
+}
+
 .data-mode {
     display: inline-block;
     font-size:70%;
+    padding: .2em 0;
     width: 6em;
     text-align: center;
-    border: 1px solid grey;
-    background-color: #ffcc55;
-    border-radius: 5px;
+    color: var(--nearly-white-color);
+    background-color: var(--light-green-color);
+    border-radius: 1em;
 }
 
 .mapping-fields > * {
@@ -38,7 +54,42 @@
 
 .mapping-fields input[type="text"] { font-family: monospace; }
 .request-fields, .response-fields { flex-wrap: wrap; }
-.request-dir-arrow, .response-dir-arrow { cursor: pointer; border: 1px solid #bbb; background: #f8f8f8; border-radius: 0.25rem; min-width: 2rem; }
 .cfg-line-label { display: block; margin: 0.35rem 0 0.1rem; }
 .cfg-preview { width: 100%; box-sizing: border-box; font-family: monospace; }
-footer { background-color: #aaa; font-size:50%; text-align: right; padding: 0.25rem; }
\ No newline at end of file
+
+.request-dir-arrow,
+.response-dir-arrow,
+#reset-btn {
+    cursor: pointer;
+    border-radius: 0.25rem;
+    border: 1px solid;
+    color:var(--dark-grey-color);
+    background-color:var(--light-grey-color);
+    border-color:var(--dark-grey-color);
+    text-shadow:1px 1px hsla(0,0%,100%,.5);
+}
+.request-dir-arrow:hover,
+.response-dir-arrow:hover,
+#reset-btn:hover {
+  color:var(--nearly-white-color);
+  background-color:var(--dark-orange-color);
+  border-color: var(--darkest-orange-color);
+  text-shadow:none
+}
+
+.request-dir-arrow,
+.response-dir-arrow {
+    min-width: 2rem;
+}
+#reset-btn {
+    float:right;
+    padding: 0.3rem 1rem;
+}
+
+footer {
+    background-color: var(--light-grey-color);
+    font-size:60%;
+    text-align: right;
+    padding: 0.25rem;
+    padding-right:1em;
+}
\ No newline at end of file