Style cleanup for channels panel

Nate Butler created

Change summary

styles/src/component/button.ts        | 122 +++++++++++++++++++++++++++-
styles/src/component/icon_button.ts   |  36 +++++---
styles/src/component/label_button.ts  |  78 ++++++++++++++++++
styles/src/component/text_button.ts   |   8 
styles/src/element/index.ts           |   4 
styles/src/element/toggle.ts          |   2 
styles/src/style_tree/collab_panel.ts |  36 ++++----
7 files changed, 241 insertions(+), 45 deletions(-)

Detailed changes

styles/src/component/button.ts 🔗

@@ -1,6 +1,118 @@
-export const ButtonVariant = {
-    Default: 'default',
-    Ghost: 'ghost'
-} as const
+import { font_sizes, useTheme } from "../common"
+import { Layer, Theme } from "../theme"
+import { TextStyle, background } from "../style_tree/components"
 
-export type Variant = typeof ButtonVariant[keyof typeof ButtonVariant]
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace Button {
+    export type Options = {
+        layer: Layer,
+        background: keyof Theme["lowest"]
+        color: keyof Theme["lowest"]
+        variant: Button.Variant
+        size: Button.Size
+        shape: Button.Shape
+        margin: {
+            top?: number
+            bottom?: number
+            left?: number
+            right?: number
+        },
+        states: {
+            enabled?: boolean,
+            hovered?: boolean,
+            pressed?: boolean,
+            focused?: boolean,
+            disabled?: boolean,
+        }
+    }
+
+    export type ToggleableOptions = Options & {
+        active_background: keyof Theme["lowest"]
+        active_color: keyof Theme["lowest"]
+    }
+
+    /** Padding added to each side of a Shape.Rectangle button */
+    export const RECTANGLE_PADDING = 2
+    export const FONT_SIZE = font_sizes.sm
+    export const ICON_SIZE = 14
+    export const CORNER_RADIUS = 6
+
+    export const variant = {
+        Default: 'filled',
+        Outline: 'outline',
+        Ghost: 'ghost'
+    } as const
+
+    export type Variant = typeof variant[keyof typeof variant]
+
+    export const shape = {
+        Rectangle: 'rectangle',
+        Square: 'square'
+    } as const
+
+    export type Shape = typeof shape[keyof typeof shape]
+
+    export const size = {
+        Small: "sm",
+        Medium: "md"
+    } as const
+
+    export type Size = typeof size[keyof typeof size]
+
+    export type BaseStyle = {
+        corder_radius: number
+        background: string | null
+        padding: {
+            top: number
+            bottom: number
+            left: number
+            right: number
+        },
+        margin: Button.Options['margin']
+        button_height: number
+    }
+
+    export type LabelButtonStyle = BaseStyle & TextStyle
+    // export type IconButtonStyle = ButtonStyle
+
+    export const button_base = (
+        options: Partial<Button.Options> = {
+            variant: Button.variant.Default,
+            shape: Button.shape.Rectangle,
+            states: {
+                hovered: true,
+                pressed: true
+            }
+        }
+    ): BaseStyle => {
+        const theme = useTheme()
+
+        const layer = options.layer ?? theme.middle
+        const color = options.color ?? "base"
+        const background_color = options.variant === Button.variant.Ghost ? null : background(layer, options.background ?? color)
+
+        const m = {
+            top: options.margin?.top ?? 0,
+            bottom: options.margin?.bottom ?? 0,
+            left: options.margin?.left ?? 0,
+            right: options.margin?.right ?? 0,
+        }
+        const size = options.size || Button.size.Medium
+        const padding = 2
+
+        const base: BaseStyle = {
+            background: background_color,
+            corder_radius: Button.CORNER_RADIUS,
+            padding: {
+                top: padding,
+                bottom: padding,
+                left: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding,
+                right: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding
+            },
+            margin: m,
+            button_height: 16,
+        }
+
+        return base
+    }
+}

styles/src/component/icon_button.ts 🔗

@@ -1,7 +1,7 @@
 import { interactive, toggleable } from "../element"
 import { background, foreground } from "../style_tree/components"
-import { useTheme, Theme } from "../theme"
-import { ButtonVariant, Variant } from "./button"
+import { useTheme, Theme, Layer } from "../theme"
+import { Button } from "./button"
 
 export type Margin = {
     top: number
@@ -17,19 +17,24 @@ interface IconButtonOptions {
     | Theme["highest"]
     color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
-    variant?: Variant
+    variant?: Button.Variant
+    size?: Button.Size
 }
 
 type ToggleableIconButtonOptions = IconButtonOptions & {
     active_color?: keyof Theme["lowest"]
+    active_layer?: Layer
 }
 
-export function icon_button({ color, margin, layer, variant = ButtonVariant.Default }: IconButtonOptions) {
+export function icon_button({ color, margin, layer, variant, size }: IconButtonOptions = {
+    variant: Button.variant.Default,
+    size: Button.size.Medium,
+}) {
     const theme = useTheme()
 
     if (!color) color = "base"
 
-    const background_color = variant === ButtonVariant.Ghost ? null : background(layer ?? theme.lowest, color)
+    const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color)
 
     const m = {
         top: margin?.top ?? 0,
@@ -38,15 +43,17 @@ export function icon_button({ color, margin, layer, variant = ButtonVariant.Defa
         right: margin?.right ?? 0,
     }
 
+    const padding = {
+        top: size === Button.size.Small ? 0 : 2,
+        bottom: size === Button.size.Small ? 0 : 2,
+        left: size === Button.size.Small ? 0 : 4,
+        right: size === Button.size.Small ? 0 : 4,
+    }
+
     return interactive({
         base: {
             corner_radius: 6,
-            padding: {
-                top: 2,
-                bottom: 2,
-                left: 4,
-                right: 4,
-            },
+            padding: padding,
             margin: m,
             icon_width: 14,
             icon_height: 14,
@@ -72,17 +79,18 @@ export function icon_button({ color, margin, layer, variant = ButtonVariant.Defa
 
 export function toggleable_icon_button(
     theme: Theme,
-    { color, active_color, margin, variant }: ToggleableIconButtonOptions
+    { color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions
 ) {
     if (!color) color = "base"
 
     return toggleable({
         state: {
-            inactive: icon_button({ color, margin, variant }),
+            inactive: icon_button({ color, margin, variant, size }),
             active: icon_button({
                 color: active_color ? active_color : color,
                 margin,
-                layer: theme.middle,
+                layer: active_layer,
+                size
             }),
         },
     })

styles/src/component/label_button.ts 🔗

@@ -0,0 +1,78 @@
+import { Interactive, interactive, toggleable, Toggleable } from "../element"
+import { TextStyle, background, text } from "../style_tree/components"
+import { useTheme } from "../theme"
+import { Button } from "./button"
+
+type LabelButtonStyle = {
+    corder_radius: number
+    background: string | null
+    padding: {
+        top: number
+        bottom: number
+        left: number
+        right: number
+    },
+    margin: Button.Options['margin']
+    button_height: number
+} & TextStyle
+
+/** Styles an Interactive&lt;ContainedText> */
+export function label_button_style(
+    options: Partial<Button.Options> = {
+        variant: Button.variant.Default,
+        shape: Button.shape.Rectangle,
+        states: {
+            hovered: true,
+            pressed: true
+        }
+    }
+): Interactive<LabelButtonStyle> {
+    const theme = useTheme()
+
+    const base = Button.button_base(options)
+    const layer = options.layer ?? theme.middle
+    const color = options.color ?? "base"
+
+    const default_state = {
+        ...base,
+        ...text(layer ?? theme.lowest, "sans", color),
+        font_size: Button.FONT_SIZE,
+    }
+
+    return interactive({
+        base: default_state,
+        state: {
+            hovered: {
+                background: background(layer, options.background ?? color, "hovered")
+            },
+            clicked: {
+                background: background(layer, options.background ?? color, "pressed")
+            }
+        }
+    })
+}
+
+/** Styles an Toggleable&lt;Interactive&lt;ContainedText>> */
+export function toggle_label_button_style(
+    options: Partial<Button.ToggleableOptions> = {
+        variant: Button.variant.Default,
+        shape: Button.shape.Rectangle,
+        states: {
+            hovered: true,
+            pressed: true
+        }
+    }
+): Toggleable<Interactive<LabelButtonStyle>> {
+    const activeOptions = {
+        ...options,
+        color: options.active_color || options.color,
+        background: options.active_background || options.background
+    }
+
+    return toggleable({
+        state: {
+            inactive: label_button_style(options),
+            active: label_button_style(activeOptions),
+        },
+    })
+}

styles/src/component/text_button.ts 🔗

@@ -6,7 +6,7 @@ import {
     text,
 } from "../style_tree/components"
 import { useTheme, Theme } from "../theme"
-import { ButtonVariant, Variant } from "./button"
+import { Button } from "./button"
 import { Margin } from "./icon_button"
 
 interface TextButtonOptions {
@@ -14,7 +14,7 @@ interface TextButtonOptions {
     | Theme["lowest"]
     | Theme["middle"]
     | Theme["highest"]
-    variant?: Variant
+    variant?: Button.Variant
     color?: keyof Theme["lowest"]
     margin?: Partial<Margin>
     text_properties?: TextProperties
@@ -25,7 +25,7 @@ type ToggleableTextButtonOptions = TextButtonOptions & {
 }
 
 export function text_button({
-    variant = ButtonVariant.Default,
+    variant = Button.variant.Default,
     color,
     layer,
     margin,
@@ -34,7 +34,7 @@ export function text_button({
     const theme = useTheme()
     if (!color) color = "base"
 
-    const background_color = variant === ButtonVariant.Ghost ? null : background(layer ?? theme.lowest, color)
+    const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color)
 
     const text_options: TextProperties = {
         size: "xs",

styles/src/element/index.ts 🔗

@@ -1,4 +1,4 @@
 import { interactive, Interactive } from "./interactive"
-import { toggleable } from "./toggle"
+import { toggleable, Toggleable } from "./toggle"
 
-export { interactive, Interactive, toggleable }
+export { interactive, Interactive, toggleable, Toggleable }

styles/src/element/toggle.ts 🔗

@@ -3,7 +3,7 @@ import { DeepPartial } from "utility-types"
 
 type ToggleState = "inactive" | "active"
 
-type Toggleable<T> = Record<ToggleState, T>
+export type Toggleable<T> = Record<ToggleState, T>
 
 export const NO_INACTIVE_OR_BASE_ERROR =
     "A toggleable object must have an inactive state, or a base property."

styles/src/style_tree/collab_panel.ts 🔗

@@ -9,7 +9,7 @@ import { interactive, toggleable } from "../element"
 import { useTheme } from "../theme"
 import collab_modals from "./collab_modals"
 import { text_button } from "../component/text_button"
-import { toggleable_icon_button } from "../component/icon_button"
+import { icon_button, toggleable_icon_button } from "../component/icon_button"
 import { indicator } from "../component/indicator"
 
 export default function contacts_panel(): any {
@@ -27,7 +27,7 @@ export default function contacts_panel(): any {
         color: foreground(layer, "on"),
         icon_width: 14,
         button_width: 16,
-        corner_radius: 8,
+        corner_radius: 8
     }
 
     const project_row = {
@@ -62,8 +62,9 @@ export default function contacts_panel(): any {
     }
 
     const header_icon_button = toggleable_icon_button(theme, {
-        layer: theme.middle,
         variant: "ghost",
+        size: "sm",
+        active_layer: theme.lowest,
     })
 
     const subheader_row = toggleable({
@@ -87,8 +88,8 @@ export default function contacts_panel(): any {
         state: {
             active: {
                 default: {
-                    ...text(layer, "ui_sans", "active", { size: "sm" }),
-                    background: background(layer, "active"),
+                    ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                    background: background(theme.lowest),
                 },
                 clicked: {
                     background: background(layer, "pressed"),
@@ -140,8 +141,8 @@ export default function contacts_panel(): any {
             },
             active: {
                 default: {
-                    ...text(layer, "ui_sans", "active", { size: "sm" }),
-                    background: background(layer, "active"),
+                    ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                    background: background(theme.lowest),
                 },
                 clicked: {
                     background: background(layer, "pressed"),
@@ -221,8 +222,8 @@ export default function contacts_panel(): any {
                 },
                 active: {
                     default: {
-                        ...text(layer, "ui_sans", "active", { size: "sm" }),
-                        background: background(layer, "active"),
+                        ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                        background: background(theme.lowest),
                     },
                     clicked: {
                         background: background(layer, "pressed"),
@@ -271,8 +272,8 @@ export default function contacts_panel(): any {
                 },
                 active: {
                     default: {
-                        ...text(layer, "ui_sans", "active", { size: "sm" }),
-                        background: background(layer, "active"),
+                        ...text(theme.lowest, "ui_sans", { size: "sm" }),
+                        background: background(theme.lowest),
                     },
                     clicked: {
                         background: background(layer, "pressed"),
@@ -306,13 +307,10 @@ export default function contacts_panel(): any {
             },
         },
         contact_button_spacing: NAME_MARGIN,
-        contact_button: interactive({
-            base: { ...contact_button },
-            state: {
-                hovered: {
-                    background: background(layer, "hovered"),
-                },
-            },
+        contact_button: icon_button({
+            variant: "ghost",
+            color: "variant",
+            size: "sm",
         }),
         disabled_button: {
             ...contact_button,
@@ -364,7 +362,7 @@ export default function contacts_panel(): any {
             }),
             state: {
                 active: {
-                    default: { background: background(layer, "active") },
+                    default: { background: background(theme.lowest) },
                 },
             },
         }),