diff --git a/styles/src/component/button.ts b/styles/src/component/button.ts index ba72851768..3b554ae37a 100644 --- a/styles/src/component/button.ts +++ b/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 = { + 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 + } +} diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index ae3fa763e7..1a2d0bcec4 100644 --- a/styles/src/component/icon_button.ts +++ b/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 - 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 }), }, }) diff --git a/styles/src/component/label_button.ts b/styles/src/component/label_button.ts new file mode 100644 index 0000000000..3f1c54a7f6 --- /dev/null +++ b/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<ContainedText> */ +export function label_button_style( + options: Partial = { + variant: Button.variant.Default, + shape: Button.shape.Rectangle, + states: { + hovered: true, + pressed: true + } + } +): Interactive { + 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<Interactive<ContainedText>> */ +export function toggle_label_button_style( + options: Partial = { + variant: Button.variant.Default, + shape: Button.shape.Rectangle, + states: { + hovered: true, + pressed: true + } + } +): Toggleable> { + 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), + }, + }) +} diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 2be2dd19cb..b911cd5b77 100644 --- a/styles/src/component/text_button.ts +++ b/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 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", diff --git a/styles/src/element/index.ts b/styles/src/element/index.ts index 81c911c7bd..d41b4e2cc3 100644 --- a/styles/src/element/index.ts +++ b/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 } diff --git a/styles/src/element/toggle.ts b/styles/src/element/toggle.ts index c3cde46d65..25217444da 100644 --- a/styles/src/element/toggle.ts +++ b/styles/src/element/toggle.ts @@ -3,7 +3,7 @@ import { DeepPartial } from "utility-types" type ToggleState = "inactive" | "active" -type Toggleable = Record +export type Toggleable = Record export const NO_INACTIVE_OR_BASE_ERROR = "A toggleable object must have an inactive state, or a base property." diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index c65887e17c..61c96ad75a 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/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) }, }, }, }),