diff --git a/apps/yaak-client/lib/theme/appearance.ts b/apps/yaak-client/lib/theme/appearance.ts index 4d199e90..00448544 100644 --- a/apps/yaak-client/lib/theme/appearance.ts +++ b/apps/yaak-client/lib/theme/appearance.ts @@ -1,48 +1,8 @@ -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; - -export type Appearance = 'light' | 'dark'; - -export function getCSSAppearance(): Appearance { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; -} - -export async function getWindowAppearance(): Promise { - const a = await getCurrentWebviewWindow().theme(); - return a ?? getCSSAppearance(); -} - -/** - * Subscribe to appearance (dark/light) changes. Note, we use Tauri Window appearance instead of - * CSS appearance because CSS won't fire the way we handle window theme management. - */ -export function subscribeToWindowAppearanceChange( - cb: (appearance: Appearance) => void, -): () => void { - const container = { - unsubscribe: () => {}, - }; - - getCurrentWebviewWindow() - .onThemeChanged((t) => { - cb(t.payload); - }) - .then((l) => { - container.unsubscribe = l; - }); - - return () => container.unsubscribe(); -} - -export function resolveAppearance( - preferredAppearance: Appearance, - appearanceSetting: string, -): Appearance { - const appearance = appearanceSetting === 'system' ? preferredAppearance : appearanceSetting; - return appearance === 'dark' ? 'dark' : 'light'; -} - -export function subscribeToPreferredAppearance(cb: (a: Appearance) => void) { - cb(getCSSAppearance()); - getWindowAppearance().then(cb); - subscribeToWindowAppearanceChange(cb); -} +export type { Appearance } from "@yaakapp-internal/theme"; +export { + getCSSAppearance, + getWindowAppearance, + resolveAppearance, + subscribeToPreferredAppearance, + subscribeToWindowAppearanceChange, +} from "@yaakapp-internal/theme"; diff --git a/apps/yaak-client/lib/theme/themes.ts b/apps/yaak-client/lib/theme/themes.ts index ce84f842..c1c44e28 100644 --- a/apps/yaak-client/lib/theme/themes.ts +++ b/apps/yaak-client/lib/theme/themes.ts @@ -1,14 +1,21 @@ -import type { GetThemesResponse } from '@yaakapp-internal/plugins'; -import { invokeCmd } from '../tauri'; -import type { Appearance } from './appearance'; -import { resolveAppearance } from './appearance'; +import type { GetThemesResponse } from "@yaakapp-internal/plugins"; +import { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme"; +import { invokeCmd } from "../tauri"; +import type { Appearance } from "./appearance"; +import { resolveAppearance } from "./appearance"; + +export { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme"; export async function getThemes() { - const themes = (await invokeCmd('cmd_get_themes')).flatMap((t) => t.themes); + const themes = ( + await invokeCmd("cmd_get_themes") + ).flatMap((t) => t.themes); themes.sort((a, b) => a.label.localeCompare(b.label)); // Remove duplicates, in case multiple plugins provide the same theme - const uniqueThemes = Array.from(new Map(themes.map((t) => [t.id, t])).values()); - return { themes: [yaakDark, yaakLight, ...uniqueThemes] }; + const uniqueThemes = Array.from( + new Map(themes.map((t) => [t.id, t])).values(), + ); + return { themes: [defaultDarkTheme, defaultLightTheme, ...uniqueThemes] }; } export async function getResolvedTheme( @@ -23,88 +30,16 @@ export async function getResolvedTheme( const darkThemes = themes.filter((t) => t.dark); const lightThemes = themes.filter((t) => !t.dark); - const dark = darkThemes.find((t) => t.id === themeDark) ?? darkThemes[0] ?? yaakDark; - const light = lightThemes.find((t) => t.id === themeLight) ?? lightThemes[0] ?? yaakLight; + const dark = + darkThemes.find((t) => t.id === themeDark) ?? + darkThemes[0] ?? + defaultDarkTheme; + const light = + lightThemes.find((t) => t.id === themeLight) ?? + lightThemes[0] ?? + defaultLightTheme; - const active = appearance === 'dark' ? dark : light; + const active = appearance === "dark" ? dark : light; return { dark, light, active }; } - -const yaakDark = { - id: 'yaak-dark', - label: 'Yaak', - dark: true, - base: { - surface: 'hsl(244,23%,14%)', - surfaceHighlight: 'hsl(244,23%,20%)', - text: 'hsl(245,23%,85%)', - textSubtle: 'hsl(245,18%,58%)', - textSubtlest: 'hsl(245,18%,45%)', - border: 'hsl(244,23%,25%)', - primary: 'hsl(266,100%,79%)', - secondary: 'hsl(245,23%,60%)', - info: 'hsl(206,100%,63%)', - success: 'hsl(150,99%,44%)', - notice: 'hsl(48,80%,63%)', - warning: 'hsl(28,100%,61%)', - danger: 'hsl(342,90%,68%)', - }, - components: { - button: { - primary: 'hsl(266,100%,71.1%)', - secondary: 'hsl(244,23%,54%)', - info: 'hsl(206,100%,56.7%)', - success: 'hsl(150,99%,37.4%)', - notice: 'hsl(48,80%,50.4%)', - warning: 'hsl(28,100%,54.9%)', - danger: 'hsl(342,90%,61.2%)', - }, - dialog: { - border: 'hsl(244,23%,24%)', - }, - sidebar: { - surface: 'hsl(243,23%,16%)', - border: 'hsl(244,23%,22%)', - }, - responsePane: { - surface: 'hsl(243,23%,16%)', - border: 'hsl(246,23%,23%)', - }, - appHeader: { - surface: 'hsl(244,23%,12%)', - border: 'hsl(244,23%,21%)', - }, - }, -}; - -const yaakLight = { - id: 'yaak-light', - label: 'Yaak', - dark: false, - base: { - surface: 'hsl(0,0%,100%)', - surfaceHighlight: 'hsl(218,24%,87%)', - text: 'hsl(217,24%,10%)', - textSubtle: 'hsl(217,24%,40%)', - textSubtlest: 'hsl(217,24%,58%)', - border: 'hsl(217,22%,90%)', - primary: 'hsl(266,100%,60%)', - secondary: 'hsl(220,24%,50%)', - info: 'hsl(206,100%,40%)', - success: 'hsl(139,66%,34%)', - notice: 'hsl(45,100%,34%)', - warning: 'hsl(30,100%,36%)', - danger: 'hsl(335,75%,48%)', - }, - components: { - sidebar: { - surface: 'hsl(220,20%,98%)', - border: 'hsl(217,22%,88%)', - surfaceHighlight: 'hsl(217,25%,90%)', - }, - }, -}; - -export const defaultDarkTheme = yaakDark; -export const defaultLightTheme = yaakLight; diff --git a/apps/yaak-client/lib/theme/window.ts b/apps/yaak-client/lib/theme/window.ts index a5dd5939..a369d94f 100644 --- a/apps/yaak-client/lib/theme/window.ts +++ b/apps/yaak-client/lib/theme/window.ts @@ -1,386 +1,13 @@ -import type { Theme, ThemeComponentColors } from '@yaakapp-internal/plugins'; -import { defaultDarkTheme, defaultLightTheme } from './themes'; -import { YaakColor } from './yaakColor'; - -export type YaakColors = { - surface: YaakColor; - surfaceHighlight?: YaakColor; - surfaceActive?: YaakColor; - - text: YaakColor; - textSubtle?: YaakColor; - textSubtlest?: YaakColor; - - border?: YaakColor; - borderSubtle?: YaakColor; - borderFocus?: YaakColor; - - shadow?: YaakColor; - backdrop?: YaakColor; - selection?: YaakColor; - - primary?: YaakColor; - secondary?: YaakColor; - info?: YaakColor; - success?: YaakColor; - notice?: YaakColor; - warning?: YaakColor; - danger?: YaakColor; -}; - -export type YaakTheme = { - id: string; - name: string; - base: YaakColors; - components?: Partial<{ - dialog: Partial; - menu: Partial; - toast: Partial; - sidebar: Partial; - responsePane: Partial; - appHeader: Partial; - button: Partial; - banner: Partial; - templateTag: Partial; - urlBar: Partial; - editor: Partial; - input: Partial; - }>; -}; - -export type YaakColorKey = keyof ThemeComponentColors; - -type ComponentName = keyof NonNullable; - -type CSSVariables = Record; - -function themeVariables( - theme: Theme, - component?: ComponentName, - base?: CSSVariables, -): CSSVariables | null { - const cmp = - component == null - ? theme.base - : (theme.components?.[component] ?? ({} as ThemeComponentColors)); - const c = (s: string | undefined) => yc(theme, s); - const vars: CSSVariables = { - surface: cmp.surface, - surfaceHighlight: cmp.surfaceHighlight ?? c(cmp.surface)?.lift(0.06).css(), - surfaceActive: cmp.surfaceActive ?? c(cmp.primary)?.lower(0.2).translucify(0.8).css(), - backdrop: cmp.backdrop ?? c(cmp.surface)?.lower(0.2).translucify(0.2).css(), - selection: cmp.selection ?? c(cmp.primary)?.lower(0.1).translucify(0.7).css(), - border: cmp.border ?? c(cmp.surface)?.lift(0.11)?.css(), - borderSubtle: cmp.borderSubtle ?? c(cmp.border)?.lower(0.06)?.css(), - borderFocus: c(cmp.info)?.translucify(0.5)?.css(), - text: cmp.text, - textSubtle: cmp.textSubtle ?? c(cmp.text)?.lower(0.2)?.css(), - textSubtlest: cmp.textSubtlest ?? c(cmp.text)?.lower(0.3)?.css(), - shadow: - cmp.shadow ?? - YaakColor.black() - .translucify(theme.dark ? 0.7 : 0.93) - .css(), - primary: cmp.primary, - secondary: cmp.secondary, - info: cmp.info, - success: cmp.success, - notice: cmp.notice, - warning: cmp.warning, - danger: cmp.danger, - }; - - // Extend with base - for (const [k, v] of Object.entries(vars)) { - if (!v && base?.[k as YaakColorKey]) { - vars[k as YaakColorKey] = base[k as YaakColorKey]; - } - } - - return vars; -} - -function templateTagColorVariables(color: YaakColor | null): Partial { - if (color == null) return {}; - - return { - text: color.lift(0.7).css(), - textSubtle: color.lift(0.4).css(), - textSubtlest: color.css(), - surface: color.lower(0.2).translucify(0.8).css(), - border: color.translucify(0.6).css(), - borderSubtle: color.translucify(0.8).css(), - surfaceHighlight: color.lower(0.1).translucify(0.7).css(), - }; -} - -function toastColorVariables(color: YaakColor | null): Partial { - if (color == null) return {}; - - return { - text: color.lift(0.8).css(), - textSubtle: color.lift(0.8).translucify(0.3).css(), - surface: color.translucify(0.9).css(), - surfaceHighlight: color.translucify(0.8).css(), - border: color.lift(0.3).translucify(0.6).css(), - }; -} - -function bannerColorVariables(color: YaakColor | null): Partial { - if (color == null) return {}; - - return { - text: color.lift(0.8).css(), - textSubtle: color.translucify(0.3).css(), - textSubtlest: color.translucify(0.6).css(), - surface: color.translucify(0.95).css(), - border: color.lift(0.3).translucify(0.8).css(), - }; -} - -function inputCSS(color: YaakColor | null): Partial { - if (color == null) return {}; - - const theme: Partial = { - border: color.css(), - }; - - return theme; -} - -function buttonSolidColorVariables( - color: YaakColor | null, - isDefault = false, -): Partial { - if (color == null) return {}; - - const theme: Partial = { - text: 'white', - surface: color.lower(0.3).css(), - surfaceHighlight: color.lower(0.1).css(), - border: color.css(), - }; - - if (isDefault) { - theme.text = undefined; // Inherit from parent - theme.surface = undefined; // Inherit from parent - theme.surfaceHighlight = color.lift(0.08).css(); - } - - return theme; -} - -function buttonBorderColorVariables( - color: YaakColor | null, - isDefault = false, -): Partial { - if (color == null) return {}; - - const vars: Partial = { - text: color.lift(0.8).css(), - textSubtle: color.lift(0.55).css(), - textSubtlest: color.lift(0.4).translucify(0.6).css(), - surfaceHighlight: color.translucify(0.8).css(), - borderSubtle: color.translucify(0.5).css(), - border: color.translucify(0.3).css(), - }; - - if (isDefault) { - vars.borderSubtle = color.lift(0.28).css(); - vars.border = color.lift(0.5).css(); - } - - return vars; -} - -function variablesToCSS( - selector: string | null, - vars: Partial | null, -): string | null { - if (vars == null) { - return null; - } - - const css = Object.entries(vars ?? {}) - .filter(([, value]) => value) - .map(([name, value]) => `--${name}: ${value};`) - .join('\n'); - - return selector == null ? css : `${selector} {\n${indent(css)}\n}`; -} - -function componentCSS(theme: Theme, component: ComponentName): string | null { - if (theme.components == null) { - return null; - } - - const themeVars = themeVariables(theme, component); - return variablesToCSS(`.x-theme-${component}`, themeVars); -} - -function buttonCSS( - theme: Theme, - color: YaakColorKey, - colors?: ThemeComponentColors, -): string | null { - const yaakColor = yc(theme, colors?.[color]); - if (yaakColor == null) { - return null; - } - - return [ - variablesToCSS(`.x-theme-button--solid--${color}`, buttonSolidColorVariables(yaakColor)), - variablesToCSS(`.x-theme-button--border--${color}`, buttonBorderColorVariables(yaakColor)), - ].join('\n\n'); -} - -function bannerCSS( - theme: Theme, - color: YaakColorKey, - colors?: ThemeComponentColors, -): string | null { - const yaakColor = yc(theme, colors?.[color]); - if (yaakColor == null) { - return null; - } - - return [variablesToCSS(`.x-theme-banner--${color}`, bannerColorVariables(yaakColor))].join( - '\n\n', - ); -} - -function toastCSS(theme: Theme, color: YaakColorKey, colors?: ThemeComponentColors): string | null { - const yaakColor = yc(theme, colors?.[color]); - if (yaakColor == null) { - return null; - } - - return [variablesToCSS(`.x-theme-toast--${color}`, toastColorVariables(yaakColor))].join('\n\n'); -} - -function templateTagCSS( - theme: Theme, - color: YaakColorKey, - colors?: ThemeComponentColors, -): string | null { - const yaakColor = yc(theme, colors?.[color]); - if (yaakColor == null) { - return null; - } - - return [ - variablesToCSS(`.x-theme-templateTag--${color}`, templateTagColorVariables(yaakColor)), - ].join('\n\n'); -} - -export function getThemeCSS(theme: Theme): string { - theme.components = theme.components ?? {}; - // Toast defaults to menu styles - theme.components.toast = theme.components.toast ?? theme.components.menu ?? {}; - const { components, id, label } = theme; - const colors = Object.keys(theme.base).reduce((prev, key) => { - // biome-ignore lint/performance/noAccumulatingSpread: none - return { ...prev, [key]: theme.base[key as YaakColorKey] }; - }, {}) as ThemeComponentColors; - - let themeCSS = ''; - try { - const baseCss = variablesToCSS(null, themeVariables(theme)); - themeCSS = [ - baseCss, - ...Object.keys(components ?? {}).map((key) => componentCSS(theme, key as ComponentName)), - variablesToCSS( - '.x-theme-button--solid--default', - buttonSolidColorVariables(yc(theme, theme.base.surface), true), - ), - variablesToCSS( - '.x-theme-button--border--default', - buttonBorderColorVariables(yc(theme, theme.base.surface), true), - ), - ...Object.keys(colors ?? {}).map((key) => - buttonCSS(theme, key as YaakColorKey, theme.components?.button ?? colors), - ), - ...Object.keys(colors ?? {}).map((key) => - bannerCSS(theme, key as YaakColorKey, theme.components?.banner ?? colors), - ), - ...Object.keys(colors ?? {}).map((key) => - toastCSS(theme, key as YaakColorKey, theme.components?.banner ?? colors), - ), - ...Object.keys(colors ?? {}).map((key) => - templateTagCSS(theme, key as YaakColorKey, theme.components?.templateTag ?? colors), - ), - ].join('\n\n'); - } catch (err) { - console.error('Failed to generate CSS', err); - } - - return [`/* ${label} */`, `[data-theme="${id}"] {`, indent(themeCSS), '}'].join('\n'); -} - -export function addThemeStylesToDocument(rawTheme: Theme | null) { - if (rawTheme == null) { - console.error('Failed to add theme styles: theme is null'); - return; - } - - const theme = completeTheme(rawTheme); - let styleEl = document.head.querySelector('style[data-theme]'); - if (!styleEl) { - styleEl = document.createElement('style'); - document.head.appendChild(styleEl); - } - - styleEl.setAttribute('data-theme', theme.id); - styleEl.setAttribute('data-updated-at', new Date().toISOString()); - styleEl.textContent = getThemeCSS(theme); -} - -export function setThemeOnDocument(theme: Theme | null) { - if (theme == null) { - console.error('Failed to set theme: theme is null'); - return; - } - - document.documentElement.setAttribute('data-theme', theme.id); -} - -export function indent(text: string, space = ' '): string { - return text - .split('\n') - .map((line) => space + line) - .join('\n'); -} - -function yc( - theme: Theme, - s: T, -): T extends string ? YaakColor : null { - if (s == null) return null as never; - return new YaakColor(s, theme.dark ? 'dark' : 'light') as never; -} - -export function completeTheme(theme: Theme): Theme { - const fallback = theme.dark ? defaultDarkTheme.base : defaultLightTheme.base; - const c = (s: string | null | undefined) => yc(theme, s); - - theme.base.primary ??= fallback.primary; - theme.base.secondary ??= fallback.secondary; - theme.base.info ??= fallback.info; - theme.base.success ??= fallback.success; - theme.base.notice ??= fallback.notice; - theme.base.warning ??= fallback.warning; - theme.base.danger ??= fallback.danger; - - theme.base.surface ??= fallback.surface; - theme.base.surfaceHighlight ??= c(theme.base.surface)?.lift(0.06)?.css(); - theme.base.surfaceActive ??= c(theme.base.primary)?.lower(0.2).translucify(0.8).css(); - - theme.base.border ??= c(theme.base.surface)?.lift(0.12)?.css(); - theme.base.borderSubtle ??= c(theme.base.border)?.lower(0.08)?.css(); - - theme.base.text ??= fallback.text; - theme.base.textSubtle ??= c(theme.base.text)?.lower(0.3)?.css(); - theme.base.textSubtlest ??= c(theme.base.text)?.lower(0.5)?.css(); - - return theme; -} +export type { + YaakColorKey, + YaakColors, + YaakTheme, +} from "@yaakapp-internal/theme"; +export { + addThemeStylesToDocument, + applyThemeToDocument, + completeTheme, + getThemeCSS, + indent, + setThemeOnDocument, +} from "@yaakapp-internal/theme"; diff --git a/apps/yaak-client/lib/theme/yaakColor.ts b/apps/yaak-client/lib/theme/yaakColor.ts index 26510c48..513e50b2 100644 --- a/apps/yaak-client/lib/theme/yaakColor.ts +++ b/apps/yaak-client/lib/theme/yaakColor.ts @@ -1,158 +1 @@ -import parseColor from 'parse-color'; - -export class YaakColor { - private readonly appearance: 'dark' | 'light' = 'light'; - - private hue = 0; - private saturation = 0; - private lightness = 0; - private alpha = 1; - - constructor(cssColor: string, appearance: 'dark' | 'light' = 'light') { - try { - this.set(cssColor); - this.appearance = appearance; - } catch (err) { - console.log('Failed to parse CSS color', cssColor, err); - } - } - - static transparent(): YaakColor { - return new YaakColor('rgb(0,0,0)', 'light').translucify(1); - } - - static white(): YaakColor { - return new YaakColor('rgb(0,0,0)', 'light').lower(1); - } - - static black(): YaakColor { - return new YaakColor('rgb(0,0,0)', 'light').lift(1); - } - - set(cssColor: string): YaakColor { - let fixedCssColor = cssColor; - if (cssColor.startsWith('#') && cssColor.length === 9) { - const [r, g, b, a] = hexToRgba(cssColor); - fixedCssColor = `rgba(${r},${g},${b},${a})`; - } - const { hsla } = parseColor(fixedCssColor); - this.hue = hsla[0]; - this.saturation = hsla[1]; - this.lightness = hsla[2]; - this.alpha = hsla[3] ?? 1; - return this; - } - - clone(): YaakColor { - return new YaakColor(this.css(), this.appearance); - } - - lower(mod: number): YaakColor { - return this.appearance === 'dark' ? this._darken(mod) : this._lighten(mod); - } - - lift(mod: number): YaakColor { - return this.appearance === 'dark' ? this._lighten(mod) : this._darken(mod); - } - - minLightness(n: number): YaakColor { - const c = this.clone(); - if (c.lightness < n) { - c.lightness = n; - } - return c; - } - - isDark(): boolean { - return this.lightness < 50; - } - - translucify(mod: number): YaakColor { - const c = this.clone(); - c.alpha = c.alpha - c.alpha * mod; - return c; - } - - opacify(mod: number): YaakColor { - const c = this.clone(); - c.alpha = this.alpha + (100 - this.alpha) * mod; - return c; - } - - desaturate(mod: number): YaakColor { - const c = this.clone(); - c.saturation = c.saturation - c.saturation * mod; - return c; - } - - saturate(mod: number): YaakColor { - const c = this.clone(); - c.saturation = this.saturation + (100 - this.saturation) * mod; - return c; - } - - lighterThan(c: YaakColor): boolean { - return this.lightness > c.lightness; - } - - css(): string { - const h = this.hue; - const s = this.saturation; - const l = this.lightness; - const a = this.alpha; - - const [r, g, b] = parseColor(`hsl(${h},${s}%,${l}%)`).rgb; - return rgbaToHex(r, g, b, a); - } - - hexNoAlpha(): string { - const h = this.hue; - const s = this.saturation; - const l = this.lightness; - - const [r, g, b] = parseColor(`hsl(${h},${s}%,${l}%)`).rgb; - return rgbaToHexNoAlpha(r, g, b); - } - - private _lighten(mod: number): YaakColor { - const c = this.clone(); - c.lightness = this.lightness + (100 - this.lightness) * mod; - return c; - } - - private _darken(mod: number): YaakColor { - const c = this.clone(); - c.lightness = this.lightness - this.lightness * mod; - return c; - } -} - -function rgbaToHex(r: number, g: number, b: number, a: number): string { - const toHex = (n: number): string => { - const hex = Number(Math.round(n)).toString(16); - return hex.length === 1 ? `0${hex}` : hex; - }; - return `#${[toHex(r), toHex(g), toHex(b), toHex(a * 255)].join('').toUpperCase()}`; -} - -function rgbaToHexNoAlpha(r: number, g: number, b: number): string { - const toHex = (n: number): string => { - const hex = Number(Math.round(n)).toString(16); - return hex.length === 1 ? `0${hex}` : hex; - }; - return `#${[toHex(r), toHex(g), toHex(b)].join('').toUpperCase()}`; -} - -function hexToRgba(hex: string): [number, number, number, number] { - const fromHex = (h: string): number => { - if (h === '') return 255; - return Number(`0x${h}`); - }; - - const r = fromHex(hex.slice(1, 3)); - const g = fromHex(hex.slice(3, 5)); - const b = fromHex(hex.slice(5, 7)); - const a = fromHex(hex.slice(7, 9)); - - return [r, g, b, a / 255]; -} +export { YaakColor } from "@yaakapp-internal/theme"; diff --git a/apps/yaak-client/main.tsx b/apps/yaak-client/main.tsx index 389683cf..626ac317 100644 --- a/apps/yaak-client/main.tsx +++ b/apps/yaak-client/main.tsx @@ -1,29 +1,33 @@ -import './main.css'; -import { RouterProvider } from '@tanstack/react-router'; -import { type } from '@tauri-apps/plugin-os'; -import { changeModelStoreWorkspace, initModelStore } from '@yaakapp-internal/models'; -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { initSync } from './init/sync'; -import { initGlobalListeners } from './lib/initGlobalListeners'; -import { jotaiStore } from './lib/jotai'; -import { router } from './lib/router'; +import "./main.css"; +import { RouterProvider } from "@tanstack/react-router"; +import { type } from "@tauri-apps/plugin-os"; +import { + changeModelStoreWorkspace, + initModelStore, +} from "@yaakapp-internal/models"; +import { setPlatformOnDocument } from "@yaakapp-internal/theme"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { initSync } from "./init/sync"; +import { initGlobalListeners } from "./lib/initGlobalListeners"; +import { jotaiStore } from "./lib/jotai"; +import { router } from "./lib/router"; const osType = type(); -document.documentElement.setAttribute('data-platform', osType); +setPlatformOnDocument(osType); -window.addEventListener('keydown', (e) => { +window.addEventListener("keydown", (e) => { const rx = /input|select|textarea/i; const target = e.target; - if (e.key !== 'Backspace') return; + if (e.key !== "Backspace") return; if (!(target instanceof Element)) return; - if (target.getAttribute('contenteditable') !== null) return; + if (target.getAttribute("contenteditable") !== null) return; if ( !rx.test(target.tagName) || - ('disabled' in target && target.disabled) || - ('readOnly' in target && target.readOnly) + ("disabled" in target && target.disabled) || + ("readOnly" in target && target.readOnly) ) { e.preventDefault(); } @@ -35,8 +39,8 @@ initModelStore(jotaiStore); initGlobalListeners(); await changeModelStoreWorkspace(null); // Load global models -console.log('Creating React root'); -createRoot(document.getElementById('root') as HTMLElement).render( +console.log("Creating React root"); +createRoot(document.getElementById("root") as HTMLElement).render( , diff --git a/apps/yaak-client/package.json b/apps/yaak-client/package.json index b0ea9a47..ee2c69dd 100644 --- a/apps/yaak-client/package.json +++ b/apps/yaak-client/package.json @@ -87,6 +87,8 @@ "@types/react-syntax-highlighter": "^15.5.13", "@types/uuid": "^10.0.0", "@types/whatwg-mimetype": "^3.0.2", + "@yaakapp-internal/theme": "^1.0.0", + "@yaakapp-internal/ui": "^1.0.0", "@vitejs/plugin-react": "^4.6.0", "autoprefixer": "^10.4.21", "decompress": "^4.2.1", diff --git a/apps/yaak-client/tailwind.config.cjs b/apps/yaak-client/tailwind.config.cjs index e6423f61..d8800fb8 100644 --- a/apps/yaak-client/tailwind.config.cjs +++ b/apps/yaak-client/tailwind.config.cjs @@ -1,146 +1,16 @@ -const plugin = require('tailwindcss/plugin'); - -const sizes = { - '2xs': '1.4rem', - xs: '1.8rem', - sm: '2.0rem', - md: '2.3rem', - lg: '2.6rem', -}; +const sharedConfig = require("@yaakapp-internal/tailwind-config"); /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ['class', '[data-resolved-appearance="dark"]'], + ...sharedConfig, content: [ - './*.{html,ts,tsx}', - './commands/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './hooks/**/*.{ts,tsx}', - './init/**/*.{ts,tsx}', - './lib/**/*.{ts,tsx}', - './routes/**/*.{ts,tsx}', - ], - theme: { - extend: { - keyframes: { - blinkRing: { - '0%, 49%': { '--tw-ring-color': 'var(--primary)' }, - '50%, 99%': { '--tw-ring-color': 'transparent' }, - '100%': { '--tw-ring-color': 'var(--primary)' }, - }, - }, - animation: { - blinkRing: 'blinkRing 150ms step-start 400ms infinite', - }, - opacity: { - disabled: '0.3', - }, - fontSize: { - xs: '0.8rem', - }, - height: sizes, - width: sizes, - minHeight: sizes, - minWidth: sizes, - lineHeight: { - // HACK: Minus 2 to account for borders inside inputs - xs: 'calc(1.75rem - 2px)', - sm: 'calc(2.0rem - 2px)', - md: 'calc(2.5rem - 2px)', - }, - transitionProperty: { - grid: 'grid', - }, - }, - fontFamily: { - mono: [ - 'var(--font-family-editor)', - 'JetBrains Mono', - 'ui-monospace', - 'SFMono-Regular', - 'Menlo', - 'Monaco', - 'Fira Code', - 'Ubuntu Mono', - 'Consolas', - 'Liberation Mono', - 'Courier New', - 'DejaVu Sans Mono', - 'Hack', - 'monospace', - ], - sans: [ - 'var(--font-family-interface)', - 'Inter UI', - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Roboto', - 'Oxygen-Sans', - 'Ubuntu', - 'Cantarell', - 'Helvetica Neue', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - ], - }, - fontSize: { - '4xs': '0.6rem', - '3xs': '0.675rem', - '2xs': '0.75rem', - xs: '0.8rem', - sm: '0.9rem', - base: '1rem', - lg: '1.12rem', - xl: '1.25rem', - '2xl': '1.5rem', - '3xl': '2rem', - '4xl': '2.5rem', - '5xl': '3rem', - editor: 'var(--editor-font-size)', - shrink: '0.8em', - }, - boxShadow: { - DEFAULT: '0 1px 3px 0 var(--shadow)', - lg: '0 10px 15px -3px var(--shadow)', - }, - colors: { - transparent: 'transparent', - placeholder: 'var(--textSubtlest)', - shadow: 'var(--shadow)', - backdrop: 'var(--backdrop)', - selection: 'var(--selection)', - - // New theme values - - surface: 'var(--surface)', - 'surface-highlight': 'var(--surfaceHighlight)', - 'surface-active': 'var(--surfaceActive)', - - text: 'var(--text)', - 'text-subtle': 'var(--textSubtle)', - 'text-subtlest': 'var(--textSubtlest)', - - border: 'var(--border)', - 'border-subtle': 'var(--borderSubtle)', - 'border-focus': 'var(--borderFocus)', - - primary: 'var(--primary)', - danger: 'var(--danger)', - secondary: 'var(--secondary)', - success: 'var(--success)', - info: 'var(--info)', - notice: 'var(--notice)', - warning: 'var(--warning)', - }, - }, - plugins: [ - require('@tailwindcss/container-queries'), - plugin(function ({ addVariant }) { - addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']); - addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus']); - }), + "./*.{html,ts,tsx}", + "./commands/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./hooks/**/*.{ts,tsx}", + "./init/**/*.{ts,tsx}", + "./lib/**/*.{ts,tsx}", + "./routes/**/*.{ts,tsx}", + "../../packages/ui/src/**/*.{ts,tsx}", ], }; diff --git a/apps/yaak-client/theme.ts b/apps/yaak-client/theme.ts index 03957687..6ff7ccff 100644 --- a/apps/yaak-client/theme.ts +++ b/apps/yaak-client/theme.ts @@ -1,12 +1,15 @@ -import { listen } from '@tauri-apps/api/event'; -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import { setWindowTheme } from '@yaakapp-internal/mac-window'; -import type { ModelPayload } from '@yaakapp-internal/models'; -import { getSettings } from './lib/settings'; -import type { Appearance } from './lib/theme/appearance'; -import { getCSSAppearance, subscribeToPreferredAppearance } from './lib/theme/appearance'; -import { getResolvedTheme } from './lib/theme/themes'; -import { addThemeStylesToDocument, setThemeOnDocument } from './lib/theme/window'; +import { listen } from "@tauri-apps/api/event"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { setWindowTheme } from "@yaakapp-internal/mac-window"; +import type { ModelPayload } from "@yaakapp-internal/models"; +import { getSettings } from "./lib/settings"; +import type { Appearance } from "./lib/theme/appearance"; +import { + getCSSAppearance, + subscribeToPreferredAppearance, +} from "./lib/theme/appearance"; +import { getResolvedTheme } from "./lib/theme/themes"; +import { applyThemeToDocument } from "@yaakapp-internal/theme"; // NOTE: CSS appearance isn't as accurate as getting it async from the window (next step), but we want // a good appearance guess so we're not waiting too long @@ -22,15 +25,15 @@ configureTheme().then( // need to show it here, after configuring the theme for the first time. await getCurrentWebviewWindow().show(); }, - (err) => console.log('Failed to configure theme', err), + (err) => console.log("Failed to configure theme", err), ); // Listen for settings changes, the re-compute theme -listen('model_write', async (event) => { - if (event.payload.change.type !== 'upsert') return; +listen("model_write", async (event) => { + if (event.payload.change.type !== "upsert") return; const model = event.payload.model.model; - if (model !== 'settings' && model !== 'plugin') return; + if (model !== "settings" && model !== "plugin") return; await configureTheme(); }).catch(console.error); @@ -42,8 +45,7 @@ async function configureTheme() { settings.themeLight, settings.themeDark, ); - addThemeStylesToDocument(theme.active); - setThemeOnDocument(theme.active); + applyThemeToDocument(theme.active); if (theme.active.base.surface != null) { setWindowTheme(theme.active.base.surface); } diff --git a/apps/yaak-client/tsconfig.json b/apps/yaak-client/tsconfig.json index 9add434b..8d760680 100644 --- a/apps/yaak-client/tsconfig.json +++ b/apps/yaak-client/tsconfig.json @@ -15,9 +15,16 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@yaakapp-internal/theme": ["../../packages/theme/src/index.ts"], + "@yaakapp-internal/theme/*": ["../../packages/theme/src/*"], + "@yaakapp-internal/ui": ["../../packages/ui/src/index.ts"], + "@yaakapp-internal/ui/*": ["../../packages/ui/src/*"], + }, }, "include": ["."], "exclude": ["vite.config.ts"], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [{ "path": "./tsconfig.node.json" }], } diff --git a/apps/yaak-proxy/index.html b/apps/yaak-proxy/index.html index 61707262..aed2c7a9 100644 --- a/apps/yaak-proxy/index.html +++ b/apps/yaak-proxy/index.html @@ -1,12 +1,27 @@ - + - - - - Yaak Proxy - - -
- - + + + + Yaak Proxy + + + + +
+ + + diff --git a/apps/yaak-proxy/main.css b/apps/yaak-proxy/main.css index dc94457c..dff6389f 100644 --- a/apps/yaak-proxy/main.css +++ b/apps/yaak-proxy/main.css @@ -1,95 +1,92 @@ -:root { - color: #f4efe7; - background: - radial-gradient(circle at top, rgba(217, 119, 6, 0.35), transparent 45%), - linear-gradient(180deg, #18212b 0%, #0f141a 100%); - font-family: "IBM Plex Sans", "Segoe UI", sans-serif; -} +@tailwind base; +@tailwind components; +@tailwind utilities; -* { - box-sizing: border-box; -} +@layer base { + html, + body, + #root { + @apply w-full h-full overflow-hidden text-text bg-surface; + } -html, -body, -#root { - margin: 0; - min-height: 100%; -} + :root { + --font-family-interface: ""; + --font-family-editor: ""; + } -body { - min-height: 100vh; -} + :root { + font-variant-ligatures: none; + } -.app-shell { - min-height: 100vh; - display: grid; - place-items: center; - padding: 24px; -} + html[data-platform="linux"] { + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + } -.hero-card { - width: min(680px, 100%); - padding: 40px; - border: 1px solid rgba(244, 239, 231, 0.12); - border-radius: 28px; - background: rgba(9, 12, 16, 0.7); - box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35); - backdrop-filter: blur(18px); -} + ::selection { + @apply bg-selection; + } -.eyebrow { - margin: 0 0 12px; - font-size: 12px; - letter-spacing: 0.24em; - text-transform: uppercase; - color: #f6ad55; -} + :not(a), + :not(input):not(textarea), + :not(input):not(textarea)::after, + :not(input):not(textarea)::before { + @apply select-none cursor-default; + } -h1 { - margin: 0; - font-size: clamp(44px, 8vw, 84px); - line-height: 0.95; -} + input, + textarea { + &::placeholder { + @apply text-placeholder; + } + } -.lede { - margin: 20px 0 0; - max-width: 48ch; - font-size: 18px; - line-height: 1.6; - color: rgba(244, 239, 231, 0.78); -} + a, + a[href] * { + @apply cursor-pointer !important; + } -.controls { - margin-top: 28px; - display: flex; - gap: 12px; -} + table th { + @apply text-left; + } -.btn { - border: 0; - border-radius: 10px; - padding: 10px 14px; - background: #f6ad55; - color: #111; - font-weight: 700; - cursor: pointer; -} + :not(iframe) { + &::-webkit-scrollbar, + &::-webkit-scrollbar-corner { + @apply w-[8px] h-[8px] bg-transparent; + } -.btn.ghost { - background: rgba(255, 255, 255, 0.12); - color: #f4efe7; -} + &::-webkit-scrollbar-track { + @apply bg-transparent; + } -.btn:disabled { - opacity: 0.6; - cursor: default; -} + &::-webkit-scrollbar-thumb { + @apply bg-text-subtlest rounded-[4px] opacity-20; + } -.status { - margin-top: 14px; - display: flex; - gap: 16px; - color: rgba(244, 239, 231, 0.88); - font-size: 14px; + &::-webkit-scrollbar-thumb:hover { + @apply opacity-40 !important; + } + } + + .hide-scrollbars { + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar { + @apply hidden !important; + } + } + + .rtl { + direction: rtl; + } + + :root { + color-scheme: light dark; + --transition-duration: 100ms ease-in-out; + --color-white: 255 100% 100%; + --color-black: 255 0% 0%; + } } diff --git a/apps/yaak-proxy/main.tsx b/apps/yaak-proxy/main.tsx index def1b0d1..7db63249 100644 --- a/apps/yaak-proxy/main.tsx +++ b/apps/yaak-proxy/main.tsx @@ -1,4 +1,5 @@ import "./main.css"; +import { Button } from "@yaakapp-internal/ui"; import { invoke } from "@tauri-apps/api/core"; import { StrictMode } from "react"; import { useState } from "react"; @@ -45,25 +46,38 @@ function App() { } return ( -
-
-

Monorepo Smoke Test

-

Yaak Proxy

-

- This is a minimal proxy app stub running on the new `apps/yaak-proxy` - and `crates-tauri/yaak-app-proxy` structure. -

-
- - -
-
- Status: {status} - {port != null ? Port: {port} : null} +
+
+
+
+

Yaak Proxy

+

Status: {status}

+

+ Port: {port ?? "Not running"} +

+
+ +
+ + + +
diff --git a/apps/yaak-proxy/package.json b/apps/yaak-proxy/package.json index 2a11a291..64fd56fd 100644 --- a/apps/yaak-proxy/package.json +++ b/apps/yaak-proxy/package.json @@ -9,6 +9,8 @@ "lint": "tsc --noEmit" }, "dependencies": { + "@yaakapp-internal/theme": "^1.0.0", + "@yaakapp-internal/ui": "^1.0.0", "@tauri-apps/api": "^2.9.1", "react": "^19.1.0", "react-dom": "^19.1.0" diff --git a/apps/yaak-proxy/postcss.config.cjs b/apps/yaak-proxy/postcss.config.cjs new file mode 100644 index 00000000..5b832920 --- /dev/null +++ b/apps/yaak-proxy/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require("@tailwindcss/nesting")(require("postcss-nesting")), + require("tailwindcss"), + require("autoprefixer"), + ], +}; diff --git a/apps/yaak-proxy/tailwind.config.cjs b/apps/yaak-proxy/tailwind.config.cjs new file mode 100644 index 00000000..6d6eace8 --- /dev/null +++ b/apps/yaak-proxy/tailwind.config.cjs @@ -0,0 +1,7 @@ +const sharedConfig = require("@yaakapp-internal/tailwind-config"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + ...sharedConfig, + content: ["./*.{html,ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"], +}; diff --git a/apps/yaak-proxy/theme.ts b/apps/yaak-proxy/theme.ts new file mode 100644 index 00000000..9a108c6f --- /dev/null +++ b/apps/yaak-proxy/theme.ts @@ -0,0 +1,9 @@ +import { + applyThemeToDocument, + defaultDarkTheme, + platformFromUserAgent, + setPlatformOnDocument, +} from "@yaakapp-internal/theme"; + +setPlatformOnDocument(platformFromUserAgent(navigator.userAgent)); +applyThemeToDocument(defaultDarkTheme); diff --git a/apps/yaak-proxy/tsconfig.json b/apps/yaak-proxy/tsconfig.json index 7ba5265a..c2eba75d 100644 --- a/apps/yaak-proxy/tsconfig.json +++ b/apps/yaak-proxy/tsconfig.json @@ -14,9 +14,16 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@yaakapp-internal/theme": ["../../packages/theme/src/index.ts"], + "@yaakapp-internal/theme/*": ["../../packages/theme/src/*"], + "@yaakapp-internal/ui": ["../../packages/ui/src/index.ts"], + "@yaakapp-internal/ui/*": ["../../packages/ui/src/*"], + }, }, "include": ["."], "exclude": ["vite.config.ts"], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [{ "path": "./tsconfig.node.json" }], } diff --git a/package-lock.json b/package-lock.json index a308f33d..cedbb42b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "yaak-monorepo", "version": "0.0.0", "workspaces": [ + "packages/ui", + "packages/theme", + "packages/tailwind-config", "packages/common-lib", "packages/plugin-runtime", "packages/plugin-runtime-types", @@ -167,6 +170,8 @@ "@types/uuid": "^10.0.0", "@types/whatwg-mimetype": "^3.0.2", "@vitejs/plugin-react": "^4.6.0", + "@yaakapp-internal/theme": "^1.0.0", + "@yaakapp-internal/ui": "^1.0.0", "autoprefixer": "^10.4.21", "decompress": "^4.2.1", "internal-ip": "^8.0.0", @@ -220,6 +225,8 @@ "version": "1.0.0", "dependencies": { "@tauri-apps/api": "^2.9.1", + "@yaakapp-internal/theme": "^1.0.0", + "@yaakapp-internal/ui": "^1.0.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -305,7 +312,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1666,7 +1672,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1688,7 +1693,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1704,7 +1708,6 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3255,7 +3258,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", - "dev": true, "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.2.0" @@ -4435,6 +4437,10 @@ "resolved": "crates/yaak-sync", "link": true }, + "node_modules/@yaakapp-internal/tailwind-config": { + "resolved": "packages/tailwind-config", + "link": true + }, "node_modules/@yaakapp-internal/tauri-client": { "resolved": "crates-tauri/yaak-app-client", "link": true @@ -4447,6 +4453,14 @@ "resolved": "crates/yaak-templates", "link": true }, + "node_modules/@yaakapp-internal/theme": { + "resolved": "packages/theme", + "link": true + }, + "node_modules/@yaakapp-internal/ui": { + "resolved": "packages/ui", + "link": true + }, "node_modules/@yaakapp-internal/ws": { "resolved": "crates/yaak-ws", "link": true @@ -4755,14 +4769,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -4776,7 +4788,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -4789,7 +4800,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -5010,7 +5020,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5274,7 +5283,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5406,7 +5414,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -6087,7 +6094,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -6496,7 +6502,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/diff": { @@ -6541,7 +6546,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/dot-case": { @@ -6908,7 +6912,7 @@ "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -7297,7 +7301,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -7575,7 +7578,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7752,7 +7754,7 @@ "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -8597,7 +8599,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -8638,7 +8639,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -9073,7 +9073,6 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -9392,7 +9391,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -10797,7 +10795,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -10829,7 +10826,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -10987,7 +10983,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11943,7 +11938,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -12042,7 +12036,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12101,7 +12094,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -12129,7 +12121,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -12158,7 +12149,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -12176,7 +12166,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -12202,7 +12191,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, "funding": [ { "type": "opencollective", @@ -12353,7 +12341,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -12367,7 +12354,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/postman-collection": { @@ -12747,7 +12733,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -12757,7 +12742,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12812,7 +12796,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -12825,7 +12808,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -13060,7 +13042,6 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -13090,7 +13071,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -13830,7 +13811,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -14296,7 +14276,6 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -14319,7 +14298,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -14360,7 +14338,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14563,7 +14540,6 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -14601,7 +14577,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -14614,7 +14589,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -14659,7 +14633,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -14669,7 +14642,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -14740,7 +14712,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -14871,7 +14842,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -14884,7 +14854,7 @@ "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -15280,7 +15250,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -16165,6 +16134,34 @@ "undici-types": "~7.16.0" } }, + "packages/tailwind-config": { + "name": "@yaakapp-internal/tailwind-config", + "version": "1.0.0", + "dependencies": { + "@tailwindcss/container-queries": "^0.1.1", + "tailwindcss": "^3.4.17" + } + }, + "packages/theme": { + "version": "1.0.0", + "dependencies": { + "@tauri-apps/api": "^2.9.1", + "@yaakapp-internal/plugins": "^1.0.0", + "parse-color": "^1.0.0" + } + }, + "packages/ui": { + "name": "@yaakapp-internal/ui", + "version": "1.0.0", + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "peerDependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + } + }, "plugins-external/faker": { "name": "@yaak/faker", "version": "1.1.1", diff --git a/package.json b/package.json index f758d6fb..3de33e94 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "url": "git+https://github.com/mountain-loop/yaak.git" }, "workspaces": [ + "packages/ui", + "packages/theme", + "packages/tailwind-config", "packages/common-lib", "packages/plugin-runtime", "packages/plugin-runtime-types", diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs new file mode 100644 index 00000000..8622f021 --- /dev/null +++ b/packages/tailwind-config/index.cjs @@ -0,0 +1,136 @@ +const plugin = require("tailwindcss/plugin"); + +const sizes = { + "2xs": "1.4rem", + xs: "1.8rem", + sm: "2.0rem", + md: "2.3rem", + lg: "2.6rem", +}; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class", '[data-resolved-appearance="dark"]'], + theme: { + extend: { + keyframes: { + blinkRing: { + "0%, 49%": { "--tw-ring-color": "var(--primary)" }, + "50%, 99%": { "--tw-ring-color": "transparent" }, + "100%": { "--tw-ring-color": "var(--primary)" }, + }, + }, + animation: { + blinkRing: "blinkRing 150ms step-start 400ms infinite", + }, + opacity: { + disabled: "0.3", + }, + fontSize: { + xs: "0.8rem", + }, + height: sizes, + width: sizes, + minHeight: sizes, + minWidth: sizes, + lineHeight: { + // HACK: Minus 2 to account for borders inside inputs + xs: "calc(1.75rem - 2px)", + sm: "calc(2.0rem - 2px)", + md: "calc(2.5rem - 2px)", + }, + transitionProperty: { + grid: "grid", + }, + }, + fontFamily: { + mono: [ + "var(--font-family-editor)", + "JetBrains Mono", + "ui-monospace", + "SFMono-Regular", + "Menlo", + "Monaco", + "Fira Code", + "Ubuntu Mono", + "Consolas", + "Liberation Mono", + "Courier New", + "DejaVu Sans Mono", + "Hack", + "monospace", + ], + sans: [ + "var(--font-family-interface)", + "Inter UI", + "-apple-system", + "BlinkMacSystemFont", + "Segoe UI", + "Roboto", + "Oxygen-Sans", + "Ubuntu", + "Cantarell", + "Helvetica Neue", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + ], + }, + fontSize: { + "4xs": "0.6rem", + "3xs": "0.675rem", + "2xs": "0.75rem", + xs: "0.8rem", + sm: "0.9rem", + base: "1rem", + lg: "1.12rem", + xl: "1.25rem", + "2xl": "1.5rem", + "3xl": "2rem", + "4xl": "2.5rem", + "5xl": "3rem", + editor: "var(--editor-font-size)", + shrink: "0.8em", + }, + boxShadow: { + DEFAULT: "0 1px 3px 0 var(--shadow)", + lg: "0 10px 15px -3px var(--shadow)", + }, + colors: { + transparent: "transparent", + placeholder: "var(--textSubtlest)", + shadow: "var(--shadow)", + backdrop: "var(--backdrop)", + selection: "var(--selection)", + + // New theme values + surface: "var(--surface)", + "surface-highlight": "var(--surfaceHighlight)", + "surface-active": "var(--surfaceActive)", + text: "var(--text)", + "text-subtle": "var(--textSubtle)", + "text-subtlest": "var(--textSubtlest)", + border: "var(--border)", + "border-subtle": "var(--borderSubtle)", + "border-focus": "var(--borderFocus)", + primary: "var(--primary)", + danger: "var(--danger)", + secondary: "var(--secondary)", + success: "var(--success)", + info: "var(--info)", + notice: "var(--notice)", + warning: "var(--warning)", + }, + }, + plugins: [ + require("@tailwindcss/container-queries"), + plugin(function ({ addVariant }) { + addVariant("hocus", ["&:hover", "&:focus-visible", "&.focus:focus"]); + addVariant("focus-visible-or-class", [ + "&:focus-visible", + "&.focus:focus", + ]); + }), + ], +}; diff --git a/packages/tailwind-config/index.d.ts b/packages/tailwind-config/index.d.ts new file mode 100644 index 00000000..3f71c5dd --- /dev/null +++ b/packages/tailwind-config/index.d.ts @@ -0,0 +1,5 @@ +import type { Config } from "tailwindcss"; + +declare const config: Config; + +export = config; diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json new file mode 100644 index 00000000..e22bb464 --- /dev/null +++ b/packages/tailwind-config/package.json @@ -0,0 +1,11 @@ +{ + "name": "@yaakapp-internal/tailwind-config", + "private": true, + "version": "1.0.0", + "main": "index.cjs", + "types": "index.d.ts", + "dependencies": { + "@tailwindcss/container-queries": "^0.1.1", + "tailwindcss": "^3.4.17" + } +} diff --git a/packages/theme/package.json b/packages/theme/package.json new file mode 100644 index 00000000..b59c778a --- /dev/null +++ b/packages/theme/package.json @@ -0,0 +1,13 @@ +{ + "name": "@yaakapp-internal/theme", + "private": true, + "version": "1.0.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "dependencies": { + "@tauri-apps/api": "^2.9.1", + "@yaakapp-internal/plugins": "^1.0.0", + "parse-color": "^1.0.0" + } +} diff --git a/packages/theme/src/appearance.ts b/packages/theme/src/appearance.ts new file mode 100644 index 00000000..9e5193af --- /dev/null +++ b/packages/theme/src/appearance.ts @@ -0,0 +1,44 @@ +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; + +export type Appearance = "light" | "dark"; + +export function getCSSAppearance(): Appearance { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +export async function getWindowAppearance(): Promise { + const appearance = await getCurrentWebviewWindow().theme(); + return appearance ?? getCSSAppearance(); +} + +export function subscribeToWindowAppearanceChange( + cb: (appearance: Appearance) => void, +): () => void { + const container = { + unsubscribe: () => {}, + }; + + getCurrentWebviewWindow() + .onThemeChanged((theme) => { + cb(theme.payload); + }) + .then((listener) => { + container.unsubscribe = listener; + }); + + return () => container.unsubscribe(); +} + +export function resolveAppearance( + preferredAppearance: Appearance, + appearanceSetting: string, +): Appearance { + const appearance = appearanceSetting === "system" ? preferredAppearance : appearanceSetting; + return appearance === "dark" ? "dark" : "light"; +} + +export function subscribeToPreferredAppearance(cb: (appearance: Appearance) => void) { + cb(getCSSAppearance()); + getWindowAppearance().then(cb); + subscribeToWindowAppearanceChange(cb); +} diff --git a/packages/theme/src/defaultThemes.ts b/packages/theme/src/defaultThemes.ts new file mode 100644 index 00000000..439871be --- /dev/null +++ b/packages/theme/src/defaultThemes.ts @@ -0,0 +1,76 @@ +import type { Theme } from "@yaakapp-internal/plugins"; + +export const defaultDarkTheme: Theme = { + id: "yaak-dark", + label: "Yaak", + dark: true, + base: { + surface: "hsl(244,23%,14%)", + surfaceHighlight: "hsl(244,23%,20%)", + text: "hsl(245,23%,85%)", + textSubtle: "hsl(245,18%,58%)", + textSubtlest: "hsl(245,18%,45%)", + border: "hsl(244,23%,25%)", + primary: "hsl(266,100%,79%)", + secondary: "hsl(245,23%,60%)", + info: "hsl(206,100%,63%)", + success: "hsl(150,99%,44%)", + notice: "hsl(48,80%,63%)", + warning: "hsl(28,100%,61%)", + danger: "hsl(342,90%,68%)", + }, + components: { + button: { + primary: "hsl(266,100%,71.1%)", + secondary: "hsl(244,23%,54%)", + info: "hsl(206,100%,56.7%)", + success: "hsl(150,99%,37.4%)", + notice: "hsl(48,80%,50.4%)", + warning: "hsl(28,100%,54.9%)", + danger: "hsl(342,90%,61.2%)", + }, + dialog: { + border: "hsl(244,23%,24%)", + }, + sidebar: { + surface: "hsl(243,23%,16%)", + border: "hsl(244,23%,22%)", + }, + responsePane: { + surface: "hsl(243,23%,16%)", + border: "hsl(246,23%,23%)", + }, + appHeader: { + surface: "hsl(244,23%,12%)", + border: "hsl(244,23%,21%)", + }, + }, +}; + +export const defaultLightTheme: Theme = { + id: "yaak-light", + label: "Yaak", + dark: false, + base: { + surface: "hsl(0,0%,100%)", + surfaceHighlight: "hsl(218,24%,87%)", + text: "hsl(217,24%,10%)", + textSubtle: "hsl(217,24%,40%)", + textSubtlest: "hsl(217,24%,58%)", + border: "hsl(217,22%,90%)", + primary: "hsl(266,100%,60%)", + secondary: "hsl(220,24%,50%)", + info: "hsl(206,100%,40%)", + success: "hsl(139,66%,34%)", + notice: "hsl(45,100%,34%)", + warning: "hsl(30,100%,36%)", + danger: "hsl(335,75%,48%)", + }, + components: { + sidebar: { + surface: "hsl(220,20%,98%)", + border: "hsl(217,22%,88%)", + surfaceHighlight: "hsl(217,25%,90%)", + }, + }, +}; diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts new file mode 100644 index 00000000..25455314 --- /dev/null +++ b/packages/theme/src/index.ts @@ -0,0 +1,26 @@ +export type { Appearance } from "./appearance"; +export { + getCSSAppearance, + getWindowAppearance, + resolveAppearance, + subscribeToPreferredAppearance, + subscribeToWindowAppearanceChange, +} from "./appearance"; +export { defaultDarkTheme, defaultLightTheme } from "./defaultThemes"; +export { YaakColor } from "./yaakColor"; +export type { + DocumentPlatform, + YaakColorKey, + YaakColors, + YaakTheme, +} from "./window"; +export { + addThemeStylesToDocument, + applyThemeToDocument, + completeTheme, + getThemeCSS, + indent, + platformFromUserAgent, + setThemeOnDocument, + setPlatformOnDocument, +} from "./window"; diff --git a/packages/theme/src/window.ts b/packages/theme/src/window.ts new file mode 100644 index 00000000..9d429f53 --- /dev/null +++ b/packages/theme/src/window.ts @@ -0,0 +1,430 @@ +import type { Theme, ThemeComponentColors } from "@yaakapp-internal/plugins"; +import { defaultDarkTheme, defaultLightTheme } from "./defaultThemes"; +import { YaakColor } from "./yaakColor"; + +export type YaakColors = { + surface: YaakColor; + surfaceHighlight?: YaakColor; + surfaceActive?: YaakColor; + text: YaakColor; + textSubtle?: YaakColor; + textSubtlest?: YaakColor; + border?: YaakColor; + borderSubtle?: YaakColor; + borderFocus?: YaakColor; + shadow?: YaakColor; + backdrop?: YaakColor; + selection?: YaakColor; + primary?: YaakColor; + secondary?: YaakColor; + info?: YaakColor; + success?: YaakColor; + notice?: YaakColor; + warning?: YaakColor; + danger?: YaakColor; +}; + +export type YaakTheme = { + id: string; + name: string; + base: YaakColors; + components?: Partial<{ + dialog: Partial; + menu: Partial; + toast: Partial; + sidebar: Partial; + responsePane: Partial; + appHeader: Partial; + button: Partial; + banner: Partial; + templateTag: Partial; + urlBar: Partial; + editor: Partial; + input: Partial; + }>; +}; + +export type YaakColorKey = keyof ThemeComponentColors; +export type DocumentPlatform = "linux" | "macos" | "windows" | "unknown"; + +type ComponentName = keyof NonNullable; +type CSSVariables = Record; + +function themeVariables( + theme: Theme, + component?: ComponentName, + base?: CSSVariables, +): CSSVariables | null { + const cmp = + component == null + ? theme.base + : (theme.components?.[component] ?? ({} as ThemeComponentColors)); + const color = (value: string | undefined) => yc(theme, value); + const vars: CSSVariables = { + surface: cmp.surface, + surfaceHighlight: + cmp.surfaceHighlight ?? color(cmp.surface)?.lift(0.06).css(), + surfaceActive: + cmp.surfaceActive ?? + color(cmp.primary)?.lower(0.2).translucify(0.8).css(), + backdrop: + cmp.backdrop ?? color(cmp.surface)?.lower(0.2).translucify(0.2).css(), + selection: + cmp.selection ?? color(cmp.primary)?.lower(0.1).translucify(0.7).css(), + border: cmp.border ?? color(cmp.surface)?.lift(0.11)?.css(), + borderSubtle: cmp.borderSubtle ?? color(cmp.border)?.lower(0.06)?.css(), + borderFocus: color(cmp.info)?.translucify(0.5)?.css(), + text: cmp.text, + textSubtle: cmp.textSubtle ?? color(cmp.text)?.lower(0.2)?.css(), + textSubtlest: cmp.textSubtlest ?? color(cmp.text)?.lower(0.3)?.css(), + shadow: + cmp.shadow ?? + YaakColor.black() + .translucify(theme.dark ? 0.7 : 0.93) + .css(), + primary: cmp.primary, + secondary: cmp.secondary, + info: cmp.info, + success: cmp.success, + notice: cmp.notice, + warning: cmp.warning, + danger: cmp.danger, + }; + + for (const [key, value] of Object.entries(vars)) { + if (!value && base?.[key as YaakColorKey]) { + vars[key as YaakColorKey] = base[key as YaakColorKey]; + } + } + + return vars; +} + +function templateTagColorVariables( + color: YaakColor | null, +): Partial { + if (color == null) return {}; + + return { + text: color.lift(0.7).css(), + textSubtle: color.lift(0.4).css(), + textSubtlest: color.css(), + surface: color.lower(0.2).translucify(0.8).css(), + border: color.translucify(0.6).css(), + borderSubtle: color.translucify(0.8).css(), + surfaceHighlight: color.lower(0.1).translucify(0.7).css(), + }; +} + +function toastColorVariables(color: YaakColor | null): Partial { + if (color == null) return {}; + + return { + text: color.lift(0.8).css(), + textSubtle: color.lift(0.8).translucify(0.3).css(), + surface: color.translucify(0.9).css(), + surfaceHighlight: color.translucify(0.8).css(), + border: color.lift(0.3).translucify(0.6).css(), + }; +} + +function bannerColorVariables(color: YaakColor | null): Partial { + if (color == null) return {}; + + return { + text: color.lift(0.8).css(), + textSubtle: color.translucify(0.3).css(), + textSubtlest: color.translucify(0.6).css(), + surface: color.translucify(0.95).css(), + border: color.lift(0.3).translucify(0.8).css(), + }; +} + +function buttonSolidColorVariables( + color: YaakColor | null, + isDefault = false, +): Partial { + if (color == null) return {}; + + const theme: Partial = { + text: "white", + surface: color.lower(0.3).css(), + surfaceHighlight: color.lower(0.1).css(), + border: color.css(), + }; + + if (isDefault) { + theme.text = undefined; + theme.surface = undefined; + theme.surfaceHighlight = color.lift(0.08).css(); + } + + return theme; +} + +function buttonBorderColorVariables( + color: YaakColor | null, + isDefault = false, +): Partial { + if (color == null) return {}; + + const vars: Partial = { + text: color.lift(0.8).css(), + textSubtle: color.lift(0.55).css(), + textSubtlest: color.lift(0.4).translucify(0.6).css(), + surfaceHighlight: color.translucify(0.8).css(), + borderSubtle: color.translucify(0.5).css(), + border: color.translucify(0.3).css(), + }; + + if (isDefault) { + vars.borderSubtle = color.lift(0.28).css(); + vars.border = color.lift(0.5).css(); + } + + return vars; +} + +function variablesToCSS( + selector: string | null, + vars: Partial | null, +): string | null { + if (vars == null) return null; + + const css = Object.entries(vars) + .filter(([, value]) => value) + .map(([name, value]) => `--${name}: ${value};`) + .join("\n"); + + return selector == null ? css : `${selector} {\n${indent(css)}\n}`; +} + +function componentCSS(theme: Theme, component: ComponentName): string | null { + if (theme.components == null) return null; + return variablesToCSS( + `.x-theme-${component}`, + themeVariables(theme, component), + ); +} + +function buttonCSS( + theme: Theme, + colorKey: YaakColorKey, + colors?: ThemeComponentColors, +): string | null { + const color = yc(theme, colors?.[colorKey]); + if (color == null) return null; + + return [ + variablesToCSS( + `.x-theme-button--solid--${colorKey}`, + buttonSolidColorVariables(color), + ), + variablesToCSS( + `.x-theme-button--border--${colorKey}`, + buttonBorderColorVariables(color), + ), + ].join("\n\n"); +} + +function bannerCSS( + theme: Theme, + colorKey: YaakColorKey, + colors?: ThemeComponentColors, +): string | null { + const color = yc(theme, colors?.[colorKey]); + if (color == null) return null; + + return variablesToCSS( + `.x-theme-banner--${colorKey}`, + bannerColorVariables(color), + ); +} + +function toastCSS( + theme: Theme, + colorKey: YaakColorKey, + colors?: ThemeComponentColors, +): string | null { + const color = yc(theme, colors?.[colorKey]); + if (color == null) return null; + + return variablesToCSS( + `.x-theme-toast--${colorKey}`, + toastColorVariables(color), + ); +} + +function templateTagCSS( + theme: Theme, + colorKey: YaakColorKey, + colors?: ThemeComponentColors, +): string | null { + const color = yc(theme, colors?.[colorKey]); + if (color == null) return null; + + return variablesToCSS( + `.x-theme-templateTag--${colorKey}`, + templateTagColorVariables(color), + ); +} + +export function getThemeCSS(theme: Theme): string { + theme.components = theme.components ?? {}; + theme.components.toast = + theme.components.toast ?? theme.components.menu ?? {}; + const { components, id, label } = theme; + const colors = Object.keys(theme.base).reduce((prev, key) => { + return { ...prev, [key]: theme.base[key as YaakColorKey] }; + }, {} as ThemeComponentColors); + + let themeCSS = ""; + try { + const baseCss = variablesToCSS(null, themeVariables(theme)); + themeCSS = [ + baseCss, + ...Object.keys(components).map((key) => + componentCSS(theme, key as ComponentName), + ), + variablesToCSS( + ".x-theme-button--solid--default", + buttonSolidColorVariables(yc(theme, theme.base.surface), true), + ), + variablesToCSS( + ".x-theme-button--border--default", + buttonBorderColorVariables(yc(theme, theme.base.surface), true), + ), + ...Object.keys(colors).map((key) => + buttonCSS( + theme, + key as YaakColorKey, + theme.components?.button ?? colors, + ), + ), + ...Object.keys(colors).map((key) => + bannerCSS( + theme, + key as YaakColorKey, + theme.components?.banner ?? colors, + ), + ), + ...Object.keys(colors).map((key) => + toastCSS( + theme, + key as YaakColorKey, + theme.components?.banner ?? colors, + ), + ), + ...Object.keys(colors).map((key) => + templateTagCSS( + theme, + key as YaakColorKey, + theme.components?.templateTag ?? colors, + ), + ), + ].join("\n\n"); + } catch (err) { + console.error("Failed to generate CSS", err); + } + + return [ + `/* ${label} */`, + `[data-theme="${id}"] {`, + indent(themeCSS), + "}", + ].join("\n"); +} + +export function addThemeStylesToDocument(rawTheme: Theme | null) { + if (rawTheme == null) { + console.error("Failed to add theme styles: theme is null"); + return; + } + + const theme = completeTheme(rawTheme); + let styleEl = document.head.querySelector("style[data-theme]"); + if (!styleEl) { + styleEl = document.createElement("style"); + document.head.appendChild(styleEl); + } + + styleEl.setAttribute("data-theme", theme.id); + styleEl.setAttribute("data-updated-at", new Date().toISOString()); + styleEl.textContent = getThemeCSS(theme); +} + +export function setThemeOnDocument(theme: Theme | null) { + if (theme == null) { + console.error("Failed to set theme: theme is null"); + return; + } + + document.documentElement.setAttribute("data-theme", theme.id); +} + +export function applyThemeToDocument(theme: Theme | null) { + addThemeStylesToDocument(theme); + setThemeOnDocument(theme); +} + +export function platformFromUserAgent(userAgent: string): DocumentPlatform { + const normalized = userAgent.toLowerCase(); + + if (normalized.includes("linux")) return "linux"; + if (normalized.includes("mac os") || normalized.includes("macintosh")) + return "macos"; + if (normalized.includes("win")) return "windows"; + return "unknown"; +} + +export function setPlatformOnDocument(platform: string | null | undefined) { + const normalized = + platform === "linux" || platform === "macos" || platform === "windows" + ? platform + : "unknown"; + document.documentElement.setAttribute("data-platform", normalized); +} + +export function indent(text: string, space = " "): string { + return text + .split("\n") + .map((line) => space + line) + .join("\n"); +} + +function yc( + theme: Theme, + value: T, +): T extends string ? YaakColor : null { + if (value == null) return null as never; + return new YaakColor(value, theme.dark ? "dark" : "light") as never; +} + +export function completeTheme(theme: Theme): Theme { + const fallback = theme.dark ? defaultDarkTheme.base : defaultLightTheme.base; + const color = (value: string | null | undefined) => yc(theme, value); + + theme.base.primary ??= fallback.primary; + theme.base.secondary ??= fallback.secondary; + theme.base.info ??= fallback.info; + theme.base.success ??= fallback.success; + theme.base.notice ??= fallback.notice; + theme.base.warning ??= fallback.warning; + theme.base.danger ??= fallback.danger; + + theme.base.surface ??= fallback.surface; + theme.base.surfaceHighlight ??= color(theme.base.surface)?.lift(0.06)?.css(); + theme.base.surfaceActive ??= color(theme.base.primary) + ?.lower(0.2) + .translucify(0.8) + .css(); + + theme.base.border ??= color(theme.base.surface)?.lift(0.12)?.css(); + theme.base.borderSubtle ??= color(theme.base.border)?.lower(0.08)?.css(); + + theme.base.text ??= fallback.text; + theme.base.textSubtle ??= color(theme.base.text)?.lower(0.3)?.css(); + theme.base.textSubtlest ??= color(theme.base.text)?.lower(0.5)?.css(); + + return theme; +} diff --git a/packages/theme/src/yaakColor.ts b/packages/theme/src/yaakColor.ts new file mode 100644 index 00000000..5b3731b4 --- /dev/null +++ b/packages/theme/src/yaakColor.ts @@ -0,0 +1,153 @@ +import parseColor from "parse-color"; + +export class YaakColor { + private readonly appearance: "dark" | "light" = "light"; + + private hue = 0; + private saturation = 0; + private lightness = 0; + private alpha = 1; + + constructor(cssColor: string, appearance: "dark" | "light" = "light") { + try { + this.set(cssColor); + this.appearance = appearance; + } catch (err) { + console.log("Failed to parse CSS color", cssColor, err); + } + } + + static transparent(): YaakColor { + return new YaakColor("rgb(0,0,0)", "light").translucify(1); + } + + static white(): YaakColor { + return new YaakColor("rgb(0,0,0)", "light").lower(1); + } + + static black(): YaakColor { + return new YaakColor("rgb(0,0,0)", "light").lift(1); + } + + set(cssColor: string): YaakColor { + let fixedCssColor = cssColor; + if (cssColor.startsWith("#") && cssColor.length === 9) { + const [r, g, b, a] = hexToRgba(cssColor); + fixedCssColor = `rgba(${r},${g},${b},${a})`; + } + const { hsla } = parseColor(fixedCssColor); + this.hue = hsla[0]; + this.saturation = hsla[1]; + this.lightness = hsla[2]; + this.alpha = hsla[3] ?? 1; + return this; + } + + clone(): YaakColor { + return new YaakColor(this.css(), this.appearance); + } + + lower(mod: number): YaakColor { + return this.appearance === "dark" ? this._darken(mod) : this._lighten(mod); + } + + lift(mod: number): YaakColor { + return this.appearance === "dark" ? this._lighten(mod) : this._darken(mod); + } + + minLightness(n: number): YaakColor { + const color = this.clone(); + if (color.lightness < n) { + color.lightness = n; + } + return color; + } + + isDark(): boolean { + return this.lightness < 50; + } + + translucify(mod: number): YaakColor { + const color = this.clone(); + color.alpha = color.alpha - color.alpha * mod; + return color; + } + + opacify(mod: number): YaakColor { + const color = this.clone(); + color.alpha = this.alpha + (100 - this.alpha) * mod; + return color; + } + + desaturate(mod: number): YaakColor { + const color = this.clone(); + color.saturation = color.saturation - color.saturation * mod; + return color; + } + + saturate(mod: number): YaakColor { + const color = this.clone(); + color.saturation = this.saturation + (100 - this.saturation) * mod; + return color; + } + + lighterThan(color: YaakColor): boolean { + return this.lightness > color.lightness; + } + + css(): string { + const [r, g, b] = parseColor( + `hsl(${this.hue},${this.saturation}%,${this.lightness}%)`, + ).rgb; + return rgbaToHex(r, g, b, this.alpha); + } + + hexNoAlpha(): string { + const [r, g, b] = parseColor( + `hsl(${this.hue},${this.saturation}%,${this.lightness}%)`, + ).rgb; + return rgbaToHexNoAlpha(r, g, b); + } + + private _lighten(mod: number): YaakColor { + const color = this.clone(); + color.lightness = this.lightness + (100 - this.lightness) * mod; + return color; + } + + private _darken(mod: number): YaakColor { + const color = this.clone(); + color.lightness = this.lightness - this.lightness * mod; + return color; + } +} + +function rgbaToHex(r: number, g: number, b: number, a: number): string { + const toHex = (n: number): string => { + const hex = Number(Math.round(n)).toString(16); + return hex.length === 1 ? `0${hex}` : hex; + }; + return `#${[toHex(r), toHex(g), toHex(b), toHex(a * 255)].join("").toUpperCase()}`; +} + +function rgbaToHexNoAlpha(r: number, g: number, b: number): string { + const toHex = (n: number): string => { + const hex = Number(Math.round(n)).toString(16); + return hex.length === 1 ? `0${hex}` : hex; + }; + return `#${[toHex(r), toHex(g), toHex(b)].join("").toUpperCase()}`; +} + +function hexToRgba(hex: string): [number, number, number, number] { + const fromHex = (value: string): number => { + if (value === "") return 255; + return Number(`0x${value}`); + }; + + const r = fromHex(hex.slice(1, 3)); + const g = fromHex(hex.slice(3, 5)); + const b = fromHex(hex.slice(5, 7)); + const a = fromHex(hex.slice(7, 9)); + + return [r, g, b, a / 255]; +} diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json new file mode 100644 index 00000000..77592bbb --- /dev/null +++ b/packages/theme/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "strict": true, + "module": "ESNext", + "moduleResolution": "Node", + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["src"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 00000000..b97529d0 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,16 @@ +{ + "name": "@yaakapp-internal/ui", + "private": true, + "version": "1.0.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "peerDependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0" + } +} diff --git a/packages/ui/src/components/Button.tsx b/packages/ui/src/components/Button.tsx new file mode 100644 index 00000000..05a36d2f --- /dev/null +++ b/packages/ui/src/components/Button.tsx @@ -0,0 +1,123 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +type ButtonColor = + | "default" + | "custom" + | "primary" + | "secondary" + | "info" + | "success" + | "notice" + | "warning" + | "danger"; + +type ButtonVariant = "border" | "solid"; +type ButtonSize = "2xs" | "xs" | "sm" | "md" | "auto"; + +export type ButtonProps = Omit< + HTMLAttributes, + "color" | "onChange" +> & { + innerClassName?: string; + color?: ButtonColor; + tone?: Exclude; + variant?: ButtonVariant; + isLoading?: boolean; + size?: ButtonSize; + justify?: "start" | "center"; + type?: "button" | "submit"; + disabled?: boolean; + title?: string; + leftSlot?: ReactNode; + rightSlot?: ReactNode; +}; + +function cx(...values: Array) { + return values.filter(Boolean).join(" "); +} + +export function Button({ + isLoading, + className, + innerClassName, + children, + color, + tone, + type = "button", + justify = "center", + size = "md", + variant = "solid", + leftSlot, + rightSlot, + disabled, + title, + onClick, + ...props +}: ButtonProps) { + const resolvedColor = color ?? tone ?? "default"; + const isDisabled = disabled || isLoading; + + return ( + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 00000000..5925536a --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,2 @@ +export { Button } from "./components/Button"; +export type { ButtonProps } from "./components/Button"; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 00000000..77592bbb --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2021", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "strict": true, + "module": "ESNext", + "moduleResolution": "Node", + "jsx": "react-jsx", + "noEmit": true + }, + "include": ["src"] +}