Use theme store to pass color_scheme directly to components (#2675)

This PR adds a theme store to allow components to directly access the
theme without requiring it to be passed down as props every time it is
used.

So before, you might need to do something like `text(theme, "variant",
"hovered")`, you could now just call `text("variant", "hovered")`.

This also means that style_trees don't need to be called with a theme
either:

```ts
export default function app(): any {
    const theme = useTheme()

    return {
        meta: {
            name: theme.name,
            is_light: theme.is_light,
        },
        command_palette: command_palette(),
        contact_notification: contact_notification(),
        // etc...
    }
}
```

We do this by creating a zustand store to store the theme, and allow it
to be accessed with `useThemeStore.getState().theme`.

```ts
import { create } from "zustand"
import { ColorScheme } from "./color_scheme"

type ThemeState = {
    theme: ColorScheme | undefined
    setTheme: (theme: ColorScheme) => void
}

export const useThemeStore = create<ThemeState>((set) => ({
    theme: undefined,
    setTheme: (theme) => set(() => ({ theme })),
}))

export const useTheme = (): ColorScheme => {
    const { theme } = useThemeStore.getState()

    if (!theme) throw new Error("Tried to use theme before it was loaded")

    return theme
}
```

Release Notes:

- N/A (No public facing changes)
This commit is contained in:
Nate Butler 2023-07-04 00:37:45 -04:00 committed by GitHub
commit 0b4c5db5e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 311 additions and 158 deletions

View file

@ -27,7 +27,8 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.5", "typescript": "^5.1.5",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"vitest": "^0.32.0" "vitest": "^0.32.0",
"zustand": "^4.3.8"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -2595,6 +2596,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"peer": true
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -2706,6 +2713,18 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/loupe": { "node_modules/loupe": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
@ -3292,6 +3311,18 @@
} }
] ]
}, },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -4025,6 +4056,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/utility-types": { "node_modules/utility-types": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
@ -4305,6 +4344,29 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zustand": {
"version": "4.3.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
"integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
} }
} }
} }

View file

@ -16,21 +16,22 @@
"@tokens-studio/types": "^0.2.3", "@tokens-studio/types": "^0.2.3",
"@types/chroma-js": "^2.4.0", "@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1", "@types/node": "^18.14.1",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitest/coverage-v8": "^0.32.0",
"ayu": "^8.0.1", "ayu": "^8.0.1",
"chroma-js": "^2.4.2", "chroma-js": "^2.4.2",
"deepmerge": "^4.3.0", "deepmerge": "^4.3.0",
"eslint": "^8.43.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"json-schema-to-typescript": "^13.0.2", "json-schema-to-typescript": "^13.0.2",
"toml": "^3.0.0", "toml": "^3.0.0",
"ts-deepmerge": "^6.0.3", "ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.5",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"vitest": "^0.32.0", "vitest": "^0.32.0",
"@typescript-eslint/eslint-plugin": "^5.60.1", "zustand": "^4.3.8"
"@typescript-eslint/parser": "^5.60.1",
"@vitest/coverage-v8": "^0.32.0",
"eslint": "^8.43.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"typescript": "^5.1.5"
} }
} }

View file

@ -4,6 +4,7 @@ import * as path from "path"
import app from "./style_tree/app" import app from "./style_tree/app"
import { ColorScheme, create_color_scheme } from "./theme/color_scheme" import { ColorScheme, create_color_scheme } from "./theme/color_scheme"
import { themes } from "./themes" import { themes } from "./themes"
import { useThemeStore } from "./theme"
const assets_directory = `${__dirname}/../../assets` const assets_directory = `${__dirname}/../../assets`
const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes")) const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
@ -20,10 +21,17 @@ function clear_themes(theme_directory: string) {
} }
} }
const all_themes: ColorScheme[] = themes.map((theme) =>
create_color_scheme(theme)
)
function write_themes(themes: ColorScheme[], output_directory: string) { function write_themes(themes: ColorScheme[], output_directory: string) {
clear_themes(output_directory) clear_themes(output_directory)
for (const color_scheme of themes) { for (const color_scheme of themes) {
const style_tree = app(color_scheme) const { setTheme } = useThemeStore.getState()
setTheme(color_scheme)
const style_tree = app()
const style_tree_json = JSON.stringify(style_tree, null, 2) const style_tree_json = JSON.stringify(style_tree, null, 2)
const temp_path = path.join(temp_directory, `${color_scheme.name}.json`) const temp_path = path.join(temp_directory, `${color_scheme.name}.json`)
const out_path = path.join( const out_path = path.join(
@ -36,8 +44,4 @@ function write_themes(themes: ColorScheme[], output_directory: string) {
} }
} }
const all_themes: ColorScheme[] = themes.map((theme) =>
create_color_scheme(theme)
)
write_themes(all_themes, `${assets_directory}/themes`) write_themes(all_themes, `${assets_directory}/themes`)

View file

@ -60,7 +60,7 @@ function write_tokens(themes: ColorScheme[], tokens_directory: string) {
for (const theme of themes) { for (const theme of themes) {
const file_name = slugify(theme.name) + ".json" const file_name = slugify(theme.name) + ".json"
const tokens = theme_tokens(theme) const tokens = theme_tokens()
const tokens_json = JSON.stringify(tokens, null, 2) const tokens_json = JSON.stringify(tokens, null, 2)
const out_path = path.join(tokens_directory, file_name) const out_path = path.join(tokens_directory, file_name)
fs.writeFileSync(out_path, tokens_json, { mode: 0o644 }) fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })

View file

@ -1,6 +1,6 @@
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { background, foreground } from "../style_tree/components" import { background, foreground } from "../style_tree/components"
import { ColorScheme } from "../theme/color_scheme" import { useTheme, ColorScheme } from "../theme"
export type Margin = { export type Margin = {
top: number top: number
@ -22,10 +22,9 @@ type ToggleableIconButtonOptions = IconButtonOptions & {
active_color?: keyof ColorScheme["lowest"] active_color?: keyof ColorScheme["lowest"]
} }
export function icon_button( export function icon_button({ color, margin, layer }: IconButtonOptions) {
theme: ColorScheme, const theme = useTheme()
{ color, margin, layer }: IconButtonOptions
) {
if (!color) color = "base" if (!color) color = "base"
const m = { const m = {
@ -75,8 +74,8 @@ export function toggleable_icon_button(
return toggleable({ return toggleable({
state: { state: {
inactive: icon_button(theme, { color, margin }), inactive: icon_button({ color, margin }),
active: icon_button(theme, { active: icon_button({
color: active_color ? active_color : color, color: active_color ? active_color : color,
margin, margin,
layer: theme.middle, layer: theme.middle,

View file

@ -5,7 +5,7 @@ import {
foreground, foreground,
text, text,
} from "../style_tree/components" } from "../style_tree/components"
import { ColorScheme } from "../theme/color_scheme" import { useTheme, ColorScheme } from "../theme"
import { Margin } from "./icon_button" import { Margin } from "./icon_button"
interface TextButtonOptions { interface TextButtonOptions {
@ -22,10 +22,13 @@ type ToggleableTextButtonOptions = TextButtonOptions & {
active_color?: keyof ColorScheme["lowest"] active_color?: keyof ColorScheme["lowest"]
} }
export function text_button( export function text_button({
theme: ColorScheme, color,
{ color, layer, margin, text_properties }: TextButtonOptions layer,
) { margin,
text_properties,
}: TextButtonOptions) {
const theme = useTheme()
if (!color) color = "base" if (!color) color = "base"
const text_options: TextProperties = { const text_options: TextProperties = {
@ -79,8 +82,8 @@ export function toggleable_text_button(
return toggleable({ return toggleable({
state: { state: {
inactive: text_button(theme, { color, margin }), inactive: text_button({ color, margin }),
active: text_button(theme, { active: text_button({
color: active_color ? active_color : color, color: active_color ? active_color : color,
margin, margin,
layer: theme.middle, layer: theme.middle,

View file

@ -17,45 +17,47 @@ import terminal from "./terminal"
import contact_list from "./contact_list" import contact_list from "./contact_list"
import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
import incoming_call_notification from "./incoming_call_notification" import incoming_call_notification from "./incoming_call_notification"
import { ColorScheme } from "../theme/color_scheme"
import welcome from "./welcome" import welcome from "./welcome"
import copilot from "./copilot" import copilot from "./copilot"
import assistant from "./assistant" import assistant from "./assistant"
import { titlebar } from "./titlebar" import { titlebar } from "./titlebar"
import editor from "./editor" import editor from "./editor"
import feedback from "./feedback" import feedback from "./feedback"
import { useTheme } from "../common"
export default function app(): any {
const theme = useTheme()
export default function app(theme: ColorScheme): any {
return { return {
meta: { meta: {
name: theme.name, name: theme.name,
is_light: theme.is_light, is_light: theme.is_light,
}, },
command_palette: command_palette(theme), command_palette: command_palette(),
contact_notification: contact_notification(theme), contact_notification: contact_notification(),
project_shared_notification: project_shared_notification(theme), project_shared_notification: project_shared_notification(),
incoming_call_notification: incoming_call_notification(theme), incoming_call_notification: incoming_call_notification(),
picker: picker(theme), picker: picker(),
workspace: workspace(theme), workspace: workspace(),
titlebar: titlebar(theme), titlebar: titlebar(),
copilot: copilot(theme), copilot: copilot(),
welcome: welcome(theme), welcome: welcome(),
context_menu: context_menu(theme), context_menu: context_menu(),
editor: editor(theme), editor: editor(),
project_diagnostics: project_diagnostics(theme), project_diagnostics: project_diagnostics(),
project_panel: project_panel(theme), project_panel: project_panel(),
contacts_popover: contacts_popover(theme), contacts_popover: contacts_popover(),
contact_finder: contact_finder(theme), contact_finder: contact_finder(),
contact_list: contact_list(theme), contact_list: contact_list(),
toolbar_dropdown_menu: toolbar_dropdown_menu(theme), toolbar_dropdown_menu: toolbar_dropdown_menu(),
search: search(theme), search: search(),
shared_screen: shared_screen(theme), shared_screen: shared_screen(),
update_notification: update_notification(theme), update_notification: update_notification(),
simple_message_notification: simple_message_notification(theme), simple_message_notification: simple_message_notification(),
tooltip: tooltip(theme), tooltip: tooltip(),
terminal: terminal(theme), terminal: terminal(),
assistant: assistant(theme), assistant: assistant(),
feedback: feedback(theme), feedback: feedback(),
color_scheme: { color_scheme: {
...theme, ...theme,
players: Object.values(theme.players), players: Object.values(theme.players),

View file

@ -1,8 +1,10 @@
import { ColorScheme } from "../theme/color_scheme"
import { text, border, background, foreground } from "./components" import { text, border, background, foreground } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
import { useTheme } from "../theme"
export default function assistant(): any {
const theme = useTheme()
export default function assistant(theme: ColorScheme): any {
return { return {
container: { container: {
background: background(theme.highest), background: background(theme.highest),

View file

@ -1,9 +1,11 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { text, background } from "./components" import { text, background } from "./components"
import { toggleable } from "../element" import { toggleable } from "../element"
import { useTheme } from "../theme"
export default function command_palette(): any {
const theme = useTheme()
export default function command_palette(theme: ColorScheme): any {
const key = toggleable({ const key = toggleable({
base: { base: {
text: text(theme.highest, "mono", "variant", "default", { text: text(theme.highest, "mono", "variant", "default", {

View file

@ -1,8 +1,10 @@
import picker from "./picker" import picker from "./picker"
import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { useTheme } from "../theme"
export default function contact_finder(): any {
const theme = useTheme()
export default function contact_finder(theme: ColorScheme): any {
const side_margin = 6 const side_margin = 6
const contact_button = { const contact_button = {
background: background(theme.middle, "variant"), background: background(theme.middle, "variant"),
@ -12,7 +14,7 @@ export default function contact_finder(theme: ColorScheme): any {
corner_radius: 8, corner_radius: 8,
} }
const picker_style = picker(theme) const picker_style = picker()
const picker_input = { const picker_input = {
background: background(theme.middle, "on"), background: background(theme.middle, "on"),
corner_radius: 6, corner_radius: 6,

View file

@ -1,4 +1,3 @@
import { ColorScheme } from "../theme/color_scheme"
import { import {
background, background,
border, border,
@ -7,7 +6,10 @@ import {
text, text,
} from "./components" } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
export default function contacts_panel(theme: ColorScheme): any { import { useTheme } from "../theme"
export default function contacts_panel(): any {
const theme = useTheme()
const name_margin = 8 const name_margin = 8
const side_padding = 12 const side_padding = 12

View file

@ -1,8 +1,10 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, foreground, text } from "./components" import { background, foreground, text } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
import { useTheme } from "../theme"
export default function contact_notification(): any {
const theme = useTheme()
export default function contact_notification(theme: ColorScheme): any {
const avatar_size = 12 const avatar_size = 12
const header_padding = 8 const header_padding = 8

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background, border } from "./components" import { background, border } from "./components"
export default function contacts_popover(theme: ColorScheme): any { export default function contacts_popover(): any {
const theme = useTheme()
return { return {
background: background(theme.middle), background: background(theme.middle),
corner_radius: 6, corner_radius: 6,

View file

@ -1,8 +1,10 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, border, border_color, text } from "./components" import { background, border, border_color, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme"
export default function context_menu(): any {
const theme = useTheme()
export default function context_menu(theme: ColorScheme): any {
return { return {
background: background(theme.middle), background: background(theme.middle),
corner_radius: 10, corner_radius: 10,

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, svg, text } from "./components" import { background, border, foreground, svg, text } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
export default function copilot(theme: ColorScheme): any { import { useTheme } from "../theme"
export default function copilot(): any {
const theme = useTheme()
const content_width = 264 const content_width = 264
const cta_button = const cta_button =

View file

@ -1,5 +1,5 @@
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { ColorScheme, Layer, StyleSets } from "../theme/color_scheme" import { Layer, StyleSets } from "../theme/color_scheme"
import { import {
background, background,
border, border,
@ -11,8 +11,11 @@ import hover_popover from "./hover_popover"
import { build_syntax } from "../theme/syntax" import { build_syntax } from "../theme/syntax"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme"
export default function editor(): any {
const theme = useTheme()
export default function editor(theme: ColorScheme): any {
const { is_light } = theme const { is_light } = theme
const layer = theme.highest const layer = theme.highest
@ -248,7 +251,7 @@ export default function editor(theme: ColorScheme): any {
invalid_hint_diagnostic: diagnostic(theme.middle, "base"), invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
invalid_information_diagnostic: diagnostic(theme.middle, "base"), invalid_information_diagnostic: diagnostic(theme.middle, "base"),
invalid_warning_diagnostic: diagnostic(theme.middle, "base"), invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
hover_popover: hover_popover(theme), hover_popover: hover_popover(),
link_definition: { link_definition: {
color: syntax.link_uri.color, color: syntax.link_uri.color,
underline: syntax.link_uri.underline, underline: syntax.link_uri.underline,

View file

@ -1,8 +1,10 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
import { useTheme } from "../theme"
export default function feedback(): any {
const theme = useTheme()
export default function feedback(theme: ColorScheme): any {
return { return {
submit_button: interactive({ submit_button: interactive({
base: { base: {

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
export default function hover_popover(theme: ColorScheme): any { export default function hover_popover(): any {
const theme = useTheme()
const base_container = { const base_container = {
background: background(theme.middle), background: background(theme.middle),
corner_radius: 8, corner_radius: 8,

View file

@ -1,9 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
export default function incoming_call_notification( export default function incoming_call_notification(): unknown {
theme: ColorScheme const theme = useTheme()
): unknown {
const avatar_size = 48 const avatar_size = 48
return { return {
window_height: 74, window_height: 74,

View file

@ -1,9 +1,11 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme"
export default function picker(): any {
const theme = useTheme()
export default function picker(theme: ColorScheme): any {
const container = { const container = {
background: background(theme.lowest), background: background(theme.lowest),
border: border(theme.lowest), border: border(theme.lowest),

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background, text } from "./components" import { background, text } from "./components"
export default function project_diagnostics(theme: ColorScheme): any { export default function project_diagnostics(): any {
const theme = useTheme()
return { return {
background: background(theme.highest), background: background(theme.highest),
tab_icon_spacing: 4, tab_icon_spacing: 4,

View file

@ -1,4 +1,3 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { import {
Border, Border,
@ -10,7 +9,10 @@ import {
} from "./components" } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import merge from "ts-deepmerge" import merge from "ts-deepmerge"
export default function project_panel(theme: ColorScheme): any { import { useTheme } from "../theme"
export default function project_panel(): any {
const theme = useTheme()
const { is_light } = theme const { is_light } = theme
type EntryStateProps = { type EntryStateProps = {
@ -65,13 +67,12 @@ export default function project_panel(theme: ColorScheme): any {
const unselected_hovered_style = merge( const unselected_hovered_style = merge(
base_properties, base_properties,
{ background: background(theme.middle, "hovered") }, { background: background(theme.middle, "hovered") },
unselected?.hovered ?? {}, unselected?.hovered ?? {}
) )
const unselected_clicked_style = merge( const unselected_clicked_style = merge(
base_properties, base_properties,
{ background: background(theme.middle, "pressed"), } { background: background(theme.middle, "pressed") },
, unselected?.clicked ?? {}
unselected?.clicked ?? {},
) )
const selected_default_style = merge( const selected_default_style = merge(
base_properties, base_properties,
@ -79,18 +80,15 @@ export default function project_panel(theme: ColorScheme): any {
background: background(theme.lowest), background: background(theme.lowest),
text: text(theme.lowest, "sans", { size: "sm" }), text: text(theme.lowest, "sans", { size: "sm" }),
}, },
selected_style?.default ?? {}, selected_style?.default ?? {}
) )
const selected_hovered_style = merge( const selected_hovered_style = merge(
base_properties, base_properties,
{ {
background: background(theme.lowest, "hovered"), background: background(theme.lowest, "hovered"),
text: text(theme.lowest, "sans", { size: "sm" }), text: text(theme.lowest, "sans", { size: "sm" }),
}, },
selected_style?.hovered ?? {}, selected_style?.hovered ?? {}
) )
const selected_clicked_style = merge( const selected_clicked_style = merge(
base_properties, base_properties,
@ -98,8 +96,7 @@ export default function project_panel(theme: ColorScheme): any {
background: background(theme.lowest, "pressed"), background: background(theme.lowest, "pressed"),
text: text(theme.lowest, "sans", { size: "sm" }), text: text(theme.lowest, "sans", { size: "sm" }),
}, },
selected_style?.clicked ?? {}, selected_style?.clicked ?? {}
) )
return toggleable({ return toggleable({

View file

@ -1,9 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
export default function project_shared_notification( export default function project_shared_notification(): unknown {
theme: ColorScheme const theme = useTheme()
): unknown {
const avatar_size = 48 const avatar_size = 48
return { return {
window_height: 74, window_height: 74,

View file

@ -1,9 +1,11 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme"
export default function search(): any {
const theme = useTheme()
export default function search(theme: ColorScheme): any {
// Search input // Search input
const editor = { const editor = {
background: background(theme.highest), background: background(theme.highest),

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background } from "./components" import { background } from "./components"
export default function sharedScreen(theme: ColorScheme) { export default function sharedScreen() {
const theme = useTheme()
return { return {
background: background(theme.highest), background: background(theme.highest),
} }

View file

@ -1,8 +1,10 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
import { useTheme } from "../theme"
export default function simple_message_notification(): any {
const theme = useTheme()
export default function simple_message_notification(theme: ColorScheme): any {
const header_padding = 8 const header_padding = 8
return { return {

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
export default function status_bar(theme: ColorScheme): any { import { useTheme } from "../common"
export default function status_bar(): any {
const theme = useTheme()
const layer = theme.lowest const layer = theme.lowest
const status_container = { const status_container = {

View file

@ -1,9 +1,11 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { text, border, background, foreground } from "./components" import { text, border, background, foreground } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../common"
export default function tab_bar(): any {
const theme = useTheme()
export default function tab_bar(theme: ColorScheme): any {
const height = 32 const height = 32
const active_layer = theme.highest const active_layer = theme.highest

View file

@ -1,6 +1,8 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
export default function terminal() {
const theme = useTheme()
export default function terminal(theme: ColorScheme) {
/** /**
* Colors are controlled per-cell in the terminal grid. * Colors are controlled per-cell in the terminal grid.
* Cells can be set to any of these more 'theme-capable' colors * Cells can be set to any of these more 'theme-capable' colors

View file

@ -1,7 +1,7 @@
import { ColorScheme } from "../common"
import { icon_button, toggleable_icon_button } from "../component/icon_button" import { icon_button, toggleable_icon_button } from "../component/icon_button"
import { toggleable_text_button } from "../component/text_button" import { toggleable_text_button } from "../component/text_button"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
@ -22,7 +22,9 @@ function build_spacing(
} }
} }
function call_controls(theme: ColorScheme) { function call_controls() {
const theme = useTheme()
const button_height = 18 const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING) const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
@ -69,7 +71,9 @@ function call_controls(theme: ColorScheme) {
* When logged in shows the user's avatar and a chevron, * When logged in shows the user's avatar and a chevron,
* When logged out only shows a chevron. * When logged out only shows a chevron.
*/ */
function user_menu(theme: ColorScheme) { function user_menu() {
const theme = useTheme()
const button_height = 18 const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING) const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
@ -155,7 +159,9 @@ function user_menu(theme: ColorScheme) {
} }
} }
export function titlebar(theme: ColorScheme): any { export function titlebar(): any {
const theme = useTheme()
const avatar_width = 15 const avatar_width = 15
const avatar_outer_width = avatar_width + 4 const avatar_outer_width = avatar_width + 4
const follower_avatar_width = 14 const follower_avatar_width = 14
@ -237,14 +243,14 @@ export function titlebar(theme: ColorScheme): any {
corner_radius: 6, corner_radius: 6,
}, },
leave_call_button: icon_button(theme, { leave_call_button: icon_button({
margin: { margin: {
left: ITEM_SPACING / 2, left: ITEM_SPACING / 2,
right: ITEM_SPACING, right: ITEM_SPACING,
}, },
}), }),
...call_controls(theme), ...call_controls(),
toggle_contacts_button: toggleable_icon_button(theme, { toggle_contacts_button: toggleable_icon_button(theme, {
margin: { margin: {
@ -261,6 +267,6 @@ export function titlebar(theme: ColorScheme): any {
background: foreground(theme.lowest, "accent"), background: foreground(theme.lowest, "accent"),
}, },
share_button: toggleable_text_button(theme, {}), share_button: toggleable_text_button(theme, {}),
user_menu: user_menu(theme), user_menu: user_menu(),
} }
} }

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
export default function dropdown_menu(theme: ColorScheme): any { import { useTheme } from "../theme"
export default function dropdown_menu(): any {
const theme = useTheme()
return { return {
row_height: 30, row_height: 30,
background: background(theme.middle), background: background(theme.middle),

View file

@ -1,7 +1,9 @@
import { ColorScheme } from "../theme/color_scheme" import { useTheme } from "../theme"
import { background, border, text } from "./components" import { background, border, text } from "./components"
export default function tooltip(theme: ColorScheme): any { export default function tooltip(): any {
const theme = useTheme()
return { return {
background: background(theme.middle), background: background(theme.middle),
border: border(theme.middle), border: border(theme.middle),

View file

@ -1,8 +1,10 @@
import { ColorScheme } from "../theme/color_scheme"
import { foreground, text } from "./components" import { foreground, text } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
import { useTheme } from "../theme"
export default function update_notification(): any {
const theme = useTheme()
export default function update_notification(theme: ColorScheme): any {
const header_padding = 8 const header_padding = 8
return { return {

View file

@ -1,4 +1,3 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { import {
border, border,
@ -9,8 +8,11 @@ import {
svg, svg,
} from "./components" } from "./components"
import { interactive } from "../element" import { interactive } from "../element"
import { useTheme } from "../theme"
export default function welcome(): any {
const theme = useTheme()
export default function welcome(theme: ColorScheme): any {
const checkbox_base = { const checkbox_base = {
corner_radius: 4, corner_radius: 4,
padding: { padding: {

View file

@ -1,4 +1,3 @@
import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color" import { with_opacity } from "../theme/color"
import { import {
background, background,
@ -11,9 +10,12 @@ import {
import statusBar from "./status_bar" import statusBar from "./status_bar"
import tabBar from "./tab_bar" import tabBar from "./tab_bar"
import { interactive } from "../element" import { interactive } from "../element"
import { titlebar } from "./titlebar" import { titlebar } from "./titlebar"
export default function workspace(theme: ColorScheme): any { import { useTheme } from "../theme"
export default function workspace(): any {
const theme = useTheme()
const { is_light } = theme const { is_light } = theme
return { return {
@ -85,7 +87,7 @@ export default function workspace(theme: ColorScheme): any {
}, },
leader_border_opacity: 0.7, leader_border_opacity: 0.7,
leader_border_width: 2.0, leader_border_width: 2.0,
tab_bar: tabBar(theme), tab_bar: tabBar(),
modal: { modal: {
margin: { margin: {
bottom: 52, bottom: 52,
@ -123,8 +125,8 @@ export default function workspace(theme: ColorScheme): any {
color: border_color(theme.lowest), color: border_color(theme.lowest),
width: 1, width: 1,
}, },
status_bar: statusBar(theme), status_bar: statusBar(),
titlebar: titlebar(theme), titlebar: titlebar(),
toolbar: { toolbar: {
height: 34, height: 34,
background: background(theme.highest), background: background(theme.highest),

View file

@ -1,3 +1,24 @@
import { create } from "zustand"
import { ColorScheme } from "./color_scheme"
type ThemeState = {
theme: ColorScheme | undefined
setTheme: (theme: ColorScheme) => void
}
export const useThemeStore = create<ThemeState>((set) => ({
theme: undefined,
setTheme: (theme) => set(() => ({ theme })),
}))
export const useTheme = (): ColorScheme => {
const { theme } = useThemeStore.getState()
if (!theme) throw new Error("Tried to use theme before it was loaded")
return theme
}
export * from "./color_scheme" export * from "./color_scheme"
export * from "./ramps" export * from "./ramps"
export * from "./syntax" export * from "./syntax"

View file

@ -15,6 +15,7 @@ import { PlayersToken, players_token } from "./players"
import { color_token } from "./token" import { color_token } from "./token"
import { Syntax } from "../syntax" import { Syntax } from "../syntax"
import editor from "../../style_tree/editor" import editor from "../../style_tree/editor"
import { useTheme } from "@/src/common"
interface ColorSchemeTokens { interface ColorSchemeTokens {
name: SingleOtherToken name: SingleOtherToken
@ -39,12 +40,14 @@ const create_shadow_token = (
} }
} }
const popover_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => { const popover_shadow_token = (): SingleBoxShadowToken => {
const theme = useTheme()
const shadow = theme.popover_shadow const shadow = theme.popover_shadow
return create_shadow_token(shadow, "popover_shadow") return create_shadow_token(shadow, "popover_shadow")
} }
const modal_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => { const modal_shadow_token = (): SingleBoxShadowToken => {
const theme = useTheme()
const shadow = theme.modal_shadow const shadow = theme.modal_shadow
return create_shadow_token(shadow, "modal_shadow") return create_shadow_token(shadow, "modal_shadow")
} }
@ -68,13 +71,15 @@ function syntax_highlight_style_color_tokens(
}, {} as ThemeSyntaxColorTokens) }, {} as ThemeSyntaxColorTokens)
} }
const syntax_tokens = (theme: ColorScheme): ColorSchemeTokens["syntax"] => { const syntax_tokens = (): ColorSchemeTokens["syntax"] => {
const syntax = editor(theme).syntax const syntax = editor().syntax
return syntax_highlight_style_color_tokens(syntax) return syntax_highlight_style_color_tokens(syntax)
} }
export function theme_tokens(theme: ColorScheme): ColorSchemeTokens { export function theme_tokens(): ColorSchemeTokens {
const theme = useTheme()
return { return {
name: { name: {
name: "themeName", name: "themeName",
@ -89,9 +94,9 @@ export function theme_tokens(theme: ColorScheme): ColorSchemeTokens {
lowest: layer_token(theme.lowest, "lowest"), lowest: layer_token(theme.lowest, "lowest"),
middle: layer_token(theme.middle, "middle"), middle: layer_token(theme.middle, "middle"),
highest: layer_token(theme.highest, "highest"), highest: layer_token(theme.highest, "highest"),
popover_shadow: popover_shadow_token(theme), popover_shadow: popover_shadow_token(),
modal_shadow: modal_shadow_token(theme), modal_shadow: modal_shadow_token(),
players: players_token(theme), players: players_token(),
syntax: syntax_tokens(theme), syntax: syntax_tokens(),
} }
} }

View file

@ -1,12 +1,14 @@
import { SingleColorToken } from "@tokens-studio/types" import { SingleColorToken } from "@tokens-studio/types"
import { color_token } from "./token" import { color_token } from "./token"
import { ColorScheme, Players } from "../color_scheme" import { Players } from "../color_scheme"
import { useTheme } from "@/src/common"
export type PlayerToken = Record<"selection" | "cursor", SingleColorToken> export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
export type PlayersToken = Record<keyof Players, PlayerToken> export type PlayersToken = Record<keyof Players, PlayerToken>
function build_player_token(theme: ColorScheme, index: number): PlayerToken { function build_player_token(index: number): PlayerToken {
const theme = useTheme()
const player_number = index.toString() as keyof Players const player_number = index.toString() as keyof Players
return { return {
@ -21,13 +23,15 @@ function build_player_token(theme: ColorScheme, index: number): PlayerToken {
} }
} }
export const players_token = (theme: ColorScheme): PlayersToken => ({ export const players_token = (): PlayersToken => {
"0": build_player_token(theme, 0), return {
"1": build_player_token(theme, 1), "0": build_player_token(0),
"2": build_player_token(theme, 2), "1": build_player_token(1),
"3": build_player_token(theme, 3), "2": build_player_token(2),
"4": build_player_token(theme, 4), "3": build_player_token(3),
"5": build_player_token(theme, 5), "4": build_player_token(4),
"6": build_player_token(theme, 6), "5": build_player_token(5),
"7": build_player_token(theme, 7), "6": build_player_token(6),
}) "7": build_player_token(7),
}
}