Implement Toggleable<T> and Interactive<T> properly

Piotr Osiewicz created

Change summary

styles/package-lock.json               | 30 +++++++++++++++
styles/package.json                    |  4 +
styles/src/styleTree/commandPalette.ts | 53 +++++++++++++++------------
styles/src/styleTree/interactive.ts    | 19 ++++++---
styles/src/styleTree/toggle.ts         | 40 +++++++++++++++-----
5 files changed, 104 insertions(+), 42 deletions(-)

Detailed changes

styles/package-lock.json 🔗

@@ -18,7 +18,9 @@
                 "chroma-js": "^2.4.2",
                 "deepmerge": "^4.3.0",
                 "toml": "^3.0.0",
-                "ts-node": "^10.9.1"
+                "ts-deepmerge": "^6.0.3",
+                "ts-node": "^10.9.1",
+                "utility-types": "^3.10.0"
             }
         },
         "node_modules/@cspotcode/source-map-support": {
@@ -180,6 +182,14 @@
             "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
             "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
         },
+        "node_modules/ts-deepmerge": {
+            "version": "6.0.3",
+            "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.0.3.tgz",
+            "integrity": "sha512-MBBJL0UK/mMnZRONMz4J1CRu5NsGtsh+gR1nkn8KLE9LXo/PCzeHhQduhNary8m5/m9ryOOyFwVKxq81cPlaow==",
+            "engines": {
+                "node": ">=14.13.1"
+            }
+        },
         "node_modules/ts-node": {
             "version": "10.9.1",
             "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -235,6 +245,14 @@
                 "node": ">=4.2.0"
             }
         },
+        "node_modules/utility-types": {
+            "version": "3.10.0",
+            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
+            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
+            "engines": {
+                "node": ">= 4"
+            }
+        },
         "node_modules/v8-compile-cache-lib": {
             "version": "3.0.1",
             "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -382,6 +400,11 @@
             "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
             "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
         },
+        "ts-deepmerge": {
+            "version": "6.0.3",
+            "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.0.3.tgz",
+            "integrity": "sha512-MBBJL0UK/mMnZRONMz4J1CRu5NsGtsh+gR1nkn8KLE9LXo/PCzeHhQduhNary8m5/m9ryOOyFwVKxq81cPlaow=="
+        },
         "ts-node": {
             "version": "10.9.1",
             "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -408,6 +431,11 @@
             "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
             "peer": true
         },
+        "utility-types": {
+            "version": "3.10.0",
+            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
+            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg=="
+        },
         "v8-compile-cache-lib": {
             "version": "3.0.1",
             "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

styles/package.json 🔗

@@ -20,7 +20,9 @@
         "chroma-js": "^2.4.2",
         "deepmerge": "^4.3.0",
         "toml": "^3.0.0",
-        "ts-node": "^10.9.1"
+        "ts-deepmerge": "^6.0.3",
+        "ts-node": "^10.9.1",
+        "utility-types": "^3.10.0"
     },
     "prettier": {
         "semi": false,

styles/src/styleTree/commandPalette.ts 🔗

@@ -1,30 +1,37 @@
 import { ColorScheme } from "../theme/colorScheme"
 import { withOpacity } from "../theme/color"
 import { text, background } from "./components"
+import { toggleable } from "./toggle"
+import { interactive } from "./interactive"
 
 export default function commandPalette(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-    return {
-        keystrokeSpacing: 8,
-        key: {
-            text: text(layer, "mono", "variant", "default", { size: "xs" }),
-            cornerRadius: 2,
-            background: background(layer, "on"),
-            padding: {
-                top: 1,
-                bottom: 1,
-                left: 6,
-                right: 6,
-            },
-            margin: {
-                top: 1,
-                bottom: 1,
-                left: 2,
-            },
-            active: {
-                text: text(layer, "mono", "on", "default", { size: "xs" }),
-                background: withOpacity(background(layer, "on"), 0.2),
-            },
+  let layer = colorScheme.highest
+  return {
+    keystrokeSpacing: 8,
+    key:
+      toggleable(interactive({
+        text: text(layer, "mono", "variant", "default", { size: "xs" }),
+        cornerRadius: 2,
+        background: background(layer, "on"),
+        padding: {
+          top: 1,
+          bottom: 1,
+          left: 6,
+          right: 6,
         },
-    }
+        margin: {
+          top: 1,
+          bottom: 1,
+          left: 2,
+        },
+      }, { hover: { cornerRadius: 4, padding: { top: 17 } } }), {
+        default: {
+          text: text(layer, "mono", "on", "default", { size: "xs" }),
+          background: withOpacity(background(layer, "on"), 0.2),
+        }
+
+      })
+    ,
+
+  }
 }

styles/src/styleTree/interactive.ts 🔗

@@ -1,3 +1,5 @@
+import { DeepPartial } from "utility-types";
+import merge from "ts-deepmerge"
 interface Interactive<T> {
   default: T,
   hover?: T,
@@ -5,21 +7,26 @@ interface Interactive<T> {
   disabled?: T,
 }
 
-export function interactive<T>(base: T, modifications: Partial<Interactive<T>>): Interactive<T> {
-  const interactiveObj: Interactive<T> = {
+// Helper function for creating Interactive<T> objects that works pretty much like Toggle<T>.
+// It takes a object to be used as a value for `default` field and then fills out other fields
+// with fields from either `base` or `modifications`. Notably, it does not touch `hover`, `clicked` and `disabled` if there are no modifications for it.
+export function interactive<T extends Object>(base: T, modifications: DeepPartial<Interactive<T>>): Interactive<T> {
+  let interactiveObj: Interactive<T> = {
     default: base,
   };
-
+  if (modifications.default !== undefined) {
+    interactiveObj.default = merge(interactiveObj.default, modifications.default) as T;
+  }
   if (modifications.hover !== undefined) {
-    interactiveObj.hover = { ...base, ...modifications.hover };
+    interactiveObj.hover = merge(interactiveObj.default, modifications.hover) as T;
   }
 
   if (modifications.clicked !== undefined) {
-    interactiveObj.clicked = { ...base, ...modifications.clicked };
+    interactiveObj.clicked = merge(interactiveObj.default, modifications.clicked) as T;
   }
 
   if (modifications.disabled !== undefined) {
-    interactiveObj.disabled = { ...base, ...modifications.disabled };
+    interactiveObj.disabled = merge(interactiveObj.default, modifications.disabled) as T;
   }
 
   return interactiveObj;

styles/src/styleTree/toggle.ts 🔗

@@ -1,17 +1,35 @@
+import { DeepPartial } from 'utility-types';
+import merge from 'ts-deepmerge';
+
 interface Toggleable<T> {
   inactive: T
   active: T,
 }
 
-export function toggleable<T>(inactive: T, modifications: Partial<Toggleable<T>>): Toggleable<T> {
-  let active: T = inactive;
-  if (modifications.active !== undefined) {
-    active = { ...inactive, ...modifications.active };
-  }
-  return {
-    inactive: inactive,
-    active: active
-  };
-
-  d
+/// Helper function for creating Toggleable objects; it takes a object of type T that is used as
+/// `inactive` member of result Toggleable<T>. `active` member is created by applying `modifications` on top of `inactive` argument.
+// Thus, the following call:
+// ```
+//   toggleable({day: 1, month: "January"}, {day: 3})
+// ```
+// To return the following object:
+// ```
+//    Toggleable<_>{
+//      inactive: { day: 1, month: "January" },
+//      active: { day: 3, month: "January" }
+//    }
+// ```
+// Remarkably, it also works for nested structures:
+// ```
+//   toggleable({first_level: "foo", second_level: {nested_member: "nested"}}, {second_level: {nested_member: "another nested thing"}})
+// ```
+// ```
+//   Toggleable<_> {
+//     inactive: {first_level: "foo", second_level: {nested_member: "nested"}},
+//     active: { first_level: "foo", second_level: {nested_member: "another nested thing"}}
+//   }
+// ```
+export function toggleable<T extends Object>(inactive: T, modifications: DeepPartial<T>): Toggleable<T> {
+  let active: T = merge(inactive, modifications) as T;
+  return { active: active, inactive: inactive };
 }