Start on TypeScript-based styling system

Nathan Sobo and Nate Butler created

Co-Authored-By: Nate Butler <1714999+iamnbutler@users.noreply.github.com>

Change summary

.gitignore               |   1 
styles/app.ts            | 632 ++++++++++++++++++++++++++++++++++++++++++
styles/components.ts     |  62 ++++
styles/core.ts           |  38 ++
styles/package-lock.json |  28 +
styles/package.json      |  14 
styles/selector-modal.ts |  59 +++
styles/theme.ts          | 121 ++++++++
8 files changed, 955 insertions(+)

Detailed changes

.gitignore 🔗

@@ -2,6 +2,7 @@
 /zed.xcworkspace
 .DS_Store
 /script/node_modules
+/styles/node_modules
 /crates/server/.env.toml
 /crates/server/static/styles.css
 /vendor/bin

styles/app.ts 🔗

@@ -0,0 +1,632 @@
+import { selectorModal } from "./selector-modal";
+import Theme from "./theme";
+
+export default function app(theme: Theme): Object {
+  return {
+    selector: selectorModal(theme),
+    workspace: {
+      background: "$surface.500",
+      leaderBorderOpacity: 0.7,
+      leader_border_width: 2.0,
+      active_tab: {
+        background: "$surface.300",
+        extends: "$workspace.tab",
+        text: "$text.primary",
+        border: {
+          bottom: false,
+        },
+      },
+      left_sidebar: {
+        extends: "$workspace.sidebar",
+        border: {
+          color: "$border.primary",
+          right: true,
+          width: 1,
+        },
+      },
+      pane_divider: {
+        color: "$border.primary",
+        width: 1,
+      },
+      right_sidebar: {
+        extends: "$workspace.sidebar",
+        border: {
+          color: "$border.primary",
+          left: true,
+          width: 1,
+        },
+      },
+      sidebar: {
+        width: 30,
+        active_item: {
+          extends: "$workspace.sidebar.item",
+          icon_color: "$text.primary.color",
+        },
+        border: {
+          color: "$border.primary",
+          right: true,
+          width: 1,
+        },
+        item: {
+          height: "$workspace.tab.height",
+          icon_color: "$text.muted.color",
+          icon_size: 18,
+        },
+        resize_handle: {
+          background: "$border.primary",
+          padding: {
+            left: 1,
+          },
+        },
+      },
+      status_bar: {
+        cursor_position: "$text.muted",
+        diagnostic_message: "$text.muted",
+        height: 24,
+        item_spacing: 8,
+        lsp_message: "$text.muted",
+        padding: {
+          left: 6,
+          right: 6,
+        },
+      },
+      tab: {
+        height: 34,
+        icon_close: "$text.muted.color",
+        icon_close_active: "$text.primary.color",
+        icon_conflict: "$status.warn",
+        icon_dirty: "$status.info",
+        icon_width: 8,
+        spacing: 10,
+        text: "$text.muted",
+        border: {
+          bottom: true,
+          color: "$border.primary",
+          left: true,
+          overlay: true,
+          width: 1,
+        },
+        padding: {
+          left: 12,
+          right: 12,
+        },
+      },
+      titlebar: {
+        avatar_width: 18,
+        height: 32,
+        share_icon_active_color: "$text.primary.color",
+        share_icon_color: "$text.muted.color",
+        title: "$text.primary",
+        avatar: {
+          corner_radius: 10,
+          border: {
+            color: "#00000088",
+            width: 1,
+          },
+        },
+        avatar_ribbon: {
+          background: "#ff0000",
+          height: 3,
+          width: 12,
+        },
+        border: {
+          bottom: true,
+          color: "$border.primary",
+          width: 1,
+        },
+        hovered_sign_in_prompt: {
+          color: "$text.secondary.color",
+          extends: "$workspace.titlebar.sign_in_prompt",
+        },
+        offline_icon: {
+          color: "$text.muted.color",
+          width: 16,
+          padding: {
+            right: 4,
+          },
+        },
+        outdated_warning: {
+          extends: "$text.muted",
+          size: 13,
+        },
+        sign_in_prompt: {
+          extends: "$text.muted",
+          size: 13,
+          underline: true,
+          padding: {
+            right: 8,
+          },
+        },
+      },
+      toolbar: {
+        height: 44,
+      },
+    },
+    chat_panel: {
+      extends: "$panel",
+      channel_name: {
+        extends: "$text.primary",
+        weight: "bold",
+      },
+      channel_name_hash: {
+        text: "$text.muted",
+        padding: {
+          right: 8,
+        },
+      },
+      channel_select: {
+        active_item: {
+          extends: "$chat_panel.channel_select.item",
+          name: "$text.primary",
+        },
+        header: {
+          extends: "$chat_panel.channel_select.active_item",
+          padding: {
+            bottom: 4,
+            left: 0,
+          },
+        },
+        hovered_active_item: {
+          extends: "$chat_panel.channel_select.hovered_item",
+          name: "$text.primary",
+        },
+        hovered_item: {
+          background: "$state.hover",
+          corner_radius: 6,
+          extends: "$chat_panel.channel_select.item",
+        },
+        item: {
+          name: "$text.secondary",
+          padding: 4,
+          hash: {
+            extends: "$text.muted",
+            margin: {
+              right: 8,
+            },
+          },
+        },
+        menu: {
+          background: "$surface.500",
+          corner_radius: 6,
+          padding: 4,
+          border: {
+            color: "$border.primary",
+            width: 1,
+          },
+          shadow: {
+            blur: 16,
+            color: "$shadow.0",
+            offset: [0, 2],
+          },
+        },
+      },
+      hovered_sign_in_prompt: {
+        color: "$text.secondary.color",
+        extends: "$chat_panel.sign_in_prompt",
+      },
+      input_editor: {
+        background: "$surface.300",
+        corner_radius: 6,
+        placeholder_text: "$text.muted",
+        selection: "$selection.host",
+        text: "$text.primary",
+        border: {
+          color: "$border.primary",
+          width: 1,
+        },
+        padding: {
+          bottom: 7,
+          left: 8,
+          right: 8,
+          top: 7,
+        },
+      },
+      message: {
+        body: "$text.secondary",
+        timestamp: "$text.muted",
+        padding: {
+          bottom: 6,
+        },
+        sender: {
+          extends: "$text.primary",
+          weight: "bold",
+          margin: {
+            right: 8,
+          },
+        },
+      },
+      pending_message: {
+        extends: "$chat_panel.message",
+        body: {
+          color: "$text.muted.color",
+        },
+        sender: {
+          color: "$text.muted.color",
+        },
+        timestamp: {
+          color: "$text.muted.color",
+        },
+      },
+      sign_in_prompt: {
+        extends: "$text.primary",
+        underline: true,
+      },
+    },
+    contacts_panel: {
+      extends: "$panel",
+      host_row_height: 28,
+      tree_branch_color: "$surface.100",
+      tree_branch_width: 1,
+      host_avatar: {
+        corner_radius: 10,
+        width: 18,
+      },
+      host_username: {
+        extends: "$text.primary",
+        padding: {
+          left: 8,
+        },
+      },
+      hovered_shared_project: {
+        background: "$state.hover",
+        corner_radius: 6,
+        extends: "$contacts_panel.shared_project",
+      },
+      hovered_unshared_project: {
+        background: "$state.hover",
+        corner_radius: 6,
+        extends: "$contacts_panel.unshared_project",
+      },
+      project: {
+        guest_avatar_spacing: 4,
+        height: 24,
+        guest_avatar: {
+          corner_radius: 8,
+          width: 14,
+        },
+        name: {
+          extends: "$text.secondary",
+          margin: {
+            right: 6,
+          },
+        },
+        padding: {
+          left: 8,
+        },
+      },
+      shared_project: {
+        extends: "$contacts_panel.project",
+        name: {
+          color: "$text.primary.color",
+        },
+      },
+      unshared_project: {
+        extends: "$contacts_panel.project",
+      },
+    },
+    editor: {
+      active_line_background: "$state.active_line",
+      background: "$surface.300",
+      code_actions_indicator: "$text.muted.color",
+      diff_background_deleted: "$state.deleted_line",
+      diff_background_inserted: "$state.inserted_line",
+      document_highlight_read_background: "#99999920",
+      document_highlight_write_background: "#99999916",
+      error_color: "$status.bad",
+      guest_selections: "$selection.guests",
+      gutter_background: "$surface.300",
+      gutter_padding_factor: 2.5,
+      highlighted_line_background: "$state.highlighted_line",
+      line_number: "$text.muted.color",
+      line_number_active: "$text.primary.color",
+      rename_fade: 0.6,
+      selection: "$selection.host",
+      text_color: "$text.secondary.color",
+      unnecessary_code_fade: 0.5,
+      autocomplete: {
+        background: "$surface.100",
+        corner_radius: 6,
+        padding: 6,
+        border: {
+          color: "$border.secondary",
+          width: 2,
+        },
+        hovered_item: {
+          background: "$state.hover",
+          extends: "$editor.autocomplete.item",
+        },
+        item: {
+          corner_radius: 6,
+          padding: {
+            bottom: 2,
+            left: 6,
+            right: 6,
+            top: 2,
+          },
+        },
+        margin: {
+          left: -14,
+        },
+        match_highlight: {
+          color: "$editor.syntax.keyword.color",
+          weight: "$editor.syntax.keyword.weight",
+        },
+        selected_item: {
+          background: "$state.selected",
+          extends: "$editor.autocomplete.item",
+        },
+      },
+      diagnostic_header: {
+        background: "$editor.background",
+        icon_width_factor: 1.5,
+        text_scale_factor: 0.857,
+        border: {
+          bottom: true,
+          color: "$border.secondary",
+          top: true,
+          width: 1,
+        },
+        code: {
+          extends: "$text.muted",
+          size: 14,
+          margin: {
+            left: 10,
+          },
+        },
+        message: {
+          highlight_text: {
+            extends: "$text.primary",
+            size: 14,
+            weight: "bold",
+          },
+          text: {
+            extends: "$text.secondary",
+            size: 14,
+          },
+        },
+      },
+      diagnostic_path_header: {
+        background: "$state.active_line",
+        text_scale_factor: 0.857,
+        filename: {
+          extends: "$text.primary",
+          size: 14,
+        },
+        path: {
+          extends: "$text.muted",
+          size: 14,
+          margin: {
+            left: 12,
+          },
+        },
+      },
+      error_diagnostic: {
+        text_scale_factor: 0.857,
+        header: {
+          border: {
+            color: "$border.primary",
+            top: true,
+            width: 1,
+          },
+        },
+        message: {
+          highlight_text: {
+            color: "$status.bad",
+            extends: "$text.secondary",
+            size: 14,
+            weight: "bold",
+          },
+          text: {
+            color: "$status.bad",
+            extends: "$text.secondary",
+            size: 14,
+          },
+        },
+      },
+      hint_diagnostic: {
+        extends: "$editor.error_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$status.info",
+          },
+          text: {
+            color: "$status.info",
+          },
+        },
+      },
+      information_diagnostic: {
+        extends: "$editor.error_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$status.info",
+          },
+          text: {
+            color: "$status.info",
+          },
+        },
+      },
+      invalid_error_diagnostic: {
+        extends: "$editor.error_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$text.muted.color",
+          },
+          text: {
+            color: "$text.muted.color",
+          },
+        },
+      },
+      invalid_hint_diagnostic: {
+        extends: "$editor.hint_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$text.muted.color",
+          },
+          text: {
+            color: "$text.muted.color",
+          },
+        },
+      },
+      invalid_information_diagnostic: {
+        extends: "$editor.information_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$text.muted.color",
+          },
+          text: {
+            color: "$text.muted.color",
+          },
+        },
+      },
+      invalid_warning_diagnostic: {
+        extends: "$editor.warning_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$text.muted.color",
+          },
+          text: {
+            color: "$text.muted.color",
+          },
+        },
+      },
+      warning_diagnostic: {
+        extends: "$editor.error_diagnostic",
+        message: {
+          highlight_text: {
+            color: "$status.warn",
+          },
+          text: {
+            color: "$status.warn",
+          },
+        },
+      },
+    },
+    project_diagnostics: {
+      background: "$surface.300",
+      tab_icon_spacing: 4,
+      tab_icon_width: 13,
+      tab_summary_spacing: 10,
+      empty_message: {
+        extends: "$text.primary",
+        size: 18,
+      },
+      status_bar_item: {
+        extends: "$text.muted",
+        margin: {
+          right: 10,
+        },
+      },
+    },
+    project_panel: {
+      extends: "$panel",
+      entry: {
+        height: 22,
+        icon_color: "$text.muted.color",
+        icon_size: 8,
+        icon_spacing: 8,
+        text: "$text.secondary",
+      },
+      hovered_entry: {
+        background: "$state.hover",
+        extends: "$project_panel.entry",
+      },
+      hovered_selected_entry: {
+        extends: "$project_panel.hovered_entry",
+        text: {
+          extends: "$text.primary",
+        },
+      },
+      padding: {
+        top: 6,
+      },
+      selected_entry: {
+        extends: "$project_panel.entry",
+        text: {
+          extends: "$text.primary",
+        },
+      },
+    },
+    search: {
+      background: "$surface.300",
+      match_background: "$state.highlighted_line",
+      tab_icon_spacing: 4,
+      tab_icon_width: 14,
+      active_hovered_option_button: {
+        background: "$surface.100",
+        extends: "$search.option_button",
+      },
+      active_option_button: {
+        background: "$surface.100",
+        extends: "$search.option_button",
+      },
+      editor: {
+        background: "$surface.500",
+        corner_radius: 6,
+        max_width: 400,
+        placeholder_text: "$text.muted",
+        selection: "$selection.host",
+        text: "$text.primary",
+        border: {
+          color: "$border.primary",
+          width: 1,
+        },
+        margin: {
+          bottom: 5,
+          left: 5,
+          right: 5,
+          top: 5,
+        },
+        padding: {
+          bottom: 3,
+          left: 13,
+          right: 13,
+          top: 3,
+        },
+      },
+      hovered_option_button: {
+        background: "$surface.100",
+        extends: "$search.option_button",
+      },
+      invalid_editor: {
+        extends: "$search.editor",
+        border: {
+          color: "$status.bad",
+          width: 1,
+        },
+      },
+      match_index: {
+        extends: "$text.secondary",
+        padding: 6,
+      },
+      option_button: {
+        background: "$surface.300",
+        corner_radius: 6,
+        extends: "$text.secondary",
+        border: {
+          color: "$border.primary",
+          width: 1,
+        },
+        margin: {
+          left: 1,
+          right: 1,
+        },
+        padding: {
+          bottom: 1,
+          left: 6,
+          right: 6,
+          top: 1,
+        },
+      },
+      option_button_group: {
+        padding: {
+          left: 2,
+          right: 2,
+        },
+      },
+      results_status: {
+        extends: "$text.primary",
+        size: 18,
+      },
+    },
+  };
+}

styles/components.ts 🔗

@@ -0,0 +1,62 @@
+import chroma from "chroma-js";
+import core, { Color } from "./core";
+import Theme, { BackgroundColor, Weight } from "./theme";
+
+export function text(
+  theme: Theme,
+  fontFamily: keyof typeof core.fontFamily,
+  color: keyof Theme["textColor"],
+  properties?: { size?: keyof typeof core["fontSize"]; weight?: Weight }
+) {
+  const sizeKey = properties.size || fontFamily === "sans" ? "sm" : "md";
+  const size = core.fontSize[sizeKey].value;
+
+  return {
+    family: core.fontFamily[fontFamily],
+    color: theme.textColor[color].value,
+    ...properties,
+    size,
+  };
+}
+
+export function border(theme: Theme, color: keyof Theme["borderColor"]) {
+  return {
+    color: theme.borderColor[color].value,
+    width: 1,
+  };
+}
+
+export interface Player {
+  selection: {
+    cursor: Color;
+    selection: Color;
+  };
+}
+
+export function player(
+  theme: Theme,
+  playerNumber: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
+): Player {
+  return {
+    selection: {
+      cursor: theme.player[playerNumber].cursorColor.value,
+      selection: theme.player[playerNumber].selectionColor.value,
+    },
+  };
+}
+
+export function backgroundColor(
+  theme: Theme,
+  name: keyof Theme["backgroundColor"],
+  state?: keyof BackgroundColor
+): Color {
+  return theme.backgroundColor[name][state || "base"].value;
+}
+
+export function shadow(theme) {
+  return {
+    blur: 16,
+    color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
+    offset: [0, 2],
+  };
+}

styles/core.ts 🔗

@@ -0,0 +1,38 @@
+export type Color = string;
+
+export default {
+  fontFamily: {
+    sans: "Zed Sans",
+    mono: "Zed Mono",
+  },
+  fontSize: {
+    "3xs": {
+      value: "8",
+      type: "fontSizes",
+    },
+    "2xs": {
+      value: "10",
+      type: "fontSizes",
+    },
+    xs: {
+      value: "12",
+      type: "fontSizes",
+    },
+    sm: {
+      value: "14",
+      type: "fontSizes",
+    },
+    md: {
+      value: "16",
+      type: "fontSizes",
+    },
+    lg: {
+      value: "18",
+      type: "fontSizes",
+    },
+    xl: {
+      value: "20",
+      type: "fontSizes",
+    },
+  },
+};

styles/package-lock.json 🔗

@@ -0,0 +1,28 @@
+{
+  "name": "styles",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "styles",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "chroma-js": "^2.4.2"
+      }
+    },
+    "node_modules/chroma-js": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+      "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+    }
+  },
+  "dependencies": {
+    "chroma-js": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+      "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+    }
+  }
+}

styles/package.json 🔗

@@ -0,0 +1,14 @@
+{
+  "name": "styles",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "chroma-js": "^2.4.2"
+  }
+}

styles/selector-modal.ts 🔗

@@ -0,0 +1,59 @@
+import { backgroundColor, border, player, shadow, text } from "./components";
+import Theme from "./theme";
+
+export function selectorModal(theme: Theme): Object {
+  const item = {
+    padding: {
+      bottom: 4,
+      left: 16,
+      right: 16,
+      top: 4,
+    },
+    cornerRadius: 6,
+    text: text(theme, "sans", "secondary"),
+    highlightText: text(theme, "sans", "feature", { weight: "bold" }),
+  };
+
+  const activeItem = {
+    ...item,
+    background: backgroundColor(theme, 500, "active"),
+    text: text(theme, "sans", "primary"),
+  };
+
+  return {
+    background: backgroundColor(theme, 500),
+    cornerRadius: 6,
+    padding: 8,
+    item,
+    activeItem,
+    border: border(theme, "primary"),
+    empty: {
+      text: text(theme, "sans", "muted"),
+      padding: {
+        bottom: 4,
+        left: 16,
+        right: 16,
+        top: 8,
+      },
+    },
+    inputEditor: {
+      background: backgroundColor(theme, 300),
+      corner_radius: 6,
+      placeholderText: text(theme, "sans", "placeholder"),
+      selection: player(theme, 1).selection,
+      text: text(theme, "mono", "primary"),
+      border: border(theme, "primary"),
+      padding: {
+        bottom: 7,
+        left: 16,
+        right: 16,
+        top: 7,
+      },
+    },
+    margin: {
+      bottom: 52,
+      top: 52,
+    },
+    shadow: shadow(theme),
+  };
+}

styles/theme.ts 🔗

@@ -0,0 +1,121 @@
+export type Color = string;
+export type Weight =
+  | "thin"
+  | "extra_light"
+  | "light"
+  | "normal"
+  | "medium"
+  | "semibold"
+  | "bold"
+  | "extra_bold"
+  | "black";
+
+interface SyntaxHighlightStyle {
+  color: { value: Color };
+  weight: { value: Weight };
+}
+
+interface Player {
+  baseColor: {
+    value: Color;
+  };
+  cursorColor: {
+    value: Color;
+  };
+  selectionColor: {
+    value: Color;
+  };
+  borderColor: {
+    value: Color;
+  };
+}
+
+export interface BackgroundColor {
+  base: {
+    value: Color;
+  };
+  hover: {
+    value: Color;
+  };
+  active: {
+    value: Color;
+  };
+  focused: {
+    value: Color;
+  };
+}
+
+export default interface Theme {
+  backgroundColor: {
+    100: BackgroundColor;
+    300: BackgroundColor;
+    500: BackgroundColor;
+  };
+  borderColor: {
+    primary: {
+      value: Color;
+    };
+    secondary: {
+      value: Color;
+    };
+    muted: {
+      value: Color;
+    };
+    focused: {
+      value: Color;
+    };
+    active: {
+      value: Color;
+    };
+  };
+  textColor: {
+    primary: {
+      value: Color;
+    };
+    secondary: {
+      value: Color;
+    };
+    muted: {
+      value: Color;
+    };
+    placeholder: {
+      value: Color;
+    };
+    active: {
+      value: Color;
+    };
+    feature: {
+      value: Color;
+    };
+  };
+  syntax: {
+    primary: SyntaxHighlightStyle;
+    comment: SyntaxHighlightStyle;
+    punctuation: SyntaxHighlightStyle;
+    constant: SyntaxHighlightStyle;
+    keyword: SyntaxHighlightStyle;
+    function: SyntaxHighlightStyle;
+    type: SyntaxHighlightStyle;
+    variant: SyntaxHighlightStyle;
+    property: SyntaxHighlightStyle;
+    enum: SyntaxHighlightStyle;
+    operator: SyntaxHighlightStyle;
+    string: SyntaxHighlightStyle;
+    number: SyntaxHighlightStyle;
+    boolean: SyntaxHighlightStyle;
+    predictive: SyntaxHighlightStyle;
+  };
+  player: {
+    1: Player;
+    2: Player;
+    3: Player;
+    4: Player;
+    5: Player;
+    6: Player;
+    7: Player;
+    8: Player;
+  };
+  shadowAlpha: {
+    value: number;
+  };
+}